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
User-defined procedures are not supported — you cannot register your own from Cypher. The framework is an internal extension point; the built-ins below are what’s available.
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. |
These mirror the CLI subcommands (log, diff, diff-range) and the MCP
tools (engrams, diff, diff_range). See Engrams
and Diffs.
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 the CLI
import / export commands use, so an exported file loads straight back with
patinadb import (: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). Embedded and CLI use installs no sandbox and is unaffected. The procedures are CSV-only; Parquet/Arrow export goes through the CLI (patinadb export --format parquet) or the server’sGET /mgmt/export.
Availability across interfaces
Procedures work everywhere the engine runs: embedded, CLI, MCP, REST, and Bolt (including the Neo4j Browser). In the server, full-text query procedures read from the local node’s copy of the index, so reads are served without a round trip to the leader.