fix(migrations): backfill entitlement source-check before 20260802000010 #456

Merged
nrupard merged 2 commits from fix/BUNYIP-457-entitlement-source-check-backfill into main 2026-08-04 16:23:38 +02:00
Owner

What

Adds migrate_reconcile::backfill_entitlement_source_check, a scoped startup backfill that delivers the application_entitlements_source_check constraint to databases that never received it, so migration 20260802000010 no longer aborts bunyip-api startup on them.

Why

20260605000010's CHECK (source IN ('admin', 'stripe', 'backfill')) on application_entitlements.source was added by the 9c082eb in-place edit (BUNYIP-79). migrate_reconcile heals the checksum on pre-edit databases but, per its own header and migrations/README.md, never retro-applies the DDL. So a database that ran the pre-edit body has application_entitlements with no application_entitlements_source_check constraint. 20260802000010 (first in v0.11.0) then widens that constraint to allow 'seed' with a bare ALTER TABLE ... DROP CONSTRAINT application_entitlements_source_check (no IF EXISTS), which raises constraint ... does not exist and aborts startup. Observed on nc-01; 0.11.0 was rolled back to 0.10.0.

A forward-only migration cannot fix this: 20260802000010 is itself the failing migration, so on an affected database the chain never reaches a later one.

Change

  • bunyip-api/src/migrate_reconcile.rs: new backfill_entitlement_source_check. Sequential guards (so no ::regclass cast hits an absent relation): return if 20260802000010 is already applied; return if application_entitlements is absent; return if the constraint already exists; otherwise ADD CONSTRAINT application_entitlements_source_check CHECK (source IN ('admin', 'stripe', 'backfill')). The migrator then runs 20260802000010's DROP + ADD, ending at the four-value constraint incl 'seed'.
  • bunyip-api/src/main.rs: invoke it after reconcile_legacy_migration_checksums and before Migrator::run.
  • bunyip-api/migrations/README.md + the module header: document the backfill and why it ships via the hook rather than a forward migration.

Idempotent and tightly scoped: a no-op once 20260802000010 is applied (so it can never fight a future migration that deliberately alters the constraint), on fresh databases (the table is created in order by the migrator), and on every boot after it has run once. 20260802000010 and LEGACY_CHECKSUMS are left untouched, so already-applied databases (c-01/prod) need no checksum reconciliation.

Verify

  • just check-container (fmt + clippy -D warnings + workspace tests) passes.
  • Validated end-to-end against postgres:16: the nc-01 shape (table without the constraint, 802 unapplied) backfills, then 20260802000010 applies and the constraint ends as CHECK (source IN ('admin', 'stripe', 'backfill', 'seed')); the c-01 shape (802 applied) skips; the fresh shape (table absent) skips.

Deploy

No version bump in this PR (bump to 0.11.1 via just create-release hotfix when ready). On the next 0.11.1 deploy, nc-01 self-heals (backfill adds the constraint, then 20260802000010 applies); c-01/prod are unaffected (802 already applied, its checksum unchanged). No per-server manual SQL.

Closes BUNYIP-457.

