© 2020 Neo4j, Inc.,

This is the Neo4j Cypher-DSL manual version 2021.3.1.

Who should read this?

This manual is written for people interested in creating Cypher queries in a typesafe way on the JVM.

The Cypher-DSL is considered EXPERIMENTAL at the time of writing. This means you can and should use it and that we are looking actively for feedback, issues and problems. We will try to keep breaking changes to a minimum, but we reserve the right to modify public methods, parameters and behaviour in case we need those changes. All public classes inside org.neo4j.cypherdsl still subject to breaking changes are annotated with @API(status = EXPERIMENTAL).

1. Introduction

1.1. Purpose

The Cypher-DSL has been developed with the needs of Spring Data Neo4j. We wanted to avoid string concatenations in our query generation and decided do go with a builder approach, much like we find with jOOQ or in the relational module of Spring Data JDBC, but for Cypher.

What we don’t have - and don’t need for our mapping purpose - at the moment is a code generator that reads the database schema and generates static classes representing labels and relationship types. That is still up to the mapping framework (in our case SDN). We however have a type safe API for Cypher that allows only generating valid Cypher constructs.

We worked closely with the OpenCypher spec here and you find a lot of these concepts in the API.

The Cypher-DSL can also be seen in the same area as the Criteria API of Spring Data Mongo.

1.2. Where to use it

The Cypher-DSL creates an Abstract Syntax Tree (AST) representing your Cypher-Statements. An instance of a org.neo4j.cypherdsl.core.Statement representing that AST is provided at the end of query building step. A Renderer is then used to create literal Java-Strings. Those can be used in any context supporting String-based queries, for example with the Neo4j Java driver or inside embedded procedures and of course with Spring Data’s Neo4j-Client.

Parameters in the generated queries will use the $form and as such be compatible with all current versions of Neo4j.

Users of SDN 6+ can use the generated org.neo4j.cypherdsl.core.Statement directly with the Neo4jTemplate or the ReactiveNeo4jTemplate. Both the imperative and the reactive variants allow the retrieval and counting of entities without rendering a String first, for example through Neo4jTemplate#findAll(Statement, Class<T>).

1.3. Java API

Find the Java-API and a generated project info here: API and project info.

2. Getting started

2.1. Prepare dependencies

Please use a dependency management system. We recommend either Maven or Gradle.

2.1.1. Maven configuration

Listing 1. Inclusion of the Neo4j Cypher-DSL in a Maven project
<dependency>
        <groupId>org.neo4j</groupId>
        <artifactId>neo4j-cypher-dsl</artifactId>
        <version>2021.3.1</version>
</dependency>

2.1.2. Gradle configuration

Listing 2. Inclusion of the Neo4j Cypher-DSL in a Gradle project
dependencies {
    implementation 'org.neo4j:neo4j-cypher-dsl:2021.3.1'
}

2.2. How to use it

You use the Cypher-DSL as you would write Cypher: it allows to write down even complex Cypher queries from top to bottom in a type safe, compile time checked way.

The examples to follow are using JDK 11. We find the var keyword especially appealing in such a DSL as the types returned by the DSL are much less important than the further building methods they offer.

The AST parts and intermediate build steps are immutable. That is, the methods create new intermediate steps. For example, you cannot reuse an ExposesLimit step, but have to use the returned object from its skip method.

An instance of a org.neo4j.cypherdsl.core.Statement is provided at the end of every query building step. This Statement needs to be rendered into a string or passed to methods supporting it as input.

Please get an instance of the default renderer via org.neo4j.cypherdsl.renderer.Renderer#getDefaultRenderer(). The renderer provides a single method render for rendering the AST into a string representation.

Furthermore, the Statement will collect parameter names and if provided, parameter values. Parameter names and values are available after the statement has been built and can for example be used directly with Neo4j-Java-Driver.

2.2.1. Examples

The following examples are 1:1 copies of the queries you will find in the Neo4j browser after running :play movies.

They use the following imports:

Listing 3. Imports needed for the examples to compile
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collection;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.neo4j.cypherdsl.core.Conditions;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.SymbolicName;
import org.neo4j.cypherdsl.core.renderer.Configuration;
import org.neo4j.cypherdsl.core.renderer.Renderer;

To match and return all the movie, build your statement like this:

Listing 4. Simple match
var m = Cypher.node("Movie").named("m"); (1)
var statement = Cypher.match(m) (2)
    .returning(m)
    .build(); (3)

assertThat(cypherRenderer.render(statement))
    .isEqualTo("MATCH (m:`Movie`) RETURN m");
1 Declare a variable storing your node labeled Movie and named m, so that you can
2 reuse it in both the match and the return part.
3 The build method becomes only available when a compilable Cypher statement can be rendered.
Find

Match all nodes with a given set of properties:

Listing 5. Find the actor named "Tom Hanks"…​
var tom = Cypher.anyNode().named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var statement = Cypher
    .match(tom).returning(tom)
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo("MATCH (tom {name: 'Tom Hanks'}) RETURN tom");

Limit the number of returned things and return only one attribute

Listing 6. Find 10 people…​
var people = Cypher.node("Person").named("people");
statement = Cypher
    .match(people)
    .returning(people.property("name"))
    .limit(10)
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo("MATCH (people:`Person`) RETURN people.name LIMIT 10");

Create complex conditions

Listing 7. Find movies released in the 1990s…​
var nineties = Cypher.node("Movie").named("nineties");
var released = nineties.property("released");
statement = Cypher
    .match(nineties)
    .where(released.gte(Cypher.literalOf(1990)).and(released.lt(Cypher.literalOf(2000))))
    .returning(nineties.property("title"))
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo(
        "MATCH (nineties:`Movie`) WHERE (nineties.released >= 1990 AND nineties.released < 2000) RETURN nineties.title");
Query

Build relationships

Listing 8. List all Tom Hanks movies…​
var tom = Cypher.node("Person").named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var tomHanksMovies = Cypher.anyNode("tomHanksMovies");
var statement = Cypher
    .match(tom.relationshipTo(tomHanksMovies, "ACTED_IN"))
    .returning(tom, tomHanksMovies)
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo(
        "MATCH (tom:`Person` {name: 'Tom Hanks'})-[:`ACTED_IN`]->(tomHanksMovies) RETURN tom, tomHanksMovies");
Listing 9. Who directed "Cloud Atlas"?
var cloudAtlas = Cypher.anyNode("cloudAtlas").withProperties("title", Cypher.literalOf("Cloud Atlas"));
var directors = Cypher.anyNode("directors");
statement = Cypher
    .match(cloudAtlas.relationshipFrom(directors, "DIRECTED"))
    .returning(directors.property("name"))
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo("MATCH (cloudAtlas {title: 'Cloud Atlas'})<-[:`DIRECTED`]-(directors) RETURN directors.name");
Listing 10. Tom Hanks' co-actors…​
tom = Cypher.node("Person").named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var movie = Cypher.anyNode("m");
var coActors = Cypher.anyNode("coActors");
var people = Cypher.node("Person").named("people");
statement = Cypher
    .match(tom.relationshipTo(movie, "ACTED_IN").relationshipFrom(coActors, "ACTED_IN"))
    .returning(coActors.property("name"))
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo(
        "MATCH (tom:`Person` {name: 'Tom Hanks'})-[:`ACTED_IN`]->(m)<-[:`ACTED_IN`]-(coActors) RETURN coActors.name");
Listing 11. How people are related to "Cloud Atlas"…​
cloudAtlas = Cypher.node("Movie").withProperties("title", Cypher.literalOf("Cloud Atlas"));
people = Cypher.node("Person").named("people");
var relatedTo = people.relationshipBetween(cloudAtlas).named("relatedTo");
statement = Cypher
    .match(relatedTo)
    .returning(people.property("name"), Functions.type(relatedTo), relatedTo.getRequiredSymbolicName())
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo(
        "MATCH (people:`Person`)-[relatedTo]-(:`Movie` {title: 'Cloud Atlas'}) RETURN people.name, type(relatedTo), relatedTo");
Solve
Listing 12. Movies and actors up to 4 "hops" away from Kevin Bacon
var bacon = Cypher.node("Person").named("bacon").withProperties("name", Cypher.literalOf("Kevin Bacon"));
var hollywood = Cypher.anyNode("hollywood");
var statement = Cypher
    .match(bacon.relationshipBetween(hollywood).length(1, 4))
    .returningDistinct(hollywood)
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo("MATCH (bacon:`Person` {name: 'Kevin Bacon'})-[*1..4]-(hollywood) RETURN DISTINCT hollywood");
Recommend
Listing 13. Extend Tom Hanks co-actors, to find co-co-actors who haven’t worked with Tom Hanks…​
var tom = Cypher.node("Person").named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var coActors = Cypher.anyNode("coActors");
var cocoActors = Cypher.anyNode("cocoActors");
var strength = Functions.count(Cypher.asterisk()).as("Strength");
var statement = Cypher
    .match(
        tom.relationshipTo(Cypher.anyNode("m"), "ACTED_IN").relationshipFrom(coActors, "ACTED_IN"),
        coActors.relationshipTo(Cypher.anyNode("m2"), "ACTED_IN").relationshipFrom(cocoActors, "ACTED_IN")
    )
    .where(
        Conditions.not(tom.relationshipTo(Cypher.anyNode(), "ACTED_IN").relationshipFrom(cocoActors, "ACTED_IN")))
    .and(tom.isNotEqualTo(cocoActors))
    .returning(
        cocoActors.property("name").as("Recommended"),
        strength
    ).orderBy(strength.asName().descending())
    .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo(""
        + "MATCH "
        + "(tom:`Person` {name: 'Tom Hanks'})-[:`ACTED_IN`]->(m)<-[:`ACTED_IN`]-(coActors), "
        + "(coActors)-[:`ACTED_IN`]->(m2)<-[:`ACTED_IN`]-(cocoActors) "
        + "WHERE (NOT (tom)-[:`ACTED_IN`]->()<-[:`ACTED_IN`]-(cocoActors) AND tom <> cocoActors) "
        + "RETURN cocoActors.name AS Recommended, count(*) AS Strength ORDER BY Strength DESC");

2.2.2. More features

Retrieving parameters being defined

A placeholder for a parameter can be defined via Cypher.parameter("param"). This placeholder will be rendered as $param and must be filled with the appropriate means of the environment you’re working with.

In addition, an arbitrary value can be bound to the name via Cypher.parameter("param", "a value") or Cypher.parameter("param").withValue("a value"). NULL is a valid value. The Cypher-DSL will not use those values, but collect them for you.

The following example shows how to access them and how to use it:

var person = Cypher.node("Person").named("p");
var statement =
    Cypher
        .match(person)
        .where(person.property("nickname").isEqualTo(Cypher.parameter("nickname")))
        .set(
            person.property("firstName").to(Cypher.parameter("firstName").withValue("Thomas")),
            person.property("name").to(Cypher.parameter("name", "Anderson"))
        )
        .returning(person)
        .build();

assertThat(cypherRenderer.render(statement))
    .isEqualTo("MATCH (p:`Person`) WHERE p.nickname = $nickname SET p.firstName = $firstName, p.name = $name RETURN p");

Collection<String> parameterNames = statement.getParameterNames();
assertThat(parameterNames).containsExactlyInAnyOrder("nickname", "firstName", "name"); (1)

Map<String, Object> parameters = statement.getParameters();
assertThat(parameters).hasSize(2); (2)
assertThat(parameters).containsEntry("firstName", "Thomas");
assertThat(parameters).containsEntry("name", "Anderson");
1 The names contain all placeholders, also those without a value
2 The parameter map contains only parameters with defined values

If you define a parameter with conflicting values, a ConflictingParametersException will be thrown the moment you try to retrieve the collected parameters.

Using the default renderer

A statement can render itself as well:

var statement = Cypher.returning(literalTrue().as("t")).build();
var cypher = statement.getCypher();
assertThat(cypher).isEqualTo("RETURN true AS t");

This, together with the above, makes the statement a complete accessor for a Cypher-statement and its parameters.

Generating formatted Cypher

The Cypher-DSL can also format the generated Cypher to some extend. The Renderer offers the overload Renderer getRenderer(Configuration configuration), taking in an instance of org.neo4j.cypherdsl.core.renderer.Configuration.

Instances of Configuration are thread-safe and reusable. The class offers a couple of static convenience methods for retrieving some variants.

var n = Cypher.anyNode("n");
var a = Cypher.node("A").named("a");
var b = Cypher.node("B").named("b");

var mergeStatement = Cypher.merge(n)
    .onCreate().set(n.property("prop").to(Cypher.literalOf(0)))
    .merge(a.relationshipBetween(b, "T"))
    .onCreate().set(a.property("name").to(Cypher.literalOf("me")))
    .onMatch().set(b.property("name").to(Cypher.literalOf("you")))
    .returning(a.property("prop")).build();

