Further query mechanisms

Implicit (or auto-commit) transactions

This is the most basic and limited form with which to run a Cypher query. The driver will not automatically retry implicit transactions, as it does instead for queries run with ExecuteQuery() and with managed transactions. Implicit transactions should only be used when the other driver query interfaces do not fit the purpose, or for quick prototyping.

You run an implicit transaction with the method SessionWithContext.Run(). It returns a ResultWithContext object that needs to be processed accordingly.

session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
result, err := session.Run(
    ctx,
    "CREATE (p:Person {name: $name}) RETURN p",
    map[string]any{
        "name": "Lucia",
    })

An implicit transaction gets committed at the latest when the session is destroyed, or before another transaction is executed within the same session. Other than that, there is no clear guarantee on when exactly an implicit transaction will be committed during the lifetime of a session. To ensure an implicit transaction is committed, you can call the .Consume(ctx) method on its result.

Since the driver cannot figure out whether the query in a SessionWithContext.Run() call requires a read or write session with the database, it defaults to write. If your implicit transaction contains read queries only, there is a performance gain in making the driver aware by setting the session config AccessMode: neo4j.AccessModeRead when creating the session.

Implicit transactions are the only ones that can be used for CALL { …​ } IN TRANSACTIONS queries.

Import CSV files

The most common use case for using SessionWithContext.Run() is for importing large CSV files into the database with the LOAD CSV Cypher clause, and preventing timeout errors due to the size of the transaction.

Import CSV data into a Neo4j database
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
result, err := session.Run(
    ctx, `
    LOAD CSV FROM 'https://data.neo4j.com/bands/artists.csv' AS line
    CALL {
        WITH line
        MERGE (:Artist {name: line[1], age: toInteger(line[2])})
    } IN TRANSACTIONS OF 2 ROWS
    `, nil)
summary, _ := result.Consume(ctx)
fmt.Println("Query updated the database?",
    summary.Counters().ContainsUpdates())
While LOAD CSV can be a convenience, there is nothing wrong in deferring the parsing of the CSV file to your Go application and avoiding LOAD CSV. In fact, moving the parsing logic to the application can give you more control over the importing process.

For more information, see Cypher — Clauses — Load CSV.

Transaction configuration

You can exert further control on implicit transactions by providing configuration callbacks after the third argument in SessionWithContext.Run() calls. The configuration callbacks allow to specify a query timeout and to attach metadata to the transaction. For more information, see Transactions — Transaction configuration.

session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
people, err := session.Run(ctx,
    "MATCH (:Person) RETURN count(*) AS n",
    nil,
    neo4j.WithTxTimeout(5*time.Second),  // remember to import `time`
    neo4j.WithTxMetadata(map[string]any{"appName": "peopleTracker"}))

Dynamic values in property keys, relationship types, and labels

In general, you should not concatenate parameters directly into a query, but rather use query parameters. There can however be circumstances where your query structure prevents the usage of parameters in all its parts. In fact, although parameters can be used for literals and expressions as well as node and relationship ids, they cannot be used for the following constructs:

  • property keys, so MATCH (n) WHERE n.$param = 'something' is invalid;

  • relationship types, so MATCH (n)-[:$param]→(m) is invalid;

  • labels, so MATCH (n:$param) is invalid.

For those queries, you are forced to use string concatenation. To protect against Cypher injections, you should enclose the dynamic values in backticks and escape them yourself.Notice that Cypher processes Unicode, so take care of the Unicode literal \u0060 as well.

Manually escaping dynamic labels before concatenation
dangerousLabel := "Person\\u0060n"
// convert \u0060 to literal backtick and then escape backticks
// remember to import `strings`
escapedLabel := strings.ReplaceAll(dangerousLabel, "\\u0060", "`")
escapedLabel = strings.ReplaceAll(escapedLabel, "`", "``")

result, err := neo4j.ExecuteQuery(ctx, driver,
    "MATCH (p:`" + escapedLabel + "` WHERE p.name = $name) RETURN p.name",
    map[string]any{
        "name": "Alice",
    },
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))

Another workaround, which avoids string concatenation, is using the APOC procedure apoc.merge.node. It supports dynamic labels and property keys, but only for node merging.

Using apoc.merge.node to create a node with dynamic labels/property keys
propertyKey := "name"
label := "Person"

