Chapter 3. Cypher workflow

This section describes how to create units of work and provide a logical context for that work.

3.1. Overview

The Neo4j Drivers expose a Cypher Channel over which database work can be carried out (see the Cypher Manual for more information on the Cypher Query Language).

Work itself is organized into sessions, transactions and queries, as follows:

Figure 3.1. Sessions, queries and transactions
sessions queries transactions

Sessions are always bound to a single transaction context, which is typically an individual database.

Using the bookmark mechanism, sessions also provide a guarantee of correct transaction sequencing, even when transactions occur over multiple cluster members. This effect is known as causal chaining.

3.2. Sessions

Sessions are lightweight containers for causally chained sequences of transactions (see Operations Manual → Causal consistency). They essentially provide context for storing transaction sequencing information in the form of bookmarks.

When a transaction begins, the session in which it is contained acquires a connection from the driver connection pool. On commit (or rollback) of the transaction, the session releases that connection again. This means that it is only when a session is carrying out work that it occupies a connection resource. When idle, no such resource is in use.

Due to the sequencing guaranteed by a session, sessions may only host one transaction at a time. For parallel execution, multiple sessions should be used. In languages where thread safety is an issue, sessions should not be considered thread-safe.

Closing a session forces any open transaction to be rolled back, and its associated connection to consequently be released back into the pool.

Sessions are bound to a single transactional context, specified on construction. Neo4j exposes each database inside its own context, thereby prohibiting cross-database transactions (or sessions) by design. Similarly, sessions bound to different databases may not be causally chained by propagating bookmarks between them.

Individual language drivers provide several session classes, each oriented around a particular programming style. Each session class provides a similar set of functionality but offers client applications a choice based on how the application is structured and what frameworks are in use, if any.

The session classes are described in Chapter 4, The session API. For more details, please see the API documentation for your language.

3.3. Transactions

Transactions are atomic units of work containing one or more Cypher Queries. Transactions may contain read or write work, and will generally be routed to an appropriate server for execution, where they will be carried out in their entirety. In case of a transaction failure, the transaction needs to be retried from the beginning. This is the responsibility of the transaction manager.

The Neo4j Drivers provide transaction management via the transaction function mechanism. This mechanism is exposed through methods on the Session object which accept a function object that can be played multiple times against different servers until it either succeeds or a timeout is reached. This approach is recommended for most client applications.

A convenient short-form alternative is the auto-commit transaction mechanism. This provides a limited form of transaction management for single-query transactions, as a trade-off for a slightly smaller code overhead. This form of transaction is useful for quick scripts and environments where high availability guarantees are not required. It is also the required form of transaction for running PERIODIC COMMIT queries, which are the only type of Cypher Query to manage their own transactions.

A lower-level unmanaged transaction API is also available for advanced use cases. This is useful when an alternative transaction management layer is applied by the client, in which error handling and retries need to be managed in a custom way. To learn more about how to use unmanaged transactions, see API documentation for the relevant language.

3.4. Queries and results

Queries consist of a request to the server to execute a Cypher statement, followed by a response back to the client with the result. Results are transmitted as a stream of records, along with header and footer metadata, and can be incrementally consumed by a client application. With reactive capabilities, the semantics of the record stream can be enhanced by allowing a Cypher result to be paused or cancelled part-way through.

To execute a Cypher Query, the query text is required along with an optional set of named parameters. The text can contain parameter placeholders that are substituted with the corresponding values at runtime. While it is possible to run non-parameterized Cypher Queries, good programming practice is to use parameters in Cypher Queries whenever possible. This allows for the caching of queries within the Cypher Engine, which is beneficial for performance. Parameter values should adhere to Section 2.1, “Values and types”.

A result summary is also generally available. This contains additional information relating to the query execution and the result content. For an EXPLAIN or PROFILE query, this is where the query plan is returned. See Cypher Manual → Profiling a query for more information on these queries.

3.5. Causal chaining and bookmarks

