Custom, Cypher Based Procedures and Functions

I wanted for a long time to be able to register Cypher statements as proper procedures and functions, so that they become callable in a standalone way.

You can achieve that with the apoc.custom.declareProcedure and apoc.custom.declareFunction procedure calls. Those register a given Cypher statement, prefixed with the custom.* namespace, overriding potentially existing ones, so you can redefine them as needed.

APOC provides also apoc.custom.asFunction and apoc.custom.asProcedure procedures, but they have been deprecated in favor of `apoc.custom.declare*`s.

The first parameter of the apoc.custom.declareProcedure and apoc.custom.declareFunction procedures, is the signature of the procedure/function you want to create. This looks similar to the signature results returned by the SHOW PROCEDURES YIELD signature and SHOW FUNCTIONS YIELD signature cypher commands, that is: - for a procedure: nameProcedure(firstParam = defaultValue :: typeParam , secondParam = defaultValue :: typeParam, …​.) :: (firstResult :: typeResult, secondResult :: typeResult, …​ ) - for a function: nameFunction(firstParam = defaultValue :: typeParam , secondParam = defaultValue :: typeParam, …​.) :: typeResult

Note that, for both procedures and functions, the = defaultValue are optionals.

If you want to create a procedure/function with a default String parameter with whitespaces, quotes (for example: "my text ' with ' quote") or "null" (as a string) you have to quote the result, e.g CALL apoc.custom.declareProcedure("procWithNullString(param='null'::STRING)::(output::STRING)", 'return $param as output')

Custom Procedures with apoc.custom.asProcedure

Given statement will be registered as a procedure, the results will be turned into a stream of records.

Table 1. Parameters
name default description

name

none

dot-separated name, will be prefixed with custom

statement

none

cypher statement to run, can use $parameters

mode

read

execution mode of the procedure: read, write, or schema

outputs

[["row","MAP"]]

List of pairs of name-type to be used as output columns, need to be in-order with the cypher statement, the default is a special case, that will collect all columns of the statement result into a map

inputs

[["params","MAP","{}"]]

Pairs or triples of name-type-default, to be used as input parameters. The default just takes an optional map, otherwise they will become proper paramters in order

description

""

A general description about the business rules implemented into the procedure

The type names are what you would expect and see in outputs of dbms.procedures or apoc.help just without the ?. The default values are parsed as JSON.

Type Names

The typeParam and typeResult in the signature parameter are what you would expect to see in outputs of SHOW PROCEDURES, SHOW FUNCTIONS or CALL apoc.help(''), just without the final ?. The following values are supported: * FLOAT, DOUBLE, INT, INTEGER, NUMBER, LONG * TEXT, STRING * BOOL, BOOLEAN * POINT, GEO, GEOMETRY * DATE, DATETIME, LOCALDATETIME, TIME, LOCALTIME, DURATION * NODE, REL, RELATIONSHIP, EDGE, PATH * MAP * MAPRESULT (valid for declareFunction, does not wrap the result in a further map. see here: [map-vs-map-result]) * LIST TYPE, LIST OF TYPE (where TYPE can be one of the previous values) * ANY

If you override procedures or functions you might need to call call db.clearQueryCaches() as lookups to internal id’s are kept in compiled query plans.

Custom Procedures with apoc.custom.declareProcedure

Here is a simple example:

CALL apoc.custom.declareProcedure('answerInteger() :: (row::INT)', 'RETURN 42 as answer')

This registers the statement as procedure custom.answer that you then can call.

CALL custom.answerInteger
Table 2. Results
answer

42

Or you can also write in this way:

CALL apoc.custom.declareProcedure('answer() :: (row::MAP)', 'RETURN 42 as answer')

In this case the result is wrapped in a stream of maps called row. Therefore, you can do:

CALL custom.answer() YIELD row
RETURN row.answer
Table 3. Results
answer

42

which is equivalent to deprecated one:

CALL apoc.custom.asProcedure('answer','RETURN 42 as answer')

We can create the function custom.powers that returns a stream of the powers of the first parameter, up to and including the power provided by the second parameter:

CALL apoc.custom.declareProcedure(
  'powers(input::INT, power::INT) :: (answer::INT)',
  'UNWIND range(0, $power) AS power
   RETURN $input ^ power AS answer'
);
Procedure, input and output names must have at least 2 characters.

We can use this function, to return 4°, 4¹, 4², and 4³, as shown in the query below:

call custom.powers(4,3);
Table 4. Results
answer

1.0

4.0

16.0

64.0

Furthermore, we can pass as the 3rd parameter a string to specify the procedure mode (default "WRITE"). It can be: - "READ" - if the procedure will only perform read operations against the graph - "WRITE" - if it may perform both read and write operations against the graph - "SCHEMA" - if it will perform operations against the schema - "DBMS" - if it will perform system operations - i.e. not against the graph

