chore(release): 0.6.28 — the lock becomes law (#107) - #108
Merged
Conversation
The checkpoint lock lived in pgrdf_mcp.ledger and only the MCP door's own writes consulted it. Measured: SQL clear_graph emptied a LOCKED graph (returned 8, quads 0) and the door then refused the repair import -- locked and empty, the sanctioned route dead. The one boundary in this release line that reported protection without enforcing it. Lock state now lives on pgrdf._pgrdf_graphs (locked, lock_reason, locked_at -- idempotent ALTERs, shipped both in the generated install and the upgrade script). require_unlocked() is called by EVERY engine write path: clear_graph, drop_graph, move_graph (src AND dst -- move clears src), copy_graph/carve_graph (dst only -- src is read), put_quad, put_construct_row(s), ingest_dispatch (one hook covers the whole parse_/load_turtle family), load_turtle_streaming, load_turtle_staged_run, parse_trig/parse_nquads (parameter graph; payload-named graphs are a documented limitation), and materialize (it writes inferred rows). Reads are never blocked: a lock is a write fence, not a read fence. lock_graph/unlock_graph take mandatory reasons -- the reason IS the record. Double-lock refuses rather than swallowing the standing reason; unlocking an unlocked graph refuses rather than validating a wrong model of the state. SCOPE, stated in the module header so #107 cannot recur one level up: this is a COORDINATION primitive, not a security boundary. Anyone who can write the graph can lock/unlock it. Security remains grants. Test note: the per-path refusal probes run in PL/pgSQL exception blocks (subtransaction rollback per probe). A catch_unwind version would have left SPI aborted after probe 1 and passed probes 2-12 for the wrong reason -- caught before it ever ran. Suite: 362 passed, 0 failed (was 356). No existing test broke: nothing ever locked a graph before, so the tightening pays no regression toll.
…lock columns The baseline pins the on-disk shape and its ELSE FALSE arm tripped on the three new columns — exactly its job. The contract now names them: locked boolean NOT NULL, lock_reason text NULL, locked_at timestamptz NULL. The golden is unchanged: the check still collapses to one 't'.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #107. The one boundary in this release line that reported protection without enforcing it becomes enforcing.
What was wrong (measured on 0.6.27)
pgrdf_checkpointreportedlocked: true;SELECT pgrdf.clear_graph(...)emptied the locked graph anyway (returned 8, quads 0); the MCP door then refused the repair import — locked and empty, sanctioned route dead. The lock lived inpgrdf_mcp.ledgerand only the door's ownguardNotLocked()consulted it.The change
Lock state lives on
pgrdf._pgrdf_graphs(locked,lock_reason,locked_at).require_unlocked()is called by every engine write path — 13 hooks / 12 paths:clear_graph·drop_graph·move_graph(src and dst — move clears src) ·copy_graph/carve_graph(dst — src is read) ·put_quad·put_construct_row(s)·ingest_dispatch(one hook, wholeparse_/load_turtlefamily) ·load_turtle_streaming·load_turtle_staged_run·parse_trig/parse_nquads(parameter graph; payload-named graphs are a documented limitation) ·materialize(writes inferred rows)Reads never blocked — a lock is a write fence.
pgrdf.lock_graph(id, reason)/unlock_graph(id, reason)— reasons mandatory, double-lock refuses, unlock-of-unlocked refuses.Scope, stated plainly (so #107 cannot recur one level up)
Coordination primitive, not a security boundary. Anyone who can write the graph can lock/unlock it. Security remains grants (partitions inherit since 0.6.25).
Cut mechanics
DDL + both functions in
sql/pgrdf--0.6.27--0.6.28.sql; the0.5.1bridge replays the delta; Rule 7 reconciled across all six sites.Verification
Step 7 (pgRDF.MCP migrates to
pgrdf.lock_graph) follows the release — their ledger keeps lifecycle, the engine keeps the lock.