max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+38 insertions,
-11 deletions
| @@ -1,3 +1,5 @@ | |||
| 1 | + | -- no-transaction | |
| 2 | + | -- | |
| 1 | 3 | -- Sando ships more than one product. Design: wiki [[sando-bento-boundary]]. | |
| 2 | 4 | -- | |
| 3 | 5 | -- Every table here was implicitly about the MNW server. `versions` keyed a | |
| @@ -21,19 +23,37 @@ | |||
| 21 | 23 | -- anybody editing either. | |
| 22 | 24 | -- | |
| 23 | 25 | -- Four tables need their PRIMARY KEY widened, which SQLite cannot do in place, | |
| 24 | - | -- so they are rebuilt. `defer_foreign_keys` holds row-level enforcement until | |
| 25 | - | -- COMMIT, which is what lets a referenced table be dropped and replaced with | |
| 26 | - | -- rows still pointing at it. (`PRAGMA foreign_keys` itself is a no-op inside a | |
| 27 | - | -- transaction, which is why it is not used here.) | |
| 26 | + | -- so they are rebuilt. That is the 12-step rebuild from the SQLite docs, and it | |
| 27 | + | -- wants `PRAGMA foreign_keys = OFF` around the whole thing. | |
| 28 | 28 | -- | |
| 29 | - | -- Deferral does not cover everything, which decides the order below. A | |
| 30 | - | -- composite `REFERENCES tiers(app, name)` is a *schema* error — "foreign key | |
| 31 | - | -- mismatch" — until `tiers` actually has a unique index on those two columns, | |
| 32 | - | -- and that is raised when the statement runs, not at COMMIT. So `tiers` is | |
| 33 | - | -- rebuilt and renamed first, alone, and only then do the tables that reference | |
| 34 | - | -- it get created. | |
| 29 | + | -- `defer_foreign_keys` is NOT a substitute, which is what this migration was | |
| 30 | + | -- written on and what broke it. Deferral does not skip enforcement, it moves | |
| 31 | + | -- the check to COMMIT and keeps a counter of outstanding violations in the | |
| 32 | + | -- meantime. With foreign keys on, `DROP TABLE` performs an implicit DELETE of | |
| 33 | + | -- every parent row, and each child row still pointing at one increments that | |
| 34 | + | -- counter. Nothing ever decrements it: the counter falls only when DML resolves | |
| 35 | + | -- a violation, and re-creating the parent by DDL is not DML. So COMMIT fails | |
| 36 | + | -- with error 787 over a database whose final state is consistent — | |
| 37 | + | -- `foreign_key_check` afterwards reports nothing. | |
| 38 | + | -- | |
| 39 | + | -- The bug could only ever fire in production. It needs rows in the child | |
| 40 | + | -- tables, and every test starts from an empty schema. The live daemon had 58 | |
| 41 | + | -- deploys and 418 gate_runs when this was found, on 2026-08-09, by the restart | |
| 42 | + | -- that was supposed to apply it. | |
| 43 | + | -- | |
| 44 | + | -- `PRAGMA foreign_keys` is a no-op inside a transaction, so this migration | |
| 45 | + | -- carries `-- no-transaction` and opens its own instead. The atomicity is the | |
| 46 | + | -- same; what changes is that the pragma lands before the transaction begins. | |
| 47 | + | -- | |
| 48 | + | -- The order below is unchanged and still load-bearing. A composite | |
| 49 | + | -- `REFERENCES tiers(app, name)` is a *schema* error — "foreign key mismatch" — | |
| 50 | + | -- until `tiers` actually has a unique index on those two columns, raised when | |
| 51 | + | -- the statement runs rather than at COMMIT, and no pragma turns that off. So | |
| 52 | + | -- `tiers` is rebuilt and renamed first, alone, and only then do the tables that | |
| 53 | + | -- reference it get created. | |
| 35 | 54 | ||
| 36 | - | PRAGMA defer_foreign_keys = ON; | |
| 55 | + | PRAGMA foreign_keys = OFF; | |
| 56 | + | BEGIN; | |
| 37 | 57 | ||
| 38 | 58 | -- Tiers are per product. MNW's `a`/`b` and pom's tiers are different pipelines | |
| 39 | 59 | -- that happen to be spelled alike. | |
| @@ -187,3 +207,10 @@ | |||
| 187 | 207 | CREATE INDEX gate_runs_lookup ON gate_runs(app, tier, version, gate_kind); | |
| 188 | 208 | CREATE INDEX backups_name_fetched ON backups(app, name, fetched_at); | |
| 189 | 209 | CREATE INDEX build_runs_by_sha ON build_runs(app, sha); | |
| 210 | + | ||
| 211 | + | COMMIT; | |
| 212 | + | ||
| 213 | + | -- Back on for the connection that ran this. The pragma is per connection rather | |
| 214 | + | -- than stored in the file, so leaving it off would only affect this one, but | |
| 215 | + | -- this one goes on to serve the daemon. | |
| 216 | + | PRAGMA foreign_keys = ON; |