© 2020 Neo4j, Inc.,

This is the Neo4j Cypher-DSL manual version 2021.1.1.

Who should read this?

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

The Cypher-DSL is considered EXPERIMENTAL at the time of writing. This means you can and should use it and that we are looking actively for feedback, issues and problems. We will try to keep breaking changes to a minimum, but we reserve the right to modify public methods, parameters and behaviour in case we need those changes for the SDN/RX project, the origin of the Cypher-DSL. 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 SDN/RX in mind: 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/RX). 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/RX respectively SDN 6 can use the generated org.neo4j.cypherdsl.core.Statement directly with the Neo4jTemplate or the ReactiveNeo4jTemplate. Both the imperative and the reactive variants allow the retrieval and counting of entities without rendering a String first, for example through Neo4jTemplate#findAll(Statement, Class<T>).

1.3. Java API

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

2. Getting started

2.1. Prepare dependencies

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

2.1.1. Maven configuration

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

2.1.2. Gradle configuration

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

2.2. How to use it

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

The examples to follow are using JDK 11. We find the var keyword especially appealing in such a DSL as the types returned by the DSL 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 build and can for example be used directly with Neo4j-Java-Driver.

2.2.1. Examples

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

They use the following imports:

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

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

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

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

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

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

Match all nodes with a given set of properties:

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

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

Limit the number of returned things and return only one attribute

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

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

Create complex conditions

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

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

Build relationships

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

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

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

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

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

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

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

2.2.2. More features

Retrieving parameters being defined

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

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

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

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

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

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

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

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

Using the default renderer

A statement can render itself as well:

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

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

Generating formatted Cypher

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

Instances of Configuration are threadsafe 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.
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 builtin 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 express 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 iin 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. Building a static meta model

5.1. Concepts

5.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 completly 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 fit’s nicely with Neo4j’s capabilities: Neo4j is a schema less 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 maybe the information we can retrieve from the database itself via db.schema.nodeTypeProperties and db.schema.relTypeProperties.

5.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 it’s default implementation NodeBase)

    • Relationships via Relationship (and it’s default implementation RelationshipBase)

  • Property, which is a 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 it’s 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.

5.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 obove

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 accomadate 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 final Property ROLE = this.property("role"); (2)

    protected ActedIn(Person start, Movie end) {
        super(start, "ACTED_IN", end); (3)
    }

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

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

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

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

        return new ActedIn(getSymbolicName().orElse(null), getLeft(), getRequiredType(), 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 see 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.

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

5.2. Possible Usage

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

movie graph

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

final class Movie extends NodeBase<Movie> {

    public static final Movie MOVIE = new Movie();

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

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

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

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

final class Person extends NodeBase<Person> {

    public static final Person PERSON = new Person();

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

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

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

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

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

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

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

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

    protected ActedIn(Person start, Movie end) {
        super(start, "ACTED_IN", end);
    }
}

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

    protected Directed(Person start, E end) {
        super(start, "DIRECTED", end);
    }
}

5.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'\\)");

5.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)

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

5.3. The Spring Data Neo4j 6 annotation processor

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

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

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

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

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

5.3.1. Configure your build

Maven

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

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

In a Gradle project, please add the following:

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

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

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-cypher-dsl-codegen-sdn6</artifactId>
    <version>{neo4j-cypher-dsl.version}</version>
    <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>

5.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 timestampe 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

The generated classes can be used in variety of places:

@RestController
@RequestMapping("/api/movies")
public final class MoviesController {

    private final MovieRepository movieRepository;

    private final PeopleRepository peopleRepository;

    MoviesController(MovieRepository movieRepository, PeopleRepository peopleRepository) {
        this.movieRepository = movieRepository;
        this.peopleRepository = peopleRepository;
    }

    @GetMapping({ "", "/" })
    public List<Movie> get() {
        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

Or build complex queries with it:

class MovieRepositoryExtImpl implements MovieRepositoryExt {

    private final Neo4jTemplate neo4jTemplate;

    MovieRepositoryExtImpl(Neo4jTemplate neo4jTemplate) {

        this.neo4jTemplate = neo4jTemplate;
    }

    @Override
    public List<Movie> findAllMoviesRelatedTo(Person personOfInterest) {

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

        var statement = Cypher.match(person)
            .where(person.NAME.isEqualTo(Cypher.parameter("name")))
            .with(person)
            .optionalMatch(new ActedIn_(person, actedIn))
            .optionalMatch(new Directed_(person, directed))
            .with(Functions.collect(actedIn).add(Functions.collect(directed))
                .as("movies"))
            .unwind("movies").as(m)
            .returningDistinct(m)
            .orderBy(Movie_.MOVIE.named(m).TITLE).ascending()
            .build();

        return this.neo4jTemplate.findAll(
            statement, Map.of("name", personOfInterest.getName()), Movie.class);
    }
}

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

6. Appendix

Query-DSL support

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

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

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

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

  • create org.neo4j.cypherdsl.core.SymbolicName

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

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

Here’s one example on how to use it:

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

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

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

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

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

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

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

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

Required dependencies

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

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

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

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

Building the Neo4j Cypher-DSL

Requirements

For the full project, including examples and native tests:

For the project, including examples but skipping native tests

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

The build requires a local copy of the project:

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

Full build (including examples and native tests)

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

Listing 16. 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 17. 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 18. Build with default settings on Linux / macOS
$ ./mvnw clean verify

On a Windows machine, use

Listing 19. 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 20. 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 21. Skipping native tests
$ ./mvnw clean verify -pl org.neo4j:neo4j-cypher-dsl

Architecture

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

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

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

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

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

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

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

Coding rules

Consistent naming

The following naming conventions are used throughout the project:

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

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

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

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

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

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

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

Structure

Neo4j-Cypher-DSL Core

The core of the Cypher-DSL is consist of a set of classese 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 26. The Cypher-DSL core package must not depend on the rendering infrastructure
MATCH (a:Main:Artifact)
MATCH (a)-[:CONTAINS]->(p1:Package)
WHERE p1.fqn in ['org.neo4j.cypherdsl.core']
WITH p1, a
MATCH (p1)-[:CONTAINS]->(t:Type)
MATCH (t)-[:DEPENDS_ON]->(t2:Type)<-[:CONTAINS]-(p2:Package)<-[:CONTAINS]-(a)
WHERE p2.fqn = 'org.neo4j.cypherdsl.core.renderer'
  AND t.fqn <> 'org.neo4j.cypherdsl.core.AbstractStatement'
RETURN t, t2

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

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

Change log

2021.1

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