Skip to content

Commit

Permalink
Experimental gremlin function (#246)
Browse files Browse the repository at this point in the history
- Allows including Gremlin steps in translated query
- Needs to be explicitly enabled
  • Loading branch information
dwitry authored Mar 2, 2019
1 parent 2dd2e56 commit 68b1219
Show file tree
Hide file tree
Showing 21 changed files with 817 additions and 9 deletions.
9 changes: 7 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ project(':translation') {

compile "org.scala-lang:scala-library:${scalaVersion}.${scalaPatchVersion}"
compile "org.scala-lang.modules:scala-java8-compat_${scalaVersion}:0.8.0"
compile "com.lihaoyi:fastparse_${scalaVersion}:2.0.4"
compile("org.opencypher:front-end-${cypherVersion}:${cypherFrontendVersion}") {
exclude group: 'org.scalatest'
exclude group: 'org.scalacheck'
Expand Down Expand Up @@ -252,11 +253,15 @@ project(':testware:integration-tests') {
useJUnit {
excludeCategories System.getProperty('excludeCategories').split(",")
}
} else {
useJUnit {
excludeCategories 'org.opencypher.gremlin.groups.SkipWithPlugin'
}
}
}

task('testGremlinGroovyTranslation', type: Test) {
systemProperty 'translate', 'gremlin+cfog_server_extensions'
systemProperty 'translate', 'gremlin+cfog_server_extensions+experimental_gremlin_function'

if (project.hasProperty('ci')) {
testLogging.showStandardStreams = true
Expand All @@ -272,7 +277,7 @@ project(':testware:integration-tests') {
check.dependsOn testGremlinGroovyTranslation

task('testBytecodeTranslation', type: Test) {
systemProperty 'translate', 'bytecode+cfog_server_extensions'
systemProperty 'translate', 'bytecode+cfog_server_extensions+experimental_gremlin_function'

if (project.hasProperty('ci')) {
testLogging.showStandardStreams = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2018-2019 "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.groups;

/**
* Tests that are skipped in 'test' Gradle task (using Gremlin plugin)
* Must have description explaining reason
*/
public interface SkipWithPlugin {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright (c) 2018-2019 "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.queries;

import static org.apache.tinkerpop.gremlin.structure.util.ElementHelper.asMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

import java.util.List;
import java.util.Map;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.opencypher.gremlin.client.CypherGremlinClient;
import org.opencypher.gremlin.client.CypherResultSet;
import org.opencypher.gremlin.groups.SkipWithPlugin;
import org.opencypher.gremlin.rules.GremlinServerExternalResource;
import org.opencypher.gremlin.test.TestCommons;

/**
* Currently experimental `gremlin` function is supported only for client-side translation, and should be enabled explicitly.
* Run this test with `-Dtranslate=gremlin+cfog_server_extensions+experimental_gremlin_function` or
* `-Dtranslate=bytecode+cfog_server_extensions+experimental_gremlin_function`
*/
@Category(SkipWithPlugin.class)
public class GremlinFunctionTest {

@ClassRule
public static final GremlinServerExternalResource gremlinServer = new GremlinServerExternalResource(TestCommons::modernGraph);

private List<Map<String, Object>> submitAndGet(String cypher) {
return gremlinServer.cypherGremlinClient().submit(cypher).all();
}

@Test
public void testGremlinFunction() {
List<Map<String, Object>> results = submitAndGet(
"MATCH (n:person {name: 'marko'}) RETURN gremlin(\"select('n').outE().label()\") as r"
);

assertThat(results)
.extracting("r")
.containsExactlyInAnyOrder("created");
}

@Test
public void testGremlinFunctionInWith() {
List<Map<String, Object>> results = submitAndGet(
"WITH gremlin(\"g.V().has('name', eq('marko'))\") AS n RETURN n.name as r"
);

assertThat(results)
.extracting("r")
.containsExactlyInAnyOrder("marko");
}

@Test
public void testGremlinFunctionMultipleValues() {
List<Map<String, Object>> results = submitAndGet(
"WITH gremlin(\"constant(1).as('a').constant(2).as('b').select('a', 'b')\") AS constants " +
" RETURN constants['a'] as a, constants['b'] as b"
);

assertThat(results)
.extracting("a", "b")
.containsExactlyInAnyOrder(tuple(1L, 2L));
}

@Test
public void gremlinAsParameter() {
CypherGremlinClient client = gremlinServer.cypherGremlinClient();

String gremlin = "select('n').values('name').as('l')";
String cypher = "MATCH (n:person)" +
"RETURN gremlin({gremlinQuery}) AS l";

CypherResultSet results = client.submit(cypher, asMap("gremlinQuery", gremlin));

assertThat(results)
.extracting("l")
.containsExactlyInAnyOrder("josh", "vadas", "peter", "marko");
}

@Test
public void dontOverwriteValues() {
List<Map<String, Object>> results = submitAndGet(
"WITH 1 as a, gremlin(\"constant(2).as('a')\") as b " +
"RETURN a, b"
);

assertThat(results)
.extracting("a", "b")
.containsExactlyInAnyOrder(tuple(1L, 2L));
}

@Test
public void useInMatch() {
List<Map<String, Object>> results = submitAndGet(
"WITH gremlin(\"g.V().has('name', eq('marko'))\") AS n " +
"MATCH (n)-[:knows]->(m) " +
"RETURN m.name as r"
);

assertThat(results)
.extracting("r")
.containsExactly("vadas", "josh");
}


@Test
public void singleValue() {
List<Map<String, Object>> results = submitAndGet(
"WITH gremlin(\"g.V().hasLabel('person')\") AS node RETURN id(node) as r"
);

assertThat(results)
.hasSize(1)
.extracting("r")
.containsExactly(0L);
}

@Test
public void list() {
List<Map<String, Object>> results = submitAndGet(
"UNWIND gremlin(\"g.V().hasLabel('person').fold()\") AS node RETURN node.name as r"
);

assertThat(results)
.extracting("r")
.containsExactly("marko", "vadas", "josh", "peter");
}

/**
* Courtesy of <a href="https://stackoverflow.com/a/42097722">Daniel Kuppitz</a>
*/
@Test
public void kahnsAlgorithm() {
List<Map<String, Object>> results = submitAndGet(

"WITH gremlin(\"g.V().not(inE()).aggregate('x')." +
" repeat(outE().aggregate('e').inV().not(inE().where(without('e'))).aggregate('x'))." +
" cap('x')\") as x " +
"UNWIND x AS n " +
"RETURN n.name as name"
);

assertThat(results)
.extracting("name")
.containsExactlyInAnyOrder("marko", "peter", "vadas", "josh", "ripple", "lop");
}

@Test
public void lowestCommonAncestor() {
List<Map<String, Object>> results = submitAndGet(
"MATCH (v {name:'vadas'}), (l {name:'lop'})" +
"WITH gremlin(\"" +
"select('l').emit().repeat(inE().outV()).as('x')" +
".repeat(outE().inV()).emit(where(eq('v')))" +
".select('x')\") AS x RETURN x.name as r"
);

assertThat(results)
.extracting("r")
.containsExactlyInAnyOrder("marko");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.opencypher.gremlin.rules.GremlinServerExternalResource;
import org.opencypher.gremlin.test.TestCommons;
import org.opencypher.gremlin.translation.translator.Translator;
import org.opencypher.gremlin.translation.translator.TranslatorFeature;
import org.opencypher.gremlin.translation.translator.TranslatorFlavor;

public class CypherGremlinServerClientSnippets {
Expand Down Expand Up @@ -222,5 +223,29 @@ public void cypherTraversalSourceWithRemote() throws Throwable {
.containsExactlyInAnyOrder("created", "knows");
}

@Test
public void translatorEnableExperimental() throws Exception {
Client gremlinClient = newGremlinClient();

// freshReadmeSnippet: enableExperimentalGremlin
CypherGremlinClient cypherGremlinClient = CypherGremlinClient.translating(
gremlinClient,
() -> Translator.builder()
.gremlinGroovy()
.enableCypherExtensions()
.enable(TranslatorFeature.EXPERIMENTAL_GREMLIN_FUNCTION)
.build()
);

List<Map<String, Object>> results = cypherGremlinClient.submit(
"MATCH (n:person {name: 'marko'}) " +
"RETURN gremlin(\"select('n').outE().label()\") as r").all();
// freshReadmeSnippet: enableExperimentalGremlin

assertThat(results)
.extracting("r")
.containsExactly("created");
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ public void custom() throws Exception {
// freshReadmeSnippet: custom
}


private class MyGremlinSteps extends GroovyGremlinSteps {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.opencypher.gremlin.server.EmbeddedGremlinServerFactory;
import org.opencypher.gremlin.test.TestCommons;
import org.opencypher.gremlin.translation.translator.Translator;
import org.opencypher.gremlin.translation.translator.TranslatorFeature;
import org.opencypher.gremlin.translation.translator.TranslatorFlavor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -95,13 +96,14 @@ private Client configuredGremlinClient() throws Exception {

private CypherGremlinClient configuredCypherGremlinClient() {
String translate = emptyToNull(System.getProperty(TOKEN_TRANSLATE));
String clientName = Optional.ofNullable(translate).orElse("traversal+cfog_server_extensions");
if ("traversal+cfog_server_extensions".equals(clientName)) {
String clientName = Optional.ofNullable(translate).orElse("traversal+cfog_server_extensions+experimental_gremlin_function");
if ("traversal+cfog_server_extensions+experimental_gremlin_function".equals(clientName)) {
return CypherGremlinClient.plugin(gremlinClient);
} else if ("bytecode+cfog_server_extensions".equals(clientName)) {
} else if ("bytecode+cfog_server_extensions+experimental_gremlin_function".equals(clientName)) {
return CypherGremlinClient.bytecode(gremlinClient.alias("g"), () -> Translator.builder()
.bytecode()
.enableCypherExtensions()
.enable(TranslatorFeature.EXPERIMENTAL_GREMLIN_FUNCTION)
.build());
} else if ("cosmosdb".equals(clientName)) {
return CypherGremlinClient.retrieving(gremlinClient, TranslatorFlavor.cosmosDb());
Expand Down
4 changes: 4 additions & 0 deletions tinkerpop/cypher-gremlin-extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ The [translation module](../../translation) relies on these extensions to produc
The easiest way to use this module is by installing the [Gremlin Server Cypher plugin](../cypher-gremlin-server-plugin) on the target Gremlin Server. The plugin includes all of the extensions and registers them on the Server.

Alternatively, add [CustomPredicate.java](src/main/java/org/opencypher/gremlin/traversal/CustomPredicate.java) and [CustomFunctions.java](src/main/java/org/opencypher/gremlin/traversal/CustomFunctions.java) to Gremlin Groovy script engine.

## Include Gremlin in Cypher query

See [gremlin function](../cypher-gremlin-server-client#gremlin-function)
21 changes: 20 additions & 1 deletion tinkerpop/cypher-gremlin-server-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,25 @@ GraphTraversal<Map<String, Object>, String> traversal = g

Note that Cypher query may return null values, represented by [string constant](https://opencypher.github.io/cypher-for-gremlin/api/0.9.13/java/constant-values.html#org.opencypher.gremlin.translation.Tokens.NULL).


## Gremlin function

Experimental `gremlin` Cypher function that allows including Gremlin steps in translated query. Note that currently
function is supported only for client-side translation, and should be enabled explicitly.

<!-- [freshReadmeSource](../../testware/integration-tests/src/test/java/org/opencypher/gremlin/snippets/CypherGremlinServerClientSnippets.java#enableExperimentalGremlin) -->
```java
CypherGremlinClient cypherGremlinClient = CypherGremlinClient.translating(
gremlinClient,
() -> Translator.builder()
.gremlinGroovy()
.enableCypherExtensions()
.enable(TranslatorFeature.EXPERIMENTAL_GREMLIN_FUNCTION)
.build()
);

List<Map<String, Object>> results = cypherGremlinClient.submit(
"MATCH (n:person {name: 'marko'}) " +
"RETURN gremlin(\"select('n').outE().label()\") as r").all();
```


Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public interface GremlinSteps<T, P> {

GremlinSteps<T, P> choose(GremlinSteps<T, P> choiceTraversal);

GremlinSteps<T, P> choose(GremlinSteps<T, P> traversalPredicate,
GremlinSteps<T, P> trueChoice);

GremlinSteps<T, P> choose(GremlinSteps<T, P> traversalPredicate,
GremlinSteps<T, P> trueChoice,
GremlinSteps<T, P> falseChoice);
Expand All @@ -104,6 +107,8 @@ GremlinSteps<T, P> choose(GremlinSteps<T, P> traversalPredicate,

GremlinSteps<T, P> emit();

GremlinSteps<T, P> emit(GremlinSteps<T, P> traversal);

GremlinSteps<T, P> flatMap(GremlinSteps<T, P> traversal);

GremlinSteps<T, P> fold();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ public GremlinSteps<Bytecode, P> choose(GremlinSteps<Bytecode, P> choiceTraversa
return this;
}

@Override
public GremlinSteps<Bytecode, P> choose(GremlinSteps<Bytecode, P> traversalPredicate,
GremlinSteps<Bytecode, P> trueChoice) {
bytecode.addStep(Symbols.choose, traversalPredicate.current(), trueChoice.current());
return this;
}


@Override
public GremlinSteps<Bytecode, P> choose(GremlinSteps<Bytecode, P> traversalPredicate,
GremlinSteps<Bytecode, P> trueChoice,
Expand Down Expand Up @@ -198,6 +206,12 @@ public GremlinSteps<Bytecode, P> emit() {
return this;
}

@Override
public GremlinSteps<Bytecode, P> emit(GremlinSteps<Bytecode, P> traversal) {
bytecode.addStep(Symbols.emit, traversal.current());
return this;
}

@Override
public GremlinSteps<Bytecode, P> flatMap(GremlinSteps<Bytecode, P> traversal) {
bytecode.addStep(Symbols.flatMap, traversal.current());
Expand Down
Loading

0 comments on commit 68b1219

Please sign in to comment.