Skip to content

Commit

Permalink
Implement Resolver iterator
Browse files Browse the repository at this point in the history
Implement an iterator that takes a slice of nodes and an
associated QuadStore and resolves to their respective
values during iteration.

Resolves #663
  • Loading branch information
Connor Newton committed Oct 18, 2018
1 parent 9652755 commit 07bd42f
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Alexander Peters <info@alexanderpeters.de>
Andrew Dunham <andrew@du.nham.ca>
Barak Michener <barakmich@google.com> <barak@cayley.io> <me@barakmich.com>
Bram Leenders <bcleenders@gmail.com>
Connor Newton <connor@ifthenelse.io>
Denys Smirnov <denis.smirnov.91@gmail.com>
Derek Liang <fr.derekliang@gmail.com>
Jay Graves <jaywgraves@gmail.com>
Expand Down
1 change: 1 addition & 0 deletions graph/iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
164 changes: 164 additions & 0 deletions graph/iterator/resolver.go
Original file line number Diff line number Diff line change
@@ -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.
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,
}
}
130 changes: 130 additions & 0 deletions graph/iterator/resolver_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
})
}
}

0 comments on commit 07bd42f

Please sign in to comment.