diff --git a/graph/iterator/not_iterator.go b/graph/iterator/not_iterator.go new file mode 100644 index 0000000..db394f8 --- /dev/null +++ b/graph/iterator/not_iterator.go @@ -0,0 +1,163 @@ +package iterator + +import ( + "github.com/google/cayley/graph" +) + +// Not iterator acts like a complement for the primary iterator. +// It will return all the vertices which are not part of the primary iterator. +type Not struct { + uid uint64 + tags graph.Tagger + primaryIt graph.Iterator + allIt graph.Iterator + result graph.Value + runstats graph.IteratorStats +} + +func NewNot(primaryIt, allIt graph.Iterator) *Not { + return &Not{ + uid: NextUID(), + primaryIt: primaryIt, + allIt: allIt, + } +} + +func (it *Not) UID() uint64 { + return it.uid +} + +// Reset resets the internal iterators and the iterator itself. +func (it *Not) Reset() { + it.result = nil + it.primaryIt.Reset() + it.allIt.Reset() +} + +func (it *Not) Tagger() *graph.Tagger { + return &it.tags +} + +func (it *Not) TagResults(dst map[string]graph.Value) { + for _, tag := range it.tags.Tags() { + dst[tag] = it.Result() + } + + for tag, value := range it.tags.Fixed() { + dst[tag] = value + } + + if it.primaryIt != nil { + it.primaryIt.TagResults(dst) + } +} + +func (it *Not) Clone() graph.Iterator { + not := NewNot(it.primaryIt.Clone(), it.allIt.Clone()) + not.tags.CopyFrom(it) + return not +} + +// SubIterators returns a slice of the sub iterators. +// The first iterator is the primary iterator, for which the complement +// is generated. +func (it *Not) SubIterators() []graph.Iterator { + return []graph.Iterator{it.primaryIt, it.allIt} +} + +// DEPRECATED +func (it *Not) ResultTree() *graph.ResultTree { + tree := graph.NewResultTree(it.Result()) + tree.AddSubtree(it.primaryIt.ResultTree()) + tree.AddSubtree(it.allIt.ResultTree()) + return tree +} + +// Next advances the Not iterator. It returns whether there is another valid +// new value. It fetches the next value of the all iterator which is not +// contained by the primary iterator. +func (it *Not) Next() bool { + graph.NextLogIn(it) + it.runstats.Next += 1 + + for graph.Next(it.allIt) { + if curr := it.allIt.Result(); !it.primaryIt.Contains(curr) { + it.result = curr + it.runstats.ContainsNext += 1 + return graph.NextLogOut(it, curr, true) + } + } + return graph.NextLogOut(it, nil, false) +} + +func (it *Not) Result() graph.Value { + return it.result +} + +// Contains checks whether the passed value is part of the primary iterator's +// complement. For a valid value, it updates the Result returned by the iterator +// to the value itself. +func (it *Not) Contains(val graph.Value) bool { + graph.ContainsLogIn(it, val) + it.runstats.Contains += 1 + + if it.primaryIt.Contains(val) { + return graph.ContainsLogOut(it, val, false) + } + + it.result = val + return graph.ContainsLogOut(it, val, true) +} + +// NextPath checks whether there is another path. Not applicable, hence it will +// return false. +func (it *Not) NextPath() bool { + return false +} + +func (it *Not) Close() { + it.primaryIt.Close() + it.allIt.Close() +} + +func (it *Not) Type() graph.Type { return graph.Not } + +func (it *Not) Optimize() (graph.Iterator, bool) { + // TODO - consider wrapping the primaryIt with a MaterializeIt + optimizedPrimaryIt, optimized := it.primaryIt.Optimize() + if optimized { + it.primaryIt = optimizedPrimaryIt + } + return it, false +} + +func (it *Not) Stats() graph.IteratorStats { + primaryStats := it.primaryIt.Stats() + allStats := it.allIt.Stats() + return graph.IteratorStats{ + NextCost: allStats.NextCost + primaryStats.ContainsCost, + ContainsCost: primaryStats.ContainsCost, + Size: allStats.Size - primaryStats.Size, + Next: it.runstats.Next, + Contains: it.runstats.Contains, + ContainsNext: it.runstats.ContainsNext, + } +} + +func (it *Not) Size() (int64, bool) { + return it.Stats().Size, false +} + +func (it *Not) Describe() graph.Description { + subIts := []graph.Description{ + it.primaryIt.Describe(), + it.allIt.Describe(), + } + + return graph.Description{ + UID: it.UID(), + Type: it.Type(), + Tags: it.tags.Tags(), + Iterators: subIts, + } +} diff --git a/graph/iterator/not_iterator_test.go b/graph/iterator/not_iterator_test.go new file mode 100644 index 0000000..ac63b7c --- /dev/null +++ b/graph/iterator/not_iterator_test.go @@ -0,0 +1,44 @@ +package iterator + +import ( + "reflect" + "testing" +) + +func TestNotIteratorBasics(t *testing.T) { + allIt := NewFixed(Identity) + allIt.Add(1) + allIt.Add(2) + allIt.Add(3) + allIt.Add(4) + + toComplementIt := NewFixed(Identity) + toComplementIt.Add(2) + toComplementIt.Add(4) + + not := NewNot(toComplementIt, allIt) + + if v, _ := not.Size(); v != 2 { + t.Errorf("Unexpected iterator size: got:%d, expected: %d", v, 2) + } + + expect := []int{1, 3} + for i := 0; i < 2; i++ { + if got := iterated(not); !reflect.DeepEqual(got, expect) { + t.Errorf("Failed to iterate Not correctly on repeat %d: got:%v expected:%v", i, got, expect) + } + not.Reset() + } + + for _, v := range []int{1, 3} { + if !not.Contains(v) { + t.Errorf("Failed to correctly check %d as true", v) + } + } + + for _, v := range []int{2, 4} { + if not.Contains(v) { + t.Errorf("Failed to correctly check %d as false", v) + } + } +} diff --git a/query/gremlin/build_iterator.go b/query/gremlin/build_iterator.go index 14245fc..d95a5e8 100644 --- a/query/gremlin/build_iterator.go +++ b/query/gremlin/build_iterator.go @@ -298,6 +298,21 @@ func buildIteratorTreeHelper(obj *otto.Object, qs graph.QuadStore, base graph.It it = buildIteratorTreeHelper(arg.Object(), qs, subIt) case "in": it = buildInOutIterator(obj, qs, subIt, true) + case "except": + arg, _ := obj.Get("_gremlin_values") + firstArg, _ := arg.Object().Get("0") + if !isVertexChain(firstArg.Object()) { + return iterator.NewNull() + } + + allIt := qs.NodesAllIterator() + toComplementIt := buildIteratorTree(firstArg.Object(), qs) + notIt := iterator.NewNot(toComplementIt, allIt) + + and := iterator.NewAnd() + and.AddSubIterator(subIt) + and.AddSubIterator(notIt) + it = and } return it } diff --git a/query/gremlin/gremlin_test.go b/query/gremlin/gremlin_test.go index 4a22f24..795714e 100644 --- a/query/gremlin/gremlin_test.go +++ b/query/gremlin/gremlin_test.go @@ -120,6 +120,20 @@ var testQueries = []struct { tag: "acd", expect: []string{"D"}, }, + { + message: "use Except to filter out a single vertex", + query: ` + g.V("A", "B").Except(g.V("A")).All() + `, + expect: []string{"B"}, + }, + { + message: "use chained Except", + query: ` + g.V("A", "B", "C").Except(g.V("B")).Except(g.V("C")).All() + `, + expect: []string{"A"}, + }, // Morphism tests. { diff --git a/query/gremlin/traversals.go b/query/gremlin/traversals.go index 2a2250b..1576fc1 100644 --- a/query/gremlin/traversals.go +++ b/query/gremlin/traversals.go @@ -38,6 +38,8 @@ func (wk *worker) embedTraversals(env *otto.Otto, obj *otto.Object) { obj.Set("Has", wk.gremlinFunc("has", obj, env)) obj.Set("Save", wk.gremlinFunc("save", obj, env)) obj.Set("SaveR", wk.gremlinFunc("saver", obj, env)) + obj.Set("Except", wk.gremlinFunc("except", obj, env)) + obj.Set("Difference", wk.gremlinFunc("except", obj, env)) } func (wk *worker) gremlinFunc(kind string, prev *otto.Object, env *otto.Otto) func(otto.FunctionCall) otto.Value {