Repository navigation
fix(db): drop/recreate partial index around support_sessions enum cast - #1838
Conversation
The support_session_status_enum migration failed in prod (P3018/42883: "operator does not exist: SupportSessionStatus = text"). ALTER COLUMN ... TYPE re-validates the support_sessions_one_open_per_user partial index, whose predicate (WHERE status = 'open') compares status to a text literal — invalid against the new enum type. Drop the index before the cast and recreate it after. The index is migration-only (partial indexes aren't expressible in schema.prisma), so Prisma's generator never emitted the drop/recreate and no CI drift check caught it. Verified on a scratch postgres:18-alpine (matching prod) seeded from the full 45-migration chain: broken chain reproduces the exact 42883 error; fixed chain applies clean with status as the enum and the partial index recreated. Closes #1837
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 24 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Failed to generate code suggestions for PR |
|
There was a problem hiding this comment.
No issues found across 1 file
Auto-approved: Focused migration fix: drops and recreates a partial index around an enum change to resolve a production migration failure. Bounded to a single file with no new behavior, it unblocks future migrations.
Re-trigger cubic
🤖 I have created a release *beep* *boop* --- <details><summary>2.35.2</summary> ## [2.35.2](v2.35.1...v2.35.2) (2026-07-16) ### Bug Fixes * **db:** drop/recreate partial index around support_sessions enum cast ([#1838](#1838)) ([4b35da0](4b35da0)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Release 2.35.2 with a database fix for support_sessions. Drops and recreates the partial index around the enum cast to prevent index/cast errors and stabilize queries. - **Bug Fixes** - DB: Rebuild partial index for `support_sessions` enum cast to avoid failures during enum updates. <sup>Written for commit d459c91. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/LucasSantana-Dev/Lucky/pull/1839?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
#1840) ## Why a second fix #1838 fixed the partial-index cause but assumed a **clean** pre-migration state. It shipped in 2.35.2 and **failed again** in prod — a different error: ``` Database error code: 42704 ERROR: constraint "support_sessions_status_check" of relation "support_sessions" does not exist ``` Root cause: **`prisma migrate deploy` does not wrap a migration in a transaction** (per-statement autocommit; confirmed via Prisma docs + [discussion #3774](prisma/orm#3774)). The *original* failed run (2.35.1) had already committed `DROP CONSTRAINT` and `CREATE TYPE` before failing on the index — so prod was left in a **partial state** (enum exists, CHECK gone, partial index gone, column still text). #1838's clean-state migration then failed re-running against that residue. ## Fix Guard every statement so re-running converges from **any** partial state: - `DROP INDEX IF EXISTS` (partial index may already be gone) - `CREATE TYPE` via `DO $$ ... EXCEPTION WHEN duplicate_object $$` (no `IF NOT EXISTS` for types; enum may already exist) - `ALTER TABLE ... DROP CONSTRAINT IF EXISTS` (CHECK may already be gone) The plain btree `support_sessions_status_expiresAt_idx` is intentionally **not** dropped — `ALTER COLUMN ... TYPE` rebuilds plain indexes automatically; only the partial index's operator-dependent predicate (`WHERE status = 'open'::text`) needs the drop/recreate. **Verified empirically on PG18** — a review claim that btree indexes block the type change was refuted by direct test. ## Verification (postgres:18-alpine, prod parity) Applied against three DB states, **each run twice** (idempotency), all converge to `status = SupportSessionStatus` enum + partial index present: | State | run 1 | re-run | |---|---|---| | CLEAN (fresh/staging) | ✓ | ✓ | | PARTIAL-A (current prod: enum exists, index+CHECK gone) | ✓ | ✓ | | PARTIAL-B (only CREATE TYPE committed) | ✓ | ✓ | Plus the full 45-migration chain applies clean with the actual file. ## Decision & rationale `/research-and-decide` → idempotent guards, **no** explicit `BEGIN/COMMIT` wrapper. Idempotency converges from any partial state (strictly stronger than atomicity, which would roll back to the partial state and still need guards to re-run), and avoids the unverified interaction between an in-file transaction and Prisma's `_prisma_migrations` bookkeeping. Class-prevention lives in the CI scratch-DB gate (#1837 follow-up), not per-migration ceremony Prisma-generated files would omit. ADR: `decisions/2026-07-16-idempotent-migrations.md`. ## Recovery (operator-gated, not in this PR) Prod is currently re-wedged (P3018). After this merges + releases: `migrate resolve --rolled-back` then redeploy re-applies this migration against PARTIAL-A (proven). Refs #1837 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Make the `support_sessions` status enum migration idempotent and re-runnable to recover from partial runs in `prisma migrate deploy` (per-statement autocommit), addressing #1837. Also adds an ADR that formalizes idempotent migrations and a CI scratch-DB gate on the prod Postgres version. - **Bug Fixes** - Guarded statements: `DROP INDEX IF EXISTS` for `"support_sessions_one_open_per_user"`, enum via `DO ... EXCEPTION WHEN duplicate_object`, and `DROP CONSTRAINT IF EXISTS` for `"support_sessions_status_check"`. - Keep plain btree `support_sessions_status_expiresAt_idx`; `ALTER COLUMN ... TYPE` rebuilds it automatically. - Verified on Postgres 18 across clean and partial states; safe to re-run. <sup>Written for commit 9b56ebb. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/LucasSantana-Dev/Lucky/pull/1840?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
…prevention) (#1843) Prevention gate for the migration-wedge incident (#1837), the mandated follow-up from `decisions/2026-07-16-idempotent-migrations.md`. The `support_session_status_enum` migration failed in prod **twice** (2.35.1, 2.35.2), each wedging all prod migrations — invisible to the schema-drift check because partial indexes / CHECK constraints / enum casts aren't expressible in `schema.prisma`. ## What it does A new `Migration Gate` workflow, triggered only on `prisma/migrations/**` changes: 1. Spins up **`postgres:18-alpine`** (pinned to the production major version). 2. **Clean-apply** — applies the full migration chain via psql (`ON_ERROR_STOP`, mirroring Prisma's non-transactional per-statement `migrate deploy`). Catches migrations that fail on a fresh DB. 3. **Idempotency re-apply** — re-applies *this PR's own* new migrations against the already-migrated DB. A migration authored per the idempotency ADR re-applies cleanly; a non-idempotent one errors. ## Proven to fire (not a no-op) Verified locally against both historical failures + the fix: | Case | Clean-apply | Re-apply | Gate | |---|---|---|---| | 2.35.1 original (no `DROP INDEX`) | ❌ `42883 operator does not exist: SupportSessionStatus = text` | — | **caught** | | 2.35.2 #1838 (no `IF EXISTS`) | ✅ | ❌ `42704 constraint does not exist` | **caught** | | current idempotent migrations | ✅ | ✅ | **pass** | ##⚠️ Needs a follow-up to actually gate This adds the **check**, but a check only blocks merge if it's in branch protection's `required_status_checks`. Like the destructive-interaction gate (see `gotcha_destructive_gate_not_required_check`), it's advisory until added. After merge, add `Migrations apply on Postgres 18` to the required contexts on `main` (and `release/**` if used) — operator or via `gh api`. ## Revisit-when Bump the pinned `postgres:18-alpine` when production Postgres changes major version — index-predicate re-validation and cast behaviour are version-sensitive (per the ADR). Refs #1837 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a CI Migration Gate that validates `prisma/migrations` against Postgres 18 to enforce idempotency and prevent wedges like #1837. It now also enforces append-only history and only re-applies added migration files. - **New Features** - Adds a `Migration Gate` workflow on PRs and merge groups to `main`/`release/**`; always reports and skips Postgres when no migration changes. - Enforces append-only migrations; rejects modified/renamed/deleted existing migration files. - On changes, spins up `postgres:18-alpine`, clean-applies the full chain with `psql -v ON_ERROR_STOP=1`, then re-applies added migrations to verify idempotency; catches partial-index, CHECK-constraint, and enum-cast issues (covers #1837). - Security hardening: uses `actions/checkout` with `persist-credentials: false`; silences SC2034. - **Migration** - After merge, make “Migrations apply on Postgres 18” a required status check for `main` (and `release/**`). - Bump the pinned Postgres image when production’s major version changes. <sup>Written for commit 6fdeef6. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/LucasSantana-Dev/Lucky/pull/1843?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added automated validation for database migration changes. * Verifies migrations apply successfully in sequence and can be reapplied safely. * Runs checks only when migration files change, with automatic cleanup of temporary test resources. <!-- end of auto-generated comment: release notes by coderabbit.ai -->



Problem
The v2.35.1 deploy failed in production (2026-07-16 02:29 UTC) at:
Production was never impacted (containers never swapped — still healthy on v2.35.0), but the failed migration wedged all future prod migrations until manually resolved (done:
migrate resolve --rolled-back+ dropped the orphan enum type; prodmigrate status= "up to date").Root cause
20260712000000_support_sessions/migration.sqlcreates a partial unique index whose predicate comparesstatusto a text literal:The enum migration drops the CHECK constraint but not this index.
ALTER COLUMN "status" TYPE "SupportSessionStatus"forces Postgres to re-validate the index predicate against the new type →SupportSessionStatus = text→ no such operator.Why nothing caught it: partial indexes aren't expressible in
schema.prisma, so Prisma's generator never emitted a drop/recreate and there is no CI drift check on migration-only objects. The original migration even documents this ("migration-only, no CI drift check runs"). Same class as #1735/#1734 — a defect only reachable when the SQL actually runs against a real DB.Fix
Drop the partial index before the type change, recreate it after (predicate is type-consistent against the enum):
Editing the migration in place is correct: it never applied successfully anywhere (rolled back in prod, never ran in staging — both still
text), so there is no checksum drift.Verification
Applied the full 45-migration chain to a scratch
postgres:18-alpine(matching prod's PG 18.3), with an injection control:operator does not exist: "SupportSessionStatus" = textstatusudt =SupportSessionStatus; partial index recreatedThe harness fails on the bug and passes on the fix — same harness, only the migration content differs.
Follow-ups (tracked in #1837, not in this PR)
scripts/deploy.sh:358—curl: command not foundin the failure-notification path, so homelab deploy failures present as SHA-mismatch timeouts instead of reporting the real error. This is why the failure cause had to be dug out of the container log.Closes #1837
Summary by cubic
Dropped and recreated the partial unique index
support_sessions_one_open_per_useraround thesupport_sessions.statusenum migration to stop the “operator does not exist: SupportSessionStatus = text” error and unblock production migrations. Closes #1837.WHERE "status" = 'open').statuschanges fromtextto the enum.Written for commit 933d0e4. Summary will update on new commits.