## What Adds `migrate_reconcile::backfill_entitlement_source_check`, a scoped startup backfill that delivers the `application_entitlements_source_check` constraint to databases that never received it, so migration `20260802000010` no longer aborts bunyip-api startup on them. ## Why `20260605000010`'s `CHECK (source IN ('admin', 'stripe', 'backfill'))` on `application_entitlements.source` was added by the `9c082eb` **in-place edit** (BUNYIP-79). `migrate_reconcile` heals the checksum on pre-edit databases but, per its own header and `migrations/README.md`, never retro-applies the DDL. So a database that ran the pre-edit body has `application_entitlements` with **no** `application_entitlements_source_check` constraint. `20260802000010` (first in v0.11.0) then widens that constraint to allow `'seed'` with a bare `ALTER TABLE ... DROP CONSTRAINT application_entitlements_source_check` (no `IF EXISTS`), which raises `constraint ... does not exist` and aborts startup. Observed on nc-01; 0.11.0 was rolled back to 0.10.0. A forward-only migration cannot fix this: `20260802000010` is itself the failing migration, so on an affected database the chain never reaches a later one. ## Change - `bunyip-api/src/migrate_reconcile.rs`: new `backfill_entitlement_source_check`. Sequential guards (so no `::regclass` cast hits an absent relation): return if `20260802000010` is already applied; return if `application_entitlements` is absent; return if the constraint already exists; otherwise `ADD CONSTRAINT application_entitlements_source_check CHECK (source IN ('admin', 'stripe', 'backfill'))`. The migrator then runs `20260802000010`'s `DROP` + `ADD`, ending at the four-value constraint incl `'seed'`. - `bunyip-api/src/main.rs`: invoke it after `reconcile_legacy_migration_checksums` and before `Migrator::run`. - `bunyip-api/migrations/README.md` + the module header: document the backfill and why it ships via the hook rather than a forward migration. Idempotent and tightly scoped: a no-op once `20260802000010` is applied (so it can never fight a future migration that deliberately alters the constraint), on fresh databases (the table is created in order by the migrator), and on every boot after it has run once. `20260802000010` and `LEGACY_CHECKSUMS` are left untouched, so already-applied databases (c-01/prod) need **no** checksum reconciliation. ## Verify - `just check-container` (fmt + clippy `-D warnings` + workspace tests) passes. - Validated end-to-end against `postgres:16`: the nc-01 shape (table without the constraint, 802 unapplied) backfills, then `20260802000010` applies and the constraint ends as `CHECK (source IN ('admin', 'stripe', 'backfill', 'seed'))`; the c-01 shape (802 applied) skips; the fresh shape (table absent) skips. ## Deploy No version bump in this PR (bump to 0.11.1 via `just create-release hotfix` when ready). On the next 0.11.1 deploy, nc-01 self-heals (backfill adds the constraint, then `20260802000010` applies); c-01/prod are unaffected (802 already applied, its checksum unchanged). No per-server manual SQL. Closes BUNYIP-457.
fix(migrations): backfill entitlement source-check before 20260802000010
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 4s
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 15s
bbaedc6c36
20260605000010's CHECK (source IN ('admin','stripe','backfill')) on application_entitlements.source was added by the 9c082eb in-place edit (BUNYIP-79), so migrate_reconcile heals the checksum but never retro-applies the DDL. A database that ran the pre-edit body therefore has application_entitlements with no application_entitlements_source_check constraint. 20260802000010 then widens that constraint to allow 'seed' with a bare DROP CONSTRAINT (no IF EXISTS), which raises "constraint ... does not exist" and aborts startup (observed on nc-01; 0.11.0 was rolled back to 0.10.0).

A forward-only migration cannot fix it: 20260802000010 is itself the failing migration, so the chain never reaches a later one on the affected database. Add migrate_reconcile::backfill_entitlement_source_check, run from main after the checksum reconcile and before the migrator: if 20260802000010 is not yet applied, application_entitlements exists, and the constraint is absent, add CHECK (source IN ('admin','stripe','backfill')) so the migrator's DROP + ADD then succeeds and widens it to include 'seed'. Tightly scoped and idempotent: a no-op once 802 is applied, on fresh databases, and on every later boot, and it cannot fight a future migration that deliberately alters the constraint. 20260802000010 and LEGACY_CHECKSUMS are left untouched, so already-applied databases (c-01/prod) need no checksum reconciliation. Verified against postgres:16 for the nc-01, c-01, and fresh shapes; documented in migrations/README.md and the module header.

#BUNYIP-457

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: nrupard <natrsmith11@gmail.com>
ci(migrations): scope immutability gate to *.sql so doc edits pass
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 30s
Check / fmt + clippy + build + tests (pull_request) Successful in 7m1s
Create release / Create release from merged PR (pull_request) Has been skipped
49aa3cb4a7
check-migration-immutability.sh diffed the whole bunyip-api/migrations directory with --diff-filter=MRD and no extension filter, so any change to bunyip-api/migrations/README.md (documentation) failed the gate even though sqlx::migrate! only checksums *.sql and never reads the README. This blocked the legitimate README update in this PR.

Filter the MRD name-only diff to paths ending in .sql before the emptiness check. A modified, renamed, or deleted *.sql still ends in .sql, so real-migration coverage is unchanged; only non-.sql docs in the dir become editable. The fail-loud exit 2 on a diff error (missing base ref / shallow clone) is untouched. Verified: the gate now passes on a README-only change and still fails on a *.sql modification.

#BUNYIP-458

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: nrupard <natrsmith11@gmail.com>
nrupard scheduled this pull request to auto merge when all checks succeed 2026-08-04 16:19:11 +02:00
nrupard deleted branch fix/BUNYIP-457-entitlement-source-check-backfill 2026-08-04 16:23:38 +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!456
No description provided.