4.4. Working with Cypher values

This section describes the types and values used by Cypher and how they map to native language types.

4.4.1. The Cypher type system

Drivers translate between application language types and the Cypher type system. To pass parameters and process results, it is important to know the basics of the Cypher type system and to understand how the Cypher types are mapped in the driver.

Cypher types are split into two groups: basic types and graph structure types. While query results can contain any of these, only basic types can be used as parameters.

4.4.1.1. Basic types

The basic types are:

  • Boolean
  • Integer
  • Float
  • String
  • List
  • Map

For information on how to work with null in Cypher, please refer to Section 3.2.10, “Working with null.

4.4.1.2. Graph structure types

In addition to the basic types which can be used both for parameters and results, the following Cypher types are valid for results only:

  • Node
  • Relationship
  • Path

When streaming a result, the server takes snapshots of the original graph structure values to pass back to the client. 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.

4.4.1.3. Type mappings

The Neo4j driver maps Cypher types to native language types as depicted in the table below.

Example 4.23. Map Neo4j types to native language types
Neo4j .NET

Null

null

Boolean

System.Boolean

Integer

System.Int64

Float

System.Double

Bytes

System.Byte[]

String

System.String

List

System.Collections.Generic.IList<T>

Map

System.Collections.IDictionary<TKey, TValue>

Node

Neo4j.Driver.V1.INode

Relationship

Neo4j.Driver.V1.IRelationship

Path

Neo4j.Driver.V1.IPath

Neo4j Java

Null

null

Boolean

java.lang.Boolean

Integer

java.lang.Long

Float

java.lang.Double

Bytes

byte[]

String

java.lang.String

List

java.util.List<T>

Map

java.util.Map<K, V>

Node

org.neo4j.driver.v1.types.Node

Relationship

org.neo4j.driver.v1.types.Relationship

Path

org.neo4j.driver.v1.types.Path

Neo4j JavaScript

Null

null

Boolean

Boolean

Integer

Integer (*)

Float

Number

Bytes

Int8Array

String

String

List

Array

Map

Object

Node

Node

Relationship

Relationship

Path

Path

Neo4j Python

Null

None

Boolean

bool

Integer

int/long (†)

Float

float

Bytes

bytes

String

str/unicode (†)

List

list

Map

dict

Node

Node

Relationship

Relationship

Path

Path

4.4.2. Statement results

A statement result is comprised of a stream of records. The result is typically handled by the receiving application as it arrives, although it is also possible to retain all or part of a result for future consumption.

Figure 4.2. Result stream
driver result stream

4.4.2.1. Records

A record provides an immutable view of part of a result. It is an ordered map of keys and values. As values within a record are both keyed and ordered, that value can be accessed either by position (0-based integer) or by key (string).

4.4.2.2. The buffer

Figure 4.3. Result buffer
driver result buffer

Most drivers contain a result buffer. This provides a staging point for results, and divides result handling into fetching (moving from the network to the buffer) and consuming (moving from the buffer to the application).

If results are consumed in the same order as they are produced, records merely pass through the buffer; if they are consumed out of order, the buffer will be utilized to retain records until they are consumed by the application. For large results, this may require a significant amount of memory and impact performance. For this reason, it is recommended to consume results in order wherever possible.

4.4.2.3. Consuming the stream

Query results will often be consumed as a stream. Drivers provide a language-idiomatic way to iterate through a result stream.

Example 4.24. Consuming the stream
public List<string> GetPeople()
{
    using (var session = Driver.Session())
    {
        return session.ReadTransaction(tx =>
        {
            var result = tx.Run("MATCH (a:Person) RETURN a.name ORDER BY a.name");
            return result.Select(record => record[0].As<string>()).ToList();
        });
    }
}
public List<String> getPeople()
{
    try ( Session session = driver.session() )
    {
        return session.readTransaction( new TransactionWork<List<String>>()
        {
            @Override
            public List<String> execute( Transaction tx )
            {
                return matchPersonNodes( tx );
            }
        } );
    }
}

private static List<String> matchPersonNodes( Transaction tx )
{
    List<String> names = new ArrayList<>();
    StatementResult result = tx.run( "MATCH (a:Person) RETURN a.name ORDER BY a.name" );
    while ( result.hasNext() )
    {
        names.add( result.next().get( 0 ).asString() );
    }
    return names;
}
const session = driver.session();
const result = session.run('MATCH (a:Person) RETURN a.name ORDER BY a.name');
const collectedNames = [];

