fix(migrations): auto-reconcile BUNYIP-79 in-place-edited checksums at startup #173

Merged
longjacksonle merged 1 commit from fix/bunyip-79-migration-checksum-reconcile into main 2026-06-19 03:37:14 +02:00

Closes #172.

Problem

Commit 9c082eb (BUNYIP-79) edited 11 already-applied migration files in place. sqlx records a per-file SHA-384 in _sqlx_migrations and recomputes it on every start, so any database that applied the original bodies fails startup with migration <version> was previously applied but has been modified and crash-loops. A developer OP was down ~24h on exactly this; with the API dead, Traefik had no backend and dependent relying parties (a lets-chat dev stack) could not complete OIDC discovery.

Fix

New bunyip-api/src/migrate_reconcile.rs, run once at startup before the migrator. For each of the 11 affected versions, if the recorded checksum still equals the pre-edit value, it is rewritten to the current embedded checksum. The UPDATE is guarded by AND checksum = <pre-edit hash>:

  • DBs that applied the originals (recorded == pre-edit) are healed.
  • DBs freshly migrated after 9c082eb (recorded == current) are untouched (rows_affected == 0).
  • Any genuinely-unexpected edit on these versions (recorded == neither) is untouched, so the immutability ratchet still catches real mistakes.

The replacement value is read from the embedded migrator, not hardcoded, so it stays correct if a listed file is touched again.

Verification

  • legacy_allowlist_matches_embedded_migrator unit test: every listed version exists in the embedded set, each hash is a well-formed SHA-384, and the pre-edit hash differs from the current embedded hash (proving the file really was edited). Runs under just test (--lib), no database required.
  • End-to-end against the dev OP: re-introduced the pre-edit checksums on two versions, restarted, and confirmed the log shows BUNYIP-79 reconcile: rewrote stale migration checksum for both, reconciled 2 ... count=2, then Database migrations completed successfully and the API listening, instead of crash-looping. The two rows are left at the current file hashes (idempotent thereafter).
  • cargo fmt, cargo clippy --lib -p bunyip-api, and scripts/check-migration-versions.sh are clean.

Scope / follow-up

Reconciliation only unblocks startup. A row marked applied is never re-run, so the DDL guards 9c082eb added are NOT retro-applied to a database that already ran the original bodies. Delivering those guards to already-migrated data requires forward-only migrations and is intentionally left as a follow-up; the README and module docs state this and reinforce that future behavioral changes must ship as fresh migrations, not in-place edits.

Closes #172. ## Problem Commit `9c082eb` (BUNYIP-79) edited 11 already-applied migration files in place. sqlx records a per-file SHA-384 in `_sqlx_migrations` and recomputes it on every start, so any database that applied the original bodies fails startup with `migration <version> was previously applied but has been modified` and crash-loops. A developer OP was down ~24h on exactly this; with the API dead, Traefik had no backend and dependent relying parties (a lets-chat dev stack) could not complete OIDC discovery. ## Fix New `bunyip-api/src/migrate_reconcile.rs`, run once at startup before the migrator. For each of the 11 affected versions, if the recorded checksum still equals the pre-edit value, it is rewritten to the current embedded checksum. The UPDATE is guarded by `AND checksum = <pre-edit hash>`: - DBs that applied the originals (recorded == pre-edit) are healed. - DBs freshly migrated after `9c082eb` (recorded == current) are untouched (`rows_affected == 0`). - Any genuinely-unexpected edit on these versions (recorded == neither) is untouched, so the immutability ratchet still catches real mistakes. The replacement value is read from the embedded migrator, not hardcoded, so it stays correct if a listed file is touched again. ## Verification - `legacy_allowlist_matches_embedded_migrator` unit test: every listed version exists in the embedded set, each hash is a well-formed SHA-384, and the pre-edit hash differs from the current embedded hash (proving the file really was edited). Runs under `just test` (`--lib`), no database required. - End-to-end against the dev OP: re-introduced the pre-edit checksums on two versions, restarted, and confirmed the log shows `BUNYIP-79 reconcile: rewrote stale migration checksum` for both, `reconciled 2 ... count=2`, then `Database migrations completed successfully` and the API listening, instead of crash-looping. The two rows are left at the current file hashes (idempotent thereafter). - `cargo fmt`, `cargo clippy --lib -p bunyip-api`, and `scripts/check-migration-versions.sh` are clean. ## Scope / follow-up Reconciliation only unblocks startup. A row marked applied is never re-run, so the DDL guards `9c082eb` added are NOT retro-applied to a database that already ran the original bodies. Delivering those guards to already-migrated data requires forward-only migrations and is intentionally left as a follow-up; the README and module docs state this and reinforce that future behavioral changes must ship as fresh migrations, not in-place edits.
fix(migrations): auto-reconcile BUNYIP-79 in-place-edited checksums at startup
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 2m36s
4b971c7f5e
Commit 9c082eb edited 11 already-applied migration files in place. sqlx records a per-file SHA-384 in `_sqlx_migrations` and recomputes it on every start, so any database that applied the original bodies fails startup with `migration <version> was previously applied but has been modified` and crash-loops until the recorded checksums are reconciled by hand. A developer OP was down ~24h on exactly this.

Add `bunyip-api/src/migrate_reconcile.rs`, run once at startup before the migrator: for each of the 11 affected versions, if the recorded checksum still equals the pre-edit value, rewrite it to the current embedded checksum. The UPDATE is guarded by `AND checksum = <pre-edit hash>`, so it heals only databases stuck on the old hash and is a no-op on freshly-migrated databases (recorded already equals the new hash) and on any unexpected drift (the immutability check still catches genuine mistakes on these versions). The replacement value is read from the embedded migrator rather than hardcoded, so it stays correct if a listed file is touched again.

A unit test asserts the allowlist stays consistent with the embedded migration set (every version present, each hash a well-formed SHA-384, and the pre-edit hash differing from the current embedded hash). Verified end-to-end against the dev OP: re-introducing the pre-edit checksums on two versions and restarting now reconciles them and boots cleanly instead of crash-looping.

Reconciliation only unblocks startup; it does not retro-apply the DDL guards 9c082eb added to a database that already ran the original bodies (a row marked applied is never re-run). Delivering those guards to already-migrated data needs forward-only migrations and is left as follow-up; the README and module docs say so, and reinforce that new changes must ship as fresh migrations, not in-place edits.

Closes #172.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/bunyip-79-migration-checksum-reconcile 2026-06-19 03:37:14 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!173
No description provided.