Skip to content

fix(db): drop/recreate partial index around support_sessions enum cast - #1838

Merged
LucasSantana-Dev merged 1 commit into
mainfrom
fix/1837-support-session-enum-index
Jul 16, 2026
Merged

LucasSantana-Dev merged 1 commit into
mainfrom
fix/1837-support-session-enum-index

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Jul 16, 2026 •

Copy link
Copy Markdown
Owner

Problem

The v2.35.1 deploy failed in production (2026-07-16 02:29 UTC) at:

Applying migration `20260713000000_support_session_status_enum`
Error: P3018 / code 42883
ERROR: operator does not exist: "SupportSessionStatus" = text

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; prod migrate status = "up to date").

Root cause

20260712000000_support_sessions/migration.sql creates a partial unique index whose predicate compares status to a text literal:

CREATE UNIQUE INDEX "support_sessions_one_open_per_user"
    ON "support_sessions"("guildId", "requestorId")
    WHERE "status" = 'open';

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):

DROP INDEX "support_sessions_one_open_per_user";
CREATE TYPE ... ; ALTER TABLE ... DROP CONSTRAINT ...; ALTER COLUMN ... TYPE ...;
CREATE UNIQUE INDEX "support_sessions_one_open_per_user" ... WHERE "status" = 'open';

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:

chain result
broken migration reproduces prod's exact operator does not exist: "SupportSessionStatus" = text
fixed migration 45/45 apply clean; status udt = SupportSessionStatus; partial index recreated

The 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 found in 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.
  • Prevention: wire the scratch-DB full-chain apply above into CI so migration-only SQL defects can't merge again.

Closes #1837


Summary by cubic

Dropped and recreated the partial unique index support_sessions_one_open_per_user around the support_sessions.status enum migration to stop the “operator does not exist: SupportSessionStatus = text” error and unblock production migrations. Closes #1837.

  • Bug Fixes
    • Drop the index before the type change; recreate it after with the same predicate (WHERE "status" = 'open').
    • Avoids predicate revalidation mismatch when status changes from text to the enum.
    • Verified by running the full migration chain on Postgres 18: broken chain fails; fixed chain succeeds.

Written for commit 933d0e4. Summary will update on new commits.

Review in cubic

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
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 63ef174a-fcc9-4824-a50c-5b9d11ba73ac

📥 Commits

Reviewing files that changed from the base of the PR and between 6303f97 and 933d0e4.

📒 Files selected for processing (1)
  • prisma/migrations/20260713000000_support_session_status_enum/migration.sql
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1837-support-session-enum-index

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@sonarqubecloud

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@LucasSantana-Dev
LucasSantana-Dev merged commit 4b35da0 into main Jul 16, 2026
42 of 43 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the fix/1837-support-session-enum-index branch July 16, 2026 13:23
LucasSantana-Dev added a commit that referenced this pull request Jul 16, 2026
🤖 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. -->
LucasSantana-Dev added a commit that referenced this pull request Jul 16, 2026
#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. -->
LucasSantana-Dev added a commit that referenced this pull request Jul 16, 2026
…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 -->
This was referenced Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1: v2.35.1 deploy failed — support_session_status_enum migration blocked by partial index predicate; prod migrations now wedged

1 participant