When working with a Causal Cluster, transactions can be chained, via a session, to ensure causal consistency. This means that for any two transactions, it is guaranteed that the second transaction will begin only after the first has been successfully committed. This is true even if the transactions are carried out on different physical cluster members. For more information on Causal Clusters, please refer to Operations Manual → Clustering.

Internally, causal chaining is carried out by passing bookmarks between transactions. Each bookmark records one or more points in transactional history for a particular database, and can be used to inform cluster members to carry out units of work in a particular sequence. On receipt of a bookmark, the server will block until it has caught up with the relevant transactional point in time.

An initial bookmark is sent from client to server on beginning a new transaction, and a final bookmark is returned on successful completion. Note that this applies to both read and write transactions.

Bookmark propagation is carried out automatically within sessions and does not require any explicit signal or setting from the application. To opt out of this mechanism, for unrelated units of work, applications can use multiple sessions. This avoids the small latency overhead of the causal chain.

Bookmarks can be passed between sessions by extracting the last bookmark from a session and passing this into the construction of another. Multiple bookmarks can also be combined if a transaction has more than one logical predecessor. Note that it is only when chaining across sessions that an application will need to work with bookmarks directly.

Figure 3.2. Passing bookmarks
driver passing bookmarks
Example 3.1. Pass bookmarks

Pass bookmarks. 

// Create a company node
private IResult AddCompany(ITransaction tx, string name)
{
    return tx.Run("CREATE (a:Company {name: $name})", new {name});
}

// Create a person node
private IResult AddPerson(ITransaction tx, string name)
{
    return tx.Run("CREATE (a:Person {name: $name})", new {name});
}

// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
private IResult Employ(ITransaction tx, string personName, string companyName)
{
    return tx.Run(@"MATCH (person:Person {name: $personName})
             MATCH (company:Company {name: $companyName})
             CREATE (person)-[:WORKS_FOR]->(company)", new {personName, companyName});
}

// Create a friendship between two people.
private IResult MakeFriends(ITransaction tx, string name1, string name2)
{
    return tx.Run(@"MATCH (a:Person {name: $name1})
             MATCH (b:Person {name: $name2})
             MERGE (a)-[:KNOWS]->(b)", new {name1, name2});
}

// Match and display all friendships.
private int PrintFriendships(ITransaction tx)
{
    var result = tx.Run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name");

    var count = 0;
    foreach (var record in result)
    {
        count++;
        Console.WriteLine($"{record["a.name"]} knows {record["b.name"]}");
    }

    return count;
}

public void AddEmployAndMakeFriends()
{
    // To collect the session bookmarks
    var savedBookmarks = new List<Bookmark>();

    // Create the first person and employment relationship.
    using (var session1 = Driver.Session(o => o.WithDefaultAccessMode(AccessMode.Write)))
    {
        session1.WriteTransaction(tx => AddCompany(tx, "Wayne Enterprises"));
        session1.WriteTransaction(tx => AddPerson(tx, "Alice"));
        session1.WriteTransaction(tx => Employ(tx, "Alice", "Wayne Enterprises"));

        savedBookmarks.Add(session1.LastBookmark);
    }

    // Create the second person and employment relationship.
    using (var session2 = Driver.Session(o => o.WithDefaultAccessMode(AccessMode.Write)))
    {
        session2.WriteTransaction(tx => AddCompany(tx, "LexCorp"));
        session2.WriteTransaction(tx => AddPerson(tx, "Bob"));
        session2.WriteTransaction(tx => Employ(tx, "Bob", "LexCorp"));

        savedBookmarks.Add(session2.LastBookmark);
    }

    // Create a friendship between the two people created above.
    using (var session3 = Driver.Session(o =>
        o.WithDefaultAccessMode(AccessMode.Write).WithBookmarks(savedBookmarks.ToArray())))
    {
        session3.WriteTransaction(tx => MakeFriends(tx, "Alice", "Bob"));

        session3.ReadTransaction(PrintFriendships);
    }
}

Pass bookmarks. 

// Create a company node
private Result addCompany(final Transaction tx, final String name )
{
    return tx.run( "CREATE (:Company {name: $name})", parameters( "name", name ) );
}

// Create a person node
private Result addPerson(final Transaction tx, final String name )
{
    return tx.run( "CREATE (:Person {name: $name})", parameters( "name", name ) );
}

// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
private Result employ(final Transaction tx, final String person, final String company )
{
    return tx.run( "MATCH (person:Person {name: $person_name}) " +
                    "MATCH (company:Company {name: $company_name}) " +
                    "CREATE (person)-[:WORKS_FOR]->(company)",
            parameters( "person_name", person, "company_name", company ) );
}

// Create a friendship between two people.
private Result makeFriends(final Transaction tx, final String person1, final String person2 )
{
    return tx.run( "MATCH (a:Person {name: $person_1}) " +
                    "MATCH (b:Person {name: $person_2}) " +
                    "MERGE (a)-[:KNOWS]->(b)",
            parameters( "person_1", person1, "person_2", person2 ) );
}

// Match and display all friendships.
private Result printFriends(final Transaction tx )
{
    Result result = tx.run( "MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name" );
    while ( result.hasNext() )
    {
        Record record = result.next();
        System.out.println( String.format( "%s knows %s", record.get( "a.name" ).asString(), record.get( "b.name" ).toString() ) );
    }
    return result;
}

public void addEmployAndMakeFriends()
{
    // To collect the session bookmarks
    List<Bookmark> savedBookmarks = new ArrayList<>();

    // Create the first person and employment relationship.
    try ( Session session1 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) )
    {
        session1.writeTransaction( tx -> addCompany( tx, "Wayne Enterprises" ) );
        session1.writeTransaction( tx -> addPerson( tx, "Alice" ) );
        session1.writeTransaction( tx -> employ( tx, "Alice", "Wayne Enterprises" ) );

        savedBookmarks.add( session1.lastBookmark() );
    }

    // Create the second person and employment relationship.
    try ( Session session2 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).build() ) )
    {
        session2.writeTransaction( tx -> addCompany( tx, "LexCorp" ) );
        session2.writeTransaction( tx -> addPerson( tx, "Bob" ) );
        session2.writeTransaction( tx -> employ( tx, "Bob", "LexCorp" ) );

        savedBookmarks.add( session2.lastBookmark() );
    }

    // Create a friendship between the two people created above.
    try ( Session session3 = driver.session( builder().withDefaultAccessMode( AccessMode.WRITE ).withBookmarks( savedBookmarks ).build() ) )
    {
        session3.writeTransaction( tx -> makeFriends( tx, "Alice", "Bob" ) );

        session3.readTransaction( this::printFriends );
    }
}

Import bookmarks. 

import java.util.ArrayList;
import java.util.List;

import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.Result;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.Bookmark;

import static org.neo4j.driver.Values.parameters;
import static org.neo4j.driver.SessionConfig.builder;

Pass bookmarks. 

// Create a company node
function addCompany (tx, name) {
  return tx.run('CREATE (a:Company {name: $name})', { name: name })
}

// Create a person node
function addPerson (tx, name) {
  return tx.run('CREATE (a:Person {name: $name})', { name: name })
}

// Create an employment relationship to a pre-existing company node.
// This relies on the person first having been created.
function addEmployee (tx, personName, companyName) {
  return tx.run(
    'MATCH (person:Person {name: $personName}) ' +
      'MATCH (company:Company {name: $companyName}) ' +
      'CREATE (person)-[:WORKS_FOR]->(company)',
    { personName: personName, companyName: companyName }
  )
}

// Create a friendship between two people.
function makeFriends (tx, name1, name2) {
  return tx.run(
    'MATCH (a:Person {name: $name1}) ' +
      'MATCH (b:Person {name: $name2}) ' +
      'MERGE (a)-[:KNOWS]->(b)',
    { name1: name1, name2: name2 }
  )
}

// To collect friend relationships
const friends = []

