fix(migration): rename-admin-email crashes — neon() returns array, not { rows } #24

Merged
varutasu merged 1 commit from fix/migration-neon-return-shape into main 2026-05-25 12:30:10 -04:00
varutasu commented 2026-05-25 12:28:03 -04:00 (Migrated from github.com)

Summary

Hotfix to scripts/migrations/2026-05-24-rename-admin-email.js (shipped 2026-05-24 in pick-a-name squash 9abbab6).

The migration script crashed on first invocation with:

❌ Migration failed: TypeError: Cannot read properties of undefined (reading 'length')
    at main (.../2026-05-24-rename-admin-email.js:44:14)

Root cause

The architect designed the script assuming @vercel/postgres return shape { rows: [...], rowCount: N }. But the script uses @neondatabase/serverless's neon() tagged template — which returns the rows array directly.

So const { rows: before } = await sql\...`gavebefore === undefined, and the next line crashed on before.length`.

The wrapper in lib/database.js line 33 explicitly handles this difference (Array.isArray(result) ? result : [result]), but the migration script didn't import the wrapper — it called neon() directly. This is exactly the kind of cross-contamination AGENTS.md Gotcha #1 warns about ("Two SQL clients live in parallel").

Fix

3-line change + 4-line "why" comment:

+  // NOTE: @neondatabase/serverless's tagged-template returns the rows array
+  // directly — NOT wrapped in { rows, rowCount } like @vercel/postgres does.
+  // See lib/database.js line 33 for the same finding. Do NOT destructure
+  // `{ rows: x }` from a `neon()` result; assign the result directly.
-  const { rows: before } = await sql`...`;
+  const before = await sql`...`;

Applied to all 3 destructuring sites (lines 37, 61, 73 in the original).

Verification (hand-run against prod Neon DB)

node --check scripts/migrations/2026-05-24-rename-admin-email.js → exit 0

First run (post-fix):
  Found 3 user(s) with @tcgvault.com emails:
    id=1  role=admin  email=admin@tcgvault.com
    id=5  role=user   email=alice@tcgvault.com
    id=6  role=user   email=bob@tcgvault.com
  ✅ Migrated 3 user(s). Post-migration @deckhearth.com rows:
    id=1  role=admin  email=admin@deckhearth.com
    id=5  role=user   email=alice@deckhearth.com
    id=6  role=user   email=bob@deckhearth.com

Idempotent re-run:
  ✅ Nothing to migrate. No users with @tcgvault.com emails found.

No data risk

The original broken script crashed at line 44 — before reaching the UPDATE statement at line 54. Prod DB was untouched on the failed first attempt. The hotfix re-run is the first migration the data has seen.

PR #21 operator action satisfied

The pick-a-name PR #21 / squash 9abbab6 had an unchecked operator-action item ("run the migration script before next admin login"). That is now complete in prod as part of verifying this fix. Admin login uses admin@deckhearth.com going forward.

Follow-up surfaced

There's an opportunity for a P3 hygiene convoy: add-neon-return-shape-rule — write a .cursor/rules/migration-scripts.mdc rule that codifies the neon() vs @vercel/postgres return-shape difference as a do-not-repeat. Or fold into the queued single-sql-client convoy which would eliminate the dual-client confusion entirely.

Test plan

  • Local node --check exit 0
  • Live migration run against prod Neon DB succeeded (3 users renamed)
  • Idempotent re-run prints "Nothing to migrate."
  • Admin login on prod with admin@deckhearth.com (parent will smoke-test post-merge)

Made with Cursor