var renderer = Renderer.getRenderer(Configuration.prettyPrinting()); (1)
assertThat(renderer.render(mergeStatement))
    .isEqualTo(
        "MERGE (n)\n" +
        "  ON CREATE SET n.prop = 0\n" +
        "MERGE (a:A)-[:T]-(b:B)\n" +
        "  ON CREATE SET a.name = 'me'\n" +
        "  ON MATCH SET b.name = 'you'\n" +
        "RETURN a.prop"
    ); (2)
1 Get a "pretty printing" instance of the renderer configuration and retrieve a renderer based on it
2 Enjoy formatted Cypher.
Escaping names

The default renderer in its default configuration will escape all names (labels and relationship types) by default. So Movie becomes `Movie` and ACTED_IN becomes `ACTED_IN`. If you don’t want this, you can either create a dedicated configuration for a renderer with that setting turned off or use the pretty printing renderer:

var relationship = Cypher.node("Person").named("a")
    .relationshipTo(Cypher.node("Movie").named("m"), "ACTED_IN").named("r");

var statement = Cypher.match(relationship).returning(relationship).build();

var defaultRenderer = Renderer.getDefaultRenderer();
assertThat(defaultRenderer.render(statement))
    .isEqualTo("MATCH (a:`Person`)-[r:`ACTED_IN`]->(m:`Movie`) RETURN r");

var escapeOnlyIfNecessary = Configuration.newConfig().alwaysEscapeNames(false).build();

var renderer = Renderer.getRenderer(escapeOnlyIfNecessary);
assertThat(renderer.render(statement))
    .isEqualTo("MATCH (a:Person)-[r:ACTED_IN]->(m:Movie) RETURN r");

renderer = Renderer.getRenderer(Configuration.prettyPrinting());
assertThat(renderer.render(statement))
    .isEqualTo("MATCH (a:Person)-[r:ACTED_IN]->(m:Movie)\nRETURN r");
Inserting raw Cypher

Cypher.raw allows for creating arbitrary expressions from raw String literals. Users discretion is advised:

var key = Cypher.name("key");
var cypher = Cypher.call("apoc.meta.schema")
    .yield("value").with("value")
    .unwind(Functions.keys(Cypher.name("value"))).as(key)
    .returning(
        key,
        Cypher.raw("value[$E]", key).as("value") (1)
    )
    .build().getCypher();

assertThat(cypher).isEqualTo(
    "CALL apoc.meta.schema() YIELD value WITH value UNWIND keys(value) AS key RETURN key, value[key] AS value");
1 The Cypher-DSL doesn’t support a dynamic lookup of properties on expression at the moment. We use a raw Cypher string with the placeholder $E which resolves to the symbolic name also passed to the raw function.

3. Properties

Nodes and Relationships expose properties. This reflects directly in the Cypher-DSL:

var personNode = Cypher.node("Person").named("p");
var movieNode = Cypher.node("Movie");
var ratedRel = personNode.relationshipTo(movieNode, "RATED").named("r");
var statement = Cypher.match(ratedRel)
        .returning(
                personNode.property("name"), (1)
                ratedRel.property("rating")) (2)
        .build();

assertThat(cypherRenderer.render(statement))
        .isEqualTo("MATCH (p:`Person`)-[r:`RATED`]->(:`Movie`) RETURN p.name, r.rating");
1 Create a property expression from a Node
2 Create a property expression from a Relationship

Both Node and Relationship should be named as in the example. The Cypher-DSL generates names if they are not named, to refer to them in the statements. Without the explicit names, the generated statement would look like this:

MATCH (geIcWNUD000:`Person`)-[TqfqBNcc001:`RATED`]->(:`Movie`) RETURN geIcWNUD000.name, TqfqBNcc001.rating

The name is of course random.

The Cypher class exposes the property method, too. This methods takes in one name (as symbolic name or as string literal) OR one expression and at least one further string, referring to the name of the property.

Passing in a symbolic name would lead to a similar result like in [properties-on-nodes-and-rel], an expression can refer to the results of functions etc.:

var epochSeconds = Cypher.property(Functions.datetime(), "epochSeconds"); (1)
var statement = Cypher.returning(epochSeconds).build();
Assertions.assertThat(cypherRenderer.render(statement))
        .isEqualTo("RETURN datetime().epochSeconds");
1 Here an expression, the datetime() function is passed in and the epocheSeconds property is dereferenced.

Nested properties are of course possible as well, either directly on nodes and relationships or via the static builder:

var node = Cypher.node("Person").named("p");

var locationPropV1 = Cypher.property(node.getRequiredSymbolicName(), "home.location", "y");
var locationPropV2 = Cypher.property("p", "home.location", "y");

var statement = Cypher.match(node)
        .where(locationPropV1.gt(Cypher.literalOf(50)))
        .returning(locationPropV2).build();

assertThat(cypherRenderer.render(statement))
        .isEqualTo("MATCH (p:`Person`) WHERE p.`home.location`.y > 50 RETURN p.`home.location`.y");

4. Functions

There are many more functions implemented in org.neo4j.cypherdsl.core.Functions. Not all of them are already documented here.

4.1. Lists

4.1.1. range()

Creates an invocation of range().

Given the following imports

import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Functions;
var range = Functions.range(0, 10);

Gives you range(0,10). The step size can be specified as well:

var range = Functions.range(0, 10, 1);

This gives you range(0,10,1).

Both variants of range also take a NumberLiteral for the start and end index and the step size.

4.1.2. Inserting a list operator

The list operator [] selects a specific value of a list or a range of values from a list. A range can either be closed or open.

Find examples to select a value at a given index, a sublist based on a closed range and sublist based on open ranges below. While the examples use Section 4.1.1, the list operator doesn’t put any restrictions on the expression at the moment.

@Test
void valueAtExample() {

        var range = Functions.range(0, 10);

        var statement = Cypher.returning(Cypher.valueAt(range, 3)).build();
        assertThat(cypherRenderer.render(statement))
                .isEqualTo("RETURN range(0, 10)[3]");
}

@Test
void subListUntilExample() {

        var range = Functions.range(Cypher.literalOf(0), Cypher.literalOf(10));

        var statement = Cypher.returning(Cypher.subListUntil(range, 3)).build();
        assertThat(cypherRenderer.render(statement))
                .isEqualTo("RETURN range(0, 10)[..3]");
}

@Test
void subListFromExample() {

        var range = Functions.range(0, 10, 1);

        var statement = Cypher.returning(Cypher.subListFrom(range, -3)).build();
        assertThat(cypherRenderer.render(statement))
                .isEqualTo("RETURN range(0, 10, 1)[-3..]");
}

@Test
void subListExample() {

        var range = Functions.range(Cypher.literalOf(0), Cypher.literalOf(10), Cypher.literalOf(1));

        var statement = Cypher.returning(Cypher.subList(range, 2, 4)).build();
        assertThat(cypherRenderer.render(statement))
                .isEqualTo("RETURN range(0, 10, 1)[2..4]");
}

4.2. Mathematical functions

The Cypher-DSL supports all mathematical functions as of Neo4j 4.2. Please find their description in the Neo4j cypher manual:

4.3. Calling arbitrary procedures and functions

Neo4j has plethora of built-in procedures and functions. The Cypher-DSL does cover a lot of them already, and they can be called in a typesafe way on many Expression-instances taking in various expressions such as the results of other calls, literals or parameters.

However, there will probably always be a gap between what the Cypher-DSL includes and what the Neo4j database brings. More so, there are fantastic libraries out there like APOC. APOC has so many procedures and functions in so many categories that it is rather futile to add shims for all of them consistently.

Probably the most important aspect of all: many Neo4j users bring their knowledge to the database themselves, in the form of their stored procedures. Those should be callable as well.

The Cypher-DSL is flexible enough to call all those procedures and functions.

4.3.1. Calling custom procedures

Procedures are called via the Cypher CALL-Clause. The CALL-Clause can appear as a StandAlone call and as an InQuery call. Both are supported by the Cypher-DSL.

Standalone procedure calls

Standalone calls are particular useful for VOID procedures. A VOID procedure is a procedure that does not declare any result fields and returns no result records and that has explicitly been declared as VOID. Let’s take the first example from here

var call = Cypher.call("db", "labels").build(); (1)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL db.labels()");

call = Cypher.call("db.labels").build();  (2)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL db.labels()");
1 Cypher.call returns a buildable statement that can be rendered. Cypher.call can be used with separate namespace and procedure name as shown here or
2 with a single argument equal to the name of the procedure.

Of course, arguments can be specified as expressions. Expressions can be literals as in [standalone-call-with-args], Cypher parameters (via Cypher.parameter) that bind your input or nested calls:

var call = Cypher
        .call("dbms.security.createUser")
        .withArgs(Cypher.literalOf("johnsmith"), Cypher.literalOf("h6u4%kr"), Cypher.literalFalse())
        .build();
assertThat(cypherRenderer.render(call))
        .isEqualTo("CALL dbms.security.createUser('johnsmith', 'h6u4%kr', false)");

Last but not least, the Cypher-DSL can of course YIELD the results from a standalone call:

var call = Cypher.call("dbms.procedures").yield("name", "signature").build(); (1)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL dbms.procedures() YIELD name, signature");

call = Cypher.call("dbms.procedures").yield(Cypher.name("name"), Cypher.name("signature")).build(); (2)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL dbms.procedures() YIELD name, signature");
1 Yielded items can be specified via string…
2 …or with symbolic names created earlier

A standalone call can spot a WHERE clause as well:

var name = Cypher.name("name");
var call = Cypher
        .call("dbms.listConfig")
        .withArgs(Cypher.literalOf("browser"))
        .yield(name)
        .where(name.matches("browser\\.allow.*"))
        .returning(Cypher.asterisk())
        .build();
assertThat(cypherRenderer.render(call)).isEqualTo(
        "CALL dbms.listConfig('browser') YIELD name WHERE name =~ 'browser\\\\.allow.*' RETURN *");
In-query procedure calls

In-query calls are only possible with non-void procedures. An In-query call happens inside the flow of a normal query. The mechanics to construct those calls via the Cypher-DSL are identical to standalone calls:

var label = Cypher.name("label");
var statement = Cypher
        .match(Cypher.anyNode().named("n")).with("n")
        .call("db.labels").yield(label).with(label)
        .returning(Functions.count(label).as("numLabels"))
        .build();
assertThat(cypherRenderer.render(statement)).isEqualTo(
        "MATCH (n) WITH n CALL db.labels() YIELD label WITH label RETURN count(label) AS numLabels");

A CALL can be used after a MATCH, a WITH and also a WHERE clause.

4.3.2. Use stored-procedure-calls as expressions (Calling custom functions)

All the mechanics described and shown above - how to define a custom call statement, supply it with arguments etc. - doesn’t distinguish between procedures and functions. Every stored procedure can be treated as a function - as long as the stored procedure returns a single value. It doesn’t matter if the single value returns a scalar or a list of objects. A list of objects is still a single value, in contrast to a stream of objects returned by a non-void procedure.

So the question is not how to call a stored custom function, but how to turn a call statement into an expression that can be used in any place in a query where an expression is valid. This is where asFunction comes in.

var p = Cypher.node("Person").named("p");
var createUuid = Cypher.call("apoc.create.uuid").asFunction(); (1)
var statement = Cypher.merge(p.withProperties(Cypher.mapOf("id", createUuid))) (2)
        .set(
                p.property("firstName").to(Cypher.literalOf("Michael")),
                p.property("surname").to(Cypher.literalOf("Hunger"))
        )
        .returning(p)
        .build();
assertThat(cypherRenderer.render(statement)).isEqualTo(
        "MERGE (p:`Person` {id: apoc.create.uuid()}) "
        + "SET p.firstName = 'Michael', p.surname = 'Hunger' "
        + "RETURN p");
1 First we define a call as seen earlier and turn it into an expression
2 This expression is than used as any other expression

Of course, arguments to those functions can be expressed as well, either as literals or expressions.

var p = Cypher.node("Person").named("p");
var createUuid = Cypher.call("apoc.create.uuid").asFunction(); (1)
var toCamelCase = Cypher.call("apoc.text.camelCase")
        .withArgs(Cypher.literalOf("first name")) (2)
        .asFunction();
var statement = Cypher.merge(p.withProperties(Cypher.mapOf("id",
        createUuid)))
        .set(p.property("surname").to(Cypher.literalOf("Simons")))
        .with(p)
        .call("apoc.create.setProperty").withArgs(
                p.getRequiredSymbolicName(),
                toCamelCase,
                Cypher.parameter("nameParam") (3)
        ).yield("node")
        .returning("node")
        .build();
assertThat(cypherRenderer.render(statement)).isEqualTo(
        "MERGE (p:`Person` {id: apoc.create.uuid()}) SET p.surname = 'Simons' "
        + "WITH p CALL apoc.create.setProperty(p, apoc.text.camelCase('first name'), $nameParam) "
        + "YIELD node RETURN node");
1 Same as before
2 A call to APOC’s camelCase function, taking in the literal of first name.
3 A call to another APOC function to which a parameter is passed. You find that corresponding placeholder as $nameParam in the following assert

4.3.3. Summary

Through Cypher.call any procedure or function can be called in case one of your favorite procedures is missing in org.neo4j.cypherdsl.core.Functions. All clauses, including YIELD and WHERE on procedures are supported. All procedures can be turned into functions. The Cypher-DSL however does not check if the procedure that is used as a function is actually eligible to do so.

If the Cypher-DSL misses an important builtin Neo4j function, please raise a ticket.

5. Parsing existing Cypher

5.1. Introduction

The cypher-dsl-parser module ins an optional add-on to the Cypher-DSL that takes your existing Cypher - either whole statements or fragments like clauses or expressions - and turn them into Cypher-DSL statements or expressions. Those fragments can be used to add custom Cypher to your generated statements without resorting to raw String literals. It allows sanitizing user input, add additional filters for labels and types to rewrite queries and more.

The parser itself is based on Neo4j’s official Cypher-Parser, thus supporting the same constructs as Neo4j itself. However, while we could theoretically parse all expressions that Neo4j 4.3.2 supports, we might cannot translate all of them into elements of the Cypher-DSL. In such cases an UnsupportedOperationException will be thrown.

The current version of the Cypher-DSL-Parser modules is rather big. This is due to the fact that we must shade the Scala-Lang library. Neo4j’s parsers depends on it. This is likely to change in the future. Until then, we rather not try to shade only the few required classes and risk a ClassNotFoundException, but include the whole library.
The decision for shading has been made for compatibility reasons: People may have a different Scala version in place or might even use this library embedded. So it’s safer to shade what we support and not depend on it.

5.2. Getting started

5.2.1. Add additional dependencies

Maven
Listing 14. Cypher-DSL parser added via Maven
<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-cypher-dsl-parser</artifactId>
    <version>2021.3.1</version>
</dependency>
Gradle
Listing 15. Gradle variant for additional dependencies
dependencies {
    implementation 'org.neo4j:neo4j-cypher-dsl-parser:2021.3.1'
}

5.2.2. Minimum JDK version

The Cypher-Parser requires JDK 11 to run which is the same version that Neo4j 4.3.2 requires.

5.2.3. Main entry point

The main entry point to parsing Cypher strings into Cypher-DSL objects is

Listing 16. Cypher Parser
import org.neo4j.cypherdsl.parser.CypherParser;

It provides a list of static methods:

Method What it does

parseNode

Parses a pattern like (n:Node {withOrWithout: 'properties'}) into a Node

parseRelationship

Parses a pattern like (m)-[{a: 'b', c: 'd'}]→(n) into a RelationshipPattern. The pattern might be a Relationship or RelationshipChain.

parseExpression

Parses an arbitrary expression.

parseClause

Parses a full clause like MATCH (m:Movie) or DELETE n etc. These clauses might be modified via callbacks and then passed on into org.neo4j.cypherdsl.core.Statement.of. This will take them in order and create a whole statement out of it. It is your responsibility to make sure those clauses are meaningful in the given order.

parseStatement

Parses a whole statement. The result can be rendered or used in a union or subquery call.

parse

An alias for parseStatement.

The README for the parser module itself contains not only our whole TCK for the parser, but also several examples of calling it. Have a look here: neo4j-cypher-dsl-parser/README.adoc.

All the methods mention above provide an overload taking in an additional org.neo4j.cypherdsl.parser.Option instance allowing to interact with the parser. Please have a look at the JavaAPI for information about the options class. The following examples show some ways of using it. Most of the configurable options represent ways to provide filters for labels or types or are callbacks when certain expressions are created.

5.3. Examples

5.3.1. Parsing user input and call in a subquery

This is helpful when you create an outer query that maybe enriched by a user. Here we assume the user does the right thing and don’t modify the query any further:

Listing 17. Just using the user supplied input
var userProvidedCypher
    = "MATCH (this)-[:LINK]-(o:Other) RETURN o as result"; (1)
var userStatement = CypherParser.parse(userProvidedCypher); (2)

var node = Cypher.node("Node").named("node");
var result = Cypher.name("result");
var cypher = Cypher (3)
    .match(node)
    .call((4)
        userStatement,
        node.as("this")
    )
    .returning(result.project("foo", "bar"))
    .build()
    .getCypher();

assertThat(cypher).isEqualTo(
    "MATCH (node:`Node`) "
    + "CALL {"
    + "WITH node "
    + "WITH node AS this " (5)
    + "MATCH (this)-[:`LINK`]-(o:`Other`) RETURN o AS result" (6)
    + "} "
    + "RETURN result{.foo, .bar}");
1 A valid standalone query that is also a valid subquery
2 Just parse it into a Statement object
3 Use the Cypher-DSL as explained throughout the docs
4 Use the overload of call that takes a Statement and a collection of expression that should be imported into the subquery
5 Notice how to WITH clauses are generated: The first one is the importing one, the second one the aliasing one
6 This is the original query

5.3.2. Ensure an alias for the return clause

We are going to work with the same test as in Listing 17, so this is not repeated. Here we make sure the query supplied by the user returns something with a required alias.

Listing 18. Using a callback to make sure that a query has an alias
var userProvidedCypher = "MATCH (this)-[:LINK]-(o:Other) RETURN o";

Function<Expression, AliasedExpression> ensureAlias = r -> {
    if (!(r instanceof AliasedExpression)) {
        return r.as("result");
    }
    return (AliasedExpression) r;
}; (1)

var options = Options.newOptions() (2)
    .withCallback( (3)
        ExpressionCreatedEventType.ON_RETURN_ITEM,
        AliasedExpression.class,
        ensureAlias
    )
    .build();

var userStatement = CypherParser.parse(userProvidedCypher, options); (4)
1 This is a Function that receives an expressions and returns a new one. It checks if the provided expressions obeys to some criteria: Here being something that is aliased or not
2 We start building new options
3 The callback from step one is passed as callback to the event ON_RETURN_ITEM and will be called for every item
4 The final option instance will be applied to the parser. The statement will render to the same result as the first example.

5.3.3. Preventing certain things

Callbacks can of course be used to prevent things. Any exception thrown will halt the parsing. Listing 19 shows how:

Listing 19. Preventing input that deletes properties
var userProvidedCypher = "MATCH (this)-[:LINK]-(o:Other) REMOVE this.something RETURN o";

UnaryOperator<Expression> preventPropertyDeletion = r -> {
    throw new RuntimeException("Not allowed to remove properties!"); (1)
};

var options = Options.newOptions()
    .withCallback( (2)
        ExpressionCreatedEventType.ON_REMOVE_PROPERTY,
        Expression.class,
        preventPropertyDeletion
    )
    .build();

assertThatExceptionOfType(RuntimeException.class)
    .isThrownBy(() -> CypherParser.parse(userProvidedCypher, options)); (3)
1 Create a callback that just throws an unchecked exception
2 Configure it for the event that should be prevented
3 Parsing will not be possible

5.3.4. Shape the return clause the way you want

The parser provides ReturnDefinition as value object. It contains information to be passed to the Clauses factory to shape a RETURN clause the way you need:

Listing 20. Shaping the return clause
var userProvidedCypher = "MATCH (this)-[:LINK]-(o:Other) RETURN distinct this, o LIMIT 23";

Function<ReturnDefinition, Return> returnClauseFactory = d -> { (1)
    var finalExpressionsReturned = d.getExpressions().stream()
        .filter(e -> e instanceof SymbolicName && "o".equals(((SymbolicName) e).getValue()))
        .map(e -> e.as("result"))
        .collect(Collectors.<Expression>toList());

    return Clauses.returning(
        false,
        finalExpressionsReturned,
        List.of(Cypher.name("o").property("x").descending()),
        d.getOptionalSkip(), d.getOptionalLimit()
    );
};

var options = Options.newOptions()
    .withReturnClauseFactory(returnClauseFactory) (2)
    .build();

var userStatement = CypherParser.parse(userProvidedCypher, options);
var cypher = userStatement.getCypher();

assertThat(cypher) (3)
    .isEqualTo("MATCH (this)-[:`LINK`]-(o:`Other`) RETURN o AS result ORDER BY o.x DESC LIMIT 23");
1 Create a factory method that takes in a definition and uses its information to build the RETURN. Or examples filters the attributes being returned and enforces an alias. It also adds some arbitrary sorting and keeps sort and limit values from the original
2 It than is parsed to the options
3 The statement has the new RETURN clause.

5.3.5. Enforcing labels

The parser can enforce labels to be present or absent with filters. This can be individually achieved when parsing node patterns, setting or removing labels with a BiFunction like the following:

Listing 21. Shaping the return clause
final BiFunction<LabelParsedEventType, Collection<String>, Collection<String>> makeSureALabelIsPresent = (e, c) -> {

    var finalLabels = new LinkedHashSet<>(c);
    switch (e) { (1)
        case ON_NODE_PATTERN:
            finalLabels.add("ForcedLabel");
            return finalLabels;
        case ON_SET:
            finalLabels.add("Modified");
            return finalLabels;
        case ON_REMOVE:
            finalLabels.remove("ForcedLabel");
            return finalLabels;
        default:
            return c;
    }
};
1 Decide on the event type what is supposed to happen

Putting this function in action involves the Options class again:

Listing 22. Enforcing a label is always set on the pattern
var options =
    Options.newOptions().withLabelFilter(makeSureALabelIsPresent).build();
var statement = CypherParser
    .parseStatement("MATCH (n:Movie) RETURN n", options)
    .getCypher();

assertThat(statement).isEqualTo("MATCH (n:`Movie`:`ForcedLabel`) RETURN n");

This can safely be used to match only nodes spotting such a label for example.

Listing 23. Enforcing that a new collection of labels always contains a specific
var options =
    Options.newOptions().withLabelFilter(makeSureALabelIsPresent).build();
var statement = CypherParser
    .parseStatement("MATCH (n:Movie) SET n:`Comedy` RETURN n", options)
    .getCypher();

assertThat(statement).isEqualTo("MATCH (n:`Movie`:`ForcedLabel`) SET n:`Comedy`:`Modified` RETURN n");

Of course, we can prevent a label to be removed:

Listing 24. Preventing a specific label to be removed
var options = Options.newOptions().withLabelFilter(makeSureALabelIsPresent).build();
var statement = CypherParser
    .parseStatement("MATCH (n:Movie) REMOVE n:`Comedy`:`ForcedLabel` RETURN n", options)
    .getCypher();

assertThat(statement).isEqualTo("MATCH (n:`Movie`:`ForcedLabel`) REMOVE n:`Comedy` RETURN n");

Changing relationship types via a filter is possible as well, but as relationships might only have one type, the number of usecases is smaller.

5.3.6. Combining the parser with SDN’s CypherdslConditionExecutor

Spring Data Neo4j 6 provides CypherdslConditionExecutor. This is a fragment that adds the capability to execute statements with added conditions to a Neo4jRepository.

Given the following repository:

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import org.springframework.data.neo4j.repository.support.CypherdslStatementExecutor;

public interface PeopleRepository extends Neo4jRepository<Person, Long>,
    CypherdslConditionExecutor<Person>, (1)
    CypherdslStatementExecutor<Person> { (2)
}
1 Allows to just add conditions to our generated queries
2 Provides an alternative to using @Query with strings

One possible use case is presented in this service:

Iterable<Person> findPeopleBornAfterThe70tiesAnd(String additionalConditions) {

    return peopleRepository.findAll(
        PERSON.BORN.gte(Cypher.literalOf(1980))
            .and(CypherParser.parseExpression(additionalConditions).asCondition()) (1)
    );
}
1 The condition that only people born later than 1980 is hard coded in the service. An arbitrary String is than parsed into a condition and attached via AND. Thus, only valid cypher can go in there and with filters and callbacks, preconditions of that Cypher can be asserted.

The downside to the above solution is that the query fragment passed to the service and eventually the repository must know the root node (which is n in case of SDN 6) and the caller code might look like this:

var exchange = restTemplate.exchange(
    "/api/people/v1/findPeopleBornAfterThe70ties?conditions={conditions}",
    HttpMethod.GET,
    null, new ParameterizedTypeReference<List<Person>>() { },
    "n.name contains \"Ricci\" OR n.name ends with 'Hirsch'"
);

Notice n.name etc. We could change the service method slightly and apply a callback like this:

