Procedures (CALL)
patinaDB has an extensible procedure framework. Procedures are invoked with
CALL, declare a fixed set of named yield columns, and stream rows back
into the query like any other operator. A trailing CALL with no RETURN
implicitly returns all yielded columns.
CALL patinadb.engrams() YIELD id, message, timestamp
RETURN id, message ORDER BY timestamp DESC
You can register your own read-only WASM procedures — see WASM Procedures below. Everything under Built-in procedures is a built-in, always-available procedure; user-defined ones are called as
CALL wasm.<name>(...)after aCREATE FUNCTION … LANGUAGE wasmDDL statement.
Bulk write clause: CALL { … } IN TRANSACTIONS
A sibling of CALL worth knowing alongside the procedures below: wrapping a
mutating subquery in CALL { … } IN TRANSACTIONS [OF n ROW[S]] [ON ERROR {CONTINUE|BREAK|FAIL}] chunks its writes into many small, independently
committed transactions instead of one giant one — the batched-ingest answer to
a LOAD CSV … CREATE memory cliff.
LOAD CSV WITH HEADERS FROM 'file:///data/accounts.csv' AS row
CALL {
WITH row
CREATE (:Account {id: toInteger(row.id), name: row.name})
} IN TRANSACTIONS OF 1000 ROWS
Each chunk is its own engram and, on a cluster, its own Raft
entry (⌈rows/n⌉ bounded entries, not one unbounded one); a later chunk sees
the writes of earlier ones (read-your-writes). ON ERROR FAIL (default) aborts
the whole statement on a failing chunk; CONTINUE skips it and keeps going;
BREAK stops after it, keeping earlier chunks. It must be the query’s final
clause, and cannot run inside an explicit Bolt BEGIN … COMMIT. See
Bulk Loading & Import.
User-defined procedures: WASM functions
Beyond the built-ins below, you can register your own read-only procedures compiled to WebAssembly:
CREATE FUNCTION fib (1) LANGUAGE wasm FROM 'file:///opt/patinadb/fib.wasm' EXPORT 'fib';
CALL wasm.fib(10) YIELD result RETURN result; -- 55
A WASM function runs deterministically and sandboxed (no filesystem, network,
clock, or randomness), which is what makes it safe to replicate across a
cluster — CREATE/DROP FUNCTION are DDL that replicate by value (compiled
module bytes, not the source path) and ride Raft snapshots. See
WASM Procedures for the full registration grammar, the
ABI, and the replication/RBAC model.
Built-in procedures
Index readiness
Since 0.16.0, CALL db.awaitIndexes() and CALL db.awaitIndexes(300) are
supported. The optional timeout is a non-negative integer number of seconds,
defaulting to 300. 0 checks readiness once without waiting. Negative values,
null, non-integers and extra arguments are rejected.
The procedure completes without output columns, not with a boolean true;
there is no YIELD ready. It preserves incoming rows when followed by another
query clause. Ordinary index DDL completes synchronously, and subsequent writes
maintain indexes synchronously, so a completed build normally needs no further
wait. The procedure checks the local persistent deferred-index-build marker;
while it is set, it polls until cleared or the timeout expires. Metadata read
errors, query deadlines and shutdown cancellation propagate rather than being
reported as success. It does not rebuild indexes itself.
This is a local readiness check for the current synchronous build lifecycle, not a validation scan of every index, a barrier for a concurrently starting DDL, or a readiness guarantee for a raw embedded store being modified without the normal query/write coordination. Future asynchronous index builders must join this readiness contract before being exposed to queries.
It also does not wait for Raft replication or history backfill: use bookmarks
for causal reads on another replica. db.awaitIndex(name) is not implemented.
History & diff
| Procedure | Yields | Purpose |
|---|---|---|
patinadb.engrams() | id, message, timestamp, … | List committed engrams (newest first). |
patinadb.diff(id) | per-operation rows | Git-style view of a single engram. |
patinadb.diffRange(from, to) | structural change rows | Move-aware structural diff between two points. |
Schema introspection
| Procedure | Yields | Description |
|---|---|---|
db.labels() | label | Every distinct node label in the graph (primary and secondary). |
db.relationshipTypes() | relationshipType | Every distinct relationship type. |
db.propertyKeys() | propertyKey | Every distinct property key across nodes and relationships. |
These are read-only and run over every surface (REST, Bolt) via the same
CALL path — not a driver/browser-only shim.
| Procedure | Yields | Description |
|---|---|---|
patinadb.ontology([options]) | view | The database’s ontology as one JSON-LD document (concepts, label combinations, relationship shapes, scoped property statistics, declared constraints and indexes). Off unless PATINADB_ONTOLOGY_DISCOVERY is enabled; standalone, current-state and unrestricted-reader only. See Database Ontology View. |
Vector / embedding search
Registered under two namespaces, like full-text search:
| Procedure | Alias | Yields | Description |
|---|---|---|---|
db.index.vector.queryNodes(name, k, queryVector) | patinadb.vector.queryNodes | node, score | k-nearest-neighbour search over a vector index (IVF-Flat ANN, cosine or Euclidean similarity). Yields real nodes. |
See Vector Search for index creation, the similarity functions, and the ANN accuracy trade-off.
Full-text search
Registered under two namespaces — the Neo4j-compatible name and a patinaDB alias — so existing Neo4j tooling works unchanged:
| Procedure | Alias | Yields |
|---|---|---|
db.index.fulltext.queryNodes(name, query) | patinadb.fulltext.queryNodes | node, score |
db.index.fulltext.queryRelationships(name, query) | patinadb.fulltext.queryRelationships | relationship, score |
queryNodes yields real nodes (not just property maps), so they render as
graph nodes in the Neo4j Browser and hydrate correctly through Bolt drivers:
CALL db.index.fulltext.queryNodes('docs', 'graph AND database~')
YIELD node, score
RETURN node.title, score ORDER BY score DESC LIMIT 10
See Full-Text Search for index creation and the query syntax.
Graph algorithms
A small set of read-only graph algorithms run over an in-memory snapshot of the
current graph. Each takes an optional node-label and relationship-type
projection (pass null to include everything) and yields real nodes plus a
per-node result. gds.*.stream aliases are provided for tooling discoverability
— note they use patinaDB’s simplified (label?, relType?, config?) signature,
not Neo4j GDS’s named-graph-projection API.
| Procedure | Alias | Yields | Purpose |
|---|---|---|---|
patinadb.algo.pageRank(label?, relType?, config?) | gds.pageRank.stream | node, score | Iterative PageRank. config = {iterations: 20, dampingFactor: 0.85}. Scores sum to ≈ 1.0. |
patinadb.algo.wcc(label?, relType?) | gds.wcc.stream | node, componentId | Weakly-connected components. componentId is the smallest node UUID in the component (stable across runs). |
patinadb.algo.degree(label?, relType?, config?) | gds.degree.stream | node, score | Degree centrality. config = {direction: 'both'} (in/out/both); a self-loop counts twice for both. |
patinadb.algo.betweenness(label?, relType?) | gds.betweenness.stream | node, score | Exact betweenness centrality (Brandes’ algorithm) over directed shortest paths. A path’s interior nodes score highest; O(V·E). |
patinadb.algo.closeness(label?, relType?) | gds.closeness.stream | node, score | Closeness centrality over directed shortest paths, Wasserman–Faust normalized ((k/(N-1))·(k/Σd)) so disconnected graphs are well-defined; unreachable-only nodes score 0. O(V·E). |
patinadb.algo.triangleCount(label?, relType?) | gds.triangleCount.stream | node, triangles | Per-node triangle count over the undirected simple graph (self-loops/parallel edges collapsed). Global count = Σ triangles / 3. |
patinadb.algo.labelPropagation(label?, relType?, config?) | gds.labelPropagation.stream | node, communityId | Deterministic synchronous label-propagation community detection. config = {maxIterations: 10}. communityId = the smallest node UUID in the community (stable across runs, like WCC). |
CALL patinadb.algo.pageRank('Page', 'LINKS', {iterations: 30})
YIELD node, score
RETURN node.title, score ORDER BY score DESC LIMIT 10
The algorithms materialize the projected graph in RAM (fine up to ~10M edges on one box), run in memory, and yield without mutating anything — so they need no replication and can run on any cluster node. Because PageRank is normalized to sum to 1, its ranking matches Neo4j but its magnitudes differ.
betweenness and closeness follow out-edges (directed shortest paths),
matching PageRank/degree; triangleCount and labelPropagation treat edges as
undirected. Every algorithm is deterministic — the crux for
labelPropagation, which is normally randomized: patinaDB uses a fixed
synchronous update with a lowest-label tie-break and a hard iteration cap (iterations/maxIterations are bounded at 1000 so a read call cannot become a
compute DoS), so every cluster node computes the same communities. Because
betweenness/closeness are O(V·E), they suit small-to-medium projections; the
in-memory-only scale ceiling applies. APOC utility procedures and
weighted/personalized PageRank variants are planned follow-ons.
What “APOC does not exist here” looks like. patinaDB ships no APOC library. Calling one as a procedure —
CALL apoc.help()— fails cleanly with an unknown procedure error. But using an APOC function inside an expression (RETURN apoc.text.join(...),WHERE apoc.coll.contains(...)) currently surfaces a raw parser dump listing the tokens the grammar expected at that point (expected EOI, AND, OR, XOR, …) instead of a dedicated “no such function” error. Both mean the same thing: the function is not available. Check the procedure/function tables in this chapter and rewrite the query against what patinaDB actually ships.
Statistics catalog
| Procedure | Yields | Description |
|---|---|---|
patinadb.stats(label?) | label, property, count, ndv, min, max | The query planner’s statistics catalog for a label (or all labels). One summary row per label (property = null, count = vertex count) plus one row per property with its present count, distinct-value count (NDV), and min/max. |
CALL patinadb.stats('Person')
YIELD label, property, count, ndv, min, max
RETURN property, count, ndv, min, max ORDER BY ndv DESC
The catalog feeds cost-based entry-point selection: it lets the planner estimate
how selective a WHERE/pattern filter is (e.g. a rare property value vs a common
one) and drive a query from the cheapest starting point. It also feeds cost-based
join ordering: a multi-pattern MATCH (a)…, (b)…, (c)… is reordered so the most
selective pattern drives the join and intermediate results stay small, instead of
joining patterns in written order. Statistics are local performance hints —
they change only which plan runs, never the result (a different join order returns
the same rows) — so they are computed lazily per node, cached, and never
replicated.
Index advisor
| Procedure | Yields | Description |
|---|---|---|
patinadb.advisor(query) | suggestion, reason, current_plan | Analyze a query string (parsed + planned exactly as EXPLAIN) and suggest an index that would flip it to a faster physical plan. Today it surfaces missing edge-sorted indexes for a traversal + ORDER BY target.prop LIMIT k shape; empty result means nothing to advise. |
CALL patinadb.advisor(
'MATCH (c:Company {name:"Acme"})<-[:WORKS_AT]-(p:Person) RETURN p ORDER BY p.age DESC LIMIT 3')
YIELD suggestion, reason, current_plan RETURN suggestion
-- suggestion: 'CREATE EDGE SORTED INDEX FOR (m:Person)<-[:WORKS_AT]-() ON m.age'
EXPLAIN prints the same advice as an Advice: footer. See
Query Planning & Performance and
Edge-Sorted Indexes.
Cache observability
| Procedure | Yields | Description |
|---|---|---|
patinadb.cache.stats() | scope, kind, bytes, entries, hit_rate, generation | The multi-level, RAM-budget-governed cache. One row per (level, scope): scope is the hot database / collection (label) / query shape (db:1/label:Ticket), kind is the cache level, and hit_rate is hits / (hits + misses). generation is reserved (yielded as null today). |
CALL patinadb.cache.stats()
YIELD scope, kind, bytes, entries, hit_rate
RETURN scope, kind, bytes, hit_rate ORDER BY bytes DESC
Read-only and node-local. When caching is disabled (the default) it yields zero
rows (no error). The same view is available over HTTP as GET /mgmt/cache and as
Prometheus gauges on /metrics. For every metric and a tuning playbook see
Cache Observability & Tuning.
CSV export
Write graph data out as neo4j-admin-style CSV — the same format
GET /mgmt/export uses, so an exported file loads straight back with
LOAD CSV/bulk upsert (:ID(uuid) reproduces vertex UUIDs, byte-identical).
| Procedure | Alias | Yields | Description |
|---|---|---|---|
patinadb.export.csv(label, path [, config]) | — | file, kind, rows | Export one label’s vertices to CSV. path ending .csv writes a single node file; otherwise path is a directory and gets nodes_<Label>.csv plus that label’s outgoing relationships as rels_<TYPE>.csv (one per edge type). config = {rels: false} skips relationships. |
patinadb.export.query(query, file) | apoc.export.csv.query | file, kind, rows | Export an arbitrary query result (selective export). CSV columns are the RETURN names. |
-- Whole label + its relationships into a directory:
CALL patinadb.export.csv('Person', '/data/exports/people')
YIELD file, kind, rows RETURN file, kind, rows
-- A selective slice via a query:
CALL patinadb.export.query(
'MATCH (p:Person) WHERE p.age > 30 RETURN p.name AS name, p.age AS age',
'/data/exports/over30.csv')
Node files carry an :ID(uuid) column, one typed column per property
(name:int / :float / :boolean / :date / :localdatetime, or a bare
string column) and a :LABEL column; an absent property is a blank cell.
Relationship files carry :START_ID(uuid) / :END_ID(uuid) / :TYPE + typed
property columns. Query-export entity cells (nodes/relationships) are rendered
as their UUID / type — use patinadb.export.csv for a structural round-trip.
Export streams row-by-row (bounded memory) over the current committed graph.
Security note. These procedures write a file on the machine running the query — the same file-write consideration as
LOAD CSVreading afile://URL. On the server both are deny-by-default: an export writes only to directories whitelisted with--allow-export-dir, and any file-I/O query is raised to require the global Admin role (details). The procedures are CSV-only; Parquet/Arrow export goes throughGET /mgmt/export?format=parquet|arrow(see REST API).
Availability across interfaces
Procedures work identically over REST and Bolt (including the Neo4j
Browser) — the same CALL dispatch serves both. Full-text and vector query
procedures read from the local node’s copy of the index, so reads are served
without a round trip to the leader.
Degree statistics (RFC 0042 A1)
CALL patinadb.stats.degrees(edgeBudget?) explicitly refreshes the local degree
catalog. It yields edge_type, direction (out/in), anchor_label,
other_label, edges, anchors (nonzero degree), population (including
zero-degree vertices), p50, p99, max, heavy_hitters, provenance,
sample_edges and sample_anchors. Both endpoint labels include secondary labels.
The cost model (PATINADB_COST_MODEL, default join since 2026-10-17, #1442)
uses fresh degree groups and requests a bounded background sample on a miss, so
this call is not required; use it for an exact catalog or a warm-up after bulk
loads. With PATINADB_COST_MODEL=off the planner does not consume these
statistics. See configuration.
The positive integer budget counts directional adjacency entries across both
trees. It defaults to PATINADB_FANOUT_RECOMP_MAX_EDGES. Above the budget the
scan samples 256 uniformly spaced UUID intervals, assuming uniform UUIDs;
structured/nonuniform IDs can bias estimates. provenance is exact or sampled.
Counts and histogram buckets are extrapolated for samples; max is the observed
maximum, and each reported heavy hitter has its exact observed degree. The
SpaceSaving list is a candidate summary, not a guarantee of the global top 16.
Percentiles are log2-bucket upper bounds, including zero-degree anchors.
Groups absent from the sample are absent from the output, not proven empty.
An exhausted edge/group/memory budget or deadline fails the refresh without
publishing a partial catalog. The explicit call can scan vertex membership for
population counts. Its cache occupies a separate bounded, governor-visible
stats.degree region; cache-disabled calls still return their freshly computed
rows. Planner paths never run this explicit refresh. With
PATINADB_COST_MODEL set to entry or join (the default), a planner miss can
request the separate bounded background sample described in configuration.
patinadb.stats(label?) now includes secondary-label holders in its summary and
property rows. Existing primary-label output columns remain unchanged.