Named Graphs, Overlays & Datasets
Implementation status: ✅ Built on the sanctioned surfaces — the
gcolumn andgraphsregistry, overlay create/write/compose,GRAPH/FROM/FROM NAMEDevaluation, thegraphquery param, and named datasets, and (since 2026-08-21) a strictgraphparam onPOST /knotthat accepts only already-registered committed-class graphs. Deliberately not built: property paths underGRAPH ?g(explicitly refused, never a silent ROOT read). Seedocs/design/named-graphs.mdfor the full design.
Every fact in quipu’s EAVT store carries a graph coordinate on top of
(entity, attribute, value) and the two time axes — the store is really a
quad store. The g column says which subgraph a fact lives in, orthogonal
to when it holds (valid_from/valid_to) and when the store learned it
(tx). Retraction, time-travel, and contradiction detection all scope
within a graph: a retraction in graph A never touches graph B.
g = 0is the reserved ROOT graph — the default committed graph, the source of truth.- A named graph’s
gis the interned term id of its graph IRI, so resolvingGRAPH <iri>is a single term lookup. - The
graphsregistry keeps one row per graph with an enforcedclass:committed(a durable branch; ROOT is the seeded, self-rooted one) oroverlay(a layer over a committed parent). A graph’s class is fixed at create.
Querying: GRAPH, FROM, and the graph param
Committed reads are ROOT-scoped by default. The default graph is ROOT alone, not an all-graph union — silence must never expose another tenant’s overlay. A query widens its dataset only by saying so:
GRAPH <iri> { … }scopes the enclosed patterns to one named graph. An unknown IRI matches nothing.GRAPH ?g { … }ranges over the active named graphs, binding?gto each match’s graph IRI. Property paths underGRAPH ?gare refused with an explicit error rather than silently reading ROOT.FROM <g…>makes the default graph the RDF merge (union) of those graphs. An unknown graph contributes nothing; an all-unknownFROMset yields an empty default graph — never a fall-through to ROOT.FROM NAMED <g…>restricts which named graphs aGRAPHclause can see. A query withFROMbut noFROM NAMEDactivates no named graphs (per SPARQL 1.1), soGRAPHmatches nothing.
SELECT ?s ?title
FROM <http://example.org/graphs/derived>
WHERE { ?s <http://example.org/title> ?title }
POST /query and the quipu_query MCP tool also take a graph request
param — a convenience that scopes the query’s default graph to one named
graph without writing a FROM or GRAPH clause:
{"query": "SELECT ?s ?o WHERE { ?s <http://example.org/p> ?o }",
"graph": "http://example.org/graphs/derived"}
Omitting it keeps the ROOT default; an unknown IRI gives an empty default
graph; a FROM clause in the query text overrides the param. The param also
resolves dataset names: passing a dataset IRI scopes the query to that
dataset’s members, so FROM <dataset> and "graph": "<dataset>" mean the
same thing. The same graph param on quipu_export / POST /export
exports one named graph’s facts instead of ROOT.
Property paths follow a fixed graph scope without crossing it: a
GRAPH <iri> closure stays inside that graph, a FROM <a> FROM <b> path
traverses their merge. A path never crosses a graph boundary — half a path
in an overlay and half in ROOT is not a fact either graph asserts.
Overlays
An overlay is a scratch layer over a committed parent branch: hypotheses go in the overlay, the committed base is never mutated. Two write primitives, one uniform read:
- Create (
quipu_overlay_create/POST /overlay/create) registers an overlay-class graph bound once to its committed parent branch (ROOT by default). Idempotent; rebinding to a different parent is an error — the binding is unforgeable. - Write (
quipu_overlay_write/POST /overlay/write) takes one of three ops:assertandretractare graph-scoped writes into the overlay;tombstonemarks a specific(e, a, v)from the parent as absent in the overlay’s composed view, without touching the parent. - Compose (
quipu_overlay_compose/POST /overlay/compose) resolves the stack[overlay > parent-branch-root]with a single rule: a triple is present iff asserted and not tombstoned, nearest-overlay-wins. Overlay asserts shadow the parent; overlay tombstones hide parent triples; everything else falls through. - Governed precedence (
"precedence": "governed"on the same tool / endpoint) inverts who wins, for reading a quarantine plane over a governed graph (quipu-e61, adopted from Spanner’s statically-defined-properties-win-over-dynamic rule): the parent’s facts are always present — an overlay value on a same-subject-same-predicate slot the parent claims is suppressed, and an overlay tombstone cannot mask a parent fact (it still masks the overlay’s own contributions). A low-trust plane may extend the governed graph but never alter what it says; promotion out of quarantine remains an explicit governed write, never a precedence flip.
Many tenants can extend the same base independently this way, and a
committed read never sees any of them unless the query names the overlay.
Overlays are one sanctioned write path for named graphs. POST /episode
accepts a graph field for ingestion into a named graph, and POST /knot
accepts a graph field under a strict contract: the target must already be
a registered committed-class graph (created via graph_create, where
authority checks live). An unknown IRI is an error and is never interned —
a typo’d plane name must not become a writable target as a side effect of
being rejected — and overlay-class graphs are refused (write through
overlay_write). The class invariant the earlier “no graph on /knot”
refusal protected survives because registration remains the only way to
mint a target. The semantic event taxonomy (episode.ingested, entity.*,
edge.*, type.new, predicate.new) emits for ROOT-graph commits only;
named-graph writes — overlay staging and committed-class targets alike — do
not emit it. Gate refusals, however, are recorded as write.refused events
for any destination graph (the payload names the graph IRI), and
registry changes (shapes.loaded, fork.*) emit their own events.
Datasets
A dataset is a name for an arbitrary set of graphs, queryable as one
unit — the reusable form of a FROM a b c clause, so a graph set can be
labelled, governed, and handed to another agent. Managed via the
quipu_datasets MCP tool or POST /datasets (create / list / show /
remove).
curl -s localhost:3030/datasets -X POST \
-H "Content-Type: application/json" \
-d '{"action": "create", "name": "http://example.org/datasets/hot",
"members": ["http://example.org/graphs/a", "http://example.org/graphs/b"]}'
FROM <dataset-iri>(and thegraphquery param) expands to the dataset’s members at resolve time; everything downstream reads the expanded set.- A dataset is never implicitly active — the ROOT-alone default is untouched; you get a dataset’s graphs only by naming it. A member naming an unregistered graph contributes nothing.
- Members may carry a declared ordering (
{"graph": …, "ord": N}); duplicate ranks are refused rather than tiebroken silently. An empty dataset is refused. - Datasets are mirrored into the meta-graph as
quipu:Dataset/quipu:includesGraphfacts, so they are queryable and governed like any other fact. Datasets are orthogonal to the overlay branch tree: the branch tree is compose’s resolution root, datasets are overlapping named sets. - The labels of a query’s active dataset compose across its member graphs — see Graph Labels for how freshness/trust/policy labels fold over the graphs a query actually reads.
Related
- REST API —
/query,/export,/overlay/*,/datasets - MCP Tools —
quipu_query,quipu_export,quipu_overlay_*,quipu_datasets - Graph Labels — labels on graphs and their composition over datasets
- Design doc:
docs/design/named-graphs.md