© 2020 Neo4j, Inc.,

This is the Neo4j Cypher-DSL manual version 2022.8.1-SNAPSHOT.

Who should read this?

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

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>2022.8.1-SNAPSHOT</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:2022.8.1-SNAPSHOT'
}

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.Locale;
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.Predicates;
import org.neo4j.cypherdsl.core.SortItem;
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")
    .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.

Retrieving identifiable expressions

The Statement as well as the intermediate build steps after defining a WITH or RETURN clause allow to retrieve identifiable expressions via getIdentifiableExpressions(). All expressions identifiable via a name such as named nodes and relationships, symbolic names or aliased expressions are included. In addition, properties are also available.

Those information can be used when dynamically building a query to verify the presence of required expressions or use them for further refinement.

The feature plays well with the driver integration and could be directly used to define mapping functions.

Statements parsed via the optional parser module are also able to return their identifiable expressions. The use case here might be evaluating a statement defined by a user being parsed and then checked if everything required is returned.

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.4.12 supports, we might cannot translate all of them into elements of the Cypher-DSL. In such cases an UnsupportedOperationException will be thrown.

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>2022.8.1-SNAPSHOT</version>
</dependency>
Gradle
Listing 15. Gradle variant for additional dependencies
dependencies {
    implementation 'org.neo4j:neo4j-cypher-dsl-parser:2022.8.1-SNAPSHOT'
}

5.2.2. Minimum JDK version

The Cypher-Parser requires JDK 11 to run which is the same version that Neo4j 4.4.12 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>>() { },
    "person.name contains \"Ricci\" OR person.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 -> personRootName.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.4.9</artifactId>
</dependency>

Any 4.x version of the Driver will work, we currently test against 4.4.9. 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 2022.0.0 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>2022.0.0</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 final Property DOB = this.property("dob");

    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:2022.8.1-SNAPSHOT

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:2022.8.1-SNAPSHOT'
}
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.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.stereotype.Service;

@Service
final class PeopleService {

    private final Person_ person;
    private final SymbolicName personRootName;

    private final PeopleRepository peopleRepository;

