From 732c2ce358615264704bffef974e64022b5f3241 Mon Sep 17 00:00:00 2001 From: Dimitry Solovyov Date: Thu, 12 Jul 2018 15:59:15 +0300 Subject: [PATCH 1/5] Pass procedure context to AST parse Signed-off-by: Dimitry Solovyov --- .../gremlin/queries/ProcedureTest.java | 4 +-- .../server/op/cypher/CypherOpProcessor.java | 6 ++-- .../gremlin/translation/CypherAst.scala | 35 +++++++++++++------ .../ir/helpers/CypherAstAssert.scala | 4 +-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/testware/integration-tests/src/test/java/org/opencypher/gremlin/queries/ProcedureTest.java b/testware/integration-tests/src/test/java/org/opencypher/gremlin/queries/ProcedureTest.java index ae4b5180..6f6731a2 100644 --- a/testware/integration-tests/src/test/java/org/opencypher/gremlin/queries/ProcedureTest.java +++ b/testware/integration-tests/src/test/java/org/opencypher/gremlin/queries/ProcedureTest.java @@ -65,8 +65,8 @@ private List> submitAndGet(String cypher, Map ir = ast.translate(flavor, procedureContext); + CypherAst ast = CypherAst.parse(cypher, parameters, procedureContext); + Seq ir = ast.translate(flavor); GraphTraversal traversal = TranslationWriter.write(ir, translator, parameters); ReturnNormalizer returnNormalizer = ReturnNormalizer.create(ast.getReturnTypes()); return traversal.toStream() diff --git a/tinkerpop/cypher-gremlin-server-plugin/src/main/java/org/opencypher/gremlin/server/op/cypher/CypherOpProcessor.java b/tinkerpop/cypher-gremlin-server-plugin/src/main/java/org/opencypher/gremlin/server/op/cypher/CypherOpProcessor.java index 8096b085..45b86661 100644 --- a/tinkerpop/cypher-gremlin-server-plugin/src/main/java/org/opencypher/gremlin/server/op/cypher/CypherOpProcessor.java +++ b/tinkerpop/cypher-gremlin-server-plugin/src/main/java/org/opencypher/gremlin/server/op/cypher/CypherOpProcessor.java @@ -97,11 +97,11 @@ private void evalCypher(Context context) throws OpProcessorException { GraphTraversalSource gts = traversal(context); DefaultGraphTraversal g = new DefaultGraphTraversal(gts.clone()); Map parameters = ParameterNormalizer.normalize(getParameters(args)); - CypherAst ast = CypherAst.parse(cypher, parameters); - ProcedureContext procedureContext = ProcedureContext.global(); + CypherAst ast = CypherAst.parse(cypher, parameters, procedureContext); + TranslatorFlavor flavor = TranslatorFlavor.gremlinServer(); - Seq ir = ast.translate(flavor, procedureContext); + Seq ir = ast.translate(flavor); Translator stringTranslator = Translator.builder() .gremlinGroovy() diff --git a/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala b/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala index 618465bd..66a0809a 100644 --- a/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala +++ b/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala @@ -44,6 +44,7 @@ import scala.collection.mutable * * @param statement AST root node * @param parameters Cypher query parameters + * @param procedures registered procedure context * @param expressionTypes expression Cypher types * @param returnTypes return types by alias * @param options pre-parser options provided by Cypher parser @@ -51,6 +52,7 @@ import scala.collection.mutable class CypherAst private ( val statement: Statement, parameters: Map[String, Any], + procedures: ProcedureContext, expressionTypes: Map[Expression, CypherType], returnTypes: Map[String, CypherType], options: Seq[PreParserOption]) { @@ -58,11 +60,10 @@ class CypherAst private ( /** * Creates an intermediate representation of the translation. * - * @param flavor translation flavor - * @param procedures registered procedure context + * @param flavor translation flavor * @return to-Gremlin translation */ - def translate(flavor: TranslatorFlavor, procedures: ProcedureContext): Seq[GremlinStep] = { + def translate(flavor: TranslatorFlavor): Seq[GremlinStep] = { val dsl = Translator .builder() .custom( @@ -96,7 +97,7 @@ class CypherAst private ( * @return to-Gremlin translation */ def buildTranslation[T, P](dsl: Translator[T, P]): T = { - val ir = translate(dsl.flavor(), ProcedureContext.empty()) + val ir = translate(dsl.flavor()) TranslationWriter.write(ir, dsl, parameters) } @@ -168,7 +169,7 @@ object CypherAst { */ @throws[CypherException] def parse(queryText: String): CypherAst = { - parse(queryText, Collections.emptyMap[String, Any]()) + parse(queryText, Collections.emptyMap[String, Any](), ProcedureContext.empty()) } /** @@ -180,14 +181,27 @@ object CypherAst { */ @throws[CypherException] def parse(queryText: String, parameters: util.Map[String, _]): CypherAst = { + parse(queryText, parameters, ProcedureContext.empty()) + } + + /** + * Constructs a new Cypher AST from the provided query. + * + * @param queryText Cypher query + * @param parameters Cypher query parameters + * @param procedures registered procedure context + * @return Cypher AST wrapper + */ + @throws[CypherException] + def parse(queryText: String, parameters: util.Map[String, _], procedures: ProcedureContext): CypherAst = { val scalaParameters = Option(parameters) .map(_.asScala.toMap) .getOrElse(Map()) - parse(queryText, scalaParameters) + parse(queryText, scalaParameters, procedures) } @throws[CypherException] - private def parse(queryText: String, parameters: Map[String, Any]): CypherAst = { + private def parse(queryText: String, parameters: Map[String, Any], procedures: ProcedureContext): CypherAst = { val PreParsedStatement(preParsedQueryText, options, offset) = CypherPreParser(queryText) val startState = InitialState(preParsedQueryText, Some(offset), EmptyPlannerName) val state = CompilationPhases @@ -198,9 +212,9 @@ object CypherAst { val statement = state.statement() val expressionTypes = getExpressionTypes(state) - val returnTypes = getReturnTypes(expressionTypes, statement) + val returnTypes = getReturnTypes(expressionTypes, statement, procedures) - new CypherAst(statement, parameters, expressionTypes, returnTypes, options) + new CypherAst(statement, parameters, procedures, expressionTypes, returnTypes, options) } private def getExpressionTypes(state: BaseState): Map[Expression, CypherType] = { @@ -217,7 +231,8 @@ object CypherAst { private def getReturnTypes( expressionTypes: Map[Expression, CypherType], - statement: Statement): Map[String, CypherType] = { + statement: Statement, + procedures: ProcedureContext): Map[String, CypherType] = { val clauses = statement match { case Query(_, part) => part match { diff --git a/translation/src/test/scala/org/opencypher/gremlin/translation/ir/helpers/CypherAstAssert.scala b/translation/src/test/scala/org/opencypher/gremlin/translation/ir/helpers/CypherAstAssert.scala index d3d9e8f2..198ac19a 100644 --- a/translation/src/test/scala/org/opencypher/gremlin/translation/ir/helpers/CypherAstAssert.scala +++ b/translation/src/test/scala/org/opencypher/gremlin/translation/ir/helpers/CypherAstAssert.scala @@ -99,11 +99,11 @@ class CypherAstAssert( } private def actualTraversal = { - actual.translate(flavor, ProcedureContext.empty) + actual.translate(flavor) } private def rewriteTraversal = { Preconditions.checkNotNull(rewriter, "Rewriter not set! Use `CypherAstAssert.rewritingWith`") - actual.translate(flavor.extend(Seq(rewriter), Seq()), ProcedureContext.empty) + actual.translate(flavor.extend(Seq(rewriter), Seq())) } } From b97a10151a7e43044075dd923631676cf0e36798 Mon Sep 17 00:00:00 2001 From: Dimitry Solovyov Date: Thu, 12 Jul 2018 17:46:19 +0300 Subject: [PATCH 2/5] Get return types from procedure signature Signed-off-by: Dimitry Solovyov --- .../gremlin/extension/TestProcedures.java | 15 +++--- .../PredefinedProcedureRegistry.java | 18 +------ .../gremlin/extension/CypherBinding.java | 8 +-- .../gremlin/extension/CypherBindingType.java | 54 +++++++++++++++++++ .../gremlin/traversal/ProcedureContext.java | 17 +++--- .../gremlin/translation/CypherAst.scala | 46 +++++++++++++++- 6 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 translation/src/main/java/org/opencypher/gremlin/extension/CypherBindingType.java diff --git a/testware/integration-tests/src/test/java/org/opencypher/gremlin/extension/TestProcedures.java b/testware/integration-tests/src/test/java/org/opencypher/gremlin/extension/TestProcedures.java index 906e9a0b..759b206c 100644 --- a/testware/integration-tests/src/test/java/org/opencypher/gremlin/extension/TestProcedures.java +++ b/testware/integration-tests/src/test/java/org/opencypher/gremlin/extension/TestProcedures.java @@ -20,6 +20,9 @@ import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.opencypher.gremlin.extension.CypherBinding.binding; +import static org.opencypher.gremlin.extension.CypherBindingType.FLOAT; +import static org.opencypher.gremlin.extension.CypherBindingType.INTEGER; +import static org.opencypher.gremlin.extension.CypherBindingType.STRING; import static org.opencypher.gremlin.extension.CypherProcedure.cypherProcedure; import java.util.HashSet; @@ -36,7 +39,7 @@ public TestProcedures() { procedures.add(cypherProcedure( "test.getName", emptyList(), - singletonList(binding("name", String.class)), + singletonList(binding("name", STRING)), arguments -> asList( singletonMap("name", "marko"), singletonMap("name", "vadas") @@ -45,8 +48,8 @@ public TestProcedures() { procedures.add(cypherProcedure( "test.inc", - singletonList(binding("a", Long.class)), - singletonList(binding("r", Long.class)), + singletonList(binding("a", INTEGER)), + singletonList(binding("r", INTEGER)), arguments -> { long a = (long) arguments.get("a"); return singletonList(singletonMap("r", a + 1)); @@ -55,8 +58,8 @@ public TestProcedures() { procedures.add(cypherProcedure( "test.incF", - singletonList(binding("a", Double.class)), - singletonList(binding("r", Double.class)), + singletonList(binding("a", FLOAT)), + singletonList(binding("r", FLOAT)), arguments -> { double a = (double) arguments.get("a"); return singletonList(singletonMap("r", a + 1)); @@ -66,7 +69,7 @@ public TestProcedures() { procedures.add(cypherProcedure( "test.multi", emptyList(), - asList(binding("foo", String.class), binding("bar", String.class)), + asList(binding("foo", STRING), binding("bar", STRING)), arguments -> { Map row = new LinkedHashMap<>(); row.put("bar", "bar"); diff --git a/testware/tck/src/test/java/org/opencypher/gremlin/traversal/PredefinedProcedureRegistry.java b/testware/tck/src/test/java/org/opencypher/gremlin/traversal/PredefinedProcedureRegistry.java index decbf09f..5fb7d655 100644 --- a/testware/tck/src/test/java/org/opencypher/gremlin/traversal/PredefinedProcedureRegistry.java +++ b/testware/tck/src/test/java/org/opencypher/gremlin/traversal/PredefinedProcedureRegistry.java @@ -26,6 +26,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.opencypher.gremlin.extension.CypherBinding; +import org.opencypher.gremlin.extension.CypherBindingType; public final class PredefinedProcedureRegistry { private PredefinedProcedureRegistry() { @@ -72,27 +73,12 @@ private static List matchArguments(String input) { Matcher matcher = ARGUMENT_PATTERN.matcher(input); while (matcher.find()) { String name = matcher.group("name"); - Class type = typeFromSignature(matcher.group("type")); + CypherBindingType type = CypherBindingType.fromSignature(matcher.group("type")); arguments.add(binding(name, type)); } return arguments; } - private static Class typeFromSignature(String type) { - switch (type) { - case "FLOAT?": - return Double.class; - case "INTEGER?": - return Long.class; - case "NUMBER?": - return Number.class; - case "STRING?": - return String.class; - default: - throw new IllegalArgumentException("Unparsable procedure type: " + type); - } - } - private static Map extractKeys(List keys, Map src) { Map dest = new HashMap<>(); for (String key : keys) { diff --git a/translation/src/main/java/org/opencypher/gremlin/extension/CypherBinding.java b/translation/src/main/java/org/opencypher/gremlin/extension/CypherBinding.java index 55dc3384..7a205f51 100644 --- a/translation/src/main/java/org/opencypher/gremlin/extension/CypherBinding.java +++ b/translation/src/main/java/org/opencypher/gremlin/extension/CypherBinding.java @@ -17,14 +17,14 @@ public final class CypherBinding { private final String name; - private final Class type; + private final CypherBindingType type; - public CypherBinding(String name, Class type) { + public CypherBinding(String name, CypherBindingType type) { this.name = name; this.type = type; } - public static CypherBinding binding(String name, Class type) { + public static CypherBinding binding(String name, CypherBindingType type) { return new CypherBinding(name, type); } @@ -32,7 +32,7 @@ public String getName() { return name; } - public Class getType() { + public CypherBindingType getType() { return type; } } diff --git a/translation/src/main/java/org/opencypher/gremlin/extension/CypherBindingType.java b/translation/src/main/java/org/opencypher/gremlin/extension/CypherBindingType.java new file mode 100644 index 00000000..cfa7a71d --- /dev/null +++ b/translation/src/main/java/org/opencypher/gremlin/extension/CypherBindingType.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 "Neo4j, Inc." [https://neo4j.com] + * + * 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 org.opencypher.gremlin.extension; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public enum CypherBindingType { + ANY(Object.class), + BOOLEAN(Boolean.class), + STRING(String.class), + NUMBER(Number.class), + FLOAT(Float.class), + INTEGER(Integer.class), + MAP(Map.class), + LIST(List.class), + NODE(Map.class), + RELATIONSHIP(Map.class); + + private final Class javaClass; + + CypherBindingType(Class javaClass) { + this.javaClass = javaClass; + } + + public Class getJavaClass() { + return javaClass; + } + + public boolean isAssignableFrom(CypherBindingType type) { + return getJavaClass().isAssignableFrom(type.getJavaClass()); + } + + public static CypherBindingType fromSignature(String type) { + return Stream.of(values()) + .filter(v -> (v.name() + "?").equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unparsable procedure type: " + type)); + } +} diff --git a/translation/src/main/java/org/opencypher/gremlin/traversal/ProcedureContext.java b/translation/src/main/java/org/opencypher/gremlin/traversal/ProcedureContext.java index c7cbb962..395852c6 100644 --- a/translation/src/main/java/org/opencypher/gremlin/traversal/ProcedureContext.java +++ b/translation/src/main/java/org/opencypher/gremlin/traversal/ProcedureContext.java @@ -18,6 +18,9 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; +import static org.opencypher.gremlin.extension.CypherBindingType.FLOAT; +import static org.opencypher.gremlin.extension.CypherBindingType.INTEGER; +import static org.opencypher.gremlin.extension.CypherBindingType.NUMBER; import java.util.ArrayList; import java.util.Arrays; @@ -31,6 +34,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import org.opencypher.gremlin.extension.CypherBinding; +import org.opencypher.gremlin.extension.CypherBindingType; import org.opencypher.gremlin.extension.CypherProcedure; public final class ProcedureContext { @@ -97,7 +101,8 @@ private Object call(String name, Collection arguments) { // Defined argument types List> defArgTypes = defArgs.stream() .map(CypherBinding::getType) - .map(type -> Number.class.isAssignableFrom(type) ? Number.class : type) + .map(type -> NUMBER.isAssignableFrom(type) ? NUMBER : type) + .map(CypherBindingType::getJavaClass) .collect(toList()); // Argument types in call @@ -125,7 +130,7 @@ private Object call(String name, Collection arguments) { Map implArgs = new HashMap<>(); for (int i = 0; i < defArgsSize; i++) { String argName = defArgs.get(i).getName(); - Class argType = defArgs.get(i).getType(); + CypherBindingType argType = defArgs.get(i).getType(); Object argValue = numericCast(callArgs.get(i), argType); implArgs.put(argName, argValue); } @@ -138,7 +143,7 @@ private Object call(String name, Collection arguments) { Map orderedRow = new LinkedHashMap<>(); for (CypherBinding res : defResults) { String resName = res.getName(); - Class resType = res.getType(); + CypherBindingType resType = res.getType(); Object resValue = numericCast(row.get(resName), resType); orderedRow.put(resName, resValue); } @@ -147,13 +152,13 @@ private Object call(String name, Collection arguments) { return results; } - private static Object numericCast(Object value, Class type) { + private static Object numericCast(Object value, CypherBindingType type) { if (value instanceof Number) { Number number = (Number) value; - if (type.equals(Long.class)) { + if (type.equals(INTEGER)) { return number.longValue(); } - if (type.equals(Double.class)) { + if (type.equals(FLOAT)) { return number.doubleValue(); } } diff --git a/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala b/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala index 66a0809a..e4164157 100644 --- a/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala +++ b/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala @@ -18,6 +18,19 @@ package org.opencypher.gremlin.translation import java.util import java.util.Collections +import org.opencypher.gremlin.extension.CypherBindingType.{ + ANY, + BOOLEAN, + FLOAT, + INTEGER, + LIST, + MAP, + NODE, + NUMBER, + RELATIONSHIP, + STRING +} +import org.opencypher.gremlin.extension._ import org.opencypher.gremlin.translation.context.WalkerContext import org.opencypher.gremlin.translation.exception.SyntaxException import org.opencypher.gremlin.translation.ir.TranslationWriter @@ -32,7 +45,7 @@ import org.opencypher.v9_0.expressions._ import org.opencypher.v9_0.frontend.phases._ import org.opencypher.v9_0.rewriting.RewriterStepSequencer import org.opencypher.v9_0.rewriting.rewriters.Never -import org.opencypher.v9_0.util.symbols.{AnyType, CypherType} +import org.opencypher.v9_0.util.symbols._ import org.opencypher.v9_0.util.{ASTNode, CypherException} import scala.collection.JavaConverters._ @@ -243,6 +256,22 @@ object CypherAst { } } + val standaloneCall = clauses.forall { + case UnresolvedCall(_, _, _, None) => true + case _ => false + } + + if (standaloneCall) { + val UnresolvedCall(Namespace(namespaceParts), ProcedureName(name), _, _) = clauses.head + val qualifiedName = namespaceParts.mkString(".") + "." + name + return procedures + .findOrThrow(qualifiedName) + .results() + .asScala + .map(b => (b.getName, bindingType(b.getType))) + .toMap + } + clauses.flatMap { case Return(_, returnItems, _, _, _, _) => returnItems.items case _ => Nil @@ -257,4 +286,19 @@ object CypherAst { None }.toMap } + + private def bindingType(typ: CypherBindingType): CypherType = { + typ match { + case ANY => CTAny + case BOOLEAN => CTBoolean + case STRING => CTString + case NUMBER => CTNumber + case FLOAT => CTFloat + case INTEGER => CTInteger + case MAP => CTMap + case LIST => CTList(CTAny) + case NODE => CTNode + case RELATIONSHIP => CTRelationship + } + } } From 4269619c9a26a263ac2ee8653532008a21bdb769 Mon Sep 17 00:00:00 2001 From: Dimitry Solovyov Date: Tue, 17 Jul 2018 17:18:30 +0300 Subject: [PATCH 3/5] Add test for standalone call return types Signed-off-by: Dimitry Solovyov --- .../gremlin/extension/CypherProcedures.scala | 22 ++++++ .../gremlin/translation/CypherAst.scala | 16 +---- .../translation/walker/CallWalker.scala | 6 +- .../gremlin/translation/CypherAstTest.java | 72 +++++++++++++++++-- 4 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 translation/src/main/scala/org/opencypher/gremlin/extension/CypherProcedures.scala diff --git a/translation/src/main/scala/org/opencypher/gremlin/extension/CypherProcedures.scala b/translation/src/main/scala/org/opencypher/gremlin/extension/CypherProcedures.scala new file mode 100644 index 00000000..7122a2b6 --- /dev/null +++ b/translation/src/main/scala/org/opencypher/gremlin/extension/CypherProcedures.scala @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018 "Neo4j, Inc." [https://neo4j.com] + * + * 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 org.opencypher.gremlin.extension + +object CypherProcedures { + def procedureName(namespaceParts: Seq[String], name: String): String = { + namespaceParts.map(_ + ".").mkString + name + } +} diff --git a/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala b/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala index e4164157..49bfb480 100644 --- a/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala +++ b/translation/src/main/scala/org/opencypher/gremlin/translation/CypherAst.scala @@ -18,18 +18,8 @@ package org.opencypher.gremlin.translation import java.util import java.util.Collections -import org.opencypher.gremlin.extension.CypherBindingType.{ - ANY, - BOOLEAN, - FLOAT, - INTEGER, - LIST, - MAP, - NODE, - NUMBER, - RELATIONSHIP, - STRING -} +import org.opencypher.gremlin.extension.CypherBindingType._ +import org.opencypher.gremlin.extension.CypherProcedures.procedureName import org.opencypher.gremlin.extension._ import org.opencypher.gremlin.translation.context.WalkerContext import org.opencypher.gremlin.translation.exception.SyntaxException @@ -263,7 +253,7 @@ object CypherAst { if (standaloneCall) { val UnresolvedCall(Namespace(namespaceParts), ProcedureName(name), _, _) = clauses.head - val qualifiedName = namespaceParts.mkString(".") + "." + name + val qualifiedName = procedureName(namespaceParts, name) return procedures .findOrThrow(qualifiedName) .results() diff --git a/translation/src/main/scala/org/opencypher/gremlin/translation/walker/CallWalker.scala b/translation/src/main/scala/org/opencypher/gremlin/translation/walker/CallWalker.scala index c04e77a9..213acc78 100644 --- a/translation/src/main/scala/org/opencypher/gremlin/translation/walker/CallWalker.scala +++ b/translation/src/main/scala/org/opencypher/gremlin/translation/walker/CallWalker.scala @@ -15,6 +15,8 @@ */ package org.opencypher.gremlin.translation.walker +import org.opencypher.gremlin.extension.CypherProcedures +import org.opencypher.gremlin.extension.CypherProcedures.procedureName import org.opencypher.gremlin.translation.GremlinSteps import org.opencypher.gremlin.translation.context.WalkerContext import org.opencypher.gremlin.translation.walker.NodeUtils._ @@ -43,7 +45,7 @@ private class CallWalker[T, P](context: WalkerContext[T, P], g: GremlinSteps[T, node match { case UnresolvedCall(Namespace(namespaceParts), ProcedureName(name), argumentOption, results) => val procedures = context.procedures - val qualifiedName = namespaceParts.mkString(".") + "." + name + val qualifiedName = procedureName(namespaceParts, name) procedures.findOrThrow(qualifiedName) val arguments = argumentOption.getOrElse { @@ -79,7 +81,7 @@ private class CallWalker[T, P](context: WalkerContext[T, P], g: GremlinSteps[T, val procedures = context.procedures node match { case UnresolvedCall(Namespace(namespaceParts), ProcedureName(name), argumentOption, results) => - val qualifiedName = namespaceParts.mkString(".") + "." + name + val qualifiedName = procedureName(namespaceParts, name) val procedure = procedures.findOrThrow(qualifiedName) val arguments = argumentOption.getOrElse { diff --git a/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java b/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java index 13e05643..b15ebefc 100644 --- a/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java +++ b/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java @@ -15,18 +15,41 @@ */ package org.opencypher.gremlin.translation; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.opencypher.gremlin.extension.CypherBinding.binding; +import static org.opencypher.gremlin.extension.CypherBindingType.ANY; +import static org.opencypher.gremlin.extension.CypherBindingType.BOOLEAN; +import static org.opencypher.gremlin.extension.CypherBindingType.FLOAT; +import static org.opencypher.gremlin.extension.CypherBindingType.INTEGER; +import static org.opencypher.gremlin.extension.CypherBindingType.LIST; +import static org.opencypher.gremlin.extension.CypherBindingType.MAP; +import static org.opencypher.gremlin.extension.CypherBindingType.NODE; +import static org.opencypher.gremlin.extension.CypherBindingType.NUMBER; +import static org.opencypher.gremlin.extension.CypherBindingType.RELATIONSHIP; +import static org.opencypher.gremlin.extension.CypherBindingType.STRING; +import static org.opencypher.gremlin.extension.CypherProcedure.cypherProcedure; -import java.util.HashMap; import java.util.Map; import org.junit.Test; import org.opencypher.gremlin.translation.groovy.GroovyPredicate; import org.opencypher.gremlin.translation.translator.Translator; +import org.opencypher.gremlin.traversal.ProcedureContext; import org.opencypher.v9_0.util.symbols.AnyType; +import org.opencypher.v9_0.util.symbols.BooleanType; import org.opencypher.v9_0.util.symbols.CypherType; +import org.opencypher.v9_0.util.symbols.FloatType; import org.opencypher.v9_0.util.symbols.IntegerType; +import org.opencypher.v9_0.util.symbols.ListType; +import org.opencypher.v9_0.util.symbols.MapType; import org.opencypher.v9_0.util.symbols.NodeType; +import org.opencypher.v9_0.util.symbols.NumberType; +import org.opencypher.v9_0.util.symbols.RelationshipType; +import org.opencypher.v9_0.util.symbols.StringType; public class CypherAstTest { @@ -35,7 +58,7 @@ public void duplicateNames() { String cypher = "MATCH (n:person)-[r:knows]->(friend:person)\n" + "WHERE n.name = 'marko'\n" + "RETURN n, friend.name AS friend"; - CypherAst ast = CypherAst.parse(cypher, new HashMap<>()); + CypherAst ast = CypherAst.parse(cypher); Map variableTypes = ast.getReturnTypes(); assertThat(variableTypes.get("n")).isInstanceOf(NodeType.class); @@ -45,7 +68,7 @@ public void duplicateNames() { @Test public void duplicateNameInAggregation() { String cypher = "MATCH (n) RETURN n.prop AS n, count(n) AS count"; - CypherAst ast = CypherAst.parse(cypher, new HashMap<>()); + CypherAst ast = CypherAst.parse(cypher); Map extractedParameters = ast.getReturnTypes(); assertThat(extractedParameters.get("n")).isInstanceOf(AnyType.class); @@ -58,7 +81,7 @@ public void returnTypesInUnion() { "UNION\n" + "MATCH (b:B)\n" + "RETURN b AS a"; - CypherAst ast = CypherAst.parse(cypher, new HashMap<>()); + CypherAst ast = CypherAst.parse(cypher); Map variableTypes = ast.getReturnTypes(); assertThat(variableTypes.get("a")).isInstanceOf(NodeType.class); @@ -68,13 +91,52 @@ public void returnTypesInUnion() { public void variableInTypeTable() { String cypher = "MATCH (a)\n" + "RETURN a, count(a) + 3"; - CypherAst ast = CypherAst.parse(cypher, new HashMap<>()); + CypherAst ast = CypherAst.parse(cypher); Map variableTypes = ast.getReturnTypes(); assertThat(variableTypes.get("a")).isInstanceOf(NodeType.class); assertThat(variableTypes.get("count(a) + 3")).isInstanceOf(IntegerType.class); } + @Test + public void standaloneCallTypes() { + ProcedureContext procedureContext = new ProcedureContext(singleton( + cypherProcedure( + "proc", + emptyList(), + asList( + binding("any", ANY), + binding("boolean", BOOLEAN), + binding("string", STRING), + binding("number", NUMBER), + binding("float", FLOAT), + binding("integer", INTEGER), + binding("map", MAP), + binding("list", LIST), + binding("node", NODE), + binding("relationship", RELATIONSHIP) + ), + arguments -> { + throw new UnsupportedOperationException(); + } + ) + )); + CypherAst ast = CypherAst.parse("CALL proc()", emptyMap(), procedureContext); + Map returnTypes = ast.getReturnTypes(); + + assertThat(returnTypes).hasSize(10); + assertThat(returnTypes.get("any")).isInstanceOf(AnyType.class); + assertThat(returnTypes.get("boolean")).isInstanceOf(BooleanType.class); + assertThat(returnTypes.get("string")).isInstanceOf(StringType.class); + assertThat(returnTypes.get("number")).isInstanceOf(NumberType.class); + assertThat(returnTypes.get("float")).isInstanceOf(FloatType.class); + assertThat(returnTypes.get("integer")).isInstanceOf(IntegerType.class); + assertThat(returnTypes.get("map")).isInstanceOf(MapType.class); + assertThat(returnTypes.get("list")).isInstanceOf(ListType.class); + assertThat(returnTypes.get("node")).isInstanceOf(NodeType.class); + assertThat(returnTypes.get("relationship")).isInstanceOf(RelationshipType.class); + } + @Test public void noCypherExtensions() { CypherAst ast = CypherAst.parse( From 2a4f9d1b0130fa6703b1861ca0f6fc13a340de49 Mon Sep 17 00:00:00 2001 From: Dimitry Solovyov Date: Wed, 18 Jul 2018 14:11:27 +0300 Subject: [PATCH 4/5] Fix return columns access in TCK Signed-off-by: Dimitry Solovyov --- .../gremlin/tck/TckGremlinCypherValueConverter.scala | 5 ++++- .../translation/ir/rewrite/SimplifyRenamedAliasesTest.scala | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/testware/tck/src/test/scala/org/opencypher/gremlin/tck/TckGremlinCypherValueConverter.scala b/testware/tck/src/test/scala/org/opencypher/gremlin/tck/TckGremlinCypherValueConverter.scala index 418fe38d..ead85380 100644 --- a/testware/tck/src/test/scala/org/opencypher/gremlin/tck/TckGremlinCypherValueConverter.scala +++ b/testware/tck/src/test/scala/org/opencypher/gremlin/tck/TckGremlinCypherValueConverter.scala @@ -22,6 +22,7 @@ import org.apache.tinkerpop.gremlin.structure.Vertex import org.opencypher.gremlin.translation.CypherAst import org.opencypher.gremlin.translation.ReturnProperties._ import org.opencypher.gremlin.translation.exception.{ConstraintException, SyntaxException, TypeException} +import org.opencypher.gremlin.traversal.ProcedureContext import org.opencypher.tools.tck.api.{CypherValueRecords, ExecutionFailed} import org.opencypher.tools.tck.constants.TCKErrorPhases.RUNTIME import org.opencypher.tools.tck.values._ @@ -98,7 +99,9 @@ object TckGremlinCypherValueConverter { private def emptyHeaderWorkaround(query: String, header: List[String], rows: List[Map[String, CypherValue]]) = { if (rows.isEmpty) { - val columns = CypherAst.parse(query, new util.HashMap[String, Any]).statement.returnColumns + val procedureContext = ProcedureContext.global() + val ast = CypherAst.parse(query, new util.HashMap[String, Any], procedureContext) + val columns = ast.statement.returnColumns CypherValueRecords.emptyWithHeader(columns) } else { CypherValueRecords(header, rows) diff --git a/translation/src/test/scala/org/opencypher/gremlin/translation/ir/rewrite/SimplifyRenamedAliasesTest.scala b/translation/src/test/scala/org/opencypher/gremlin/translation/ir/rewrite/SimplifyRenamedAliasesTest.scala index 7b5beda4..d6f4f795 100644 --- a/translation/src/test/scala/org/opencypher/gremlin/translation/ir/rewrite/SimplifyRenamedAliasesTest.scala +++ b/translation/src/test/scala/org/opencypher/gremlin/translation/ir/rewrite/SimplifyRenamedAliasesTest.scala @@ -91,8 +91,8 @@ class SimplifyRenamedAliasesTest { SimplifyRenamedAliases ), postConditions = Nil - ), - ProcedureContext.empty) + ) + ) val newTraversalCount = steps.count(_ == Vertex) assertThat(newTraversalCount).isEqualTo(1) From c05c1758fdc60e3a048255272a47ae827d0b6b1c Mon Sep 17 00:00:00 2001 From: Dimitry Solovyov Date: Wed, 18 Jul 2018 14:51:40 +0300 Subject: [PATCH 5/5] Add test showing return types for yielded columns Signed-off-by: Dimitry Solovyov --- .../gremlin/translation/CypherAstTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java b/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java index b15ebefc..e3e66b94 100644 --- a/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java +++ b/translation/src/test/java/org/opencypher/gremlin/translation/CypherAstTest.java @@ -137,6 +137,35 @@ public void standaloneCallTypes() { assertThat(returnTypes.get("relationship")).isInstanceOf(RelationshipType.class); } + @Test + public void callYieldTypes() { + ProcedureContext procedureContext = new ProcedureContext(singleton( + cypherProcedure( + "proc", + emptyList(), + asList( + binding("a", STRING), + binding("b", INTEGER), + binding("c", FLOAT) + ), + arguments -> { + throw new UnsupportedOperationException(); + } + ) + )); + CypherAst ast = CypherAst.parse( + "CALL proc() " + + "YIELD b, c " + + "RETURN b, c", + emptyMap(), + procedureContext); + Map returnTypes = ast.getReturnTypes(); + + assertThat(returnTypes).hasSize(2); + assertThat(returnTypes.get("b")).isInstanceOf(AnyType.class); + assertThat(returnTypes.get("c")).isInstanceOf(AnyType.class); + } + @Test public void noCypherExtensions() { CypherAst ast = CypherAst.parse(