feat(migrations): reconcile user_cards UNIQUE constraint per Option A (B6) #149
No reviewers
Labels
No labels
agent-context-drift
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: rstillwell/deckhearth#149
Loading…
Reference in a new issue
No description provided.
Delete branch "convoy/reconcile-b6-user-cards-unique"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Brief 6 of the
reconcile-historical-add-scriptsconvoy. Adds one newmigration
migrations/1781000000006_reconcile-user-cards-unique.jsthat defensively reconciles the
user_cardsUNIQUE constraint to thecanonical 3-column shape declared by
migrations/1779853647564_initial-schema.js(
UNIQUE(user_id, card_id, is_foil)).Finding 1 — operator decision (Option A, ratified 2026-06-14)
The convoy architect surfaced a conflict between two historical sources:
migrations/1779853647564_initial-schema.jsdeclaresUNIQUE(user_id, card_id, is_foil)(3-col) onuser_cards(line 82).scripts/fix-user-cards-constraints.jswouldADD CONSTRAINT user_cards_user_card_unique UNIQUE (user_id, card_id)(2-col, stricter).The operator picked Option A: keep the 3-col constraint, drop /
treat-as-no-op the 2-col one. Foil and non-foil copies of the same card
are semantically separate rows. The strict 2-col UNIQUE forbids that
distinction. The historical script was either never applied to prod OR
applied and then reverted; current prod has the 3-col version (matching
initial-schema, runtime confirmed against
pages/api/cards/[id]/ownership.jsand theuser_cardsownership flow).Migration shape
Two
pgm.sql()blocks, both idempotent:ALTER TABLE user_cards DROP CONSTRAINT IF EXISTS user_cards_user_card_unique;— defensive, removes the stricter 2-colconstraint only if an env happened to apply
scripts/fix-user-cards-constraints.js. No-op everywhere else.DO $$ BEGIN ALTER TABLE user_cards ADD CONSTRAINT user_cards_user_id_card_id_is_foil_key UNIQUE (user_id, card_id, is_foil); EXCEPTION WHEN duplicate_object THEN NULL; END $$;—re-asserts the canonical 3-col constraint under its
Postgres-auto-generated name (the same name
initial-schema's inlineUNIQUE(...)produces). No-op against fresh / initial-schema-appliedenvs; constructive against any env where the constraint had been
dropped.
down()is a hard-stub throw — reverting would re-install the 2-colconstraint (the regression Option A rejected).
Constraint names verified
user_cards_user_id_card_id_is_foil_key— Postgres auto-generatedname for the inline
UNIQUE(user_id, card_id, is_foil)declaration inmigrations/1779853647564_initial-schema.jsline 82. Convention:<table>_<col1>_<col2>_..._key. Used in the migration'sDO $$ EXCEPTIONADD block.user_cards_user_card_unique— the explicit name used byscripts/fix-user-cards-constraints.jsline 52. Used in the migration'sdefensive
DROP CONSTRAINT IF EXISTS.(Note: the brief speculatively suggested the script's would-be name might
be the default
user_cards_user_id_card_id_key. The actual script namesits constraint explicitly; the migration uses the script's literal name.)
Why this is safe against all 3 prod states
initial-schemaonly)pgmigrationsrow insertedfix-user-cards-constraints.jspgmigrationsrow insertedfix-user-cards-constraints.jsinitial-schema)Verification
node --check migrations/1781000000006_*.js→ exit 0npm run lint→ 0 errors (1 unrelated pre-existing warning incomponents/CollectionsPageView.js, baseline)npm run test:run→ 26 files / 131 tests passinitial-schema.jsline 82and
scripts/fix-user-cards-constraints.jsline 52Optional Neon-branch verification (deferred to convoy B7's runbook):
post-migration,
INSERTtwo rows with same(user_id, card_id)butis_foil=trueandis_foil=falseto confirm both coexist; insert athird duplicate of either to confirm the
_keyconstraint rejects itwith
duplicate key value violates unique constraint "user_cards_user_id_card_id_is_foil_key".Files changed
migrations/1781000000006_reconcile-user-cards-unique.js(new, 100 LOC)Test plan
node --checknpm run migrate upagainst prod after merge — expected: no-op(DROP doesn't match, ADD's EXCEPTION swallows duplicate), only
change is the new
pgmigrationsrownpm run migrate upagainst a fresh Neon branchcreated from empty parent — expected: no-op DDL-wise, only change
is the
pgmigrationsrow (sequence:initial-schemaalready declaredthe 3-col under auto-gen name; this migration's ADD's EXCEPTION
swallows)
Notes for reviewer
depends_on: []in the convoy'sslice_dependencies:block — disjoint files vs B1–B5; PR base ismain, not the convoy parent branch.docs/SCHEMA_MAP.md, and AGENTS.mdGotcha #6 once B1–B6 land. This PR does not touch any of those.
scripts/fix-user-cards-constraints.jsisuntouched (no-go-zones rule). It will be retired by the queued
retire-graveyard-scripts-after-auditfollow-up once all of B1–B6land.
Made with Cursor
The latest updates on your projects. Learn more about Vercel for GitHub.
Pipeline Health
Build + CI gates
Build runs on Vercel; this CI runs lint and schema-map drift only (no duplicate build).
Role reports
See individual comments above for details. This rollup updates automatically.