diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index 16c8b51..7af6e5b 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -5,9 +5,7 @@ ** xref:extending-neo4j/customized-code.adoc[] ** xref:extending-neo4j/project-setup.adoc[] ** xref:extending-neo4j/values-and-types.adoc[] -** xref:extending-neo4j/procedures.adoc[] -** xref:extending-neo4j/functions.adoc[] -** xref:extending-neo4j/aggregation-functions.adoc[] +** xref:extending-neo4j/user-defined-procedures-functions.adoc[] ** xref:extending-neo4j/security-plugins.adoc[] ** xref:extending-neo4j/full-text-analyzer-provider.adoc[] ** xref:extending-neo4j/unmanaged-extensions.adoc[] diff --git a/modules/ROOT/pages/extending-neo4j/aggregation-functions.adoc b/modules/ROOT/pages/extending-neo4j/aggregation-functions.adoc deleted file mode 100644 index 26d03af..0000000 --- a/modules/ROOT/pages/extending-neo4j/aggregation-functions.adoc +++ /dev/null @@ -1,190 +0,0 @@ -:description: How to write, test, and deploy a user-defined aggregation function for Neo4j. - -:org-neo4j-procedure-UserAggregationFunction: {neo4j-javadocs-base-uri}/org/neo4j/procedure/UserAggregationFunction.html - - -[[extending-neo4j-aggregation-functions]] -= User-defined aggregation functions - -_User-defined aggregation functions_ are functions that aggregate data and return a single result. -For a comparison between user-defined procedures, functions, and aggregation functions, see xref:extending-neo4j/customized-code.adoc[]. - - -[[call-user-defined-aggregation-function]] -== Call an aggregation function - -User-defined aggregation functions are called in the same way as any other Cypher aggregation function. -The function name must be fully qualified, so a function named `longestString` defined in the package `org.neo4j.examples` could be called using: - -[source, cypher, role="noplay"] ----- -MATCH (p: Person) WHERE p.age = 36 -RETURN org.neo4j.examples.longestString(p.name) ----- - - -[[writing-user-defined-aggregation-function]] -== Writing a user-defined aggregation function - -User-defined aggregation functions are annotated with `@UserAggregationFunction`. -The annotated function must return an instance of an aggregator class. -An aggregator class contains one method annotated with `@UserAggregationUpdate` and one method annotated with `@UserAggregationResult`. -The method annotated with `@UserAggregationUpdate` will be called multiple times and enables the class to aggregate data. -When the aggregation is done, the method annotated with `@UserAggregationResult` will be called once and the result of the aggregation will be returned. - -Particular things to note: - -* All functions are annotated with `@UserAggregationFunction`. -* The aggregation function name must be namespaced and is not allowed in reserved namespaces. -* If a user-defined aggregation function is registered with the same name as a built-in function in a deprecated namespace, the built-in function is shadowed. - -See xref:extending-neo4j/values-and-types.adoc[] for details on values and types. - -For more details, see the Neo4j Javadocs for link:{org-neo4j-procedure-UserAggregationFunction}[`org.neo4j.procedure.UserAggregationFunction`^]. - -[NOTE] -==== -The correct way to signal an error from within an aggregation function is to throw `RuntimeException`. -==== - -[source, java] ----- -package example; - -import org.neo4j.procedure.Description; -import org.neo4j.procedure.Name; -import org.neo4j.procedure.UserAggregationFunction; -import org.neo4j.procedure.UserAggregationResult; -import org.neo4j.procedure.UserAggregationUpdate; - -public class LongestString -{ - @UserAggregationFunction - @Description( "org.neo4j.function.example.longestString(string) - aggregates the longest string found" ) - public LongStringAggregator longestString() - { - return new LongStringAggregator(); - } - - public static class LongStringAggregator - { - private int longest; - private String longestString; - - @UserAggregationUpdate - public void findLongest( - @Name( "string" ) String string ) - { - if ( string != null && string.length() > longest) - { - longest = string.length(); - longestString = string; - } - } - - @UserAggregationResult - public String result() - { - return longestString; - } - } -} ----- - - -== Integration tests - -Tests for user-defined aggregation functions are created in the same way as those for normal user-defined functions. - - -.A template for testing a user-defined aggregation function that finds the longest string. -[source, java] ----- -package example; - -import org.junit.Rule; -import org.junit.Test; -import org.neo4j.driver.v1.*; -import org.neo4j.harness.junit.Neo4jRule; - -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertThat; - -public class LongestStringTest -{ - // This rule starts a Neo4j instance - @Rule - public Neo4jRule neo4j = new Neo4jRule() - - // This is the function to test - .withAggregationFunction( LongestString.class ); - - @Test - public void shouldAllowIndexingAndFindingANode() throws Throwable - { - // This is in a try-block, to make sure you close the driver after the test - try( Driver driver = GraphDatabase.driver( neo4j.boltURI() , Config.build().withEncryptionLevel( Config.EncryptionLevel.NONE ).toConfig() ) ) - { - // Given - Session session = driver.session(); - - // When - String result = session.run( "UNWIND ["abc", "abcd", "ab"] AS string RETURN example.longestString(string) AS result").single().get("result").asString(); - - // Then - assertThat( result, equalTo( "abcd" ) ); - } - } -} ----- - - -[[reserved-function-namespaces]] -== Reserved function namespaces - - -List of reserved function namespaces and names:: - -* * -* date -* date.realtime -* date.statement -* date.transaction -* date.truncate -* datetime -* datetime.fromepoch -* datetime.fromepochmillis -* datetime.realtime -* datetime.statement -* datetime.transaction -* datetime.truncate -* db.nameFromElementId -* duration -* duration.between -* duration.inDays -* duration.inMonths -* duration.inSeconds -* graph.byElementId -* graph.byName -* graph.names -* graph.propertiesByName -* localdatetime -* localdatetime.realtime -* localdatetime.statement -* localdatetime.transaction -* localdatetime.truncate -* localtime -* localtime.realtime -* localtime.statement -* localtime.transaction -* localtime.truncate -* point.distance -* point.withinBBox -* time -* time.realtime -* time.statement -* time.transaction -* time.truncate -* vector.similarity.cosine -* vector.similarity.euclidean - diff --git a/modules/ROOT/pages/extending-neo4j/customized-code.adoc b/modules/ROOT/pages/extending-neo4j/customized-code.adoc index 184dccf..e5b2efe 100644 --- a/modules/ROOT/pages/extending-neo4j/customized-code.adoc +++ b/modules/ROOT/pages/extending-neo4j/customized-code.adoc @@ -4,8 +4,8 @@ [[neo4j-customized-code]] = Neo4j customized code -_User-defined procedures_ and _user-defined functions_ are mechanisms that enable you to extend Neo4j by writing customized code, which can be invoked directly from Cypher. -This is the preferred means for extending Neo4j. +Neo4j allows you to extend its functionality by writing your own code using _user-defined_ procedures and functions. +They can be invoked directly from Cypher and are the preferred way of extending Neo4j. Examples of use cases for procedures and functions are: @@ -14,10 +14,19 @@ Examples of use cases for procedures and functions are: * To perform graph-global operations, such as counting connected components or finding dense nodes. * To express a procedural operation that is difficult to express declaratively with Cypher. -Procedures and functions are written in Java and compiled into JAR files. -They are deployed to the database by dropping that JAR file into the _plugins_ directory on each standalone or clustered server. +== Writing user-defined procedures and functions + +Procedures and functions should be implemented in a language within the JVM ecosystem and packaged into a JAR file. +See xref:extending-neo4j/user-defined-procedures-functions.adoc#create-user-defined-procedure-or-function[Create a procedure or function] for details. + +== Deploying user-defined procedures and functions + +Once compiled, these JAR files are deployed to the _NEO4J_HOME/plugins_ directory on each standalone or clustered server. For the location of the _plugins_ directory, refer to link:{neo4j-docs-base-uri}/operations-manual/{page-version}/configuration/file-locations[Operations Manual -> Default file locations]. -The database must be restarted on each server to pick up new procedures and functions. + + +[[comparison-of-procedures-and-functions]] +== Comparison of procedures and functions Procedures and functions can take arguments and return results. In addition, procedures can perform write operations on the database. @@ -51,6 +60,85 @@ In addition, procedures can perform write operations on the database. |=== +[[memory-resource-tracking]] +== Memory resource tracking + +[NOTE] +==== +The memory resource tracking API for the procedure framework is available for preview. +Future versions of Neo4j might contain breaking changes to this API. +==== + +If your procedure or function allocates significant amounts of heap memory, you can register allocations to count towards the configured transaction limits, see link:{neo4j-docs-base-uri}/operations-manual/{page-version}/performance/memory-configuration/#memory-configuration-limit-transaction-memory[Operations Manual -> Limit transaction memory usage] for more information. +This allows you to avoid `OutOfMemory` errors and terminate queries instead of causing uncontrollable memory consumption. +Memory allocations also show up in query profiles. + +To do this you need to inject `org.neo4j.procedure.memory.ProcedureMemory` as a field in your procedure/function class. +`ProcedureMemory` has various methods to allow you to register allocations. +For example (see javadocs for a full reference): + +* `ProcedureMemoryTracker newTracker()` creates a new memory resource tracker that is bound to the current transaction. +* `HeapEstimator heapEstimator()` estimates the heap size of classes and instances. +* `HeapTrackingCollectionFactory collections()` lets you create collections that have built-in memory tracking of their internal structure. + +It's usually difficult and time-consuming to implement memory resource tracking. +These are a few considerations and caveats that are worth keeping in mind: + +- Limit the scope of the memory management. + Focus only on parts that can grow significantly in memory and ignore minor underestimation. +- Beware of overestimation by registering allocations of the same instance multiple times. + You can add reference counting or other mechanisms to avoid overestimation if that is a concern. +- It's common not to know the size of an instance before it has been allocated, which may lead you to register allocations after they have already been made. + The memory tracker implementation tries to prevent this by always pre-registering a certain amount of memory in the internal memory pools. +- It's cumbersome in Java to know when an instance has been garbage-collected. + Typically, you register the release of memory at the point when it's possible for that memory to be garbage-collected. + To account for this, memory trackers may internally choose not to register the release of memory instantaneously. +- Testing memory resource tracking can be difficult. + One approach is to use a third-party library, like JAMM (Java Agent for Memory Measurements), and assert that the estimates are close enough for some given input. + + +.A basic example of memory resource tracking in user defined procedures +[source, java] +---- +package org.example; + +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; +import org.neo4j.procedure.memory.ProcedureMemory; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class MyProcedures { + + @Context + public ProcedureMemory memory; + + record Output(Long value) {} + + @Procedure("org.example.memoryHungryRange") + public Stream memoryHungryRange(@Name("size") int size) { + final var tracker = memory.newTracker(); + + // Register the allocation of the long array below + tracker.allocateHeap(memory.heapEstimator().sizeOfLongArray(size)); + // The actual allocation + final var result = new long[size]; + + for (int i = 0; i < size; i++) result[i] = i; + + return Arrays.stream(result) + .mapToObj(Output::new) + // Release all registered allocations when the stream is closed + .onClose(tracker::close); + } +} + +---- + +== Built-in procedures and functions + Neo4j also comes bundled with a number of _built-in_ procedures and functions. The available built-in procedures vary depending on edition and mode, as described in link:{neo4j-docs-base-uri}/operations-manual/{page-version}/reference/procedures[Operations Manual -> Procedures]. diff --git a/modules/ROOT/pages/extending-neo4j/functions.adoc b/modules/ROOT/pages/extending-neo4j/functions.adoc deleted file mode 100644 index 8a0f003..0000000 --- a/modules/ROOT/pages/extending-neo4j/functions.adoc +++ /dev/null @@ -1,177 +0,0 @@ -:description: How to write, test and deploy a user-defined function for Neo4j. - -:org-neo4j-procedure-UserFunction: {neo4j-javadocs-base-uri}/org/neo4j/procedure/UserFunction.html - - -[[extending-neo4j-functions]] -= User-defined functions - -_User-defined functions_ are simpler forms of procedures that return a single value and are read-only. -Although they are less powerful in capability, they are often easier to use and more efficient than procedures for many common tasks. -For a comparison between user-defined procedures, functions, and aggregation functions see xref:extending-neo4j/customized-code.adoc[]. - - -[[call-udf]] -== Call a user-defined function - -User-defined functions are called in the same way as any other Cypher function. -The function name must be fully qualified, so a function named `join` defined in the package `org.neo4j.examples` could be called using: - -[source, cypher, role="noplay"] ----- -MATCH (p: Person) WHERE p.age = 36 -RETURN org.neo4j.examples.join(collect(p.names)) ----- - - -[[writing-udf]] -== Create a function - -User-defined functions are created similarly to how procedures are created. -But unlike procedures, they are annotated with `@UserFunction` and return a single value instead of a stream of values. - -Particular things to note: - -* All functions are annotated with `@UserFunction`. -* The function name must be namespaced and is not allowed in reserved namespaces. -* If a function is registered with the same name as a built-in function in a deprecated namespace, the built-in function is shadowed. - -See xref:extending-neo4j/values-and-types.adoc[] for details on values and types. - -For more details, see the link:{org-neo4j-procedure-UserFunction}[Neo4j Javadocs for `org.neo4j.procedure.UserFunction`^]. - -[NOTE] -==== -The correct way to signal an error from within a function is to throw `RuntimeException`. -==== - -[source, java] ----- -package example; - -import java.util.List; - -import org.neo4j.procedure.Description; -import org.neo4j.procedure.Name; -import org.neo4j.procedure.UserFunction; - -public class Join -{ - @UserFunction - @Description("example.join(['s1','s2',...], delimiter) - join the given strings with the given delimiter.") - public String join( - @Name("strings") List strings, - @Name(value = "delimiter", defaultValue = ",") String delimiter) { - if (strings == null || delimiter == null) { - return null; - } - return String.join(delimiter, strings); - } -} ----- - - -== Integration tests - -Tests for user-defined functions are created in the same way as those for procedures. - -.A template for testing a user-defined function that joins a list of strings. -[source, java] ----- -package example; - -import org.junit.jupiter.api.AfterAll; -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.Session; -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 JoinTest { - - private Neo4j embeddedDatabaseServer; - - @BeforeAll - void initializeNeo4j() { - this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() - .withDisabledServer() - .withFunction(Join.class) - .build(); - } - - @AfterAll - void closeNeo4j() { - this.embeddedDatabaseServer.close(); - } - - @Test - void joinsStrings() { - // This is in a try-block, to make sure we close the driver after the test - try(Driver driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI()); - Session session = driver.session()) { - - // When - String result = session.run( "RETURN example.join(['Hello', 'World']) AS result").single().get("result").asString(); - - // Then - assertThat( result).isEqualTo(( "Hello,World" )); - } - } -} ----- - - -[[reserved-function-namespaces]] -== Reserved function namespaces - -List of reserved function namespaces:: - -* * -* date -* date.realtime -* date.statement -* date.transaction -* date.truncate -* datetime -* datetime.fromepoch -* datetime.fromepochmillis -* datetime.realtime -* datetime.statement -* datetime.transaction -* datetime.truncate -* db.nameFromElementId -* duration -* duration.between -* duration.inDays -* duration.inMonths -* duration.inSeconds -* graph.byElementId -* graph.byName -* graph.names -* graph.propertiesByName -* localdatetime -* localdatetime.realtime -* localdatetime.statement -* localdatetime.transaction -* localdatetime.truncate -* localtime -* localtime.realtime -* localtime.statement -* localtime.transaction -* localtime.truncate -* point.distance -* point.withinBBox -* time -* time.realtime -* time.statement -* time.transaction -* time.truncate -* vector.similarity.cosine -* vector.similarity.euclidean - diff --git a/modules/ROOT/pages/extending-neo4j/procedures.adoc b/modules/ROOT/pages/extending-neo4j/procedures.adoc deleted file mode 100644 index 2fd12c3..0000000 --- a/modules/ROOT/pages/extending-neo4j/procedures.adoc +++ /dev/null @@ -1,298 +0,0 @@ -:description: How to write, test, and deploy a user-defined procedure for Neo4j. - -:procedure-template-url: https://github.com/neo4j-examples/neo4j-procedure-template/ - - -[[extending-neo4j-procedures]] -= 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 xref:extending-neo4j/customized-code.adoc[]. - -[NOTE] -==== -User-defined procedures requiring execution on the system database need to include the annotation `@SystemProcedure` or they will be classed as a user database procedure. -==== - -[[call-procedure]] -== 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: - -[source, cypher, role="noplay"] ----- -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 link:{neo4j-docs-base-uri}/cypher-manual/{page-version}/clauses/call[Cypher Manual -> `CALL` procedure]. - - -[[user-defined-procedures]] -== Create a procedure - -Make sure you have read and followed the preparatory setup instructions in xref:extending-neo4j/project-setup.adoc[]. - -[TIP] -==== -The example discussed below is available as link:{procedure-template-url}[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. - -.An example using JUnit 5 for testing a procedure that returns relationship types found in the graph. -[source, java] ----- -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); - } - } -} ----- - -[NOTE] -==== -The previous example uses JUnit 5, which requires the use of `org.neo4j.harness.junit.extension.Neo4jExtension`. -If you want to use JUnit 4 with Neo4j 4.x or 5, use `org.neo4j.harness.junit.rule.Neo4jRule` instead. -==== - -== 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 link:{procedure-template-url}[Neo4j Procedure Template^] repository. - -Particular things to note: - -* All procedures are annotated `@Procedure`. -* The procedure annotation can take three optional arguments: `name`, `mode`, and `eager`. -** `name` is used to specify a different name for the procedure than the default generated, which is `class.path.nameOfMethod`. - If `mode` is specified, `name` must be specified as well. -** `name` is not allowed in a reserved namespace, and having a `name` without a namespace is deprecated behavior. -** If a procedure is registered with the same name as a built-in procedure in a deprecated namespace, the built-in procedure is shadowed. -** `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 default `mode` is `READ`. - 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 to `false`. - If it is set to `true`, the Cypher planner plans an extra `eager` 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: -+ -[source, cypher] ----- -MATCH (n) -WHERE n.key = 'value' -WITH n -CALL example.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 as `eager` 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`. - -[NOTE] -==== -The correct way to signal an error from within a procedure is to throw `RuntimeException`. -==== - - -[[injectable-resources]] -== 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` -//* `SecurityContext` -//* `ProcedureTransaction` -//* `ProcedureMemory` Candidate for public API but not stable yet - -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 link:{neo4j-docs-base-uri}/operations-manual/{page-version}/security/securing-extensions[Operations Manual -> Securing extensions]. - -[[memory-resource-tracking]] -== Memory Resource Tracking - -[NOTE] -==== -The memory resource tracking API for the procedure framework is available for preview. -Future versions of Neo4j might contain breaking changes to this API. -==== - -If your procedure or function allocates significant amounts of heap memory, you can register allocations to count towards the configured transaction limits, see link:{neo4j-docs-base-uri}/operations-manual/{page-version}/performance/memory-configuration/#memory-configuration-limit-transaction-memory[Operations Manual -> Limit transaction memory usage] for more information. -This allows you to avoid `OutOfMemory` errors that cause database restarts. -Memory allocations also show up in query profiles. - -To do this you need to inject `org.neo4j.procedure.memory.ProcedureMemory` as a field in your procedure/function class. -`ProcedureMemory` has various methods to allow you to register allocations. -For example (see javadocs for a full reference): - -* `ProcedureMemoryTracker newTracker()` creates a new memory resource tracker that is bound to the current transaction. -* `HeapEstimator heapEstimator()` estimates the heap size of classes and instances. -* `HeapTrackingCollectionFactory collections()` lets you create collections that have built-in memory tracking of their internal structure. - -It's usually difficult and time-consuming to implement memory resource tracking. -These are a few considerations and caveats that are worth keeping in mind: - -- Limit the scope of the memory management. - Focus only on parts that can grow significantly in memory and ignore minor underestimation. -- Beware of overestimation by registering allocations of the same instance multiple times. - You can add reference counting or other mechanisms to avoid overestimation if that is a concern. -- It's common not to know the size of an instance before it has been allocated, which may lead you to register allocations after they have already been made. - The memory tracker implementation tries to prevent this by always pre-registering a certain amount of memory in the internal memory pools. -- It's cumbersome in Java to know when an instance has been garbage-collected. - Typically, you register the release of memory at the point when it's possible for that memory to be garbage-collected. - To account for this, memory trackers may internally choose not to register the release of memory instantaneously. -- Testing memory resource tracking can be difficult. - One approach is to use a third-party library, like JAMM (Java Agent for Memory Measurements), and assert that the estimates are close enough for some given input. - - -.A basic example of memory resource tracking in user defined procedures. -[source, java] ----- -package org.example; - -import org.neo4j.procedure.Context; -import org.neo4j.procedure.Name; -import org.neo4j.procedure.Procedure; -import org.neo4j.procedure.memory.ProcedureMemory; - -import java.util.Arrays; -import java.util.stream.Stream; - -public class MyProcedures { - - @Context - public ProcedureMemory memory; - - record Output(Long value) {} - - @Procedure("org.example.memoryHungryRange") - public Stream memoryHungryRange(@Name("size") int size) { - final var tracker = memory.newTracker(); - - // Register the allocation of the long array below - tracker.allocateHeap(memory.heapEstimator().sizeOfLongArray(size)); - // The actual allocation - final var result = new long[size]; - - for (int i = 0; i < size; i++) result[i] = i; - - return Arrays.stream(result) - .mapToObj(Output::new) - // Release all registered allocations when the stream is closed - .onClose(tracker::close); - } -} - ----- - - -[[reserved-procedure-namespaces]] -== Reserved procedure namespaces - -List of reserved procedure namespaces:: - -* cdc.* -* date.* -* datetime.* -* db.* -* dbms.* -* duration.* -* graph.* -* internal.* -* localdatetime.* -* localtime.* -* time.* -* tx.* -* unsupported.* - diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc new file mode 100644 index 0000000..a34ae95 --- /dev/null +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -0,0 +1,608 @@ +:description: How to write, test, and deploy a user-defined procedure, function, or aggregation function for Neo4j. + +:procedure-template-url: https://github.com/neo4j-examples/neo4j-procedure-template/tree/5.x +:org-neo4j-procedure-UserFunction: {neo4j-javadocs-base-uri}/org/neo4j/procedure/UserFunction.html +:org-neo4j-procedure-UserAggregationFunction: {neo4j-javadocs-base-uri}/org/neo4j/procedure/UserAggregationFunction.html + +[[extending-neo4j-procedures-and-functions]] += User-defined procedures and functions + +Defining procedures and functions enables you to extend Neo4j by writing customized code, which can be invoked directly from Cypher. +Procedures and functions can take arguments, perform operations on the database, and return results. + +* _User-defined procedures_ are the most powerful form of customization, allowing you to perform complex operations and return multiple results. +* _User-defined functions_ are simpler forms of procedures that return a single value and are read-only. +Although they are less powerful in capability, they are often easier to use and more efficient than procedures for many common tasks. +* _User-defined aggregation functions_ are functions that aggregate data and return a single result. + +For a comparison between user-defined procedures, functions, and aggregation functions, see xref:extending-neo4j/customized-code.adoc#comparison-of-procedures-and-functions[Comparison of procedures and functions]. + +[NOTE] +==== +User-defined procedures requiring execution on the system database need to include the annotation `@SystemProcedure` or they will be classified as standard database procedures. +==== + +[[create-user-defined-procedure-or-function]] +== Create a procedure or function + +Make sure you have read and followed the preparatory setup instructions in xref:extending-neo4j/project-setup.adoc[]. + +[TIP] +==== +The example discussed below is available as link:{procedure-template-url}[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, function, or aggregation function should do, then write a test that proves that it does it right. +Finally, write a procedure, function, or aggregation function that passes the test. + +=== Create integration tests + +You can use the test dependencies _Neo4j Harness_ and _JUnit_ to write integration tests for your procedure, function, or aggregation function. +The tests should start a Neo4j server, load the procedure, and execute queries against it. + +[.tabbed-example] +===== +[.include-with-procedure-integration-tests] +====== +The following is an example using Neo4j Harness and JUnit 5 for testing a procedure that returns relationship types found in the graph: + +[source, java] +---- +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); + } + } +} +---- + +[NOTE] +==== +The example uses JUnit 5, which requires the use of `org.neo4j.harness.junit.extension.Neo4jExtension`. +If you want to use JUnit 4 with Neo4j 4.x or 5, use `org.neo4j.harness.junit.rule.Neo4jRule` instead. +==== + +====== +[.include-with-function-integration-tests] +====== + +The following is an example that uses Neo4j Harness for testing a user-defined function that joins a list of strings: + +[source, java] +---- +package example; + +import org.junit.jupiter.api.AfterAll; +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.Session; +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 JoinTest { + + private Neo4j embeddedDatabaseServer; + + @BeforeAll + void initializeNeo4j() { + this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() + .withDisabledServer() + .withFunction(Join.class) + .build(); + } + + @AfterAll + void closeNeo4j() { + this.embeddedDatabaseServer.close(); + } + + @Test + void joinsStrings() { + // This is in a try-block, to make sure we close the driver after the test + try(Driver driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI()); + Session session = driver.session()) { + + // When + String result = session.run( "RETURN example.join(['Hello', 'World']) AS result").single().get("result").asString(); + + // Then + assertThat( result).isEqualTo(( "Hello,World" )); + } + } +} +---- + +====== +[.include-with-aggregation-function-integration-tests] +====== + +The following is an example that uses Neo4j Harness and JUnit 5 for testing a user-defined aggregation function that finds the longest string: + +[source, java] +---- +package example; + +import org.junit.Rule; +import org.junit.Test; +import org.neo4j.driver.v1.*; +import org.neo4j.harness.junit.Neo4jRule; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +public class LongestStringTest +{ + // This rule starts a Neo4j instance + @Rule + public Neo4jRule neo4j = new Neo4jRule() + + // This is the function to test + .withAggregationFunction( LongestString.class ); + + @Test + public void shouldAllowIndexingAndFindingANode() throws Throwable + { + // This is in a try-block, to make sure you close the driver after the test + try( Driver driver = GraphDatabase.driver( neo4j.boltURI() , Config.build().withEncryptionLevel( Config.EncryptionLevel.NONE ).toConfig() ) ) + { + // Given + Session session = driver.session(); + + // When + String result = session.run( "UNWIND ["abc", "abcd", "ab"] AS string RETURN example.longestString(string) AS result").single().get("result").asString(); + + // Then + assertThat( result, equalTo( "abcd" ) ); + } + } +} +---- + +====== +===== + + +=== Define your procedure, function, or aggregation function + +With the test in place, write a procedure, function, or aggregation function that fulfills the expectations of the test. +The full example is available in the link:{procedure-template-url}[Neo4j Procedure Template^] repository. + +See xref:extending-neo4j/values-and-types.adoc[] for details on values and types. + +Particular things to note: + +[.tabbed-example] +===== +[.include-with-procedures] +====== + +* All procedures are annotated `@Procedure`. +* The procedure annotation can take three optional arguments: `name`, `mode`, and `eager`. +** `name` is used to specify a different name for the procedure than the default generated, which is `class.path.nameOfMethod`. + If `mode` is specified, `name` must be specified as well. +** `name` is not allowed in a reserved namespace, and having a `name` without a namespace is deprecated behavior. +** If a procedure is registered with the same name as a built-in procedure in a deprecated namespace, the built-in procedure is shadowed. +** `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 default `mode` is `READ`. + 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 to `false`. + If it is set to `true`, the Cypher planner plans an extra `eager` 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: ++ +[source, cypher] +---- +MATCH (n) +WHERE n.key = 'value' +WITH n +CALL example.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 as `eager` 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`. + +====== +[.include-with-functions] +====== + +* All functions are annotated with `@UserFunction`. +* The function name must be namespaced and is not allowed in reserved namespaces. +* If a function is registered with the same name as a built-in function in a deprecated namespace, the built-in function is shadowed. + +For more details, see the link:{org-neo4j-procedure-UserFunction}[Neo4j Javadocs for `org.neo4j.procedure.UserFunction`^]. + +====== +[.include-with-aggregation-functions] +====== + +* All functions are annotated with `@UserAggregationFunction`. +* The annotated function must return an instance of an aggregator class. +* An aggregator class contains one method annotated with `@UserAggregationUpdate` and one method annotated with `@UserAggregationResult`. +The method annotated with `@UserAggregationUpdate` is called multiple times and enables the class to aggregate data. +When the aggregation is done, the method annotated with `@UserAggregationResult` is called once and the result of the aggregation is returned. +* The aggregation function name must be namespaced and is not allowed in reserved namespaces. +* If a user-defined aggregation function is registered with the same name as a built-in function in a deprecated namespace, the built-in function is shadowed. + +For more details, see the Neo4j Javadocs for link:{org-neo4j-procedure-UserAggregationFunction}[`org.neo4j.procedure.UserAggregationFunction`^]. + +====== +===== + +[NOTE] +==== +The correct way to signal an error from within an aggregation function is to throw `RuntimeException`. +==== + +=== Write your user-defined procedure, function, and aggregation function + +The following examples show how to write a user-defined procedure, function, and aggregation function. + +[.tabbed-example] +===== +[.include-with-procedure-example] +====== + +The following is an example of a user-defined procedure that returns the relationship types going in and out of a node: + +[source, java] +---- +package example; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.logging.Log; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; + +public class GetRelationshipTypes { + // This gives us a log instance that outputs messages to the + // standard log, normally found under `data/log/console.log` + @Context + public Log log; + + /** + * This procedure takes a Node and gets the relationships going in and out of it + * + * @param node The node to get the relationships for + * @return A RelationshipTypes instance with the relations (incoming and outgoing) for a given node. + */ + @Procedure(name = "example.getRelationshipTypes") + @Description("Get the different relationships going in and out of a node.") + public Stream getRelationshipTypes(@Name("node") Node node) { + List outgoing = new ArrayList<>(); + node.getRelationships(Direction.OUTGOING).iterator() + .forEachRemaining(rel -> AddDistinct(outgoing, rel)); + + List incoming = new ArrayList<>(); + node.getRelationships(Direction.INCOMING).iterator() + .forEachRemaining(rel -> AddDistinct(incoming, rel)); + + return Stream.of(new RelationshipTypes(incoming, outgoing)); + } +---- + +====== +[.include-with-function-example] +====== + +The following is an example of a user-defined function that joins a list of strings with a given delimiter: + +[source, java] +---- +package example; + +import java.util.List; + +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.UserFunction; + +/** + * This is an example how you can create a simple user-defined function for Neo4j. + */ +public class Join { + + @UserFunction + @Description("example.join(['s1','s2',...], delimiter) - join the given strings with the given delimiter.") + public String join( + @Name("strings") List strings, + @Name(value = "delimiter", defaultValue = ",") String delimiter) { + if (strings == null || delimiter == null) { + return null; + } + return String.join(delimiter, strings); + } +} +---- + +====== +[.include-with-aggregation-function-example] +====== +The following is an example of a user-defined aggregation function that finds the longest string: + +[source, java] +---- +package example; + +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.UserAggregationFunction; +import org.neo4j.procedure.UserAggregationResult; +import org.neo4j.procedure.UserAggregationUpdate; + +public class LongestString +{ + @UserAggregationFunction + @Description( "org.neo4j.function.example.longestString(string) - aggregates the longest string found" ) + public LongStringAggregator longestString() + { + return new LongStringAggregator(); + } + + public static class LongStringAggregator + { + private int longest; + private String longestString; + + @UserAggregationUpdate + public void findLongest( + @Name( "string" ) String string ) + { + if ( string != null && string.length() > longest) + { + longest = string.length(); + longestString = string; + } + } + + @UserAggregationResult + public String result() + { + return longestString; + } + } +} +---- +====== +===== + +[[injectable-resources]] +=== Injectable resources + +When writing procedures, functions, or aggregation functions, 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` +//* `SecurityContext` +//* `ProcedureTransaction` +//* `ProcedureMemory` Candidate for public API but not stable yet + +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. +Procedure, functions, and aggregation functions written to use these restricted APIs are not loaded by default, and you need to use the `dbms.security.procedures.unrestricted` to load them. +Read more about this config setting in link:{neo4j-docs-base-uri}/operations-manual/{page-version}/security/securing-extensions[Operations Manual -> Securing extensions]. + +[[call-procedure-or-function]] +== Call user-defined procedures or functions + +You can call user-defined procedures and functions from Cypher queries in the same way as built-in procedures and functions. + +[.tabbed-example] +===== +[.include-with-call-procedures] +====== +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: + +[source, cypher, role="noplay"] +---- +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 link:{neo4j-docs-base-uri}/cypher-manual/current/clauses/call[Cypher Manual -> `CALL` procedure]. + +====== + +[.include-with-call-functions] +====== +User-defined functions are called in the same way as any other Cypher function. +The function name must be fully qualified, so a function named `join` defined in the package `org.neo4j.examples` could be called using: + +[source, cypher, role="noplay"] +---- +MATCH (p: Person) WHERE p.age = 36 +RETURN org.neo4j.examples.join(collect(p.names)) +---- + +====== +[.include-with-call-aggregation-functions] +====== + +User-defined aggregation functions are called in the same way as any other Cypher aggregation function. +The function name must be fully qualified, so a function named `longestString` defined in the package `org.neo4j.examples` could be called using: + +[source, cypher, role="noplay"] +---- +MATCH (p: Person) WHERE p.age = 36 +RETURN org.neo4j.examples.longestString(p.name) +---- +====== +===== + +[[reserved-and-deprecated-namespaces]] +== Reserved and deprecated procedure namespaces + +[NOTE] +==== +Note that deprecated procedure and function namespaces will be moved to reserved in the next major Cypher version. +For more information about Neo4j and Cypher versioning, see link:https://neo4j.com/docs/operations-manual/5/introduction/#_cypher_versions[Operations manual -> Introduction]. +==== + +[.tabbed-example] +===== +[.include-with-procedure-namespaces] +====== + +The following table shows the reserved procedure namespaces: + +* cdc.* +* date.* +* datetime.* +* db.* +* dbms.* +* duration.* +* graph.* +* internal.* +* localdatetime.* +* localtime.* +* time.* +* tx.* +* unsupported.* + + +====== + +[.include-with-function-namespaces] +====== + +The following table shows the reserved function namespaces: + +* * +* date +* date.realtime +* date.statement +* date.transaction +* date.truncate +* datetime +* datetime.fromepoch +* datetime.fromepochmillis +* datetime.realtime +* datetime.statement +* datetime.transaction +* datetime.truncate +* db.nameFromElementId +* duration +* duration.between +* duration.inDays +* duration.inMonths +* duration.inSeconds +* graph.byElementId +* graph.byName +* graph.names +* graph.propertiesByName +* localdatetime +* localdatetime.realtime +* localdatetime.statement +* localdatetime.transaction +* localdatetime.truncate +* localtime +* localtime.realtime +* localtime.statement +* localtime.transaction +* localtime.truncate +* point.distance +* point.withinBBox +* time +* time.realtime +* time.statement +* time.transaction +* time.truncate +* vector.similarity.cosine +* vector.similarity.euclidean + +====== +===== \ No newline at end of file diff --git a/modules/ROOT/pages/traversal-framework/index.adoc b/modules/ROOT/pages/traversal-framework/index.adoc index 50e6810..065dac0 100644 --- a/modules/ROOT/pages/traversal-framework/index.adoc +++ b/modules/ROOT/pages/traversal-framework/index.adoc @@ -28,7 +28,7 @@ image::graphdb-traversal-description.svg[role="middle"] == Using the Traversal Framework The Traversal Framework can be used xref:java-embedded/traversal.adoc[embedded in Java applications]. -It can also be used when extending Neo4j with a xref:/extending-neo4j/procedures.adoc[User-defined Procedure]. +It can also be used when extending Neo4j with a xref:extending-neo4j/user-defined-procedures-functions.adoc[User-defined procedures and functions]. For an example, see xref:traversal-framework/traversal-framework-example.adoc#traversal-in-a-procedure-example[User-defined procedure with a Traversal Framework]. [[traversal-vs-cypher]] @@ -44,7 +44,7 @@ Some of the advantages of using the Traversal Framework over Cypher include: See xref:/traversal-framework/traversal-framework-java-api.adoc#traversal-java-api-evaluator[Evaluator] for more information. * With Cypher, it is not possible to specify the order in which paths are expanded (e.g. depth-first). However, with the Traversal Framework, it is possible to specify the xref:/traversal-framework/traversal-framework-java-api.adoc#traversal-java-api-branchselector[order of paths traversed]. -* With Cypher, relationships are only traversed when `RELATIONSHIP_GLOBAL` uniqueness is specified. +* With Cypher, relationships are only traversed when `RELATIONSHIP_GLOBAL` uniqueness is specified. By using the Traversal Framework, it is possible to specify the xref:/traversal-framework/traversal-framework-java-api.adoc#traversal-java-api-uniqueness[uniqueness constraints on the path traversed]. [WARNING] diff --git a/modules/ROOT/pages/traversal-framework/traversal-framework-example.adoc b/modules/ROOT/pages/traversal-framework/traversal-framework-example.adoc index d5b1355..af8b60f 100644 --- a/modules/ROOT/pages/traversal-framework/traversal-framework-example.adoc +++ b/modules/ROOT/pages/traversal-framework/traversal-framework-example.adoc @@ -173,7 +173,7 @@ KNOWS [[traversal-in-a-procedure-example]] == Implementing a user-defined procedure -This example shows how to implement a xref:/extending-neo4j/procedures.adoc[user-defined procedure] using the Traversal Framework. +This example shows how to implement a xref:extending-neo4j/user-defined-procedures-functions.adoc[user-defined procedure] using the Traversal Framework. The transaction and logger are made available through the Procedure Framework: [source, java]