diff --git a/migrations/1781000000002_reconcile-collections-columns.js b/migrations/1781000000002_reconcile-collections-columns.js new file mode 100644 index 0000000..b8f7439 --- /dev/null +++ b/migrations/1781000000002_reconcile-collections-columns.js @@ -0,0 +1,114 @@ +/** + * Reconcile-historical-add-scripts convoy — Brief 2. + * + * Backfills the `collections`-table columns that historical scripts added to + * prod but which `migrations/1779853647564_initial-schema.js` never captured: + * + * - `visibility VARCHAR(20) DEFAULT 'private'` (from `scripts/add-collaboration-features.js`, lines 15-21 — collections half only) + * - `tcg VARCHAR(50) DEFAULT 'MTG'` (from `scripts/add-collaboration-features.js`, lines 15-21 — collections half only) + * - `tags TEXT` (from `scripts/add-collaboration-features.js`, lines 15-21 — collections half only) + * - `slug VARCHAR(100) UNIQUE` (from `scripts/add-collection-slugs.js`, lines 17-20) + * - `idx_collections_slug` UNIQUE INDEX (from `scripts/add-collection-slugs.js`, line 79) + * - `check_slug_format` CHECK constraint (from `scripts/add-collection-slugs.js`, line 91) + * - `image TEXT` (from `scripts/add-image-column.js`, lines 14-17) + * + * Excluded from B2 (out of scope per architect inventory): + * - `collection_permissions`, `collection_activity`, `users.is_pending`, + * `idx_collections_visibility`, `idx_collection_permissions_*`, + * `idx_collection_activity_collection_id` — these belong to B3 + * (`reconcile-collaboration-tables`) per the architect plan. + * - `is_system_collection` — already captured by + * `migrations/1780378340194_system-collection-description.js`. + * + * Excluded DML: per-row UPDATE that backfills slugs from `name` via + * `lib/slug-utils.js::generateUniqueSlug` (script lines 41-74). Generating + * slugs on a fresh env is moot — there are no pre-existing collections to + * backfill — and replicating the runtime helper inside a migration would + * couple the schema timeline to application code. Operators of long-lived + * envs already ran the backfill historically; this migration does NOT + * re-run it. + * + * Idempotency strategy (D2): every statement is `IF NOT EXISTS`-guarded. + * + * - `ADD COLUMN IF NOT EXISTS` — Postgres native; the `UNIQUE` inline + * clause on `slug` is ignored when the column already exists, so prod + * (which already has the autogen `collections_slug_key` constraint) + * and fresh envs (where the autogen constraint will be added) both + * converge correctly. + * - `CREATE UNIQUE INDEX IF NOT EXISTS` — Postgres native. + * - `ALTER TABLE ... ADD CONSTRAINT` for CHECK — Postgres has no + * `IF NOT EXISTS` clause for named constraints, so we wrap in a + * `DO $$ BEGIN ... END $$` block that checks `pg_constraint` first. + * + * Down-migration is a hard stub. See `down()` below — matches + * `1779853647564_initial-schema.js` style. + * + * @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(` + ALTER TABLE collections + ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'private', + ADD COLUMN IF NOT EXISTS tcg VARCHAR(50) DEFAULT 'MTG', + ADD COLUMN IF NOT EXISTS tags TEXT + `); + + pgm.sql(` + ALTER TABLE collections + ADD COLUMN IF NOT EXISTS image TEXT + `); + + pgm.sql(` + ALTER TABLE collections + ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE + `); + + pgm.sql(` + CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug + ON collections(slug) + `); + + pgm.sql(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'check_slug_format' + AND conrelid = 'collections'::regclass + ) THEN + ALTER TABLE collections + ADD CONSTRAINT check_slug_format + CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50); + END IF; + END + $$; + `); +}; + +/** + * Down-migration is a hard stub. This is a reconciliation migration: the + * prod schema state is the source of truth, and dropping these columns + * would discard slugs / visibility flags / tcg labels / tags / images that + * application code reads on every collection page render. If you need a + * clean schema for testing, branch the Neon database and run + * `npm run migrate up` against the branch instead of rolling this back. + * + * @returns {void} + */ +export const down = () => { + throw new Error( + '[migration:1781000000002_reconcile-collections-columns] Refusing to drop reconciled columns. ' + + 'Rolling back this migration would erase visibility/tcg/tags/slug/image data on every ' + + 'collections row in the database. If you need a clean schema for testing, branch the ' + + 'Neon database and run `npm run migrate up` against the branch instead. See ' + + 'migrations/1781000000002_reconcile-collections-columns.js up() docstring for the ' + + 'reconciliation rationale.' + ); +};