check_circleSeal
Freeze a graph so nothing can change it, and record exactly what it holds. Seal is how you say "this graph is finished", before you carve from it, publish it or hand it on.
What it is
Sealing is two steps, usually done together:
- Lock.
lock_graphmakes the graph read-only. Every write path refuses with SQLSTATE55P03until someone callsunlock_graph. Reads keep working. - Fingerprint.
graph_digestreturns the graph's canonical identity: a SHA-256 over its W3C RDFC-1.0 canonical form.graph_manifestwraps that digest, two others, the triple counts and the engine version into one JSON certificate.
Indexes need no sealing step. The loaders build them as they load.
How you run it
pgrdf.lock_graph(graph_id BIGINT, reason TEXT) → BOOLEAN
pgrdf.unlock_graph(graph_id BIGINT, reason TEXT) → BOOLEAN
pgrdf.graph_digest(graph_id BIGINT) → TEXT
pgrdf.graph_manifest(graph_id BIGINT) → JSONBA worked example
Load a small graph, then lock it:
SELECT pgrdf.add_graph('http://example.org/products'); -- → 1
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
ex:widget ex:label "Widget" ; ex:price 10 .
ex:gadget ex:label "Gadget" ; ex:price 25 .
', pgrdf.graph_id('http://example.org/products')); -- → 4
SELECT pgrdf.lock_graph(pgrdf.graph_id('http://example.org/products'), 'release review');
-- → tWrites refuse
SELECT * FROM pgrdf.sparql(
'INSERT DATA { GRAPH <http://example.org/products>
{ <http://example.org/gizmo> <http://example.org/price> 5 } }');
-- ERROR: 55P03: pgrdf: graph 1 is locked (release review): SPARQL UPDATE refused.
-- Unlock with pgrdf.unlock_graph(1, '<reason>').
SELECT pgrdf.materialize(pgrdf.graph_id('http://example.org/products'));
-- ERROR: 55P03: pgrdf: graph 1 is locked (release review): materialize refused.
-- Unlock with pgrdf.unlock_graph(1, '<reason>').The same refusal covers loading (parse_*, load_*), SPARQL UPDATE (including CLEAR GRAPH), clear_graph, drop_graph, materialize, and being the destination of copy_graph, move_graph or carve_graph.
Reads keep working
SELECT * FROM pgrdf.sparql(
'SELECT ?label ?price WHERE { GRAPH <http://example.org/products>
{ ?p <http://example.org/label> ?label ; <http://example.org/price> ?price } }
ORDER BY ?price');
-- {"label": "Widget", "price": "10"}
-- {"label": "Gadget", "price": "25"}Queries, export_graph, the digests, and using the graph as the source of copy_graph or carve_graph all work on a locked graph.
The inventory shows the lock
SELECT iri, asserted, locked, lock_reason FROM pgrdf.graph_inventory() WHERE locked;
-- iri | asserted | locked | lock_reason
-- -----------------------------+----------+--------+----------------
-- http://example.org/products | 4 | t | release reviewRecord the fingerprint
SELECT pgrdf.graph_digest(pgrdf.graph_id('http://example.org/products'));
-- → b397118cdba33a34831c8f376308f78a38a0b6336e206f3e60e09494d9b5d2deEqual identity digests mean two graphs hold the same triples; unequal digests mean they differ. Keep the method label, rdfc-1.0-sha256, next to any digest you store. For the full certificate, ask for the manifest:
SELECT pgrdf.graph_manifest(pgrdf.graph_id('http://example.org/products'));{
"graph_id": 1,
"iri": "http://example.org/products",
"captured_at": "2026-09-10 08:57:39.35664+00",
"engine": { "version": "0.6.34", "build_id": "v0.6.34", "extversion": "0.6.34" },
"counts": { "asserted": 4, "inferred": 0 },
"digests": {
"bytes": { "value": "b397118cdba33a34831c8f376308f78a38a0b6336e206f3e60e09494d9b5d2de", "method": "sha256-of-canonical-ntriples" },
"identity": { "value": "b397118cdba33a34831c8f376308f78a38a0b6336e206f3e60e09494d9b5d2de", "method": "rdfc-1.0-sha256" },
"structure": { "value": "0f25915406b4b973bd38d59bd55e05d129fd0ae4d7576f68f7f88bed747481f7", "method": "pgrdf-fd1-sha256" }
},
"not_carried": [
"inferred triples (count above is a check value; re-derive with pgrdf.materialize)",
"lifecycle state and locks (a restored copy starts unlocked and unmaterialized)",
"attestation and provenance chains (provenance-shaped triples travel as plain triples; the proof that made them true does not)"
]
}What each digest answers is explained in Identity and export.
Unseal
SELECT pgrdf.unlock_graph(pgrdf.graph_id('http://example.org/products'), 'review passed');
-- → tRules
- A reason is required for both locking and unlocking, and
lock_graphrefuses an empty one (22023). The reason appears in every refusal and in the inventory. - Locking a locked graph refuses (
55P03), so a standing reason is never overwritten. Unlocking an unlocked graph refuses (55000). - A lock is a coordination tool, not a security boundary. Anyone who can write to the graph can unlock it. Use PostgreSQL roles and privileges to control who may write.
- Digests cover asserted triples only. Inferred triples are left out, so re-running
materializedoesn't change the fingerprint.
Where it sits in a chain
- After Import, before Carve: freeze the source so every slice you carve comes from the same recorded version. Carving from a locked graph works.
- After Validate: freeze the result you're about to publish, then Unload it or leave it in place for queries.
- After Reason, never before it:
materializerefuses on a locked graph.
See also
- Locking a graph — the lock in the context of graph management.
- Identity and export — the three digests and the manifest in detail.
- Unload — package a sealed graph as files and remove it.