result.subscribe({
  onNext: record => {
    const name = record.get(0);
    collectedNames.push(name);
  },
  onCompleted: () => {
    session.close();

    console.log('Names: ' + collectedNames.join(', '));
  },
  onError: error => {
    console.log(error);
  }
});
def get_people(self):
    with self._driver.session() as session:
        return session.read_transaction(self.match_person_nodes)

@staticmethod
def match_person_nodes(tx):
    result = tx.run("MATCH (a:Person) RETURN a.name ORDER BY a.name")
    return [record["a.name"] for record in result]

4.4.2.4. Retaining results

The result record stream is available until another statement is run in the session, or until the current transaction is closed. To hold on to the results beyond this scope, the results need to be explicitly retained. For retaining results, each driver offers methods that collect the result stream and translate it into standard data structures for that language. Retained results can be processed while the session is free to take on the next workload. The saved results can also be used directly to run a new statement.

Example 4.25. Retain results for further processing
public int AddEmployees(string companyName)
{
    using (var session = Driver.Session())
    {
        var persons = session.ReadTransaction(tx => tx.Run("MATCH (a:Person) RETURN a.name AS name").ToList());
        return persons.Sum(person => session.WriteTransaction(tx =>
        {
            tx.Run("MATCH (emp:Person {name: $person_name}) " +
                "MERGE (com:Company {name: $company_name}) " +
                "MERGE (emp)-[:WORKS_FOR]->(com)",
                new {person_name = person["name"].As<string>(), company_name = companyName});
            return 1;
        }));
    }
}
public int addEmployees( final String companyName )
{
    try ( Session session = driver.session() )
    {
        int employees = 0;
        List<Record> persons = session.readTransaction( new TransactionWork<List<Record>>()
        {
            @Override
            public List<Record> execute( Transaction tx )
            {
                return matchPersonNodes( tx );
            }
        } );
        for ( final Record person : persons )
        {
            employees += session.writeTransaction( new TransactionWork<Integer>()
            {
                @Override
                public Integer execute( Transaction tx )
                {
                    tx.run( "MATCH (emp:Person {name: $person_name}) " +
                            "MERGE (com:Company {name: $company_name}) " +
                            "MERGE (emp)-[:WORKS_FOR]->(com)",
                            parameters( "person_name", person.get( "name" ).asString(), "company_name",
                                    companyName ) );
                    return 1;
                }
            } );
        }
        return employees;
    }
}

private static List<Record> matchPersonNodes( Transaction tx )
{
    return tx.run( "MATCH (a:Person) RETURN a.name AS name" ).list();
}
const session = driver.session();

const readTxPromise = session.readTransaction(tx => tx.run('MATCH (a:Person) RETURN a.name AS name'));

const addEmployeesPromise = readTxPromise.then(result => {
  const nameRecords = result.records;

  let writeTxsPromise = Promise.resolve();
  for (let i = 0; i < nameRecords.length; i++) {
    const name = nameRecords[i].get('name');

    writeTxsPromise = writeTxsPromise.then(() =>
      session.writeTransaction(tx =>
        tx.run(
          'MATCH (emp:Person {name: $person_name}) ' +
          'MERGE (com:Company {name: $company_name}) ' +
          'MERGE (emp)-[:WORKS_FOR]->(com)',
          {'person_name': name, 'company_name': companyName})));
  }

  return writeTxsPromise.then(() => nameRecords.length);
});

addEmployeesPromise.then(employeesCreated => {
  session.close();
  console.log('Created ' + employeesCreated + ' employees');
});
def add_employees(self, company_name):
    with self._driver.session() as session:
        employees = 0
        persons = session.read_transaction(self.match_person_nodes)

        for person in persons:
            employees += session.write_transaction(self.add_employee_to_company, person, company_name)

        return employees

@staticmethod
def add_employee_to_company(tx, person, company_name):
    tx.run("MATCH (emp:Person {name: $person_name}) "
           "MERGE (com:Company {name: $company_name}) "
           "MERGE (emp)-[:WORKS_FOR]->(com)",
           person_name=person["name"], company_name=company_name)
    return 1

@staticmethod
def match_person_nodes(tx):
    return list(tx.run("MATCH (a:Person) RETURN a.name AS name"))

4.4.3. Statement result summaries

Supplementary information such as query statistics, timings and server information can be obtained from the statement result summary. If this detail is accessed before the entire result has been consumed, the remainder of the result will be buffered.

See also the language-specific driver API documentation.