The Neo4j Type System

At this point, we should take a look at the Cypher type system. As Neo4j is written in Java (the j in Neo4j stands for Java after all), there are some discrepancies between the types stored in the Neo4j database and native Go types.

Some values like strings, floats, booleans, and nulls map directly to Go types, but more complex types need special handling.

Go Types to Neo4j Types
Go Type Neo4j Cypher Type Notes

null

nil

List

[]interface{} or []any

Map

map[string]interface{} or map[string]any

Boolean

bool

Integer

int64

Float

float64

String

string

ByteArray

[]byte

Date

neo4j.Date

See Temporal Data Types

Time

neo4j.OffsetTime

See Temporal Data Types

LocalTime

neo4j.LocalTime

See Temporal Data Types

DateTime

time.Time*

See Temporal Data Types

LocalDateTime

neo4j.LocalDateTime

See Temporal Data Types

Duration

neo4j.Duration

See Durations

Point

neo4j.Point

See Spatial Data Types

Node

neo4j.Node

See Nodes & Relationships

Relationship

neo4j.Relationship

See Nodes & Relationships

Path

neo4j.Path

See Nodes & Relationships

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

Let’s take a look at some of these types in more detail.

Nodes & Relationships

Working with Nodes and Relationships are very similar. Both have an .Id property which represents the internal Neo4j ID as an int64 and a .Props property which holds a map of properties.

As an example, let’s take the following code snippet:

go
Return Nodes and Relationships
result, err := transaction.Run(
	"MATCH path = (p:Person)-[r:ACTED_IN]->(m:Movie {title: $title}) RETURN p, r, m, path",
	map[string]interface{}{"title": "Arthur"})

if err != nil {
	return nil, err
}

The query will return one record for each :Person and :Movie node with an :ACTED_IN relationship between them.

Extracting Values

As mentioned in the previous lesson, you can use the alias as defined in the RETURN portion of the Cypher statement:

go
Accessing a value by Alias
for result.NextRecord(&record) {
	// Get the value of `m`
	movie, _ := record.Get("m")
	movieByAlias := movie.(neo4j.Node) // m, a :Movie node as type neo4j.Node()
}

Or alternatively, you can either retrieve a value by its index in the Values property:

go
Accessing a value by Index
// Check keys to find the index
fmt.Println(result.Keys()) // ['p', 'm', 'a', 'path']

for result.NextRecord(&record) {
	// Get the first value, in this case `p`
	personByIndex := record.Values[0].(neo4j.Node) // p, a :Person node as type neo4j.Node()
}

When casting an item, you can use the second returned value to check that the value has been successfully cast as the requested type.

go
Casting Values
// Get a Relationship and check assertion
actedInRelationship, _ := record.Get("r")
actedIn, ok := actedInRelationship.(neo4j.Relationship)

if !ok {
	// Value is not a relationship
	return nil, fmt.Errorf("expected a neo4j.Relationship, got: %v", reflect.TypeOf(result))
}

Working with Nodes

Node struct Definition
go
type Node struct {
	// Deprecated: Id is deprecated and will be removed in 6.0. Use ElementId instead.
	Id        int64          // Id of this Node.
	ElementId string         // ElementId of this Node.
	Labels    []string       // Labels attached to this Node.
	Props     map[string]any // Properties of this Node.
}

This definition has been taken from the source code for the Neo4j Go Driver

