From 09943c3eb63d5bf385d92c5d961c1a8c1bfc14a2 Mon Sep 17 00:00:00 2001 From: kortschak Date: Thu, 31 Jul 2014 09:36:43 +0930 Subject: [PATCH] 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 +}