// Match and display all friendships.
function findFriendships (tx) {
  const result = tx.run('MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name')

  result.subscribe({
    onNext: record => {
      const name1 = record.get(0)
      const name2 = record.get(1)

      friends.push({ name1: name1, name2: name2 })
    }
  })
}

// To collect the session bookmarks
const savedBookmarks = []

// Create the first person and employment relationship.
const session1 = driver.session({ defaultAccessMode: neo4j.WRITE })
const first = session1
  .writeTransaction(tx => addCompany(tx, 'Wayne Enterprises'))
  .then(() => session1.writeTransaction(tx => addPerson(tx, 'Alice')))
  .then(() =>
    session1.writeTransaction(tx =>
      addEmployee(tx, 'Alice', 'Wayne Enterprises')
    )
  )
  .then(() => {
    savedBookmarks.push(session1.lastBookmark())
  })
  .then(() => session1.close())

// Create the second person and employment relationship.
const session2 = driver.session({ defaultAccessMode: neo4j.WRITE })
const second = session2
  .writeTransaction(tx => addCompany(tx, 'LexCorp'))
  .then(() => session2.writeTransaction(tx => addPerson(tx, 'Bob')))
  .then(() =>
    session2.writeTransaction(tx => addEmployee(tx, 'Bob', 'LexCorp'))
  )
  .then(() => {
    savedBookmarks.push(session2.lastBookmark())
  })
  .then(() => session2.close())

// Create a friendship between the two people created above.
const last = Promise.all([first, second]).then(() => {
  const session3 = driver.session({
    defaultAccessMode: neo4j.WRITE,
    bookmarks: savedBookmarks
  })

  return session3
    .writeTransaction(tx => makeFriends(tx, 'Alice', 'Bob'))
    .then(() =>
      session3.readTransaction(findFriendships).then(() => session3.close())
    )
})

class BookmarksExample:

    def __init__(self, uri, auth):
        self.driver = GraphDatabase.driver(uri, auth=auth)

    def close(self):
        self.driver.close()

    # Create a person node.
    @classmethod
    def create_person(cls, tx, name):
        tx.run("CREATE (:Person {name: $name})", name=name)

    # Create an employment relationship to a pre-existing company node.
    # This relies on the person first having been created.
    @classmethod
    def employ(cls, tx, person_name, company_name):
        tx.run("MATCH (person:Person {name: $person_name}) "
               "MATCH (company:Company {name: $company_name}) "
               "CREATE (person)-[:WORKS_FOR]->(company)",
               person_name=person_name, company_name=company_name)

    # Create a friendship between two people.
    @classmethod
    def create_friendship(cls, tx, name_a, name_b):
        tx.run("MATCH (a:Person {name: $name_a}) "
               "MATCH (b:Person {name: $name_b}) "
               "MERGE (a)-[:KNOWS]->(b)",
               name_a=name_a, name_b=name_b)

    # Match and display all friendships.
    @classmethod
    def print_friendships(cls, tx):
        result = tx.run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
        for record in result:
            print("{} knows {}".format(record["a.name"], record["b.name"]))

    def main(self):
        saved_bookmarks = []  # To collect the session bookmarks

        # Create the first person and employment relationship.
        with self.driver.session() as session_a:
            session_a.write_transaction(self.create_person, "Alice")
            session_a.write_transaction(self.employ, "Alice", "Wayne Enterprises")
            saved_bookmarks.append(session_a.last_bookmark())

        # Create the second person and employment relationship.
        with self.driver.session() as session_b:
            session_b.write_transaction(self.create_person, "Bob")
            session_b.write_transaction(self.employ, "Bob", "LexCorp")
            saved_bookmarks.append(session_b.last_bookmark())

        # Create a friendship between the two people created above.
        with self.driver.session(bookmarks=saved_bookmarks) as session_c:
            session_c.write_transaction(self.create_friendship, "Alice", "Bob")
            session_c.read_transaction(self.print_friendships)

3.6. Routing transactions using access modes

