convoy: reconcile user profile schema missed by initial-schema backfill (reconcile-historical-add-scripts B5) #153

Merged
varutasu merged 1 commit from convoy/reconcile-b5-user-profile into main 2026-06-14 09:18:20 -04:00
varutasu commented 2026-06-14 09:05:55 -04:00 (Migrated from github.com)

Summary

Brief 5 of reconcile-historical-add-scripts. Folds the union of DDL effects from scripts/add-user-profile-columns.js (#8) and scripts/add-user-profile-fields.js (#9) into a single idempotent node-pg-migrate migration so a brand-new Neon branch reaches structural parity with prod for the user-profile surface.

  • New file: migrations/1781000000005_reconcile-user-profile.js (pre-assigned timestamp per convoy § Slice dependencies).
  • Idempotency: every column ADD uses ADD COLUMN IF NOT EXISTS; both new tables use CREATE TABLE IF NOT EXISTS; every index uses CREATE INDEX IF NOT EXISTS; CHECK constraints are wrapped in DO \$\$ ... EXCEPTION WHEN duplicate_object THEN NULL; END \$\$; blocks (Postgres pre-15 has no ADD CONSTRAINT IF NOT EXISTS for CHECK).
  • Per Finding 2, BOTH `profile_image_url` (from #8, read by `pages/api/auth/register.js`) and `avatar_url` (from #9, read by the user/avatar surface) are added. Cleanup deferred to the queued `unify-user-avatar-column` follow-up.
  • `down()` is a hard stub.

Files changed

  • `migrations/1781000000005_reconcile-user-profile.js` (new, 191 lines).

No other files touched (no scripts edited per no-go-zones; no convoy file edits — B7 owns the as-shipped sweep).

Per-column table (16 ADDs)

# Column Type Default Source script Constraints
1 `first_name` VARCHAR(255) #8#9
2 `last_name` VARCHAR(255) #8#9
3 `username` VARCHAR(255) #8#9 inline UNIQUE
4 `profile_image_url` TEXT #8 only — (read by auth/register)
5 `bio` TEXT #9 only
6 `avatar_url` TEXT #9 only — (read by user/avatar)
7 `favorite_games` JSONB `'["MTG"]'` #9 only
8 `collection_visibility` VARCHAR(20) `'private'` #9 only check_collection_visibility
9 `preferred_currency` VARCHAR(3) `'USD'` #9 only check_preferred_currency
10 `cards_per_page` INTEGER 50 #9 only check_cards_per_page
11 `default_view` VARCHAR(10) `'grid'` #9 only check_default_view
12 `notifications_email` BOOLEAN true #9 only
13 `notifications_marketing` BOOLEAN false #9 only
14 `two_factor_enabled` BOOLEAN false #9 only
15 `theme` VARCHAR(10) `'system'` #9 only check_theme
16 `language` VARCHAR(5) `'en'` #9 only check_language

Per-index list (6)

Index Table Columns
`idx_users_username` `users` `(username)`
`idx_users_email` `users` `(email)`
`idx_user_settings_user_id` `user_settings` `(user_id)`
`idx_user_settings_key` `user_settings` `(setting_key)`
`idx_user_avatars_user_id` `user_avatars` `(user_id)`
`idx_user_avatars_active` `user_avatars` `(user_id, is_active)`

Per-CHECK list (6, all wrapped in DO/EXCEPTION blocks)

Constraint Column Allowed values
`check_collection_visibility` `collection_visibility` `'private', 'public', 'unlisted'`
`check_preferred_currency` `preferred_currency` `'USD', 'EUR', 'GBP', 'CAD', 'JPY'`
`check_cards_per_page` `cards_per_page` `25, 50, 100`
`check_default_view` `default_view` `'grid', 'list'`
`check_theme` `theme` `'light', 'dark', 'system'`
`check_language` `language` `'en', 'es', 'fr', 'de', 'ja'`

Tables created (2)

Table Shape
`user_settings` `(id SERIAL PK, user_id INTEGER FK→users ON DELETE CASCADE, setting_key VARCHAR(100) NOT NULL, setting_value JSONB NOT NULL, created_at TIMESTAMP DEFAULT NOW, updated_at TIMESTAMP DEFAULT NOW, UNIQUE(user_id, setting_key))`
`user_avatars` `(id SERIAL PK, user_id INTEGER FK→users ON DELETE CASCADE, filename VARCHAR(255) NOT NULL, original_name VARCHAR(255), mime_type VARCHAR(100), file_size INTEGER, file_path TEXT NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW, updated_at TIMESTAMP DEFAULT NOW)`

FK shape mirrors initial-schema's `ON DELETE CASCADE` pattern for per-user join tables.

Acceptance criteria

  • Exactly one new file under `migrations/` with pre-assigned timestamp `1781000000005`.
  • Filename: `1781000000005_reconcile-user-profile.js`.
  • Every column ADD guarded by `IF NOT EXISTS`.
  • Both new tables guarded by `CREATE TABLE IF NOT EXISTS`.
  • Every index guarded by `CREATE INDEX IF NOT EXISTS`.
  • Every CHECK constraint wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$;` (R5 — pre-15 Postgres has no `ADD CONSTRAINT IF NOT EXISTS` for CHECK).
  • BOTH `profile_image_url` and `avatar_url` are present (Finding 2 — must NOT pick one).
  • `down()` throws with a clear message naming the migration.
  • No edits to historical `scripts/add-user-profile-*.js` (no-go-zones).
  • No edits to `scripts/setup-neon-db.js` or any other file outside `migrations/`.
  • No defaults-backfill DML (column DEFAULTs handle fresh-env semantics; prod already has historical-script values).

Test plan

  • `node --check migrations/1781000000005_reconcile-user-profile.js` → exit 0.
  • `npm run lint` → 0 errors (1 pre-existing warning in `components/CollectionsPageView.js` unchanged from baseline; not introduced by this PR).
  • `npm run test:run` → 131/131 tests pass across 26 files (Layout / Modal / auth-secret / permission-middleware / vocab / scanner / etc.). No profile/settings test fixtures exist that depend on these columns; no regressions.
  • Static idempotency proof:
    • 16 `ADD COLUMN IF NOT EXISTS` (one ALTER TABLE block, comma-separated).
    • 2 `CREATE TABLE IF NOT EXISTS` (user_settings, user_avatars).
    • 6 `CREATE INDEX IF NOT EXISTS` (one block).
    • 6 `DO $$ ... EXCEPTION WHEN duplicate_object` blocks for CHECK constraints.
  • Optional Neon-branch test deferred to D5 manual operator runbook (B7 will document); for this brief size the static idempotency proof + per-column table is sufficient pre-merge.

Notes for reviewer

  • Discrepancy flag (overshoot vs architect's plan): the user prompt and architect's brief outline give different ALTER counts. The user prompt states 15 ALTERs; the architect's brief outline (reconcile-historical-add-scripts.md § Brief outline row) says 13 ALTERs; the actual unique column union of scripts #8#9 is 16 (the 15 columns in script #9 + `profile_image_url` from script #8 which is NOT in #9). I shipped all 16 because the convoy's Finding 2 explicitly mandates that both `profile_image_url` AND `avatar_url` exist. Either count in the planning docs was a draft-stage approximation; the prod-parity invariant is the operative target.
  • Undershoot check (anything in historical scripts not in plan?): scripts #8 and #9 also contain defaults-backfill DML (`UPDATE users SET ... WHERE ... IS NULL`). Per the convoy's brief outline ("Defer defaults-backfill UPDATE — column DEFAULTs handle it") this is intentionally not replicated. Column DEFAULTs handle new-row semantics on fresh envs; prod rows already have values from the historical script's run.
  • Username UNIQUE handling: uses inline `ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE` (matching both historical scripts). Postgres autogen-names the unique constraint; subsequent reapplies are no-ops because the column already exists.
  • `user_settings` / `users` duplication: several columns (`theme`, `language`, etc.) are also addressable via the `user_settings` key/value table. This is the duplication flagged in `.convoys/ship-readiness.md` § P2 #14 and is explicitly out of scope for this brief — deferred to the queued `unify-user-avatar-column` follow-up's broader column-vs-keyvalue reconciliation pass.

Made with Cursor

## Summary Brief 5 of `reconcile-historical-add-scripts`. Folds the union of DDL effects from `scripts/add-user-profile-columns.js` (#8) and `scripts/add-user-profile-fields.js` (#9) into a single idempotent `node-pg-migrate` migration so a brand-new Neon branch reaches structural parity with prod for the user-profile surface. - New file: `migrations/1781000000005_reconcile-user-profile.js` (pre-assigned timestamp per convoy § Slice dependencies). - Idempotency: every column ADD uses `ADD COLUMN IF NOT EXISTS`; both new tables use `CREATE TABLE IF NOT EXISTS`; every index uses `CREATE INDEX IF NOT EXISTS`; CHECK constraints are wrapped in `DO \$\$ ... EXCEPTION WHEN duplicate_object THEN NULL; END \$\$;` blocks (Postgres pre-15 has no `ADD CONSTRAINT IF NOT EXISTS` for CHECK). - Per **Finding 2**, BOTH \`profile_image_url\` (from #8, read by \`pages/api/auth/register.js\`) and \`avatar_url\` (from #9, read by the user/avatar surface) are added. Cleanup deferred to the queued \`unify-user-avatar-column\` follow-up. - \`down()\` is a hard stub. ## Files changed - \`migrations/1781000000005_reconcile-user-profile.js\` (new, 191 lines). No other files touched (no scripts edited per no-go-zones; no convoy file edits — B7 owns the as-shipped sweep). ## Per-column table (16 ADDs) | # | Column | Type | Default | Source script | Constraints | | - | ------ | ---- | ------- | ------------- | ----------- | | 1 | \`first_name\` | VARCHAR(255) | — | #8 ∪ #9 | — | | 2 | \`last_name\` | VARCHAR(255) | — | #8 ∪ #9 | — | | 3 | \`username\` | VARCHAR(255) | — | #8 ∪ #9 | inline UNIQUE | | 4 | \`profile_image_url\` | TEXT | — | #8 only | — (read by auth/register) | | 5 | \`bio\` | TEXT | — | #9 only | — | | 6 | \`avatar_url\` | TEXT | — | #9 only | — (read by user/avatar) | | 7 | \`favorite_games\` | JSONB | \`'[\"MTG\"]'\` | #9 only | — | | 8 | \`collection_visibility\` | VARCHAR(20) | \`'private'\` | #9 only | check_collection_visibility | | 9 | \`preferred_currency\` | VARCHAR(3) | \`'USD'\` | #9 only | check_preferred_currency | | 10| \`cards_per_page\` | INTEGER | 50 | #9 only | check_cards_per_page | | 11| \`default_view\` | VARCHAR(10) | \`'grid'\` | #9 only | check_default_view | | 12| \`notifications_email\` | BOOLEAN | true | #9 only | — | | 13| \`notifications_marketing\`| BOOLEAN | false | #9 only | — | | 14| \`two_factor_enabled\` | BOOLEAN | false | #9 only | — | | 15| \`theme\` | VARCHAR(10) | \`'system'\` | #9 only | check_theme | | 16| \`language\` | VARCHAR(5) | \`'en'\` | #9 only | check_language | ## Per-index list (6) | Index | Table | Columns | | ----- | ----- | ------- | | \`idx_users_username\` | \`users\` | \`(username)\` | | \`idx_users_email\` | \`users\` | \`(email)\` | | \`idx_user_settings_user_id\` | \`user_settings\` | \`(user_id)\` | | \`idx_user_settings_key\` | \`user_settings\` | \`(setting_key)\` | | \`idx_user_avatars_user_id\` | \`user_avatars\` | \`(user_id)\` | | \`idx_user_avatars_active\` | \`user_avatars\` | \`(user_id, is_active)\` | ## Per-CHECK list (6, all wrapped in DO/EXCEPTION blocks) | Constraint | Column | Allowed values | | ---------- | ------ | -------------- | | \`check_collection_visibility\` | \`collection_visibility\` | \`'private', 'public', 'unlisted'\` | | \`check_preferred_currency\` | \`preferred_currency\` | \`'USD', 'EUR', 'GBP', 'CAD', 'JPY'\` | | \`check_cards_per_page\` | \`cards_per_page\` | \`25, 50, 100\` | | \`check_default_view\` | \`default_view\` | \`'grid', 'list'\` | | \`check_theme\` | \`theme\` | \`'light', 'dark', 'system'\` | | \`check_language\` | \`language\` | \`'en', 'es', 'fr', 'de', 'ja'\` | ## Tables created (2) | Table | Shape | | ----- | ----- | | \`user_settings\` | \`(id SERIAL PK, user_id INTEGER FK→users ON DELETE CASCADE, setting_key VARCHAR(100) NOT NULL, setting_value JSONB NOT NULL, created_at TIMESTAMP DEFAULT NOW, updated_at TIMESTAMP DEFAULT NOW, UNIQUE(user_id, setting_key))\` | | \`user_avatars\` | \`(id SERIAL PK, user_id INTEGER FK→users ON DELETE CASCADE, filename VARCHAR(255) NOT NULL, original_name VARCHAR(255), mime_type VARCHAR(100), file_size INTEGER, file_path TEXT NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW, updated_at TIMESTAMP DEFAULT NOW)\` | FK shape mirrors initial-schema's \`ON DELETE CASCADE\` pattern for per-user join tables. ## Acceptance criteria - [x] Exactly one new file under \`migrations/\` with pre-assigned timestamp \`1781000000005\`. - [x] Filename: \`1781000000005_reconcile-user-profile.js\`. - [x] Every column ADD guarded by \`IF NOT EXISTS\`. - [x] Both new tables guarded by \`CREATE TABLE IF NOT EXISTS\`. - [x] Every index guarded by \`CREATE INDEX IF NOT EXISTS\`. - [x] Every CHECK constraint wrapped in \`DO \$\$ ... EXCEPTION WHEN duplicate_object THEN NULL; END \$\$;\` (R5 — pre-15 Postgres has no \`ADD CONSTRAINT IF NOT EXISTS\` for CHECK). - [x] BOTH \`profile_image_url\` and \`avatar_url\` are present (Finding 2 — must NOT pick one). - [x] \`down()\` throws with a clear message naming the migration. - [x] No edits to historical \`scripts/add-user-profile-*.js\` (no-go-zones). - [x] No edits to \`scripts/setup-neon-db.js\` or any other file outside \`migrations/\`. - [x] No defaults-backfill DML (column DEFAULTs handle fresh-env semantics; prod already has historical-script values). ## Test plan - [x] \`node --check migrations/1781000000005_reconcile-user-profile.js\` → exit 0. - [x] \`npm run lint\` → 0 errors (1 pre-existing warning in \`components/CollectionsPageView.js\` unchanged from baseline; not introduced by this PR). - [x] \`npm run test:run\` → 131/131 tests pass across 26 files (Layout / Modal / auth-secret / permission-middleware / vocab / scanner / etc.). No profile/settings test fixtures exist that depend on these columns; no regressions. - [x] Static idempotency proof: - 16 \`ADD COLUMN IF NOT EXISTS\` (one ALTER TABLE block, comma-separated). - 2 \`CREATE TABLE IF NOT EXISTS\` (user_settings, user_avatars). - 6 \`CREATE INDEX IF NOT EXISTS\` (one block). - 6 \`DO \$\$ ... EXCEPTION WHEN duplicate_object\` blocks for CHECK constraints. - [ ] Optional Neon-branch test deferred to D5 manual operator runbook (B7 will document); for this brief size the static idempotency proof + per-column table is sufficient pre-merge. ## Notes for reviewer - **Discrepancy flag (overshoot vs architect's plan):** the user prompt and architect's brief outline give different ALTER counts. The user prompt states **15 ALTERs**; the architect's brief outline (reconcile-historical-add-scripts.md § Brief outline row) says **13 ALTERs**; the actual unique column union of scripts #8 ∪ #9 is **16** (the 15 columns in script #9 + \`profile_image_url\` from script #8 which is NOT in #9). I shipped all 16 because the convoy's Finding 2 explicitly mandates that both \`profile_image_url\` AND \`avatar_url\` exist. Either count in the planning docs was a draft-stage approximation; the prod-parity invariant is the operative target. - **Undershoot check (anything in historical scripts not in plan?):** scripts #8 and #9 also contain defaults-backfill DML (\`UPDATE users SET ... WHERE ... IS NULL\`). Per the convoy's brief outline ("Defer defaults-backfill UPDATE — column DEFAULTs handle it") this is intentionally not replicated. Column DEFAULTs handle new-row semantics on fresh envs; prod rows already have values from the historical script's run. - **Username UNIQUE handling:** uses inline \`ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE\` (matching both historical scripts). Postgres autogen-names the unique constraint; subsequent reapplies are no-ops because the column already exists. - **\`user_settings\` / \`users\` duplication:** several columns (\`theme\`, \`language\`, etc.) are also addressable via the \`user_settings\` key/value table. This is the duplication flagged in \`.convoys/ship-readiness.md\` § P2 #14 and is **explicitly out of scope** for this brief — deferred to the queued \`unify-user-avatar-column\` follow-up's broader column-vs-keyvalue reconciliation pass. <!-- pipeline: brief=5, convoy=reconcile-historical-add-scripts --> Made with [Cursor](https://cursor.com)
vercel[bot] commented 2026-06-14 09:06:01 -04:00 (Migrated from github.com)

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tcg-vault Ready Ready Preview, Comment Jun 14, 2026 1:06pm

Request Review

[vc]: #KGML1t6MfqOd+yScEDiaEF93juP77BUdBJzIWfawLQI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0Y2ctdmF1bHQiLCJwcm9qZWN0SWQiOiJwcmpfRjZXOEVvRkd3Y0g3aWVGcnRvRlNlOXdVVkFhNSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0Y2ctdmF1bHQtZ2l0LWNvbnZveS1yZWNvbmNpLTg0MDEwMS1yYW5kYWxsLXN0aWxsd2VsbHMtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcmFuZGFsbC1zdGlsbHdlbGxzLXByb2plY3RzL3RjZy12YXVsdC8yRmlmVUxVcDE5OVd1WVRLOVlMS3pGSjFhd3FTIiwicHJldmlld1VybCI6InRjZy12YXVsdC1naXQtY29udm95LXJlY29uY2ktODQwMTAxLXJhbmRhbGwtc3RpbGx3ZWxscy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn1dLCJyZXF1ZXN0UmV2aWV3VXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3ZlcmNlbC1hZ2VudC9yZXF1ZXN0LXJldmlldz9vd25lcj1zdHdsLWxhYnMmcmVwbz10Y2ctdmF1bHQmcHI9MTUzIn0= The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.link/github-learn-more). | Project | Deployment | Actions | Updated (UTC) | | :--- | :----- | :------ | :------ | | [tcg-vault](https://vercel.com/randall-stillwells-projects/tcg-vault) | ![Ready](https://vercel.com/static/status/ready.svg) [Ready](https://vercel.com/randall-stillwells-projects/tcg-vault/2FifULUp199WuYTK9YLKzFJ1awqS) | [Preview](https://tcg-vault-git-convoy-reconci-840101-randall-stillwells-projects.vercel.app), [Comment](https://vercel.live/open-feedback/tcg-vault-git-convoy-reconci-840101-randall-stillwells-projects.vercel.app?via=pr-comment-feedback-link) | Jun 14, 2026 1:06pm | <a href="https://vercel.com/vercel-agent/request-review?owner=stwl-labs&repo=tcg-vault&pr=153" rel="noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://agents-vade-review.vercel.sh/request-review-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://agents-vade-review.vercel.sh/request-review-light.svg"><img src="https://agents-vade-review.vercel.sh/request-review-light.svg" alt="Request Review"></picture></a>
github-actions[bot] commented 2026-06-14 09:15:04 -04:00 (Migrated from github.com)

Pipeline Health

Build + CI gates

Gate Status
Vercel build (Preview) pass
CI: Lint in progress
CI: Schema map fresh skipped
Preview smoke in progress
Visual diff ⏭ skipped or pending

Build runs on Vercel; this CI runs lint and schema-map drift only (no duplicate build).

Role reports

Role Status
Reviewer report pending
A11y audit pending
Design system audit pending

See individual comments above for details. This rollup updates automatically.

<!-- pipeline-rollup --> ## Pipeline Health ### Build + CI gates | Gate | Status | | --- | --- | | Vercel build (Preview) | ✅ pass | | CI: Lint | ⏳ in progress | | CI: Schema map fresh | ❌ skipped | | Preview smoke | ⏳ in progress | | Visual diff | ⏭ skipped or pending | _Build runs on Vercel; this CI runs lint and schema-map drift only (no duplicate build)._ ### Role reports | Role | Status | | --- | --- | | Reviewer report | ⏳ pending | | A11y audit | ⏳ pending | | Design system audit | ⏳ pending | See individual comments above for details. This rollup updates automatically.
Sign in to join this conversation.
No description provided.