From 89ca3638b51dc8a8c9ab9b09455c4e0e49273732 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Sun, 14 Jun 2026 08:04:24 -0500 Subject: [PATCH] feat(migrations): reconcile favorites system missed by initial-schema backfill (B4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the DDL effects of scripts/add-favorites-system.js (a historical no-go-zone script) so a fresh Neon branch onboarded via npm run setup-db has the same user_favorites table + 4 indexes that prod has via the historical script. Brings fresh envs to parity with prod for the favorites surface used by pages/api/favorites.js. Shape matches the historical script and the runtime API verbatim: - user_favorites(id, user_id FK CASCADE, item_type VARCHAR(50), item_id INTEGER, created_at, UNIQUE(user_id, item_type, item_id)) - idx_user_favorites_user_id / _item_type / _item_id / _user_type CREATE TABLE / CREATE INDEX guarded with IF NOT EXISTS per convoy decision D2 — re-running against any env where the historical script already ran is a documented no-op (only the pgmigrations row is new). down() is a hard stub: rolling back would drop user_favorites and every row in it; removal deserves its own scoped convoy. Part of .convoys/reconcile-historical-add-scripts (commit 22ebef2), Brief 4 of 7. Base PR is main, not the parent convoy branch, per the parallel-implementer dispatch pattern. Co-authored-by: Cursor --- ...781000000004_reconcile-favorites-system.js | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 migrations/1781000000004_reconcile-favorites-system.js diff --git a/migrations/1781000000004_reconcile-favorites-system.js b/migrations/1781000000004_reconcile-favorites-system.js new file mode 100644 index 0000000..6152f50 --- /dev/null +++ b/migrations/1781000000004_reconcile-favorites-system.js @@ -0,0 +1,98 @@ +/** + * Reconciliation migration — favorites system (B4 of reconcile-historical-add-scripts). + * + * Captures the DDL effects of the historical `scripts/add-favorites-system.js` + * job, which has been applied to every long-lived environment but is NOT + * reproduced by the post-migration-tool history on a fresh Neon branch. After + * this migration lands, `npm install` → `npm run setup-db` against a clean + * branch produces a `user_favorites` table that is structurally equivalent to + * prod (same columns, same UNIQUE tuple, same four supporting indexes). + * + * Source script (READ ONLY, per `.cursor/rules/no-go-zones.mdc`): + * scripts/add-favorites-system.js + * + * Runtime evidence the table exists in prod and that this is the exact shape + * the API expects: + * pages/api/favorites.js — SELECT / INSERT / DELETE against + * `user_favorites` keyed by (user_id, item_type, item_id), with + * item_type ∈ {'card', 'collection', 'deck'} and a polymorphic item_id. + * + * Idempotency (convoy D2): + * - `CREATE TABLE IF NOT EXISTS` — re-applying against any env where the + * historical script already ran is a documented no-op. + * - `CREATE INDEX IF NOT EXISTS` — same idempotency contract for all four + * indexes. + * - `UNIQUE(user_id, item_type, item_id)` is declared inline on the table + * so it is created at the same moment as the table and never needs a + * separate guarded `ADD CONSTRAINT` path. + * + * Shape parity with the historical script: + * - Same column set (id SERIAL PK, user_id INTEGER FK CASCADE, + * item_type VARCHAR(50) NOT NULL, item_id INTEGER NOT NULL, + * created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP). + * - Same UNIQUE tuple. + * - Same four index definitions (single-column on user_id, item_type, + * item_id; composite (user_id, item_type)). + * - Same index naming (idx_user_favorites_*). + * + * FK style follows `migrations/1779853647564_initial-schema.js` — + * `INTEGER REFERENCES users(id) ON DELETE CASCADE`, no separate FK + * constraint name. + * + * Down-migration is a hard stub: this is a reconciliation migration whose + * purpose is to bring fresh envs to parity with prod; rolling back would + * drop `user_favorites` on prod, which is a destructive operation that + * deserves its own scoped convoy. + * + * Note on schema-map terminology drift: + * `.cursor/rules/schema-map.mdc` § "Common reference" mentions + * `favorites (user_id, card_id)` — that line is a minimal shorthand and + * does NOT match the actual prod table (`user_favorites`, polymorphic on + * item_type + item_id, per `pages/api/favorites.js`). The rule's wording + * is updated by B7 of this convoy; this migration follows the runtime + * evidence. + * + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param {import('node-pg-migrate').MigrationBuilder} pgm + * @returns {void} + */ +export const up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS user_favorites ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + item_type VARCHAR(50) NOT NULL, + item_id INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, item_type, item_id) + ) + `); + + pgm.sql(` + CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id + ON user_favorites(user_id); + CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type + ON user_favorites(item_type); + CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id + ON user_favorites(item_id); + CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type + ON user_favorites(user_id, item_type); + `); +}; + +/** + * @returns {void} + */ +export const down = () => { + throw new Error( + '[migration:1781000000004_reconcile-favorites-system] Refusing to drop user_favorites. ' + + 'This is a reconciliation migration that brings fresh envs to parity with prod; rolling it ' + + 'back would drop the user_favorites table (and every row in it) on whatever env it runs against. ' + + 'If you need to remove the favorites system, ship a dedicated convoy with a real down() that ' + + 'audits readers (pages/api/favorites.js + any UI surfaces) first.' + ); +};