mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
f5b0062d6e
* fix(store): NUL Layer B covers columns added after 084 through a later generated trigger file (BUG-3108) The trigger generator had one hardwired target, 084, which runs before every later migration's columns exist, so every post-084 column could only be baselined. The trigger files are now an ordered list; each protected column is assigned to one, 084 by default. The generator writes every file and the pin test compares every file. The startup re-assertion wants and re-executes only the files already applied, because it runs after every migration, including the ones before a later column exists. 094 carries the three post-084 columns that hold caller text: items.lease_holder (request body, verbatim), item_relation_links.source_field_key (a schema field key) and item_decisions.model (the admin model pin). 084 is byte-identical. TestNULTriggerFilesFollowTheirColumns walks the migration chain one file at a time and fails when a column's trigger file does not run after the migration that introduced it, naming the fix. The next post-084 column therefore fails there instead of going quietly into the baseline. Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N * test(store): a NUL trigger restore does not re-execute a trigger file that is not yet applied (BUG-3108) Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N * fix(store): item_relation_links.source_field_key stays out of Layer B on migration 088's derivation, now recorded as an exclusion (BUG-3108) Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N * docs(store): ScanNUL's cost comment counts the two post-084 protected columns (BUG-3108) Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N * fix(store): later trigger files get their own name prefix so an older binary cannot drop them; the ordering instrument takes a column's latest appearance (BUG-3108) An older binary's restore reads triggers by GLOB 'pad_nul_*' and drops any it does not expect. Had 094's triggers used that prefix, a binary predating 094 started with --force would have removed them all. Each later file now uses pad_nul<NNN>_, and the restore reads one GLOB per file it knows. columnIntroductions records the last time a column appeared after being absent, so a column dropped and re-added, or a table renamed and replaced, counts as introduced again. The instrument is parameterised so a synthetic chain can show that rule going red. Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N * docs(store): the source_field_key exclusion names the check that covers it, a decoded value (BUG-3108) Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N
58 lines
3.0 KiB
Go
58 lines
3.0 KiB
Go
package store
|
|
|
|
// nulTriggerMigrationHeader is the generated migration's preamble.
|
|
//
|
|
// It lives here rather than only in the .sql file so the generator and the file
|
|
// share one definition — the pin test compares the whole rendered text, header
|
|
// included, which is what makes a hand edit to the file detectable.
|
|
const nulTriggerMigrationHeader = `-- Layer B of the NUL invariant (DOC-2823 S2): the database owns the rule.
|
|
--
|
|
-- GENERATED from internal/store/nulcolumns.go. Do not edit by hand — run
|
|
-- GEN_NUL_TRIGGERS=1 go test ./internal/store/ -run TestGenerateNULTriggerMigration
|
|
-- and commit the result. TestNULTriggersMatchTheList fails if this file and the
|
|
-- Go list disagree.
|
|
--
|
|
-- WHY TRIGGERS AND NOT ONLY THE GO GUARD. S1's Layer A makes the invariant a
|
|
-- property of the BINARY. BUG-2813 is the window where that is not enough: an
|
|
-- older binary serving the same SQLite file has no such guard, and a rollback,
|
|
-- a staged rollout or a second instance writes rows the invariant forbids.
|
|
-- A trigger is enforced by the FILE, so it holds for every writer.
|
|
--
|
|
-- SQLITE ONLY. Postgres refuses a NUL in text natively (SQLSTATE 22021) and an
|
|
-- escape decoding to one in jsonb (22P05), so the database already owns the
|
|
-- rule there; adding triggers would duplicate an existing guarantee.
|
|
--
|
|
-- PREDICATE, measured on modernc.org/sqlite v1.57.0 / SQLite 3.53.3 in
|
|
-- TASK-2824 rather than assumed:
|
|
-- * instr(col, char(0)) finds a real NUL. length() does NOT — it C-truncates
|
|
-- at the NUL — so instr is the only builtin to trust here.
|
|
-- * json_tree DECODES the six-character escape, exposing a real NUL in both
|
|
-- the value and key columns, and a DOUBLED backslash stays literal text
|
|
-- with no false positive.
|
|
-- * a Go-bound real-NUL parameter reaches the trigger intact, which is what
|
|
-- makes BEFORE INSERT the right shape: an old binary's write is seen.
|
|
--
|
|
-- No DB-side repair is attempted, deliberately: the same measurement shows
|
|
-- SQLite's string functions disagree about NUL-bearing text, so transforming
|
|
-- such a value in SQL cannot be trusted. Repair is S3, in Go.
|
|
|
|
`
|
|
|
|
// nulLaterTriggerMigrationHeader is the preamble of every trigger file after
|
|
// the first. The reasoning is 084's and is not repeated; what differs is why
|
|
// the file exists at all.
|
|
const nulLaterTriggerMigrationHeader = `-- Layer B of the NUL invariant (DOC-2823 S2), for columns added after 084.
|
|
--
|
|
-- GENERATED from internal/store/nulcolumns.go. Do not edit by hand — run
|
|
-- GEN_NUL_TRIGGERS=1 go test ./internal/store/ -run TestGenerateNULTriggerMigration
|
|
-- and commit the result. TestNULTriggersMatchTheList fails if this file and the
|
|
-- Go list disagree.
|
|
--
|
|
-- WHY A SEPARATE FILE (BUG-3108). A trigger can only follow its column. 084
|
|
-- runs before these columns exist, and SQLite re-parses every trigger during
|
|
-- an ALTER TABLE ... RENAME, so a trigger there naming a later column breaks
|
|
-- the next table rebuild. The predicate, the marker and the SQLite-only scope
|
|
-- are 084's; its header carries the measurements.
|
|
|
|
`
|