Files
pad/internal
xarmian 46c55e32f3 fix(store): re-assert triggers after a table rebuild, and correct two inherited classifications
[P1] A future table rebuild would have silently removed the protection. SQLite
drops a table's triggers with the table, this codebase rebuilds tables to change
constraints (migrations 025, 055, 056, 057, 068, 072), and migration 084 would
never run again because it is recorded as applied. The FTS equivalent warns and
moves on, which is the right cost for a derived index and the wrong one for a
data invariant: a missing FTS trigger breaks search visibly, a missing NUL
trigger is silently no protection against the exact writer Layer B exists for.
The triggers are now RE-ASSERTED after every migration pass, and the test
proves the loss is real before measuring the restoration.

[P1] Six caller-influenced columns were unprotected. workspaces.slug,
collections.slug, views.slug and agent_roles.slug are all `slug := input.Slug`
with slugify only as a FALLBACK — the census's exclusion note was about
items.slug, which is genuinely derived (ItemCreate has no Slug field), and I
read it as covering slugs generally. Plus comment_reactions.emoji and
oauth_clients.logo_url.

[P1/P2] Two classifications were wrong in the census and I inherited both.
agent_roles.tools is FREE TEXT — migration 019 says so in as many words
("free-text notes about preferred tools/models") — and classing it JSON would
refuse a user's note that happens to be valid JSON carrying an escape. And of
the six oauth request columns, only session_data is JSON: RequestForm is
`.Encode()`, and scopes/granted_scopes/audience/granted_audience are
`strings.Join(..., " ")`. The census extended oauth_clients' jsonStringList
classing across tables that do not use it. 20 columns reclassified.

[P2] The migration's header told readers to run a generator I had deleted. It
is a real test now, skipped unless GEN_NUL_TRIGGERS is set — an artifact that
instructs you to run something nonexistent is worse than one with no
instructions.

[P2] The pin compared trigger NAMES and counts, which would pass a wrong
BEFORE UPDATE OF clause, a wrong predicate, or a changed marker — the parts
that do the work. It compares the whole rendered text byte for byte now, and
reports the first differing line.

[P2] The census matched TEXT/CHAR/CLOB only, so a column declared JSON or with
no declared type — both BLOB affinity, both holding text fine — would have
slipped past. Widened to match what CANNOT hold text. It immediately surfaced
item_yjs_updates.update_data, which is correct: that is the binary column Layer
A exempts, and it is now recorded as an explicit exclusion rather than hidden
by a filter. The table filter also matched "_fts" anywhere in a name, which
would skip a real table called something like user_fts_settings.

[P2] The differential leg claimed persistence and only checked that no error
came back. It reads the value back and compares it now.

[P2] Trigger refusals set Ordinal 0 against a documented 1-based field,
rendering as "parameter 0". A database refusal knows the COLUMN, not the
parameter position, so it sets no ordinal and Error() says so.

Full Go suite green on SQLite and Postgres 17; lint 0 issues.

Claude-Session: https://claude.ai/code/session_01XLtX4dbjBpApbAv3SuBcTm
2026-09-01 21:16:17 +00:00
..
2026-03-26 01:52:36 +00:00