Iterable<Person> findPeopleBornAfterThe70tiesAndV2(String additionalConditions) {

    Function<Expression, Expression> enforceReference =
        e -> Constants.NAME_OF_ROOT_NODE.property(((SymbolicName) e).getValue()); (1)
    var parserOptions = Options.newOptions()
        .withCallback(
            ExpressionCreatedEventType.ON_NEW_VARIABLE,
            Expression.class,
            enforceReference
        ) (2)
        .build();

    return peopleRepository.findAll(
        PERSON.BORN.gte(Cypher.literalOf(1980)).and(
            CypherParser.parseExpression(
                additionalConditions,
                parserOptions (3)
            ).asCondition()
        )
    );
}
1 Create a function that takes the value of a variable created in the fragment and use it to look up a property on the SDN root node.
2 Create an instance of parsing options. It’s probably a good idea todo this once and store them away in an instance variable. Options are thread safe.
3 Apply them when calling the corresponding parse method

Now it’s enough to pass "name contains \"Ricci\" OR name ends with 'Hirsch'" into the exchange presented above and things will work out of the box. Further validation and sanitiy checks are of course up to you.

6. Integration with the Neo4j-Java-Driver

6.1. Introduction

The Cypher-DSL is - apart from API Guardian - dependency free. It has however some optional dependency you can add to use more functionality. One of those dependencies is the Neo4j-Java-Driver. The Neo4j-Java-Driver - or sometimes Bolt for Java or Bolt-Driver - implements Neo4j’s Bolt protocol and provides a connection to a single Neo4j instance or a cluster.

While the Cypher-DSL creates statements that eventually will be rendered as a String, it has some knowledge about the statements generated:

  • Will the statements return something?

  • Or will the statements just run updates?

  • Will the statements be profiled?

  • Do the statements carry parameter definitions and values for parameters?

We can use that knowledge to provide a thin shim for using the Cypher-DSL with the API the Neo4j-Java-Driver provides without reinventing the wheel.

In most cases the Cypher-DSL will generate one of two types of statements:

  • org.neo4j.cypherdsl.core.Statement: A statement without a result. It can be executed, but it cannot be fetched. You will be able to check for the number of affected database entities via the driver’s ResultSummary.

  • org.neo4j.cypherdsl.core.ResultStatement: A statement known to have a result. It can be executed or fetched.

Both interfaces can be turned into ExecutableStatements, providing the appropriate methods, which will be discussed below.

This API is not required to use the driver nor is the driver required for using the Cypher-DSL. The main API provided with the Cypher-DSL is Statement#getCypher() for retrieving a Cypher-String and optional call to Statement#getParameters() to retrieve parameters stored with the statement.
Those can be used in many ways and forms with the Neo4j-Java-Driver or mapping frameworks like Neo4j-OGM or Spring Data Neo4j 6+.

6.2. Add additional dependencies

Listing 25. Neo4j-Java-Driver added via Maven
<dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
    <artifactId>4.2.7</artifactId>
</dependency>

Any 4.x version of the Driver will work, we currently test against 4.2.7. In case you want to use our integration with reactive sessions, you will have to add Project Reactor. To the outside world we expose - much like the driver - the vendor agnostic Reactive Streams spec.

The coordinates of Project reactor are io.projectreactor:reactor-core. Those can be added in the appropriate form to a Maven or Gradle project. We test with 2020.0.6 of Reactor and use their BOM module imported into dependency management instead of a fixed version for the core module itself:

Listing 26. Optional Project reactor dependencies.
<dependencyManagement>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-bom</artifactId>
        <version>2020.0.6</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencyManagement>

6.3. Imperative (blocking) API

You create an instance of the Neo4j driver. This instance should be a long living instance in your application:

Listing 27. Create a driver instance
driver = GraphDatabase.driver(neo4j.getBoltUrl(), AuthTokens.basic("neo4j", neo4j.getAdminPassword()));

Both org.neo4j.driver.Session and org.neo4j.driver.Transaction implement a QueryRunner (the same applies for the reactive variants). It is your responsibility to pick the right kind of interaction with the database (auto commit, transactions or transactional functions). If you pass a Session to the Cypher-DSL, auto commit transactions will be used. If you pass in a transaction object, unmanaged transactions will be used and you must commit or rollback as needed and take care for retries.

A safe bet is to pass one of the Cypher-DSL methods to the driver as a READ or WRITE transaction.

The Cypher-DSL won’t close, commit or rollback anything you pass to it. You open resources, you close them. In all cases where the Cypher-DSL opens or provides a resource, it will take care closing it correctly.

Our goal when writing this small integration with the official driver was to apply best practices when working with the driver without adding too much cognitive overhead or introducing new types.

6.3.1. Executing statements

We first have a look at statements that don’t return data, but may execute updates or call stored procedures. We will use the following listing as example:

Listing 28. Example statement that does not return any data.
var m = Cypher.node("Test").named("m");
var statement = ExecutableStatement.of(Cypher.create(m)
    .set(m.property("name").to(Cypher.anonParameter("statementWithoutResult")))
    .build());
Inside transactional functions

The driver has the concept of transactional functions. This basically describes a transaction manager understanding the semantics of a Neo4j cluster, possible errors that can occur, routing and such. The transaction manager is able to retry statements in certain error cases.

Transactional functions are passed as TransactionalWork<T> to a session. The executeWith API on Statement fits this interface and you can pass it as a method reference. It will always return a result summary:

Listing 29. Executing statements in a managed transaction
try (var session = driver.session()) { (1)
    var summary = session.writeTransaction(statement::executeWith); (2)
    assertThat(summary.counters().nodesCreated()).isEqualTo(1); (3)
}
1 Open a session
2 Pass the executeWith method from the Cypher-DSL statement to the driver
3 Do something with the result summary
Read more about transactional functions in the official manual: Imperative sessions and transaction functions.
With an unmanaged transaction

A transaction you retrieve from a session is called unmanaged transaction. It is your responsibility to commit or rollback and close it.

Listing 30. Executing statements in an unmanaged transaction
try (var session = driver.session();
    var tx = session.beginTransaction() (1)
) {
    var summary = statement.executeWith(tx); (2)
    tx.commit(); (3)

    assertThat(summary.counters().nodesCreated()).isEqualTo(1);
}
1 Get a transaction from the session
2 Pass it to the statement, execute and get the result summary
3 Do more things in the transaction, than commit or rollback it

The advantage here is that you have more control and are not limited to idempotent operations for the transactional function. The disadvantage has been hidden away in that example: You have to take care of exceptions that might happen due to cluster errors yourself.

If you want to use an auto-commit transaction (for example for a PERIODIC COMMIT or an APOC call), just pass a Session to executeWith.

6.3.2. Fetching data

The following examples will use this statement, returning the title of movies in the Movie-Graph:

Listing 31. Example statement that does return data.
var m = Cypher.node("Movie").named("m");
var statement = ExecutableStatement.of(Cypher.match(m)
    .returning(m.property("title").as("title")).build());

We provide two forms of accessing data via the imperative API: Pulling everything into a list or streaming records. The reactive API is fully non-blocking and uses a publishing API.

Inside transactional functions
Listing 32. Fetching statements in a managed transaction
try (var session = driver.session()) { (1)

    var moviesTitles = session.readTransaction(statement::fetchWith) (2)
        .stream() (3)
        .map(r -> r.get("title").asString())
        .collect(Collectors.toList());

    assertMovieTitleList(moviesTitles);
}
1 Open a session
2 Pass the fetchWith method from the Cypher-DSL statement to the driver
3 The readTransaction (or writeTransaction) returns whatever the transactional work returns Our transactional work fetchWith returns a java.util.List of records Here we turn it into a Java stream (not connected to Neo4j) and map and collect it

fetchWith does support a second parameter, a mapping function Function<Record, T> mappingFunction. We can use this with a transactional function as well and do the mapping inside the transaction. For the example, this form here is easier to read, but the other one may have benefits in regards of memory allocation.

You also can stream the result. In contrast to the plain Driver API, we don’t hand out the stream itself but expect a closure dealing with the stream. We do this to make it impossible to use the stream - which is still connected to the database - outside the transaction. With managed transactions, it looks like this:

Listing 33. Streaming statements in a managed transaction
try (var session = driver.session()) { (1)

    var summary = session.readTransaction(tx -> (2)
        statement.streamWith(tx, s -> { (3)
            var moviesTitles = s.map(r -> r.get("title").asString())
                .collect(Collectors.toList()); (4)
            assertMovieTitleList(moviesTitles);
        })
    );
    assertThat(summary.query().text()).isEqualTo(statement.getCypher());
}
1 Open a session
2 Open a managed transaction, but don’t pass a method reference this time
3 Instead, call streamWith, pass the transaction and a consumer for the stream s.
4 Inside this example, we fully materialize the stream and map records as they pass by As the streaming method will always return the result summary, this will also be the type of the transaction function
With an unmanaged transaction

Now the same examples with unmanaged transactions. First, listening them. Here we pass a mapping function directly to fetchWith. The pattern of opening a session and transaction is the same as before.

Listing 34. Listening results with an unmanaged transaction
try (var session = driver.session();
    var tx = session.beginTransaction()) {

    var moviesTitles = statement.fetchWith(
        tx, (1)
        r -> r.get("title").asString() (2)
    );
    tx.commit();

    assertMovieTitleList(moviesTitles);
}
1 The unmanaged transaction passed to the statement,
2 and the mapping function applied. moviesTitle is a List containing String elements in the end.

The streaming example looks pretty similar to the one applied with the transactional function. As we pass the Session object itself, we are using an auto-commit transaction.

Listing 35. Streaming results with an auto-commit-transaction
try (var session = driver.session()) {

    var summary = statement.streamWith(session, stream -> {
        var moviesTitles = stream
            .map(r -> r.get("title").asString())
            .collect(Collectors.toList());
        assertMovieTitleList(moviesTitles);
    });
    assertThat(summary.query().text()).isEqualTo(statement.getCypher());
}

Again, please note that the stream is only available in the closure. It is not meant to live outside the transactional scope.

6.4. Reactive API

For the reactive API the variants inside a transactional function are focused here as they are probably the most complex ones to get right: You have to deal with to resources: The session itself, and the inner publisher returned from the transactional function.

To create a reactive, executable statement from either a Statement or ResultStatement use the factory methods from ReactiveExecutableStatement:

Listing 36. Reactive Example statement that does not return any data.
var m = Cypher.node("Test").named("m");
var statement = ReactiveExecutableStatement.of(Cypher.create(m)
    .set(m.property("name").to(Cypher.anonParameter("statementsWithoutResultReactive")))
    .build());

6.4.1. Executing statements

The most important part in the following example is not necessarily our API, but the use of Reactors fromDirect operator when converting a publisher of a single element into a Mono: That operator trusts the user that the publisher will return zero or one element and doesn’t cancel after the first item being emitted and thus rolling back the transaction:

Listing 37. Reactive streaming results with an managed transaction
Mono.usingWhen(
    Mono.fromSupplier(driver::rxSession), (1)
    s -> Mono.fromDirect(s.writeTransaction(statement::executeWith)), (2)
    RxSession::close (3)
).as(StepVerifier::create)
    .expectNextMatches(r -> r.counters().nodesCreated() == 1) (4)
    .verifyComplete();
1 The session should be opened non-blocking and as late as possible
2 This is the actual call from the Cypher-DSL into the driver
3 Closed what you opened
4 The reactive executeWith-variant returns the summary as well

6.4.2. Fetching results

With a reactive, transactional function

This example uses our original statement and arbitrary skips 2 elements and then takes only 30 elements. As the transaction is managed by the driver, canceling the publisher via take() won’t rollback the transaction.

Listing 38. Reactive streaming results with an managed transaction
Flux.usingWhen(
    Mono.fromSupplier(driver::rxSession),
    s -> s.readTransaction(statement::fetchWith),
    RxSession::close
)
    .skip(2)
    .take(30)
    .as(StepVerifier::create)
    .expectNextCount(30)
    .verifyComplete();
Inside an auto-commit transaction

One use case might be the bulk loading of data. By using an auto-commit transaction we effectively state that we neither manage a transaction ourselves nor we want the driver to manage one for us. In this case we use Neo4j’s LOAD CSV clause that basically describes a Cypher script that runs in its own transaction. The server might or might not start streaming results after the configured periodic commit rate has been hit:

Listing 39. Using PERIODIC COMMIT LOAD CSV from a reactive auto-commit transaction:
var row = Cypher.name("row");
var a = Cypher.node("Author").withProperties("name", Functions.trim(Cypher.name("author"))).named("a");
var m = Cypher.node("Book").withProperties("name", row.property("Title")).named("b");

var statement = ReactiveExecutableStatement.of(Cypher.usingPeriodicCommit(10)
    .loadCSV(URI.create("file:///books.csv"), true).as(row).withFieldTerminator(";")
    .create(m)
    .with(m.getRequiredSymbolicName(), row)
    .unwind(Functions.split(row.property("Authors"), "&")).as("author")
    .merge(a)
    .create(a.relationshipTo(m, "WROTE").named("r"))
    .returningDistinct(m.property("name").as("name"))
    .build());

