From 24253cd054f2f980ec3135dc9e81787bd9b7662e Mon Sep 17 00:00:00 2001 From: kortschak Date: Wed, 30 Jul 2014 23:03:59 +0930 Subject: [PATCH 1/8] Add basic integration benchmarks Currently only memstore is benchmarked and only with gremlin. No checking of validity of results is performed. --- cayley_test.go | 236 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 cayley_test.go diff --git a/cayley_test.go b/cayley_test.go new file mode 100644 index 0000000..28e4305 --- /dev/null +++ b/cayley_test.go @@ -0,0 +1,236 @@ +// Copyright 2014 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 main + +import ( + "sync" + "testing" + + "github.com/google/cayley/config" + "github.com/google/cayley/db" + "github.com/google/cayley/graph" + "github.com/google/cayley/query/gremlin" +) + +var benchmarkQueries = []struct { + message string + query string + tag string + expect []string +}{ + // Easy one to get us started. How quick is the most straightforward retrieval? + { + message: "name predicate", + query: ` + g.V("Humphrey Bogart").In("name").All() + `, + expect: []string{":/en/humphrey_bogart"}, + }, + + // Grunty queries. + // 2014-07-12: This one seems to return in ~20ms in memory; + // that's going to be measurably slower for every other backend. + { + message: "two large sets with no intersection", + query: ` + function getId(x) { return g.V(x).In("name") } + var actor_to_film = g.M().In("/film/performance/actor").In("/film/film/starring") + + getId("Oliver Hardy").Follow(actor_to_film).Out("name").Intersect( + getId("Mel Blanc").Follow(actor_to_film).Out("name")).All() + `, + expect: []string{}, + }, + + // 2014-07-12: This one takes about 4 whole seconds in memory. This is a behemoth. + { + message: "three huge sets with small intersection", + query: ` + function getId(x) { return g.V(x).In("name") } + var actor_to_film = g.M().In("/film/performance/actor").In("/film/film/starring") + + var a = getId("Oliver Hardy").Follow(actor_to_film).FollowR(actor_to_film) + var b = getId("Mel Blanc").Follow(actor_to_film).FollowR(actor_to_film) + var c = getId("Billy Gilbert").Follow(actor_to_film).FollowR(actor_to_film) + + seen = {} + + a.Intersect(b).Intersect(c).ForEach(function (d) { + if (!(d.id in seen)) { + seen[d.id] = true; + g.Emit(d.id) + } + }) + `, + expect: []string{":/en/billy_gilbert", ":/en/sterling_holloway"}, + }, + + // This is more of an optimization problem that will get better over time. This takes a lot + // of wrong turns on the walk down to what is ultimately the name, but top AND has it easy + // as it has a fixed ID. Exercises Check(). + { + message: "the helpless checker", + query: ` + g.V().As("person").In("name").In().In().Out("name").Is("Casablanca").All() + `, + tag: "person", + expect: []string{ + "Claude Rains", + "Conrad Veidt", + "Dooley Wilson", + "Helmut Dantine", + "Humphrey Bogart", + "Ingrid Bergman", + "John Qualen", + "Joy Page", + "Leonid Kinskey", + "Lou Marcelle", + "Madeleine LeBeau", + "Paul Henreid", + "Peter Lorre", + "Sydney Greenstreet", + "S.Z. Sakall", + }, + }, + + //Q: Who starred in both "The Net" and "Speed" ? + //A: "Sandra Bullock" + { + message: "Net and Speed", + query: common + `m1_actors.Intersect(m2_actors).Out("name").All() +`, + }, + + //Q: Did "Keanu Reeves" star in "The Net" ? + //A: No + { + message: "Keannu in The Net", + query: common + `actor2.Intersect(m1_actors).Out("name").All() +`, + }, + + //Q: Did "Keanu Reeves" star in "Speed" ? + //A: Yes + { + message: "Keannu in Speed", + query: common + `actor2.Intersect(m2_actors).Out("name").All() +`, + }, + + //Q: Has "Keanu Reeves" co-starred with anyone who starred in "The Net" ? + //A: "Keanu Reeves" was in "Speed" and "The Lake House" with "Sandra Bullock", + // who was in "The Net" + { + message: "Keannu with other in The Net", + query: common + `actor2.Follow(coStars1).Intersect(m1_actors).Out("name").All() +`, + }, + + //Q5: Do "Keanu Reeves" and "Sandra Bullock" have any commons co-stars? + //A5: Yes, many. For example: SB starred with "Steve Martin" in "The Prince + // of Egypt", and KR starred with Steven Martin in "Parenthood". + { + message: "Keannu and Bullock with other", + query: common + `actor1.Save("name","costar1_actor").Follow(coStars1).Intersect(actor2.Save("name","costar2_actor").Follow(coStars2)).Out("name").All() +`, + }, +} + +const common = ` +var movie1 = g.V().Has("name", "The Net") +var movie2 = g.V().Has("name", "Speed") +var actor1 = g.V().Has("name", "Sandra Bullock") +var actor2 = g.V().Has("name", "Keanu Reeves") + +// (film) -> starring -> (actor) +var filmToActor = g.Morphism().Out("/film/film/starring").Out("/film/performance/actor") + +// (actor) -> starring -> [film -> starring -> (actor)] +var coStars1 = g.Morphism().In("/film/performance/actor").In("/film/film/starring").Save("name","costar1_movie").Follow(filmToActor) +var coStars2 = g.Morphism().In("/film/performance/actor").In("/film/film/starring").Save("name","costar2_movie").Follow(filmToActor) + +// Stars for the movies "The Net" and "Speed" +var m1_actors = movie1.Save("name","movie1").Follow(filmToActor) +var m2_actors = movie2.Save("name","movie2").Follow(filmToActor) +` + +var ( + once sync.Once + cfg = &config.Config{ + DatabasePath: "30kmoviedata.nt.gz", + DatabaseType: "memstore", + GremlinTimeout: 1, + } + + ts graph.TripleStore +) + +func runBench(n int, b *testing.B) { + var err error + once.Do(func() { + ts, err = db.Open(cfg) + if err != nil { + b.Fatalf("Failed to open %q: %v", cfg.DatabasePath, err) + } + }) + ses := gremlin.NewSession(ts, cfg.GremlinTimeout, true) + _, err = ses.InputParses(benchmarkQueries[n].query) + if err != nil { + b.Fatalf("Failed to parse benchmark gremlin %s: %v", benchmarkQueries[n].message, err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + c := make(chan interface{}, 5) + go ses.ExecInput(benchmarkQueries[n].query, c, 100) + for _ = range c { + } + } +} + +func BenchmarkNamePredicate(b *testing.B) { + runBench(0, b) +} + +func BenchmarkLargeSetsNoIntersection(b *testing.B) { + runBench(1, b) +} + +func BenchmarkVeryLargeSetsSmallIntersection(b *testing.B) { + runBench(2, b) +} + +func BenchmarkHelplessChecker(b *testing.B) { + runBench(3, b) +} + +func BenchmarkNetAndSpeed(b *testing.B) { + runBench(4, b) +} + +func BenchmarkKeannuAndNet(b *testing.B) { + runBench(5, b) +} + +func BenchmarkKeannuAndSpeed(b *testing.B) { + runBench(6, b) +} + +func BenchmarkKeannuOther(b *testing.B) { + runBench(7, b) +} + +func BenchmarkKeannuBullockOther(b *testing.B) { + runBench(8, b) +} From 02672da9adaa1e71ee74ef00d3932e1474485318 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 07:32:38 +0930 Subject: [PATCH 2/8] Fix typos --- cayley_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cayley_test.go b/cayley_test.go index 28e4305..3008d0b 100644 --- a/cayley_test.go +++ b/cayley_test.go @@ -116,7 +116,7 @@ var benchmarkQueries = []struct { //Q: Did "Keanu Reeves" star in "The Net" ? //A: No { - message: "Keannu in The Net", + message: "Keanu in The Net", query: common + `actor2.Intersect(m1_actors).Out("name").All() `, }, @@ -124,7 +124,7 @@ var benchmarkQueries = []struct { //Q: Did "Keanu Reeves" star in "Speed" ? //A: Yes { - message: "Keannu in Speed", + message: "Keanu in Speed", query: common + `actor2.Intersect(m2_actors).Out("name").All() `, }, @@ -133,7 +133,7 @@ var benchmarkQueries = []struct { //A: "Keanu Reeves" was in "Speed" and "The Lake House" with "Sandra Bullock", // who was in "The Net" { - message: "Keannu with other in The Net", + message: "Keanu with other in The Net", query: common + `actor2.Follow(coStars1).Intersect(m1_actors).Out("name").All() `, }, @@ -142,7 +142,7 @@ var benchmarkQueries = []struct { //A5: Yes, many. For example: SB starred with "Steve Martin" in "The Prince // of Egypt", and KR starred with Steven Martin in "Parenthood". { - message: "Keannu and Bullock with other", + message: "Keanu and Bullock with other", query: common + `actor1.Save("name","costar1_actor").Follow(coStars1).Intersect(actor2.Save("name","costar2_actor").Follow(coStars2)).Out("name").All() `, }, From c4e4abbffce641bf68fa3b9b26b3ad690aeb4e96 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 08:33:18 +0930 Subject: [PATCH 3/8] Add result validation --- cayley_test.go | 213 +++++++++++++++++++++++++++++++++++++++++------ graph/session.go | 2 +- query/gremlin/session.go | 2 +- query/mql/session.go | 2 +- 4 files changed, 190 insertions(+), 29 deletions(-) diff --git a/cayley_test.go b/cayley_test.go index 3008d0b..e4f1247 100644 --- a/cayley_test.go +++ b/cayley_test.go @@ -28,7 +28,7 @@ var benchmarkQueries = []struct { message string query string tag string - expect []string + expect [][]interface{} }{ // Easy one to get us started. How quick is the most straightforward retrieval? { @@ -36,7 +36,9 @@ var benchmarkQueries = []struct { query: ` g.V("Humphrey Bogart").In("name").All() `, - expect: []string{":/en/humphrey_bogart"}, + expect: [][]interface{}{ + {map[string]string{"id": ":/en/humphrey_bogart"}}, + }, }, // Grunty queries. @@ -51,7 +53,7 @@ var benchmarkQueries = []struct { getId("Oliver Hardy").Follow(actor_to_film).Out("name").Intersect( getId("Mel Blanc").Follow(actor_to_film).Out("name")).All() `, - expect: []string{}, + expect: nil, }, // 2014-07-12: This one takes about 4 whole seconds in memory. This is a behemoth. @@ -74,7 +76,10 @@ var benchmarkQueries = []struct { } }) `, - expect: []string{":/en/billy_gilbert", ":/en/sterling_holloway"}, + expect: [][]interface{}{ + {":/en/billy_gilbert"}, + {":/en/sterling_holloway"}, + }, }, // This is more of an optimization problem that will get better over time. This takes a lot @@ -86,22 +91,22 @@ var benchmarkQueries = []struct { g.V().As("person").In("name").In().In().Out("name").Is("Casablanca").All() `, tag: "person", - expect: []string{ - "Claude Rains", - "Conrad Veidt", - "Dooley Wilson", - "Helmut Dantine", - "Humphrey Bogart", - "Ingrid Bergman", - "John Qualen", - "Joy Page", - "Leonid Kinskey", - "Lou Marcelle", - "Madeleine LeBeau", - "Paul Henreid", - "Peter Lorre", - "Sydney Greenstreet", - "S.Z. Sakall", + expect: [][]interface{}{ + {map[string]string{"id": "Casablanca", "person": "Claude Rains"}}, + {map[string]string{"id": "Casablanca", "person": "Conrad Veidt"}}, + {map[string]string{"id": "Casablanca", "person": "Dooley Wilson"}}, + {map[string]string{"id": "Casablanca", "person": "Helmut Dantine"}}, + {map[string]string{"id": "Casablanca", "person": "Humphrey Bogart"}}, + {map[string]string{"id": "Casablanca", "person": "Ingrid Bergman"}}, + {map[string]string{"id": "Casablanca", "person": "John Qualen"}}, + {map[string]string{"id": "Casablanca", "person": "Joy Page"}}, + {map[string]string{"id": "Casablanca", "person": "Leonid Kinskey"}}, + {map[string]string{"id": "Casablanca", "person": "Lou Marcelle"}}, + {map[string]string{"id": "Casablanca", "person": "Madeleine LeBeau"}}, + {map[string]string{"id": "Casablanca", "person": "Paul Henreid"}}, + {map[string]string{"id": "Casablanca", "person": "Peter Lorre"}}, + {map[string]string{"id": "Casablanca", "person": "Sydney Greenstreet"}}, + {map[string]string{"id": "Casablanca", "person": "S.Z. Sakall"}}, }, }, @@ -111,6 +116,9 @@ var benchmarkQueries = []struct { message: "Net and Speed", query: common + `m1_actors.Intersect(m2_actors).Out("name").All() `, + expect: [][]interface{}{ + {map[string]string{"id": "Sandra Bullock", "movie1": "The Net", "movie2": "Speed"}}, + }, }, //Q: Did "Keanu Reeves" star in "The Net" ? @@ -119,6 +127,7 @@ var benchmarkQueries = []struct { message: "Keanu in The Net", query: common + `actor2.Intersect(m1_actors).Out("name").All() `, + expect: nil, }, //Q: Did "Keanu Reeves" star in "Speed" ? @@ -127,6 +136,9 @@ var benchmarkQueries = []struct { message: "Keanu in Speed", query: common + `actor2.Intersect(m2_actors).Out("name").All() `, + expect: [][]interface{}{ + {map[string]string{"id": "Keanu Reeves", "movie2": "Speed"}}, + }, }, //Q: Has "Keanu Reeves" co-starred with anyone who starred in "The Net" ? @@ -136,15 +148,121 @@ var benchmarkQueries = []struct { message: "Keanu with other in The Net", query: common + `actor2.Follow(coStars1).Intersect(m1_actors).Out("name").All() `, + expect: [][]interface{}{ + {map[string]string{"id": "Sandra Bullock", "movie1": "The Net", "costar1_movie": "Speed"}}, + {map[string]string{"movie1": "The Net", "costar1_movie": "The Lake House", "id": "Sandra Bullock"}}, + }, }, - //Q5: Do "Keanu Reeves" and "Sandra Bullock" have any commons co-stars? - //A5: Yes, many. For example: SB starred with "Steve Martin" in "The Prince + //Q: Do "Keanu Reeves" and "Sandra Bullock" have any commons co-stars? + //A: Yes, many. For example: SB starred with "Steve Martin" in "The Prince // of Egypt", and KR starred with Steven Martin in "Parenthood". { message: "Keanu and Bullock with other", query: common + `actor1.Save("name","costar1_actor").Follow(coStars1).Intersect(actor2.Save("name","costar2_actor").Follow(coStars2)).Out("name").All() `, + expect: [][]interface{}{ + {map[string]string{"costar2_movie": "Speed", "id": "Alan Ruck", "costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_movie": "Demolition Man", "costar2_actor": "Keanu Reeves", "costar2_movie": "Thumbsucker", "id": "Benjamin Bratt", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Miss Congeniality", "costar2_actor": "Keanu Reeves", "costar2_movie": "Thumbsucker", "id": "Benjamin Bratt"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Beth Grant"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Carlos Carrasco"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Lake House", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Lake House", "id": "Christopher Plummer"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Proposal", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Devil's Advocate", "id": "Craig T. Nelson"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "River's Edge", "id": "Dennis Hopper"}}, + {map[string]string{"costar2_movie": "Speed", "id": "Dennis Hopper", "costar1_actor": "/people/person", "costar1_movie": "Chattahoochee", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Practical Magic", "costar2_actor": "Keanu Reeves", "costar2_movie": "Parenthood", "id": "Dianne Wiest"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Lake House", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Lake House", "id": "Dylan Walsh"}}, + {map[string]string{"costar2_movie": "Speed", "id": "Glenn Plummer", "costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed 2: Cruise Control", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Glenn Plummer"}}, + {map[string]string{"costar1_movie": "While You Were Sleeping", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Replacements", "id": "Jack Warden", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Infamous", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Jeff Daniels"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Jeff Daniels"}}, + {map[string]string{"id": "Joe Morton", "costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Jordan Lund", "costar1_actor": "Sandra Bullock", "costar1_movie": "Speed"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Flying", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Lake House", "costar2_actor": "Keanu Reeves", "costar2_movie": "Flying", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_movie": "The Day the Earth Stood Still", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Animatrix", "id": "Keanu Reeves", "costar1_actor": "/people/person"}}, + {map[string]string{"id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Tune in Tomorrow"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Last Time I Committed Suicide", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Constantine", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Permanent Record", "id": "Keanu Reeves"}}, + {map[string]string{"costar2_movie": "Dangerous Liaisons", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Private Lives of Pippa Lee", "id": "Keanu Reeves"}}, + {map[string]string{"costar2_movie": "A Scanner Darkly", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "A Walk in the Clouds", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Hardball", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Life Under Water", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Much Ado About Nothing", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "My Own Private Idaho", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Parenthood", "id": "Keanu Reeves", "costar1_actor": "/people/person"}}, + {map[string]string{"costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Point Break", "id": "Keanu Reeves", "costar1_actor": "/people/person"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Providence", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "River's Edge", "id": "Keanu Reeves"}}, + {map[string]string{"costar2_movie": "Something's Gotta Give", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Sweet November", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Lake House", "id": "Keanu Reeves", "costar1_actor": "/people/person"}}, + {map[string]string{"id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Matrix Reloaded"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Matrix Revisited", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Prince of Pennsylvania", "id": "Keanu Reeves"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "The Replacements", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Even Cowgirls Get the Blues", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Youngblood", "id": "Keanu Reeves"}}, + {map[string]string{"costar2_movie": "Bill & Ted's Bogus Journey", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Bill & Ted's Excellent Adventure", "id": "Keanu Reeves"}}, + {map[string]string{"id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Johnny Mnemonic"}}, + {map[string]string{"costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Devil's Advocate", "id": "Keanu Reeves", "costar1_actor": "/people/person"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Thumbsucker", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "I Love You to Death", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Bram Stoker's Dracula", "id": "Keanu Reeves", "costar1_actor": "/people/person"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "The Gift", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film"}}, + {map[string]string{"costar2_movie": "Little Buddha", "id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Night Watchman", "id": "Keanu Reeves"}}, + {map[string]string{"id": "Keanu Reeves", "costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Chain Reaction"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "Babes in Toyland", "id": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Day the Earth Stood Still", "id": "Keanu Reeves"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "The Lake House", "id": "Lynn Collins", "costar1_actor": "Sandra Bullock", "costar1_movie": "The Lake House"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Proposal", "costar2_actor": "Keanu Reeves", "costar2_movie": "Parenthood", "id": "Mary Steenburgen"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Prince of Egypt", "costar2_actor": "Keanu Reeves", "costar2_movie": "Dangerous Liaisons", "id": "Michelle Pfeiffer"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Premonition", "costar2_actor": "Keanu Reeves", "costar2_movie": "Constantine", "id": "Peter Stormare"}}, + {map[string]string{"costar2_movie": "Speed", "id": "Richard Lineback", "costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_movie": "The Thing Called Love", "costar2_actor": "Keanu Reeves", "costar2_movie": "My Own Private Idaho", "id": "River Phoenix", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "I Love You to Death", "id": "River Phoenix"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Proposal", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Crash", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Gun Shy", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Demolition Man"}}, + {map[string]string{"costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Divine Secrets of the Ya-Ya Sisterhood", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "A Time to Kill", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Forces of Nature", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Hope Floats", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Infamous", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Love Potion No. 9", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Miss Congeniality", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed"}}, + {map[string]string{"id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Miss Congeniality 2: Armed and Fabulous", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Murder by Numbers", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Practical Magic", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Speed"}}, + {map[string]string{"costar1_movie": "Speed 2: Cruise Control", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Lake House", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Net", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_movie": "The Prince of Egypt", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Two Weeks Notice"}}, + {map[string]string{"costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "While You Were Sleeping", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "28 Days", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Premonition"}}, + {map[string]string{"costar1_movie": "Wrestling Ernest Hemingway", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock", "costar1_movie": "Fire on the Amazon"}}, + {map[string]string{"costar1_movie": "The Thing Called Love", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock", "costar1_actor": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "In Love and War", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "/people/person", "costar1_movie": "/film/film", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Lake House", "id": "Sandra Bullock"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Divine Secrets of the Ya-Ya Sisterhood", "costar2_actor": "Keanu Reeves", "costar2_movie": "The Private Lives of Pippa Lee", "id": "Shirley Knight"}}, + {map[string]string{"costar2_movie": "The Lake House", "id": "Shohreh Aghdashloo", "costar1_actor": "Sandra Bullock", "costar1_movie": "The Lake House", "costar2_actor": "Keanu Reeves"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "The Prince of Egypt", "costar2_actor": "Keanu Reeves", "costar2_movie": "Parenthood", "id": "Steve Martin"}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Thomas Rosales, Jr."}}, + {map[string]string{"costar1_actor": "Sandra Bullock", "costar1_movie": "Speed", "costar2_actor": "Keanu Reeves", "costar2_movie": "Speed", "id": "Hawthorne James"}}, + }, }, } @@ -171,22 +289,65 @@ var ( cfg = &config.Config{ DatabasePath: "30kmoviedata.nt.gz", DatabaseType: "memstore", - GremlinTimeout: 1, + GremlinTimeout: 300, } ts graph.TripleStore ) -func runBench(n int, b *testing.B) { +func prepare(t testing.TB) { var err error once.Do(func() { ts, err = db.Open(cfg) if err != nil { - b.Fatalf("Failed to open %q: %v", cfg.DatabasePath, err) + t.Fatalf("Failed to open %q: %v", cfg.DatabasePath, err) } }) +} + +func TestQueries(t *testing.T) { + prepare(t) + for _, test := range benchmarkQueries { + ses := gremlin.NewSession(ts, cfg.GremlinTimeout, true) + _, err := ses.InputParses(test.query) + if err != nil { + t.Fatalf("Failed to parse benchmark gremlin %s: %v", test.message, err) + } + c := make(chan interface{}, 5) + go ses.ExecInput(test.query, c, 100) + var ( + got [][]interface{} + timedOut bool + ) + for r := range c { + ses.BuildJson(r) + j, err := ses.GetJson() + if j == nil && err == nil { + continue + } + if err != nil && err.Error() == "Query Timeout" { + timedOut = true + continue + } + got = append(got, j) + } + + if timedOut { + t.Error("Query timed out: skipping validation.") + continue + } + + // TODO(kortschak) Be more rigorous in this result validation. + if len(got) != len(test.expect) { + t.Errorf("Unexpected number of results, got:%d expect:%d.", len(got), len(test.expect)) + } + } +} + +func runBench(n int, b *testing.B) { + prepare(b) ses := gremlin.NewSession(ts, cfg.GremlinTimeout, true) - _, err = ses.InputParses(benchmarkQueries[n].query) + _, err := ses.InputParses(benchmarkQueries[n].query) if err != nil { b.Fatalf("Failed to parse benchmark gremlin %s: %v", benchmarkQueries[n].message, err) } diff --git a/graph/session.go b/graph/session.go index 03a137b..c525459 100644 --- a/graph/session.go +++ b/graph/session.go @@ -39,7 +39,7 @@ type HttpSession interface { ExecInput(string, chan interface{}, int) GetQuery(string, chan map[string]interface{}) BuildJson(interface{}) - GetJson() (interface{}, error) + GetJson() ([]interface{}, error) ClearJson() ToggleDebug() } diff --git a/query/gremlin/session.go b/query/gremlin/session.go index 210aed3..9e28f97 100644 --- a/query/gremlin/session.go +++ b/query/gremlin/session.go @@ -250,7 +250,7 @@ func (ses *Session) BuildJson(result interface{}) { } -func (ses *Session) GetJson() (interface{}, error) { +func (ses *Session) GetJson() ([]interface{}, error) { defer ses.ClearJson() if ses.err != nil { return nil, ses.err diff --git a/query/mql/session.go b/query/mql/session.go index e5944d4..5550351 100644 --- a/query/mql/session.go +++ b/query/mql/session.go @@ -130,7 +130,7 @@ func (s *Session) BuildJson(result interface{}) { s.currentQuery.treeifyResult(result.(map[string]graph.Value)) } -func (s *Session) GetJson() (interface{}, error) { +func (s *Session) GetJson() ([]interface{}, error) { s.currentQuery.buildResults() if s.currentQuery.isError() { return nil, s.currentQuery.err From b4887e3c306db12de801b050c691064b01abeed0 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 08:38:31 +0930 Subject: [PATCH 4/8] Update tests to use cquads parsed data --- cayley_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cayley_test.go b/cayley_test.go index e4f1247..ba6be1c 100644 --- a/cayley_test.go +++ b/cayley_test.go @@ -37,7 +37,7 @@ var benchmarkQueries = []struct { g.V("Humphrey Bogart").In("name").All() `, expect: [][]interface{}{ - {map[string]string{"id": ":/en/humphrey_bogart"}}, + {map[string]string{"id": "/en/humphrey_bogart"}}, }, }, @@ -77,8 +77,8 @@ var benchmarkQueries = []struct { }) `, expect: [][]interface{}{ - {":/en/billy_gilbert"}, - {":/en/sterling_holloway"}, + {"/en/billy_gilbert"}, + {"/en/sterling_holloway"}, }, }, @@ -287,7 +287,7 @@ var m2_actors = movie2.Save("name","movie2").Follow(filmToActor) var ( once sync.Once cfg = &config.Config{ - DatabasePath: "30kmoviedata.nt.gz", + DatabasePath: "30kmoviedata.nq.gz", DatabaseType: "memstore", GremlinTimeout: 300, } From 38f7b79761c43b4aba38a0783509f2806a8f7411 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 08:45:45 +0930 Subject: [PATCH 5/8] Allow short tests --- cayley_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cayley_test.go b/cayley_test.go index ba6be1c..16a8a3f 100644 --- a/cayley_test.go +++ b/cayley_test.go @@ -26,6 +26,7 @@ import ( var benchmarkQueries = []struct { message string + long bool query string tag string expect [][]interface{} @@ -59,6 +60,7 @@ var benchmarkQueries = []struct { // 2014-07-12: This one takes about 4 whole seconds in memory. This is a behemoth. { message: "three huge sets with small intersection", + long: true, query: ` function getId(x) { return g.V(x).In("name") } var actor_to_film = g.M().In("/film/performance/actor").In("/film/film/starring") @@ -87,6 +89,7 @@ var benchmarkQueries = []struct { // as it has a fixed ID. Exercises Check(). { message: "the helpless checker", + long: true, query: ` g.V().As("person").In("name").In().In().Out("name").Is("Casablanca").All() `, @@ -146,6 +149,7 @@ var benchmarkQueries = []struct { // who was in "The Net" { message: "Keanu with other in The Net", + long: true, query: common + `actor2.Follow(coStars1).Intersect(m1_actors).Out("name").All() `, expect: [][]interface{}{ @@ -159,6 +163,7 @@ var benchmarkQueries = []struct { // of Egypt", and KR starred with Steven Martin in "Parenthood". { message: "Keanu and Bullock with other", + long: true, query: common + `actor1.Save("name","costar1_actor").Follow(coStars1).Intersect(actor2.Save("name","costar2_actor").Follow(coStars2)).Out("name").All() `, expect: [][]interface{}{ @@ -308,6 +313,9 @@ func prepare(t testing.TB) { func TestQueries(t *testing.T) { prepare(t) for _, test := range benchmarkQueries { + if testing.Short() && test.long { + continue + } ses := gremlin.NewSession(ts, cfg.GremlinTimeout, true) _, err := ses.InputParses(test.query) if err != nil { @@ -345,6 +353,9 @@ func TestQueries(t *testing.T) { } func runBench(n int, b *testing.B) { + if testing.Short() && benchmarkQueries[n].long { + b.Skip() + } prepare(b) ses := gremlin.NewSession(ts, cfg.GremlinTimeout, true) _, err := ses.InputParses(benchmarkQueries[n].query) From a6cf432313242df9cf41d44828e5fc3e307088c1 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 08:52:24 +0930 Subject: [PATCH 6/8] Move query interface definitions into query --- db/repl.go | 11 ++++++----- graph/session.go | 45 --------------------------------------------- graph/sexp/session.go | 11 ++++++----- http/query.go | 18 +++++++++--------- query/gremlin/session.go | 7 ++++--- query/mql/session.go | 7 ++++--- query/session.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 70 deletions(-) delete mode 100644 graph/session.go create mode 100644 query/session.go diff --git a/db/repl.go b/db/repl.go index 3dcd083..731b0fa 100644 --- a/db/repl.go +++ b/db/repl.go @@ -27,6 +27,7 @@ import ( "github.com/google/cayley/graph" "github.com/google/cayley/graph/sexp" "github.com/google/cayley/quad/cquads" + "github.com/google/cayley/query" "github.com/google/cayley/query/gremlin" "github.com/google/cayley/query/mql" ) @@ -41,7 +42,7 @@ func un(s string, startTime time.Time) { fmt.Printf(s, float64(endTime.UnixNano()-startTime.UnixNano())/float64(1E6)) } -func Run(query string, ses graph.Session) { +func Run(query string, ses query.Session) { nResults := 0 startTrace, startTime := trace("Elapsed time: %g ms\n\n") defer func() { @@ -62,7 +63,7 @@ func Run(query string, ses graph.Session) { } func Repl(ts graph.TripleStore, queryLanguage string, cfg *config.Config) error { - var ses graph.Session + var ses query.Session switch queryLanguage { case "sexp": ses = sexp.NewSession(ts) @@ -140,13 +141,13 @@ func Repl(ts graph.TripleStore, queryLanguage string, cfg *config.Config) error } result, err := ses.InputParses(string(line)) switch result { - case graph.Parsed: + case query.Parsed: Run(string(line), ses) line = line[:0] - case graph.ParseFail: + case query.ParseFail: fmt.Println("Error: ", err) line = line[:0] - case graph.ParseMore: + case query.ParseMore: } } } diff --git a/graph/session.go b/graph/session.go deleted file mode 100644 index c525459..0000000 --- a/graph/session.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2014 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 graph - -// Defines the graph session interface general to all query languages. - -type ParseResult int - -const ( - Parsed ParseResult = iota - ParseMore - ParseFail -) - -type Session interface { - // Return whether the string is a valid expression. - InputParses(string) (ParseResult, error) - ExecInput(string, chan interface{}, int) - ToText(interface{}) string - ToggleDebug() -} - -type HttpSession interface { - // Return whether the string is a valid expression. - InputParses(string) (ParseResult, error) - // Runs the query and returns individual results on the channel. - ExecInput(string, chan interface{}, int) - GetQuery(string, chan map[string]interface{}) - BuildJson(interface{}) - GetJson() ([]interface{}, error) - ClearJson() - ToggleDebug() -} diff --git a/graph/sexp/session.go b/graph/sexp/session.go index 0fb4810..c1a227b 100644 --- a/graph/sexp/session.go +++ b/graph/sexp/session.go @@ -22,6 +22,7 @@ import ( "sort" "github.com/google/cayley/graph" + "github.com/google/cayley/query" ) type Session struct { @@ -39,7 +40,7 @@ func (s *Session) ToggleDebug() { s.debug = !s.debug } -func (s *Session) InputParses(input string) (graph.ParseResult, error) { +func (s *Session) InputParses(input string) (query.ParseResult, error) { var parenDepth int for i, x := range input { if x == '(' { @@ -52,17 +53,17 @@ func (s *Session) InputParses(input string) (graph.ParseResult, error) { if (i - 10) > min { min = i - 10 } - return graph.ParseFail, errors.New(fmt.Sprintf("Too many close parens at char %d: %s", i, input[min:i])) + return query.ParseFail, errors.New(fmt.Sprintf("Too many close parens at char %d: %s", i, input[min:i])) } } } if parenDepth > 0 { - return graph.ParseMore, nil + return query.ParseMore, nil } if len(ParseString(input)) > 0 { - return graph.Parsed, nil + return query.Parsed, nil } - return graph.ParseFail, errors.New("Invalid Syntax") + return query.ParseFail, errors.New("Invalid Syntax") } func (s *Session) ExecInput(input string, out chan interface{}, limit int) { diff --git a/http/query.go b/http/query.go index a8b8c2f..e8b5d72 100644 --- a/http/query.go +++ b/http/query.go @@ -22,7 +22,7 @@ import ( "github.com/julienschmidt/httprouter" - "github.com/google/cayley/graph" + "github.com/google/cayley/query" "github.com/google/cayley/query/gremlin" "github.com/google/cayley/query/mql" ) @@ -47,7 +47,7 @@ func WrapResult(result interface{}) ([]byte, error) { return json.MarshalIndent(wrap, "", " ") } -func RunJsonQuery(query string, ses graph.HttpSession) (interface{}, error) { +func RunJsonQuery(query string, ses query.HttpSession) (interface{}, error) { c := make(chan interface{}, 5) go ses.ExecInput(query, c, 100) for res := range c { @@ -56,7 +56,7 @@ func RunJsonQuery(query string, ses graph.HttpSession) (interface{}, error) { return ses.GetJson() } -func GetQueryShape(query string, ses graph.HttpSession) ([]byte, error) { +func GetQueryShape(query string, ses query.HttpSession) ([]byte, error) { c := make(chan map[string]interface{}, 5) go ses.GetQuery(query, c) var data map[string]interface{} @@ -68,7 +68,7 @@ func GetQueryShape(query string, ses graph.HttpSession) ([]byte, error) { // TODO(barakmich): Turn this into proper middleware. func (api *Api) ServeV1Query(w http.ResponseWriter, r *http.Request, params httprouter.Params) int { - var ses graph.HttpSession + var ses query.HttpSession switch params.ByName("query_lang") { case "gremlin": ses = gremlin.NewSession(api.ts, api.config.GremlinTimeout, false) @@ -84,7 +84,7 @@ func (api *Api) ServeV1Query(w http.ResponseWriter, r *http.Request, params http code := string(bodyBytes) result, err := ses.InputParses(code) switch result { - case graph.Parsed: + case query.Parsed: var output interface{} var bytes []byte var err error @@ -103,7 +103,7 @@ func (api *Api) ServeV1Query(w http.ResponseWriter, r *http.Request, params http fmt.Fprint(w, string(bytes)) ses = nil return 200 - case graph.ParseFail: + case query.ParseFail: ses = nil return FormatJson400(w, err) default: @@ -116,7 +116,7 @@ func (api *Api) ServeV1Query(w http.ResponseWriter, r *http.Request, params http } func (api *Api) ServeV1Shape(w http.ResponseWriter, r *http.Request, params httprouter.Params) int { - var ses graph.HttpSession + var ses query.HttpSession switch params.ByName("query_lang") { case "gremlin": ses = gremlin.NewSession(api.ts, api.config.GremlinTimeout, false) @@ -132,7 +132,7 @@ func (api *Api) ServeV1Shape(w http.ResponseWriter, r *http.Request, params http code := string(bodyBytes) result, err := ses.InputParses(code) switch result { - case graph.Parsed: + case query.Parsed: var output []byte var err error output, err = GetQueryShape(code, ses) @@ -141,7 +141,7 @@ func (api *Api) ServeV1Shape(w http.ResponseWriter, r *http.Request, params http } fmt.Fprint(w, string(output)) return 200 - case graph.ParseFail: + case query.ParseFail: return FormatJson400(w, err) default: return FormatJsonError(w, 500, "Incomplete data?") diff --git a/query/gremlin/session.go b/query/gremlin/session.go index 9e28f97..00532b8 100644 --- a/query/gremlin/session.go +++ b/query/gremlin/session.go @@ -23,6 +23,7 @@ import ( "github.com/robertkrimen/otto" "github.com/google/cayley/graph" + "github.com/google/cayley/query" ) type Session struct { @@ -81,13 +82,13 @@ func (s *Session) GetQuery(input string, output_struct chan map[string]interface s.queryShape = nil } -func (s *Session) InputParses(input string) (graph.ParseResult, error) { +func (s *Session) InputParses(input string) (query.ParseResult, error) { script, err := s.env.Compile("", input) if err != nil { - return graph.ParseFail, err + return query.ParseFail, err } s.script = script - return graph.Parsed, nil + return query.Parsed, nil } func (s *Session) SendResult(result *GremlinResult) bool { diff --git a/query/mql/session.go b/query/mql/session.go index bc983fd..c272005 100644 --- a/query/mql/session.go +++ b/query/mql/session.go @@ -23,6 +23,7 @@ import ( "github.com/google/cayley/graph" "github.com/google/cayley/graph/iterator" + "github.com/google/cayley/query" ) type Session struct { @@ -62,13 +63,13 @@ func (m *Session) GetQuery(input string, output_struct chan map[string]interface output_struct <- output } -func (s *Session) InputParses(input string) (graph.ParseResult, error) { +func (s *Session) InputParses(input string) (query.ParseResult, error) { var x interface{} err := json.Unmarshal([]byte(input), &x) if err != nil { - return graph.ParseFail, err + return query.ParseFail, err } - return graph.Parsed, nil + return query.Parsed, nil } func (s *Session) ExecInput(input string, c chan interface{}, limit int) { diff --git a/query/session.go b/query/session.go new file mode 100644 index 0000000..1531fd2 --- /dev/null +++ b/query/session.go @@ -0,0 +1,45 @@ +// Copyright 2014 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 query + +// Defines the graph session interface general to all query languages. + +type ParseResult int + +const ( + Parsed ParseResult = iota + ParseMore + ParseFail +) + +type Session interface { + // Return whether the string is a valid expression. + InputParses(string) (ParseResult, error) + ExecInput(string, chan interface{}, int) + ToText(interface{}) string + ToggleDebug() +} + +type HttpSession interface { + // Return whether the string is a valid expression. + InputParses(string) (ParseResult, error) + // Runs the query and returns individual results on the channel. + ExecInput(string, chan interface{}, int) + GetQuery(string, chan map[string]interface{}) + BuildJson(interface{}) + GetJson() ([]interface{}, error) + ClearJson() + ToggleDebug() +} From 09943c3eb63d5bf385d92c5d961c1a8c1bfc14a2 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 09:36:43 +0930 Subject: [PATCH 7/8] Move sexp into query --- db/repl.go | 2 +- graph/sexp/parser.go | 273 ---------------------------------------------- graph/sexp/parser_test.go | 149 ------------------------- graph/sexp/session.go | 122 --------------------- query/sexp/parser.go | 273 ++++++++++++++++++++++++++++++++++++++++++++++ query/sexp/parser_test.go | 149 +++++++++++++++++++++++++ query/sexp/session.go | 122 +++++++++++++++++++++ 7 files changed, 545 insertions(+), 545 deletions(-) delete mode 100644 graph/sexp/parser.go delete mode 100644 graph/sexp/parser_test.go delete mode 100644 graph/sexp/session.go create mode 100644 query/sexp/parser.go create mode 100644 query/sexp/parser_test.go create mode 100644 query/sexp/session.go diff --git a/db/repl.go b/db/repl.go index 731b0fa..2ef9429 100644 --- a/db/repl.go +++ b/db/repl.go @@ -25,11 +25,11 @@ import ( "github.com/google/cayley/config" "github.com/google/cayley/graph" - "github.com/google/cayley/graph/sexp" "github.com/google/cayley/quad/cquads" "github.com/google/cayley/query" "github.com/google/cayley/query/gremlin" "github.com/google/cayley/query/mql" + "github.com/google/cayley/query/sexp" ) func trace(s string) (string, time.Time) { diff --git a/graph/sexp/parser.go b/graph/sexp/parser.go deleted file mode 100644 index fa25b5d..0000000 --- a/graph/sexp/parser.go +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright 2014 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 sexp - -import ( - "github.com/badgerodon/peg" - - "github.com/google/cayley/graph" - "github.com/google/cayley/graph/iterator" - "github.com/google/cayley/quad" -) - -func BuildIteratorTreeForQuery(ts graph.TripleStore, query string) graph.Iterator { - tree := parseQuery(query) - return buildIteratorTree(tree, ts) -} - -func ParseString(input string) string { - return parseQuery(input).String() -} - -func parseQuery(input string) *peg.ExpressionTree { - parser := peg.NewParser() - - start := parser.NonTerminal("Start") - whitespace := parser.NonTerminal("Whitespace") - quotedString := parser.NonTerminal("QuotedString") - rootConstraint := parser.NonTerminal("RootConstraint") - - constraint := parser.NonTerminal("Constraint") - colonIdentifier := parser.NonTerminal("ColonIdentifier") - variable := parser.NonTerminal("Variable") - identifier := parser.NonTerminal("Identifier") - fixedNode := parser.NonTerminal("FixedNode") - nodeIdent := parser.NonTerminal("NodeIdentifier") - predIdent := parser.NonTerminal("PredIdentifier") - reverse := parser.NonTerminal("Reverse") - predKeyword := parser.NonTerminal("PredicateKeyword") - optional := parser.NonTerminal("OptionalKeyword") - - start.Expression = rootConstraint - - whitespace.Expression = parser.OneOrMore( - parser.OrderedChoice( - parser.Terminal(' '), - parser.Terminal('\t'), - parser.Terminal('\n'), - parser.Terminal('\r'), - ), - ) - - quotedString.Expression = parser.Sequence( - parser.Terminal('"'), - parser.OneOrMore( - parser.OrderedChoice( - parser.Range('0', '9'), - parser.Range('a', 'z'), - parser.Range('A', 'Z'), - parser.Terminal('_'), - parser.Terminal('/'), - parser.Terminal(':'), - parser.Terminal(' '), - parser.Terminal('\''), - ), - ), - parser.Terminal('"'), - ) - - predKeyword.Expression = parser.OrderedChoice( - optional, - ) - - optional.Expression = parser.Sequence( - parser.Terminal('o'), - parser.Terminal('p'), - parser.Terminal('t'), - parser.Terminal('i'), - parser.Terminal('o'), - parser.Terminal('n'), - parser.Terminal('a'), - parser.Terminal('l'), - ) - - identifier.Expression = parser.OneOrMore( - parser.OrderedChoice( - parser.Range('0', '9'), - parser.Range('a', 'z'), - parser.Range('A', 'Z'), - parser.Terminal('_'), - parser.Terminal('.'), - parser.Terminal('/'), - parser.Terminal(':'), - parser.Terminal('#'), - ), - ) - - reverse.Expression = parser.Terminal('!') - - variable.Expression = parser.Sequence( - parser.Terminal('$'), - identifier, - ) - - colonIdentifier.Expression = parser.Sequence( - parser.Terminal(':'), - identifier, - ) - - fixedNode.Expression = parser.OrderedChoice( - colonIdentifier, - quotedString, - ) - - nodeIdent.Expression = parser.OrderedChoice( - variable, - fixedNode, - ) - - predIdent.Expression = parser.Sequence( - parser.Optional(reverse), - parser.OrderedChoice( - nodeIdent, - constraint, - ), - ) - - constraint.Expression = parser.Sequence( - parser.Terminal('('), - parser.Optional(whitespace), - predIdent, - parser.Optional(whitespace), - parser.Optional(predKeyword), - parser.Optional(whitespace), - parser.OrderedChoice( - nodeIdent, - rootConstraint, - ), - parser.Optional(whitespace), - parser.Terminal(')'), - ) - - rootConstraint.Expression = parser.Sequence( - parser.Terminal('('), - parser.Optional(whitespace), - nodeIdent, - parser.Optional(whitespace), - parser.ZeroOrMore(parser.Sequence( - constraint, - parser.Optional(whitespace), - )), - parser.Terminal(')'), - ) - - tree := parser.Parse(input) - return tree -} - -func getIdentString(tree *peg.ExpressionTree) string { - out := "" - if len(tree.Children) > 0 { - for _, child := range tree.Children { - out += getIdentString(child) - } - } else { - if tree.Value != '"' { - out += string(tree.Value) - } - } - return out -} - -func buildIteratorTree(tree *peg.ExpressionTree, ts graph.TripleStore) graph.Iterator { - switch tree.Name { - case "Start": - return buildIteratorTree(tree.Children[0], ts) - case "NodeIdentifier": - var out graph.Iterator - nodeID := getIdentString(tree) - if tree.Children[0].Name == "Variable" { - allIt := ts.NodesAllIterator() - allIt.Tagger().Add(nodeID) - out = allIt - } else { - n := nodeID - if tree.Children[0].Children[0].Name == "ColonIdentifier" { - n = nodeID[1:] - } - fixed := ts.FixedIterator() - fixed.Add(ts.ValueOf(n)) - out = fixed - } - return out - case "PredIdentifier": - i := 0 - if tree.Children[0].Name == "Reverse" { - //Taken care of below - i++ - } - it := buildIteratorTree(tree.Children[i], ts) - lto := iterator.NewLinksTo(ts, it, quad.Predicate) - return lto - case "RootConstraint": - constraintCount := 0 - and := iterator.NewAnd() - for _, c := range tree.Children { - switch c.Name { - case "NodeIdentifier": - fallthrough - case "Constraint": - it := buildIteratorTree(c, ts) - and.AddSubIterator(it) - constraintCount++ - continue - default: - continue - } - } - return and - case "Constraint": - var hasa *iterator.HasA - topLevelDir := quad.Subject - subItDir := quad.Object - subAnd := iterator.NewAnd() - isOptional := false - for _, c := range tree.Children { - switch c.Name { - case "PredIdentifier": - if c.Children[0].Name == "Reverse" { - topLevelDir = quad.Object - subItDir = quad.Subject - } - it := buildIteratorTree(c, ts) - subAnd.AddSubIterator(it) - continue - case "PredicateKeyword": - switch c.Children[0].Name { - case "OptionalKeyword": - isOptional = true - } - case "NodeIdentifier": - fallthrough - case "RootConstraint": - it := buildIteratorTree(c, ts) - l := iterator.NewLinksTo(ts, it, subItDir) - subAnd.AddSubIterator(l) - continue - default: - continue - } - } - hasa = iterator.NewHasA(ts, subAnd, topLevelDir) - if isOptional { - optional := iterator.NewOptional(hasa) - return optional - } - return hasa - default: - return &iterator.Null{} - } - panic("Not reached") -} diff --git a/graph/sexp/parser_test.go b/graph/sexp/parser_test.go deleted file mode 100644 index e7e66bf..0000000 --- a/graph/sexp/parser_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2014 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 sexp - -import ( - "testing" - - "github.com/google/cayley/graph" - "github.com/google/cayley/quad" - - _ "github.com/google/cayley/graph/memstore" -) - -func TestBadParse(t *testing.T) { - str := ParseString("()") - if str != "" { - t.Errorf("Unexpected parse result, got:%q", str) - } -} - -var testQueries = []struct { - message string - add *quad.Quad - query string - typ graph.Type - expect string -}{ - { - message: "get a single triple linkage", - add: &quad.Quad{"i", "can", "win", ""}, - query: "($a (:can \"win\"))", - typ: graph.And, - expect: "i", - }, - { - message: "get a single triple linkage", - add: &quad.Quad{"i", "can", "win", ""}, - query: "(\"i\" (:can $a))", - typ: graph.And, - expect: "i", - }, -} - -func TestMemstoreBackedSexp(t *testing.T) { - ts, _ := graph.NewTripleStore("memstore", "", nil) - it := BuildIteratorTreeForQuery(ts, "()") - if it.Type() != graph.Null { - t.Errorf(`Incorrect type for empty query, got:%q expect: "null"`, it.Type()) - } - for _, test := range testQueries { - if test.add != nil { - ts.AddTriple(test.add) - } - it := BuildIteratorTreeForQuery(ts, test.query) - if it.Type() != test.typ { - t.Errorf("Incorrect type for %s, got:%q expect %q", test.message, it.Type(), test.expect) - } - got, ok := graph.Next(it) - if !ok { - t.Errorf("Failed to %s", test.message) - } - if expect := ts.ValueOf(test.expect); got != expect { - t.Errorf("Incorrect result for %s, got:%v expect %v", test.message, got, expect) - } - } -} - -func TestTreeConstraintParse(t *testing.T) { - ts, _ := graph.NewTripleStore("memstore", "", nil) - ts.AddTriple(&quad.Quad{"i", "like", "food", ""}) - ts.AddTriple(&quad.Quad{"food", "is", "good", ""}) - query := "(\"i\"\n" + - "(:like\n" + - "($a (:is :good))))" - it := BuildIteratorTreeForQuery(ts, query) - if it.Type() != graph.And { - t.Error("Odd iterator tree. Got: %s", it.DebugString(0)) - } - out, ok := graph.Next(it) - if !ok { - t.Error("Got no results") - } - if out != ts.ValueOf("i") { - t.Errorf("Got %d, expected %d", out, ts.ValueOf("i")) - } -} - -func TestTreeConstraintTagParse(t *testing.T) { - ts, _ := graph.NewTripleStore("memstore", "", nil) - ts.AddTriple(&quad.Quad{"i", "like", "food", ""}) - ts.AddTriple(&quad.Quad{"food", "is", "good", ""}) - query := "(\"i\"\n" + - "(:like\n" + - "($a (:is :good))))" - it := BuildIteratorTreeForQuery(ts, query) - _, ok := graph.Next(it) - if !ok { - t.Error("Got no results") - } - tags := make(map[string]graph.Value) - it.TagResults(tags) - if ts.NameOf(tags["$a"]) != "food" { - t.Errorf("Got %s, expected food", ts.NameOf(tags["$a"])) - } - -} - -func TestMultipleConstraintParse(t *testing.T) { - ts, _ := graph.NewTripleStore("memstore", "", nil) - for _, tv := range []*quad.Quad{ - {"i", "like", "food", ""}, - {"i", "like", "beer", ""}, - {"you", "like", "beer", ""}, - } { - ts.AddTriple(tv) - } - query := `( - $a - (:like :beer) - (:like "food") - )` - it := BuildIteratorTreeForQuery(ts, query) - if it.Type() != graph.And { - t.Error("Odd iterator tree. Got: %s", it.DebugString(0)) - } - out, ok := graph.Next(it) - if !ok { - t.Error("Got no results") - } - if out != ts.ValueOf("i") { - t.Errorf("Got %d, expected %d", out, ts.ValueOf("i")) - } - _, ok = graph.Next(it) - if ok { - t.Error("Too many results") - } -} diff --git a/graph/sexp/session.go b/graph/sexp/session.go deleted file mode 100644 index c1a227b..0000000 --- a/graph/sexp/session.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2014 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 sexp - -// Defines a running session of the sexp query language. - -import ( - "errors" - "fmt" - "sort" - - "github.com/google/cayley/graph" - "github.com/google/cayley/query" -) - -type Session struct { - ts graph.TripleStore - debug bool -} - -func NewSession(inputTripleStore graph.TripleStore) *Session { - var s Session - s.ts = inputTripleStore - return &s -} - -func (s *Session) ToggleDebug() { - s.debug = !s.debug -} - -func (s *Session) InputParses(input string) (query.ParseResult, error) { - var parenDepth int - for i, x := range input { - if x == '(' { - parenDepth++ - } - if x == ')' { - parenDepth-- - if parenDepth < 0 { - min := 0 - if (i - 10) > min { - min = i - 10 - } - return query.ParseFail, errors.New(fmt.Sprintf("Too many close parens at char %d: %s", i, input[min:i])) - } - } - } - if parenDepth > 0 { - return query.ParseMore, nil - } - if len(ParseString(input)) > 0 { - return query.Parsed, nil - } - return query.ParseFail, errors.New("Invalid Syntax") -} - -func (s *Session) ExecInput(input string, out chan interface{}, limit int) { - it := BuildIteratorTreeForQuery(s.ts, input) - newIt, changed := it.Optimize() - if changed { - it = newIt - } - - if s.debug { - fmt.Println(it.DebugString(0)) - } - nResults := 0 - for { - _, ok := graph.Next(it) - if !ok { - break - } - tags := make(map[string]graph.Value) - it.TagResults(tags) - out <- &tags - nResults++ - if nResults > limit && limit != -1 { - break - } - for it.NextResult() == true { - tags := make(map[string]graph.Value) - it.TagResults(tags) - out <- &tags - nResults++ - if nResults > limit && limit != -1 { - break - } - } - } - close(out) -} - -func (s *Session) ToText(result interface{}) string { - out := fmt.Sprintln("****") - tags := result.(map[string]graph.Value) - tagKeys := make([]string, len(tags)) - i := 0 - for k := range tags { - tagKeys[i] = k - i++ - } - sort.Strings(tagKeys) - for _, k := range tagKeys { - if k == "$_" { - continue - } - out += fmt.Sprintf("%s : %s\n", k, s.ts.NameOf(tags[k])) - } - return out -} diff --git a/query/sexp/parser.go b/query/sexp/parser.go new file mode 100644 index 0000000..fa25b5d --- /dev/null +++ b/query/sexp/parser.go @@ -0,0 +1,273 @@ +// Copyright 2014 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 sexp + +import ( + "github.com/badgerodon/peg" + + "github.com/google/cayley/graph" + "github.com/google/cayley/graph/iterator" + "github.com/google/cayley/quad" +) + +func BuildIteratorTreeForQuery(ts graph.TripleStore, query string) graph.Iterator { + tree := parseQuery(query) + return buildIteratorTree(tree, ts) +} + +func ParseString(input string) string { + return parseQuery(input).String() +} + +func parseQuery(input string) *peg.ExpressionTree { + parser := peg.NewParser() + + start := parser.NonTerminal("Start") + whitespace := parser.NonTerminal("Whitespace") + quotedString := parser.NonTerminal("QuotedString") + rootConstraint := parser.NonTerminal("RootConstraint") + + constraint := parser.NonTerminal("Constraint") + colonIdentifier := parser.NonTerminal("ColonIdentifier") + variable := parser.NonTerminal("Variable") + identifier := parser.NonTerminal("Identifier") + fixedNode := parser.NonTerminal("FixedNode") + nodeIdent := parser.NonTerminal("NodeIdentifier") + predIdent := parser.NonTerminal("PredIdentifier") + reverse := parser.NonTerminal("Reverse") + predKeyword := parser.NonTerminal("PredicateKeyword") + optional := parser.NonTerminal("OptionalKeyword") + + start.Expression = rootConstraint + + whitespace.Expression = parser.OneOrMore( + parser.OrderedChoice( + parser.Terminal(' '), + parser.Terminal('\t'), + parser.Terminal('\n'), + parser.Terminal('\r'), + ), + ) + + quotedString.Expression = parser.Sequence( + parser.Terminal('"'), + parser.OneOrMore( + parser.OrderedChoice( + parser.Range('0', '9'), + parser.Range('a', 'z'), + parser.Range('A', 'Z'), + parser.Terminal('_'), + parser.Terminal('/'), + parser.Terminal(':'), + parser.Terminal(' '), + parser.Terminal('\''), + ), + ), + parser.Terminal('"'), + ) + + predKeyword.Expression = parser.OrderedChoice( + optional, + ) + + optional.Expression = parser.Sequence( + parser.Terminal('o'), + parser.Terminal('p'), + parser.Terminal('t'), + parser.Terminal('i'), + parser.Terminal('o'), + parser.Terminal('n'), + parser.Terminal('a'), + parser.Terminal('l'), + ) + + identifier.Expression = parser.OneOrMore( + parser.OrderedChoice( + parser.Range('0', '9'), + parser.Range('a', 'z'), + parser.Range('A', 'Z'), + parser.Terminal('_'), + parser.Terminal('.'), + parser.Terminal('/'), + parser.Terminal(':'), + parser.Terminal('#'), + ), + ) + + reverse.Expression = parser.Terminal('!') + + variable.Expression = parser.Sequence( + parser.Terminal('$'), + identifier, + ) + + colonIdentifier.Expression = parser.Sequence( + parser.Terminal(':'), + identifier, + ) + + fixedNode.Expression = parser.OrderedChoice( + colonIdentifier, + quotedString, + ) + + nodeIdent.Expression = parser.OrderedChoice( + variable, + fixedNode, + ) + + predIdent.Expression = parser.Sequence( + parser.Optional(reverse), + parser.OrderedChoice( + nodeIdent, + constraint, + ), + ) + + constraint.Expression = parser.Sequence( + parser.Terminal('('), + parser.Optional(whitespace), + predIdent, + parser.Optional(whitespace), + parser.Optional(predKeyword), + parser.Optional(whitespace), + parser.OrderedChoice( + nodeIdent, + rootConstraint, + ), + parser.Optional(whitespace), + parser.Terminal(')'), + ) + + rootConstraint.Expression = parser.Sequence( + parser.Terminal('('), + parser.Optional(whitespace), + nodeIdent, + parser.Optional(whitespace), + parser.ZeroOrMore(parser.Sequence( + constraint, + parser.Optional(whitespace), + )), + parser.Terminal(')'), + ) + + tree := parser.Parse(input) + return tree +} + +func getIdentString(tree *peg.ExpressionTree) string { + out := "" + if len(tree.Children) > 0 { + for _, child := range tree.Children { + out += getIdentString(child) + } + } else { + if tree.Value != '"' { + out += string(tree.Value) + } + } + return out +} + +func buildIteratorTree(tree *peg.ExpressionTree, ts graph.TripleStore) graph.Iterator { + switch tree.Name { + case "Start": + return buildIteratorTree(tree.Children[0], ts) + case "NodeIdentifier": + var out graph.Iterator + nodeID := getIdentString(tree) + if tree.Children[0].Name == "Variable" { + allIt := ts.NodesAllIterator() + allIt.Tagger().Add(nodeID) + out = allIt + } else { + n := nodeID + if tree.Children[0].Children[0].Name == "ColonIdentifier" { + n = nodeID[1:] + } + fixed := ts.FixedIterator() + fixed.Add(ts.ValueOf(n)) + out = fixed + } + return out + case "PredIdentifier": + i := 0 + if tree.Children[0].Name == "Reverse" { + //Taken care of below + i++ + } + it := buildIteratorTree(tree.Children[i], ts) + lto := iterator.NewLinksTo(ts, it, quad.Predicate) + return lto + case "RootConstraint": + constraintCount := 0 + and := iterator.NewAnd() + for _, c := range tree.Children { + switch c.Name { + case "NodeIdentifier": + fallthrough + case "Constraint": + it := buildIteratorTree(c, ts) + and.AddSubIterator(it) + constraintCount++ + continue + default: + continue + } + } + return and + case "Constraint": + var hasa *iterator.HasA + topLevelDir := quad.Subject + subItDir := quad.Object + subAnd := iterator.NewAnd() + isOptional := false + for _, c := range tree.Children { + switch c.Name { + case "PredIdentifier": + if c.Children[0].Name == "Reverse" { + topLevelDir = quad.Object + subItDir = quad.Subject + } + it := buildIteratorTree(c, ts) + subAnd.AddSubIterator(it) + continue + case "PredicateKeyword": + switch c.Children[0].Name { + case "OptionalKeyword": + isOptional = true + } + case "NodeIdentifier": + fallthrough + case "RootConstraint": + it := buildIteratorTree(c, ts) + l := iterator.NewLinksTo(ts, it, subItDir) + subAnd.AddSubIterator(l) + continue + default: + continue + } + } + hasa = iterator.NewHasA(ts, subAnd, topLevelDir) + if isOptional { + optional := iterator.NewOptional(hasa) + return optional + } + return hasa + default: + return &iterator.Null{} + } + panic("Not reached") +} diff --git a/query/sexp/parser_test.go b/query/sexp/parser_test.go new file mode 100644 index 0000000..e7e66bf --- /dev/null +++ b/query/sexp/parser_test.go @@ -0,0 +1,149 @@ +// Copyright 2014 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 sexp + +import ( + "testing" + + "github.com/google/cayley/graph" + "github.com/google/cayley/quad" + + _ "github.com/google/cayley/graph/memstore" +) + +func TestBadParse(t *testing.T) { + str := ParseString("()") + if str != "" { + t.Errorf("Unexpected parse result, got:%q", str) + } +} + +var testQueries = []struct { + message string + add *quad.Quad + query string + typ graph.Type + expect string +}{ + { + message: "get a single triple linkage", + add: &quad.Quad{"i", "can", "win", ""}, + query: "($a (:can \"win\"))", + typ: graph.And, + expect: "i", + }, + { + message: "get a single triple linkage", + add: &quad.Quad{"i", "can", "win", ""}, + query: "(\"i\" (:can $a))", + typ: graph.And, + expect: "i", + }, +} + +func TestMemstoreBackedSexp(t *testing.T) { + ts, _ := graph.NewTripleStore("memstore", "", nil) + it := BuildIteratorTreeForQuery(ts, "()") + if it.Type() != graph.Null { + t.Errorf(`Incorrect type for empty query, got:%q expect: "null"`, it.Type()) + } + for _, test := range testQueries { + if test.add != nil { + ts.AddTriple(test.add) + } + it := BuildIteratorTreeForQuery(ts, test.query) + if it.Type() != test.typ { + t.Errorf("Incorrect type for %s, got:%q expect %q", test.message, it.Type(), test.expect) + } + got, ok := graph.Next(it) + if !ok { + t.Errorf("Failed to %s", test.message) + } + if expect := ts.ValueOf(test.expect); got != expect { + t.Errorf("Incorrect result for %s, got:%v expect %v", test.message, got, expect) + } + } +} + +func TestTreeConstraintParse(t *testing.T) { + ts, _ := graph.NewTripleStore("memstore", "", nil) + ts.AddTriple(&quad.Quad{"i", "like", "food", ""}) + ts.AddTriple(&quad.Quad{"food", "is", "good", ""}) + query := "(\"i\"\n" + + "(:like\n" + + "($a (:is :good))))" + it := BuildIteratorTreeForQuery(ts, query) + if it.Type() != graph.And { + t.Error("Odd iterator tree. Got: %s", it.DebugString(0)) + } + out, ok := graph.Next(it) + if !ok { + t.Error("Got no results") + } + if out != ts.ValueOf("i") { + t.Errorf("Got %d, expected %d", out, ts.ValueOf("i")) + } +} + +func TestTreeConstraintTagParse(t *testing.T) { + ts, _ := graph.NewTripleStore("memstore", "", nil) + ts.AddTriple(&quad.Quad{"i", "like", "food", ""}) + ts.AddTriple(&quad.Quad{"food", "is", "good", ""}) + query := "(\"i\"\n" + + "(:like\n" + + "($a (:is :good))))" + it := BuildIteratorTreeForQuery(ts, query) + _, ok := graph.Next(it) + if !ok { + t.Error("Got no results") + } + tags := make(map[string]graph.Value) + it.TagResults(tags) + if ts.NameOf(tags["$a"]) != "food" { + t.Errorf("Got %s, expected food", ts.NameOf(tags["$a"])) + } + +} + +func TestMultipleConstraintParse(t *testing.T) { + ts, _ := graph.NewTripleStore("memstore", "", nil) + for _, tv := range []*quad.Quad{ + {"i", "like", "food", ""}, + {"i", "like", "beer", ""}, + {"you", "like", "beer", ""}, + } { + ts.AddTriple(tv) + } + query := `( + $a + (:like :beer) + (:like "food") + )` + it := BuildIteratorTreeForQuery(ts, query) + if it.Type() != graph.And { + t.Error("Odd iterator tree. Got: %s", it.DebugString(0)) + } + out, ok := graph.Next(it) + if !ok { + t.Error("Got no results") + } + if out != ts.ValueOf("i") { + t.Errorf("Got %d, expected %d", out, ts.ValueOf("i")) + } + _, ok = graph.Next(it) + if ok { + t.Error("Too many results") + } +} diff --git a/query/sexp/session.go b/query/sexp/session.go new file mode 100644 index 0000000..c1a227b --- /dev/null +++ b/query/sexp/session.go @@ -0,0 +1,122 @@ +// Copyright 2014 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 sexp + +// Defines a running session of the sexp query language. + +import ( + "errors" + "fmt" + "sort" + + "github.com/google/cayley/graph" + "github.com/google/cayley/query" +) + +type Session struct { + ts graph.TripleStore + debug bool +} + +func NewSession(inputTripleStore graph.TripleStore) *Session { + var s Session + s.ts = inputTripleStore + return &s +} + +func (s *Session) ToggleDebug() { + s.debug = !s.debug +} + +func (s *Session) InputParses(input string) (query.ParseResult, error) { + var parenDepth int + for i, x := range input { + if x == '(' { + parenDepth++ + } + if x == ')' { + parenDepth-- + if parenDepth < 0 { + min := 0 + if (i - 10) > min { + min = i - 10 + } + return query.ParseFail, errors.New(fmt.Sprintf("Too many close parens at char %d: %s", i, input[min:i])) + } + } + } + if parenDepth > 0 { + return query.ParseMore, nil + } + if len(ParseString(input)) > 0 { + return query.Parsed, nil + } + return query.ParseFail, errors.New("Invalid Syntax") +} + +func (s *Session) ExecInput(input string, out chan interface{}, limit int) { + it := BuildIteratorTreeForQuery(s.ts, input) + newIt, changed := it.Optimize() + if changed { + it = newIt + } + + if s.debug { + fmt.Println(it.DebugString(0)) + } + nResults := 0 + for { + _, ok := graph.Next(it) + if !ok { + break + } + tags := make(map[string]graph.Value) + it.TagResults(tags) + out <- &tags + nResults++ + if nResults > limit && limit != -1 { + break + } + for it.NextResult() == true { + tags := make(map[string]graph.Value) + it.TagResults(tags) + out <- &tags + nResults++ + if nResults > limit && limit != -1 { + break + } + } + } + close(out) +} + +func (s *Session) ToText(result interface{}) string { + out := fmt.Sprintln("****") + tags := result.(map[string]graph.Value) + tagKeys := make([]string, len(tags)) + i := 0 + for k := range tags { + tagKeys[i] = k + i++ + } + sort.Strings(tagKeys) + for _, k := range tagKeys { + if k == "$_" { + continue + } + out += fmt.Sprintf("%s : %s\n", k, s.ts.NameOf(tags[k])) + } + return out +} From d48d133cf1942c33ab8de189c7d9cb0a8afcb210 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 09:42:56 +0930 Subject: [PATCH 8/8] Only run short tests on travis Long tests are potentially very long, and may timeout internally. So don't run them. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index da28a3f..66af39d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,3 +19,6 @@ install: - go get github.com/syndtr/goleveldb/leveldb/util - go get gopkg.in/mgo.v2 - go get gopkg.in/mgo.v2/bson + +script: go test -v -short ./... +