From 95e671a7a0b3e13bf2ffc80c9fb099c9073bb683 Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Tue, 14 Nov 2023 15:22:31 +0200 Subject: [PATCH 01/11] document reloadable procedures --- .../pages/extending-neo4j/customized-code.adoc | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/ROOT/pages/extending-neo4j/customized-code.adoc b/modules/ROOT/pages/extending-neo4j/customized-code.adoc index c4b44c5..4bf017d 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. +These mechanisms 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,16 @@ 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. +To write these procedures and functions, you must use Java and compile them into JAR files. Once compiled, these JAR files are deployed to the _NEO4J_HOME/plugins_ directory on each standalone or clustered server and loaded during start-up. 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. + +label:enterprise-only[] From Neo4j 5.14 onwards, you can use the built-in procedure `CALL dbms.reloadProcedure()` to reload procedures and functions without restarting the DBMS. +Keep in mind that in a cluster deployment, you need to call this procedure on each cluster member. + +[NOTE] +==== +The reload process is isolated so that the operation only affects new transactions. Currently, running transactions continue to execute with a snapshot of the available procedures at their respective initialization. +==== Procedures and functions can take arguments and return results. In addition, procedures can perform write operations on the database. From ad0141c467a00af1a75bd0e37893a44d24e25df8 Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 11:38:16 +0100 Subject: [PATCH 02/11] Merge user-defined procedure, functions, and aggregated functions and document reloadable procedures --- modules/ROOT/content-nav.adoc | 4 +- .../aggregation-functions.adoc | 195 ----- .../extending-neo4j/customized-code.adoc | 103 ++- .../ROOT/pages/extending-neo4j/functions.adoc | 181 ---- .../pages/extending-neo4j/procedures.adoc | 302 ------- .../user-defined-procedures-functions.adoc | 770 ++++++++++++++++++ .../ROOT/pages/traversal-framework/index.adoc | 4 +- .../traversal-framework-example.adoc | 2 +- .../partials/user-procedures-functions.adoc | 8 + 9 files changed, 876 insertions(+), 693 deletions(-) delete mode 100644 modules/ROOT/pages/extending-neo4j/aggregation-functions.adoc delete mode 100644 modules/ROOT/pages/extending-neo4j/functions.adoc delete mode 100644 modules/ROOT/pages/extending-neo4j/procedures.adoc create mode 100644 modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc create mode 100644 modules/ROOT/partials/user-procedures-functions.adoc 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 4db23ed..0000000 --- a/modules/ROOT/pages/extending-neo4j/aggregation-functions.adoc +++ /dev/null @@ -1,195 +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-and-deprecated-namespaces]] -== Reserved and deprecated function namespaces - -Note that deprecated 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/current/introduction/#_cypher_versions[Operations manual -> Introduction]. -[[reserved-and-deprecated-function-namespaces]] -.Overview of reserved and deprecated function namespaces and names -[options="header", cols="m,m"] -|=== -| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 -| * | abac.* -| date | builtin.* -| date.realtime | cdc.* -| date.statement | coll.* -| date.transaction | date.* -| date.truncate | datetime.* -| datetime | db.* -| datetime.fromepoch | dbms.* -| datetime.fromepochmillis | duration.* -| datetime.realtime | graph.* -| datetime.statement | internal.* -| datetime.transaction | localdatetime.* -| datetime.truncate | localtime.* -| db.nameFromElementId | math.* -| duration | plugin.* -| duration.between | point.* -| duration.inDays | stored.* -| duration.inMonths | string.* -| duration.inSeconds | time.* -| graph.byElementId | tx.* -| graph.byName | unsupported.* -| graph.names | vector.* -| 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 4bf017d..a2d1220 100644 --- a/modules/ROOT/pages/extending-neo4j/customized-code.adoc +++ b/modules/ROOT/pages/extending-neo4j/customized-code.adoc @@ -14,16 +14,23 @@ 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. -To write these procedures and functions, you must use Java and compile them into JAR files. Once compiled, these JAR files are deployed to the _NEO4J_HOME/plugins_ directory on each standalone or clustered server and loaded during start-up. +== Writing user-defined procedures and functions + +To write these procedures and functions, you must use Java and compile them into JAR files. +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 and loaded during start-up. 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]. -label:enterprise-only[] From Neo4j 5.14 onwards, you can use the built-in procedure `CALL dbms.reloadProcedure()` to reload procedures and functions without restarting the DBMS. -Keep in mind that in a cluster deployment, you need to call this procedure on each cluster member. +label:new[Introduced in 2026.09] label:enterprise-only[] You can use the built-in procedure `dbms.reloadProcedures()` to reload procedures and functions without restarting the DBMS. +See xref:extending-neo4j/user-defined-procedures-functions.adoc#reload-procedures-functions[Reloading procedures and functions] for details. -[NOTE] -==== -The reload process is isolated so that the operation only affects new transactions. Currently, running transactions continue to execute with a snapshot of the available procedures at their respective initialization. -==== +include::partial$user-procedures-functions.adoc[] + +[[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. @@ -57,11 +64,89 @@ 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 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); + } +} + +---- + +== 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}/procedures[Operations Manual -> Procedures]. Running `SHOW PROCEDURES` displays the full list of procedures available in your Neo4j DBMS, including user-defined procedures. The built-in functions are described in link:{neo4j-docs-base-uri}/cypher-manual/current/functions[Cypher Manual -> Functions]. -Running `SHOW FUNCTIONS` displays the full list of all the functions available in your Neo4j DBMS, including user-defined functions. - +Running `SHOW FUNCTIONS` displays the full list of all the functions available in your Neo4j DBMS, including user-defined functions. \ No newline at end of file diff --git a/modules/ROOT/pages/extending-neo4j/functions.adoc b/modules/ROOT/pages/extending-neo4j/functions.adoc deleted file mode 100644 index fcfb550..0000000 --- a/modules/ROOT/pages/extending-neo4j/functions.adoc +++ /dev/null @@ -1,181 +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-and-deprecated-namespaces]] -== Reserved and deprecated function namespaces - -Note that deprecated 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/current/introduction/#_cypher_versions[Operations manual -> Introduction]. -[[reserved-and-deprecated-function-namespaces]] -.Overview of reserved and deprecated function namespaces and names -[options="header", cols="m,m"] -|=== -| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 -| * | abac.* -| date | builtin.* -| date.realtime | cdc.* -| date.statement | coll.* -| date.transaction | date.* -| date.truncate | datetime.* -| datetime | db.* -| datetime.fromepoch | dbms.* -| datetime.fromepochmillis | duration.* -| datetime.realtime | graph.* -| datetime.statement | internal.* -| datetime.transaction | localdatetime.* -| datetime.truncate | localtime.* -| db.nameFromElementId | math.* -| duration | plugin.* -| duration.between | point.* -| duration.inDays | stored.* -| duration.inMonths | string.* -| duration.inSeconds | time.* -| graph.byElementId | tx.* -| graph.byName | unsupported.* -| graph.names | vector.* -| 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/extending-neo4j/procedures.adoc b/modules/ROOT/pages/extending-neo4j/procedures.adoc deleted file mode 100644 index 64d1bfb..0000000 --- a/modules/ROOT/pages/extending-neo4j/procedures.adoc +++ /dev/null @@ -1,302 +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/current/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-and-deprecated-namespaces]] -== Reserved and deprecated procedure namespaces - -Note that deprecated procedure 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/current/introduction/#_cypher_versions[Operations manual -> Introduction]. -[[reserved-and-deprecated-procedure-namespaces]] -.Overview of reserved and deprecated procedure namespaces -[options="header", cols="m,m"] -|=== -| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 -| cdc.* | * -| date.* | abac.* -| datetime.* | builtin.* -| db.* | coll.* -| dbms.* | math.* -| duration.* | plugin.* -| graph.* | point.* -| internal.* | stored.* -| localdatetime.* | string.* -| localtime.* | vector.* -| 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..cb22004 --- /dev/null +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -0,0 +1,770 @@ +: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/ +: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 classed as a user database procedure. +==== + +[[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 + +The test dependencies include _Neo4j Harness_ and _JUnit_. +These can be used to write integration tests for procedures and functions. +The tests should start a Neo4j instance, load the procedure, and execute queries against it. + +[.tabbed-example] +===== +[.include-with-procedure-integration-tests] +====== +The following is 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); + } + } +} +---- +====== +[.include-with-function-integration-tests] +====== + +The following is an example 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" )); + } + } +} +---- + +====== +[.include-with-function-integration-tests] +====== + +The following is an example 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" ) ); + } + } +} +---- + +====== +===== + + +=== 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 all nodes in the graph: + +[source, java] +---- +package example; + +import java.util.stream.Stream; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.ResourceIterator; +import org.neo4j.graphdb.Transaction; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Mode; +import org.neo4j.procedure.Procedure; + +/** + * This is an example returning {@link org.neo4j.graphdb.Entity Entities} from stored procedures. + * {@link Node Nodes} and {@link org.neo4j.graphdb.Relationship relationships} are both entities + * and can only be accessed in their transaction. So it is important that you use the injected one + * and not open a new one; otherwise you can access them from the outside. + */ +public class EntityResultExample { + + @Context + public Transaction tx; + + public record EntityContainer(Node node) { + } + + @Procedure(name = "example.allnodes", mode = Mode.READ) + public Stream allnodes() { + ResourceIterator nodes = tx.execute("MATCH (n) RETURN n").columnAs("n"); + return nodes.stream().map(EntityContainer::new); + } +} +---- + +====== +[.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) +---- +====== +===== + +[role=label--new-2026.09 label--enterprise-only label--admin-only] +== Reload procedures and functions + +You can use the built-in procedure `dbms.reloadProcedures()` to reload the user supplied procedure or function JAR files from the _plugins_ directory into a running Neo4j, without restarting it. +This procedure is available only on `block` format. + +For more information about plugins, see link:https://neo4j.com/docs/operations-manual/current/configuration/plugins/[Operations Manual -> Configure plugins]. + +=== Syntax + +The syntax for the `dbms.reloadProcedures()` procedure is as follows: + +.Details +|=== +| Syntax 3+m| dbms.reloadProcedures(namespaces = * :: STRING) :: (name :: STRING, type :: STRING) +| Description 3+a| Reload procedures from disk. +.2+| Input arguments | Name | Type | Description +| namespaces | STRING | Optional. A glob pattern selecting which namespaces to reload. Defaults to '\*', meaning everything. Example: 'com.example.*'. +.3+| Input arguments | Name | Type | Description +| name | STRING | The fully qualified name of each entry point that was reloaded. +| type | STRING | The type of each entry point that was reloaded. Possible values are: PROCEDURE, FUNCTION or AGGREGATION FUNCTION. +| Mode 3+| DBMS +|=== + +A successful call returns one row for every entry point it brought back into service. +An empty result means nothing matched the pattern. + +[NOTE] +==== +The namespace argument genuinely limits the operation. +It is not advisory. +A plugin whose namespace does not match the pattern is left untouched, even if its JAR file changed on disk. +Three namespaces are permanently excluded and cannot be reloaded: `apoc.*`, `gds.*`, and `aura.*`. +==== + +=== Example + +include::partial$user-procedures-functions.adoc[] + +You can use the following example to test the `dbms.reloadProcedures()` procedure to reload a user-defined procedure or function without restarting the server, as well as withdraw a procedure or function. + +. Confirm the procedure is available: ++ +[source, cypher] +---- +SHOW PROCEDURES YIELD name, description, admin +WHERE name = 'dbms.reloadProcedures' +RETURN name, description, admin; +---- ++ +[queryresult] +---- +name description admin +"dbms.reloadProcedures" "Reload procedures from disk." TRUE +---- + +. Call the plugin currently in service: ++ +[source, cypher] +---- +CALL com.example.version(); +---- ++ +The query returns the version of the plugin currently in service, which is "v1". + +. Replace the JAR while the server keeps running: ++ +[source, bash] +---- +rm plugins/example-v1.jar +cp example-v2.jar plugins/ +---- + +. Reload that namespace: ++ +[source, cypher] +---- +CALL dbms.reloadProcedures('com.example.*'); +---- ++ +[queryresult] +---- +name type +"com.example.version" "PROCEDURE" +"com.example.newProcedure" "PROCEDURE" +"com.example.greet" "FUNCTION" +"com.example.joinAll" "AGGREGATION FUNCTION" +---- + +. Confirm the new version is serving: ++ +[source, cypher] +---- +CALL com.example.version(); +---- ++ +The query returns the version of the plugin currently in service, which is now "v2". ++ +[source, cypher] +---- +CALL com.example.newProcedure(); +---- ++ +The query returns a procedure that did not exist a moment ago. + +=== Withdraw a procedure or function + +Remove a JAR and reload its namespace unregisters everything that JAR provided, so that it is no longer available to Cypher. +For example, to withdraw the `com.example.greet` function, remove the JAR, and reload the namespace: + +.Remove the JAR and add a new one that does not contain the `com.example.greet` function: +[source, bash] +---- +rm plugins/example-v2.jar +cp example-v3.jar plugins/ +---- + +.Reload the namespace +[source, cypher] +---- +CALL dbms.reloadProcedures('com.example.*'); +---- +The query returns the remaining entry points in that namespace: + +[queryresult] +---- +name type +"com.example.version" "PROCEDURE" +"com.example.newProcedure" "PROCEDURE" +"com.example.joinAll" "AGGREGATION FUNCTION" +---- + +=== Confirm that no restart happened + +You can check that the server did not restart by checking the server start time and uptime. +The server start time can be read from Cypher and should be unchanged across a reload. + +[source, cypher] +---- +CALL dbms.queryJmx('java.lang:type=Runtime') YIELD attributes +RETURN datetime({epochMillis: attributes.StartTime.value}) AS serverStarted, + duration({milliseconds: attributes.Uptime.value}) AS uptime; +---- + +=== Troubleshooting + +A reload builds an intermediate registry and only swaps it into service if the whole load succeeds. +If any JAR in the directory is invalid, the reload fails and the previously loaded plugins keep serving, unchanged. +When a corrupt file is in the plugins directory, the reload will return https://neo4j.com/docs/status-codes/current/errors/gql-errors/52N24/[52N24] and the plugin that was already in service will continue to answer correctly. +Removing the invalid file and reloading again restores normal operation with no restart. + +==== Logging + +Every reload writes a matched pair of entries to _debug.log_, giving operators an audit trail of what was reloaded and when. +For example: +[source, log] +---- +INFO c.n.d.p.ReloadProcedure "Reloading procedure namespace `com.example.*`." +INFO c.n.d.p.ReloadProcedure "Reloaded 2 procedures, 1 functions, and 1 aggregation functions." +---- + +==== Query cache invalidation + +Cached Cypher plans that reference a changed signature are discarded automatically, so a client does not continue to run a stale plan against the old plugin. +For example, if a procedure signature changes, the following log entry is written to _debug.log_: +[source, log] +---- +INFO o.n.c.i.c.CypherQueryCaches "Discarded stale query from the query cache after 5 seconds. +Reason: Procedure or function signature have been modified. Query id: 79." +---- + +[[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/current/introduction/#_cypher_versions[Operations manual -> Introduction]. +==== + +[.tabbed-example] +===== +[.include-with-procedure-namespaces] +====== + +The following table shows the reserved and deprecated procedure namespaces: + +[options="header", cols="m,m"] +|=== +| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 +| cdc.* | * +| date.* | abac.* +| datetime.* | builtin.* +| db.* | coll.* +| dbms.* | math.* +| duration.* | plugin.* +| graph.* | point.* +| internal.* | stored.* +| localdatetime.* | string.* +| localtime.* | vector.* +| time.* | +| tx.* | +| unsupported.* | +|=== + +====== + +[.include-with-function-namespaces] +====== + +The following table shows the reserved and deprecated function namespaces: + +[options="header", cols="m,m"] +|=== +| Reserved | Deprecated in Cypher 25 since Neo4j 2025.11 +| * | abac.* +| date | builtin.* +| date.realtime | cdc.* +| date.statement | coll.* +| date.transaction | date.* +| date.truncate | datetime.* +| datetime | db.* +| datetime.fromepoch | dbms.* +| datetime.fromepochmillis | duration.* +| datetime.realtime | graph.* +| datetime.statement | internal.* +| datetime.transaction | localdatetime.* +| datetime.truncate | localtime.* +| db.nameFromElementId | math.* +| duration | plugin.* +| duration.between | point.* +| duration.inDays | stored.* +| duration.inMonths | string.* +| duration.inSeconds | time.* +| graph.byElementId | tx.* +| graph.byName | unsupported.* +| graph.names | vector.* +| 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] diff --git a/modules/ROOT/partials/user-procedures-functions.adoc b/modules/ROOT/partials/user-procedures-functions.adoc new file mode 100644 index 0000000..94682d6 --- /dev/null +++ b/modules/ROOT/partials/user-procedures-functions.adoc @@ -0,0 +1,8 @@ +[CAUTION] +==== +Keep in mind that in a cluster deployment, you need to deploy the JAR files and run the `dbms.reloadProcedures()` procedure on each cluster member. +Otherwise, the cluster members will be out of sync, leading to unexpected behavior. + +The reload process is isolated so that the operation only affects new transactions. +Currently running transactions continue to execute with a snapshot of the available procedures or functions at their respective initialization. +==== \ No newline at end of file From 63b87f010f8c59d4ca4c99da70c0fd1048cbd86a Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 11:56:55 +0100 Subject: [PATCH 03/11] add cypher 25 label --- .../extending-neo4j/user-defined-procedures-functions.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index cb22004..8cbba10 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -507,7 +507,7 @@ RETURN org.neo4j.examples.longestString(p.name) ====== ===== -[role=label--new-2026.09 label--enterprise-only label--admin-only] +[role=label--new-2026.09 label--enterprise-only label--admin-only label--cypher-25] == Reload procedures and functions You can use the built-in procedure `dbms.reloadProcedures()` to reload the user supplied procedure or function JAR files from the _plugins_ directory into a running Neo4j, without restarting it. From 2414015dbd968e05f97015a9d059b5ada0c332f9 Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 11:59:20 +0100 Subject: [PATCH 04/11] fix the tab example --- .../extending-neo4j/user-defined-procedures-functions.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index 8cbba10..5346bbf 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -178,7 +178,7 @@ public class JoinTest { ---- ====== -[.include-with-function-integration-tests] +[.include-with-aggregation-function-integration-tests] ====== The following is an example template for testing a user-defined aggregation function that finds the longest string: From 93e98b8cd86a6f7596888b11473153c6640f7901 Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 12:03:14 +0100 Subject: [PATCH 05/11] add an anchor for reloadable procedures --- .../pages/extending-neo4j/user-defined-procedures-functions.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index 5346bbf..8957f15 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -508,6 +508,7 @@ RETURN org.neo4j.examples.longestString(p.name) ===== [role=label--new-2026.09 label--enterprise-only label--admin-only label--cypher-25] +[reload-procedures-functions] == Reload procedures and functions You can use the built-in procedure `dbms.reloadProcedures()` to reload the user supplied procedure or function JAR files from the _plugins_ directory into a running Neo4j, without restarting it. From 87e7c7572ebdcba96617847f9c515614b90caac9 Mon Sep 17 00:00:00 2001 From: Natalia Ivakina <82437520+NataliaIvakina@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:56:57 +0200 Subject: [PATCH 06/11] Apply suggestion from @NataliaIvakina --- .../extending-neo4j/user-defined-procedures-functions.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index 8957f15..c15f67e 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -508,7 +508,7 @@ RETURN org.neo4j.examples.longestString(p.name) ===== [role=label--new-2026.09 label--enterprise-only label--admin-only label--cypher-25] -[reload-procedures-functions] +[[reload-procedures-functions]] == Reload procedures and functions You can use the built-in procedure `dbms.reloadProcedures()` to reload the user supplied procedure or function JAR files from the _plugins_ directory into a running Neo4j, without restarting it. From c5da968f7ea193c3907fdf0ec871f5a0cad47c6a Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 15:23:27 +0100 Subject: [PATCH 07/11] update the syntax table and the query result of dbms.reloadProcedures --- .../user-defined-procedures-functions.adoc | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index c15f67e..9034163 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -522,14 +522,14 @@ The syntax for the `dbms.reloadProcedures()` procedure is as follows: .Details |=== -| Syntax 3+m| dbms.reloadProcedures(namespaces = * :: STRING) :: (name :: STRING, type :: STRING) -| Description 3+a| Reload procedures from disk. -.2+| Input arguments | Name | Type | Description +| *Syntax* 3+m| dbms.reloadProcedures(namespaces = * :: STRING) :: (name :: STRING, type :: STRING) +| *Description* 3+a| Reload procedures from disk. +.2+| *Input arguments* | *Name* | *Type* | *Description* | namespaces | STRING | Optional. A glob pattern selecting which namespaces to reload. Defaults to '\*', meaning everything. Example: 'com.example.*'. -.3+| Input arguments | Name | Type | Description +.3+| *Returned arguments* | *Name* | *Type* | *Description* | name | STRING | The fully qualified name of each entry point that was reloaded. | type | STRING | The type of each entry point that was reloaded. Possible values are: PROCEDURE, FUNCTION or AGGREGATION FUNCTION. -| Mode 3+| DBMS +| *Mode* 3+| DBMS |=== A successful call returns one row for every entry point it brought back into service. @@ -560,8 +560,14 @@ RETURN name, description, admin; + [queryresult] ---- -name description admin -"dbms.reloadProcedures" "Reload procedures from disk." TRUE ++------------------------------------------------------------------+ +| name | description | admin | ++------------------------------------------------------------------+ +| "dbms.reloadProcedures" | "Reload procedures from disk." | TRUE | ++------------------------------------------------------------------+ + +1 row +ready to start consuming query after 111 ms, results consumed after another 8 ms ---- . Call the plugin currently in service: From ebce4320a29264d2c08c47ab26a15e4c2fd2a957 Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 16:21:41 +0100 Subject: [PATCH 08/11] simplified the example --- .../user-defined-procedures-functions.adoc | 69 ++++--------------- 1 file changed, 12 insertions(+), 57 deletions(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index 9034163..ec8613b 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -549,7 +549,9 @@ include::partial$user-procedures-functions.adoc[] You can use the following example to test the `dbms.reloadProcedures()` procedure to reload a user-defined procedure or function without restarting the server, as well as withdraw a procedure or function. -. Confirm the procedure is available: +The example assumes you have a Neo4j server running with the `example-v1.jar` plugin in the _plugins_ directory, which contains a procedure `com.example.someProcedure()`. + +. Confirm that reload procedure is available by running the following query: + [source, cypher] ---- @@ -570,16 +572,8 @@ RETURN name, description, admin; ready to start consuming query after 111 ms, results consumed after another 8 ms ---- -. Call the plugin currently in service: -+ -[source, cypher] ----- -CALL com.example.version(); ----- -+ -The query returns the version of the plugin currently in service, which is "v1". - -. Replace the JAR while the server keeps running: +. Replace the `example-v1.jar` with `example-v2.jar` while the server keeps running. +The new JAR file contains the `com.example.someProcedure` as well as some new procedures and functions: + [source, bash] ---- @@ -596,55 +590,16 @@ CALL dbms.reloadProcedures('com.example.*'); + [queryresult] ---- -name type -"com.example.version" "PROCEDURE" -"com.example.newProcedure" "PROCEDURE" -"com.example.greet" "FUNCTION" -"com.example.joinAll" "AGGREGATION FUNCTION" ----- - -. Confirm the new version is serving: -+ -[source, cypher] ----- -CALL com.example.version(); +name type +"com.example.someProcedure" "PROCEDURE" +"com.example.newProcedure" "PROCEDURE" +"com.example.newFunction" "FUNCTION" +"com.example.newAggregationFunction" "AGGREGATION FUNCTION" ---- + -The query returns the version of the plugin currently in service, which is now "v2". -+ -[source, cypher] ----- -CALL com.example.newProcedure(); ----- -+ -The query returns a procedure that did not exist a moment ago. - -=== Withdraw a procedure or function - -Remove a JAR and reload its namespace unregisters everything that JAR provided, so that it is no longer available to Cypher. -For example, to withdraw the `com.example.greet` function, remove the JAR, and reload the namespace: - -.Remove the JAR and add a new one that does not contain the `com.example.greet` function: -[source, bash] ----- -rm plugins/example-v2.jar -cp example-v3.jar plugins/ ----- +The procedure returns all the entry points in that namespace, including the new ones. -.Reload the namespace -[source, cypher] ----- -CALL dbms.reloadProcedures('com.example.*'); ----- -The query returns the remaining entry points in that namespace: - -[queryresult] ----- -name type -"com.example.version" "PROCEDURE" -"com.example.newProcedure" "PROCEDURE" -"com.example.joinAll" "AGGREGATION FUNCTION" ----- +Similarly, you can withdraw a procedure or function by removing its JAR file and reloading the namespace. === Confirm that no restart happened From 27cab6249fd0bff22bfc7dd41f6f830e10e2a50a Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Mon, 21 Sep 2026 21:05:34 +0100 Subject: [PATCH 09/11] apply suggestions from review --- modules/ROOT/pages/extending-neo4j/customized-code.adoc | 8 ++++---- .../user-defined-procedures-functions.adoc | 8 ++++---- modules/ROOT/partials/user-procedures-functions.adoc | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/ROOT/pages/extending-neo4j/customized-code.adoc b/modules/ROOT/pages/extending-neo4j/customized-code.adoc index a2d1220..43aa955 100644 --- a/modules/ROOT/pages/extending-neo4j/customized-code.adoc +++ b/modules/ROOT/pages/extending-neo4j/customized-code.adoc @@ -5,7 +5,7 @@ = Neo4j customized code Neo4j allows you to extend its functionality by writing your own code using _user-defined_ procedures and functions. -These mechanisms can be invoked directly from Cypher and are the preferred way of extending Neo4j. +They can be invoked directly from Cypher and are the preferred way of extending Neo4j. Examples of use cases for procedures and functions are: @@ -16,12 +16,12 @@ Examples of use cases for procedures and functions are: == Writing user-defined procedures and functions -To write these procedures and functions, you must use Java and compile them into JAR files. +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 and loaded during start-up. +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]. label:new[Introduced in 2026.09] label:enterprise-only[] You can use the built-in procedure `dbms.reloadProcedures()` to reload procedures and functions without restarting the DBMS. @@ -74,7 +74,7 @@ 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. +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. diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index ec8613b..1ae5a0b 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -19,7 +19,7 @@ For a comparison between user-defined procedures, functions, and aggregation fun [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. +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]] @@ -40,7 +40,7 @@ Finally, write a procedure, function, or aggregation function that passes the te The test dependencies include _Neo4j Harness_ and _JUnit_. These can be used to write integration tests for procedures and functions. -The tests should start a Neo4j instance, load the procedure, and execute queries against it. +The tests should start a Neo4j server, load the procedure, and execute queries against it. [.tabbed-example] ===== @@ -512,7 +512,7 @@ RETURN org.neo4j.examples.longestString(p.name) == Reload procedures and functions You can use the built-in procedure `dbms.reloadProcedures()` to reload the user supplied procedure or function JAR files from the _plugins_ directory into a running Neo4j, without restarting it. -This procedure is available only on `block` format. +This procedure is not supported on Windows. For more information about plugins, see link:https://neo4j.com/docs/operations-manual/current/configuration/plugins/[Operations Manual -> Configure plugins]. @@ -540,7 +540,7 @@ An empty result means nothing matched the pattern. The namespace argument genuinely limits the operation. It is not advisory. A plugin whose namespace does not match the pattern is left untouched, even if its JAR file changed on disk. -Three namespaces are permanently excluded and cannot be reloaded: `apoc.*`, `gds.*`, and `aura.*`. +Three namespaces `apoc.*`, `gds.*`, and `aura.*` are not reloadable by default. ==== === Example diff --git a/modules/ROOT/partials/user-procedures-functions.adoc b/modules/ROOT/partials/user-procedures-functions.adoc index 94682d6..9fb2904 100644 --- a/modules/ROOT/partials/user-procedures-functions.adoc +++ b/modules/ROOT/partials/user-procedures-functions.adoc @@ -4,5 +4,5 @@ Keep in mind that in a cluster deployment, you need to deploy the JAR files and Otherwise, the cluster members will be out of sync, leading to unexpected behavior. The reload process is isolated so that the operation only affects new transactions. -Currently running transactions continue to execute with a snapshot of the available procedures or functions at their respective initialization. +Already running transactions continue to execute with a snapshot of the available procedures or functions at their respective initialization. ==== \ No newline at end of file From 4bcbff57b03f3c755729b60f4ce7af43c9abcd0f Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Tue, 22 Sep 2026 10:02:32 +0100 Subject: [PATCH 10/11] Update the wording around neo4j harness and junit --- .../user-defined-procedures-functions.adoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index 1ae5a0b..2d76acd 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -38,15 +38,14 @@ Finally, write a procedure, function, or aggregation function that passes the te === Create integration tests -The test dependencies include _Neo4j Harness_ and _JUnit_. -These can be used to write integration tests for procedures and functions. +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 JUnit 5 for testing a procedure that returns relationship types found in the graph: +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] ---- @@ -125,7 +124,7 @@ public class GetRelationshipTypesTests { [.include-with-function-integration-tests] ====== -The following is an example template for testing a user-defined function that joins a list of strings: +The following is an example that uses Neo4j Harness for testing a user-defined function that joins a list of strings: [source, java] ---- @@ -181,7 +180,7 @@ public class JoinTest { [.include-with-aggregation-function-integration-tests] ====== -The following is an example template for testing a user-defined aggregation function that finds the longest string: +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] ---- From 19818fd40e699a6f91d95f38b1fd71d80c7dd792 Mon Sep 17 00:00:00 2001 From: Reneta Popova Date: Tue, 22 Sep 2026 11:02:33 +0100 Subject: [PATCH 11/11] Apply suggestion from @recrwplay Co-authored-by: Neil Dewhurst --- .../extending-neo4j/user-defined-procedures-functions.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc index 2d76acd..1b40796 100644 --- a/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc +++ b/modules/ROOT/pages/extending-neo4j/user-defined-procedures-functions.adoc @@ -539,7 +539,7 @@ An empty result means nothing matched the pattern. The namespace argument genuinely limits the operation. It is not advisory. A plugin whose namespace does not match the pattern is left untouched, even if its JAR file changed on disk. -Three namespaces `apoc.*`, `gds.*`, and `aura.*` are not reloadable by default. +Three namespaces `apoc.{asterisk}`, `gds.{asterisk}`, and `aura.{asterisk}` are not reloadable by default. ==== === Example