Skip to content

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:

  1. Lock. lock_graph makes the graph read-only. Every write path refuses with SQLSTATE 55P03 until someone calls unlock_graph. Reads keep working.
  2. Fingerprint. graph_digest returns the graph's canonical identity: a SHA-256 over its W3C RDFC-1.0 canonical form. graph_manifest wraps 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 ​

sql
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)            → JSONB

A worked example ​

Load a small graph, then lock it:

sql
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');
-- → t

Writes refuse ​

sql
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 ​

sql
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 ​

sql
SELECT iri, asserted, locked, lock_reason FROM pgrdf.graph_inventory() WHERE locked;
--              iri             | asserted | locked |  lock_reason
-- -----------------------------+----------+--------+----------------
--  http://example.org/products |        4 | t      | release review

Record the fingerprint ​

sql
SELECT pgrdf.graph_digest(pgrdf.graph_id('http://example.org/products'));
-- → b397118cdba33a34831c8f376308f78a38a0b6336e206f3e60e09494d9b5d2de

Equal 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:

sql
SELECT pgrdf.graph_manifest(pgrdf.graph_id('http://example.org/products'));
json
{
  "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 ​

sql
SELECT pgrdf.unlock_graph(pgrdf.graph_id('http://example.org/products'), 'review passed');
-- → t

Rules ​

  • A reason is required for both locking and unlocking, and lock_graph refuses 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 materialize doesn'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: materialize refuses on a locked graph.

See also ​

pgRDF is released under the MIT license. Documentation built with VitePress, served via GitHub Pages.