Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ProcedureYieldsPurpose
patinadb.engrams()id, message, timestamp, …List committed engrams (newest first).
patinadb.diff(id)per-operation rowsGit-style view of a single engram.
patinadb.diffRange(from, to)structural change rowsMove-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.

Registered under two namespaces — the Neo4j-compatible name and a patinaDB alias — so existing Neo4j tooling works unchanged:

ProcedureAliasYields
db.index.fulltext.queryNodes(name, query)patinadb.fulltext.queryNodesnode, score
db.index.fulltext.queryRelationships(name, query)patinadb.fulltext.queryRelationshipsrelationship, 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.

ProcedureAliasYieldsPurpose
patinadb.algo.pageRank(label?, relType?, config?)gds.pageRank.streamnode, scoreIterative PageRank. config = {iterations: 20, dampingFactor: 0.85}. Scores sum to ≈ 1.0.
patinadb.algo.wcc(label?, relType?)gds.wcc.streamnode, componentIdWeakly-connected components. componentId is the smallest node UUID in the component (stable across runs).
patinadb.algo.degree(label?, relType?, config?)gds.degree.streamnode, scoreDegree centrality. config = {direction: 'both'} (in/out/both); a self-loop counts twice for both.
patinadb.algo.betweenness(label?, relType?)gds.betweenness.streamnode, scoreExact 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.streamnode, scoreCloseness 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.streamnode, trianglesPer-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.streamnode, communityIdDeterministic 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

ProcedureYieldsDescription
patinadb.stats(label?)label, property, count, ndv, min, maxThe 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

ProcedureYieldsDescription
patinadb.advisor(query)suggestion, reason, current_planAnalyze 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

ProcedureYieldsDescription
patinadb.cache.stats()scope, kind, bytes, entries, hit_rate, generationThe 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).

ProcedureAliasYieldsDescription
patinadb.export.csv(label, path [, config])file, kind, rowsExport 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.queryfile, kind, rowsExport 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 CSV reading a file:// 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’s GET /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.