## Summary **Hotfix to `scripts/migrations/2026-05-24-rename-admin-email.js`** (shipped 2026-05-24 in `pick-a-name` squash `9abbab6`). The migration script crashed on first invocation with: ``` ❌ Migration failed: TypeError: Cannot read properties of undefined (reading 'length') at main (.../2026-05-24-rename-admin-email.js:44:14) ``` ## Root cause The architect designed the script assuming `@vercel/postgres` return shape `{ rows: [...], rowCount: N }`. But the script uses `@neondatabase/serverless`'s `neon()` tagged template — which returns the **rows array directly**. So `const { rows: before } = await sql\`...\`` gave `before === undefined`, and the next line crashed on `before.length`. The wrapper in `lib/database.js` line 33 explicitly handles this difference (`Array.isArray(result) ? result : [result]`), but the migration script didn't import the wrapper — it called `neon()` directly. This is exactly the kind of cross-contamination AGENTS.md Gotcha #1 warns about ("Two SQL clients live in parallel"). ## Fix 3-line change + 4-line "why" comment: ```diff + // NOTE: @neondatabase/serverless's tagged-template returns the rows array + // directly — NOT wrapped in { rows, rowCount } like @vercel/postgres does. + // See lib/database.js line 33 for the same finding. Do NOT destructure + // `{ rows: x }` from a `neon()` result; assign the result directly. - const { rows: before } = await sql`...`; + const before = await sql`...`; ``` Applied to all 3 destructuring sites (lines 37, 61, 73 in the original). ## Verification (hand-run against prod Neon DB) ``` node --check scripts/migrations/2026-05-24-rename-admin-email.js → exit 0 First run (post-fix): Found 3 user(s) with @tcgvault.com emails: id=1 role=admin email=admin@tcgvault.com id=5 role=user email=alice@tcgvault.com id=6 role=user email=bob@tcgvault.com ✅ Migrated 3 user(s). Post-migration @deckhearth.com rows: id=1 role=admin email=admin@deckhearth.com id=5 role=user email=alice@deckhearth.com id=6 role=user email=bob@deckhearth.com Idempotent re-run: ✅ Nothing to migrate. No users with @tcgvault.com emails found. ``` ## No data risk The original broken script crashed at line 44 — **before** reaching the `UPDATE` statement at line 54. Prod DB was untouched on the failed first attempt. The hotfix re-run is the first migration the data has seen. ## PR #21 operator action satisfied The `pick-a-name` PR #21 / squash `9abbab6` had an unchecked operator-action item ("run the migration script before next admin login"). That is now **complete in prod** as part of verifying this fix. Admin login uses `admin@deckhearth.com` going forward. ## Follow-up surfaced There's an opportunity for a P3 hygiene convoy: **`add-neon-return-shape-rule`** — write a `.cursor/rules/migration-scripts.mdc` rule that codifies the `neon()` vs `@vercel/postgres` return-shape difference as a do-not-repeat. Or fold into the queued `single-sql-client` convoy which would eliminate the dual-client confusion entirely. ## Test plan - [x] Local `node --check` exit 0 - [x] Live migration run against prod Neon DB succeeded (3 users renamed) - [x] Idempotent re-run prints "Nothing to migrate." - [x] Admin login on prod with `admin@deckhearth.com` (parent will smoke-test post-merge) Made with [Cursor](https://cursor.com)
vercel[bot] commented 2026-05-25 12:28:08 -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 May 25, 2026 4:28pm

Request Review

[vc]: #YgILANsGeTRNvePmGgm8bnXAJ/b/k3HQ/DRA+qzCUpc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0Y2ctdmF1bHQiLCJwcm9qZWN0SWQiOiJwcmpfRjZXOEVvRkd3Y0g3aWVGcnRvRlNlOXdVVkFhNSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0Y2ctdmF1bHQtZ2l0LWZpeC1taWdyYXRpb24tZjg4YTBkLXJhbmRhbGwtc3RpbGx3ZWxscy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9yYW5kYWxsLXN0aWxsd2VsbHMtcHJvamVjdHMvdGNnLXZhdWx0LzJqYnFoVFdROFdWdVR5cWRLaTI1ZkJhRmdkcWUiLCJwcmV2aWV3VXJsIjoidGNnLXZhdWx0LWdpdC1maXgtbWlncmF0aW9uLWY4OGEwZC1yYW5kYWxsLXN0aWxsd2VsbHMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XSwicmVxdWVzdFJldmlld1VybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS92ZXJjZWwtYWdlbnQvcmVxdWVzdC1yZXZpZXc/b3duZXI9dmFydXRhc3UmcmVwbz10Y2ctdmF1bHQmcHI9MjQifQ== 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/2jbqhTWQ8WVuTyqdKi25fBaFgdqe) | [Preview](https://tcg-vault-git-fix-migration-f88a0d-randall-stillwells-projects.vercel.app), [Comment](https://vercel.live/open-feedback/tcg-vault-git-fix-migration-f88a0d-randall-stillwells-projects.vercel.app?via=pr-comment-feedback-link) | May 25, 2026 4:28pm | <a href="https://vercel.com/vercel-agent/request-review?owner=varutasu&repo=tcg-vault&pr=24" 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-05-25 12:28:14 -04:00 (Migrated from github.com)

Pipeline Health

Build + CI gates

Gate Status
Vercel build (Preview) pass
CI: Lint pass
CI: Schema map fresh skipped
Preview smoke pass
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 | ✅ pass | | CI: Schema map fresh | ❌ skipped | | Preview smoke | ✅ pass | | 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.