Flux.using(driver::rxSession, statement::fetchWith, RxSession::close)
    .as(StepVerifier::create)
    .expectNextCount(50)
    .verifyComplete();

7. Building a static meta model

7.1. Concepts

7.1.1. A static meta model is optional

First let’s stress this: a static meta model is optional for the Cypher-DSL. It is completely OK to use the Cypher-DSL as shown in the examples in the "How to use it" part: all labels, types and properties can be named as you go. The Cypher-DSL will still give you type-safety in regard to the Cypher’s syntax.

This fits nicely with Neo4j’s capabilities: Neo4j is a schemaless database. Nodes, their relationships between each other, their labels and properties can be changed as you go but all of this information can still be queried.

In a schemaless database or a database with dynamic scheme the scheme is often defined by the application. This definition takes many forms: It can be through an object mapper like Neo4j-OGM or Spring Data Neo4j 6+, or maybe in form of Graph-QL schemes.

Another source may be the information we can retrieve from the database itself via db.schema.nodeTypeProperties and db.schema.relTypeProperties.

7.1.2. Building blocks

The Cypher DSL offers the following building blocks as part of the public API:

  • Two pattern elements:

    • Nodes via Node (and its default implementation NodeBase)

    • Relationships via Relationship (and its default implementation RelationshipBase)

  • Property, which is a holder for properties

When you use Cypher-DSL like this:

var m = Cypher.node("Movie").named("m");
var statement = Cypher.match(m).returning(m).build();

m will be a Node instance having the label Movie and an alias of m. m can be used everywhere where a pattern element can be used according to the openCypher spec. You don’t have to care about its type. That’s why we vouched for the JDK11+ local type inference here and omitted the declaration of the type Node: it just reads better.

7.1.3. A very simple, static model

Both NodeBase and RelationshipBase are meant to be extended to put your static model into something that is usable with the Cypher-DSL.

Nodes

Start by extending NodeBase like this:

import java.util.List;

import org.neo4j.cypherdsl.core.MapExpression;
import org.neo4j.cypherdsl.core.NodeBase;
import org.neo4j.cypherdsl.core.NodeLabel;
import org.neo4j.cypherdsl.core.Properties;
import org.neo4j.cypherdsl.core.SymbolicName;

public final class Movie extends NodeBase<Movie> { (1)

    public static final Movie MOVIE = new Movie(); (2)

    public Movie() {
        super("Movie"); (3)
    }

    private Movie(SymbolicName symbolicName, List<NodeLabel> labels, Properties properties) { (4)
        super(symbolicName, labels, properties);
    }

    @Override
    public Movie named(SymbolicName newSymbolicName) { (5)

        return new Movie(newSymbolicName, getLabels(), getProperties());
    }

    @Override
    public Movie withProperties(MapExpression newProperties) { (6)

        return new Movie(getSymbolicName().orElse(null), getLabels(), Properties.create(newProperties));
    }
}
1 Extend from NodeBase and specify your class as a "self" type-argument
2 Optional: Create one static instance of your model
3 This is where you specify one or more label
4 This constructor is optional, it is used in the next two steps
5 named must be overridden and must return new copies of the node, with the changed symbolic name. It must be overridden to guarantee type integrity.
6 Same as above

With that in place, you can already use it like this:

var cypher = Cypher.match(Movie.MOVIE)
    .returning(Movie.MOVIE)
    .build().getCypher();

and it would generate a Cypher string like this: "MATCH (jwKyXzwS000:`Movie) RETURN jwKyXzwS000"`, with generated variable names.

If you don’t like them, you can just rename one instance of the movie-model like this:

var movie = Movie.MOVIE.named("m");
var cypher = Cypher.match(movie)
    .returning(movie)
    .build().getCypher();

Of course, properties belong into a model as well. You add them like this:

import org.neo4j.cypherdsl.core.Property;

public final class Movie extends NodeBase<Movie> { (1)

    public final Property TITLE = this.property("title"); (2)
}
1 Same class before, extending from NodeBase.
2 Use this and the property method to create a new Property instance, stored on the given instance

A possible usage scenario looks like this:

var movie = Movie.MOVIE.named("m");
var cypher = Cypher.match(movie)
    .where(movie.TITLE.isEqualTo(Cypher.literalOf("The Matrix"))) (1)
    .returning(movie)
    .build().getCypher();

Assertions.assertThat(cypher)
    .isEqualTo("MATCH (m:`Movie`) WHERE m.title = 'The Matrix' RETURN m");
1 Make sure to use the renamed instance everywhere. Here: For accessing the property. Alternatively, don’t rename.
Relationships

Relationships are a bit more complicated. Relationships of the same type can be used between nodes with different labels. We have these scenarios:

  • (s:LabelA) - (r:SomeType) → (e:LabelB):

  • (s:LabelA) - (r:SomeType) → (e)

  • (s) - (r:SomeType) → (e)

We either have a type that is used only between the same set of labels, or a type is used always with one fixed label or a type is used between arbitrary labels.

To accommodate for that, the default relationship implementation, RelationshipBase, spots three type parameters:

public class RelationshipBase<S extends NodeBase<?>, E extends NodeBase<?>, SELF extends RelationshipBase<S, E, SELF>> {
}

S is the type of a start node, E of an end node and SELF is the concrete implementation itself. The public API of RelationshipBase enforces a direction from start to end (LTR, left to right).

We just have a look at the first case to make the concepts clear. We model the ACTED_IN relationship of the movie graph. It exists between people and movies (going from person to movie) and has an attribute roles:

import org.neo4j.cypherdsl.core.MapExpression;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Properties;
import org.neo4j.cypherdsl.core.Property;
import org.neo4j.cypherdsl.core.RelationshipBase;
import org.neo4j.cypherdsl.core.SymbolicName;

public final class ActedIn extends RelationshipBase<Person, Movie, ActedIn> { (1)

    public static final String $TYPE = "ACTED_IN";

    public final Property ROLE = this.property("role"); (2)

    protected ActedIn(Person start, Movie end) {
        super(start, $TYPE, end); (3)
    }

    private ActedIn(SymbolicName symbolicName, Node start, Properties properties, Node end) { (4)
        super(symbolicName, start, $TYPE, properties, end);
    }

    @Override
    public ActedIn named(SymbolicName newSymbolicName) { (5)

        return new ActedIn(newSymbolicName, getLeft(), getDetails().getProperties(), getRight());
    }

    @Override
    public ActedIn withProperties(MapExpression newProperties) { (6)

        return new ActedIn(getSymbolicName().orElse(null), getLeft(), Properties.create(newProperties), getRight());
    }
}
1 The base class, with 3 concrete types. A Person, the Movie from [static-movie] and the type itself.
2 Same idea with as with nodes: Store all properties as final attributes.
3 There is no default constructor for a relationship: Start and end node must be specified. The type is fixed.
4 Copy constructor for the following two methods
5 Required to rename this relationship
6 Required for querying properties

In contrast to the movie, we don’t need a static attribute allowing access to the relationship. This is stored at the owner. In this case, the Person:

public final class Person extends NodeBase<Person> {

    public static final Person PERSON = new Person();

    public final Directed<Movie> DIRECTED = new Directed<>(this, Movie.MOVIE);

    public final ActedIn ACTED_IN = new ActedIn(this, Movie.MOVIE); (1)

    public final Property NAME = this.property("name");

    public final Property FIRST_NAME = this.property("firstName");

    public final Property BORN = this.property("born");
}
1 We create the relationship properties with this concrete instance pointing to another, concrete instance.

7.1.4. Summary

While there are possible other ways to define a static meta model with the Cypher-DSL, this is the way we create them with the neo4j-cypher-dsl-codegen models.

7.2. Possible Usage

Please assume we did model the "Movie graph" (:play movies in Neo4j-Browser) with the following scheme:

movie graph

and these classes, which have been generated with the building blocks described earlier (the required functions have been omitted for brevity):

final class Movie extends NodeBase<Movie> {

    public static final Movie MOVIE = new Movie();

    public final Property TAGLINE = this.property("tagline");

    public final Property TITLE = this.property("title");

    public final Property RELEASED = this.property("released");

    public Movie() {
        super("Movie");
    }
}

final class Person extends NodeBase<Person> {

    public static final Person PERSON = new Person();

    public final Property NAME = this.property("name");

    public final Property FIRST_NAME = this.property("firstName");

    public final Directed<Movie> DIRECTED = new Directed<>(this, Movie.MOVIE);

    public final ActedIn ACTED_IN = new ActedIn(this, Movie.MOVIE);

    public final Property BORN = this.property("born");

    public Person() {
        super("Person");
    }
}

final class ActedIn extends RelationshipBase<Person, Movie, ActedIn> {

    public static final String $TYPE = "ACTED_IN";

    public final Property ROLE = this.property("role");

    protected ActedIn(Person start, Movie end) {
        super(start, $TYPE, end);
    }
}

final class Directed<E extends NodeBase<?>> extends RelationshipBase<Person, E, Directed<E>> {

    public static final String $TYPE  = "DIRECTED";

    protected Directed(Person start, E end) {
        super(start, $TYPE, end);
    }
}

7.2.1. Work with properties

Properties can be used like normal objects:

var cypher = Cypher.match(Person.PERSON)
    .returning(Person.PERSON.NAME, Person.PERSON.BORN)
    .build().getCypher();

Assertions.assertThat(cypher)
    .matches("MATCH \\(\\w+:`Person`\\) RETURN \\w+\\.name, \\w+\\.born");

Of course, new properties can be derived:

var cypher = Cypher.match(Person.PERSON)
    .returning(Person.PERSON.NAME.concat(Cypher.literalOf(" whatever")))
    .build().getCypher();

Assertions.assertThat(cypher)
    .matches("MATCH \\(\\w+:`Person`\\) RETURN \\(\\w+\\.name \\+ ' whatever'\\)");

7.2.2. Query nodes or relationships by properties

Use withProperties (and named you like) to model your queries as needed. Applicable to properties of nodes such as the title:

var movie = Movie.MOVIE.withProperties(Movie.MOVIE.TITLE, Cypher.literalOf("The Matrix")).named("m1");
var cypher = Cypher.match(movie)
    .returning(movie)
    .build().getCypher();

Assertions.assertThat(cypher)
    .isEqualTo("MATCH (m1:`Movie` {title: 'The Matrix'}) RETURN m1");

and relationships:

var actedIn = Person.PERSON.ACTED_IN.withProperties(Person.PERSON.ACTED_IN.ROLE, Cypher.literalOf("Neo"));
var cypher = Cypher.match(actedIn)
    .returning(Movie.MOVIE)
    .build().getCypher();

Assertions.assertThat(cypher)
    .matches("MATCH \\(\\w+:`Person`\\)-\\[\\w+:`ACTED_IN` \\{role: 'Neo'}]->\\(\\w+:`Movie`\\) RETURN \\w+");

Note that the query will look like this, as we didn’t rename the objects and they used generated names:

`MATCH (dZVpwHhe000:`Person`)-[JcVKsSrn001:`ACTED_IN` {role: 'Neo'}]->(cDWeUJSI002:`Movie`) RETURN cDWeUJSI002`` as we didn't specify aliases)

7.2.3. Work with relationships

Relationships can be worked with like with properties:

var cypher = Cypher.match(Person.PERSON.DIRECTED)
    .match(Person.PERSON.ACTED_IN)
    .returning(Person.PERSON.DIRECTED, Person.PERSON.ACTED_IN)
    .build().getCypher();

They are quite flexible together with the inverse method. The following example also shows how to include non-static parts:

var otherPerson = Person.PERSON.named("o");
var cypher = Cypher.match(
        Person.PERSON.DIRECTED.inverse()
            .relationshipTo(otherPerson, "FOLLOWS") (1)
    )
    .where(otherPerson.NAME.isEqualTo(Cypher.literalOf("Someone")))
    .returning(Person.PERSON)
    .build().getCypher();

Assertions.assertThat(cypher)
    .matches(
        "MATCH \\(\\w+:`Movie`\\)<-\\[:`DIRECTED`]-\\(\\w+:`Person`\\)-\\[:`FOLLOWS`]->\\(o:`Person`\\) WHERE o\\.name = 'Someone' RETURN \\w+");
1 Using a non-static fragment

7.3. The Spring Data Neo4j 6 annotation processor

We provide a Java annotation processor for Spring Data Neo4j under the following coordinates:

Listing 40. Coordinates of the SDN 6 annotation processor
org.neo4j:neo4j-cypher-dsl-codegen-sdn6:2021.3.1

The annotation processor understands classes annotated with @Node and @RelationshipProperties. Inside those classes @Relationship and @Property are read.

The processor generates a static meta model for each annotated class found in the same package with an underscore (_) added to the name.

The processor needs Spring Data Neo4j 6 and the Cypher-DSL in version 2021.1.0 or later on it’s classpath. We recommend using it explicitly on the separate annotation processor classpath (via --processor-path to javac).

