feat(migrations): reconcile collections columns missed by initial-schema backfill (B2) #150
1 changed files with 114 additions and 0 deletions
114
migrations/1781000000002_reconcile-collections-columns.js
Normal file
114
migrations/1781000000002_reconcile-collections-columns.js
Normal file
|
|
@ -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.'
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue