From 36d0f48d15dd5b529bafe15de500b46ad1303cb0 Mon Sep 17 00:00:00 2001 From: Barak Michener Date: Fri, 30 Oct 2015 16:50:33 -0400 Subject: [PATCH 1/4] graph/path: Add LabelContext to the path query language. --- data/testdata.nq | 2 ++ graph/path/morphism_apply_functions.go | 56 +++++++++++++++++++++++++++------- graph/path/path.go | 14 ++++++++- graph/path/path_test.go | 15 +++++++-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/data/testdata.nq b/data/testdata.nq index 12fe713..8b9f2eb 100644 --- a/data/testdata.nq +++ b/data/testdata.nq @@ -11,3 +11,5 @@ "cool_person" . . . + "smart_person" "smart_graph" . + "smart_person" "smart_graph" . diff --git a/graph/path/morphism_apply_functions.go b/graph/path/morphism_apply_functions.go index 7444c20..185aec2 100644 --- a/graph/path/morphism_apply_functions.go +++ b/graph/path/morphism_apply_functions.go @@ -25,11 +25,14 @@ import ( // join puts two iterators together by intersecting their result sets with an AND // Since we're using an and iterator, it's a good idea to put the smallest result // set first so that Next() produces fewer values to check Contains(). -func join(qs graph.QuadStore, itL, itR graph.Iterator) graph.Iterator { +func join(qs graph.QuadStore, its ...graph.Iterator) graph.Iterator { and := iterator.NewAnd(qs) - and.AddSubIterator(itL) - and.AddSubIterator(itR) - + for _, it := range its { + if it == nil { + continue + } + and.AddSubIterator(it) + } return and } @@ -122,8 +125,9 @@ func outMorphism(tags []string, via ...interface{}) morphism { Reversal: func(ctx *context) (morphism, *context) { return inMorphism(tags, via...), ctx }, Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) { path := buildViaPath(qs, via...) - return inOutIterator(path, in, false, tags), ctx + return inOutIterator(path, in, false, tags, ctx), ctx }, + tags: tags, } } @@ -134,8 +138,9 @@ func inMorphism(tags []string, via ...interface{}) morphism { Reversal: func(ctx *context) (morphism, *context) { return outMorphism(tags, via...), ctx }, Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) { path := buildViaPath(qs, via...) - return inOutIterator(path, in, true, tags), ctx + return inOutIterator(path, in, true, tags, ctx), ctx }, + tags: tags, } } @@ -145,13 +150,36 @@ func bothMorphism(tags []string, via ...interface{}) morphism { Reversal: func(ctx *context) (morphism, *context) { return bothMorphism(tags, via...), ctx }, Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) { path := buildViaPath(qs, via...) - inSide := inOutIterator(path, in, true, tags) - outSide := inOutIterator(path, in.Clone(), false, tags) + inSide := inOutIterator(path, in, true, tags, ctx) + outSide := inOutIterator(path, in.Clone(), false, tags, ctx) or := iterator.NewOr() or.AddSubIterator(inSide) or.AddSubIterator(outSide) return or, ctx }, + tags: tags, + } +} + +func labelContextMorphism(via ...interface{}) morphism { + var path *Path + if len(via) == 0 { + path = nil + } else { + path = buildViaPath(nil, via...) + } + return morphism{ + Name: "label_context", + Reversal: func(ctx *context) (morphism, *context) { + out := ctx.copy() + ctx.labelSet = path + return labelContextMorphism(via...), &out + }, + Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) { + out := ctx.copy() + out.labelSet = path + return in, &out + }, } } @@ -290,7 +318,7 @@ func buildSave( return join(qs, from, save) } -func inOutIterator(viaPath *Path, from graph.Iterator, inIterator bool, tags []string) graph.Iterator { +func inOutIterator(viaPath *Path, from graph.Iterator, inIterator bool, tags []string, ctx *context) graph.Iterator { start, goal := quad.Subject, quad.Object if inIterator { start, goal = goal, start @@ -303,8 +331,14 @@ func inOutIterator(viaPath *Path, from graph.Iterator, inIterator bool, tags []s source := iterator.NewLinksTo(viaPath.qs, from, start) trail := iterator.NewLinksTo(viaPath.qs, viaIter, quad.Predicate) - - route := join(viaPath.qs, source, trail) + var label graph.Iterator + if ctx != nil { + if ctx.labelSet != nil { + labeliter := ctx.labelSet.BuildIteratorOn(viaPath.qs) + label = iterator.NewLinksTo(viaPath.qs, labeliter, quad.Label) + } + } + route := join(viaPath.qs, source, trail, label) return iterator.NewHasA(viaPath.qs, route, goal) } diff --git a/graph/path/path.go b/graph/path/path.go index 7e364ef..25ba54a 100644 --- a/graph/path/path.go +++ b/graph/path/path.go @@ -23,7 +23,6 @@ type morphism struct { Reversal func(*context) (morphism, *context) Apply applyMorphism tags []string - context context } // context allows a high-level change to the way paths are constructed. Some @@ -51,6 +50,12 @@ type context struct { labelSet *Path } +func (c context) copy() context { + return context{ + labelSet: c.labelSet, + } +} + // Path represents either a morphism (a pre-defined path stored for later use), // or a concrete path, consisting of a morphism and an underlying QuadStore. type Path struct { @@ -266,6 +271,13 @@ func (p *Path) Has(via interface{}, nodes ...string) *Path { return p } +// LabelContext restricts the following operations (such as In, Out) to only +// traverse edges that match the given set of labels. +func (p *Path) LabelContext(via ...interface{}) *Path { + p.stack = append(p.stack, labelContextMorphism(via...)) + return p +} + // Back returns to a previously tagged place in the path. Any constraints applied after the Tag will remain in effect, but traversal continues from the tagged point instead, not from the end of the chain. // // For example: diff --git a/graph/path/path_test.go b/graph/path/path_test.go index ecae42d..08b2fe6 100644 --- a/graph/path/path_test.go +++ b/graph/path/path_test.go @@ -146,7 +146,7 @@ func testSet(qs graph.QuadStore) []test { { message: "implicit All", path: StartPath(qs), - expect: []string{"alice", "bob", "charlie", "dani", "emily", "fred", "greg", "follows", "status", "cool_person", "predicates", "are"}, + expect: []string{"alice", "bob", "charlie", "dani", "emily", "fred", "greg", "follows", "status", "cool_person", "predicates", "are", "smart_graph", "smart_person"}, }, { message: "follow", @@ -178,7 +178,7 @@ func testSet(qs graph.QuadStore) []test { message: "show a simple save", path: StartPath(qs).Save("status", "somecool"), tag: "somecool", - expect: []string{"cool_person", "cool_person", "cool_person"}, + expect: []string{"cool_person", "cool_person", "cool_person", "smart_person", "smart_person"}, }, { message: "show a simple saveR", @@ -228,6 +228,17 @@ func testSet(qs graph.QuadStore) []test { path: StartPath(qs, "fred").FollowReverse(grandfollows), expect: []string{"alice", "charlie", "dani"}, }, + // Context tests + { + message: "query without label limitation", + path: StartPath(qs, "greg").Out("status"), + expect: []string{"smart_person", "cool_person"}, + }, + { + message: "query with label limitation", + path: StartPath(qs, "greg").LabelContext("smart_graph").Out("status"), + expect: []string{"smart_person"}, + }, } } From 283aca83c2f1af99caa5b861d7a09e04353750e1 Mon Sep 17 00:00:00 2001 From: Barak Michener Date: Fri, 30 Oct 2015 17:16:40 -0400 Subject: [PATCH 2/4] Expose LabelContext in gremlin --- graph/path/morphism_apply_functions.go | 9 ++++++--- graph/path/path.go | 9 ++++++++- query/gremlin/build_iterator.go | 6 ++++++ query/gremlin/gremlin_test.go | 7 +++++++ query/gremlin/traversals.go | 1 + 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/graph/path/morphism_apply_functions.go b/graph/path/morphism_apply_functions.go index 185aec2..0a9db5e 100644 --- a/graph/path/morphism_apply_functions.go +++ b/graph/path/morphism_apply_functions.go @@ -16,6 +16,7 @@ package path import ( "fmt" + "reflect" "github.com/google/cayley/graph" "github.com/google/cayley/graph/iterator" @@ -161,25 +162,27 @@ func bothMorphism(tags []string, via ...interface{}) morphism { } } -func labelContextMorphism(via ...interface{}) morphism { +func labelContextMorphism(tags []string, via ...interface{}) morphism { var path *Path if len(via) == 0 { path = nil } else { path = buildViaPath(nil, via...) + path = path.Tag(tags...) } return morphism{ Name: "label_context", Reversal: func(ctx *context) (morphism, *context) { out := ctx.copy() ctx.labelSet = path - return labelContextMorphism(via...), &out + return labelContextMorphism(tags, via...), &out }, Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) { out := ctx.copy() out.labelSet = path return in, &out }, + tags: tags, } } @@ -364,7 +367,7 @@ func buildViaPath(qs graph.QuadStore, via ...interface{}) *Path { case string: return StartPath(qs, p) default: - panic(fmt.Sprint("Invalid type passed to buildViaPath. ", p)) + panic(fmt.Sprintln("Invalid type passed to buildViaPath.", reflect.TypeOf(v), p)) } } var strings []string diff --git a/graph/path/path.go b/graph/path/path.go index 25ba54a..03e0df0 100644 --- a/graph/path/path.go +++ b/graph/path/path.go @@ -274,7 +274,14 @@ func (p *Path) Has(via interface{}, nodes ...string) *Path { // LabelContext restricts the following operations (such as In, Out) to only // traverse edges that match the given set of labels. func (p *Path) LabelContext(via ...interface{}) *Path { - p.stack = append(p.stack, labelContextMorphism(via...)) + p.stack = append(p.stack, labelContextMorphism(nil, via...)) + return p +} + +// LabelContextWithTags is exactly like LabelContext, except it tags the value +// of the label used in the traversal with the tags provided. +func (p *Path) LabelContextWithTags(tags []string, via ...interface{}) *Path { + p.stack = append(p.stack, labelContextMorphism(tags, via...)) return p } diff --git a/query/gremlin/build_iterator.go b/query/gremlin/build_iterator.go index 511a83e..221a0f7 100644 --- a/query/gremlin/build_iterator.go +++ b/query/gremlin/build_iterator.go @@ -163,6 +163,12 @@ func buildPathFromObject(obj *otto.Object) *path.Path { return p.InPredicates() case "OutPredicates": return p.OutPredicates() + case "LabelContext": + labels, tags, ok := getViaData(obj) + if !ok { + return nil + } + return p.LabelContextWithTags(tags, labels...) default: panic(fmt.Sprint("Unimplemented Gremlin function", gremlinType)) } diff --git a/query/gremlin/gremlin_test.go b/query/gremlin/gremlin_test.go index f7468cf..5194669 100644 --- a/query/gremlin/gremlin_test.go +++ b/query/gremlin/gremlin_test.go @@ -278,6 +278,13 @@ var testQueries = []struct { `, expect: []string{"are", "follows", "status"}, }, + { + message: "traverse using LabelContext", + query: ` + g.V("greg").LabelContext("smart_graph").Out("status").All() + `, + expect: []string{"smart_person"}, + }, } func runQueryGetTag(g []quad.Quad, query string, tag string) []string { diff --git a/query/gremlin/traversals.go b/query/gremlin/traversals.go index b5304e4..36d67cd 100644 --- a/query/gremlin/traversals.go +++ b/query/gremlin/traversals.go @@ -42,6 +42,7 @@ var traversals = []string{ "Difference", "InPredicates", "OutPredicates", + "LabelContext", } func (wk *worker) embedTraversals(env *otto.Otto, obj *otto.Object) { From 58a735f32913fc5652062e0bdf9f9cd78732da8b Mon Sep 17 00:00:00 2001 From: Barak Michener Date: Fri, 30 Oct 2015 17:42:16 -0400 Subject: [PATCH 3/4] fix gremlin test, add reversal test --- graph/path/path_test.go | 5 +++++ query/gremlin/gremlin_test.go | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/graph/path/path_test.go b/graph/path/path_test.go index 08b2fe6..eb810c6 100644 --- a/graph/path/path_test.go +++ b/graph/path/path_test.go @@ -239,6 +239,11 @@ func testSet(qs graph.QuadStore) []test { path: StartPath(qs, "greg").LabelContext("smart_graph").Out("status"), expect: []string{"smart_person"}, }, + { + message: "reverse context", + path: StartPath(qs, "greg").Tag("base").LabelContext("smart_graph").Out("status").Tag("status").Back("base"), + expect: []string{"greg"}, + }, } } diff --git a/query/gremlin/gremlin_test.go b/query/gremlin/gremlin_test.go index 5194669..1a1d1b8 100644 --- a/query/gremlin/gremlin_test.go +++ b/query/gremlin/gremlin_test.go @@ -217,7 +217,7 @@ var testQueries = []struct { g.V().Save("status", "somecool").All() `, tag: "somecool", - expect: []string{"cool_person", "cool_person", "cool_person"}, + expect: []string{"cool_person", "cool_person", "cool_person", "smart_person", "smart_person"}, }, { message: "show a simple saveR", @@ -289,8 +289,8 @@ var testQueries = []struct { func runQueryGetTag(g []quad.Quad, query string, tag string) []string { js := makeTestSession(g) - c := make(chan interface{}, 5) - js.Execute(query, c, -1) + c := make(chan interface{}, 1) + go js.Execute(query, c, -1) var results []string for res := range c { From fab678e456ca4fb17966b0c390c6adb20f1f069d Mon Sep 17 00:00:00 2001 From: Barak Michener Date: Fri, 30 Oct 2015 17:57:07 -0400 Subject: [PATCH 4/4] Add LabelContext to the docs --- docs/GremlinAPI.md | 35 ++++++++++++++++++++++++++++++++--- query/gremlin/gremlin_test.go | 7 +++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/GremlinAPI.md b/docs/GremlinAPI.md index f02d221..fcbf02d 100644 --- a/docs/GremlinAPI.md +++ b/docs/GremlinAPI.md @@ -52,11 +52,11 @@ For these examples, suppose we have the following graph: +-------+ +------+ | alice |----- ->| fred |<-- +-------+ \---->+-------+-/ +------+ \-+-------+ - ----->| #bob# | | | emily | + ----->| #bob# | | |*emily*| +---------+--/ --->+-------+ | +-------+ | charlie | / v +---------+ / +--------+ - \--- +--------+ | #greg# | + \--- +--------+ |*#greg#*| \-->| #dani# |------------>+--------+ +--------+ ``` @@ -66,7 +66,11 @@ Where every link is a "follows" relationship, and the nodes with an extra `#` in ``` dani -- status --> cool_person ``` -Perhaps these are the influencers in our community. +Perhaps these are the influencers in our community. So too are extra `*`s in the name -- these are our smart people, according to the `smart_graph` label, eg, the quad: +``` +greg status smart_person smart_graph . +``` + To load above graph into cayley and reproduce the following examples: @@ -193,6 +197,31 @@ g.V().Has("follows", "bob") g.V("charlie").Out("follows").Has("follows", "fred") ``` +####**`path.LabelContext([labelPath], [tags])`** + +Arguments: + + * `predicatePath` (Optional): One of: + * null or undefined: In future traversals, consider all edges, regardless of subgraph. + * a string: The name of the subgraph to restrict traversals to. + * a list of strings: A set of subgraphs to restrict traversals to. + * a query path object: The target of which is a set of subgraphs. + * `tags` (Optional): One of: + * null or undefined: No tags + * a string: A single tag to add the last traversed label to the output set. + * a list of strings: Multiple tags to use as keys to save the label used to the output set. + +Sets (or removes) the subgraph context to consider in the following traversals. Affects all In(), Out(), and Both() calls that follow it. The default LabelContext is null (all subgraphs) +Example: +```javascript +// Find the status of people Dani follows +g.V("dani").Out("follows").Out("status") +// Find only the statuses provided by the smart_graph +g.V("dani").Out("follows").LabelContext("smart_graph").Out("status") +// Find all people followed by people with statuses in the smart_graph. +g.V().LabelContext("smart_graph").In("status").LabelContext(null).In("follows") +``` + ### Tagging ####**`path.Tag(tag)`** diff --git a/query/gremlin/gremlin_test.go b/query/gremlin/gremlin_test.go index 1a1d1b8..44a548b 100644 --- a/query/gremlin/gremlin_test.go +++ b/query/gremlin/gremlin_test.go @@ -285,6 +285,13 @@ var testQueries = []struct { `, expect: []string{"smart_person"}, }, + { + message: "open and close a LabelContext", + query: ` + g.V().LabelContext("smart_graph").In("status").LabelContext(null).In("follows").All() + `, + expect: []string{"dani", "fred"}, + }, } func runQueryGetTag(g []quad.Quad, query string, tag string) []string {