Please make sure to use @Relationship when you use the annotation processor. While we do our best to detect possible, implicit associations during annotation processing, we can’t load classes that are being processed that very moment to check if the Spring infrastructure would provide a custom converter for them and make them a simple property. We won’t generate fields when in doubt but relationships if we find the corresponding class.

7.3.1. Configure your build

Maven

As a Maven user, please configure the build as follows:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <annotationProcessorPaths>
                    <annotationProcessorPath>
                        <groupId>org.neo4j</groupId>
                        <artifactId>neo4j-cypher-dsl-codegen-sdn6</artifactId>
                        <version>{neo4j-cypher-dsl.version}</version>
                    </annotationProcessorPath>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
Gradle

In a Gradle project, please add the following:

dependencies {
    annotationProcessor 'org.neo4j:neo4j-cypher-dsl-codegen-sdn6:2021.3.1'
}
As dependency on the classpath
We recommend the annotation processor path for two reasons: The processor needs SDN as dependency. While SDN is already on the classpath, it might not fit the one we build the annotation processor against exactly. While the processor is lenient in that regard, your dependency setup might not. Furthermore: Why should you have the annotation processor as a dependency in your final artifact? This would be unnecessary.

If you insist on having the SDN 6 annotation processor on the standard class path, please include with your Spring Data Neo4j 6 application like as follows to avoid dependency conflicts:

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-cypher-dsl-codegen-sdn6</artifactId>
    <version>{neo4j-cypher-dsl.version}</version>
    <optional>true</optional>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-neo4j</artifactId>
        </exclusion>
    </exclusions>
</dependency>

7.3.2. Usage

The processor supports the following arguments:

Name Meaning

org.neo4j.cypherdsl.codegen.prefix,

An optional prefix for the generated classes

org.neo4j.cypherdsl.codegen.suffix

An optional suffix for the generated classes

org.neo4j.cypherdsl.codegen.indent_style

The indent style (Use TAB for tabs, SPACE for spaces)

org.neo4j.cypherdsl.codegen.indent_size

The number of whitespaces for the indent style SPACE

org.neo4j.cypherdsl.codegen.timestamp

An optional timestamp in ISO_OFFSET_DATE_TIME format for the generated classes. Defaults to the time of generation.

org.neo4j.cypherdsl.codegen.add_at_generated

A flag whether @Generated should be added

org.neo4j.cypherdsl.codegen.sdn.custom_converter_classes

A comma separated list of custom Spring converter classes (Can be Converter, GenericConverter, ConverterAware, pretty much everything you can register with org.springframework.data.neo4j.core.convert.Neo4jConversions ). However, these converters must have a default-non-args constructor in the context of the annotation processor.

The generated classes can be used in a variety of places:

import java.util.Collection;
import java.util.List;

import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Functions;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
final class MovieService {

    private final MovieRepository movieRepository;

    MovieService(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }

    List<Movie> findAll() {

        return movieRepository
            .findAll(Sort.by(Movie_.MOVIE.TITLE.getName()).ascending()); (1)
    }
}
1 Pass the name of the property TITLE to Spring Data’s sort facility

Spring Data Neo4j 6.1 has two additional query fragments, that makes working with the Cypher-DSL very efficient:

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import org.springframework.data.neo4j.repository.support.CypherdslStatementExecutor;

public interface PeopleRepository extends Neo4jRepository<Person, Long>,
    CypherdslConditionExecutor<Person>, (1)
    CypherdslStatementExecutor<Person> { (2)
}
1 Allows to just add conditions to our generated queries
2 Provides an alternative to using @Query with strings

Both interfaces are independent of each other, they can be used together like in this example or separately (just picking the one you need). You can read more about them in the Spring Data Neo4j 6.1 documentation, chapter Spring Data Neo4j Extensions.

The repository above can be used like this:

import java.util.Optional;
import java.util.function.Function;

import org.neo4j.cypherdsl.core.Conditions;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.Functions;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.stereotype.Service;

@Service
final class PeopleService {

    static final Person_ PERSON = Person_.PERSON.named("n");

    private final PeopleRepository peopleRepository;

    PeopleService(PeopleRepository peopleRepository) {
        this.peopleRepository = peopleRepository;
    }

    Iterable<Person> findPeopleBornInThe70tiesOr(Optional<String> optionalName) {

        return peopleRepository.findAll(
            PERSON.BORN.gte(Cypher.literalOf(1970)).and(PERSON.BORN.lt(Cypher.literalOf(1980))) (1)
                .or(optionalName
                    .map(name -> PERSON.NAME.isEqualTo(Cypher.anonParameter(name))) (2)
                    .orElseGet(Conditions::noCondition)) (3)
        );
    }

    Optional<PersonDetails> findDetails(String name) {

        var d = Movie_.MOVIE.named("d");
        var a = Movie_.MOVIE.named("a");
        var m = Movie_.MOVIE.named("movies");
        var r = Cypher.anyNode("relatedPerson");
        var statement = Cypher.match(Person_.PERSON.withProperties("name", Cypher.anonParameter(name)))
            .optionalMatch(d.DIRECTORS)
            .optionalMatch(a.ACTORS)
            .optionalMatch(Person_.PERSON.relationshipTo(m).relationshipFrom(r, ActedIn_.$TYPE))
            .returningDistinct(
                Person_.PERSON.getRequiredSymbolicName(),
                Functions.collectDistinct(d).as("directed"),
                Functions.collectDistinct(a).as("actedIn"),
                Functions.collectDistinct(r).as("related")).build();

        return peopleRepository.findOne(statement, PersonDetails.class); (4)
    }
}
1 Using literals
2 Using an optional parameter
3 or when it’s not filled, an empty condition
4 Project a person onto PersonDetails, using a complex query.

Here is another complex example. The MovieRepository is also a CypherdslStatementExecutor:

Collection<Movie> findAllRelatedTo(Person person) {

    var p = Person_.PERSON.named("p");
    var a = Movie_.MOVIE.named("a");
    var d = Movie_.MOVIE.named("d");
    var m = Cypher.name("m");

    var statement = Cypher.match(p)
        .where(p.NAME.isEqualTo(Cypher.anonParameter(person.getName()))) (1)
        .with(p)
        .optionalMatch(new ActedIn_(p, a))
        .optionalMatch(new Directed_(p, d))
        .with(Functions.collect(a).add(Functions.collect(d))
            .as("movies"))
        .unwind("movies").as(m)
        .returningDistinct(m)
        .orderBy(Movie_.MOVIE.named(m).TITLE).ascending()
        .build();

    return this.movieRepository.findAll(statement);
}
1 Here we use an anonymous parameter to store the name value

The full example is in neo4j-cypher-dsl-examples/neo4j-cypher-dsl-examples-sdn6.

8. Appendix

Query-DSL support

The Neo4j Cypher-DSL has some support for Query-DSL. It can

  • turn instances of com.querydsl.core.types.Predicate into org.neo4j.cypherdsl.core.Condition,

  • turn instances of com.querydsl.core.types.Expression into org.neo4j.cypherdsl.core.Expression,

  • create org.neo4j.cypherdsl.core.Node instances from com.querydsl.core.types.Path and also

  • create org.neo4j.cypherdsl.core.SymbolicName

With this, many static meta models based on Query-DSL can be used to create Queries and match on nodes. Most operations supported by Query-DSL are translated into Cypher that is understood by Neo4j 4.0+, so that most predicates should work - at least from a syntactic point of view - out of the box.

Expressions are most useful to address properties in return statements and the like.

Here’s one example on how to use it:

QPerson n = new QPerson("n"); (1)
Statement statement = Cypher.match(Cypher.adapt(n).asNode()) (2)
    .where(Cypher.adapt(n.firstName.eq("P").and(n.age.gt(25))).asCondition()) (3)
    .returning(Cypher.adapt(n).asName()) (4)
    .build();

assertThat(statement.getParameters()).isEmpty();
assertThat(statement.getCypher())
    .isEqualTo("MATCH (n:`Person`) WHERE n.firstName = 'P' AND n.age > 25 RETURN n");
1 This makes use of a "Q"-class generated by Query-DSL APT (using the general processor). alias and class based paths works, too. Please make sure to name the instance accordingly when you use it as node (see next step)
2 Adapt the "Q"-class into a node. Please note it must be named accordingly, otherwise you query won’t return the expected results
3 Create some Query-DSL predicate based on the properties and adapt it as condition
4 Return some Query-DSL properties and adapt it as expression

The Statement offers a way to render all constants as parameters, so that they don’t bust the query cache:

QPerson n = new QPerson("n");
Statement statement = Cypher.match(Cypher.adapt(n).asNode())
    .where(Cypher.adapt(n.firstName.eq("P").and(n.age.gt(25))).asCondition())
    .returning(Cypher.adapt(n).asName())
    .build();

statement.setRenderConstantsAsParameters(true); (1)
assertThat(statement.getParameters()).containsEntry("pcdsl01", "P"); (2)
assertThat(statement.getParameters()).containsEntry("pcdsl02", 25);
assertThat(statement.getCypher())
    .isEqualTo("MATCH (n:`Person`) WHERE n.firstName = $pcdsl01 AND n.age > $pcdsl02 RETURN n"); (3)
1 Set this to true before accessing parameters of the statement or rendering the statement
2 Access the statements parameters for generated parameter names
3 Compare the statement to the first listening. Constants are gone now

The Neo4j Cypher-DSL will collect all parameters defined via Query-DSL for you:

QPerson n = new QPerson("n");
Statement statement = Cypher.match(Cypher.adapt(n).asNode())
    .where(Cypher.adapt(n.firstName.eq(new Param<>(String.class, "name"))
            .and(n.age.gt(new Param<>(Integer.class, "age"))) (1)
        ).asCondition()
    )
    .returning(Cypher.adapt(n).asName())
    .build();

assertThat(statement.getParameterNames()).hasSize(2); (2)
assertThat(statement.getCypher())
    .isEqualTo("MATCH (n:`Person`) WHERE n.firstName = $name AND n.age > $age RETURN n");
1 Basically the same predicate as above, but with parameters, which will be turned into correct placeholders
2 Access the parameter names via the Statement object

Required dependencies

The Query-DSL support in the Neo4j Cypher-DSL is optional, and the Query-DSL dependency is only in the provided scope. To make use of Cypher.adapt(), you must add the following dependency in addition to the Cypher-DSL:

Maven
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-core</artifactId>
    <version>4.4.0</version>
    <scope>provided</scope>
</dependency>
Gradle
dependencies {
    implementation 'com.querydsl:query-dsl-core:4.4.0'
}

In case you want to use an annotation processor, you have to add additional dependencies and depending on your Java environment. We use the following in our tests:

<dependencies>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>4.4.0</version>
        <classifier>general</classifier>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
</dependencies>

Building the Neo4j Cypher-DSL

Requirements

For the full project, including examples and native tests:

For the project, including examples but skipping native tests

Maven 3.6.3 is our build tool of choice. We provide the Maven wrapper, see mvnw respectively mvnw.cmd in the project root; the wrapper downloads the appropriate Maven version automatically.

The build requires a local copy of the project:

Listing 41. Clone the Neo4j Cypher-DSL
$ git clone git@github.com:neo4j-contrib/cypher-dsl.git

Full build (including examples and native tests)

Before you proceed, verify your locally installed JDK version. The output should be similar:

Listing 42. Verify your JDK
$ java -version
openjdk version "11.0.10" 2021-01-19
OpenJDK Runtime Environment GraalVM CE 21.0.0 (build 11.0.10+8-jvmci-21.0-b06)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.0 (build 11.0.10+8-jvmci-21.0-b06, mixed mode, sharing)

Check whether GraalVM native-image is present with:

Listing 43. Check for the present of native-image
$ gu list
ComponentId              Version             Component name                Stability           Origin
--------------------------------------------------------------------------------------------------------
js                       21.0.0              Graal.js                      -
graalvm                  21.0.0              GraalVM Core                  -
native-image             21.0.0              Native Image                  Early adopter       github.com

You should see native-image in the list. If not, install it via gu install native-image.

After that, use ./mvnw on a Unix-like operating system to build the Cypher-DSL:

Listing 44. Build with default settings on Linux / macOS
$ ./mvnw clean verify

On a Windows machine, use

Listing 45. Build with default settings on Windows
$ mvnw.cmd clean verify

Skipping native tests

On a plain JDK 11 or higher, run the following to skip the native tests:

Listing 46. Skipping native tests
$ ./mvnw clean verify -pl \!org.neo4j:neo4j-cypher-dsl-native-tests

Build only the core module

The core module can be build on plain JDK 11 with:

Listing 47. Skipping native tests
$ ./mvnw clean verify -pl org.neo4j:neo4j-cypher-dsl

Architecture

