v0.1.0:41 — second hotfix to migration 0009; migration regression tests
The v0.1.0:40 migration was correct on clean installs but crashed at COMMIT on any database with rows in discount_redemptions: SQLite's deferred FK check saw the dropped parent's bookkeeping as unsatisfied even after the rename. Fix is to rebuild discount_redemptions in the same transaction (stash → drop → rebuild → restore) plus orphan cleanup. Migration is idempotent; operators on :40 with a checksum mismatch recover by deleting the version=9 row from _sqlx_migrations and restarting. Lands the missing migration test scaffolding too. The four tests in licensing-service/tests/migrations.rs apply migrations against a realistic populated database (products, policies, invoices, licenses, machines, discount codes, redemptions, webhooks, tip attempts). The regression test fails with the exact 787 error against the v40 migration — would have caught the bug pre-release. KEYSAT_INTEGRATION.md is removed from this repo; it now lives in the parent licensing/ folder.
This commit is contained in:
@@ -9,8 +9,36 @@
|
||||
import { VersionInfo } from '@start9labs/start-sdk'
|
||||
|
||||
export const v0_1_0 = VersionInfo.of({
|
||||
version: '0.1.0:40',
|
||||
version: '0.1.0:41',
|
||||
releaseNotes: [
|
||||
`Alpha-iteration revision 41 of v0.1.0 — Second hotfix to migration 0009. v40 passed on a clean install but crashed on any database with existing rows in discount_redemptions. Also lands the migration test infrastructure that would have caught this before it shipped.`,
|
||||
``,
|
||||
`**The bug.** v40 rebuilt only the discount_codes table (using \`PRAGMA defer_foreign_keys = 1\` to avoid intra-transaction FK errors). On a clean install that works. On any install with even one row in discount_redemptions — the child table whose foreign key points back into discount_codes — it doesn't. SQLite's deferred FK check fires at COMMIT, sees the dropped parent's row-deletion bookkeeping as unsatisfied (regardless of whether the rebuilt table contains the same IDs), and rolls the whole transaction back with "FOREIGN KEY constraint failed (787)". Daemon kept restart-looping; API never came up.`,
|
||||
``,
|
||||
`**Why defer_foreign_keys wasn't enough.** When you DROP a table with inbound foreign keys from another rowset, SQLite's commit-time check enumerates those inbound rows and verifies they reference valid parents. The rebuild renames discount_codes_new back to discount_codes — same name, same row IDs — but SQLite's FK bookkeeping had tracked "the original discount_codes was dropped," and the deferred check sees that as a violation. defer_foreign_keys postpones the firing, but doesn't reconcile the bookkeeping.`,
|
||||
``,
|
||||
`**The fix.** Rebuild discount_redemptions inside the same transaction so its FK is freshly bound to the new discount_codes:`,
|
||||
``,
|
||||
`1. Heal pre-existing orphan FKs in discount_codes — NULL-out applies_to_product_id / applies_to_policy_id rows pointing at deleted parents.`,
|
||||
`2. Delete orphan redemptions (rows whose code_id no longer exists).`,
|
||||
`3. Stash discount_redemptions to a TEMP table, then DROP it — eliminates the inbound FK chain.`,
|
||||
`4. Rebuild discount_codes with the new CHECK constraint that includes 'set_price'.`,
|
||||
`5. Recreate discount_redemptions and restore data from the stash.`,
|
||||
``,
|
||||
`The COMMIT-time FK check now passes because both tables are clean and consistent, with FK references freshly bound at table-creation time rather than carried over from before the drop.`,
|
||||
``,
|
||||
`**Migration is idempotent.** Re-running 0009 against an already-migrated database produces the same end state. Operators who hit the v40 failure and worked around it (manual deletion of redemptions, then reboot) will see a checksum mismatch on this update — sqlx records each migration's content hash and v41's 0009 differs from v40's. Recovery is one command on the StartOS service shell:`,
|
||||
``,
|
||||
`\`\`\``,
|
||||
`sqlite3 /data/keysat.db "DELETE FROM _sqlx_migrations WHERE version = 9;"`,
|
||||
`\`\`\``,
|
||||
``,
|
||||
`Then restart the service. The new (idempotent) 0009 re-applies cleanly. No data loss — the migration is a structural rebuild, not a data change.`,
|
||||
``,
|
||||
`**Migration regression tests.** Both v39 and v40 shipped because no test exercised migrations against a populated database — every test started from an empty DB. v41 lands \`licensing-service/tests/migrations.rs\` with four integration tests that boot a real SQLite, seed realistic fixtures (products, policies, invoices, licenses, machines, discount codes, redemptions, webhooks, tip attempts), and apply migrations against the populated state. The regression test fails with the exact error 787 against the v40 migration; future migrations get the same scrutiny automatically. \`cargo test\` now reports 13 tests, up from 9.`,
|
||||
``,
|
||||
`**No data loss in v40's failure.** sqlx rolled back the wrapping transaction; the discount_codes / discount_redemptions tables are unchanged from v38 state.`,
|
||||
``,
|
||||
`Alpha-iteration revision 40 of v0.1.0 — Hotfix: migration 0009 in v0.1.0:39 was malformed and put the daemon in a startup-restart loop. This release is the corrected migration; install it to recover.`,
|
||||
``,
|
||||
`**The bug.** Migration 0009 in :39 did its own \`BEGIN TRANSACTION\` / \`COMMIT\` and a \`PRAGMA foreign_keys = OFF\`. sqlx-migrate already wraps each .sql file in a transaction, and SQLite doesn't allow nested transactions — so the inner BEGIN failed, sqlx rolled back the wrapping txn, the migration was never recorded as applied, and the daemon panicked on every boot. StartOS showed "Running" but the API was unreachable because the process kept exiting before binding port 8080.`,
|
||||
|
||||
Reference in New Issue
Block a user