Stored Procedure Runtime and Transaction Script Extensions
This document describes the current procedure runtime in NornicDB, including:
- registry-backed
CALLexecution parity behavior - user-defined procedure DDL (
CREATE/DROP PROCEDURE) - startup precompile/loading of persisted procedures
- Nornic transaction script extensions (
BEGIN TRANSACTION,BEGINshorthand, conditional rollback scripts)
What Is Standard vs Extension
- Neo4j-compatible surface:
CALL ...SHOW PROCEDURESCALL dbms.procedures()- existing built-in/APOC-style procedure behavior and
YIELDsemantics
- Nornic extensions:
CREATE [OR REPLACE] PROCEDURE ... MODE ... AS ...DROP PROCEDURE ...- transaction script blocks using
BEGIN TRANSACTIONorBEGINwith inlineCOMMIT/ROLLBACK - conditional transaction script pattern:
CASE WHEN ... THEN ROLLBACK ELSE RETURN ... COMMIT
Runtime Contract
- A single registry-backed runtime resolves both built-in and user-defined procedures.
SHOW PROCEDURESandCALL dbms.procedures()are generated from the same runtime state.- Procedure metadata includes:
- canonical
name signaturedescriptionmode(READ,WRITE,SCHEMA,ADMIN,DBMS)worksOnSystem- argument cardinality (
minArgs,maxArgs)
- canonical
Procedure DDL Syntax (Nornic Extension)
CREATE OR REPLACE PROCEDURE nornic.touchUser($id, $ts)
MODE WRITE
AS
MATCH (u:User {id: $id})
SET u.last_seen = $ts
RETURN uDROP PROCEDURE nornic.touchUserDDL constraints
CREATE PROCEDUREandDROP PROCEDUREare rejected inside an active explicit transaction.- Procedure mode validation is enforced at create/compile time (for example,
MODE READcannot contain write operations). - Procedure modes are authorization contracts for Bolt callers:
WRITErequires thewriteentitlement,SCHEMArequiresschema, andADMINorDBMSrequiresadmin.READrequiresread. - Dynamic statements run by procedures inherit the invoking caller's effective entitlements. A query supplied through a parameter cannot bypass the caller's write, schema, or admin permissions.
APOC Custom Procedures (apoc.custom)
The APOC form of the same feature is portable to Neo4j with APOC. A statement installed under a name is called as custom.<name>:
CALL apoc.custom.installProcedure(
'reach(start :: INTEGER, depth = 1 :: INTEGER) :: (n :: INTEGER)',
'MATCH (s:Step {n: $start})-[:NEXT*1..10]->(t) WHERE t.n - s.n <= $depth RETURN t.n AS n',
'neo4j', 'read', 'steps reachable from a start');
CALL custom.reach(1) YIELD n RETURN n; // depth defaults to 1
CALL custom.reach($start, 3) YIELD n RETURN n;apoc.custom.installProcedure(signature, statement, databaseName = 'neo4j', mode = 'read', description = '')(APOC 5) andapoc.custom.declareProcedure(signature, statement, mode = 'read', description = '')(APOC 4) install it, replacing a procedure of the same name. The procedure is stored in the current database's procedure catalog, likeCREATE PROCEDURE, and is available again after a restart;databaseNameis accepted for compatibility.- The signature declares the arguments (
name :: TYPE, optionally with a default,name = value :: TYPE; optional arguments come last) and the output columns (:: (column :: TYPE, …), or:: VOID). The statement reads the arguments as parameters ($start) and returns the output columns. modeisread,write,schemaordbms; areadprocedure can't contain writes.apoc.custom.dropProcedure(name, databaseName = 'neo4j')removes it;apoc.custom.list()andapoc.custom.show()list the installed procedures (type, name, description, mode, statement, inputs, outputs, forceSingle).- A custom procedure is one for the whole server: whoever calls it, on any database, runs its statement with their own permissions. Installing, replacing and dropping one need the
adminentitlement, asCREATE/DROP PROCEDUREdo and as APOC 5 marksinstallProcedureanddropProcedure. A call of a custom procedure is never answered from the result cache, so the call after a replacement runs the new statement. - Custom functions (
apoc.custom.installFunction) are not provided.
Startup Precompile and Persistence
- User-defined procedures are persisted in the database metadata graph under catalog label
_ProcedureCatalog. - Catalog payload is msgpack-encoded and stored as a base64 string property.
- On executor startup:
- built-ins are registered
- persisted user-defined procedures are loaded
- each definition is compiled and registered before query execution
This provides fast runtime dispatch without first-call compilation overhead.
Transaction Script Extensions (Nornic Extension)
Explicit form
BEGIN TRANSACTION
CALL nornic.touchUser('u-10', datetime())
YIELD u
RETURN u.id, u.last_seen
COMMITShorthand form (equivalent)
BEGIN
CALL nornic.touchUser('u-10', datetime())
YIELD u
RETURN u.id, u.last_seen
COMMITConditional rollback script
BEGIN TRANSACTION
CALL nornic.touchUser('u-10', datetime())
YIELD u
CASE
WHEN u.age < 18 THEN ROLLBACK
ELSE
RETURN u.id, u.last_seen
COMMITWhitespace and casing variations are accepted for these script forms.
Compatibility Behavior and Errors
- Unknown procedure:
unknown procedure: <name> (try SHOW PROCEDURES for available procedures)
- Argument arity mismatch:
- minimum:
procedure <name> requires at least N arguments, got M - maximum:
procedure <name> accepts at most N arguments, got M
- minimum:
- Unknown YIELD column:
unknown YIELD column: <column>
Built-in Procedure Surface
Built-ins are registered in pkg/cypher/procedure_registry_builtin.go, including:
- core schema/info (
db.labels,db.relationshipTypes,db.propertyKeys,db.info,db.ping) - fulltext/vector (
db.index.fulltext.*,db.index.vector.*,db.create.set*VectorProperty) - DBMS (
dbms.components,dbms.info,dbms.listConfig,dbms.clientConfig,dbms.listConnections,dbms.procedures,dbms.functions) - index/stats/ops (
db.await*,db.resampleIndex,db.stats.*,db.clearQueryCaches,tx.setMetaData) - NornicDB (
nornicdb.version,nornicdb.stats,kp.decayInfo)
Plugin-based User Procedures
Procedure plugins remain supported via the plugin system:
- plugin can expose
Procedures() map[string]...in addition to function entries - procedure handlers are registered in the same global runtime registry
- plugin procedures are visible in both
SHOW PROCEDURESandCALL dbms.procedures()
Supported plugin handler shapes:
cypher.ProcedureHandler, or its equivalent Go function type:func(context.Context, *cypher.StorageExecutor, string, []interface{}) (*cypher.ExecuteResult, error)- The existing
func(context.Context, string, []interface{}) (*cypher.ExecuteResult, error)ABI remains supported.
The canonical ABI receives the calling executor and statement context, rather than a captured global storage backend. For example, a plugin's Procedures() entry can declare a read procedure using:
Handler: cypher.ProcedureHandler(func(ctx context.Context, exec *cypher.StorageExecutor, query string, args []interface{}) (*cypher.ExecuteResult, error) {
return &cypher.ExecuteResult{
Columns: []string{"value"},
Rows: [][]interface{}{{args[0]}},
}, nil
}),
Mode: "READ",
MinArgs: 1,
MaxArgs: 1,Names preserve their registered case. Declare WRITE, SCHEMA, ADMIN, or DBMS when appropriate; an unknown mode or unsupported/nil handler fails the plugin load. Every procedure declaration is validated before any procedure from that plugin is published. Arity, authorization, database selection, and YIELD continue through the shared procedure registry.
CALL apoc.cypher.doIt(statement, params) YIELD value is a built-in WRITE procedure. It runs parameterized data statements through the calling transaction, and returns each inner row as a value map. For example:
CALL apoc.cypher.doIt('CREATE (n:Person {name:$name}) RETURN n.name AS name',
{name:'Ada'})
YIELD value RETURN value.nameRolling back the caller rolls back the inner write. The same statement policy as the other dynamic procedures rejects administrative/internal statements. This is the two-argument APOC procedure contract; it does not promote scalar functions such as apoc.math.sigmoid to procedures.
Tests Covering This Feature
pkg/cypher/procedure_ddl_test.gopkg/cypher/transaction_script_test.go- existing procedure compatibility tests under
pkg/cypher/*procedures*_test.go
Migration Guidance
- Existing
CALLusage requires no query changes. - Use procedure DDL for Nornic-native persisted procedures.
- For maximal compatibility with Neo4j clients, keep application logic based on
CALLand avoid relying on Nornic-only DDL/transaction-script syntax in portable query sets.