The Neo4j-Cypher-DSL consists of one main module: org.neo4j.cypherdsl.core. The coordinates of that module org.neo4j:neo4j-cypher-dsl, the JDK module name is org.neo4j.cypherdsl.core. The rendering feature is part of the core module.

All other modules depend on the core. As the core reflects the Cypher language, it is not meant to be extendable. Therefore, there is little to know API to do so with the AST visitor being the exception.

We document our rules structure with jQAssistant, a Neo4j based tool for software analysis. jQAssistant is integrated in our build.

To run the analysis standalone, you have to compile and build the project first. As you can skip both examples and native tests, JDK 8 is sufficient todo so:

java --version (1)
./mvnw clean verify \
  -pl \!org.neo4j:neo4j-cypher-dsl-examples,\!org.neo4j:neo4j-cypher-dsl-native-tests (2)
./mvnw jqassistant:server (3)
1 Must be at least Java 8
2 Build and verify all modules except the examples and the native tests, which includes all scannes
3 Start a local Neo4j server, reachable at http://localhost:7474/browser/. The instance contains a dedicated jQAssistant dashboard as well: http://localhost:7474/jqassistant/dashboard/

If you want to change the default Neo4j HTTP- and Bolt-ports, start the server like this:

./mvnw jqassistant:server -Djqassistant.embedded.httpPort=4711 -Djqassistant.embedded.boltPort=9999

Coding rules

Consistent naming

The following naming conventions are used throughout the project:

Listing 48. All Java types must be located in packages that start with org.neo4j.cypherdsl.
MATCH
  (project:Maven:Project)-[:CREATES]->(:Artifact)-[:CONTAINS]->(type:Type)
WHERE NOT type.fqn starts with 'org.neo4j.cypherdsl'
  AND type.fqn <> 'module-info'
RETURN
  project as Project, collect(type) as TypeWithWrongName
API
General considerations

