From e4060c6090a8b14bafe73c14b3e456734dff5a8b Mon Sep 17 00:00:00 2001 From: Connor Newton Date: Thu, 18 Oct 2018 13:51:58 +0100 Subject: [PATCH] Implement Resolver iterator Implement an iterator that takes a slice of nodes and an associated QuadStore and resolves to their respective values during iteration. Resolves #663 --- graph/iterator.go | 1 + graph/iterator/resolver.go | 164 ++++++++++++++++++++++++++++++++ graph/iterator/resolver_test.go | 130 +++++++++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 graph/iterator/resolver.go create mode 100644 graph/iterator/resolver_test.go diff --git a/graph/iterator.go b/graph/iterator.go index 3fec13ad2..f11bf7e6e 100644 --- a/graph/iterator.go +++ b/graph/iterator.go @@ -272,6 +272,7 @@ const ( Regex = Type("regexp") Count = Type("count") Recursive = Type("recursive") + Resolver = Type("resolver") ) // String returns a string representation of the Type. diff --git a/graph/iterator/resolver.go b/graph/iterator/resolver.go new file mode 100644 index 000000000..7b4037f5f --- /dev/null +++ b/graph/iterator/resolver.go @@ -0,0 +1,164 @@ +// Copyright 2018 The Cayley Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iterator + +import ( + "context" + "fmt" + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/quad" +) + +var _ graph.Iterator = &Resolver{} + +// A Resolver iterator consists of it's nodes, an index (where it is in the, +// process of iterating) and a store to resolve values from. +type Resolver struct { + qs graph.QuadStore + uid uint64 + tags graph.Tagger + nodes []quad.Value + index int + err error + result graph.Value +} + +// Creates a new Resolver iterator with a custom comparator. +func NewResolver(qs graph.QuadStore, nodes ...quad.Value) *Resolver { + it := &Resolver{ + uid: NextUID(), + qs: qs, + nodes: make([]quad.Value, 0, 20), + } + // Enforce uniqueness + unique := make(map[quad.Value]bool, 20) + for _, node := range nodes { + if _, ok := unique[node]; !ok { + unique[node] = true + it.nodes = append(it.nodes, node) + } + } + return it +} + +func (it *Resolver) UID() uint64 { + return it.uid +} + +func (it *Resolver) Reset() { + it.index = 0 + it.err = nil + it.result = nil +} + +func (it *Resolver) Close() error { + return nil +} + +func (it *Resolver) Tagger() *graph.Tagger { + return &it.tags +} + +func (it *Resolver) TagResults(dst map[string]graph.Value) { + it.tags.TagResult(dst, it.Result()) +} + +func (it *Resolver) Clone() graph.Iterator { + out := NewResolver(it.qs, it.nodes...) + out.tags.CopyFrom(it) + return out +} + +func (it *Resolver) String() string { + return fmt.Sprintf("Resolver(%v)", it.nodes) +} + +// Register this iterator as a Resolver iterator. +func (it *Resolver) Type() graph.Type { return graph.Fixed } + +// Check if the passed value is equal to one of the nodes stored in the iterator. +func (it *Resolver) Contains(ctx context.Context, value graph.Value) bool { + graph.ContainsLogIn(it, value) + index := 0 + for index < len(it.nodes) { + node := it.nodes[index] + if it.qs.ValueOf(node) == value { + return graph.ContainsLogOut(it, value, true) + } + index++ + } + return graph.ContainsLogOut(it, value, false) +} + +// Next advances the iterator. +func (it *Resolver) Next(ctx context.Context) bool { + graph.NextLogIn(it) + if it.index == len(it.nodes) { + it.result = nil + return graph.NextLogOut(it, false) + } + node := it.nodes[it.index] + value := it.qs.ValueOf(node) + if value == nil { + it.result = nil + it.err = fmt.Errorf("not found: %v", node) + return false + } + it.result = value + it.index++ + return graph.NextLogOut(it, true) +} + +func (it *Resolver) Err() error { + return it.err +} + +func (it *Resolver) Result() graph.Value { + return it.result +} + +func (it *Resolver) NextPath(ctx context.Context) bool { + return false +} + +func (it *Resolver) SubIterators() []graph.Iterator { + return nil +} + +// Returns a Null iterator if it's empty so that upstream iterators can optimize it +// away, otherwise there is no optimization. +func (it *Resolver) Optimize() (graph.Iterator, bool) { + if len(it.nodes) == 0 { + return NewNull(), true + } + return it, false +} + +// Size is the number of m stored. +func (it *Resolver) Size() (int64, bool) { + return int64(len(it.nodes)), true +} + +func (it *Resolver) Stats() graph.IteratorStats { + s, exact := it.Size() + return graph.IteratorStats{ + // Lookup cost is size of set + ContainsCost: s - int64(it.index), + // Next is (presumably) O(1) from store + NextCost: 1, + Size: s, + ExactSize: exact, + } +} diff --git a/graph/iterator/resolver_test.go b/graph/iterator/resolver_test.go new file mode 100644 index 000000000..c6ebc1107 --- /dev/null +++ b/graph/iterator/resolver_test.go @@ -0,0 +1,130 @@ +package iterator_test + +import ( + "testing" + "github.com/cayleygraph/cayley" + "github.com/cayleygraph/cayley/quad" + "context" + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/iterator" +) + +func TestResolverIteratorIterate(t *testing.T) { + var ctx context.Context + qs, err := cayley.NewMemoryGraph() + if err != nil { + t.Fatalf("error creating graph: %v", err) + } + nodes := []quad.Value{ + quad.String("1"), + quad.String("2"), + quad.String("3"), + quad.String("4"), + quad.String("5"), + } + expected := make(map[quad.Value]graph.Value) + for _, node := range nodes { + qs.AddQuad(quad.Make(quad.String("0"), nil, node, nil)) + expected[node] = qs.ValueOf(node) + } + it := iterator.NewResolver(qs, nodes...) + for _, node := range nodes { + if it.Next(ctx) != true { + t.Fatal("unexpected end of iterator") + } + if err := it.Err(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if value := it.Result(); value != expected[node] { + t.Fatalf("unexpected quad value: expected %v, got %v", expected[node], value) + } + } + if it.Next(ctx) != false { + t.Fatal("expected end of iterator") + } + if it.Result() != nil { + t.Fatal("expected nil result") + } +} + +func TestResolverIteratorNotFoundError(t *testing.T) { + var ctx context.Context + qs, err := cayley.NewMemoryGraph() + if err != nil { + t.Fatalf("error creating graph: %v", err) + } + nodes := []quad.Value{ + quad.String("1"), + quad.String("2"), + quad.String("3"), + quad.String("4"), + quad.String("5"), + } + skip := 3 + for i, node := range nodes { + // Simulate a missing subject + if i == skip { + continue + } + qs.AddQuad(quad.Make(quad.String("0"), nil, node, nil)) + } + count := 0 + it := iterator.NewResolver(qs, nodes...) + for it.Next(ctx) { + count++ + } + if count != skip { + t.Fatal("unexpected end of iterator") + } + if it.Err() == nil { + t.Fatal("unexpected not found error") + } + if it.Result() != nil { + t.Fatal("expected nil result") + } +} + +func TestResolverIteratorContains(t *testing.T) { + tests := []struct { + name string + nodes []quad.Value + subject quad.Value + contains bool + }{ + { + "contains", + []quad.Value{ + quad.String("1"), + quad.String("2"), + quad.String("3"), + }, + quad.String("2"), + true, + }, + { + "not contains", + []quad.Value{ + quad.String("1"), + quad.String("3"), + }, + quad.String("2"), + false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var ctx context.Context + qs, err := cayley.NewMemoryGraph() + if err != nil { + t.Fatalf("error creating graph: %v", err) + } + for _, node := range test.nodes { + qs.AddQuad(quad.Make(quad.String("0"), nil, node, nil)) + } + it := iterator.NewResolver(qs, test.nodes...) + if it.Contains(ctx, qs.ValueOf(test.subject)) != test.contains { + t.Fatal("unexpected result") + } + }) + } +}