From c66d6d399418b454c3a262ebad2110039fe0fcdf Mon Sep 17 00:00:00 2001 From: kortschak Date: Sat, 28 Jun 2014 01:43:13 +0930 Subject: [PATCH] Move query language packages into query --- gremlin/gremlin_build_iterator.go | 315 ------------------------------- gremlin/gremlin_env.go | 95 ---------- gremlin/gremlin_finals.go | 274 --------------------------- gremlin/gremlin_functional_test.go | 230 ---------------------- gremlin/gremlin_session.go | 266 -------------------------- gremlin/gremlin_test.nt | 11 -- gremlin/gremlin_traversals.go | 184 ------------------ http/cayley_http_query.go | 4 +- mql/mql_build_iterator.go | 181 ------------------ mql/mql_fill.go | 114 ----------- mql/mql_functional_test.go | 264 -------------------------- mql/mql_query.go | 111 ----------- mql/mql_session.go | 144 -------------- query/gremlin/gremlin_build_iterator.go | 315 +++++++++++++++++++++++++++++++ query/gremlin/gremlin_env.go | 95 ++++++++++ query/gremlin/gremlin_finals.go | 274 +++++++++++++++++++++++++++ query/gremlin/gremlin_functional_test.go | 230 ++++++++++++++++++++++ query/gremlin/gremlin_session.go | 266 ++++++++++++++++++++++++++ query/gremlin/gremlin_test.nt | 11 ++ query/gremlin/gremlin_traversals.go | 184 ++++++++++++++++++ query/mql/mql_build_iterator.go | 181 ++++++++++++++++++ query/mql/mql_fill.go | 114 +++++++++++ query/mql/mql_functional_test.go | 264 ++++++++++++++++++++++++++ query/mql/mql_query.go | 111 +++++++++++ query/mql/mql_session.go | 144 ++++++++++++++ repl.go | 4 +- 26 files changed, 2193 insertions(+), 2193 deletions(-) delete mode 100644 gremlin/gremlin_build_iterator.go delete mode 100644 gremlin/gremlin_env.go delete mode 100644 gremlin/gremlin_finals.go delete mode 100644 gremlin/gremlin_functional_test.go delete mode 100644 gremlin/gremlin_session.go delete mode 100644 gremlin/gremlin_test.nt delete mode 100644 gremlin/gremlin_traversals.go delete mode 100644 mql/mql_build_iterator.go delete mode 100644 mql/mql_fill.go delete mode 100644 mql/mql_functional_test.go delete mode 100644 mql/mql_query.go delete mode 100644 mql/mql_session.go create mode 100644 query/gremlin/gremlin_build_iterator.go create mode 100644 query/gremlin/gremlin_env.go create mode 100644 query/gremlin/gremlin_finals.go create mode 100644 query/gremlin/gremlin_functional_test.go create mode 100644 query/gremlin/gremlin_session.go create mode 100644 query/gremlin/gremlin_test.nt create mode 100644 query/gremlin/gremlin_traversals.go create mode 100644 query/mql/mql_build_iterator.go create mode 100644 query/mql/mql_fill.go create mode 100644 query/mql/mql_functional_test.go create mode 100644 query/mql/mql_query.go create mode 100644 query/mql/mql_session.go diff --git a/gremlin/gremlin_build_iterator.go b/gremlin/gremlin_build_iterator.go deleted file mode 100644 index b6e9a45..0000000 --- a/gremlin/gremlin_build_iterator.go +++ /dev/null @@ -1,315 +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 gremlin - -import ( - "strconv" - - "github.com/barakmich/glog" - "github.com/robertkrimen/otto" - - "github.com/google/cayley/graph" -) - -func getStrings(obj *otto.Object, field string) []string { - strings := make([]string, 0) - val, _ := obj.Get(field) - if !val.IsUndefined() { - export, _ := val.Export() - array := export.([]interface{}) - for _, arg := range array { - strings = append(strings, arg.(string)) - } - } - return strings -} - -func getStringArgs(obj *otto.Object) []string { return getStrings(obj, "string_args") } - -func buildIteratorTree(obj *otto.Object, ts graph.TripleStore) graph.Iterator { - if !isVertexChain(obj) { - return graph.NewNullIterator() - } - return buildIteratorTreeHelper(obj, ts, graph.NewNullIterator()) -} - -func makeListOfStringsFromArrayValue(obj *otto.Object) []string { - var output []string - lengthValue, _ := obj.Get("length") - length, _ := lengthValue.ToInteger() - ulength := uint32(length) - for index := uint32(0); index < ulength; index += 1 { - name := strconv.FormatInt(int64(index), 10) - value, err := obj.Get(name) - if err != nil { - continue - } - if !value.IsString() { - continue - } - s, _ := value.ToString() - output = append(output, s) - } - return output -} - -func buildIteratorFromValue(val otto.Value, ts graph.TripleStore) graph.Iterator { - if val.IsNull() || val.IsUndefined() { - return ts.GetNodesAllIterator() - } - if val.IsPrimitive() { - thing, _ := val.Export() - switch v := thing.(type) { - case string: - it := ts.MakeFixed() - it.AddValue(ts.GetIdFor(v)) - return it - default: - glog.Errorln("Trying to build unknown primitive value.") - } - } - switch val.Class() { - case "Object": - return buildIteratorTree(val.Object(), ts) - case "Array": - // Had better be an array of strings - strings := makeListOfStringsFromArrayValue(val.Object()) - it := ts.MakeFixed() - for _, x := range strings { - it.AddValue(ts.GetIdFor(x)) - } - return it - case "Number": - fallthrough - case "Boolean": - fallthrough - case "Date": - fallthrough - case "String": - it := ts.MakeFixed() - str, _ := val.ToString() - it.AddValue(ts.GetIdFor(str)) - return it - default: - glog.Errorln("Trying to handle unsupported Javascript value.") - return graph.NewNullIterator() - } -} - -func buildInOutIterator(obj *otto.Object, ts graph.TripleStore, base graph.Iterator, isReverse bool) graph.Iterator { - argList, _ := obj.Get("_gremlin_values") - if argList.Class() != "GoArray" { - glog.Errorln("How is arglist not an array? Return nothing.", argList.Class()) - return graph.NewNullIterator() - } - argArray := argList.Object() - lengthVal, _ := argArray.Get("length") - length, _ := lengthVal.ToInteger() - var predicateNodeIterator graph.Iterator - if length == 0 { - predicateNodeIterator = ts.GetNodesAllIterator() - } else { - zero, _ := argArray.Get("0") - predicateNodeIterator = buildIteratorFromValue(zero, ts) - } - if length >= 2 { - var tags []string - one, _ := argArray.Get("1") - if one.IsString() { - s, _ := one.ToString() - tags = append(tags, s) - } else if one.Class() == "Array" { - tags = makeListOfStringsFromArrayValue(one.Object()) - } - for _, tag := range tags { - predicateNodeIterator.AddTag(tag) - } - } - - in, out := "s", "o" - if isReverse { - in, out = out, in - } - lto := graph.NewLinksToIterator(ts, base, in) - and := graph.NewAndIterator() - and.AddSubIterator(graph.NewLinksToIterator(ts, predicateNodeIterator, "p")) - and.AddSubIterator(lto) - return graph.NewHasaIterator(ts, and, out) -} - -func buildIteratorTreeHelper(obj *otto.Object, ts graph.TripleStore, base graph.Iterator) graph.Iterator { - var it graph.Iterator - it = base - // TODO: Better error handling - kindVal, _ := obj.Get("_gremlin_type") - stringArgs := getStringArgs(obj) - var subIt graph.Iterator - prevVal, _ := obj.Get("_gremlin_prev") - if !prevVal.IsObject() { - subIt = base - } else { - subIt = buildIteratorTreeHelper(prevVal.Object(), ts, base) - } - - kind, _ := kindVal.ToString() - switch kind { - case "vertex": - if len(stringArgs) == 0 { - it = ts.GetNodesAllIterator() - } else { - fixed := ts.MakeFixed() - for _, name := range stringArgs { - fixed.AddValue(ts.GetIdFor(name)) - } - it = fixed - } - case "tag": - it = subIt - for _, tag := range stringArgs { - it.AddTag(tag) - } - case "save": - all := ts.GetNodesAllIterator() - if len(stringArgs) > 2 || len(stringArgs) == 0 { - return graph.NewNullIterator() - } - if len(stringArgs) == 2 { - all.AddTag(stringArgs[1]) - } else { - all.AddTag(stringArgs[0]) - } - predFixed := ts.MakeFixed() - predFixed.AddValue(ts.GetIdFor(stringArgs[0])) - subAnd := graph.NewAndIterator() - subAnd.AddSubIterator(graph.NewLinksToIterator(ts, predFixed, "p")) - subAnd.AddSubIterator(graph.NewLinksToIterator(ts, all, "o")) - hasa := graph.NewHasaIterator(ts, subAnd, "s") - and := graph.NewAndIterator() - and.AddSubIterator(hasa) - and.AddSubIterator(subIt) - it = and - case "saver": - all := ts.GetNodesAllIterator() - if len(stringArgs) > 2 || len(stringArgs) == 0 { - return graph.NewNullIterator() - } - if len(stringArgs) == 2 { - all.AddTag(stringArgs[1]) - } else { - all.AddTag(stringArgs[0]) - } - predFixed := ts.MakeFixed() - predFixed.AddValue(ts.GetIdFor(stringArgs[0])) - subAnd := graph.NewAndIterator() - subAnd.AddSubIterator(graph.NewLinksToIterator(ts, predFixed, "p")) - subAnd.AddSubIterator(graph.NewLinksToIterator(ts, all, "s")) - hasa := graph.NewHasaIterator(ts, subAnd, "o") - and := graph.NewAndIterator() - and.AddSubIterator(hasa) - and.AddSubIterator(subIt) - it = and - case "has": - fixed := ts.MakeFixed() - if len(stringArgs) < 2 { - return graph.NewNullIterator() - } - for _, name := range stringArgs[1:] { - fixed.AddValue(ts.GetIdFor(name)) - } - predFixed := ts.MakeFixed() - predFixed.AddValue(ts.GetIdFor(stringArgs[0])) - subAnd := graph.NewAndIterator() - subAnd.AddSubIterator(graph.NewLinksToIterator(ts, predFixed, "p")) - subAnd.AddSubIterator(graph.NewLinksToIterator(ts, fixed, "o")) - hasa := graph.NewHasaIterator(ts, subAnd, "s") - and := graph.NewAndIterator() - and.AddSubIterator(hasa) - and.AddSubIterator(subIt) - it = and - case "morphism": - it = base - case "and": - arg, _ := obj.Get("_gremlin_values") - firstArg, _ := arg.Object().Get("0") - if !isVertexChain(firstArg.Object()) { - return graph.NewNullIterator() - } - argIt := buildIteratorTree(firstArg.Object(), ts) - - and := graph.NewAndIterator() - and.AddSubIterator(subIt) - and.AddSubIterator(argIt) - it = and - case "back": - arg, _ := obj.Get("_gremlin_back_chain") - argIt := buildIteratorTree(arg.Object(), ts) - and := graph.NewAndIterator() - and.AddSubIterator(subIt) - and.AddSubIterator(argIt) - it = and - case "is": - fixed := ts.MakeFixed() - for _, name := range stringArgs { - fixed.AddValue(ts.GetIdFor(name)) - } - and := graph.NewAndIterator() - and.AddSubIterator(fixed) - and.AddSubIterator(subIt) - it = and - case "or": - arg, _ := obj.Get("_gremlin_values") - firstArg, _ := arg.Object().Get("0") - if !isVertexChain(firstArg.Object()) { - return graph.NewNullIterator() - } - argIt := buildIteratorTree(firstArg.Object(), ts) - - or := graph.NewOrIterator() - or.AddSubIterator(subIt) - or.AddSubIterator(argIt) - it = or - case "both": - // Hardly the most efficient pattern, but the most general. - // Worth looking into an Optimize() optimization here. - clone := subIt.Clone() - it1 := buildInOutIterator(obj, ts, subIt, false) - it2 := buildInOutIterator(obj, ts, clone, true) - - or := graph.NewOrIterator() - or.AddSubIterator(it1) - or.AddSubIterator(it2) - it = or - case "out": - it = buildInOutIterator(obj, ts, subIt, false) - case "follow": - // Follow a morphism - arg, _ := obj.Get("_gremlin_values") - firstArg, _ := arg.Object().Get("0") - if isVertexChain(firstArg.Object()) { - return graph.NewNullIterator() - } - it = buildIteratorTreeHelper(firstArg.Object(), ts, subIt) - case "followr": - // Follow a morphism - arg, _ := obj.Get("_gremlin_followr") - if isVertexChain(arg.Object()) { - return graph.NewNullIterator() - } - it = buildIteratorTreeHelper(arg.Object(), ts, subIt) - case "in": - it = buildInOutIterator(obj, ts, subIt, true) - } - return it -} diff --git a/gremlin/gremlin_env.go b/gremlin/gremlin_env.go deleted file mode 100644 index 4e7f332..0000000 --- a/gremlin/gremlin_env.go +++ /dev/null @@ -1,95 +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 gremlin - -// Builds a new Gremlin environment pointing at a session. - -import ( - "github.com/barakmich/glog" - "github.com/robertkrimen/otto" -) - -func BuildGremlinEnv(ses *GremlinSession) *otto.Otto { - env := otto.New() - setupGremlin(env, ses) - return env -} - -func concatStringArgs(call otto.FunctionCall) *[]interface{} { - outStrings := make([]interface{}, 0) - for _, arg := range call.ArgumentList { - if arg.IsString() { - outStrings = append(outStrings, arg.String()) - } - if arg.IsObject() && arg.Class() == "Array" { - obj, _ := arg.Export() - for _, x := range obj.([]interface{}) { - outStrings = append(outStrings, x.(string)) - } - } - } - return &outStrings -} - -func isVertexChain(obj *otto.Object) bool { - val, _ := obj.Get("_gremlin_type") - if x, _ := val.ToString(); x == "vertex" { - return true - } - val, _ = obj.Get("_gremlin_prev") - if val.IsObject() { - return isVertexChain(val.Object()) - } - return false -} - -func setupGremlin(env *otto.Otto, ses *GremlinSession) { - graph, _ := env.Object("graph = {}") - graph.Set("Vertex", func(call otto.FunctionCall) otto.Value { - call.Otto.Run("var out = {}") - out, err := call.Otto.Object("out") - if err != nil { - glog.Error(err.Error()) - return otto.TrueValue() - } - out.Set("_gremlin_type", "vertex") - outStrings := concatStringArgs(call) - if len(*outStrings) > 0 { - out.Set("string_args", *outStrings) - } - embedTraversals(env, ses, out) - embedFinals(env, ses, out) - return out.Value() - }) - - graph.Set("Morphism", func(call otto.FunctionCall) otto.Value { - call.Otto.Run("var out = {}") - out, _ := call.Otto.Object("out") - out.Set("_gremlin_type", "morphism") - embedTraversals(env, ses, out) - return out.Value() - }) - graph.Set("Emit", func(call otto.FunctionCall) otto.Value { - value := call.Argument(0) - if value.IsDefined() { - ses.SendResult(&GremlinResult{metaresult: false, err: "", val: &value, actualResults: nil}) - } - return otto.NullValue() - }) - env.Run("graph.V = graph.Vertex") - env.Run("graph.M = graph.Morphism") - env.Run("g = graph") - -} diff --git a/gremlin/gremlin_finals.go b/gremlin/gremlin_finals.go deleted file mode 100644 index f8c978d..0000000 --- a/gremlin/gremlin_finals.go +++ /dev/null @@ -1,274 +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 gremlin - -import ( - "github.com/barakmich/glog" - "github.com/robertkrimen/otto" - - "github.com/google/cayley/graph" -) - -const GremlinTopResultTag = "id" - -func embedFinals(env *otto.Otto, ses *GremlinSession, obj *otto.Object) { - obj.Set("All", allFunc(env, ses, obj)) - obj.Set("GetLimit", limitFunc(env, ses, obj)) - obj.Set("ToArray", toArrayFunc(env, ses, obj, false)) - obj.Set("ToValue", toValueFunc(env, ses, obj, false)) - obj.Set("TagArray", toArrayFunc(env, ses, obj, true)) - obj.Set("TagValue", toValueFunc(env, ses, obj, true)) - obj.Set("Map", mapFunc(env, ses, obj)) - obj.Set("ForEach", mapFunc(env, ses, obj)) -} - -func allFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - it := buildIteratorTree(obj, ses.ts) - it.AddTag(GremlinTopResultTag) - ses.limit = -1 - ses.count = 0 - runIteratorOnSession(it, ses) - return otto.NullValue() - } -} - -func limitFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - if len(call.ArgumentList) > 0 { - limitVal, _ := call.Argument(0).ToInteger() - it := buildIteratorTree(obj, ses.ts) - it.AddTag(GremlinTopResultTag) - ses.limit = int(limitVal) - ses.count = 0 - runIteratorOnSession(it, ses) - } - return otto.NullValue() - } -} - -func toArrayFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object, withTags bool) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - it := buildIteratorTree(obj, ses.ts) - it.AddTag(GremlinTopResultTag) - limit := -1 - if len(call.ArgumentList) > 0 { - limitParsed, _ := call.Argument(0).ToInteger() - limit = int(limitParsed) - } - var val otto.Value - var err error - if !withTags { - array := runIteratorToArrayNoTags(it, ses, limit) - val, err = call.Otto.ToValue(array) - } else { - array := runIteratorToArray(it, ses, limit) - val, err = call.Otto.ToValue(array) - } - - if err != nil { - glog.Error(err) - return otto.NullValue() - } - return val - } -} - -func toValueFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object, withTags bool) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - it := buildIteratorTree(obj, ses.ts) - it.AddTag(GremlinTopResultTag) - limit := 1 - var val otto.Value - var err error - if !withTags { - array := runIteratorToArrayNoTags(it, ses, limit) - if len(array) < 1 { - return otto.NullValue() - } - val, err = call.Otto.ToValue(array[0]) - } else { - array := runIteratorToArray(it, ses, limit) - if len(array) < 1 { - return otto.NullValue() - } - val, err = call.Otto.ToValue(array[0]) - } - if err != nil { - glog.Error(err) - return otto.NullValue() - } else { - return val - } - - } -} - -func mapFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - it := buildIteratorTree(obj, ses.ts) - it.AddTag(GremlinTopResultTag) - limit := -1 - if len(call.ArgumentList) == 0 { - return otto.NullValue() - } - callback := call.Argument(len(call.ArgumentList) - 1) - if len(call.ArgumentList) > 1 { - limitParsed, _ := call.Argument(0).ToInteger() - limit = int(limitParsed) - } - runIteratorWithCallback(it, ses, callback, call, limit) - return otto.NullValue() - } -} - -func tagsToValueMap(m map[string]graph.TSVal, ses *GremlinSession) map[string]string { - outputMap := make(map[string]string) - for k, v := range m { - outputMap[k] = ses.ts.GetNameFor(v) - } - return outputMap -} - -func runIteratorToArray(it graph.Iterator, ses *GremlinSession, limit int) []map[string]string { - output := make([]map[string]string, 0) - count := 0 - it, _ = it.Optimize() - for { - if ses.doHalt { - return nil - } - _, ok := it.Next() - if !ok { - break - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - output = append(output, tagsToValueMap(tags, ses)) - count++ - if limit >= 0 && count >= limit { - break - } - for it.NextResult() == true { - if ses.doHalt { - return nil - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - output = append(output, tagsToValueMap(tags, ses)) - count++ - if limit >= 0 && count >= limit { - break - } - } - } - it.Close() - return output -} - -func runIteratorToArrayNoTags(it graph.Iterator, ses *GremlinSession, limit int) []string { - output := make([]string, 0) - count := 0 - it, _ = it.Optimize() - for { - if ses.doHalt { - return nil - } - val, ok := it.Next() - if !ok { - break - } - output = append(output, ses.ts.GetNameFor(val)) - count++ - if limit >= 0 && count >= limit { - break - } - } - it.Close() - return output -} - -func runIteratorWithCallback(it graph.Iterator, ses *GremlinSession, callback otto.Value, this otto.FunctionCall, limit int) { - count := 0 - it, _ = it.Optimize() - for { - if ses.doHalt { - return - } - _, ok := it.Next() - if !ok { - break - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - val, _ := this.Otto.ToValue(tagsToValueMap(tags, ses)) - val, _ = callback.Call(this.This, val) - count++ - if limit >= 0 && count >= limit { - break - } - for it.NextResult() == true { - if ses.doHalt { - return - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - val, _ := this.Otto.ToValue(tagsToValueMap(tags, ses)) - val, _ = callback.Call(this.This, val) - count++ - if limit >= 0 && count >= limit { - break - } - } - } - it.Close() -} - -func runIteratorOnSession(it graph.Iterator, ses *GremlinSession) { - if ses.lookingForQueryShape { - graph.OutputQueryShapeForIterator(it, ses.ts, &(ses.queryShape)) - return - } - it, _ = it.Optimize() - glog.V(2).Infoln(it.DebugString(0)) - for { - // TODO(barakmich): Better halting. - if ses.doHalt { - return - } - _, ok := it.Next() - if !ok { - break - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - cont := ses.SendResult(&GremlinResult{metaresult: false, err: "", val: nil, actualResults: &tags}) - if !cont { - break - } - for it.NextResult() == true { - if ses.doHalt { - return - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - cont := ses.SendResult(&GremlinResult{metaresult: false, err: "", val: nil, actualResults: &tags}) - if !cont { - break - } - } - } - it.Close() -} diff --git a/gremlin/gremlin_functional_test.go b/gremlin/gremlin_functional_test.go deleted file mode 100644 index f6c65fb..0000000 --- a/gremlin/gremlin_functional_test.go +++ /dev/null @@ -1,230 +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 gremlin - -import ( - "sort" - "testing" - - . "github.com/smartystreets/goconvey/convey" - - "github.com/google/cayley/graph/memstore" -) - -// +---+ +---+ -// | A |------- ->| F |<-- -// +---+ \------>+---+-/ +---+ \--+---+ -// ------>|#B#| | | E | -// +---+-------/ >+---+ | +---+ -// | C | / v -// +---+ -/ +---+ -// ---- +---+/ |#G#| -// \-->|#D#|------------->+---+ -// +---+ -// - -func buildTripleStore() *GremlinSession { - ts := memstore.MakeTestingMemstore() - return NewGremlinSession(ts, -1, false) -} - -func shouldBeUnordered(actual interface{}, expected ...interface{}) string { - if len(expected) != 1 { - return "Only one list supported" - } - actualStr := actual.([]string) - expectedStr := expected[0].([]string) - sort.Strings(actualStr) - sort.Strings(expectedStr) - return ShouldResemble(actualStr, expectedStr) -} - -func runQueryGetTag(query string, tag string) ([]string, int) { - js := buildTripleStore() - output := make([]string, 0) - c := make(chan interface{}, 5) - js.ExecInput(query, c, -1) - count := 0 - for result := range c { - count++ - data := result.(*GremlinResult) - if data.val == nil { - val := (*data.actualResults)[tag] - if val != nil { - output = append(output, js.ts.GetNameFor(val)) - } - } - } - return output, count -} - -func ConveyQuery(doc string, query string, expected []string) { - ConveyQueryTag(doc, query, GremlinTopResultTag, expected) -} - -func ConveyQueryTag(doc string, query string, tag string, expected []string) { - Convey(doc, func() { - actual, _ := runQueryGetTag(query, tag) - So(actual, shouldBeUnordered, expected) - }) -} - -func TestGremlin(t *testing.T) { - Convey("With a default memtriplestore", t, func() { - - ConveyQuery("Can get a single vertex", - `g.V("A").All()`, - []string{"A"}) - - ConveyQuery("Can use .Out()", - `g.V("A").Out("follows").All()`, - []string{"B"}) - - ConveyQuery("Can use .In()", - `g.V("B").In("follows").All()`, - []string{"A", "C", "D"}) - - ConveyQuery("Can use .Both()", - `g.V("F").Both("follows").All()`, - []string{"B", "G", "E"}) - - ConveyQuery("Can use .Tag()-.Is()-.Back()", - `g.V("B").In("follows").Tag("foo").Out("status").Is("cool").Back("foo").All()`, - []string{"D"}) - - ConveyQuery("Can separate .Tag()-.Is()-.Back()", - ` - x = g.V("C").Out("follows").Tag("foo").Out("status").Is("cool").Back("foo") - x.In("follows").Is("D").Back("foo").All() - `, - []string{"B"}) - - Convey("Can do multiple .Back()s", func() { - query := ` - g.V("E").Out("follows").As("f").Out("follows").Out("status").Is("cool").Back("f").In("follows").In("follows").As("acd").Out("status").Is("cool").Back("f").All() - ` - expected := []string{"D"} - actual, _ := runQueryGetTag(query, "acd") - So(actual, shouldBeUnordered, expected) - }) - - }) -} - -func TestGremlinMorphism(t *testing.T) { - Convey("With a default memtriplestore", t, func() { - - ConveyQuery("Simple morphism works", - ` - grandfollows = g.M().Out("follows").Out("follows") - g.V("C").Follow(grandfollows).All() - `, - []string{"G", "F", "B"}) - - ConveyQuery("Reverse morphism works", - ` - grandfollows = g.M().Out("follows").Out("follows") - g.V("F").FollowR(grandfollows).All() - `, []string{"A", "C", "D"}) - - }) -} - -func TestGremlinIntersection(t *testing.T) { - Convey("With a default memtriplestore", t, func() { - ConveyQuery("Simple intersection", - ` - function follows(x) { return g.V(x).Out("follows") } - - follows("D").And(follows("C")).All() - `, []string{"B"}) - - ConveyQuery("Simple Morphism Intersection", - ` - grandfollows = g.M().Out("follows").Out("follows") - function gfollows(x) { return g.V(x).Follow(grandfollows) } - - gfollows("A").And(gfollows("C")).All() - `, []string{"F"}) - - ConveyQuery("Double Morphism Intersection", - ` - grandfollows = g.M().Out("follows").Out("follows") - function gfollows(x) { return g.V(x).Follow(grandfollows) } - - gfollows("E").And(gfollows("C")).And(gfollows("B")).All() - `, []string{"G"}) - - ConveyQuery("Reverse Intersection", - ` - grandfollows = g.M().Out("follows").Out("follows") - - g.V("G").FollowR(grandfollows).Intersect(g.V("F").FollowR(grandfollows)).All() - `, []string{"C"}) - - ConveyQuery("Standard sort of morphism intersection, continue follow", - ` - gfollowers = g.M().In("follows").In("follows") - function cool(x) { return g.V(x).As("a").Out("status").Is("cool").Back("a") } - cool("G").Follow(gfollowers).Intersect(cool("B").Follow(gfollowers)).All() - `, []string{"C"}) - - }) -} - -func TestGremlinHas(t *testing.T) { - Convey("With a default memtriplestore", t, func() { - ConveyQuery("Test a simple Has", - `g.V().Has("status", "cool").All()`, - []string{"G", "D", "B"}) - - ConveyQuery("Test a double Has", - `g.V().Has("status", "cool").Has("follows", "F").All()`, - []string{"B"}) - - }) -} - -func TestGremlinTag(t *testing.T) { - Convey("With a default memtriplestore", t, func() { - ConveyQueryTag("Test a simple save", - `g.V().Save("status", "somecool").All()`, - "somecool", - []string{"cool", "cool", "cool"}) - - ConveyQueryTag("Test a simple saveR", - `g.V("cool").SaveR("status", "who").All()`, - "who", - []string{"G", "D", "B"}) - - ConveyQueryTag("Test an out save", - `g.V("D").Out(null, "pred").All()`, - "pred", - []string{"follows", "follows", "status"}) - - ConveyQueryTag("Test a tag list", - `g.V("D").Out(null, ["pred", "foo", "bar"]).All()`, - "foo", - []string{"follows", "follows", "status"}) - - ConveyQuery("Test a pred list", - `g.V("D").Out(["follows", "status"]).All()`, - []string{"B", "G", "cool"}) - - ConveyQuery("Test a predicate path", - `g.V("D").Out(g.V("follows"), "pred").All()`, - []string{"B", "G"}) - }) -} diff --git a/gremlin/gremlin_session.go b/gremlin/gremlin_session.go deleted file mode 100644 index a0b0483..0000000 --- a/gremlin/gremlin_session.go +++ /dev/null @@ -1,266 +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 gremlin - -import ( - "errors" - "fmt" - "sort" - "time" - - "github.com/robertkrimen/otto" - - "github.com/google/cayley/graph" -) - -type GremlinSession struct { - ts graph.TripleStore - currentChannel chan interface{} - env *otto.Otto - debug bool - limit int - count int - dataOutput []interface{} - lookingForQueryShape bool - queryShape map[string]interface{} - err error - script *otto.Script - doHalt bool - timeoutSec time.Duration - emptyEnv *otto.Otto -} - -func NewGremlinSession(inputTripleStore graph.TripleStore, timeoutSec int, persist bool) *GremlinSession { - var g GremlinSession - g.ts = inputTripleStore - g.env = BuildGremlinEnv(&g) - g.limit = -1 - g.count = 0 - g.lookingForQueryShape = false - if persist { - g.emptyEnv = g.env - } - if timeoutSec < 0 { - g.timeoutSec = time.Duration(-1) - } else { - g.timeoutSec = time.Duration(timeoutSec) - } - g.ClearJson() - return &g -} - -type GremlinResult struct { - metaresult bool - err string - val *otto.Value - actualResults *map[string]graph.TSVal -} - -func (g *GremlinSession) ToggleDebug() { - g.debug = !g.debug -} - -func (g *GremlinSession) GetQuery(input string, output_struct chan map[string]interface{}) { - defer close(output_struct) - g.queryShape = make(map[string]interface{}) - g.lookingForQueryShape = true - g.env.Run(input) - output_struct <- g.queryShape - g.queryShape = nil -} - -func (g *GremlinSession) InputParses(input string) (graph.ParseResult, error) { - script, err := g.env.Compile("", input) - if err != nil { - return graph.ParseFail, err - } - g.script = script - return graph.Parsed, nil -} - -func (g *GremlinSession) SendResult(result *GremlinResult) bool { - if g.limit >= 0 && g.limit == g.count { - return false - } - if g.doHalt { - return false - } - if g.currentChannel != nil { - g.currentChannel <- result - g.count++ - if g.limit >= 0 && g.limit == g.count { - return false - } else { - return true - } - } - return false -} - -var halt = errors.New("Query Timeout") - -func (g *GremlinSession) runUnsafe(input interface{}) (otto.Value, error) { - g.doHalt = false - defer func() { - if caught := recover(); caught != nil { - if caught == halt { - g.err = halt - return - } - panic(caught) // Something else happened, repanic! - } - }() - - g.env.Interrupt = make(chan func(), 1) // The buffer prevents blocking - - if g.timeoutSec != -1 { - go func() { - time.Sleep(g.timeoutSec * time.Second) // Stop after two seconds - g.doHalt = true - if g.env != nil { - g.env.Interrupt <- func() { - panic(halt) - } - g.env = g.emptyEnv - } - }() - } - - return g.env.Run(input) // Here be dragons (risky code) -} - -func (g *GremlinSession) ExecInput(input string, out chan interface{}, limit int) { - defer close(out) - g.err = nil - g.currentChannel = out - var err error - var value otto.Value - if g.script == nil { - value, err = g.runUnsafe(input) - } else { - value, err = g.runUnsafe(g.script) - } - if err != nil { - out <- &GremlinResult{metaresult: true, - err: err.Error(), - val: &value, - actualResults: nil} - } else { - out <- &GremlinResult{metaresult: true, - err: "", - val: &value, - actualResults: nil} - } - g.currentChannel = nil - g.script = nil - g.env = g.emptyEnv - return -} - -func (s *GremlinSession) ToText(result interface{}) string { - data := result.(*GremlinResult) - if data.metaresult { - if data.err != "" { - return fmt.Sprintln("Error: ", data.err) - } - if data.val != nil { - s, _ := data.val.Export() - if data.val.IsObject() { - typeVal, _ := data.val.Object().Get("_gremlin_type") - if !typeVal.IsUndefined() { - s = "[internal Iterator]" - } - } - return fmt.Sprintln("=>", s) - } - return "" - } - var out string - out = fmt.Sprintln("****") - if data.val == nil { - tags := data.actualResults - 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.GetNameFor((*tags)[k])) - } - } else { - if data.val.IsObject() { - export, _ := data.val.Export() - mapExport := export.(map[string]string) - for k, v := range mapExport { - out += fmt.Sprintf("%s : %v\n", k, v) - } - } else { - strVersion, _ := data.val.ToString() - out += fmt.Sprintf("%s\n", strVersion) - } - } - return out -} - -// Web stuff -func (ses *GremlinSession) BuildJson(result interface{}) { - data := result.(*GremlinResult) - if !data.metaresult { - if data.val == nil { - obj := make(map[string]string) - tags := data.actualResults - tagKeys := make([]string, len(*tags)) - i := 0 - for k, _ := range *tags { - tagKeys[i] = k - i++ - } - sort.Strings(tagKeys) - for _, k := range tagKeys { - obj[k] = ses.ts.GetNameFor((*tags)[k]) - } - ses.dataOutput = append(ses.dataOutput, obj) - } else { - if data.val.IsObject() { - export, _ := data.val.Export() - ses.dataOutput = append(ses.dataOutput, export) - } else { - strVersion, _ := data.val.ToString() - ses.dataOutput = append(ses.dataOutput, strVersion) - } - } - } - -} - -func (ses *GremlinSession) GetJson() (interface{}, error) { - defer ses.ClearJson() - if ses.err != nil { - return nil, ses.err - } - if ses.doHalt { - return nil, halt - } - return ses.dataOutput, nil -} - -func (ses *GremlinSession) ClearJson() { - ses.dataOutput = nil -} diff --git a/gremlin/gremlin_test.nt b/gremlin/gremlin_test.nt deleted file mode 100644 index 3febca3..0000000 --- a/gremlin/gremlin_test.nt +++ /dev/null @@ -1,11 +0,0 @@ -A follows B . -C follows B . -C follows D . -D follows B . -B follows F . -F follows G . -D follows G . -E follows F . -B status cool . -D status cool . -G status cool . diff --git a/gremlin/gremlin_traversals.go b/gremlin/gremlin_traversals.go deleted file mode 100644 index c0f4704..0000000 --- a/gremlin/gremlin_traversals.go +++ /dev/null @@ -1,184 +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 gremlin - -// Adds special traversal functions to JS Gremlin objects. Most of these just build the chain of objects, and won't often need the session. - -import ( - "github.com/barakmich/glog" - "github.com/robertkrimen/otto" -) - -func embedTraversals(env *otto.Otto, ses *GremlinSession, obj *otto.Object) { - obj.Set("In", gremlinFunc("in", obj, env, ses)) - obj.Set("Out", gremlinFunc("out", obj, env, ses)) - obj.Set("Is", gremlinFunc("is", obj, env, ses)) - obj.Set("Both", gremlinFunc("both", obj, env, ses)) - obj.Set("Follow", gremlinFunc("follow", obj, env, ses)) - obj.Set("FollowR", gremlinFollowR("followr", obj, env, ses)) - obj.Set("And", gremlinFunc("and", obj, env, ses)) - obj.Set("Intersect", gremlinFunc("and", obj, env, ses)) - obj.Set("Union", gremlinFunc("or", obj, env, ses)) - obj.Set("Or", gremlinFunc("or", obj, env, ses)) - obj.Set("Back", gremlinBack("back", obj, env, ses)) - obj.Set("Tag", gremlinFunc("tag", obj, env, ses)) - obj.Set("As", gremlinFunc("tag", obj, env, ses)) - obj.Set("Has", gremlinFunc("has", obj, env, ses)) - obj.Set("Save", gremlinFunc("save", obj, env, ses)) - obj.Set("SaveR", gremlinFunc("saver", obj, env, ses)) -} - -func gremlinFunc(kind string, prevObj *otto.Object, env *otto.Otto, ses *GremlinSession) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - call.Otto.Run("var out = {}") - out, _ := call.Otto.Object("out") - out.Set("_gremlin_type", kind) - out.Set("_gremlin_values", call.ArgumentList) - out.Set("_gremlin_prev", prevObj) - outStrings := concatStringArgs(call) - if len(*outStrings) > 0 { - out.Set("string_args", *outStrings) - } - embedTraversals(env, ses, out) - if isVertexChain(call.This.Object()) { - embedFinals(env, ses, out) - } - return out.Value() - } -} - -func gremlinBack(kind string, prevObj *otto.Object, env *otto.Otto, ses *GremlinSession) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - call.Otto.Run("var out = {}") - out, _ := call.Otto.Object("out") - out.Set("_gremlin_type", kind) - out.Set("_gremlin_values", call.ArgumentList) - outStrings := concatStringArgs(call) - if len(*outStrings) > 0 { - out.Set("string_args", *outStrings) - } - var otherChain *otto.Object - var thisObj *otto.Object - if len(*outStrings) != 0 { - otherChain, thisObj = reverseGremlinChainTo(call.Otto, prevObj, (*outStrings)[0].(string)) - } else { - otherChain, thisObj = reverseGremlinChainTo(call.Otto, prevObj, "") - } - out.Set("_gremlin_prev", thisObj) - out.Set("_gremlin_back_chain", otherChain) - embedTraversals(env, ses, out) - if isVertexChain(call.This.Object()) { - embedFinals(env, ses, out) - } - return out.Value() - - } -} - -func gremlinFollowR(kind string, prevObj *otto.Object, env *otto.Otto, ses *GremlinSession) func(otto.FunctionCall) otto.Value { - return func(call otto.FunctionCall) otto.Value { - call.Otto.Run("var out = {}") - out, _ := call.Otto.Object("out") - out.Set("_gremlin_type", kind) - out.Set("_gremlin_values", call.ArgumentList) - outStrings := concatStringArgs(call) - if len(*outStrings) > 0 { - out.Set("string_args", *outStrings) - } - if len(call.ArgumentList) == 0 { - return prevObj.Value() - } - arg := call.Argument(0) - if isVertexChain(arg.Object()) { - return prevObj.Value() - } - newChain, _ := reverseGremlinChainTo(call.Otto, arg.Object(), "") - out.Set("_gremlin_prev", prevObj) - out.Set("_gremlin_followr", newChain) - embedTraversals(env, ses, out) - if isVertexChain(call.This.Object()) { - embedFinals(env, ses, out) - } - return out.Value() - - } -} - -func reverseGremlinChainTo(env *otto.Otto, prevObj *otto.Object, tag string) (*otto.Object, *otto.Object) { - env.Run("var _base_object = {}") - base, err := env.Object("_base_object") - if err != nil { - glog.Error(err) - return otto.NullValue().Object(), otto.NullValue().Object() - } - if isVertexChain(prevObj) { - base.Set("_gremlin_type", "vertex") - } else { - base.Set("_gremlin_type", "morphism") - } - return reverseGremlinChainHelper(env, prevObj, base, tag) -} - -func reverseGremlinChainHelper(env *otto.Otto, chain *otto.Object, newBase *otto.Object, tag string) (*otto.Object, *otto.Object) { - kindVal, _ := chain.Get("_gremlin_type") - kind, _ := kindVal.ToString() - - if tag != "" { - if kind == "tag" { - tags := getStringArgs(chain) - for _, t := range tags { - if t == tag { - return newBase, chain - } - } - } - } - - if kind == "morphism" || kind == "vertex" { - return newBase, chain - } - var newKind string - switch kind { - case "in": - newKind = "out" - case "out": - newKind = "in" - default: - newKind = kind - } - prev, _ := chain.Get("_gremlin_prev") - env.Run("var out = {}") - out, _ := env.Object("out") - out.Set("_gremlin_type", newKind) - values, _ := chain.Get("_gremlin_values") - out.Set("_gremlin_values", values) - back, _ := chain.Get("_gremlin_back_chain") - out.Set("_gremlin_back_chain", back) - out.Set("_gremlin_prev", newBase) - strings, _ := chain.Get("string_args") - out.Set("string_args", strings) - return reverseGremlinChainHelper(env, prev.Object(), out, tag) -} - -func debugChain(obj *otto.Object) bool { - val, _ := obj.Get("_gremlin_type") - x, _ := val.ToString() - glog.V(2).Infoln(x) - val, _ = obj.Get("_gremlin_prev") - if val.IsObject() { - return debugChain(val.Object()) - } - return false -} diff --git a/http/cayley_http_query.go b/http/cayley_http_query.go index 4a99dd0..8170e13 100644 --- a/http/cayley_http_query.go +++ b/http/cayley_http_query.go @@ -23,8 +23,8 @@ import ( "github.com/julienschmidt/httprouter" "github.com/google/cayley/graph" - "github.com/google/cayley/gremlin" - "github.com/google/cayley/mql" + "github.com/google/cayley/query/gremlin" + "github.com/google/cayley/query/mql" ) type SuccessQueryWrapper struct { diff --git a/mql/mql_build_iterator.go b/mql/mql_build_iterator.go deleted file mode 100644 index 6273696..0000000 --- a/mql/mql_build_iterator.go +++ /dev/null @@ -1,181 +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 mql - -import ( - "errors" - "fmt" - "log" - "math" - "strings" - - "github.com/google/cayley/graph" -) - -func (m *MqlQuery) buildFixed(s string) graph.Iterator { - f := m.ses.ts.MakeFixed() - f.AddValue(m.ses.ts.GetIdFor(s)) - return f -} - -func (m *MqlQuery) buildResultIterator(path MqlPath) graph.Iterator { - all := m.ses.ts.GetNodesAllIterator() - all.AddTag(string(path)) - return graph.NewOptionalIterator(all) -} - -func (m *MqlQuery) BuildIteratorTree(query interface{}) { - m.isRepeated = make(map[MqlPath]bool) - m.queryStructure = make(map[MqlPath]map[string]interface{}) - m.queryResult = make(map[MqlResultPath]map[string]interface{}) - m.queryResult[""] = make(map[string]interface{}) - - m.it, m.err = m.buildIteratorTreeInternal(query, NewMqlPath()) - if m.err != nil { - m.isError = true - } -} - -func (m *MqlQuery) buildIteratorTreeInternal(query interface{}, path MqlPath) (graph.Iterator, error) { - var it graph.Iterator - var err error - err = nil - switch t := query.(type) { - case bool: - // for JSON booleans - // Treat the bool as a string and call it a day. - // Things which are really bool-like are special cases and will be dealt with separately. - if t { - it = m.buildFixed("true") - } - it = m.buildFixed("false") - case float64: - // for JSON numbers - // Damn you, Javascript, and your lack of integer values. - if math.Floor(t) == t { - // Treat it like an integer. - it = m.buildFixed(fmt.Sprintf("%d", t)) - } else { - it = m.buildFixed(fmt.Sprintf("%f", t)) - } - case string: - // for JSON strings - it = m.buildFixed(t) - case []interface{}: - // for JSON arrays - m.isRepeated[path] = true - if len(t) == 0 { - it = m.buildResultIterator(path) - } else if len(t) == 1 { - it, err = m.buildIteratorTreeInternal(t[0], path) - } else { - err = errors.New(fmt.Sprintf("Multiple fields at location root%s", path.DisplayString())) - } - case map[string]interface{}: - // for JSON objects - it, err = m.buildIteratorTreeMapInternal(t, path) - case nil: - it = m.buildResultIterator(path) - default: - log.Fatal("Unknown JSON type?", query) - } - if err != nil { - return nil, err - } - it.AddTag(string(path)) - return it, nil -} - -func (m *MqlQuery) buildIteratorTreeMapInternal(query map[string]interface{}, path MqlPath) (graph.Iterator, error) { - it := graph.NewAndIterator() - it.AddSubIterator(m.ses.ts.GetNodesAllIterator()) - var err error - err = nil - outputStructure := make(map[string]interface{}) - for key, subquery := range query { - outputStructure[key] = nil - reverse := false - pred := key - if strings.HasPrefix(pred, "@") { - i := strings.Index(pred, ":") - if i != -1 { - pred = pred[(i + 1):] - } - } - if strings.HasPrefix(pred, "!") { - reverse = true - pred = strings.TrimPrefix(pred, "!") - } - - // Other special constructs here - var subit graph.Iterator - if key == "id" { - subit, err = m.buildIteratorTreeInternal(subquery, path.Follow(key)) - if err != nil { - return nil, err - } - it.AddSubIterator(subit) - } else { - subit, err = m.buildIteratorTreeInternal(subquery, path.Follow(key)) - if err != nil { - return nil, err - } - subAnd := graph.NewAndIterator() - predFixed := m.ses.ts.MakeFixed() - predFixed.AddValue(m.ses.ts.GetIdFor(pred)) - subAnd.AddSubIterator(graph.NewLinksToIterator(m.ses.ts, predFixed, "p")) - if reverse { - lto := graph.NewLinksToIterator(m.ses.ts, subit, "s") - subAnd.AddSubIterator(lto) - hasa := graph.NewHasaIterator(m.ses.ts, subAnd, "o") - it.AddSubIterator(hasa) - } else { - lto := graph.NewLinksToIterator(m.ses.ts, subit, "o") - subAnd.AddSubIterator(lto) - hasa := graph.NewHasaIterator(m.ses.ts, subAnd, "s") - it.AddSubIterator(hasa) - } - } - } - if err != nil { - return nil, err - } - m.queryStructure[path] = outputStructure - return it, nil -} - -type MqlResultPathSlice []MqlResultPath - -func (sl MqlResultPathSlice) Len() int { - return len(sl) -} - -func (sl MqlResultPathSlice) Less(i, j int) bool { - iLen := len(strings.Split(string(sl[i]), "\x30")) - jLen := len(strings.Split(string(sl[j]), "\x30")) - if iLen < jLen { - return true - } - if iLen == jLen { - if len(string(sl[i])) < len(string(sl[j])) { - return true - } - } - return false -} - -func (sl MqlResultPathSlice) Swap(i, j int) { - sl[i], sl[j] = sl[j], sl[i] -} diff --git a/mql/mql_fill.go b/mql/mql_fill.go deleted file mode 100644 index 26de32a..0000000 --- a/mql/mql_fill.go +++ /dev/null @@ -1,114 +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 mql - -import ( - "sort" - - "github.com/google/cayley/graph" -) - -func (m *MqlQuery) treeifyResult(tags map[string]graph.TSVal) map[MqlResultPath]string { - // Transform the map into something a little more interesting. - results := make(map[MqlPath]string) - for k, v := range tags { - results[MqlPath(k)] = m.ses.ts.GetNameFor(v) - } - resultPaths := make(map[MqlResultPath]string) - for k, v := range results { - resultPaths[k.ToResultPathFromMap(results)] = v - } - - var paths MqlResultPathSlice - - for path, _ := range resultPaths { - paths = append(paths, path) - } - - sort.Sort(paths) - - // Build Structure - for _, path := range paths { - currentPath := path.getPath() - value := resultPaths[path] - namePath := path.AppendValue(value) - if _, ok := m.queryResult[namePath]; !ok { - targetPath, key := path.splitLastPath() - if path == "" { - targetPath, key = "", value - if _, ok := m.queryResult[""][value]; !ok { - m.resultOrder = append(m.resultOrder, value) - } - } - if _, ok := m.queryStructure[currentPath]; ok { - // If there's substructure, then copy that in. - newStruct := m.copyPathStructure(currentPath) - if m.isRepeated[currentPath] && currentPath != "" { - switch t := m.queryResult[targetPath][key].(type) { - case nil: - x := make([]interface{}, 0) - x = append(x, newStruct) - m.queryResult[targetPath][key] = x - m.queryResult[namePath] = newStruct - case []interface{}: - m.queryResult[targetPath][key] = append(t, newStruct) - m.queryResult[namePath] = newStruct - } - - } else { - m.queryResult[namePath] = newStruct - m.queryResult[targetPath][key] = newStruct - } - } - } - } - - // Fill values - for _, path := range paths { - currentPath := path.getPath() - value := resultPaths[path] - namePath := path.AppendValue(value) - if _, ok := m.queryStructure[currentPath]; ok { - // We're dealing with ids. - if _, ok := m.queryResult[namePath]["id"]; ok { - m.queryResult[namePath]["id"] = value - } - } else { - // Just a value. - targetPath, key := path.splitLastPath() - if m.isRepeated[currentPath] { - switch t := m.queryResult[targetPath][key].(type) { - case nil: - x := make([]interface{}, 0) - x = append(x, value) - m.queryResult[targetPath][key] = x - case []interface{}: - m.queryResult[targetPath][key] = append(t, value) - } - - } else { - m.queryResult[targetPath][key] = value - } - } - } - - return resultPaths -} - -func (m *MqlQuery) buildResults() { - for _, v := range m.resultOrder { - m.results = append(m.results, m.queryResult[""][v]) - } -} diff --git a/mql/mql_functional_test.go b/mql/mql_functional_test.go deleted file mode 100644 index 97c2eac..0000000 --- a/mql/mql_functional_test.go +++ /dev/null @@ -1,264 +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 mql - -import ( - "encoding/json" - "testing" - - . "github.com/smartystreets/goconvey/convey" - - "github.com/google/cayley/graph/memstore" -) - -// +---+ +---+ -// | A |------- ->| F |<-- -// +---+ \------>+---+-/ +---+ \--+---+ -// ------>|#B#| | | E | -// +---+-------/ >+---+ | +---+ -// | C | / v -// +---+ -/ +---+ -// ---- +---+/ |#G#| -// \-->|#D#|------------->+---+ -// +---+ -// - -func buildTripleStore() *MqlSession { - ts := memstore.MakeTestingMemstore() - return NewMqlSession(ts) -} - -func compareJsonInterfaces(actual interface{}, expected interface{}, path MqlPath, t *testing.T) { - isError := false - switch ex := expected.(type) { - case bool: - switch ac := actual.(type) { - case bool: - if ac != ex { - isError = true - } - default: - t.Log("Mismatched type") - isError = true - } - case float64: - switch ac := actual.(type) { - case float64: - if ac != ex { - isError = true - } - default: - t.Log("Mismatched type") - isError = true - } - case string: - switch ac := actual.(type) { - case string: - if ac != ex { - isError = true - } - default: - isError = true - } - case []interface{}: - switch ac := actual.(type) { - case []interface{}: - if len(ac) != len(ex) { - t.Log("Different lengths") - isError = true - } else { - for i, elem := range ex { - compareJsonInterfaces(ac[i], elem, path.Follow(string(i)), t) - } - } - default: - t.Log("Mismatched type") - isError = true - } - case map[string]interface{}: - switch ac := actual.(type) { - case map[string]interface{}: - for k, v := range ex { - actual_value, ok := ac[k] - if !ok { - t.Log("Key", k, "not in actual output.") - isError = true - } else { - compareJsonInterfaces(actual_value, v, path.Follow(string(k)), t) - } - } - default: - t.Log("Mismatched type") - isError = true - } - case nil: - switch ac := actual.(type) { - case nil: - if ac != ex { - isError = true - } - default: - t.Log("Mismatched type") - isError = true - } - default: - t.Error("Unknown JSON type?", expected) - } - - if isError { - actual_bytes, _ := json.MarshalIndent(actual, "", " ") - expected_bytes, _ := json.MarshalIndent(expected, "", " ") - t.Error(path.DisplayString(), ":\n", string(actual_bytes), "\nexpected", string(expected_bytes)) - } -} - -func runAndTestQuery(query string, expected string, t *testing.T) { - ses := buildTripleStore() - c := make(chan interface{}, 5) - go ses.ExecInput(query, c, -1) - for result := range c { - ses.BuildJson(result) - } - actual_struct, _ := ses.GetJson() - var expected_struct interface{} - json.Unmarshal([]byte(expected), &expected_struct) - compareJsonInterfaces(actual_struct, expected_struct, NewMqlPath(), t) - ses.ClearJson() -} - -func TestGetAllIds(t *testing.T) { - Convey("Should get all IDs in the database", t, func() { - query := ` - [{"id": null}] - ` - expected := ` - [ - {"id": "A"}, - {"id": "follows"}, - {"id": "B"}, - {"id": "C"}, - {"id": "D"}, - {"id": "F"}, - {"id": "G"}, - {"id": "E"}, - {"id": "status"}, - {"id": "cool"}, - {"id": "status_graph"} - ] - ` - runAndTestQuery(query, expected, t) - }) -} - -func TestGetCool(t *testing.T) { - query := ` - [{"id": null, "status": "cool"}] - ` - expected := ` - [ - {"id": "B", "status": "cool"}, - {"id": "D", "status": "cool"}, - {"id": "G", "status": "cool"} - ] - ` - runAndTestQuery(query, expected, t) -} - -func TestGetFollowsList(t *testing.T) { - query := ` - [{"id": "C", "follows": []}] - ` - expected := ` - [{ - "id": "C", - "follows": [ - "B", "D" - ] - }] - ` - runAndTestQuery(query, expected, t) -} - -func TestGetFollowsStruct(t *testing.T) { - query := ` - [{"id": null, "follows": {"id": null, "status": "cool"}}] - ` - expected := ` - [ - {"id": "A", "follows": {"id": "B", "status": "cool"}}, - {"id": "C", "follows": {"id": "D", "status": "cool"}}, - {"id": "D", "follows": {"id": "G", "status": "cool"}}, - {"id": "F", "follows": {"id": "G", "status": "cool"}} - ] - ` - runAndTestQuery(query, expected, t) -} - -func TestGetFollowsReverseStructList(t *testing.T) { - query := ` - [{"id": null, "!follows": [{"id": null, "status" : "cool"}]}] - ` - expected := ` - [ - {"id": "F", "!follows": [{"id": "B", "status": "cool"}]}, - {"id": "B", "!follows": [{"id": "D", "status": "cool"}]}, - {"id": "G", "!follows": [{"id": "D", "status": "cool"}]} - ] - ` - runAndTestQuery(query, expected, t) -} - -func TestGetRevFollowsList(t *testing.T) { - query := ` - [{"id": "F", "!follows": []}] - ` - expected := ` - [{ - "id": "F", - "!follows": [ - "B", "E" - ] - }] - ` - runAndTestQuery(query, expected, t) -} - -func TestCoFollows(t *testing.T) { - query := ` - [{"id": null, "@A:follows": "B", "@B:follows": "D"}] - ` - expected := ` - [{ - "id": "C", - "@A:follows": "B", - "@B:follows": "D" - }] - ` - runAndTestQuery(query, expected, t) -} - -func TestRevCoFollows(t *testing.T) { - query := ` - [{"id": null, "!follows": {"id": "C"}, "@a:!follows": "D"}] - ` - expected := ` - [{ - "id": "B", - "!follows": {"id": "C"}, - "@a:!follows": "D" - }] - ` - runAndTestQuery(query, expected, t) -} diff --git a/mql/mql_query.go b/mql/mql_query.go deleted file mode 100644 index 66d8179..0000000 --- a/mql/mql_query.go +++ /dev/null @@ -1,111 +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 mql - -import ( - "fmt" - "strings" - - "github.com/google/cayley/graph" -) - -type MqlPath string -type MqlResultPath string - -type MqlQuery struct { - ses *MqlSession - it graph.Iterator - isRepeated map[MqlPath]bool - queryStructure map[MqlPath]map[string]interface{} - queryResult map[MqlResultPath]map[string]interface{} - results []interface{} - resultOrder []string - isError bool - err error -} - -func (mqlQuery *MqlQuery) copyPathStructure(path MqlPath) map[string]interface{} { - output := make(map[string]interface{}) - for k, v := range mqlQuery.queryStructure[path] { - output[k] = v - } - return output -} - -func NewMqlPath() MqlPath { - return "" -} -func (p MqlPath) Follow(s string) MqlPath { - return MqlPath(fmt.Sprintf("%s\x1E%s", p, s)) -} - -func (p MqlPath) DisplayString() string { - return strings.Replace(string(p), "\x1E", ".", -1) -} - -func NewMqlResultPath() MqlResultPath { - return "" -} - -func (p MqlResultPath) FollowPath(followPiece string, value string) MqlResultPath { - if string(p) == "" { - return MqlResultPath(fmt.Sprintf("%s\x1E%s", value, followPiece)) - } - return MqlResultPath(fmt.Sprintf("%s\x1E%s\x1E%s", p, value, followPiece)) -} - -func (p MqlResultPath) getPath() MqlPath { - out := NewMqlPath() - pathPieces := strings.Split(string(p), "\x1E") - for len(pathPieces) > 1 { - a := pathPieces[1] - pathPieces = pathPieces[2:] - out = out.Follow(a) - } - return out -} - -func (p MqlResultPath) splitLastPath() (MqlResultPath, string) { - pathPieces := strings.Split(string(p), "\x1E") - return MqlResultPath(strings.Join(pathPieces[:len(pathPieces)-1], "\x1E")), pathPieces[len(pathPieces)-1] -} - -func (p MqlResultPath) AppendValue(value string) MqlResultPath { - if string(p) == "" { - return MqlResultPath(value) - } - return MqlResultPath(fmt.Sprintf("%s\x1E%s", p, value)) -} - -func (p MqlPath) ToResultPathFromMap(resultMap map[MqlPath]string) MqlResultPath { - output := NewMqlResultPath() - pathPieces := strings.Split(string(p), "\x1E")[1:] - pathSoFar := NewMqlPath() - for _, piece := range pathPieces { - output = output.FollowPath(piece, resultMap[pathSoFar]) - pathSoFar = pathSoFar.Follow(piece) - } - return output -} - -func NewMqlQuery(ses *MqlSession) *MqlQuery { - var q MqlQuery - q.ses = ses - q.results = make([]interface{}, 0) - q.resultOrder = make([]string, 0) - q.err = nil - q.isError = false - return &q -} diff --git a/mql/mql_session.go b/mql/mql_session.go deleted file mode 100644 index 96d88bf..0000000 --- a/mql/mql_session.go +++ /dev/null @@ -1,144 +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 mql - -import ( - "encoding/json" - "fmt" - "sort" - - "github.com/barakmich/glog" - - "github.com/google/cayley/graph" -) - -type MqlSession struct { - ts graph.TripleStore - currentQuery *MqlQuery - debug bool -} - -func NewMqlSession(ts graph.TripleStore) *MqlSession { - var m MqlSession - m.ts = ts - return &m -} - -func (m *MqlSession) ToggleDebug() { - m.debug = !m.debug -} - -func (m *MqlSession) GetQuery(input string, output_struct chan map[string]interface{}) { - defer close(output_struct) - var mqlQuery interface{} - err := json.Unmarshal([]byte(input), &mqlQuery) - if err != nil { - return - } - m.currentQuery = NewMqlQuery(m) - m.currentQuery.BuildIteratorTree(mqlQuery) - output := make(map[string]interface{}) - graph.OutputQueryShapeForIterator(m.currentQuery.it, m.ts, &output) - nodes := output["nodes"].([]graph.Node) - new_nodes := make([]graph.Node, 0) - for _, n := range nodes { - n.Tags = nil - new_nodes = append(new_nodes, n) - } - output["nodes"] = new_nodes - output_struct <- output -} - -func (m *MqlSession) InputParses(input string) (graph.ParseResult, error) { - var x interface{} - err := json.Unmarshal([]byte(input), &x) - if err != nil { - return graph.ParseFail, err - } - return graph.Parsed, nil -} - -func (m *MqlSession) ExecInput(input string, c chan interface{}, limit int) { - defer close(c) - var mqlQuery interface{} - err := json.Unmarshal([]byte(input), &mqlQuery) - if err != nil { - return - } - m.currentQuery = NewMqlQuery(m) - m.currentQuery.BuildIteratorTree(mqlQuery) - if m.currentQuery.isError { - return - } - it, _ := m.currentQuery.it.Optimize() - if glog.V(2) { - glog.V(2).Infoln(it.DebugString(0)) - } - for { - _, ok := it.Next() - if !ok { - break - } - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - c <- &tags - for it.NextResult() == true { - tags := make(map[string]graph.TSVal) - it.TagResults(&tags) - c <- &tags - } - } -} - -func (m *MqlSession) ToText(result interface{}) string { - tags := *(result.(*map[string]graph.TSVal)) - out := fmt.Sprintln("****") - tagKeys := make([]string, len(tags)) - m.currentQuery.treeifyResult(tags) - m.currentQuery.buildResults() - r, _ := json.MarshalIndent(m.currentQuery.results, "", " ") - fmt.Println(string(r)) - i := 0 - for k, _ := range tags { - tagKeys[i] = string(k) - i++ - } - sort.Strings(tagKeys) - for _, k := range tagKeys { - if k == "$_" { - continue - } - out += fmt.Sprintf("%s : %s\n", k, m.ts.GetNameFor(tags[k])) - } - return out -} - -func (m *MqlSession) BuildJson(result interface{}) { - m.currentQuery.treeifyResult(*(result.(*map[string]graph.TSVal))) -} - -func (m *MqlSession) GetJson() (interface{}, error) { - m.currentQuery.buildResults() - if m.currentQuery.isError { - return nil, m.currentQuery.err - } else { - return m.currentQuery.results, nil - } -} - -func (m *MqlSession) ClearJson() { - // Since we create a new MqlQuery underneath every query, clearing isn't necessary. - return -} diff --git a/query/gremlin/gremlin_build_iterator.go b/query/gremlin/gremlin_build_iterator.go new file mode 100644 index 0000000..b6e9a45 --- /dev/null +++ b/query/gremlin/gremlin_build_iterator.go @@ -0,0 +1,315 @@ +// 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 gremlin + +import ( + "strconv" + + "github.com/barakmich/glog" + "github.com/robertkrimen/otto" + + "github.com/google/cayley/graph" +) + +func getStrings(obj *otto.Object, field string) []string { + strings := make([]string, 0) + val, _ := obj.Get(field) + if !val.IsUndefined() { + export, _ := val.Export() + array := export.([]interface{}) + for _, arg := range array { + strings = append(strings, arg.(string)) + } + } + return strings +} + +func getStringArgs(obj *otto.Object) []string { return getStrings(obj, "string_args") } + +func buildIteratorTree(obj *otto.Object, ts graph.TripleStore) graph.Iterator { + if !isVertexChain(obj) { + return graph.NewNullIterator() + } + return buildIteratorTreeHelper(obj, ts, graph.NewNullIterator()) +} + +func makeListOfStringsFromArrayValue(obj *otto.Object) []string { + var output []string + lengthValue, _ := obj.Get("length") + length, _ := lengthValue.ToInteger() + ulength := uint32(length) + for index := uint32(0); index < ulength; index += 1 { + name := strconv.FormatInt(int64(index), 10) + value, err := obj.Get(name) + if err != nil { + continue + } + if !value.IsString() { + continue + } + s, _ := value.ToString() + output = append(output, s) + } + return output +} + +func buildIteratorFromValue(val otto.Value, ts graph.TripleStore) graph.Iterator { + if val.IsNull() || val.IsUndefined() { + return ts.GetNodesAllIterator() + } + if val.IsPrimitive() { + thing, _ := val.Export() + switch v := thing.(type) { + case string: + it := ts.MakeFixed() + it.AddValue(ts.GetIdFor(v)) + return it + default: + glog.Errorln("Trying to build unknown primitive value.") + } + } + switch val.Class() { + case "Object": + return buildIteratorTree(val.Object(), ts) + case "Array": + // Had better be an array of strings + strings := makeListOfStringsFromArrayValue(val.Object()) + it := ts.MakeFixed() + for _, x := range strings { + it.AddValue(ts.GetIdFor(x)) + } + return it + case "Number": + fallthrough + case "Boolean": + fallthrough + case "Date": + fallthrough + case "String": + it := ts.MakeFixed() + str, _ := val.ToString() + it.AddValue(ts.GetIdFor(str)) + return it + default: + glog.Errorln("Trying to handle unsupported Javascript value.") + return graph.NewNullIterator() + } +} + +func buildInOutIterator(obj *otto.Object, ts graph.TripleStore, base graph.Iterator, isReverse bool) graph.Iterator { + argList, _ := obj.Get("_gremlin_values") + if argList.Class() != "GoArray" { + glog.Errorln("How is arglist not an array? Return nothing.", argList.Class()) + return graph.NewNullIterator() + } + argArray := argList.Object() + lengthVal, _ := argArray.Get("length") + length, _ := lengthVal.ToInteger() + var predicateNodeIterator graph.Iterator + if length == 0 { + predicateNodeIterator = ts.GetNodesAllIterator() + } else { + zero, _ := argArray.Get("0") + predicateNodeIterator = buildIteratorFromValue(zero, ts) + } + if length >= 2 { + var tags []string + one, _ := argArray.Get("1") + if one.IsString() { + s, _ := one.ToString() + tags = append(tags, s) + } else if one.Class() == "Array" { + tags = makeListOfStringsFromArrayValue(one.Object()) + } + for _, tag := range tags { + predicateNodeIterator.AddTag(tag) + } + } + + in, out := "s", "o" + if isReverse { + in, out = out, in + } + lto := graph.NewLinksToIterator(ts, base, in) + and := graph.NewAndIterator() + and.AddSubIterator(graph.NewLinksToIterator(ts, predicateNodeIterator, "p")) + and.AddSubIterator(lto) + return graph.NewHasaIterator(ts, and, out) +} + +func buildIteratorTreeHelper(obj *otto.Object, ts graph.TripleStore, base graph.Iterator) graph.Iterator { + var it graph.Iterator + it = base + // TODO: Better error handling + kindVal, _ := obj.Get("_gremlin_type") + stringArgs := getStringArgs(obj) + var subIt graph.Iterator + prevVal, _ := obj.Get("_gremlin_prev") + if !prevVal.IsObject() { + subIt = base + } else { + subIt = buildIteratorTreeHelper(prevVal.Object(), ts, base) + } + + kind, _ := kindVal.ToString() + switch kind { + case "vertex": + if len(stringArgs) == 0 { + it = ts.GetNodesAllIterator() + } else { + fixed := ts.MakeFixed() + for _, name := range stringArgs { + fixed.AddValue(ts.GetIdFor(name)) + } + it = fixed + } + case "tag": + it = subIt + for _, tag := range stringArgs { + it.AddTag(tag) + } + case "save": + all := ts.GetNodesAllIterator() + if len(stringArgs) > 2 || len(stringArgs) == 0 { + return graph.NewNullIterator() + } + if len(stringArgs) == 2 { + all.AddTag(stringArgs[1]) + } else { + all.AddTag(stringArgs[0]) + } + predFixed := ts.MakeFixed() + predFixed.AddValue(ts.GetIdFor(stringArgs[0])) + subAnd := graph.NewAndIterator() + subAnd.AddSubIterator(graph.NewLinksToIterator(ts, predFixed, "p")) + subAnd.AddSubIterator(graph.NewLinksToIterator(ts, all, "o")) + hasa := graph.NewHasaIterator(ts, subAnd, "s") + and := graph.NewAndIterator() + and.AddSubIterator(hasa) + and.AddSubIterator(subIt) + it = and + case "saver": + all := ts.GetNodesAllIterator() + if len(stringArgs) > 2 || len(stringArgs) == 0 { + return graph.NewNullIterator() + } + if len(stringArgs) == 2 { + all.AddTag(stringArgs[1]) + } else { + all.AddTag(stringArgs[0]) + } + predFixed := ts.MakeFixed() + predFixed.AddValue(ts.GetIdFor(stringArgs[0])) + subAnd := graph.NewAndIterator() + subAnd.AddSubIterator(graph.NewLinksToIterator(ts, predFixed, "p")) + subAnd.AddSubIterator(graph.NewLinksToIterator(ts, all, "s")) + hasa := graph.NewHasaIterator(ts, subAnd, "o") + and := graph.NewAndIterator() + and.AddSubIterator(hasa) + and.AddSubIterator(subIt) + it = and + case "has": + fixed := ts.MakeFixed() + if len(stringArgs) < 2 { + return graph.NewNullIterator() + } + for _, name := range stringArgs[1:] { + fixed.AddValue(ts.GetIdFor(name)) + } + predFixed := ts.MakeFixed() + predFixed.AddValue(ts.GetIdFor(stringArgs[0])) + subAnd := graph.NewAndIterator() + subAnd.AddSubIterator(graph.NewLinksToIterator(ts, predFixed, "p")) + subAnd.AddSubIterator(graph.NewLinksToIterator(ts, fixed, "o")) + hasa := graph.NewHasaIterator(ts, subAnd, "s") + and := graph.NewAndIterator() + and.AddSubIterator(hasa) + and.AddSubIterator(subIt) + it = and + case "morphism": + it = base + case "and": + arg, _ := obj.Get("_gremlin_values") + firstArg, _ := arg.Object().Get("0") + if !isVertexChain(firstArg.Object()) { + return graph.NewNullIterator() + } + argIt := buildIteratorTree(firstArg.Object(), ts) + + and := graph.NewAndIterator() + and.AddSubIterator(subIt) + and.AddSubIterator(argIt) + it = and + case "back": + arg, _ := obj.Get("_gremlin_back_chain") + argIt := buildIteratorTree(arg.Object(), ts) + and := graph.NewAndIterator() + and.AddSubIterator(subIt) + and.AddSubIterator(argIt) + it = and + case "is": + fixed := ts.MakeFixed() + for _, name := range stringArgs { + fixed.AddValue(ts.GetIdFor(name)) + } + and := graph.NewAndIterator() + and.AddSubIterator(fixed) + and.AddSubIterator(subIt) + it = and + case "or": + arg, _ := obj.Get("_gremlin_values") + firstArg, _ := arg.Object().Get("0") + if !isVertexChain(firstArg.Object()) { + return graph.NewNullIterator() + } + argIt := buildIteratorTree(firstArg.Object(), ts) + + or := graph.NewOrIterator() + or.AddSubIterator(subIt) + or.AddSubIterator(argIt) + it = or + case "both": + // Hardly the most efficient pattern, but the most general. + // Worth looking into an Optimize() optimization here. + clone := subIt.Clone() + it1 := buildInOutIterator(obj, ts, subIt, false) + it2 := buildInOutIterator(obj, ts, clone, true) + + or := graph.NewOrIterator() + or.AddSubIterator(it1) + or.AddSubIterator(it2) + it = or + case "out": + it = buildInOutIterator(obj, ts, subIt, false) + case "follow": + // Follow a morphism + arg, _ := obj.Get("_gremlin_values") + firstArg, _ := arg.Object().Get("0") + if isVertexChain(firstArg.Object()) { + return graph.NewNullIterator() + } + it = buildIteratorTreeHelper(firstArg.Object(), ts, subIt) + case "followr": + // Follow a morphism + arg, _ := obj.Get("_gremlin_followr") + if isVertexChain(arg.Object()) { + return graph.NewNullIterator() + } + it = buildIteratorTreeHelper(arg.Object(), ts, subIt) + case "in": + it = buildInOutIterator(obj, ts, subIt, true) + } + return it +} diff --git a/query/gremlin/gremlin_env.go b/query/gremlin/gremlin_env.go new file mode 100644 index 0000000..4e7f332 --- /dev/null +++ b/query/gremlin/gremlin_env.go @@ -0,0 +1,95 @@ +// 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 gremlin + +// Builds a new Gremlin environment pointing at a session. + +import ( + "github.com/barakmich/glog" + "github.com/robertkrimen/otto" +) + +func BuildGremlinEnv(ses *GremlinSession) *otto.Otto { + env := otto.New() + setupGremlin(env, ses) + return env +} + +func concatStringArgs(call otto.FunctionCall) *[]interface{} { + outStrings := make([]interface{}, 0) + for _, arg := range call.ArgumentList { + if arg.IsString() { + outStrings = append(outStrings, arg.String()) + } + if arg.IsObject() && arg.Class() == "Array" { + obj, _ := arg.Export() + for _, x := range obj.([]interface{}) { + outStrings = append(outStrings, x.(string)) + } + } + } + return &outStrings +} + +func isVertexChain(obj *otto.Object) bool { + val, _ := obj.Get("_gremlin_type") + if x, _ := val.ToString(); x == "vertex" { + return true + } + val, _ = obj.Get("_gremlin_prev") + if val.IsObject() { + return isVertexChain(val.Object()) + } + return false +} + +func setupGremlin(env *otto.Otto, ses *GremlinSession) { + graph, _ := env.Object("graph = {}") + graph.Set("Vertex", func(call otto.FunctionCall) otto.Value { + call.Otto.Run("var out = {}") + out, err := call.Otto.Object("out") + if err != nil { + glog.Error(err.Error()) + return otto.TrueValue() + } + out.Set("_gremlin_type", "vertex") + outStrings := concatStringArgs(call) + if len(*outStrings) > 0 { + out.Set("string_args", *outStrings) + } + embedTraversals(env, ses, out) + embedFinals(env, ses, out) + return out.Value() + }) + + graph.Set("Morphism", func(call otto.FunctionCall) otto.Value { + call.Otto.Run("var out = {}") + out, _ := call.Otto.Object("out") + out.Set("_gremlin_type", "morphism") + embedTraversals(env, ses, out) + return out.Value() + }) + graph.Set("Emit", func(call otto.FunctionCall) otto.Value { + value := call.Argument(0) + if value.IsDefined() { + ses.SendResult(&GremlinResult{metaresult: false, err: "", val: &value, actualResults: nil}) + } + return otto.NullValue() + }) + env.Run("graph.V = graph.Vertex") + env.Run("graph.M = graph.Morphism") + env.Run("g = graph") + +} diff --git a/query/gremlin/gremlin_finals.go b/query/gremlin/gremlin_finals.go new file mode 100644 index 0000000..f8c978d --- /dev/null +++ b/query/gremlin/gremlin_finals.go @@ -0,0 +1,274 @@ +// 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 gremlin + +import ( + "github.com/barakmich/glog" + "github.com/robertkrimen/otto" + + "github.com/google/cayley/graph" +) + +const GremlinTopResultTag = "id" + +func embedFinals(env *otto.Otto, ses *GremlinSession, obj *otto.Object) { + obj.Set("All", allFunc(env, ses, obj)) + obj.Set("GetLimit", limitFunc(env, ses, obj)) + obj.Set("ToArray", toArrayFunc(env, ses, obj, false)) + obj.Set("ToValue", toValueFunc(env, ses, obj, false)) + obj.Set("TagArray", toArrayFunc(env, ses, obj, true)) + obj.Set("TagValue", toValueFunc(env, ses, obj, true)) + obj.Set("Map", mapFunc(env, ses, obj)) + obj.Set("ForEach", mapFunc(env, ses, obj)) +} + +func allFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + it := buildIteratorTree(obj, ses.ts) + it.AddTag(GremlinTopResultTag) + ses.limit = -1 + ses.count = 0 + runIteratorOnSession(it, ses) + return otto.NullValue() + } +} + +func limitFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + if len(call.ArgumentList) > 0 { + limitVal, _ := call.Argument(0).ToInteger() + it := buildIteratorTree(obj, ses.ts) + it.AddTag(GremlinTopResultTag) + ses.limit = int(limitVal) + ses.count = 0 + runIteratorOnSession(it, ses) + } + return otto.NullValue() + } +} + +func toArrayFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object, withTags bool) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + it := buildIteratorTree(obj, ses.ts) + it.AddTag(GremlinTopResultTag) + limit := -1 + if len(call.ArgumentList) > 0 { + limitParsed, _ := call.Argument(0).ToInteger() + limit = int(limitParsed) + } + var val otto.Value + var err error + if !withTags { + array := runIteratorToArrayNoTags(it, ses, limit) + val, err = call.Otto.ToValue(array) + } else { + array := runIteratorToArray(it, ses, limit) + val, err = call.Otto.ToValue(array) + } + + if err != nil { + glog.Error(err) + return otto.NullValue() + } + return val + } +} + +func toValueFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object, withTags bool) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + it := buildIteratorTree(obj, ses.ts) + it.AddTag(GremlinTopResultTag) + limit := 1 + var val otto.Value + var err error + if !withTags { + array := runIteratorToArrayNoTags(it, ses, limit) + if len(array) < 1 { + return otto.NullValue() + } + val, err = call.Otto.ToValue(array[0]) + } else { + array := runIteratorToArray(it, ses, limit) + if len(array) < 1 { + return otto.NullValue() + } + val, err = call.Otto.ToValue(array[0]) + } + if err != nil { + glog.Error(err) + return otto.NullValue() + } else { + return val + } + + } +} + +func mapFunc(env *otto.Otto, ses *GremlinSession, obj *otto.Object) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + it := buildIteratorTree(obj, ses.ts) + it.AddTag(GremlinTopResultTag) + limit := -1 + if len(call.ArgumentList) == 0 { + return otto.NullValue() + } + callback := call.Argument(len(call.ArgumentList) - 1) + if len(call.ArgumentList) > 1 { + limitParsed, _ := call.Argument(0).ToInteger() + limit = int(limitParsed) + } + runIteratorWithCallback(it, ses, callback, call, limit) + return otto.NullValue() + } +} + +func tagsToValueMap(m map[string]graph.TSVal, ses *GremlinSession) map[string]string { + outputMap := make(map[string]string) + for k, v := range m { + outputMap[k] = ses.ts.GetNameFor(v) + } + return outputMap +} + +func runIteratorToArray(it graph.Iterator, ses *GremlinSession, limit int) []map[string]string { + output := make([]map[string]string, 0) + count := 0 + it, _ = it.Optimize() + for { + if ses.doHalt { + return nil + } + _, ok := it.Next() + if !ok { + break + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + output = append(output, tagsToValueMap(tags, ses)) + count++ + if limit >= 0 && count >= limit { + break + } + for it.NextResult() == true { + if ses.doHalt { + return nil + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + output = append(output, tagsToValueMap(tags, ses)) + count++ + if limit >= 0 && count >= limit { + break + } + } + } + it.Close() + return output +} + +func runIteratorToArrayNoTags(it graph.Iterator, ses *GremlinSession, limit int) []string { + output := make([]string, 0) + count := 0 + it, _ = it.Optimize() + for { + if ses.doHalt { + return nil + } + val, ok := it.Next() + if !ok { + break + } + output = append(output, ses.ts.GetNameFor(val)) + count++ + if limit >= 0 && count >= limit { + break + } + } + it.Close() + return output +} + +func runIteratorWithCallback(it graph.Iterator, ses *GremlinSession, callback otto.Value, this otto.FunctionCall, limit int) { + count := 0 + it, _ = it.Optimize() + for { + if ses.doHalt { + return + } + _, ok := it.Next() + if !ok { + break + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + val, _ := this.Otto.ToValue(tagsToValueMap(tags, ses)) + val, _ = callback.Call(this.This, val) + count++ + if limit >= 0 && count >= limit { + break + } + for it.NextResult() == true { + if ses.doHalt { + return + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + val, _ := this.Otto.ToValue(tagsToValueMap(tags, ses)) + val, _ = callback.Call(this.This, val) + count++ + if limit >= 0 && count >= limit { + break + } + } + } + it.Close() +} + +func runIteratorOnSession(it graph.Iterator, ses *GremlinSession) { + if ses.lookingForQueryShape { + graph.OutputQueryShapeForIterator(it, ses.ts, &(ses.queryShape)) + return + } + it, _ = it.Optimize() + glog.V(2).Infoln(it.DebugString(0)) + for { + // TODO(barakmich): Better halting. + if ses.doHalt { + return + } + _, ok := it.Next() + if !ok { + break + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + cont := ses.SendResult(&GremlinResult{metaresult: false, err: "", val: nil, actualResults: &tags}) + if !cont { + break + } + for it.NextResult() == true { + if ses.doHalt { + return + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + cont := ses.SendResult(&GremlinResult{metaresult: false, err: "", val: nil, actualResults: &tags}) + if !cont { + break + } + } + } + it.Close() +} diff --git a/query/gremlin/gremlin_functional_test.go b/query/gremlin/gremlin_functional_test.go new file mode 100644 index 0000000..f6c65fb --- /dev/null +++ b/query/gremlin/gremlin_functional_test.go @@ -0,0 +1,230 @@ +// 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 gremlin + +import ( + "sort" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/google/cayley/graph/memstore" +) + +// +---+ +---+ +// | A |------- ->| F |<-- +// +---+ \------>+---+-/ +---+ \--+---+ +// ------>|#B#| | | E | +// +---+-------/ >+---+ | +---+ +// | C | / v +// +---+ -/ +---+ +// ---- +---+/ |#G#| +// \-->|#D#|------------->+---+ +// +---+ +// + +func buildTripleStore() *GremlinSession { + ts := memstore.MakeTestingMemstore() + return NewGremlinSession(ts, -1, false) +} + +func shouldBeUnordered(actual interface{}, expected ...interface{}) string { + if len(expected) != 1 { + return "Only one list supported" + } + actualStr := actual.([]string) + expectedStr := expected[0].([]string) + sort.Strings(actualStr) + sort.Strings(expectedStr) + return ShouldResemble(actualStr, expectedStr) +} + +func runQueryGetTag(query string, tag string) ([]string, int) { + js := buildTripleStore() + output := make([]string, 0) + c := make(chan interface{}, 5) + js.ExecInput(query, c, -1) + count := 0 + for result := range c { + count++ + data := result.(*GremlinResult) + if data.val == nil { + val := (*data.actualResults)[tag] + if val != nil { + output = append(output, js.ts.GetNameFor(val)) + } + } + } + return output, count +} + +func ConveyQuery(doc string, query string, expected []string) { + ConveyQueryTag(doc, query, GremlinTopResultTag, expected) +} + +func ConveyQueryTag(doc string, query string, tag string, expected []string) { + Convey(doc, func() { + actual, _ := runQueryGetTag(query, tag) + So(actual, shouldBeUnordered, expected) + }) +} + +func TestGremlin(t *testing.T) { + Convey("With a default memtriplestore", t, func() { + + ConveyQuery("Can get a single vertex", + `g.V("A").All()`, + []string{"A"}) + + ConveyQuery("Can use .Out()", + `g.V("A").Out("follows").All()`, + []string{"B"}) + + ConveyQuery("Can use .In()", + `g.V("B").In("follows").All()`, + []string{"A", "C", "D"}) + + ConveyQuery("Can use .Both()", + `g.V("F").Both("follows").All()`, + []string{"B", "G", "E"}) + + ConveyQuery("Can use .Tag()-.Is()-.Back()", + `g.V("B").In("follows").Tag("foo").Out("status").Is("cool").Back("foo").All()`, + []string{"D"}) + + ConveyQuery("Can separate .Tag()-.Is()-.Back()", + ` + x = g.V("C").Out("follows").Tag("foo").Out("status").Is("cool").Back("foo") + x.In("follows").Is("D").Back("foo").All() + `, + []string{"B"}) + + Convey("Can do multiple .Back()s", func() { + query := ` + g.V("E").Out("follows").As("f").Out("follows").Out("status").Is("cool").Back("f").In("follows").In("follows").As("acd").Out("status").Is("cool").Back("f").All() + ` + expected := []string{"D"} + actual, _ := runQueryGetTag(query, "acd") + So(actual, shouldBeUnordered, expected) + }) + + }) +} + +func TestGremlinMorphism(t *testing.T) { + Convey("With a default memtriplestore", t, func() { + + ConveyQuery("Simple morphism works", + ` + grandfollows = g.M().Out("follows").Out("follows") + g.V("C").Follow(grandfollows).All() + `, + []string{"G", "F", "B"}) + + ConveyQuery("Reverse morphism works", + ` + grandfollows = g.M().Out("follows").Out("follows") + g.V("F").FollowR(grandfollows).All() + `, []string{"A", "C", "D"}) + + }) +} + +func TestGremlinIntersection(t *testing.T) { + Convey("With a default memtriplestore", t, func() { + ConveyQuery("Simple intersection", + ` + function follows(x) { return g.V(x).Out("follows") } + + follows("D").And(follows("C")).All() + `, []string{"B"}) + + ConveyQuery("Simple Morphism Intersection", + ` + grandfollows = g.M().Out("follows").Out("follows") + function gfollows(x) { return g.V(x).Follow(grandfollows) } + + gfollows("A").And(gfollows("C")).All() + `, []string{"F"}) + + ConveyQuery("Double Morphism Intersection", + ` + grandfollows = g.M().Out("follows").Out("follows") + function gfollows(x) { return g.V(x).Follow(grandfollows) } + + gfollows("E").And(gfollows("C")).And(gfollows("B")).All() + `, []string{"G"}) + + ConveyQuery("Reverse Intersection", + ` + grandfollows = g.M().Out("follows").Out("follows") + + g.V("G").FollowR(grandfollows).Intersect(g.V("F").FollowR(grandfollows)).All() + `, []string{"C"}) + + ConveyQuery("Standard sort of morphism intersection, continue follow", + ` + gfollowers = g.M().In("follows").In("follows") + function cool(x) { return g.V(x).As("a").Out("status").Is("cool").Back("a") } + cool("G").Follow(gfollowers).Intersect(cool("B").Follow(gfollowers)).All() + `, []string{"C"}) + + }) +} + +func TestGremlinHas(t *testing.T) { + Convey("With a default memtriplestore", t, func() { + ConveyQuery("Test a simple Has", + `g.V().Has("status", "cool").All()`, + []string{"G", "D", "B"}) + + ConveyQuery("Test a double Has", + `g.V().Has("status", "cool").Has("follows", "F").All()`, + []string{"B"}) + + }) +} + +func TestGremlinTag(t *testing.T) { + Convey("With a default memtriplestore", t, func() { + ConveyQueryTag("Test a simple save", + `g.V().Save("status", "somecool").All()`, + "somecool", + []string{"cool", "cool", "cool"}) + + ConveyQueryTag("Test a simple saveR", + `g.V("cool").SaveR("status", "who").All()`, + "who", + []string{"G", "D", "B"}) + + ConveyQueryTag("Test an out save", + `g.V("D").Out(null, "pred").All()`, + "pred", + []string{"follows", "follows", "status"}) + + ConveyQueryTag("Test a tag list", + `g.V("D").Out(null, ["pred", "foo", "bar"]).All()`, + "foo", + []string{"follows", "follows", "status"}) + + ConveyQuery("Test a pred list", + `g.V("D").Out(["follows", "status"]).All()`, + []string{"B", "G", "cool"}) + + ConveyQuery("Test a predicate path", + `g.V("D").Out(g.V("follows"), "pred").All()`, + []string{"B", "G"}) + }) +} diff --git a/query/gremlin/gremlin_session.go b/query/gremlin/gremlin_session.go new file mode 100644 index 0000000..a0b0483 --- /dev/null +++ b/query/gremlin/gremlin_session.go @@ -0,0 +1,266 @@ +// 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 gremlin + +import ( + "errors" + "fmt" + "sort" + "time" + + "github.com/robertkrimen/otto" + + "github.com/google/cayley/graph" +) + +type GremlinSession struct { + ts graph.TripleStore + currentChannel chan interface{} + env *otto.Otto + debug bool + limit int + count int + dataOutput []interface{} + lookingForQueryShape bool + queryShape map[string]interface{} + err error + script *otto.Script + doHalt bool + timeoutSec time.Duration + emptyEnv *otto.Otto +} + +func NewGremlinSession(inputTripleStore graph.TripleStore, timeoutSec int, persist bool) *GremlinSession { + var g GremlinSession + g.ts = inputTripleStore + g.env = BuildGremlinEnv(&g) + g.limit = -1 + g.count = 0 + g.lookingForQueryShape = false + if persist { + g.emptyEnv = g.env + } + if timeoutSec < 0 { + g.timeoutSec = time.Duration(-1) + } else { + g.timeoutSec = time.Duration(timeoutSec) + } + g.ClearJson() + return &g +} + +type GremlinResult struct { + metaresult bool + err string + val *otto.Value + actualResults *map[string]graph.TSVal +} + +func (g *GremlinSession) ToggleDebug() { + g.debug = !g.debug +} + +func (g *GremlinSession) GetQuery(input string, output_struct chan map[string]interface{}) { + defer close(output_struct) + g.queryShape = make(map[string]interface{}) + g.lookingForQueryShape = true + g.env.Run(input) + output_struct <- g.queryShape + g.queryShape = nil +} + +func (g *GremlinSession) InputParses(input string) (graph.ParseResult, error) { + script, err := g.env.Compile("", input) + if err != nil { + return graph.ParseFail, err + } + g.script = script + return graph.Parsed, nil +} + +func (g *GremlinSession) SendResult(result *GremlinResult) bool { + if g.limit >= 0 && g.limit == g.count { + return false + } + if g.doHalt { + return false + } + if g.currentChannel != nil { + g.currentChannel <- result + g.count++ + if g.limit >= 0 && g.limit == g.count { + return false + } else { + return true + } + } + return false +} + +var halt = errors.New("Query Timeout") + +func (g *GremlinSession) runUnsafe(input interface{}) (otto.Value, error) { + g.doHalt = false + defer func() { + if caught := recover(); caught != nil { + if caught == halt { + g.err = halt + return + } + panic(caught) // Something else happened, repanic! + } + }() + + g.env.Interrupt = make(chan func(), 1) // The buffer prevents blocking + + if g.timeoutSec != -1 { + go func() { + time.Sleep(g.timeoutSec * time.Second) // Stop after two seconds + g.doHalt = true + if g.env != nil { + g.env.Interrupt <- func() { + panic(halt) + } + g.env = g.emptyEnv + } + }() + } + + return g.env.Run(input) // Here be dragons (risky code) +} + +func (g *GremlinSession) ExecInput(input string, out chan interface{}, limit int) { + defer close(out) + g.err = nil + g.currentChannel = out + var err error + var value otto.Value + if g.script == nil { + value, err = g.runUnsafe(input) + } else { + value, err = g.runUnsafe(g.script) + } + if err != nil { + out <- &GremlinResult{metaresult: true, + err: err.Error(), + val: &value, + actualResults: nil} + } else { + out <- &GremlinResult{metaresult: true, + err: "", + val: &value, + actualResults: nil} + } + g.currentChannel = nil + g.script = nil + g.env = g.emptyEnv + return +} + +func (s *GremlinSession) ToText(result interface{}) string { + data := result.(*GremlinResult) + if data.metaresult { + if data.err != "" { + return fmt.Sprintln("Error: ", data.err) + } + if data.val != nil { + s, _ := data.val.Export() + if data.val.IsObject() { + typeVal, _ := data.val.Object().Get("_gremlin_type") + if !typeVal.IsUndefined() { + s = "[internal Iterator]" + } + } + return fmt.Sprintln("=>", s) + } + return "" + } + var out string + out = fmt.Sprintln("****") + if data.val == nil { + tags := data.actualResults + 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.GetNameFor((*tags)[k])) + } + } else { + if data.val.IsObject() { + export, _ := data.val.Export() + mapExport := export.(map[string]string) + for k, v := range mapExport { + out += fmt.Sprintf("%s : %v\n", k, v) + } + } else { + strVersion, _ := data.val.ToString() + out += fmt.Sprintf("%s\n", strVersion) + } + } + return out +} + +// Web stuff +func (ses *GremlinSession) BuildJson(result interface{}) { + data := result.(*GremlinResult) + if !data.metaresult { + if data.val == nil { + obj := make(map[string]string) + tags := data.actualResults + tagKeys := make([]string, len(*tags)) + i := 0 + for k, _ := range *tags { + tagKeys[i] = k + i++ + } + sort.Strings(tagKeys) + for _, k := range tagKeys { + obj[k] = ses.ts.GetNameFor((*tags)[k]) + } + ses.dataOutput = append(ses.dataOutput, obj) + } else { + if data.val.IsObject() { + export, _ := data.val.Export() + ses.dataOutput = append(ses.dataOutput, export) + } else { + strVersion, _ := data.val.ToString() + ses.dataOutput = append(ses.dataOutput, strVersion) + } + } + } + +} + +func (ses *GremlinSession) GetJson() (interface{}, error) { + defer ses.ClearJson() + if ses.err != nil { + return nil, ses.err + } + if ses.doHalt { + return nil, halt + } + return ses.dataOutput, nil +} + +func (ses *GremlinSession) ClearJson() { + ses.dataOutput = nil +} diff --git a/query/gremlin/gremlin_test.nt b/query/gremlin/gremlin_test.nt new file mode 100644 index 0000000..3febca3 --- /dev/null +++ b/query/gremlin/gremlin_test.nt @@ -0,0 +1,11 @@ +A follows B . +C follows B . +C follows D . +D follows B . +B follows F . +F follows G . +D follows G . +E follows F . +B status cool . +D status cool . +G status cool . diff --git a/query/gremlin/gremlin_traversals.go b/query/gremlin/gremlin_traversals.go new file mode 100644 index 0000000..c0f4704 --- /dev/null +++ b/query/gremlin/gremlin_traversals.go @@ -0,0 +1,184 @@ +// 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 gremlin + +// Adds special traversal functions to JS Gremlin objects. Most of these just build the chain of objects, and won't often need the session. + +import ( + "github.com/barakmich/glog" + "github.com/robertkrimen/otto" +) + +func embedTraversals(env *otto.Otto, ses *GremlinSession, obj *otto.Object) { + obj.Set("In", gremlinFunc("in", obj, env, ses)) + obj.Set("Out", gremlinFunc("out", obj, env, ses)) + obj.Set("Is", gremlinFunc("is", obj, env, ses)) + obj.Set("Both", gremlinFunc("both", obj, env, ses)) + obj.Set("Follow", gremlinFunc("follow", obj, env, ses)) + obj.Set("FollowR", gremlinFollowR("followr", obj, env, ses)) + obj.Set("And", gremlinFunc("and", obj, env, ses)) + obj.Set("Intersect", gremlinFunc("and", obj, env, ses)) + obj.Set("Union", gremlinFunc("or", obj, env, ses)) + obj.Set("Or", gremlinFunc("or", obj, env, ses)) + obj.Set("Back", gremlinBack("back", obj, env, ses)) + obj.Set("Tag", gremlinFunc("tag", obj, env, ses)) + obj.Set("As", gremlinFunc("tag", obj, env, ses)) + obj.Set("Has", gremlinFunc("has", obj, env, ses)) + obj.Set("Save", gremlinFunc("save", obj, env, ses)) + obj.Set("SaveR", gremlinFunc("saver", obj, env, ses)) +} + +func gremlinFunc(kind string, prevObj *otto.Object, env *otto.Otto, ses *GremlinSession) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + call.Otto.Run("var out = {}") + out, _ := call.Otto.Object("out") + out.Set("_gremlin_type", kind) + out.Set("_gremlin_values", call.ArgumentList) + out.Set("_gremlin_prev", prevObj) + outStrings := concatStringArgs(call) + if len(*outStrings) > 0 { + out.Set("string_args", *outStrings) + } + embedTraversals(env, ses, out) + if isVertexChain(call.This.Object()) { + embedFinals(env, ses, out) + } + return out.Value() + } +} + +func gremlinBack(kind string, prevObj *otto.Object, env *otto.Otto, ses *GremlinSession) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + call.Otto.Run("var out = {}") + out, _ := call.Otto.Object("out") + out.Set("_gremlin_type", kind) + out.Set("_gremlin_values", call.ArgumentList) + outStrings := concatStringArgs(call) + if len(*outStrings) > 0 { + out.Set("string_args", *outStrings) + } + var otherChain *otto.Object + var thisObj *otto.Object + if len(*outStrings) != 0 { + otherChain, thisObj = reverseGremlinChainTo(call.Otto, prevObj, (*outStrings)[0].(string)) + } else { + otherChain, thisObj = reverseGremlinChainTo(call.Otto, prevObj, "") + } + out.Set("_gremlin_prev", thisObj) + out.Set("_gremlin_back_chain", otherChain) + embedTraversals(env, ses, out) + if isVertexChain(call.This.Object()) { + embedFinals(env, ses, out) + } + return out.Value() + + } +} + +func gremlinFollowR(kind string, prevObj *otto.Object, env *otto.Otto, ses *GremlinSession) func(otto.FunctionCall) otto.Value { + return func(call otto.FunctionCall) otto.Value { + call.Otto.Run("var out = {}") + out, _ := call.Otto.Object("out") + out.Set("_gremlin_type", kind) + out.Set("_gremlin_values", call.ArgumentList) + outStrings := concatStringArgs(call) + if len(*outStrings) > 0 { + out.Set("string_args", *outStrings) + } + if len(call.ArgumentList) == 0 { + return prevObj.Value() + } + arg := call.Argument(0) + if isVertexChain(arg.Object()) { + return prevObj.Value() + } + newChain, _ := reverseGremlinChainTo(call.Otto, arg.Object(), "") + out.Set("_gremlin_prev", prevObj) + out.Set("_gremlin_followr", newChain) + embedTraversals(env, ses, out) + if isVertexChain(call.This.Object()) { + embedFinals(env, ses, out) + } + return out.Value() + + } +} + +func reverseGremlinChainTo(env *otto.Otto, prevObj *otto.Object, tag string) (*otto.Object, *otto.Object) { + env.Run("var _base_object = {}") + base, err := env.Object("_base_object") + if err != nil { + glog.Error(err) + return otto.NullValue().Object(), otto.NullValue().Object() + } + if isVertexChain(prevObj) { + base.Set("_gremlin_type", "vertex") + } else { + base.Set("_gremlin_type", "morphism") + } + return reverseGremlinChainHelper(env, prevObj, base, tag) +} + +func reverseGremlinChainHelper(env *otto.Otto, chain *otto.Object, newBase *otto.Object, tag string) (*otto.Object, *otto.Object) { + kindVal, _ := chain.Get("_gremlin_type") + kind, _ := kindVal.ToString() + + if tag != "" { + if kind == "tag" { + tags := getStringArgs(chain) + for _, t := range tags { + if t == tag { + return newBase, chain + } + } + } + } + + if kind == "morphism" || kind == "vertex" { + return newBase, chain + } + var newKind string + switch kind { + case "in": + newKind = "out" + case "out": + newKind = "in" + default: + newKind = kind + } + prev, _ := chain.Get("_gremlin_prev") + env.Run("var out = {}") + out, _ := env.Object("out") + out.Set("_gremlin_type", newKind) + values, _ := chain.Get("_gremlin_values") + out.Set("_gremlin_values", values) + back, _ := chain.Get("_gremlin_back_chain") + out.Set("_gremlin_back_chain", back) + out.Set("_gremlin_prev", newBase) + strings, _ := chain.Get("string_args") + out.Set("string_args", strings) + return reverseGremlinChainHelper(env, prev.Object(), out, tag) +} + +func debugChain(obj *otto.Object) bool { + val, _ := obj.Get("_gremlin_type") + x, _ := val.ToString() + glog.V(2).Infoln(x) + val, _ = obj.Get("_gremlin_prev") + if val.IsObject() { + return debugChain(val.Object()) + } + return false +} diff --git a/query/mql/mql_build_iterator.go b/query/mql/mql_build_iterator.go new file mode 100644 index 0000000..6273696 --- /dev/null +++ b/query/mql/mql_build_iterator.go @@ -0,0 +1,181 @@ +// 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 mql + +import ( + "errors" + "fmt" + "log" + "math" + "strings" + + "github.com/google/cayley/graph" +) + +func (m *MqlQuery) buildFixed(s string) graph.Iterator { + f := m.ses.ts.MakeFixed() + f.AddValue(m.ses.ts.GetIdFor(s)) + return f +} + +func (m *MqlQuery) buildResultIterator(path MqlPath) graph.Iterator { + all := m.ses.ts.GetNodesAllIterator() + all.AddTag(string(path)) + return graph.NewOptionalIterator(all) +} + +func (m *MqlQuery) BuildIteratorTree(query interface{}) { + m.isRepeated = make(map[MqlPath]bool) + m.queryStructure = make(map[MqlPath]map[string]interface{}) + m.queryResult = make(map[MqlResultPath]map[string]interface{}) + m.queryResult[""] = make(map[string]interface{}) + + m.it, m.err = m.buildIteratorTreeInternal(query, NewMqlPath()) + if m.err != nil { + m.isError = true + } +} + +func (m *MqlQuery) buildIteratorTreeInternal(query interface{}, path MqlPath) (graph.Iterator, error) { + var it graph.Iterator + var err error + err = nil + switch t := query.(type) { + case bool: + // for JSON booleans + // Treat the bool as a string and call it a day. + // Things which are really bool-like are special cases and will be dealt with separately. + if t { + it = m.buildFixed("true") + } + it = m.buildFixed("false") + case float64: + // for JSON numbers + // Damn you, Javascript, and your lack of integer values. + if math.Floor(t) == t { + // Treat it like an integer. + it = m.buildFixed(fmt.Sprintf("%d", t)) + } else { + it = m.buildFixed(fmt.Sprintf("%f", t)) + } + case string: + // for JSON strings + it = m.buildFixed(t) + case []interface{}: + // for JSON arrays + m.isRepeated[path] = true + if len(t) == 0 { + it = m.buildResultIterator(path) + } else if len(t) == 1 { + it, err = m.buildIteratorTreeInternal(t[0], path) + } else { + err = errors.New(fmt.Sprintf("Multiple fields at location root%s", path.DisplayString())) + } + case map[string]interface{}: + // for JSON objects + it, err = m.buildIteratorTreeMapInternal(t, path) + case nil: + it = m.buildResultIterator(path) + default: + log.Fatal("Unknown JSON type?", query) + } + if err != nil { + return nil, err + } + it.AddTag(string(path)) + return it, nil +} + +func (m *MqlQuery) buildIteratorTreeMapInternal(query map[string]interface{}, path MqlPath) (graph.Iterator, error) { + it := graph.NewAndIterator() + it.AddSubIterator(m.ses.ts.GetNodesAllIterator()) + var err error + err = nil + outputStructure := make(map[string]interface{}) + for key, subquery := range query { + outputStructure[key] = nil + reverse := false + pred := key + if strings.HasPrefix(pred, "@") { + i := strings.Index(pred, ":") + if i != -1 { + pred = pred[(i + 1):] + } + } + if strings.HasPrefix(pred, "!") { + reverse = true + pred = strings.TrimPrefix(pred, "!") + } + + // Other special constructs here + var subit graph.Iterator + if key == "id" { + subit, err = m.buildIteratorTreeInternal(subquery, path.Follow(key)) + if err != nil { + return nil, err + } + it.AddSubIterator(subit) + } else { + subit, err = m.buildIteratorTreeInternal(subquery, path.Follow(key)) + if err != nil { + return nil, err + } + subAnd := graph.NewAndIterator() + predFixed := m.ses.ts.MakeFixed() + predFixed.AddValue(m.ses.ts.GetIdFor(pred)) + subAnd.AddSubIterator(graph.NewLinksToIterator(m.ses.ts, predFixed, "p")) + if reverse { + lto := graph.NewLinksToIterator(m.ses.ts, subit, "s") + subAnd.AddSubIterator(lto) + hasa := graph.NewHasaIterator(m.ses.ts, subAnd, "o") + it.AddSubIterator(hasa) + } else { + lto := graph.NewLinksToIterator(m.ses.ts, subit, "o") + subAnd.AddSubIterator(lto) + hasa := graph.NewHasaIterator(m.ses.ts, subAnd, "s") + it.AddSubIterator(hasa) + } + } + } + if err != nil { + return nil, err + } + m.queryStructure[path] = outputStructure + return it, nil +} + +type MqlResultPathSlice []MqlResultPath + +func (sl MqlResultPathSlice) Len() int { + return len(sl) +} + +func (sl MqlResultPathSlice) Less(i, j int) bool { + iLen := len(strings.Split(string(sl[i]), "\x30")) + jLen := len(strings.Split(string(sl[j]), "\x30")) + if iLen < jLen { + return true + } + if iLen == jLen { + if len(string(sl[i])) < len(string(sl[j])) { + return true + } + } + return false +} + +func (sl MqlResultPathSlice) Swap(i, j int) { + sl[i], sl[j] = sl[j], sl[i] +} diff --git a/query/mql/mql_fill.go b/query/mql/mql_fill.go new file mode 100644 index 0000000..26de32a --- /dev/null +++ b/query/mql/mql_fill.go @@ -0,0 +1,114 @@ +// 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 mql + +import ( + "sort" + + "github.com/google/cayley/graph" +) + +func (m *MqlQuery) treeifyResult(tags map[string]graph.TSVal) map[MqlResultPath]string { + // Transform the map into something a little more interesting. + results := make(map[MqlPath]string) + for k, v := range tags { + results[MqlPath(k)] = m.ses.ts.GetNameFor(v) + } + resultPaths := make(map[MqlResultPath]string) + for k, v := range results { + resultPaths[k.ToResultPathFromMap(results)] = v + } + + var paths MqlResultPathSlice + + for path, _ := range resultPaths { + paths = append(paths, path) + } + + sort.Sort(paths) + + // Build Structure + for _, path := range paths { + currentPath := path.getPath() + value := resultPaths[path] + namePath := path.AppendValue(value) + if _, ok := m.queryResult[namePath]; !ok { + targetPath, key := path.splitLastPath() + if path == "" { + targetPath, key = "", value + if _, ok := m.queryResult[""][value]; !ok { + m.resultOrder = append(m.resultOrder, value) + } + } + if _, ok := m.queryStructure[currentPath]; ok { + // If there's substructure, then copy that in. + newStruct := m.copyPathStructure(currentPath) + if m.isRepeated[currentPath] && currentPath != "" { + switch t := m.queryResult[targetPath][key].(type) { + case nil: + x := make([]interface{}, 0) + x = append(x, newStruct) + m.queryResult[targetPath][key] = x + m.queryResult[namePath] = newStruct + case []interface{}: + m.queryResult[targetPath][key] = append(t, newStruct) + m.queryResult[namePath] = newStruct + } + + } else { + m.queryResult[namePath] = newStruct + m.queryResult[targetPath][key] = newStruct + } + } + } + } + + // Fill values + for _, path := range paths { + currentPath := path.getPath() + value := resultPaths[path] + namePath := path.AppendValue(value) + if _, ok := m.queryStructure[currentPath]; ok { + // We're dealing with ids. + if _, ok := m.queryResult[namePath]["id"]; ok { + m.queryResult[namePath]["id"] = value + } + } else { + // Just a value. + targetPath, key := path.splitLastPath() + if m.isRepeated[currentPath] { + switch t := m.queryResult[targetPath][key].(type) { + case nil: + x := make([]interface{}, 0) + x = append(x, value) + m.queryResult[targetPath][key] = x + case []interface{}: + m.queryResult[targetPath][key] = append(t, value) + } + + } else { + m.queryResult[targetPath][key] = value + } + } + } + + return resultPaths +} + +func (m *MqlQuery) buildResults() { + for _, v := range m.resultOrder { + m.results = append(m.results, m.queryResult[""][v]) + } +} diff --git a/query/mql/mql_functional_test.go b/query/mql/mql_functional_test.go new file mode 100644 index 0000000..97c2eac --- /dev/null +++ b/query/mql/mql_functional_test.go @@ -0,0 +1,264 @@ +// 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 mql + +import ( + "encoding/json" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/google/cayley/graph/memstore" +) + +// +---+ +---+ +// | A |------- ->| F |<-- +// +---+ \------>+---+-/ +---+ \--+---+ +// ------>|#B#| | | E | +// +---+-------/ >+---+ | +---+ +// | C | / v +// +---+ -/ +---+ +// ---- +---+/ |#G#| +// \-->|#D#|------------->+---+ +// +---+ +// + +func buildTripleStore() *MqlSession { + ts := memstore.MakeTestingMemstore() + return NewMqlSession(ts) +} + +func compareJsonInterfaces(actual interface{}, expected interface{}, path MqlPath, t *testing.T) { + isError := false + switch ex := expected.(type) { + case bool: + switch ac := actual.(type) { + case bool: + if ac != ex { + isError = true + } + default: + t.Log("Mismatched type") + isError = true + } + case float64: + switch ac := actual.(type) { + case float64: + if ac != ex { + isError = true + } + default: + t.Log("Mismatched type") + isError = true + } + case string: + switch ac := actual.(type) { + case string: + if ac != ex { + isError = true + } + default: + isError = true + } + case []interface{}: + switch ac := actual.(type) { + case []interface{}: + if len(ac) != len(ex) { + t.Log("Different lengths") + isError = true + } else { + for i, elem := range ex { + compareJsonInterfaces(ac[i], elem, path.Follow(string(i)), t) + } + } + default: + t.Log("Mismatched type") + isError = true + } + case map[string]interface{}: + switch ac := actual.(type) { + case map[string]interface{}: + for k, v := range ex { + actual_value, ok := ac[k] + if !ok { + t.Log("Key", k, "not in actual output.") + isError = true + } else { + compareJsonInterfaces(actual_value, v, path.Follow(string(k)), t) + } + } + default: + t.Log("Mismatched type") + isError = true + } + case nil: + switch ac := actual.(type) { + case nil: + if ac != ex { + isError = true + } + default: + t.Log("Mismatched type") + isError = true + } + default: + t.Error("Unknown JSON type?", expected) + } + + if isError { + actual_bytes, _ := json.MarshalIndent(actual, "", " ") + expected_bytes, _ := json.MarshalIndent(expected, "", " ") + t.Error(path.DisplayString(), ":\n", string(actual_bytes), "\nexpected", string(expected_bytes)) + } +} + +func runAndTestQuery(query string, expected string, t *testing.T) { + ses := buildTripleStore() + c := make(chan interface{}, 5) + go ses.ExecInput(query, c, -1) + for result := range c { + ses.BuildJson(result) + } + actual_struct, _ := ses.GetJson() + var expected_struct interface{} + json.Unmarshal([]byte(expected), &expected_struct) + compareJsonInterfaces(actual_struct, expected_struct, NewMqlPath(), t) + ses.ClearJson() +} + +func TestGetAllIds(t *testing.T) { + Convey("Should get all IDs in the database", t, func() { + query := ` + [{"id": null}] + ` + expected := ` + [ + {"id": "A"}, + {"id": "follows"}, + {"id": "B"}, + {"id": "C"}, + {"id": "D"}, + {"id": "F"}, + {"id": "G"}, + {"id": "E"}, + {"id": "status"}, + {"id": "cool"}, + {"id": "status_graph"} + ] + ` + runAndTestQuery(query, expected, t) + }) +} + +func TestGetCool(t *testing.T) { + query := ` + [{"id": null, "status": "cool"}] + ` + expected := ` + [ + {"id": "B", "status": "cool"}, + {"id": "D", "status": "cool"}, + {"id": "G", "status": "cool"} + ] + ` + runAndTestQuery(query, expected, t) +} + +func TestGetFollowsList(t *testing.T) { + query := ` + [{"id": "C", "follows": []}] + ` + expected := ` + [{ + "id": "C", + "follows": [ + "B", "D" + ] + }] + ` + runAndTestQuery(query, expected, t) +} + +func TestGetFollowsStruct(t *testing.T) { + query := ` + [{"id": null, "follows": {"id": null, "status": "cool"}}] + ` + expected := ` + [ + {"id": "A", "follows": {"id": "B", "status": "cool"}}, + {"id": "C", "follows": {"id": "D", "status": "cool"}}, + {"id": "D", "follows": {"id": "G", "status": "cool"}}, + {"id": "F", "follows": {"id": "G", "status": "cool"}} + ] + ` + runAndTestQuery(query, expected, t) +} + +func TestGetFollowsReverseStructList(t *testing.T) { + query := ` + [{"id": null, "!follows": [{"id": null, "status" : "cool"}]}] + ` + expected := ` + [ + {"id": "F", "!follows": [{"id": "B", "status": "cool"}]}, + {"id": "B", "!follows": [{"id": "D", "status": "cool"}]}, + {"id": "G", "!follows": [{"id": "D", "status": "cool"}]} + ] + ` + runAndTestQuery(query, expected, t) +} + +func TestGetRevFollowsList(t *testing.T) { + query := ` + [{"id": "F", "!follows": []}] + ` + expected := ` + [{ + "id": "F", + "!follows": [ + "B", "E" + ] + }] + ` + runAndTestQuery(query, expected, t) +} + +func TestCoFollows(t *testing.T) { + query := ` + [{"id": null, "@A:follows": "B", "@B:follows": "D"}] + ` + expected := ` + [{ + "id": "C", + "@A:follows": "B", + "@B:follows": "D" + }] + ` + runAndTestQuery(query, expected, t) +} + +func TestRevCoFollows(t *testing.T) { + query := ` + [{"id": null, "!follows": {"id": "C"}, "@a:!follows": "D"}] + ` + expected := ` + [{ + "id": "B", + "!follows": {"id": "C"}, + "@a:!follows": "D" + }] + ` + runAndTestQuery(query, expected, t) +} diff --git a/query/mql/mql_query.go b/query/mql/mql_query.go new file mode 100644 index 0000000..66d8179 --- /dev/null +++ b/query/mql/mql_query.go @@ -0,0 +1,111 @@ +// 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 mql + +import ( + "fmt" + "strings" + + "github.com/google/cayley/graph" +) + +type MqlPath string +type MqlResultPath string + +type MqlQuery struct { + ses *MqlSession + it graph.Iterator + isRepeated map[MqlPath]bool + queryStructure map[MqlPath]map[string]interface{} + queryResult map[MqlResultPath]map[string]interface{} + results []interface{} + resultOrder []string + isError bool + err error +} + +func (mqlQuery *MqlQuery) copyPathStructure(path MqlPath) map[string]interface{} { + output := make(map[string]interface{}) + for k, v := range mqlQuery.queryStructure[path] { + output[k] = v + } + return output +} + +func NewMqlPath() MqlPath { + return "" +} +func (p MqlPath) Follow(s string) MqlPath { + return MqlPath(fmt.Sprintf("%s\x1E%s", p, s)) +} + +func (p MqlPath) DisplayString() string { + return strings.Replace(string(p), "\x1E", ".", -1) +} + +func NewMqlResultPath() MqlResultPath { + return "" +} + +func (p MqlResultPath) FollowPath(followPiece string, value string) MqlResultPath { + if string(p) == "" { + return MqlResultPath(fmt.Sprintf("%s\x1E%s", value, followPiece)) + } + return MqlResultPath(fmt.Sprintf("%s\x1E%s\x1E%s", p, value, followPiece)) +} + +func (p MqlResultPath) getPath() MqlPath { + out := NewMqlPath() + pathPieces := strings.Split(string(p), "\x1E") + for len(pathPieces) > 1 { + a := pathPieces[1] + pathPieces = pathPieces[2:] + out = out.Follow(a) + } + return out +} + +func (p MqlResultPath) splitLastPath() (MqlResultPath, string) { + pathPieces := strings.Split(string(p), "\x1E") + return MqlResultPath(strings.Join(pathPieces[:len(pathPieces)-1], "\x1E")), pathPieces[len(pathPieces)-1] +} + +func (p MqlResultPath) AppendValue(value string) MqlResultPath { + if string(p) == "" { + return MqlResultPath(value) + } + return MqlResultPath(fmt.Sprintf("%s\x1E%s", p, value)) +} + +func (p MqlPath) ToResultPathFromMap(resultMap map[MqlPath]string) MqlResultPath { + output := NewMqlResultPath() + pathPieces := strings.Split(string(p), "\x1E")[1:] + pathSoFar := NewMqlPath() + for _, piece := range pathPieces { + output = output.FollowPath(piece, resultMap[pathSoFar]) + pathSoFar = pathSoFar.Follow(piece) + } + return output +} + +func NewMqlQuery(ses *MqlSession) *MqlQuery { + var q MqlQuery + q.ses = ses + q.results = make([]interface{}, 0) + q.resultOrder = make([]string, 0) + q.err = nil + q.isError = false + return &q +} diff --git a/query/mql/mql_session.go b/query/mql/mql_session.go new file mode 100644 index 0000000..96d88bf --- /dev/null +++ b/query/mql/mql_session.go @@ -0,0 +1,144 @@ +// 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 mql + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/barakmich/glog" + + "github.com/google/cayley/graph" +) + +type MqlSession struct { + ts graph.TripleStore + currentQuery *MqlQuery + debug bool +} + +func NewMqlSession(ts graph.TripleStore) *MqlSession { + var m MqlSession + m.ts = ts + return &m +} + +func (m *MqlSession) ToggleDebug() { + m.debug = !m.debug +} + +func (m *MqlSession) GetQuery(input string, output_struct chan map[string]interface{}) { + defer close(output_struct) + var mqlQuery interface{} + err := json.Unmarshal([]byte(input), &mqlQuery) + if err != nil { + return + } + m.currentQuery = NewMqlQuery(m) + m.currentQuery.BuildIteratorTree(mqlQuery) + output := make(map[string]interface{}) + graph.OutputQueryShapeForIterator(m.currentQuery.it, m.ts, &output) + nodes := output["nodes"].([]graph.Node) + new_nodes := make([]graph.Node, 0) + for _, n := range nodes { + n.Tags = nil + new_nodes = append(new_nodes, n) + } + output["nodes"] = new_nodes + output_struct <- output +} + +func (m *MqlSession) InputParses(input string) (graph.ParseResult, error) { + var x interface{} + err := json.Unmarshal([]byte(input), &x) + if err != nil { + return graph.ParseFail, err + } + return graph.Parsed, nil +} + +func (m *MqlSession) ExecInput(input string, c chan interface{}, limit int) { + defer close(c) + var mqlQuery interface{} + err := json.Unmarshal([]byte(input), &mqlQuery) + if err != nil { + return + } + m.currentQuery = NewMqlQuery(m) + m.currentQuery.BuildIteratorTree(mqlQuery) + if m.currentQuery.isError { + return + } + it, _ := m.currentQuery.it.Optimize() + if glog.V(2) { + glog.V(2).Infoln(it.DebugString(0)) + } + for { + _, ok := it.Next() + if !ok { + break + } + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + c <- &tags + for it.NextResult() == true { + tags := make(map[string]graph.TSVal) + it.TagResults(&tags) + c <- &tags + } + } +} + +func (m *MqlSession) ToText(result interface{}) string { + tags := *(result.(*map[string]graph.TSVal)) + out := fmt.Sprintln("****") + tagKeys := make([]string, len(tags)) + m.currentQuery.treeifyResult(tags) + m.currentQuery.buildResults() + r, _ := json.MarshalIndent(m.currentQuery.results, "", " ") + fmt.Println(string(r)) + i := 0 + for k, _ := range tags { + tagKeys[i] = string(k) + i++ + } + sort.Strings(tagKeys) + for _, k := range tagKeys { + if k == "$_" { + continue + } + out += fmt.Sprintf("%s : %s\n", k, m.ts.GetNameFor(tags[k])) + } + return out +} + +func (m *MqlSession) BuildJson(result interface{}) { + m.currentQuery.treeifyResult(*(result.(*map[string]graph.TSVal))) +} + +func (m *MqlSession) GetJson() (interface{}, error) { + m.currentQuery.buildResults() + if m.currentQuery.isError { + return nil, m.currentQuery.err + } else { + return m.currentQuery.results, nil + } +} + +func (m *MqlSession) ClearJson() { + // Since we create a new MqlQuery underneath every query, clearing isn't necessary. + return +} diff --git a/repl.go b/repl.go index 6db1ea2..62f3e5e 100644 --- a/repl.go +++ b/repl.go @@ -25,9 +25,9 @@ import ( "github.com/google/cayley/config" "github.com/google/cayley/graph" "github.com/google/cayley/graph/sexp" - "github.com/google/cayley/gremlin" - "github.com/google/cayley/mql" "github.com/google/cayley/nquads" + "github.com/google/cayley/query/gremlin" + "github.com/google/cayley/query/mql" ) func trace(s string) (string, time.Time) {