Moreover, we can pass a description parameter as the 4th parameter, which will be returned by the call apoc.custom.list and SHOW PROCEDURES.

Custom Functions with apoc.custom.declareFunction

Here is a simple example:

CALL apoc.custom.declareFunction('answerFun() :: INT', 'RETURN 42 as answer')

This registers the statement as procedure custom.answer that you then can call.

RETURN custom.answerFun()
Table 5. Results
answer

42

Or you can also write in this way:

CALL apoc.custom.declareFunction('answerFunMap() :: MAP', 'RETURN 42 as answer')

In this case the result is wrapped in a map of key answer and value 42. Therefore, you can do:

RETURN custom.answerFunMap() AS row
Table 6. Results
row

{ "answer": 42 }

In case you return a map, you can choose to wrap the result in another map with key the name of the result, and value the value obtained. For example, declaring this function:

CALL apoc.custom.declareFunction('answerFunMap(value:: INT) :: MAP', 'RETURN {a: $value}')

you can execute:

return custom.answerFunMap(2) as row
Table 7. Results
row
{
    "{a: $value}":
    {"a": 2}
  }

Note that the {"a": 2} is wrapped by a map with key {a: $value}", that is the column name which would be returned from the RETURN {a: $value}

Instead, using MAPRESULT:

CALL apoc.custom.declareFunction('answerFunMapResult(value:: INT) :: MAPRESULT', 'RETURN $value')

you can execute:

RETURN custom.answerFunMapResult(2) AS row
Table 8. Results
row
{"a": 2}

which is equivalent to deprecated one:

CALL apoc.custom.asFunction('answer','RETURN 42 as answer')

We can create the function custom.powers that returns a stream of the powers of the first parameter, up to and including the power provided by the second parameter:

CALL apoc.custom.declareProcedure(
  'powers(input::INT, power::INT) :: (answer::INT)',
  'UNWIND range(0, $power) AS power
   RETURN $input ^ power AS answer'
);

We can create the function custom.double, that doubles the provided value, by running the following function:

CALL apoc.custom.declareFunction(
  'double(input::INT) :: INT',
  'RETURN $input*2 as answer'
);
Function, input and output names must have at least 2 characters.

We can use this function, as shown in the query below:

RETURN custom.double(83) AS value;
Table 9. Results
value

166

Furthermore, we can pass as a 3rd parameter a boolean (with default false) which, if true, in case the function returns a list of a single element, it will return only the single element itself and not the list.

For example:

CALL apoc.custom.declareFunction('forceSingleTrue(input::ANY) :: LIST OF INT',
  'RETURN 1',
  true
);
Table 10. Results
value

1

otherwise with false the result will be a singleton list:

CALL apoc.custom.declareFunction('forceSingleFalse(input::ANY) :: LIST OF INT',
  'RETURN 1',
  false
);
Table 11. Results
value

[1]

Moreover, we can pass a description parameter as the 4th parameter, which will be returned by the call apoc.custom.list and SHOW FUNCTIONS.

List of registered procedures/function with apoc.custom.list

The procedure apoc.custom.list provide a list of all registered procedures/function via apoc.custom.declareProcedure and apoc.custom.declareFunction, apoc.custom.asProcedure and apoc.custom.asFunction.

Given the this call:

CALL apoc.custom.list

The output will look like the following table:

type name description mode statement inputs outputs forceSingle

"function"

"answer"

<null>

<null>

"RETURN $input as answer"

[["input","number"]]

"long"

false

"procedure"

"answer"

"Procedure that answer to the Ultimate Question of Life, the Universe, and Everything"

"read"

"RETURN $input as answer"

[["input","int","42"]]

[["answer","number"]]

<null>

Remove a procedure apoc.custom.removeProcedure

The procedure apoc.custom.removeProcedure allows to delete the targeted custom procedure.

Given the this call:

CALL apoc.custom.removeProcedure(<name>)

Fields:

argument description

name

the procedure name

Remove a procedure apoc.custom.removeFunction

The procedure apoc.custom.removeFunction allows to delete the targeted custom function.

Given the this call:

CALL apoc.custom.removeFunction(<name>)

Fields:

argument description

name

the function name

How to manage procedure/function replication in a Causal Cluster

In order to replicate the procedure/function in a cluster environment you can tune the following parameters:

name type description

apoc.custom.procedures.refresh

long (default 60000)

the refresh time that allows replicating the procedure/function changes to each cluster member

Export metadata

To import custom procedures in another database (for example after a ./neo4j-admin backup and /neo4j-admin restore), please see the apoc.systemdb.export.metadata procedure.