SQL API reference
Every capability is reachable from plain SQL. The Studio and any agent integration are conveniences over this surface, not privileged paths.
Generate this list yourself:
SELECT p.proname || '(' || pg_get_function_arguments(p.oid) || ') -> '
|| pg_get_function_result(p.oid)
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public' AND p.proname LIKE 'og\_%'
ORDER BY 1;
Graphs and types — spec 002
| Function | Purpose |
|---|---|
og_create_graph(name) → int4 |
create (or return) a graph namespace |
og_drop_graph(name) |
drop a graph and all of its storage |
og_create_type(graph, name, kind, parents text[] = '{}', is_abstract = false) → int4 |
declare an entity, relation or attribute type; parents gives (multiple) inheritance |
og_drop_type(graph, name, cascade = false) |
refuses while instances exist unless cascade |
og_add_property(graph, type, prop, data_type, required = false, is_key = false) |
becomes a real column on the type table and every descendant |
og_create_index(graph, type, prop) |
B-tree on the property column, across the hierarchy |
og_add_role(graph, rel_type, role, player_type, ordinal, card_min = 0, card_max = null) |
named participant slot; ordinal 0 = source, 1 = target, 2+ = n-ary |
og_add_role_player(graph, rel_type, edge_id, role, player) |
attach an n-ary participant |
og_add_rule(graph, rel_type, characteristic, target_type = null) |
transitive / symmetric / reflexive / inverse |
Property types: string, int, long, float, bool, datetime, date, uuid, numeric, jsonb, text[], int[], vector(N).
Inheritance
| Function | Purpose |
|---|---|
og_subtypes(type_id) → int4[] |
the type and every descendant, via one indexed range scan |
og_supertypes(type_id) → int4[] |
the type and every ancestor |
og_is_subtype(sub, sup) → bool |
single range comparison |
og_type_id(graph, name) → int4 / og_type_name(type_id) → text |
name resolution |
og_relabel(graph_id) |
rebuild interval labels (diagnostics/repair) |
Data — spec 001
| Function | Purpose |
|---|---|
og_create_node(graph, type, props = '{}') → int8 |
returns the node id |
og_create_edge(graph, rel_type, src, dst, props = '{}') → int8 |
validates role constraints, maintains both adjacency directions |
og_set_node_props(id, props) |
merge properties |
og_delete_node(id) → int8 / og_delete_edge(id) → int8 |
node delete detaches incident edges |
og_node_json(id) → jsonb / og_edge_json(id) → jsonb |
full entity as JSON, property names mapped back from columns |
Traversal and identifiers
| Function | Purpose |
|---|---|
og_expand(src, etypes int4[], dir "char") → TABLE(nbr, eid) |
neighbours; dir is 'o' or 'i'. Inlinable, so the planner sees through it |
og_expand_batch(srcs int8[], etypes, dir) |
many start points in one call |
og_vlp(src, etypes, dir, minhop, maxhop) → TABLE(node, depth, path) |
variable-length walk with trail semantics; dir 'b' for both. One row per path, so degreeᵏ rows |
og_reach(src, etypes, dir, minhop, maxhop) → TABLE(node, depth) |
the same walk as reachability: visited set, one row per node, depth is the first depth it was reached at. Bounded by |V|+|E| however deep the question goes |
og_reach_sql(...) |
pure-SQL reachability, UNION instead of a visited set. The floor og_reach is measured against |
og_csr_build(etypes, dir) → TABLE(nodes, edges, bytes, build_ms) |
compile the topology into backend-local memory. See the caveats below |
og_csr_reach(src, minhop, maxhop) → TABLE(node, depth) / og_csr_hops(src, dst, maxhop) → int |
reachability and bidirectional shortest-path length over the compiled graph |
og_csr_stats() / og_csr_drop() |
what this backend holds; free it |
og_nodes(root_type) → TABLE(id, type_id) / og_edges(root_type) |
subtype-aware scans |
og_degree(src, etype, dir) / og_degree_all(src, dir) |
degree, used for cost estimation |
og_make_id(shard, type_id, local), og_id_type, og_id_shard, og_id_local |
identifier encoding |
Cypher picks between og_vlp and og_reach on its own: og_reach when the query binds no path or relationship variable, its projection cannot observe how many paths reach a node, and the depth is past where enumeration is cheaper. Nothing routes to og_csr_* automatically, because a compiled graph is a frozen snapshot that row-level security is never consulted about — that is a choice a caller makes explicitly. og_reach and the og_csr_* functions are PARALLEL RESTRICTED; a plan containing one runs in the leader. Numbers and the full rule: deep-traversal.md.
Operations
| Function | Purpose |
|---|---|
og_graph_stats(graph) → jsonb |
counts per type, adjacency packing ratio, supernode count |
og_degree_distribution(graph) → jsonb |
degree histogram |
og_reorganize(graph) → int8 |
online repack of fragmented adjacency segments |
og_check_integrity() → TABLE(kind, entity_id, detail) |
cross-checks registry, typed tables and adjacency. Empty result = pass |
Cypher — spec 003
| Function | Purpose |
|---|---|
og_cypher(graph, query, params = '{}') → SETOF jsonb |
execute |
og_cypher_json(graph, query, params) → jsonb |
one array, for PostgREST/RPC |
og_cypher_sql(graph, query) → text |
the compiled SQL — embed it in your own queries |
og_cypher_explain(graph, query, analyze = false) → jsonb |
compiled SQL plus the PostgreSQL plan |
og_cypher_check(query) → jsonb |
parse only; no database access |
og_cypher_columns(query) → text[] |
result column names in RETURN order — jsonb sorts keys, so the row cannot tell you. Parse only. Empty for RETURN * |
TypeQL — spec 010
TypeDB 3.x TypeQL over the same graph, catalog and transaction. See typeql.md for the supported syntax and the storage mapping.
| Function | Purpose |
|---|---|
og_typeql(graph, query, params = '{}') → SETOF jsonb |
execute one query |
og_typeql_script(graph, script) → int8 |
run a whole .tql file; returns the number of blocks |
og_typeql_sql(graph, query) → text |
the compiled SQL for a read query |
og_typeql_check(query) → jsonb |
parse only; no database access |
og_typeql_schema(graph) → text |
dump the schema back out as TypeQL |
| View | Purpose |
|---|---|
og_typeql_attribute |
one row per ownership: owner, attribute type, value |
og_typeql_role |
one row per role assignment of a reified relation |
Vectors — spec 004
| Function | Purpose |
|---|---|
og_add_embedding(graph, type, prop, dims, metric = 'cosine', source_prop = null) |
declare a vector(N) property on a node or relationship type and build the HNSW index |
og_vector_search(graph, type, prop, query, k = 10, filter = null) → TABLE(id, score, entity) |
top-k; filter is a SQL predicate on the same relation, so it pushes down |
og_similar(graph, id, prop, k = 10) |
"things like this one", relationships included |
og_hybrid_search(graph, type, prop, query, anchor = null, k = 10, vector_weight = 1, graph_weight = 1) |
RRF fusion of vector rank and graph proximity; returns both component scores |
og_vector_search_exact(graph, type, prop, query, k) |
brute-force ground truth for measuring recall |
og_stale_embeddings(graph) → TABLE(entity_id, type_name, prop) |
embeddings whose source property changed |
og_mark_embedded(entity_id, prop) |
record that an embedding is current |
og_embedding_stats(graph) → jsonb |
declared embeddings, dimensions, metrics |
Metrics: cosine (<=>), l2 (<->), ip (<#>). Scores are normalised so higher is always better.
Interoperability — spec 005
| Function | Purpose |
|---|---|
og_enable_rls(graph, type, policy_expr) |
row-level security across the hierarchy; applies mid-traversal |
og_map_table(graph, source_table, type, id_column, property_map jsonb) |
expose an existing table as a node type without copying |
og_materialize_mapping(graph, type) → int8 |
convert a mapping into native storage; queries do not change |
og_interop_report(graph) → jsonb |
mappings, secured types, available views |
Views: og_node_view, og_edge_view, og_type_view, og_property_view, og_role_view.
Semantic web — spec 006
| Function | Purpose |
|---|---|
og_load_rdf(graph, document, format = 'turtle') → jsonb |
Turtle / N-Triples; maps OWL classes to types, rdfs:subClassOf to inheritance, domain/range to roles |
og_dump_rdf(graph, format = 'turtle') → text |
serialise back, including unmapped triples |
og_mapping_report(graph) → jsonb |
what did not map, and why |
og_add_prefix(prefix, iri) / og_set_iri(graph, type, iri) |
namespace handling |
Agents — spec 008
| Function | Purpose |
|---|---|
og_schema(graph, token_budget = null) → jsonb |
machine-readable schema; truncates by instance count when a budget is given and says so |
og_schema_for(graph, question) → jsonb |
the subset lexically relevant to one question |
og_explain_error(graph, query) → jsonb |
stable error code, message, spelling suggestions |
og_diagnose_empty(graph, query) → jsonb |
walks the pattern element by element and reports where it became empty |
og_estimate(graph, query) → jsonb |
dry-run: estimated rows, cost, and concrete advice |
og_create_role(name, limits jsonb) / og_apply_role(name) |
per-role statement_timeout, work_mem, read-only, row caps |
og_enable_history(graph, type) |
start capturing change history (off by default — it costs writes) |
og_history(id) → TABLE(recorded_at, op, payload) |
change log for one entity |
og_as_of(id, timestamptz) → jsonb |
state at a past instant; errors rather than returning the current value when no history is retained |
og_set_source(entity_id, source, confidence = null, author = null) |
provenance metadata |
Audit log: og_data.og_audit records every og_cypher and og_typeql call with principal (session_user), query, row count, duration and error code — and, since spec 014, db_user (the caller's current_user, accepted only if session_user is a member of it), actor, source and request_id (from the og.* settings, application-asserted), schema_version (the database-wide schema version sequence, not the graph's) and action (the Action the write came through). Rows are written only through the SECURITY DEFINER og__audit; write-level roles hold no DML on og_audit, og_history, og_source or the h_* history tables. Two limits: a failed query's audit row is rolled back with the query (SEC-18), and any reader may call og__audit directly and so append rows with caller-chosen query text — rows cannot be edited or deleted, but their content is not proof.
For spec 008's og_as_of: it now returns NULL at an instant after the entity was deleted, and breaks ties within one transaction by hist_id. Subtypes created after og_enable_history are captured too.
Schema evolution — spec 012
A schema change on a populated graph is four calls — expand, migrate, validate, contract — and one row in og_catalog.schema_change that records where it stands. Most kinds are catalog edits whose cost does not depend on the amount of data; only alter_property_type (and set_required with a default) rewrite rows, in batches. See ADR-028.
| Function | Purpose |
|---|---|
og_schema_plan(graph, change jsonb) → int4 |
record a change and report its impact — counts, sample ids of rows that would violate it, lock notes. Changes nothing |
og_schema_expand(change_id) → jsonb |
add the new structure beside the old; new writes must satisfy the new rule from here on |
og_schema_migrate(change_id, batch = 10000) → jsonb |
move up to batch rows per table; resumable. Call until {"done": true}. Returns {moved, done, migrated} |
og_schema_validate(change_id) → jsonb |
{phase, violations, samples}; moves to validated only with zero violations |
og_schema_contract(change_id) → jsonb |
remove the old structure. Only from validated |
og_schema_abort(change_id) → jsonb |
undo an expand. Does not remove a property added by add_property |
og_schema_apply(graph, change) → jsonb |
all phases in one transaction; fails with OG_SCHEMA_VIOLATION (23514) and changes nothing if validation finds violations |
og_schema_changes(graph) → TABLE(change_id, kind, phase, spec, report, created_by, created_at, updated_at) |
every recorded change, newest first |
og_schema_snapshot(graph, version = null, at = null) → jsonb |
the graph's catalog — types, parents, properties, roles, temporal/stamped flags — as it stood at a version or an instant; current if neither |
Phases: planned → expanded → migrating → validated → contracted, or aborted from any phase before contracted. Calling one out of order raises OG_SCHEMA_PHASE (SQLSTATE 55000).
kind |
change JSON | what it does physically |
|---|---|---|
add_property |
{type, property, data_type} |
ADD COLUMN, no rewrite; values already in __ext under that key move in. Optional only — use set_required afterwards |
drop_property |
{type, property} |
contract: DROP COLUMN. History tables keep the column |
rename_property |
{type, property, new_name} |
catalog only. Between expand and contract both names read and write the same column. The physical column keeps its old name |
alter_property_type |
{type, property, to} |
shadow column p_x__s<id>; a trigger attached at expand (og_c_shadow_<id>, dropped at contract or abort) fills it on every write, raw SQL included, cast to the new type (a value the new type rejects fails at once); migrate casts through text in batches; contract swaps columns, rebuilds indexes on it, re-applies column grants. History tables follow |
set_required |
{type, property, default?} |
CHECK … NOT VALID at expand, default filled in batches, VALIDATE CONSTRAINT (writes continue), then SET NOT NULL without a scan |
add_unique |
{type, property} |
validate reports duplicates across the hierarchy; contract builds a unique index per concrete table |
rename_type |
{type, new_name} |
catalog only; node ids do not change |
alter_role |
{relation, role, player, card_min?, card_max?} |
validate checks existing edges and role players; contract updates the role |
add_parent / remove_parent |
{type, parent} |
catalog + interval relabel; no row is rewritten. Inherited columns, temporal, stamping and write grants follow |
A property can be dropped, renamed or retyped only on the type that declares it. Not supported: splitting or merging types, TypeQL redefine/undefine through this pipeline, hierarchy-wide uniqueness. og_create_index, og_map_table and og_materialize_mapping now bump the schema version, so cached Cypher plans are invalidated.
Temporal — spec 013
Valid time (when it was true) and system time (since when we have believed it) per row, for the types you choose. The query does not change; the instant is a setting. See ADR-027.
| Function | Purpose |
|---|---|
og_enable_temporal(graph, type) |
version a type and its subtypes, including subtypes created later. Adds __valid tstzrange and __sys_from (existing rows: valid for all time, believed from now), creates og_data.h_<type_id>, attaches the capture trigger. Also turns on stamping |
og_temporal_types(graph) → text[] |
the temporal types of a graph |
og_valid_at() / og_system_at() → timestamptz |
the session's instant; now() when unset |
og_asof_active() → bool |
whether either instant is set |
og_require_current(what) → bool |
raises OG_TEMPORAL_UNSUPPORTED (SQLSTATE 0A000) when an instant is set. Compiled into query shapes that can only see the present |
og_meta(id) → jsonb |
an element's system columns, including valid and sys_from |
| Setting | Read or write | Meaning |
|---|---|---|
og.valid_at |
read | answer as of this valid time |
og.system_at |
read | answer as the database believed it at this time |
og.valid_from |
write | the change takes effect at this instant (default now()); '-infinity' is a correction |
og.valid_to |
write | end of validity for the new version (default open-ended, or the old end on update) |
SET og.valid_at = '2021-06-01';
SET og.system_at = '2024-01-01';
SELECT og_cypher('kb', $$ MATCH (p:Person {name: "ann"}) RETURN p.salary $$);
The compiled SQL is the same for every instant. A temporal type reads three branches: current rows; open history rows (__sys_to = 'infinity', valid-time slices still believed), visible whenever their validity contains the instant and found through a partial GiST index on __valid; and closed history rows (superseded beliefs), read only when an instant is set. So a query with no instant still probes that partial index once per temporal type. Single hops over temporal relationship types read the edge tables (the same three branches) instead of the adjacency segments — always, not only when an instant is set. With an instant set, these refuse rather than answer from the present: unlabelled node patterns (in a graph with temporal node types), variable-length paths over temporal relationship types, relationship variables without a single type, and TypeQL. An update dated after the end of the current validity raises OG_TEMPORAL_RANGE (22023). Raw SQL writes are versioned by the same trigger.
An update or delete with og.valid_from in the future leaves the value that holds until then in an open history row, so queries with no instant keep seeing it until that date. Not implemented: retention policy, editing past valid-time slices.
Over Bolt, tx_metadata keys valid_at/validAt, system_at/systemAt, valid_from/validFrom and valid_to/validTo become these settings for that transaction (auto-commit RUN included).
Write security — spec 014
Write privileges are PostgreSQL table and column grants on the per-type storage tables, so they hold for raw SQL exactly as for Cypher. The shared registries (og_node, og_edge, og_adj, og_role_player) carry row-level security that lets a role write a registry row only if it could write that type's storage table. Reads of the registries are not restricted. See ADR-026.
| Function | Purpose |
|---|---|
og_grant_write(role, graph, type, ops text[], props text[] = null) |
ops ⊆ create, set, delete; props limits create/set to those properties, null means every property including undeclared ones. Reaches subtypes, subtypes created later, and columns added later. Grants read access if the role has none |
og_revoke_write(role, graph, type, ops = null) |
take back some or all operations |
og_type_priv(type_id, priv) → bool |
used by the registry policies: may the current role INSERT/UPDATE/DELETE that type's storage |
og_enable_stamping(graph, type) |
adds __created_at, __created_by, __updated_at, __updated_by, __actor, __source, set by trigger from current_user, og.actor, og.source. Values written into the row are overwritten, raw SQL included. Subtypes inherit it |
og_meta(id) → jsonb |
read the stamps (and validity, spec 013) |
og_define_action(graph, name, body, run_as, lang = 'cypher', params = '{}', precondition = null, description = null) → int4 |
a named write. params is {"name": "string", …} with types string, int, float, number, bool, list, map, any; every declared key is required and no other key is accepted. precondition is Cypher returning ok. Creates og_action.og_action_<id>(jsonb), SECURITY DEFINER, owned by run_as — the caller must be able to SET ROLE run_as |
og_grant_action(role, graph, name) / og_revoke_action(role, graph, name) |
EXECUTE on that action's function |
og_drop_action(graph, name) |
drop the action and its function |
og_run_action(graph, name, params = '{}') → SETOF jsonb |
run it. Needs EXECUTE on the action's function; the body runs with run_as's privileges |
| Error | SQLSTATE | When |
|---|---|---|
OG_WRITE_DENIED: role 'r' may not set properties on 'Person' (property 'salary') |
42501 | the engine's pre-check; PostgreSQL enforces the same rule either way |
OG_RESERVED_PROPERTY |
42501 | writing _id, _type, _src, _dst or any __… property |
OG_ACTION_REJECTED |
23514 | the action's precondition did not hold |
| parameter errors | 22023 | unknown, missing or mistyped action parameter |
Context settings: og.actor, og.source, og.reason, og.request_id reach the audit log (og_data.og_audit); og.actor and og.source also reach stamped rows. The Bolt gateway maps tx_metadata keys actor, source, reason, request_id/requestId to transaction-local settings.
A write-level role without schema privileges that writes an undeclared property now keeps it in __ext instead of failing on og_add_property. Limits: TypeQL action bodies receive no parameters; the audit row of an action names run_as as db_user, not the caller. The action name is cleared on every exit, so a failed action does not leave it on later audit rows.