Working with Cypher values

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 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 can be potentially found in results although not all 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 driver maps 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 1. Map Neo4j types to native language types
Neo4j 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 type Go type

null

nil

List

[]interface{}

Map

map[string]interface{}

Boolean

bool

Integer

int64

Float

float64

String

string

ByteArray

[]byte

Date

neo4j.Date

Time

neo4j.OffsetTime

LocalTime

neo4j.LocalTime

DateTime

time.Time*

LocalDateTime

neo4j.LocalDateTime

Duration

neo4j.Duration

Point

neo4j.Point

Node

neo4j.Node

Relationship

neo4j.Relationship

Path

neo4j.Path

* When a time.Time value is sent/received through the driver and its Zone() returns a name of Offset, the value is stored with its offset value rather than its zone name.

Neo4j 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 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 2 type Python 3 type

null

None

None

List

list

list

Map

dict

dict

Boolean

bool

bool

Integer

int / long*

int

Float

float

float

String

unicode**

str

ByteArray

bytearray

bytearray

Date

neotime.Date

neotime.Date

Time

neotime.Time

neotime.Time

LocalTime

neotime.Time††

neotime.Time††

DateTime

neotime.DateTime

neotime.DateTime

LocalDateTime

neotime.DateTime††

neotime.DateTime††

Duration

neotime.Duration***

neotime.Duration***

Point

Point

Point

Node

Node

Node

Relationship

Relationship

Relationship

Path

Path

Path

* While Cypher uses 64-bit signed integers, int can only hold integers up to sys.maxint in Python 2; long is used for values above this.

** In Python 2, a str passed as a parameter will always be implicitly converted to unicode via UTF-8.

*** A timedelta passed as a parameter will always be implicitly converted to neotime.Duration.

† Where tzinfo is not None.

†† Where tzinfo is None.

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.

driver result stream
Figure 1. Result stream

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

The buffer

driver result buffer
Figure 2. 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.

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 2. 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();
        });
    }
}
func getPeople(driver neo4j.Driver) ([]string, error) {
	session, err := driver.Session(neo4j.AccessModeRead)
	if err != nil {
		return nil, err
	}
	defer session.Close()

	people, err := session.ReadTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		var list []string

		result, err := tx.Run("MATCH (a:Person) RETURN a.name ORDER BY a.name", nil)
		if err != nil {
			return nil, err
		}

		for result.Next() {
			list = append(list, result.Record().GetByIndex(0).(string))
		}

		if err = result.Err(); err != nil {
			return nil, err
		}

		return list, nil
	})
	if err != nil {
		return nil, err
	}

	return people.([]string), nil
}
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]

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 3. 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;
        }));
    }
}
func addPersonsAsEmployees(driver neo4j.Driver, companyName string) (int, error) {
	session, err := driver.Session(neo4j.AccessModeWrite)
	if err != nil {
		return 0, err
	}
	defer session.Close()

	persons, err := neo4j.Collect(session.ReadTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		return tx.Run("MATCH (a:Person) RETURN a.name AS name", nil)
	}))
	if err != nil {
		return 0, err
	}

	employees := 0
	for _, person := range persons {
		_, err = session.WriteTransaction(func(tx neo4j.Transaction) (interface{}, error) {
			return tx.Run("MATCH (emp:Person {name: $person_name}) "+
				"MERGE (com:Company {name: $company_name}) "+
				"MERGE (emp)-[:WORKS_FOR]->(com)", map[string]interface{}{"person_name": person.GetByIndex(0), "company_name": companyName})
		})
		if err != nil {
			return 0, err
		}

		employees++
	}

	return employees, nil
}
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"))

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.