Transaction Handling

This section describes the usage of transactions during the execution of an algorithm. When an algorithm procedure is called from Cypher, the procedure call is executed within the same transaction as the Cypher statement.

1. During graph projection

During graph projection, new transactions are used that do not inherit the transaction state of the Cypher transaction. This means that changes from the Cypher transaction state are not visible to the graph projection transactions.

For example, the following statement will only create an empty graph (assuming the MyLabel label was not already present in the Neo4j database):

CREATE (n:MyLabel) // the new node is part of Cypher transaction state
WITH *
CALL gds.graph.create('myGraph', 'MyLabel', '*')
YIELD nodeCount
RETURN nodeCount
Table 1. Results
nodeCount

0

The situation is the same when using an anonymous projection with an algorithm procedure:

CREATE (n:MyWccLabel) // the new node is part of Cypher transaction state
WITH *
CALL gds.wcc.stats({nodeProjection: 'MyWccLabel', relationshipProjection:'*'})
YIELD componentCount
RETURN componentCount
Table 2. Results
componentCount

0

2. During results writing

Results from algorithms (node properties, for example) are written to the graph in new transactions. The number of transactions used depends on the size of the results and the writeConcurrency configuration parameter (for more details, please refer to sections Write and Common Configuration parameters). These transactions are committed independently from the Cypher transaction. This means, if the Cypher transaction is terminated (either by the user or by the database system), already committed write transactions will not be rolled back.

2.1. Transaction writing examples

The code in this section is for illustrative purposes. The goal is to demonstrate correct usage of the GDS library write functionality with Cypher Shell and Java API.

2.1.1. Cypher Shell

Example for incorrect use.

:BEGIN

// Create the in-memory graph
CALL gds.graph.create.cypher(
  'test',
  'MATCH (n) WHERE n:Artist OR n:Genre RETURN id(n) AS id',
  'MATCH (a:Artist)<-[:RELEASED_BY]-(:Album)-[:HAS_GENRE]->(g:Genre)
   RETURN id(g) AS source, id(a) AS target, "IS_ASSOCIATED_WITH" AS type'
);

// Delete the old stuff
MATCH ()-[r:SIMILAR_TO]->() DELETE r;

// Run the algorithm
CALL gds.nodeSimilarity.write(
  'test', {
    writeRelationshipType: 'SIMILAR_TO',
    writeProperty: 'score'
  }
);

:COMMIT

The issue with the above statement is that all the queries run in the same transaction.

A correct handling of the above statement would be to run each statement in its own transaction, which is shown below. Notice the reordering of the statements, this ensures that the in-memory graph will have the most recent changes after the removal of the relationships.

First remove the unwanted relationships.

:BEGIN

MATCH ()-[r:SIMILAR_TO]->() DELETE r;

:COMMIT

Create the in-memory graph.

:BEGIN

CALL gds.graph.create.cypher(
  'test',
  'MATCH (n) WHERE n:Artist OR n:Genre RETURN id(n) AS id',
  'MATCH (a:Artist)<-[:RELEASED_BY]-(:Album)-[:HAS_GENRE]->(g:Genre)
   RETURN id(g) AS source, id(a) AS target, "IS_ASSOCIATED_WITH" AS type'
);

:COMMIT

Run the algorithm.

:BEGIN

CALL gds.nodeSimilarity.write(
  'test', {
    writeRelationshipType: 'SIMILAR_TO',
    writeProperty: 'score'
  }
);

:COMMIT

2.1.2. Java API

The same issue can be seen using the Java API, the examples are below.

Constants used throughout the examples below:
// Removes the in-memory graph (if exists) from the graph catalog
static final String CYPHER_DROP_GDS_GRAPH_IF_EXISTS =
    "CALL gds.graph.drop('test', false)";

// Creates the in-memory graph
static final String CYPHER_CREATE_GDS_GRAPH_ARTIST_GENRE =
    "CALL gds.graph.create.cypher(" +
    "    'test', " +
    "    'MATCH (n) WHERE n:Artist OR n:Genre RETURN id(n) AS id', " +
    "    'MATCH (a:Artist)<-[:RELEASED_BY]-(:Album)-[:HAS_GENRE]->(g:Genre) " +
    "       RETURN id(g) AS source, id(a) AS target, \"IS_ASSOCIATED_WITH\" AS type'" +
    ")";

// Runs NodeSimilarity in `write` mode over the in-memory graph
static final String CYPHER_WRITE_SIMILAR_TO =
    "CALL gds.nodeSimilarity.write(" +
    "   'test', {" +
    "       writeRelationshipType: 'SIMILAR_TO'," +
    "       writeProperty: 'score'"+
    "   }"
    ");";
Incorrect use:
try (var session = driver.session()) {
	var params = Map.<String, Object>of("graphName", "genre-related-to-artist");
	session.writeTransaction(tx -> {
		tx.run(CYPHER_DROP_GDS_GRAPH_IF_EXISTS, params).consume();
		tx.run(CYPHER_CREATE_GDS_GRAPH_ARTIST_GENRE, params).consume();
		tx.run("MATCH ()-[r:SIMILAR_TO]->() DELETE r").consume();
		return tx.run(CYPHER_WRITE_SIMILAR_TO, params).consume();
	});
}

Here we are facing the same issue with running everything in the same transaction. This can be written correctly by splitting each statement in its own transaction.

Correct handling of the statements:
try (var session = driver.session()) {

    // First run the remove statement
    session.writeTransaction(tx -> {
        return tx.run("MATCH ()-[r:SIMILAR_TO]->() DELETE r").consume();
    });

    // Create the in-memory graph
    var params = Map.<String, Object>of("graphName", "genre-related-to-artist");
	session.writeTransaction(tx -> {
	    tx.run(CYPHER_DROP_GDS_GRAPH_IF_EXISTS, params).consume();
	    return tx.run(CYPHER_CREATE_GDS_GRAPH_ARTIST_GENRE, params).consume();
    });

	// Run the algorithm
    session.writeTransaction(tx -> {
        return tx.run(CYPHER_WRITE_SIMILAR_TO, params).consume();
    });
}

3. Transaction termination

The Cypher transaction can be terminated by either the user or the database system. This will eventually terminate all transactions that have been opened during graph projection, algorithm execution, or results writing. It is not immediately visible and can take a moment for the transactions to recognize that the Cypher transaction has been terminated.