We use @API Guardian to keep track of what we expose as public or internal API. To keep things both clear and concise, we restrict the usage of those annotations to interfaces, classes (only public methods and constructors: and annotations.

Listing 49. @API Guardian annotations must not be used on fields
MATCH (c:Java)-[:ANNOTATED_BY]->(a)-[:OF_TYPE]->(t:Type {fqn: 'org.apiguardian.api.API'}),
      (p)-[:DECLARES]->(c)
WHERE c:Member AND NOT (c:Constructor OR c:Method)
RETURN p.fqn, c.name

Public interfaces, classes or annotations are either part of internal or public API and therefore must have a status:

Listing 50. Define which Java artifacts are part of internal or public API
MATCH (c:Java)-[:ANNOTATED_BY]->(a)-[:OF_TYPE]->(t:Type {fqn: 'org.apiguardian.api.API'}),
      (a)-[:HAS]->({name: 'status'})-[:IS]->(s)
WHERE ANY (label IN labels(c) WHERE label in ['Interface', 'Class', 'Annotation'])
WITH  c, trim(split(s.signature, ' ')[1]) AS status
WITH  c, status,
      CASE status
        WHEN 'INTERNAL' THEN 'Internal'
        ELSE 'Public'
      END AS type
MERGE (a:Api {type: type, status: status})
MERGE (c)-[:IS_PART_OF]->(a)
RETURN c,a
Internal API

While we are pretty clear about the intended use of our classes (being experimental, public API or strictly internal), we want to make sure that no-one can coincidentally inherit from internal classes that we couldn’t restrict to default package visibility:

Listing 51. Non abstract, public classes that are only part of internal API must be final
MATCH (c:Class)-[:IS_PART_OF]->(:Api {type: 'Internal'})
WHERE c.visibility = 'public'
  AND coalesce(c.abstract, false) = false
  AND NOT exists(c.final)
RETURN c.name

Structure

Neo4j-Cypher-DSL Core

The core of the Cypher-DSL is consist of a set of classes that loosely reassemble the openCypher spec, especially in the railroad diagrams.

The main package of the core module is org.neo4j.cypherdsl.core which also reflects in the JDK module name: org.neo4j.cypherdsl.core. Part of the Cypher-DSL core is also the renderer package as the main goal of the core is to render Cypher. The renderer package is a sub-package of core as it is an essential part of it and in addition, the above mentioned JDK module name should reflect exactly one package.

So while all other subpackages in core can be used freely from the core classes themselves, we don’t want to access the renderer package apart from one exception: The AbstractStatement class can be used to invoke the rendering process without explicitly specifying a renderer:

Listing 52. The Cypher-DSL core package must not depend on the rendering infrastructure
MATCH (a:Main:Artifact)
MATCH (a)-[:CONTAINS]->(p1:Package)
WHERE p1.fqn in ['org.neo4j.cypherdsl.core']
WITH p1, a
MATCH (p1)-[:CONTAINS]->(t:Type)
MATCH (t)-[:DEPENDS_ON]->(t2:Type)<-[:CONTAINS]-(p2:Package)<-[:CONTAINS]-(a)
WHERE p2.fqn = 'org.neo4j.cypherdsl.core.renderer'
  AND t.fqn <> 'org.neo4j.cypherdsl.core.AbstractStatement'
RETURN t, t2

The renderer package is not only free to use the whole core, it must do so to fulfill its purpose. The ast and utils packages however should not have dependencies outside their own:

Listing 53. Supporting packages must not depend on anything from the outside
MATCH (a:Main:Artifact)
MATCH (a)-[:CONTAINS]->(p1:Package)-[:DEPENDS_ON]->(p2:Package)<-[:CONTAINS]-(a)
WHERE p1.fqn IN ['org.neo4j.cypherdsl.core.ast', 'org.neo4j.cypherdsl.core.utils']
RETURN p1,p2;

There should not be any internal, public API inside the core package. Everything that needs to be public for reasons (like being used in the renderer), must go into the internal package. This package won’t be exported via module-info.java.

Listing 54. Public internal API goes into a dedicated package
MATCH (c:Class)-[:IS_PART_OF]->(:Api {type: 'Internal'})
MATCH (p:Package) - [:CONTAINS] -> (c)
WHERE c.visibility = 'public'
AND coalesce(c.abstract, false) = false
AND p.fqn = 'org.neo4j.cypherdsl.core'
RETURN c, p

Change log

2021.3

2021.3.1

2021.3.1 is a pure bug fix release. API Guardian cannot be an optional dependency, otherwise compiling programs with -Werror will fail, as the @API-annotation has runtime and not class retention.

πŸ› Bug Fixes
  • GH-203 - Introduce a scope for the PatternComprehension.

  • Revert "GH-202 - Make API Guardian an optional / provided dependency."

  • Support empty BooleanBuilder in QueryDSL adapter.

2021.3.0
πŸš€ New module: The Cypher-DSL-Parser

2021.3 builds straight upon 2021.2.3, with few additions to the existing API, that didn’t quite fit with a patch release, but belong conceptually into this release, which brings a completely new module: The neo4j-cypher-dsl-parser.

What’s behind that name? A Cypher-Parser build on the official Neo4j 4.3 parser frontend and creating a Cypher-DSL-AST or single expressions usable in the context of the Cypher-DSL.

The module lives under the following coordinates org.neo4j:neo4j-cypher-dsl-parser and requires JDK 11+ (the same version like Neo4j does). We created a couple of examples, but we are sure you will have tons of more ideas and therefore a looking for your feedback.

Here’s a sneak preview. It shows you can add a user supplied Cypher fragment to something you are building using the DSL.

var userProvidedCypher
    = "MATCH (this)-[:LINK]-(o:Other) RETURN o as result";
var userStatement = CypherParser.parse(userProvidedCypher);

var node = Cypher.node("Node").named("node");
var result = Cypher.name("result");
var cypher = Cypher
    .match(node)
    .call(
        userStatement,
        node.as("this")
    )
    .returning(result.project("foo", "bar"))
    .build()
    .getCypher();

For this release a big thank you goes out to the Cypher-operations team at Neo4j, listening to our requests and ideas!

2021.2

2021.2.3

2021.2.3 is a rather big release as it contains many small improvements and API functionality required by our next major release. Those are brought in now so that they can be benefial to others without bumping a major version.

πŸš€ Features
  • GH-195 - Add collection parameter support for ExposesReturning.

  • Introduce a ExposesPatternLengthAccessors for uniform access to relationships and chains thereof. [improvement]

  • Allow creating instances of FunctionInvocation directly. [improvement]

  • Provide factory methods of MapProjection and KeyValueMapEntry as public API.

  • Provide set and remove labels operation as public API.

  • Provide set and mutate of expressions as public API.

  • Provide factory methods of Hints as public API.

  • GH-200 - Provide an API to define named paths based on a Node pattern.

  • Provide an option to escape names only when necessary. [improvement]

πŸ“– Documentation
  • Add documentation for escaping names.

  • GH-198 - Fix spelling errors in JavaDoc and documentation.

πŸ› Bug Fixes
  • Make Case an interface and let it extend Expression. [bug]

  • GH-197 - Fix eagerly resolved symbolic names in negated pattern conditions.

  • GH-197 - Clear name cache after leaving a statement.

  • GH-199 - Bring back with(Named… vars).

🧹 Housekeeping
  • Don’t use fixed driver versions in doc.

  • Pass builder as constructor argument.

  • Improve Return and With internals.

  • Update Driver, SDN integration and Spring Boot example dependencies.

  • GH-202 - Make API Guardian an optional / provided dependency.

Thanks to @meistermeier and @aldrinm for their contributions.

2021.2.2
πŸš€ Features
  • Allow all expresions to be used as conditions. [improvement]

  • Add support for unary minus and plus operations. [new-feature]

  • Add support for generatic dynamic distinct aggregating function calls. [new-feature]

  • GH-190 - Introduce a union type for named things and aliased expressions.

  • Provide means to pass additional types to the relationship base class. [new-feature]

  • GH-193 - Allow MATCH after YIELD.

  • GH-189 - Provide an alternate api for methods consuming collections via vargs.

πŸ“– Documentation
  • Improve inheritance example. [static-model, codegen]

πŸ› Bug Fixes
  • Fix parameter collector when running as GraalVM native image

  • GH-192 - Don’t introduce new symbolic names in conditional pattern expressions.

🧹 Housekeeping
  • GH-178 - Upgrade SDN 6 examples to Spring Boot 2.5 final.

Thanks to @meistermeier for the contribution of the API improvements in regard to collections.

2021.2.1
πŸš€ Features
  • Distinguish between statements and result statements: The Cypher-DSL knows whether a statement would actually return data or not

  • Provide optional integration with the Neo4j-Java-Driver to execute statements.

  • Allow to register Spring converters with the annotation processor. [codegen]

  • GH-182 - Add support for scalar converter functions.

  • GH-183 - Add trim function.

  • GH-184 - Add split function.

  • GH-180 - Add support for LOAD CSV and friends.

  • GH-187 - Add returningRaw for returning arbitrary (aliased) Cypher fragments (bot as part of a statement or as a general RETURN xxx clause without preceding query)

  • Resolve named parameters in raw literals: You can mix now the expression placeholder $E and named parameters in raw Cypher literals giving you much more flexibility in regards what to pass to the raw litera.

πŸ› Bug Fixes
  • GH-177 - Create a valid loadable and instantiable name when working on nested, inner classes. [codegen]

  • GH-186 - Pretty print subqueries and fix double rendering of Labels after subquery.

🧹 Housekeeping
  • Remove unnecessary subpackage 'valid'. [codegen] (test code only)

  • Upgrade to GraalVM 21.1.0.

  • Update Spring dependencies for codegen.

Thanks to @Andy2003 for contributing to this release.

2021.2.0

2021.2 doesn’t bring any new features apart from being now a Java library supporting the Java module system not only with automatic module names but also with a correct module-info.java when running on JDK 11+ on the module path.

The Cypher-DSL uses the technique of JEP 238: Multi-Release JAR Files to provide a module-info.java for projects being on JDK 11+.

The MR-Jar allows us to compile for JDK 8 but also support JDK 11 (we choose 11 as it is the current LTS release as time of writing).

To use the Cypher-DSL in a modular application you would need to require the following modules:

module org.neo4j.cypherdsl.examples.core {

        requires org.neo4j.cypherdsl.core;
}

This release comes with a small catch: We do support using some QueryDSL constructs. Query-DSL will have correct automatic module names in their 5.x release and we asked them to backport those to the 4.x line on which the Cypher-DSL optionally depends (See 2805).

Until then we statically require (that is "optional" in module speak) Query-DSL via the artifact name. This can cause errors when the artifact (querydsl-core.jar) is renamed via the build process or similar. We are gonna improve that as soon as we can depend on fixed automatic module names.

Apart from this big change there is no change in any public API. This release should be a drop-in replacement for the prior release.

A big thank you to @sormuras for his invaluable lessons about the Java module system.

2021.1

2021.1.2

This release comes with two notable things: It uses a couple of annotations on the API to guide developers using it correctly. IDEs like IDEA will now issue warnings if you don’t use a returned builder, or a new instance of an object while wrongly assuming you mutated state.

In the light of that we discovered that the RelationshipChain pattern was mutable and returning a mutated instance while it should have returned a fresh one.

Warning This might be a breaking change for users having code like this:

var pattern = Cypher.node("Start").named("s")
  .relationshipTo(Cypher.anyNode())
  .relationshipTo(Cypher.node("End").named("e"));
pattern.named("x");

Prior to 2021.1.2 this would give the pattern the name x and modify it in place. From 2021.1.2 onwards you must use the returned value:

pattern = pattern.named("x");

We think that this change is crucial and necessary as all other patterns are immutable as intended and in sum, they build up truly immutable statements. One pattern that is mutable like the above invalides the whole guarantee about the statement.

πŸš€ Features
  • Add named(SymbolicName s) to RelationshipChain.

  • Generate $TYPE field containing the relationship type. [SDN 6 Annotation Processor]

  • Introduce some optional annotations for guidance along the api.

πŸ“– Documentation
  • GH-173 - Improve documentation. [A collection of small improvements]

πŸ› Bug Fixes
  • GH-174 - Extract types via the visitor API and avoid casting element types. [SDN 6 Annotation Processor]

  • Ensure immutability of RelationshipChain.

🧹 Housekeeping
  • Remove unnecessary close (will be taken care of via @Container). [Only test related]

  • Run tests on JDK 16

2021.1.1

This is a drop-in replacement for 2021.1.0. Introducing the interface for Property broke the mutate operation, for which no test was in place. This and the bug has been fixed.

πŸ› Bug Fixes
  • GH-168 - Fix mutating containers by properties.

2021.1.0

2021.1.0 comes with a ton of new features and a handful of breaking changes. Fear not, the breaking changes are resolvable by recompiling your application. We turned Node, Relationship and Property into interfaces and provide now NodeBase and RelationshipBase so that you can use them to build a static meta-model of your application. A PropertyBase might follow.

Find out everything about the new possibility to define a static meta model in the manual. The manual also includes a major part about the two new modules we offer: org.neo4j:neo4j-cypher-dsl-codegen-core and org.neo4j:neo4j-cypher-dsl-codegen-sdn6. neo4j-cypher-dsl-codegen-core provides the infrastructure necessary to build code generators for creating a domain model following our recommendation and neo4j-cypher-dsl-codegen-sdn6 is a first implementation of that: A Java annotation processor that can be added to any Spring Data Neo4j 6 project in version 6.0.6 or higher. It will find your annotated domain classes and turn them into a model you can use to build queries.

Last but not least: We added support for some expressions of the more generic QueryDSL. This will require com.querydsl:querydsl-core on the class path but only if you decide to call Cypher#adapt(foreignExpression). This is a feature that is driven by Spring Data Neo4j 6.1 in which we build upon this to provide a QuerydslPredicateExecutor. Find more in this section of the manual.

πŸš€ Features
  • GH-154 - Make Node and Relationship extendable.

  • GH-155 - Provide infrastructure for generating a static meta model.

  • GH-156 - Create an annotation processor for Spring Data Neo4j 6.

  • GH-167 - Add support for some Query-DSL expressions.

  • Introduce a statement context for allowing anonymous parameters (use Cypher#anonParameter() to define a parameter with a value but without a name. The name will be accessible on the statement after rendering).

  • Make rendering of constants as parameters configurable.

  • Allow specification of the direction while creating a sort item.

  • Introduce an interface for Property.

πŸ“– Documentation
  • GH-152 - Document usage of PatterElement in tests.

  • GH-164 - Improve public extendable API and add documentation.

πŸ› Bug Fixes
  • Fix SymbolicName#toString.

  • Clear visited name cache after single queries.

🧹 Housekeeping
  • GH-165 - Simplify poms.

  • GH-166 - Improve Cypher.literalOf.

  • Exclude all example projects from release.

2021.0

2021.0.2
This will already be the last release of the 2021.0 line. 2021.1 will be API compatible but not ABI compatible, as some classes have been changed into interfaces. That means it is not a drop in replacement, but your application needs to be recompiled.
πŸš€ Features
  • GH-157 - Provide a method to turn a Java map into an expression.

  • GH-158 - Improve pretty printing of subqueries.

  • Allow the use of raw cypher as expressions.

  • Allow symbolic names to be used as aliases.

  • Cache some symbolic names.

  • Add support for the keys() function.

πŸ“– Documentation
  • GH-152 - Document usage of PatterElement in tests.

πŸ› Bug Fixes
  • GH-149 - Avoid possible stackoverflow exception during visitor traversal.

  • GH-159 - Fix missing labels for nodes after WITH.

🧹 Housekeeping
  • GH-148 - Add jQAssistant rules and improve building documentation.

  • Add Maven PMD plugin.

Thanks Andy for the improvements of the pretty printer.

2021.0.1
πŸš€ Features
  • GH-147 - Configuration infrastructure for renderer. First use case being a simple, pretty printing renderer.

The feature looks like this:

var c = node("Configuration").named("c");
var d = node("Cypher-DSL").named("d");

var mergeStatement = merge(c.relationshipTo(d, "CONFIGURES"))
    .onCreate()
        .set(
            d.property("version").to(literalOf("2021.0.1")),
            c.property("prettyPrint").to(literalTrue())
        )
    .onMatch().set(c.property("indentStyle").to(literalOf("TAB")))
    .returning(d).build();

var renderer = Renderer.getRenderer(Configuration.prettyPrinting());
System.out.println(renderer.render(mergeStatement));

and gives you:

MERGE (c:Configuration)-[:CONFIGURES]->(d:`Cypher-DSL`)
  ON CREATE SET d.version = '2021.0.1', c.prettyPrint = true
  ON MATCH SET c.indentStyle = 'TAB'
RETURN d
2021.0.0

2021.0.0 comes with a lot of new features. Thanks to Andy for his contributions!

Andy is one of our first users outside Spring Data Neo4j 6. He started to use the Cypher-DSL in Neo4j GraphQL Java. Neo4j GraphQL Java is a library to translate GraphQL based schemas and queries to Cypher and execute those statements with the Neo4j database. It can be used from a wide variety of frameworks.

We are happy and proud to be part of this and even more so about the input and contribution we got back from Andy.

Of course thanks for your input in form of tickets and discussions go out to @utnaf, @aaramg, @K-Lovelace and @maximelovino as well!

Noteworthy

Two things should be mentioned:

The bugfix for GH-121 might change behavior for some users: The changes prevents the forced rendering of an alias for objects when the original object - the one that has been aliased - is passed down to the DSL after an alias has been created.

The original intention for that behaviour was related to Map projection, in which the alias is actually rendered before the object.

So now the use of an aliased expression the first time triggers a AS b respectively b: a in a map projection. All further calls will just render b. If the original object is used again, a will be rendered. If that is not desired in your query and you rely on the alias, make sure you use the aliased expression returned from .as("someAlias").

The other thing are the combined features GH-135 and GH-146. The Statement class has become a fully fledged accessor to the Cypher String and the parameters used and if provided, the values for those. The following shows a small example:

var person = Cypher.node("Person").named("p");
var statement = Cypher
    .match(person)
    .where(person.property("nickname").isEqualTo(Cypher.parameter("nickname")))
    .set(
        person.property("firstName").to(Cypher.parameter("firstName").withValue("Thomas")),
        person.property("name").to(Cypher.parameter("name", "Anderson"))
    )
    .returning(person)
    .build();

assertThat(statement.getCypher())
    .isEqualTo("MATCH (p:`Person`) WHERE p.nickname = $nickname SET p.firstName = $firstName, p.name = $name RETURN p");

Collection<String> parameterNames = statement.getParameterNames();
assertThat(parameterNames).containsExactlyInAnyOrder("nickname", "firstName", "name");

Map<String, Object> parameters = statement.getParameters();
assertThat(parameters).hasSize(2);
assertThat(parameters).containsEntry("firstName", "Thomas");
assertThat(parameters).containsEntry("name", "Anderson");
πŸš€ Features
  • GH-122 - Add support for index hints.

  • GH-123 - Expose nested building of nested properties as public API.

  • GH-124 - Add support for Neo4j’s mathematical functions.

  • GH-127 - Allow dynamic property lookup.

  • GH-128 - Provide asConditions for RelationshipPatterns.

  • GH-129 - Allow Expressions as Parameter for Skip and Limit.

  • GH-131 - Add support for projections on symbolic names.

  • GH-133 - Allow symbolic names to be used as condition.

  • GH-135 - Collect parameters defined on a statement.

  • GH-141 - Provide a property function on all expressions.

  • GH-142 - Provide a point function accepting generic expressions as parameter.

  • GH-146 - Allow a statement to render itself.

πŸ“– Documentation
  • GH-126 - Document how to call arbitrary functions and procedures.

πŸ› Bug Fixes
  • Prevent double rendering of Node content when using generated names.

  • GH-121 - Don’t force rendering of aliases when the original object is used.

  • GH-137 - Fix grouping of nested conditions.

🧹 Housekeeping
  • Switch to GraalVM 20.3.0.

  • GH-125 - Use GraalVM image from ghcr.io.

  • GH-139 - Ensure indention via tabs.

  • GH-140 - Provide editorconfig.

  • GH-143 - Remove union types.

2020.1

2020.1.6

No new features. Re-released 2020.1.5 due downtime and staging issues of oss.sonatype.org.

2020.1.5
πŸš€ Features
  • GH-116 - Add support for creating calls to Neo4j reduce().

  • GH-119 - Add support for passing symbolic names to nodes and relationships functions.

  • GH-117 - Introduce mutating operator.

2020.1.4
πŸš€ Features
  • GH-114 - Support chained properties.

  • GH-115 - Support named items in YIELD and symbolic names as NamedPath reference.

2020.1.3
πŸš€ Features
  • GH-111 - Provide a programmatic way of creating an optional match.

πŸ› Bug Fixes
  • GH-110 - Fix collapsing of empty conditions.

2020.1.2
πŸš€ Features
  • GH-88 - Add support for Neo4j 4.0 subqueries.

  • GH-104 - Add support for merge actions.

  • GH-101 - Introduce asFunction on an ongoing call definition.

πŸ› Bug Fixes
  • GH-106 - Escape symbolic names, property lookups, aliases and map keys.

🧹 Housekeeping
  • GH-105 - Remove ::set-env from GH-Actions ci.

Further improvements:
  • Add support for EXPLAIN and PROFILE keywords.

  • Qualify a yield call (only relevant for JDK15+)

  • Fix wrong offsets in the documentation.

  • Improve JavaDoc and document internal API.

  • Allow WITH clause after YIELD.

  • Improve reusability of fragments.

  • Make ORDER clause buildable.

  • Remove parts of an experimental API.

We do publish the Project info now: Project info, including the Java API.

2020.1.1
πŸš€ Features
  • List comprehensions can now be build based on named paths.

2020.1.0
πŸš€ Features
  • GH-74 - Automatically generate symbolic name if required: Node and Relationship objects generate a symbolic name if required and not set

  • Added several new functions

    • GH-77 properties()

    • GH-81 relationships()

    • GH-83 startNode(), endNode(),

    • GH-89 All temporal functions

  • GH-76 - Added the list operator ([] for accessing sub lists and indexes).

πŸ› Bug Fixes
  • GH-82 - Expose all necessary interfaces for call

  • GH-84 - Fix rendering of nested sub trees.

  • GH-95 - NPE during the creation of map projections

  • GH-96 - Make sure aliased expressions are not rendered multiple times.

🧹 Housekeeping
  • GH-67 - Improvements in regards of Java generics.

  • GH-68 - Clean up the Functions api.

  • GH-69 - Avoid star and static imports.

  • GH-72 - Some release cleanup

  • GH-75 - Move Assert to internal utils package.

  • GH-89 - RelationshipDetails is now internal API.

  • GH-93 - Ensure compatibility with GraalVM native.

  • GH-94 - Bring back SymbolicName#concat.

2020.0

2020.0.1

This is the first patch release for the rebooted Cypher-DSL project.

πŸš€ Features
  • GH-64 - Add function invocation for builtin point function.

  • GH-65 - Add support for defining calls to stored procedures.

  • Cypher.literalOf accepts now boolean values as well

🧹 Housekeeping
  • Improvements to the manual and Java Docs.

Thanks to @Andy2003 for contributing to this release.

2020.0.0

This is the first version of the rebooted Neo4j Cypher-DSL project. This version has been extracted from SDN-RX.

It’s a completely revamped API and we use it in all places in SDN/RX for generating Cypher-Queries.

We use CalVer in the same way Spring does since early 2020 (see Updates to Spring Versions) from this release onwards.

You’ll find the manual of the latest release version under http://neo4j-contrib.github.io/cypher-dsl/current/ and the current development version - or main - under http://neo4j-contrib.github.io/cypher-dsl/main/.