github.com/cayleygraph/cayley@v0.7.7/graph/shape/shape_test.go (about) 1 // Copyright 2014 The Cayley Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package shape_test 16 17 import ( 18 "context" 19 "reflect" 20 "testing" 21 22 "github.com/cayleygraph/cayley/graph" 23 "github.com/cayleygraph/cayley/graph/graphmock" 24 . "github.com/cayleygraph/cayley/graph/shape" 25 "github.com/cayleygraph/quad" 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 ) 29 30 func intVal(v int) graph.Ref { 31 return graphmock.IntVal(v) 32 } 33 34 var _ Optimizer = ValLookup(nil) 35 var _ graph.QuadStore = ValLookup(nil) 36 37 type ValLookup map[quad.Value]graph.Ref 38 39 func (qs ValLookup) OptimizeShape(s Shape) (Shape, bool) { 40 return s, false // emulate dumb quad store 41 } 42 func (qs ValLookup) ValueOf(v quad.Value) graph.Ref { 43 return qs[v] 44 } 45 46 func (ValLookup) NewQuadWriter() (quad.WriteCloser, error) { 47 panic("not implemented") 48 } 49 func (ValLookup) ApplyDeltas(_ []graph.Delta, _ graph.IgnoreOpts) error { 50 panic("not implemented") 51 } 52 func (ValLookup) Quad(_ graph.Ref) quad.Quad { 53 panic("not implemented") 54 } 55 func (ValLookup) QuadIterator(_ quad.Direction, _ graph.Ref) graph.Iterator { 56 panic("not implemented") 57 } 58 func (ValLookup) QuadIteratorSize(ctx context.Context, d quad.Direction, val graph.Ref) (graph.Size, error) { 59 panic("not implemented") 60 } 61 func (ValLookup) NodesAllIterator() graph.Iterator { 62 panic("not implemented") 63 } 64 func (ValLookup) QuadsAllIterator() graph.Iterator { 65 panic("not implemented") 66 } 67 func (ValLookup) NameOf(_ graph.Ref) quad.Value { 68 panic("not implemented") 69 } 70 func (ValLookup) Stats(ctx context.Context, exact bool) (graph.Stats, error) { 71 panic("not implemented") 72 } 73 func (ValLookup) Close() error { 74 panic("not implemented") 75 } 76 func (ValLookup) QuadDirection(_ graph.Ref, _ quad.Direction) graph.Ref { 77 panic("not implemented") 78 } 79 func (ValLookup) Type() string { 80 panic("not implemented") 81 } 82 83 func emptySet() Shape { 84 return NodesFrom{ 85 Dir: quad.Predicate, 86 Quads: Intersect{Quads{ 87 {Dir: quad.Object, 88 Values: Lookup{quad.IRI("not-existent")}, 89 }, 90 }}, 91 } 92 } 93 94 var optimizeCases = []struct { 95 name string 96 from Shape 97 expect Shape 98 opt bool 99 qs ValLookup 100 }{ 101 { 102 name: "all", 103 from: AllNodes{}, 104 opt: false, 105 expect: AllNodes{}, 106 }, 107 { 108 name: "page min limit", 109 from: Page{ 110 Limit: 5, 111 From: Page{ 112 Limit: 3, 113 From: AllNodes{}, 114 }, 115 }, 116 opt: true, 117 expect: Page{ 118 Limit: 3, 119 From: AllNodes{}, 120 }, 121 }, 122 { 123 name: "page skip and limit", 124 from: Page{ 125 Skip: 3, Limit: 3, 126 From: Page{ 127 Skip: 2, Limit: 5, 128 From: AllNodes{}, 129 }, 130 }, 131 opt: true, 132 expect: Page{ 133 Skip: 5, Limit: 2, 134 From: AllNodes{}, 135 }, 136 }, 137 { 138 name: "intersect tagged all", 139 from: Intersect{Save{Tags: []string{"id"}, From: AllNodes{}}}, 140 opt: true, 141 expect: Save{Tags: []string{"id"}, From: AllNodes{}}, 142 }, 143 { 144 name: "intersect quads and lookup resolution", 145 from: Intersect{ 146 Quads{ 147 {Dir: quad.Subject, Values: Lookup{quad.IRI("bob")}}, 148 }, 149 Quads{ 150 {Dir: quad.Object, Values: Lookup{quad.IRI("alice")}}, 151 }, 152 }, 153 opt: true, 154 expect: Quads{ 155 {Dir: quad.Subject, Values: Fixed{intVal(1)}}, 156 {Dir: quad.Object, Values: Fixed{intVal(2)}}, 157 }, 158 qs: ValLookup{ 159 quad.IRI("bob"): intVal(1), 160 quad.IRI("alice"): intVal(2), 161 }, 162 }, 163 { 164 name: "intersect nodes, remove all, join intersects", 165 from: Intersect{ 166 AllNodes{}, 167 NodesFrom{Dir: quad.Subject, Quads: Quads{}}, 168 Intersect{ 169 Lookup{quad.IRI("alice")}, 170 Unique{NodesFrom{Dir: quad.Object, Quads: Quads{}}}, 171 }, 172 }, 173 opt: true, 174 expect: Intersect{ 175 Fixed{intVal(1)}, 176 QuadsAction{Result: quad.Subject}, 177 Unique{QuadsAction{Result: quad.Object}}, 178 }, 179 qs: ValLookup{ 180 quad.IRI("alice"): intVal(1), 181 }, 182 }, 183 { 184 name: "push Save out of intersect", 185 from: Intersect{ 186 Save{ 187 Tags: []string{"id"}, 188 From: NodesFrom{Dir: quad.Subject, Quads: Quads{}}, 189 }, 190 Unique{NodesFrom{Dir: quad.Object, Quads: Quads{}}}, 191 }, 192 opt: true, 193 expect: Save{ 194 Tags: []string{"id"}, 195 From: Intersect{ 196 QuadsAction{Result: quad.Subject}, 197 Unique{QuadsAction{Result: quad.Object}}, 198 }, 199 }, 200 }, 201 { 202 name: "collapse empty set", 203 from: Intersect{Quads{ 204 {Dir: quad.Subject, Values: Union{ 205 Unique{emptySet()}, 206 }}, 207 }}, 208 opt: true, 209 expect: Null{}, 210 }, 211 { // remove "all nodes" in intersect, merge Fixed and order them first 212 name: "remove all in intersect and reorder", 213 from: Intersect{ 214 AllNodes{}, 215 Fixed{intVal(1), intVal(2)}, 216 Save{From: AllNodes{}, Tags: []string{"all"}}, 217 Fixed{intVal(2)}, 218 }, 219 opt: true, 220 expect: Save{ 221 From: Intersect{ 222 Fixed{intVal(1), intVal(2)}, 223 Fixed{intVal(2)}, 224 }, 225 Tags: []string{"all"}, 226 }, 227 }, 228 { 229 name: "remove HasA-LinksTo pairs", 230 from: NodesFrom{ 231 Dir: quad.Subject, 232 Quads: Quads{{ 233 Dir: quad.Subject, 234 Values: Fixed{intVal(1)}, 235 }}, 236 }, 237 opt: true, 238 expect: Fixed{intVal(1)}, 239 }, 240 { // pop fixed tags to the top of the tree 241 name: "pop fixed tags", 242 from: NodesFrom{Dir: quad.Subject, Quads: Quads{ 243 QuadFilter{Dir: quad.Predicate, Values: Intersect{ 244 FixedTags{ 245 Tags: map[string]graph.Ref{"foo": intVal(1)}, 246 On: NodesFrom{Dir: quad.Subject, 247 Quads: Quads{ 248 QuadFilter{Dir: quad.Object, Values: FixedTags{ 249 Tags: map[string]graph.Ref{"bar": intVal(2)}, 250 On: Fixed{intVal(3)}, 251 }}, 252 }, 253 }, 254 }, 255 }}, 256 }}, 257 opt: true, 258 expect: FixedTags{ 259 Tags: map[string]graph.Ref{"foo": intVal(1), "bar": intVal(2)}, 260 On: NodesFrom{Dir: quad.Subject, Quads: Quads{ 261 QuadFilter{Dir: quad.Predicate, Values: QuadsAction{ 262 Result: quad.Subject, 263 Filter: map[quad.Direction]graph.Ref{quad.Object: intVal(3)}, 264 }}, 265 }}, 266 }, 267 }, 268 { // remove optional empty set from intersect 269 name: "remove optional empty set", 270 from: IntersectOpt{ 271 Sub: Intersect{ 272 AllNodes{}, 273 Save{From: AllNodes{}, Tags: []string{"all"}}, 274 Fixed{intVal(2)}, 275 }, 276 Opt: []Shape{Save{ 277 From: emptySet(), 278 Tags: []string{"name"}, 279 }}, 280 }, 281 opt: true, 282 expect: Save{ 283 From: Fixed{intVal(2)}, 284 Tags: []string{"all"}, 285 }, 286 }, 287 { // push fixed node from intersect into nodes.quads 288 name: "push fixed into nodes.quads", 289 from: Intersect{ 290 Fixed{intVal(1)}, 291 NodesFrom{ 292 Dir: quad.Subject, 293 Quads: Quads{ 294 {Dir: quad.Predicate, Values: Fixed{intVal(2)}}, 295 { 296 Dir: quad.Object, 297 Values: NodesFrom{ 298 Dir: quad.Subject, 299 Quads: Quads{ 300 {Dir: quad.Predicate, Values: Fixed{intVal(2)}}, 301 }, 302 }, 303 }, 304 }, 305 }, 306 }, 307 opt: true, 308 expect: NodesFrom{ 309 Dir: quad.Subject, 310 Quads: Quads{ 311 {Dir: quad.Subject, Values: Fixed{intVal(1)}}, 312 {Dir: quad.Predicate, Values: Fixed{intVal(2)}}, 313 { 314 Dir: quad.Object, 315 Values: QuadsAction{ 316 Result: quad.Subject, 317 Filter: map[quad.Direction]graph.Ref{ 318 quad.Predicate: intVal(2), 319 }, 320 }, 321 }, 322 }, 323 }, 324 }, 325 { 326 name: "all optional", 327 from: Intersect{IntersectOpt{ 328 Sub: Intersect{ 329 Save{Tags: []string{"id"}, From: AllNodes{}}, 330 }, 331 Opt: []Shape{ 332 NodesFrom{Dir: quad.Subject, Quads: Quads{ 333 QuadFilter{Dir: quad.Object, Values: Save{Tags: []string{"status"}, From: AllNodes{}}}, 334 QuadFilter{Dir: quad.Predicate, Values: Fixed{intVal(1)}}, 335 }}, 336 }, 337 }}, 338 opt: true, 339 expect: Save{ 340 Tags: []string{"id"}, 341 From: IntersectOpt{ 342 Sub: Intersect{AllNodes{}}, 343 Opt: []Shape{ 344 QuadsAction{Result: quad.Subject, 345 Save: map[quad.Direction][]string{quad.Object: {"status"}}, 346 Filter: map[quad.Direction]graph.Ref{quad.Predicate: intVal(1)}, 347 }, 348 }, 349 }, 350 }, 351 }, 352 } 353 354 func TestOptimize(t *testing.T) { 355 for _, c := range optimizeCases { 356 t.Run(c.name, func(t *testing.T) { 357 qs := c.qs 358 got, opt := Optimize(c.from, qs) 359 assert.Equal(t, c.expect, got) 360 assert.Equal(t, c.opt, opt) 361 }) 362 } 363 } 364 365 func TestWalk(t *testing.T) { 366 var s Shape = NodesFrom{ 367 Dir: quad.Subject, 368 Quads: Quads{ 369 {Dir: quad.Subject, Values: Fixed{intVal(1)}}, 370 {Dir: quad.Predicate, Values: Fixed{intVal(2)}}, 371 { 372 Dir: quad.Object, 373 Values: QuadsAction{ 374 Result: quad.Subject, 375 Filter: map[quad.Direction]graph.Ref{ 376 quad.Predicate: intVal(2), 377 }, 378 }, 379 }, 380 }, 381 } 382 var types []string 383 Walk(s, func(s Shape) bool { 384 types = append(types, reflect.TypeOf(s).String()) 385 return true 386 }) 387 require.Equal(t, []string{ 388 "shape.NodesFrom", 389 "shape.Quads", 390 "shape.Fixed", 391 "shape.Fixed", 392 "shape.QuadsAction", 393 }, types) 394 }