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
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.
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.
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.