result, err := neo4j.ExecuteQuery(ctx, driver,
    "CALL apoc.merge.node($labels, $properties)",
    map[string]any{
        "labels": []string{label},
        "properties": map[string]any{propertyKey: "Alice"},
    },
    neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"))
If you are running Neo4j in Docker, APOC needs to be enabled when starting the container. See APOC - Installation - Docker.

Logging

The driver splits logging between driver events and Bolt events. To enable driver logging, use the Config.Log option when instantiating the driver:

// import "github.com/neo4j/neo4j-go-driver/v5/neo4j/config"

driver, err := neo4j.NewDriverWithContext(
    dbUri,
    neo4j.BasicAuth(dbUser, dbPassword, ""),
    func(conf *config.Config) {
        conf.Log = neo4j.ConsoleLogger(neo4j.DEBUG)
    })
Example of log output upon driver connection
2023-07-03 08:07:19.316   INFO  [pool 1] Created
2023-07-03 08:07:19.316   INFO  [router 1] Created {context: map[address:localhost:7687]}
2023-07-03 08:07:19.316   INFO  [driver 1] Created { target: localhost:7687 }
2023-07-03 08:07:19.316  DEBUG  [session 2] Created
2023-07-03 08:07:19.316   INFO  [router 1] Reading routing table from initial router: localhost:7687
2023-07-03 08:07:19.316  DEBUG  [pool 1] Trying to borrow connection from [localhost:7687]
2023-07-03 08:07:19.316   INFO  [pool 1] Connecting to localhost:7687
2023-07-03 08:07:19.320   INFO  [bolt5 bolt-58@localhost:7687] Connected
2023-07-03 08:07:19.320   INFO  [bolt5 bolt-58@localhost:7687] Retrieving routing table
2023-07-03 08:07:19.320  DEBUG  [pool 1] Returning connection to localhost:7687 {alive:true}
2023-07-03 08:07:19.320  DEBUG  [bolt5 bolt-58@localhost:7687] Resetting connection internal state
2023-07-03 08:07:19.320  DEBUG  [router 1] New routing table for 'neo4j', TTL 300
2023-07-03 08:07:19.320  DEBUG  [session 2] Resolved home database, uses db 'neo4j'
2023-07-03 08:07:19.320  DEBUG  [pool 1] Trying to borrow connection from [localhost:7687]
2023-07-03 08:07:19.321  DEBUG  [pool 1] Returning connection to localhost:7687 {alive:true}
2023-07-03 08:07:19.321  DEBUG  [bolt5 bolt-58@localhost:7687] Resetting connection internal state
2023-07-03 08:07:19.321  DEBUG  [router 1] Cleaning up
2023-07-03 08:07:19.321  DEBUG  [session 2] Closed

Bolt logging can be enabled either:

  • per-query, with the configuration callback neo4j.ExecuteQueryBoltLogger(). This applies to individual queries run using ExecuteQuery().

  • per-session, with the configuration option BoltLogger. This applies to all queries within a session.

Enable logging for a query run with ExecuteQuery
result, err := neo4j.ExecuteQuery(ctx, driver,
    "RETURN 42 AS n", nil, neo4j.EagerResultTransformer,
    neo4j.ExecuteQueryWithDatabase("neo4j"),
    neo4j.ExecuteQueryWithBoltLogger(neo4j.ConsoleBoltLogger()))
Enable logging for a session
session := driver.NewSession(ctx, neo4j.SessionConfig{
    DatabaseName: "neo4j",
    BoltLogger: neo4j.ConsoleBoltLogger(),
})
defer session.Close(ctx)
session.Run(ctx, "RETURN 42 AS n", nil)
Example of Bolt logging output
2023-07-03 07:57:09.929   BOLT  [bolt-53@localhost:7687] C: BEGIN {"db":"neo4j"}
2023-07-03 07:57:09.930   BOLT  [bolt-53@localhost:7687] S: SUCCESS {}
2023-07-03 07:57:09.930   BOLT  [bolt-53@localhost:7687] C: RUN "RETURN 42 AS n" null null
2023-07-03 07:57:09.930   BOLT  [bolt-53@localhost:7687] C: PULL {"n":1000}
2023-07-03 07:57:09.936   BOLT  [bolt-53@localhost:7687] S: SUCCESS {"fields":["n"],"t_first":5}
2023-07-03 07:57:09.937   BOLT  [bolt-53@localhost:7687] S: RECORD [42]
2023-07-03 07:57:09.937   BOLT  [bolt-53@localhost:7687] S: SUCCESS {"t_first":1,"db":"neo4j"}
2023-07-03 07:57:09.937   BOLT  [bolt-53@localhost:7687] C: COMMIT
2023-07-03 07:57:09.938   BOLT  [bolt-53@localhost:7687] S: SUCCESS {"bookmark":"FB:kcwQhRyDJPONRxudy+QyzPSuSAaQ"}

Glossary

LTS

A Long Term Support release is one guaranteed to be supported for a number of years. Neo4j 4.4 is LTS, and Neo4j 5 will also have an LTS version.

Aura

Aura is Neo4j’s fully managed cloud service. It comes with both free and paid plans.

Cypher

Cypher is Neo4j’s graph query language that lets you retrieve data from the database. It is like SQL, but for graphs.

APOC

Awesome Procedures On Cypher (APOC) is a library of (many) functions that can not be easily expressed in Cypher itself.

Bolt

Bolt is the protocol used for interaction between Neo4j instances and drivers. It listens on port 7687 by default.

ACID

Atomicity, Consistency, Isolation, Durability (ACID) are properties guaranteeing that database transactions are processed reliably. An ACID-compliant DBMS ensures that the data in the database remains accurate and consistent despite failures.

eventual consistency

A database is eventually consistent if it provides the guarantee that all cluster members will, at some point in time, store the latest version of the data.

causal consistency

A database is causally consistent if read and write queries are seen by every member of the cluster in the same order. This is stronger than eventual consistency.

NULL

The null marker is not a type but a placeholder for absence of value. For more information, see Cypher → Working with null.

transaction

A transaction is a unit of work that is either committed in its entirety or rolled back on failure. An example is a bank transfer: it involves multiple steps, but they must all succeed or be reverted, to avoid money being subtracted from one account but not added to the other.

backpressure

Backpressure is a force opposing the flow of data. It ensures that the client is not being overwhelmed by data faster than it can handle.

transaction function

A transaction function is a callback executed by an ExecuteRead or ExecuteWrite call. The driver automatically re-executes the callback in case of server failure.

DriverWithContext

A DriverWithContext object holds the details required to establish connections with a Neo4j database.