Transactions can be executed in either read or write mode; this is known as the access mode. In a Causal Cluster, each transaction will be routed to an appropriate server based on the mode. When using a single instance, all transactions will be passed to that one server.

Routing Cypher by identifying reads and writes can improve the utilization of available cluster resources. Since read servers are typically more plentiful than write servers, it is beneficial to direct read traffic to read servers instead of the write server. Doing so helps in keeping write servers available for write transactions.

Access mode is generally specified by the method used to call the transaction function. Session classes provide a method for calling reads and another for writes.

As a fallback for auto-commit and unmanaged transactions, a default access mode can also be provided at session level. This is only used in cases when the access mode cannot otherwise be specified. In case a transaction function is used within that session, the default access mode will be overridden.

The driver does not parse Cypher and therefore cannot automatically determine whether a transaction is intended to carry out read or write operations. As a result, a write transaction tagged as a read will still be sent to a read server, but will fail on execution.

Example 3.2. Read-write transaction

Read write transaction. 

public long AddPerson(string name)
{
    using (var session = Driver.Session())
    {
        session.WriteTransaction(tx => CreatePersonNode(tx, name));
        return session.ReadTransaction(tx => MatchPersonNode(tx, name));
    }
}

private static IResult CreatePersonNode(ITransaction tx, string name)
{
    return tx.Run("CREATE (a:Person {name: $name})", new {name});
}

private static long MatchPersonNode(ITransaction tx, string name)
{
    var result = tx.Run("MATCH (a:Person {name: $name}) RETURN id(a)", new {name});
    return result.Single()[0].As<long>();
}

Read write transaction. 

public long addPerson( final String name )
{
    try ( Session session = driver.session() )
    {
        session.writeTransaction( new TransactionWork<Void>()
        {
            @Override
            public Void execute( Transaction tx )
            {
                return createPersonNode( tx, name );
            }
        } );
        return session.readTransaction( new TransactionWork<Long>()
        {
            @Override
            public Long execute( Transaction tx )
            {
                return matchPersonNode( tx, name );
            }
        } );
    }
}

private static Void createPersonNode( Transaction tx, String name )
{
    tx.run( "CREATE (a:Person {name: $name})", parameters( "name", name ) );
    return null;
}

private static long matchPersonNode( Transaction tx, String name )
{
    Result result = tx.run( "MATCH (a:Person {name: $name}) RETURN id(a)", parameters( "name", name ) );
    return result.single().get( 0 ).asLong();
}

Import read write transaction. 

import org.neo4j.driver.Session;
import org.neo4j.driver.Result;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.TransactionWork;

import static org.neo4j.driver.Values.parameters;

Read write transaction. 

const session = driver.session()

try {
  await session.writeTransaction(tx =>
    tx.run('CREATE (a:Person {name: $name})', { name: personName })
  )

  const result = await session.readTransaction(tx =>
    tx.run('MATCH (a:Person {name: $name}) RETURN id(a)', {
      name: personName
    })
  )

  const singleRecord = result.records[0]
  const createdNodeId = singleRecord.get(0)

  console.log('Matched created node with id: ' + createdNodeId)
} finally {
  await session.close()
}

def create_person_node(tx, name):
    tx.run("CREATE (a:Person {name: $name})", name=name)

def match_person_node(tx, name):
    result = tx.run("MATCH (a:Person {name: $name}) RETURN count(a)", name=name)
    return result.single()[0]

def add_person(name):
    with driver.session() as session:
        session.write_transaction(create_person_node, name)
        persons = session.read_transaction(match_person_node, name)
        return persons

3.7. Databases and execution context

Neo4j offers the ability to work with multiple databases within the same DBMS.

For a single instance, this is limited to one user database, plus the system database.

From a driver API perspective, a database can be selected on session construction, and is used as an execution context for the transactions within that session. It is not currently possible to execute a transaction across multiple databases.

In a multi-database environment, the server tags one database as default. This is selected whenever a session is created without naming a particular database as default. In an environment with a single database, that database is always the default.

