Expand description
Implementation of the apoc.trigger.* procedure family.
Triggers are Cypher snippets that fire on commit boundaries. Each trigger receives the transaction’s diff as a set of well-known parameters that mirror Neo4j APOC’s surface:
$createdNodes/$createdRelationships— newly-created elements (id wasn’t in the store pre-tx)$deletedNodes/$deletedRelationships— deleted elements, captured pre-tx so the trigger sees their final shape before deletion$assignedNodeProperties/$assignedRelationshipProperties— property changes on existing elements, list of{key, old, new, node|relationship}$removedNodeProperties/$removedRelationshipProperties— property removals, list of{key, old, node|relationship}$assignedLabels/$removedLabels— label changes on existing nodes, list of{label, node}
All four phases work — before (pre-commit; errors abort
the tx; trigger writes commit atomically with the user’s
writes), after (post-commit, sync; recurses through the
cluster commit path so trigger writes replicate),
afterAsync (post-commit, spawned in a background tokio
task so it doesn’t block the response), and rollback
(fires when the commit fails, including before-trigger
aborts).
§Procedure surface
Five procedures: install / drop / list / start /
stop. Install/drop emit GraphCommand::InstallTrigger /
DropTrigger so the cluster commit machinery replicates
the registry across every peer (Raft log + routing-mode
DDL fan-out). Firing is leader/coordinator-only — exactly
one peer per logical commit fires.
Recursion safety has two layers: the thread-local
SUPPRESSING flag catches the sync-call path, and the
from_trigger: bool parameter on commit_buffered_commands
catches the async-recursive cluster commit path. Either
alone is sufficient; both together close every observed
self-firing scenario.
§Multi-db
databaseName is accepted on every procedure but ignored —
Mesh is single-db today. Callers can pass any string.
Structs§
- Label
Change - One label-change record (assignment or removal).
elementis always aValue::Nodesince labels are node-scope. - Property
Change - One property-change record (assignment or removal) emitted
per (element, key) tuple touched by the tx. The
elementfield is aValue::NodeorValue::Edgedepending on scope so the trigger body can inspect the wider shape. - Trigger
Diff - Per-tx diff handed to a trigger as
$created*/$deleted*/$assigned*/$removed*params. Computed inmeshdb-rpc(whereGraphCommandlives) and passed in to the firing helpers.into_param_mapshapes the lists into Neo4j APOC’s expected param surface. - Trigger
Registry - In-memory cache + persistent backing of the trigger
registry. Mutating operations (install / drop) write
through to storage; reads served from the cache. The
Arc<dyn StorageEngine>is held internally so the procedure call surface only needs the registry handle — not a separate store reference — to mutate the persistent state. - Trigger
Spec - Persisted trigger spec. Roundtrips through
serde_jsonfor storage in thetrigger_metacolumn family. Thenamefield duplicates the storage key — kept on the value too so list / show output is self-describing.
Functions§
- drop_
call apoc.trigger.drop(databaseName, name). Same write-path shape asinstall_call— the drop rides through the cluster commit so every peer’s storage drops the entry. The yieldedremovedcolumn is alwaystruein V2 because the actual apply happens on the leader/applier path; the install/drop procedure itself only buffers the command.- fire_
after_ triggers - Fire every registered after-phase trigger. Backwards-compat
shim for callers that haven’t migrated to
fire_phase_triggersyet. - fire_
before_ triggers - Fire every registered before-phase trigger against the pre-commit diff. Returns Ok if every trigger ran cleanly, Err with the offending message if one failed — propagated to abort the originator’s commit. Trigger writes are buffered into the supplied writer so the caller can merge them into the prepared command batch.
- fire_
phase_ triggers - Fire every registered trigger of
phaseagainst the given diff. Errors are logged and swallowed — the original tx has already committed (forafter/afterAsync/rollback) or already aborted (forrollback), so a failing trigger can’t undo anything. Used as the workhorse for the three non-blocking phases;beforehas its own entry point with abort semantics. - install_
call apoc.trigger.install(databaseName, name, statement, selector, config).databaseNameis accepted but ignored — Mesh is single-database today.selectorandconfigare inspected for aphase(defaults to “after”) andparams(defaults to empty); other keys are ignored for forward-compat.- is_
suppressed - Whether the calling thread is currently inside a trigger invocation. The commit hook calls this before firing.
- list_
call apoc.trigger.list().- now_ms
- Helper: wall-clock millis since the UNIX epoch. Used to
stamp
installed_at_mson a fresh trigger spec. - start_
call apoc.trigger.start(databaseName, name)— un-pause a previously stopped trigger. Looks up the existing spec through the registry, setspaused = false, and re-emits the spec via the writer so the cluster commit replicates the unpause. Errors if the trigger doesn’t exist.- stop_
call apoc.trigger.stop(databaseName, name)— pause a registered trigger. Mirrorsstart_call; setspaused = true.- with_
suppression - Run
bodywith the suppression flag set. The flag resets to the prior value on drop — even on panic — so a panicking trigger doesn’t leave the calling thread permanently suppressed.