User-defined procedures
A user-defined procedure is a mechanism that enables you to extend Neo4j by writing customized code, which can be invoked directly from Cypher. Procedures can take arguments, perform operations on the database, and return results. For a comparison between user-defined procedures, functions, and aggregation functions see Neo4j customized code.
User-defined procedures requiring execution on the system database need to include the annotation |
Call a procedure
To call a user-defined procedure, use a Cypher CALL
clause.
The procedure name must be fully qualified, so a procedure named findDenseNodes
defined in the package org.neo4j.examples
could be called using:
CALL org.neo4j.examples.findDenseNodes(1000)
CALL
may be the only clause within a Cypher statement or may be combined with other clauses.
Arguments can be supplied directly within the query or taken from the associated parameter set.
For full details, see the documentation in Cypher Manual → CALL
procedure.
Create a procedure
Make sure you have read and followed the preparatory setup instructions in Setting up a plugin project.
The example discussed below is available as a repository on GitHub. To get started quickly you can fork the repository and work with the code as you follow along in the guide below. |
First, decide what the procedure should do, then write a test that proves that it does it right. Finally, write a procedure that passes the test.
Integration tests
The test dependencies include Neo4j Harness and JUnit. These can be used to write integration tests for procedures. The tests should start a Neo4j instance, load the procedure, and execute queries against it.
package example;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
import org.neo4j.driver.Value;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;
import static org.assertj.core.api.Assertions.assertThat;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class GetRelationshipTypesTests {
private Driver driver;
private Neo4j embeddedDatabaseServer;
@BeforeAll
void initializeNeo4j() {
this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.withProcedure(GetRelationshipTypes.class)
.build();
this.driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI());
}
@AfterAll
void closeDriver(){
this.driver.close();
this.embeddedDatabaseServer.close();
}
@AfterEach
void cleanDb(){
try(Session session = driver.session()) {
session.run("MATCH (n) DETACH DELETE n");
}
}
/**
* We should be getting the correct values when there is only one type in each direction
*/
@Test
public void shouldReturnTheTypesWhenThereIsOneEachWay() {
final String expectedIncoming = "INCOMING";
final String expectedOutgoing = "OUTGOING";
// In a try-block, to make sure we close the session after the test
try(Session session = driver.session()) {
//Create our data in the database.
session.run(String.format("CREATE (:Person)-[:%s]->(:Movie {id:1})-[:%s]->(:Person)", expectedIncoming, expectedOutgoing));
//Execute our procedure against it.
Record record = session.run("MATCH (u:Movie {id:1}) CALL example.getRelationshipTypes(u) YIELD outgoing, incoming RETURN outgoing, incoming").single();
//Get the incoming / outgoing relationships from the result
assertThat(record.get("incoming").asList(Value::asString)).containsOnly(expectedIncoming);
assertThat(record.get("outgoing").asList(Value::asString)).containsOnly(expectedOutgoing);
}
}
}
The previous example uses JUnit 5, which requires the use of |
Define a procedure
With the test in place, write a procedure that fulfills the expectations of the test. The full example is available in the Neo4j Procedure Template repository.
Particular things to note:
-
All procedures are annotated
@Procedure
. -
The procedure annotation can take three optional arguments:
name
,mode
, andeager
.-
name
is used to specify a different name for the procedure than the default generated, which isclass.path.nameOfMethod
. Ifmode
is specified,name
must be specified as well. -
mode
is used to declare the types of interactions that the procedure performs. A procedure fails if it attempts to execute database operations that violate its mode. The defaultmode
isREAD
. The following modes are available:-
READ
— This procedure only performs read operations against the graph. -
WRITE
— This procedure performs read and write operations against the graph. -
SCHEMA
— This procedure performs operations against the schema, i.e. create and drop indexes and constraints. A procedure with this mode can read graph data, but not write. -
DBMS
— This procedure performs system operations such as user management and query management. A procedure with this mode is not able to read or write graph data.
-
-
eager
is a boolean setting defaulting tofalse
. If it is set totrue
, the Cypher planner plans an extraeager
operation before and after calling the procedure. This is useful in cases where the procedure makes changes to the database in a way that could interact with the operations preceding or following the procedure. For example:MATCH (n) WHERE n.key = 'value' WITH n CALL deleteNeighbours(n, 'FOLLOWS')
This query can delete some of the nodes that are matched by the Cypher query, and the
n.key
lookup will fail. Marking this procedure aseager
prevents this from causing an error in Cypher code. However, it is still possible for the procedure to interfere with itself by trying to read entities it has previously deleted. It is the responsibility of the procedure author to handle that case.
-
-
The context of the procedure, which is the same as each resource that the procedure wants to use, is annotated
@Context
.
The correct way to signal an error from within a procedure is to throw |
Injectable resources
When writing procedures, some resources can be injected into the procedure from the database.
To inject these, use the @Context
annotation.
The classes that can be injected are:
-
Log
-
TerminationGuard
-
GraphDatabaseService
-
Transaction
All of the above classes are considered safe and future-proof and do not compromise the security of the database.
Several unsupported (restricted) classes can also be injected and can be changed with little or no notice.
Procedures written to use these restricted APIs are not loaded by default, and you need to use the dbms.security.procedures.unrestricted
to load unsafe procedures.
Read more about this config setting in Operations Manual → Securing extensions.