For more information about managing multiple databases within the same DBMS, refer to Cypher Manual → Neo4j databases and graphs which has a full breakdown of the Neo4j data storage hierarchy.

3.7.1. The system database

Some administrative operations must be carried out against the system database. To do this, construct a session targeting the database called system and execute Cypher as usual. Please see Cypher Manual → Databases for more information.

3.7.2. Database selection

The database selection on the client side happens on the session API.

You pass the name of the database to the driver during session creation. If you don’t specify a name, the default database is used. The database name must not be null, nor an empty string.

The selection of database is only possible when the driver is connected against Neo4j Enterprise Edition. Changing to any other database than the default database in Neo4j Community Edition will lead to a runtime error.

Please note that the database that is requested must exist.

Example 3.3. Database selection on session creation

Database selection on session creation. 

using (var session = _driver.Session(SessionConfigBuilder.ForDatabase("examples")))
{
    session.Run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").Consume();
}

void SessionConfig(SessionConfigBuilder configBuilder) =>
    configBuilder.WithDatabase("examples")
        .WithDefaultAccessMode(AccessMode.Read)
        .Build();

using (var session = _driver.Session(SessionConfig))
{
    var result = session.Run("MATCH (a:Greeting) RETURN a.message as msg");
    var msg = result.Single()[0].As<string>();
    Console.WriteLine(msg);
}

Database selection on session creation. 

try ( Session session = driver.session( SessionConfig.forDatabase( "examples" ) ) )
{
    session.run( "CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a" ).consume();
}

SessionConfig sessionConfig = SessionConfig.builder()
        .withDatabase( "examples" )
        .withDefaultAccessMode( AccessMode.READ )
        .build();
try ( Session session = driver.session( sessionConfig ) )
{
    String msg = session.run( "MATCH (a:Greeting) RETURN a.message as msg" ).single().get( "msg" ).asString();
    System.out.println(msg);
}

Import database selection on session creation. 

import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;

Database selection on session creation. 

const session = driver.session({ database: 'examples' })
try {
  const result = await session.writeTransaction(tx =>
    tx.run(
      'CREATE (a:Greeting {message: "Hello, Example-Database"}) RETURN a.message'
    )
  )

  const singleRecord = result.records[0]
  const greeting = singleRecord.get(0)

  console.log(greeting)
} finally {
  await session.close()
}

const readSession = driver.session({
  database: 'examples',
  defaultAccessMode: neo4j.READ
})
try {
  const result = await readSession.writeTransaction(tx =>
    tx.run('MATCH (a:Greeting) RETURN a.message')
  )

  const singleRecord = result.records[0]
  const greeting = singleRecord.get(0)

  console.log(greeting)
} finally {
  await readSession.close()
}

Database selection on session creation. 

with driver.session(database="example") as session:
    session.run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").consume()

with driver.session(database="example", default_access_mode=READ_ACCESS) as session:
    message = session.run("MATCH (a:Greeting) RETURN a.message as msg").single().get("msg")
    print(message)

from neo4j import READ_ACCESS

3.8. Type mapping

Drivers translate between application language types and the Cypher Types.

To pass parameters and process results, it is important to know the basics of how Cypher works with types and to understand how the Cypher Types are mapped in the driver.

The table below shows the available data types. All types can be potentially found in the result, although not all types can be used as parameters.

Cypher Type Parameter Result

null*

List

Map

Boolean

Integer

Float

String

ByteArray

Date

Time

LocalTime

DateTime

LocalDateTime

Duration

Point

Node**

 

Relationship**

 

Path**

 

* The null marker is not a type but a placeholder for absence of value. For information on how to work with null in Cypher, please refer to Cypher Manual → Working with null.

** Nodes, relationships and paths are passed in results as snapshots of the original graph entities. While the original entity IDs are included in these snapshots, no permanent link is retained back to the underlying server-side entities, which may be deleted or otherwise altered independently of the client copies. Graph structures may not be used as parameters because it depends on application context whether such a parameter would be passed by reference or by value, and Cypher provides no mechanism to denote this. Equivalent functionality is available by simply passing either the ID for pass-by-reference, or an extracted map of properties for pass-by-value.

