116ed0d1f8
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.
132 lines
6.4 KiB
SQL
132 lines
6.4 KiB
SQL
-- Allow `kind = 'set_price'` on discount_codes (added at the daemon
|
|
-- level in v0.1.0:26 but the migration that created the CHECK constraint
|
|
-- in 0004 didn't include it, so existing instances reject the new kind
|
|
-- with "CHECK constraint failed").
|
|
--
|
|
-- SQLite doesn't support ALTER TABLE ... DROP/ALTER CONSTRAINT, so we
|
|
-- rebuild discount_codes: copy → drop old → rename. sqlx-migrate already
|
|
-- wraps each .sql file in a transaction; nested transactions aren't
|
|
-- allowed, so we don't BEGIN here.
|
|
--
|
|
-- Earlier revisions of this migration relied on `PRAGMA defer_foreign_keys`
|
|
-- alone to let DROP TABLE discount_codes succeed while discount_redemptions
|
|
-- still had FK references back into it. That fails at COMMIT on any
|
|
-- instance with even one row in discount_redemptions: SQLite's deferred
|
|
-- FK check sees the dropped parent's row-deletion bookkeeping as
|
|
-- unsatisfied, regardless of whether discount_codes_new (now renamed
|
|
-- back to discount_codes) contains the same IDs. SQLite error 787,
|
|
-- whole transaction rolls back, daemon won't boot.
|
|
--
|
|
-- The robust fix is to rebuild discount_redemptions inside the same
|
|
-- transaction so its FK is freshly bound to the new discount_codes:
|
|
-- 1. heal any pre-existing orphan rows
|
|
-- 2. stash discount_redemptions into a TEMP table
|
|
-- 3. drop discount_redemptions (eliminates the inbound FK)
|
|
-- 4. rebuild discount_codes with the new CHECK
|
|
-- 5. recreate discount_redemptions and restore data
|
|
-- defer_foreign_keys still postpones intra-transaction FK firing; the
|
|
-- COMMIT-time check passes because both tables are clean and consistent.
|
|
--
|
|
-- This migration is idempotent: re-running it produces the same end state.
|
|
-- That matters because operators who hit the broken first revision and
|
|
-- worked around it (manually deleting redemptions then booting) will get
|
|
-- a checksum mismatch on the next .s9pk update; the recovery is to
|
|
-- DELETE FROM _sqlx_migrations WHERE version = 9 and let this fixed
|
|
-- version re-apply.
|
|
|
|
PRAGMA defer_foreign_keys = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 1. Heal orphan FK references inherited from earlier manual SQL wipes.
|
|
-- ---------------------------------------------------------------------------
|
|
UPDATE discount_codes
|
|
SET applies_to_product_id = NULL
|
|
WHERE applies_to_product_id IS NOT NULL
|
|
AND applies_to_product_id NOT IN (SELECT id FROM products);
|
|
|
|
UPDATE discount_codes
|
|
SET applies_to_policy_id = NULL
|
|
WHERE applies_to_policy_id IS NOT NULL
|
|
AND applies_to_policy_id NOT IN (SELECT id FROM policies);
|
|
|
|
-- A redemption pointing at a no-longer-existing discount_code can't be
|
|
-- meaningful — drop it. The first revision of this migration didn't
|
|
-- handle this case at all, so any leftover orphans have to go.
|
|
DELETE FROM discount_redemptions
|
|
WHERE code_id NOT IN (SELECT id FROM discount_codes);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 2. Stash discount_redemptions, drop the table to break the inbound FK.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TEMP TABLE _dr_stash AS SELECT * FROM discount_redemptions;
|
|
DROP TABLE discount_redemptions;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 3. Rebuild discount_codes with the new CHECK constraint.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE discount_codes_new (
|
|
id TEXT PRIMARY KEY,
|
|
code TEXT NOT NULL UNIQUE,
|
|
kind TEXT NOT NULL, -- 'percent' | 'fixed_sats' | 'set_price' | 'free_license'
|
|
amount INTEGER NOT NULL,
|
|
max_uses INTEGER,
|
|
used_count INTEGER NOT NULL DEFAULT 0,
|
|
expires_at TEXT,
|
|
applies_to_product_id TEXT,
|
|
applies_to_policy_id TEXT,
|
|
referrer_label TEXT,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY (applies_to_product_id) REFERENCES products(id),
|
|
FOREIGN KEY (applies_to_policy_id) REFERENCES policies(id),
|
|
CHECK (kind IN ('percent', 'fixed_sats', 'set_price', 'free_license')),
|
|
CHECK (amount >= 0),
|
|
CHECK (used_count >= 0)
|
|
);
|
|
|
|
INSERT INTO discount_codes_new
|
|
SELECT id, code, kind, amount, max_uses, used_count, expires_at,
|
|
applies_to_product_id, applies_to_policy_id, referrer_label,
|
|
description, active, created_at, updated_at
|
|
FROM discount_codes;
|
|
|
|
DROP TABLE discount_codes;
|
|
ALTER TABLE discount_codes_new RENAME TO discount_codes;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_discount_codes_active ON discount_codes(active);
|
|
CREATE INDEX IF NOT EXISTS idx_discount_codes_product ON discount_codes(applies_to_product_id);
|
|
CREATE INDEX IF NOT EXISTS idx_discount_codes_policy ON discount_codes(applies_to_policy_id);
|
|
CREATE INDEX IF NOT EXISTS idx_discount_codes_expires ON discount_codes(expires_at);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 4. Recreate discount_redemptions (same shape as 0004) and restore data.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE discount_redemptions (
|
|
id TEXT PRIMARY KEY,
|
|
code_id TEXT NOT NULL,
|
|
invoice_id TEXT NOT NULL,
|
|
license_id TEXT,
|
|
status TEXT NOT NULL,
|
|
discount_applied_sats INTEGER NOT NULL,
|
|
base_price_sats INTEGER NOT NULL,
|
|
final_price_sats INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY (code_id) REFERENCES discount_codes(id),
|
|
FOREIGN KEY (invoice_id) REFERENCES invoices(id),
|
|
FOREIGN KEY (license_id) REFERENCES licenses(id),
|
|
CHECK (status IN ('pending', 'redeemed', 'cancelled'))
|
|
);
|
|
|
|
INSERT INTO discount_redemptions SELECT * FROM _dr_stash;
|
|
DROP TABLE _dr_stash;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_discount_redemptions_code ON discount_redemptions(code_id);
|
|
CREATE INDEX IF NOT EXISTS idx_discount_redemptions_invoice ON discount_redemptions(invoice_id);
|
|
CREATE INDEX IF NOT EXISTS idx_discount_redemptions_license ON discount_redemptions(license_id);
|
|
CREATE INDEX IF NOT EXISTS idx_discount_redemptions_status ON discount_redemptions(status);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_discount_redemptions_one_per_invoice
|
|
ON discount_redemptions(invoice_id);
|