    PeopleService(PeopleRepository peopleRepository, Neo4jMappingContext mappingContext) {
        this.peopleRepository = peopleRepository;
        this.personRootName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(
            mappingContext.getRequiredPersistentEntity(Person.class));
        this.person = Person_.PERSON.named(personRootName);
    }

    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.

An example of using a mixture of anonymous and named parameters is shown in the following listing:

Optional<Person> createNewPerson(NewPersonCmd newPersonCmd) {
    var p = Person_.PERSON.withProperties(
        Person_.PERSON.NAME, Cypher.anonParameter(newPersonCmd.getName()) (1)
    ).named("p");

    var statement = Cypher.merge(p)
        .onCreate().set(
            p.BORN, Cypher.parameter("arbitraryName")
                .withValue(newPersonCmd.getDob().getYear()), (2)
            p.DOB, Cypher.anonParameter(newPersonCmd.getDob()) (3)
        ).returning(p).build();
    return peopleRepository.findOne(statement);
}
1 An anonymous parameter with a simple String-value
2 A named parameter with an Integer value extracted from a complex datatype
3 An anonymous parameter again, but one that holds a complex datatype. Such datatype can be anything that is understood by the Neo4j-Java-Driver, such as in this case, a temporal but also maps and lists. It will always be passed as a value, never as a literal.

The statement above will be rendered like this:

[http-nio-auto-1-exec-1] 2022-06-21 11:40:57,732 DEBUG    org.springframework.data.neo4j.cypher: 313 - Executing:
MERGE (p:`Person` {name: $pcdsl01}) ON CREATE SET p.born = $arbitraryName, p.dob = $pcdsl02 RETURN p
[http-nio-auto-1-exec-1] 2022-06-21 11:40:57,735 TRACE    org.springframework.data.neo4j.cypher: 334 - with parameters:
:param arbitraryName => 1990
:param pcdsl01 => 'Liv Lisa Fries'
:param pcdsl02 => 1990-10-31T22:42Z[UTC]

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:

At least GraalVM 21.3.0 is required due to restrictions in org.graalvm.buildtools:native-maven-plugin. The rationale behind that is explained here and we don’t do apply any of the suggested workarounds as the releases are done with GraalVM 21.3.0 anyway.

For the project, including examples but skipping native tests

Maven 3.8.4 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

Fast build

This is useful if you want to just have an install of a snapshot version. No tests are run, no verification is done.
Listing 42. Fast build (only compiling and producing packages)
$ ./mvnw -Dfast package

For a local install - maybe to try out a future release - you can also specify the version number:

Listing 43. Fast build (locally installed, with an artificial version number)
$ ./mvnw -Dfast -Drevision=1337 -Dchangelist= install

Full build (including examples and native tests)

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

Listing 44. 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 45. 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 46. Build with default settings on Linux / macOS
$ ./mvnw clean verify

On a Windows machine, use

Listing 47. 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 48. 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 49. Build only the core module
$ ./mvnw clean verify -am -pl org.neo4j:neo4j-cypher-dsl

CI-friendly version numbers

We use CI-friendly version numbers, the current build will always identify itself as 9999-SNAPSHOT. If you need to create a specific version you can specify the revision, the changelist and an optional hash like this:

Listing 50. Specifying revision and changelist
$ ./mvnw clean package -pl org.neo4j:neo4j-cypher-dsl -Drevision=2022.1.0 -Dchangelist=-SNAPSHOT

Releasing (Only relevant for the current maintainers)

Prepare a release with:

./mvnw exec:exec@prepare-release -pl :neo4j-cypher-dsl-parent -Drevision=2020.0.1 -Dchangelist= -Dcypher-dsl.version.next=2020.0.2-SNAPSHOT

and then let do Teamcity the rest, but chose the same version number there, too.

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 ArchUnit within a single unittest named org.neo4j.cypherdsl.core.PackageAndAPIStructureTest.

Coding rules

Consistent naming

The following naming conventions are used throughout the project:

Listing 51. All Java types must be located in packages that start with org.neo4j.cypherdsl.
@BeforeAll
void importCorePackage() {
    coreClasses = new ClassFileImporter()
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
        .importPackages("org.neo4j.cypherdsl.core..");
}
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 52. @API Guardian annotations must not be used on fields
@Test
void coreMostNotDependOnRendering() {
    ArchRule rule = noClasses().that()
        .resideInAPackage("..core")
        .and(not(simpleName("AbstractStatement")))
        .should().dependOnClassesThat(resideInAPackage("..renderer.."));
    rule.check(coreClasses);
}
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.

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 53. Non abstract, public classes that are only part of internal API must be final
@Test
void internalPublicClassesMustBeFinal() {
    ArchRule rule = classes().that()
        .areAnnotatedWith(API.class)
        .and().arePublic()
        .and().areTopLevelClasses()
        .and(not(modifier(JavaModifier.ABSTRACT)))
        .and(new DescribedPredicate<JavaClass>("Is internal API") {
            @Override
            public boolean test(JavaClass input) {
                API.Status status = input.getAnnotationOfType(API.class).status();
                return "INTERNAL".equals(status.name());
            }
        })
        .should().haveModifier(JavaModifier.FINAL)
        .andShould(ArchConditions.not(ArchConditions.resideInAPackage("..core")));
    rule.check(coreClasses);
}

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 54. The Cypher-DSL core package must not depend on the rendering infrastructure
@Test
void coreMostNotDependOnRendering() {
    ArchRule rule = noClasses().that()
        .resideInAPackage("..core")
        .and(not(simpleName("AbstractStatement")))
        .should().dependOnClassesThat(resideInAPackage("..renderer.."));
    rule.check(coreClasses);
}

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 55. Supporting packages must not depend on anything from the outside
@ParameterizedTest
@ValueSource(strings = { "..core.ast", "..core.utils" })
void independentSupportPackages(String supportPackage) {
    ArchRule rule = noClasses().that()
        .resideInAPackage(supportPackage)
        .should().dependOnClassesThat(
            resideInAPackage("..core..").and(not(resideInAPackage(supportPackage)))
        );
    rule.check(coreClasses);
}

Change log

2022.8

2022.8.0
๐Ÿš€ Features
  • Add yield * for standalone calls. (#497)

๐Ÿ“ Documentation
  • Add missing value to sanitize JavaDoc. (#496)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump testcontainers.version from 1.17.5 to 1.17.6 (#502)

    • Bump maven-install-plugin from 3.0.1 to 3.1.0 (#501)

    • Bump japicmp-maven-plugin from 0.16.0 to 0.17.1 (#499)

    • Bump mockito.version from 4.8.1 to 4.9.0 (#498)

    • Bump jackson-bom from 2.13.4.20221013 to 2.14.0 (#492)

    • Bump checker-qual from 3.26.0 to 3.27.0 (#493)

    • Bump reactor-bom from 2020.0.24 to 2022.0.0 (#495)

    • Bump native-maven-plugin from 0.9.16 to 0.9.17 (#491)

    • Bump maven-shade-plugin from 3.4.0 to 3.4.1 (#487)

    • Bump checkstyle from 10.3.4 to 10.4 (#488)

    • Bump joda-time from 2.12.0 to 2.12.1 (#486)

    • Bump spring-boot-starter-parent from 2.7.4 to 2.7.5 (#485)

    • Bump asciidoctorj from 2.5.6 to 2.5.7 (#483)

    • Bump native-maven-plugin from 0.9.14 to 0.9.16 (#482)

    • Bump mockito.version from 4.8.0 to 4.8.1 (#481)

2022.7

2022.7.3
๐Ÿš€ Features
  • Add point.withinBBox and convenience methods for cartesian points and coordinates. (#475)

๐Ÿ”„๏ธ Refactorings
  • Remove superfluous field.

๐Ÿ›  Build
  • Replace jQAssistant with easier to maintain Archunit test. (#466)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump jackson-bom from 2.13.4 to 2.13.4.20221013 (#479)

    • Bump neo4j-cypher-javacc-parser from 4.4.11 to 4.4.12 (#478)

    • Bump reactor-bom from 2020.0.23 to 2020.0.24 (#477)

    • Bump joda-time from 2.11.2 to 2.12.0 (#476)

    • Bump archunit from 0.23.1 to 1.0.0 (#471)

    • Bump neo4j-java-driver from 4.4.6 to 4.4.9 (#474)

    • Bump testcontainers.version from 1.17.3 to 1.17.5 (#470)

    • Bump checker-qual from 3.25.0 to 3.26.0 (#472)

    • Bump asm from 9.3 to 9.4 (#468)

    • Bump joda-time from 2.11.1 to 2.11.2 (#465)

    • Bump spring-boot-starter-parent from 2.7.3 to 2.7.4 (#464)

    • Bump junit-bom from 5.9.0 to 5.9.1 (#463)

    • Bump asciidoctorj from 2.5.5 to 2.5.6 (#462)

    • Bump checkstyle from 10.3.3 to 10.3.4 (#461)

    • Bump native-maven-plugin from 0.9.13 to 0.9.14 (#460)

    • Bump spring-data-neo4j from 6.3.2 to 6.3.3 (#459)

    • Bump jqassistant.plugin.git from 1.8.0 to 1.9.0 (#458)

    • Bump maven-jar-plugin from 3.2.2 to 3.3.0 (#457)

    • Bump reactor-bom from 2020.0.22 to 2020.0.23 (#456)

    • Bump maven-shade-plugin from 3.3.0 to 3.4.0 (#455)

    • Bump maven-pmd-plugin from 3.18.0 to 3.19.0 (#454)

    • Bump neo4j-cypher-javacc-parser from 4.4.10 to 4.4.11 (#452)

    • Bump mockito.version from 4.7.0 to 4.8.0 (#451)

2022.7.2

No breaking changes. This adds a new module - neo4j-cypher-dsl-schema-name-support - that contains only one class, dedicated to sanitise and quote names that are meant to be used as labels and types. We are using it internally for all our quoting needs and if you have the need in your application to create dynamic queries that deal with the modification of labels and types, you might want to have a look at that module. It is dependency free and safe to shade. The background to do label and type manipulation is this: Cypher does not support them as parameters, you need to concatenate your query for this. In all other cases, please use proper parameter, but especially for string values.

Thanks to @AzuObs @AlexNeo4J and @robsdedude for their feedback on this work and also to @harshitp-fens for their inspiration of the ON_DELETE_ITEM parser callback.

๐Ÿš€ Features
  • Provide ON_DELETE_ITEM event type. (#449)

  • Introduce standalone schema-name support module. (#445)

๐Ÿ›  Build
  • Fix the build on a restricted TeamCity instance. (#450)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump checker-qual from 3.24.0 to 3.25.0 (#448)

    • Bump japicmp-maven-plugin from 0.15.7 to 0.16.0 (#447)

    • Bump jackson-bom from 2.13.3 to 2.13.4 (#446)

    • Bump checkstyle from 10.3.2 to 10.3.3 (#444)

    • Bump maven-checkstyle-plugin from 3.1.2 to 3.2.0 (#443)

    • Bump maven-pmd-plugin from 3.17.0 to 3.18.0 (#442)

    • Bump joda-time from 2.11.0 to 2.11.1 (#441)

2022.7.1

No breaking changes. This is an important bug-fix release and a safe drop-in replacement for 2022.7.0 and all 2022.6 releases. If you can life with some deprecation warnings, it can be used as a drop-in for the 2022.5 and 2022.4 series, too.

๐Ÿ› Bug Fixes
  • Escape escaped Unicode 0060 (backtick) proper. (#436)

๐Ÿ”„๏ธ Refactorings
  • Don’t double escape already escaped backticks.

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump mockito.version from 4.6.1 to 4.7.0 (#434):

    • Bump reactor-bom from 2020.0.21 to 2020.0.22 (#433):

    • Bump joda-time from 2.10.14 to 2.11.0 (#432):

    • Bump neo4j-cypher-javacc-parser from 4.4.9 to 4.4.10 (#431):

    • Bump maven-javadoc-plugin from 3.4.0 to 3.4.1 (#430):

    • Bump spring-boot-starter-parent from 2.7.2 to 2.7.3 (#439)

    • Bump flatten-maven-plugin from 1.2.7 to 1.3.0 (#437):

2022.7.0

No breaking changes, the minor version has been bumped due to new default methods of internal interfaces. This release is - again - a safe drop-in replacement for the prior (2022.6.1) one.

Thanks to @AakashSorathiya and Nicolas Mervaillie for their input on this release.

๐Ÿš€ Features
  • Add support for includesAll and includesAny operations on expressions for list properties

  • Support org.neo4j.cypher.internal.ast.factory.ASTExpressionFactory#ands

๐Ÿ”„๏ธ Refactorings
  • Add cause to unsupported to UnsupportedCypherException.

๐Ÿ›  Build
  • Use current JBang action to verify on JDK 8. (#421)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump maven-site-plugin from 3.12.0 to 3.12.1 (#428)

    • Bump checker-qual from 3.23.0 to 3.24.0 (#429)

    • Bump checkstyle from 10.3.1 to 10.3.2 (#425)

    • Bump junit-bom from 5.8.2 to 5.9.0 (#424)

    • Bump maven-resources-plugin from 3.2.0 to 3.3.0 (#423)

    • Bump asciidoctorj from 2.5.4 to 2.5.5 (#422)

2022.6

2022.6.1
๐Ÿ› Bug Fixes
  • Include aliased expression in local scopes. (#420)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump neo4j-cypher-javacc-parser from 4.4.8 to 4.4.9 (#418)

    • Bump maven-install-plugin from 3.0.0-M1 to 3.0.1 (#417)

    • Bump spring-boot-starter-parent from 2.7.1 to 2.7.2 (#416)

    • Bump maven-deploy-plugin from 3.0.0-M2 to 3.0.0 (#415)

    • Bump exec-maven-plugin from 3.0.0 to 3.1.0 (#414)

    • Bump native-maven-plugin from 0.9.12 to 0.9.13 (#413)

    • Bump spring-data-neo4j from 6.3.1 to 6.3.2 (#412)

    • Bump reactor-bom from 2020.0.20 to 2020.0.21 (#411)

    • Bump checker-qual from 3.22.2 to 3.23.0 (#410)

2022.6.0

Functions.internalId() has been deprecated to accomodate for Neo4j 5 later this year. We are unsure if we will do this to id(), too (The method with the same name will be deprecated in Neo4j 5 and eventually be replaced by elementId()).

๐Ÿš€ Features
  • Add fallback from elementId to id on older Neo4j versions. (#407)

  • Add calls for elementId(). (#406)

๐Ÿ”„๏ธ Refactorings
  • Deprecate internalId in favor of elementId on nodes. (#408)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump testcontainers.version from 1.17.2 to 1.17.3 (#403)

    • Bump checkstyle from 10.3 to 10.3.1 (#404)

    • Bump jna from 5.12.0 to 5.12.1 (#405)

2022.5

2022.5.0

No breaking changes, the minor version has been bumped due to new default methods of internal interfaces. This release is - again - a safe drop-in replacement for the prior (2022.4.0) one.

Thanks to @hindog, @bhspencer, @Hardu2203 and @irene221b for their input on this release.

๐Ÿš€ Features
  • Add explicit set operation to PropertyContainer. (#394)

  • Support "WITH *, <expr>" by handling the 'returnAll' flag received from parser (#367)

๐Ÿ”„๏ธ Refactorings
  • refactor: Remove superfluous whitespaces before MapExpression in pretty printer. (#401)

๐Ÿ“ Documentation
  • Add an example how to use Cypher parameters with`CypherdslStatementExecutor`. (#395)

  • Improve JavaDoc of TemporalLiteral.

  • Add correction method description.

๐Ÿ›  Build
  • Use latest Neo4j 4.4 for integration tests.

  • Add a CODEOWNERS declaration.

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump jna from 5.11.0 to 5.12.0 (#399)

    • Bump spring-boot-starter-parent from 2.7.0 to 2.7.1 (#398)

    • Bump spring-data-neo4j from 6.3.0 to 6.3.1 (#397)

    • Bump native-maven-plugin from 0.9.11 to 0.9.12 (#396)

    • Bump reactor-bom from 2020.0.19 to 2020.0.20 (#392)

    • Bump checker-qual from 3.22.1 to 3.22.2 (#390)

    • Bump neo4j-cypher-javacc-parser from 4.4.7 to 4.4.8 (#391)

    • Bump maven-enforcer-plugin from 3.0.0 to 3.1.0 (#386)

    • Bump joda-time from 2.10.10 to 2.10.14 (#387)

    • Bump asciidoctorj from 2.5.3 to 2.5.4 (#380)

    • Bump assertj-core from 3.22.0 to 3.23.1 (#383)

    • Bump checker-qual from 3.22.0 to 3.22.1 (#382)

    • Bump mockito.version from 4.6.0 to 4.6.1 (#381)

    • Bump neo4j-java-driver from 4.4.5 to 4.4.6 (#379)

    • Bump maven-pmd-plugin from 3.16.0 to 3.17.0 (#378)

    • Bump asciidoctorj-diagram from 2.2.1 to 2.2.3 (#377)

    • Bump mockito.version from 4.5.1 to 4.6.0 (#376)

    • Bump checkstyle from 10.2 to 10.3 (#375)

    • Bump neo4j-cypher-javacc-parser from 4.4.6 to 4.4.7 (#373)

    • Bump testcontainers.version from 1.17.1 to 1.17.2 (#371)

    • Bump spring-data-neo4j from 6.2.4 to 6.3.0 (#368)

    • Bump jackson-bom from 2.13.2.20220328 to 2.13.3 (#370)

    • Bump reactor-bom from 2020.0.18 to 2020.0.19 (#369)

2022.4

2022.4.0
  • Added withoutResults to both in-statement and standalone call-builders so that one can use procedures without results inside a pipeline. This won’t break anything, as the corresponding interface is not meant to implemented by downstream libraries

  • Compound conditions are now correctly immutable (as stated by the contract and its JavaDoc). This might break things if you have them changed inflight.

Thanks to @Andy2003 for his input on this release.

๐Ÿš€ Features
  • Allow procedure calls without results after a match clause. (#361)

๐Ÿ› Bug Fixes
  • Make CompoundCondition immutable obliging the interfaces contract. (#365)

  • Don’t skip symbolic names if present and already in scope. (#363)

๐Ÿ›  Build
  • Update github-push-action to 0.6.0.

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump testcontainers.version from 1.16.3 to 1.17.1 (#352)

    • Bump reactor-bom from 2020.0.17 to 2020.0.18 (#353)

    • Bump mockito.version from 4.4.0 to 4.5.1 (#354)

    • Bump checkstyle from 10.1 to 10.2 (#355)

    • Bump spring-boot-starter-parent from 2.6.6 to 2.6.7 (#356)

    • Bump maven-javadoc-plugin from 3.3.2 to 3.4.0 (#357)

    • Bump maven-site-plugin from 3.11.0 to 3.12.0 (#358)

    • Bump spring-data-neo4j from 6.2.3 to 6.2.4 (#359)

    • Bump neo4j-cypher-javacc-parser from 4.4.5 to 4.4.6 (#360)

    • Bump checker-qual from 3.21.4 to 3.22.0 (#364)

2022.3

2022.3.0

No breaking changes. The minor version has been incremented due to the following changes:

  • Changes in the ExposesSubqueryCall (new methods to expose callInTransactions, but that interface is not meant for external implementations anyway)

  • Added a new type Dialect and a new default method enterWithResult on the Visitor interface (have a look at the JavaDoc for the rationale behind it).

๐Ÿš€ Features
  • Add support for dialects.

  • Add support for toString(Expression). (#344)

  • Support CALL {} IN TRANSACTIONS.

  • Add parameter callbacks to the parser. (#336)

๐Ÿ› Bug Fixes
  • Prevent ClassCastException when using String arguments to import variables into a subquery.

  • Make generated static model usable with self referential associations. (#337, Thanks to @ChristophB for his input on #335).

  • Fix tag of CypherParser entry point. (docs)

๐Ÿ“ Documentation
  • Add information about GraalVM 21.3.0 and org.graalvm.buildtools:native-maven-plugin to CONTRIBUTING.adoc.

๐Ÿ›  Build
  • Fix publish_docs workflow.

  • Add support for registering allDeclaredConstructors. (#342)

  • Add RegisterForReflection and processor replacing static reflection-config.json. (#341)

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump jackson-bom from 2.13.2 to 2.13.2.20220328 (#346)

    • Bump asm from 9.2 to 9.3 (#347)

    • Bump jacoco-maven-plugin from 0.8.7 to 0.8.8 (#345)

    • Update managed version of error_prone_annotations to 2.12.1, avoiding compilation issues in IDEA.

    • Bump spring-boot-starter-parent from 2.6.5 to 2.6.6 (#340)

    • Bump checker-qual from 3.21.3 to 3.21.4 (#339)

    • Bump maven-shade-plugin from 3.2.4 to 3.3.0 (#338)

2022.2

2022.2.1

No breaking changes.

๐Ÿš€ Features
  • Add randomUUID to predefined functions.

  • Support additional mutate expression types. (#312)

๐Ÿ› Bug Fixes
  • Don’t create empty WITH clause without renames. (#320)

  • Fix rendering of nested FOREACH statements. (#318)

  • Check for field type too when computing internalId usage.

๐Ÿ“ Documentation
  • Add example how to merge-find things via Springs CypherdslStatementExecutor.

๐Ÿงน Housekeeping
  • Remove Awaitility test-dependency.

  • Dependency upgrades:

    • Bump spring-data-neo4j from 6.2.2 to 6.2.3 (#332)

    • Bump neo4j-cypher-javacc-parser from 4.4.4 to 4.4.5 (#330)

    • Bump checkstyle from 10.0 to 10.1 (#329)

    • Bump jna from 5.10.0 to 5.11.0 (#331)

    • Bump spring-boot-starter-parent from 2.6.4 to 2.6.5 (#333)

    • Bump native-maven-plugin from 0.9.10 to 0.9.11 (#334)

    • Bump neo4j-java-driver from 4.4.3 to 4.4.5 (#328)

    • Bump reactor-bom from 2020.0.16 to 2020.0.17 (#327)

    • Bump mockito.version from 4.3.1 to 4.4.0 (#325)

    • Bump checkstyle from 9.3 to 10.0 (#323)

    • Bump guava from 31.0.1-jre to 31.1-jre (#324)

    • Bump checker-qual from 3.21.2 to 3.21.3 (#322)

    • Bump awaitility from 4.1.1 to 4.2.0 (#321)

    • Bump japicmp-maven-plugin from 0.15.6 to 0.15.7 (#313)

    • Bump spring-boot-starter-parent from 2.6.3 to 2.6.4 (#314)

2022.2.0

No breaking changes. The minor version has been incremented to notify about a couple of new methods in the parser module, allowing for more and different types of parsing events to be emitted.

Thanks to @ikwattro for his input and feedback in this release.

๐Ÿš€ Features
  • Emit pattern created event on merge clauses.

  • Add callbacks for a "pattern element created event". (#303)

๐Ÿ“ Documentation
  • Add an example how to track changed properties to nodes.

  • Add rewrite example.

  • Add examples how to extract modified labels for the Cypher parser.

๐Ÿ›  Build
  • Fix surefire settings.

  • Add a 'fast' profile.

  • Reorder module-info.java creation before shading so that javadoc wont fail on vanilla JDK.

๐Ÿงน Housekeeping
  • Dependency upgrades:

    • Bump maven-site-plugin from 3.10.0 to 3.11.0 (#311)

    • Bump native-maven-plugin from 0.9.9 to 0.9.10 (#310)

    • Bump maven-pmd-plugin from 3.15.0 to 3.16.0 (#309)

    • Bump spring-data-neo4j from 6.2.1 to 6.2.2 (#308)

    • Bump reactor-bom from 2020.0.15 to 2020.0.16 (#307)

    • Bump slf4j.version from 1.7.35 to 1.7.36 (#306)

    • Bump maven-javadoc-plugin from 3.3.1 to 3.3.2 (#305)

    • Bump neo4j-cypher-javacc-parser from 4.4.3 to 4.4.4 (#304)

    • Bump checker-qual from 3.21.1 to 3.21.2 (#298)

2022.1

2022.1.0

No breaking changes. The minor version has been incremented to notify about new default methods in our interfaces. Those shouldn’t concern you as a user though, as they are not meant to be implemented by you.

Noteworthy

Our integration tests on GitHub now uses the official GraalVM action: https://github.com/marketplace/actions/github-action-for-graalvm. Thanks, Gerrit, for integrating it.

๐Ÿš€ Features
  • Add size and hasSize on expressions. (#267)

๐Ÿงน Housekeeping
  • Some polishing (mainly working on getting a "warning free" build in all the tools)

  • Tons of dependency upgrades:

    • Bump testcontainers.version from 1.16.2 to 1.16.3 (#289)

    • Bump spring-boot-starter-parent from 2.6.2 to 2.6.3 (#290)

    • Bump mockito.version from 4.2.0 to 4.3.1 (#291)

    • Bump slf4j.version from 1.7.33 to 1.7.35 (#292)

    • Bump japicmp-maven-plugin from 0.15.4 to 0.15.6 (#293)

    • Bump neo4j-java-driver from 4.4.2 to 4.4.3 (#294)

    • Bump checkstyle from 9.2.1 to 9.3 (#295)

    • Bump asciidoctor-maven-plugin from 2.2.1 to 2.2.2 (#296)

    • Bump asciidoctorj from 2.5.2 to 2.5.3 (#285)

    • Bump maven-jar-plugin from 3.2.1 to 3.2.2 (#284)

    • Bump spring-data-neo4j from 6.2.0 to 6.2.1 (#283)

    • Bump reactor-bom from 2020.0.14 to 2020.0.15 (#282)

    • Bump slf4j.version from 1.7.32 to 1.7.33 (#281)

    • Bump neo4j-cypher-javacc-parser from 4.4.2 to 4.4.3 (#280)

    • Bump maven-jar-plugin from 3.2.0 to 3.2.1 (#277)

    • Bump checker-qual from 3.21.0 to 3.21.1 (#276)

    • Bump assertj-core from 3.21.0 to 3.22.0 (#272)

    • Bump maven-site-plugin from 3.9.1 to 3.10.0 (#270)

    • Bump maven-deploy-plugin from 3.0.0-M1 to 3.0.0-M2 (#271)

    • Bump checkstyle from 9.2 to 9.2.1 (#269)

    • Bump spring-boot-starter-parent from 2.6.1 to 2.6.2 (#268)

    • Bump Maven to 3.8.4.

2022.0

2022.0.0

Starting with the 2022 release line, all current experimental warnings have been removed, and we consider our API stable.

Noteworthy

As we have marked the API as stable we do enforce semantic versioning in our builds now. The parser module neo4j-cypher-dsl-parser has been updated to the Neo4j 4.4 parser and therefore doesn’t bring in Scala dependencies anymore. And last but not least, we added the Contributor Covenant Code of Conduct.

๐Ÿš€ Features
  • Indent CREATE in subqueries. (#254)

๐Ÿ› Bug Fixes
  • Fix broken asciidoc includes.

  • Give messages constant a better name (The bundle name we used might clash with other bundle names).

๐Ÿงน Housekeeping
  • Tons of dependency upgrades:

    • Bump reactor-bom from 2020.0.13 to 2020.0.14 (#265)

    • Bump checker-qual from 3.20.0 to 3.21.0 (#264)

    • Bump mockito.version from 4.1.0 to 4.2.0 (#263)

    • Bump neo4j-cypher-javacc-parser from 4.4.0 to 4.4.2 (#262)

    • Bump checker-qual from 3.19.0 to 3.20.0 (#261)

    • Bump neo4j-java-driver from 4.4.1 to 4.4.2 (#260)

    • Bump spring-boot-starter-parent from 2.5.6 to 2.6.1 (#259)

    • Bump checkstyle from 9.1 to 9.2 (#256)

    • Bump junit-bom from 5.8.1 to 5.8.2 (#257)

    • Bump mockito.version from 4.0.0 to 4.1.0 (#255)

Thanks again to Andy for his contributions and feedback.

2021.4

2021.4.2
๐Ÿ› Bug Fixes
  • GH-252 - Use a namespace for the message bundle.

Thanks to @Andy2003 for spotting this.

๐Ÿงน Housekeeping
  • Tons of dependency upgrades in test scope

  • The parser module now uses the Neo4j 4.3.7 parser

  • Bump apiguardian-api from 1.1.1 to 1.1.2 (#250)

2021.4.1
๐Ÿš€ Features

GH-230 - Add a way for a programmatic sort definition on expressions.

๐Ÿงน Housekeeping
  • Tons of dependency upgrades in test scope

  • Upgrade to Neo4j-Java-Driver 4.4.1. (a provided dependency)

  • The parser module now uses the Neo4j 4.3.6 parser

2021.4.0

2021.4.0 updates the optional dependency to Querydsl to 5.0.0. While this is API not a breaking change, it can be when the Cypher-DSL is run together with Querydsl on the Java Module path. Querydsl maintainer finally introduced automatic module names for all their module on which we can no reliable depend. As that module name is however different from the generated one, it will be a breaking change on the module path. Therefore we bump our version, too.

๐Ÿงน Housekeeping
  • Upgrade Querydsl to 5.0.0

2021.3

2021.3.4

2021.3.4 is a pure bug fix release.

๐Ÿ› Bug Fixes
  • GH-252 - Use a namespace for the message bundle.

2021.3.3

2021.3.3 is a pure housekeeping release, however a release we are proud of. We do analyze this project now with SonarQube vie Sonarcloud and are happy to announce that we have a quadruple A rating:

20211027 sonarcoud

In addition, we finally invited Dependabot taking care of at least creating the PRs.

๐Ÿงน Housekeeping
  • Fix reliability and security issues

  • Fix a minor amounts of remaining code smells

  • Bump several dependencies (only test and build related)

2021.3.2
๐Ÿš€ Features
  • Add support for QueryDSL’s equalsIgnoreCase operator

  • GH-204 - Provide access to identifiable expressions (See Section 2.2.2.3)

๐Ÿงน Housekeeping
  • Fix several (compile) warnings

  • Fix several spelling errors in api docs

  • Upgrade Spring Data Neo4j to 6.1.5 (In module org.neo4j.cypherdsl.codegen.sdn6)

  • Upgrade Neo4j Cypher Parser to 4.3.4 (In module neo4j-cypher-dsl-parser).

  • Verify examples on JDK 17

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.4

2021.2.4 is a pure bug fix release.

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

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/.