Skip to content

v1.0.0-alpha-306

@ignis-celestis ignis-celestis tagged this 28 Jul 14:30
The audit ledger was asked for database-level immutability. It turned out the
framework already shipped append-only triggers for three tables and not one of
them existed in any database. Four defects, each enough on its own, all silent.

A schema provider supersedes a same-named .sql on purpose — it is how a module
moves from the raw form to the declarative one. But Doctrine Schema has no
notion of a trigger, so a .sql carrying one is not a duplicate of its provider,
it is the half the provider cannot express. backup_catalog,
release_build_manifests and observability_deploy_audit_log all lost their
append-only trigger that way. The publisher now scans a superseded stub for DDL
no Schema can represent and refuses, naming the file; plain table-and-index
stubs still supersede quietly, so the intended behaviour is untouched.

Raw stubs also hardcoded the vortos_ prefix. Framework tables are named through
t(), which is prefix-aware, so on an install that puts them in a vortos. schema
every one of those stubs named a table that does not exist. Raw stubs write
{vortos} now and the publisher substitutes the configured prefix.

The statement splitter did explode(';'). A PL/pgSQL body is full of semicolons,
so every trigger stub was torn into fragments and emitted as separate addSql()
calls — unrunnable, and proven so by running one: unterminated dollar-quoted
string. It is dollar-quote, literal and comment aware now.

And the destructive-SQL detector read a guard as the thing it guards against: a
trigger declaring BEFORE TRUNCATE and raising 'TRUNCATE is prohibited' was
classified destructive, so the migration generated as Contract phase, which an
install gating contract migrations manually would never apply. It also matched
TRUNCATE inside the identifier audit_events_no_truncate. Matching now blanks
dollar-quoted bodies, string literals, line comments and trigger event clauses,
and every pattern is word-anchored.

audit_events gets the guarantee this started as. It had tamper evidence — hash
chain plus HMAC — and no prevention, because the table it replaced had triggers
and the audit-spine unification quietly traded one for the other. Now: UPDATE
refused, TRUNCATE refused by a statement-level trigger (row triggers do not fire
on TRUNCATE, so the whole ledger could otherwise go in one statement), and
DELETE allowed only where a checkpoint for that chain already covers the row's
sequence. Retention documents its ordering as archive, checkpoint, delete and
only purges an archived contiguous prefix; that invariant was upheld by one
class being careful and is upheld by the database now. Not a session flag: a
flag asks whether the caller claims to be retention, which anyone can claim, and
the checkpoint asks whether the row is safe to lose.

Verified against a real Postgres through the migration path, not a hand-run
script. The integration test runs the shipped stub rather than a copy, so the
file and the test cannot drift.

Split from 53a9b5dc75e767e84805c05dac8c2d2ca4d13e0f
Assets 2
Loading