neo4j.Node is a struct that allows you to access:

  1. .Id - The Internal ID of the Node as an int64
    eg 8491

  2. .Labels - An array of strings representing the labels attributed to the Node
    eg. ['Person, 'Actor']

  3. .Props - A map of properties assigned to the Node
    eg. {name: 'Tom Hanks', tmdbId: '31' }

go
Working with Node objects
personNode, _ := record.Get("p")
person := personNode.(neo4j.Node)

fmt.Println(person.Id)     // (1)
fmt.Println(person.Labels) // (2)
fmt.Println(person.Props)  // (3)

Internal IDs

Internal IDs refer to the position in the Neo4j store files where the record is held. These numbers can be re-used, a best practice is to always look up a node by an indexed property rather than relying on an internal ID.

Working with Relationships

Relationship struct Definition
go
type Relationship struct {
	// Deprecated: Id is deprecated and will be removed in 6.0. Use ElementId instead.
	Id        int64  // Id of this Relationship.
	ElementId string // ElementId of this Relationship.
	// Deprecated: StartId is deprecated and will be removed in 6.0. Use StartElementId instead.
	StartId        int64  // Id of the start Node of this Relationship.
	StartElementId string // ElementId of the start Node of this Relationship.
	// Deprecated: EndId is deprecated and will be removed in 6.0. Use EndElementId instead.
	EndId        int64          // Id of the end Node of this Relationship.
	EndElementId string         // ElementId of the end Node of this Relationship.
	Type         string         // Type of this Relationship.
	Props        map[string]any // Properties of this Relationship.
}

This definition has been taken from the source code for the Neo4j Go Driver

neo4j.Relationship is a struct that allows you to access:

  1. .Id - The internal ID of the relationship as an int64
    eg. 9876

  2. .Type - The type of the relationship
    eg. ACTED_IN

  3. .Props - A map of properties assigned to the Relationship
    eg. {role: 'Woody'}

  4. .StartId - The internal ID for the node at the start of the relationship

  5. .EndId - The internal ID for the node at the end of the relationship

Working with Paths

Path struct Definition
go
type Path struct {
	Nodes         []Node // All the nodes in the path.
	Relationships []Relationship
}

This definition has been taken from the source code for the Neo4j Go Driver

If you return a path of nodes and relationships, they will be returned as an instance of a neo4j.Path.

go
Working with Path Objects
returnedPath, _ := record.Get("path")
path := returnedPath.(neo4j.Path)

nodes := path.Nodes
relationships := path.Relationships

for node := range nodes {
	fmt.Println(node) // neo4j.Node
}
for relationship := range relationships {
	fmt.Println(relationship) // neo4j.Relationship
}

Path Segments

A path is split into segments representing each relationship in the path. For example, say we have a path of (p:Person)-[:ACTED_IN]→(m:Movie)-[:IN_GENRE]→(g:Genre), there would be two relationships.

  1. (p:Person)-[:ACTED_IN]→(m:Movie)

  2. (m:Movie)-[:IN_GENRE]→(g:Genre)

You can access the relationships within the path through the .Relationships property, and the nodes in the path can be accessed through the .Nodes property.

go
Iterating over Segments
returnedPath, _ := record.Get("path")
path := returnedPath.(neo4j.Path)

nodes := path.Nodes
relationships := path.Relationships

for node := range nodes {
	fmt.Println(node) // neo4j.Node
}
for relationship := range relationships {
	fmt.Println(relationship) // neo4j.Relationship
}

Temporal Data Types

Temporal data types are extensions of Go’s time.Time type.

You can access the individual parts of the Time struct using the appropriate method.

go
Working with Times
timeProperty, _ := record.Get("time")
time := timeProperty.(neo4j.Time).Time()

fmt.Println(time.Year())  // 2022
fmt.Println(time.Month()) // January
fmt.Println(time.Day())   // 4

// For Time, DateTime,
fmt.Println(time.Day()) // 4

Read more time support from gobyexample.com

Durations

The neo4j.Duration type provides properties for accessing the Neo4j duration type. A Neo4j duration type contains the following data:

  • Months - an int64

  • Days - an int64

  • Seconds - an int64

  • Nanos - an int

go
Working with Durations
// duration('P1Y2M3DT12H34M56S')
// 1 year, 2 months, 3 days; 12 hours, 34 minutes, 56 seconds
durationProperty, _ := record.Get("duration")
duration := durationProperty.(neo4j.Duration)

fmt.Println(duration.Months)  // 14 (1 year, 2 months = 14 months)
fmt.Println(duration.Days)    // 3
fmt.Println(duration.Seconds) // 45296
fmt.Println(duration.Nanos)   // 987600000

Spatial Data Types

Cypher has built-in support for handling spatial values (Points), and the underlying database supports storing these point values as properties on nodes and relationships.

Points

Points can be stored in Neo4j as 2D points (x and y, or latitude and longitude) or 3D points (x, y and z, or latitude, longitude and height).

The Neo4j Go Driver provides two types to represent these types, Point2D and Point3D. Both of these types have .X and .Y properties along with a .SpatialRefId which is used to represent the ID of the coordinate reference system.

SpatialRefId Description Cypher Example

7203

Point2D in the cartesian space.

point({x: 10, y: 20})

4326

Point2D in the WGS84 space.

point({latitude: 10, longitude: 20})

9157

Point3D in the cartesian space.

point({x: 10, y: 20, z: 30})

4979

Point3D in the WGS84 space.

point({longitude:20, latitude:10, height:30})

Point2D

A Point2D struct represents a two-dimensional in Cartesian space or in the WGS84 space. The .SpatialRefId property indicates the type of the coordinate and the .X and .Y property represent the location.

When a type is created with latitude and longitude values, the values are saved as Y and X respectively.

go
Working with Point2D Values
wgs842DResult, _ := record.Get("wgs842D")
wgs842D := wgs842DResult.(neo4j.Point2D)
// {SpatialRefId: xxxx, X: 10, y: 20}

cartesian2DResult, _ := record.Get("cartesian2D")
cartesian2D := cartesian2DResult.(neo4j.Point2D)
// {SpatialRefId: xxxx, X: 10, y: 20}

Point3D

The Point3D struct is similar to the Point2D type, with the addition of a .Z property.

go
Working with Point3D Values
wgs842DResult, _ := record.Get("wgs842D")
wgs842D := wgs842DResult.(neo4j.Point2D)
// {SpatialRefId: xxxx, X: 10, y: 20}

cartesian2DResult, _ := record.Get("cartesian2D")
cartesian2D := cartesian2DResult.(neo4j.Point2D)
// {SpatialRefId: xxxx, X: 10, y: 20}

Distance

When using the point.distance function in Cypher, the distance calculated between two points is returned as a float.

cypher
WITH point({x: 1, y:1}) AS one,
     point({x: 10, y: 10}) AS two

RETURN point.distance(one, two) // 12.727922061357855

For more information on Spatial types, see the Cypher Manual.

Check Your Understanding

1. Accessing Node Properties

Which of the following options is a valid method for accessing the name property on the node object node?

  • node["name"]

  • node.Props.name

  • node.Props["name"]

  • node.Properties["name"]

  • node.Get("name")

  • PropertyOf(node, "name")

Hint

Node and relationship properties are stored in the Props member.

Solution

The answer is node.Props["name"].

2. Relationship Types

What method would you use to access the type of the relationship actedIn?

  • actedIn["type"]

  • actedIn.Labels

  • actedIn.Type

  • neo4j.GetRelationshipType(actedIn)

Hint

You would use this method to access the type of the relationship.

Solution

The answer is actedIn.Type.

Lesson Summary

In this lesson you have learned how to handle data returned by a Cypher statement.

As we progress through this module, you will use the knowledge gained so far to read data from, and write data back to the database. In the next lesson you will learn how to add type safety to your Go project.