The Neo4j Drivers map Cypher Types to and from native language types as depicted in the table below. Custom types (those not available in the language or standard library) are highlighted in bold.

Example 3.4. Map Neo4j types to native language types
Neo4j Cypher Type .NET Type

null

null

List

IList<object>

Map

IDictionary<string, object>

Boolean

bool

Integer

long

Float

double

String

string

ByteArray

byte[]

Date

LocalDate

Time

OffsetTime

LocalTime

LocalTime

DateTime*

ZonedDateTime

LocalDateTime

LocalDateTime

Duration

Duration

Point

Point

Node

INode

Relationship

IRelationship

Path

IPath

* Time zone names adhere to the IANA system, rather than the Windows system. Inbound conversion is carried out using Extended Windows-Olson zid mapping as defined by Unicode CLDR.

Neo4j Cypher Type Java Type

null

null

List

List<Object>

Map

Map<String, Object>

Boolean

boolean

Integer

long

Float

double

String

String

ByteArray

byte[]

Date

LocalDate

Time

OffsetTime

LocalTime

LocalTime

DateTime

ZonedDateTime

LocalDateTime

LocalDateTime

Duration

IsoDuration*

Point

Point

Node

Node

Relationship

Relationship

Path

Path

* A Duration or Period passed as a parameter will always be implicitly converted to IsoDuration.

Neo4j Cypher Type JavaScript Type

null

null

List

Array

Map

Object

Boolean

Boolean

Integer

Integer*

Float

Number

String

String

ByteArray

Int8Array

Date

Date

Time

Time

LocalTime

LocalTime

DateTime

DateTime

LocalDateTime

LocalDateTime

Duration

Duration

Point

Point

Node

Node

Relationship

Relationship

Path

Path

* JavaScript has no native integer type so a custom type is provided. For convenience, this can be disabled through configuration so that the native Number type is used instead. Note that this can lead to a loss of precision.

Neo4j Type Python 3 Type

null

None

List

list

Map

dict

Boolean

bool

Integer

int

Float

float

String

str

ByteArray

bytearray

Date

neo4j.time.Date

Time

neo4j.time.Time

LocalTime

neo4j.time.Time††

DateTime

neo4j.time.DateTime

LocalDateTime

neo4j.time.DateTime††

Duration

neo4j.time.Duration*

Point

neo4j.spatial.Point

Node

neo4j.graph.Node

Relationship

neo4j.graph.Relationship

Path

neo4j.graph.Path

* A datetime.timedelta object passed as a parameter will always be implicitly converted to neo4j.time.Duration

† Where tzinfo is not None

†† Where tzinfo is None

3.9. Exceptions and error handling

When executing Cypher or carrying out other operations with the driver, certain exceptions and error cases may arise. Server-generated exceptions are each associated with a status code that describes the nature of the problem and a message that provides more detail.

The classifications are listed in the table below.

Table 3.1. Server status code classifications
Classification Description

ClientError

The client application has caused an error. The application should amend and retry the operation.

DatabaseError

The server has caused an error. Retrying the operation will generally be unsuccessful.

TransientError

A temporary error has occurred. The application should retry the operation.

3.9.1. Service unavailable

A Service Unavailable exception will be signalled when the driver is no longer able to establish communication with the server, even after retries.

Encountering this condition usually indicates a fundamental networking or database problem.

While certain mitigations can be made by the driver to avoid this issue, there will always be cases when this is impossible. As such, it is highly recommended to ensure client applications contain a code path that can be followed when the client is no longer able to communicate with the server.

3.9.2. Transient errors

Transient errors are those which are generated by the server and marked as safe to retry without alteration to the original request. Examples of such errors are deadlocks and memory issues.

When using transaction functions, the driver will usually be able to automatically retry when a transient failure occurs.