fix(target-postgres): avoid uncast array_position for pg.enum ORDER BY - #30191
fix(target-postgres): avoid uncast array_position for pg.enum ORDER BY#30191StevenMcClankerton wants to merge 1 commit into
Conversation
…ay_position in ORDER BY `ORDER BY`/`DISTINCT ON` on a `pg.enum(...)` column rewrote to `array_position(ARRAY[...]::text[], <col>)` with no cast on the column argument, so Postgres rejected it with 42883 (no `array_position(text[], <enum>)` overload) — ordering by any native-enum column failed at runtime. Gate the rewrite on the column's codec (`pg/enum@1`) rather than on the mere presence of a value-set: a native enum already sorts by declaration order under a plain column reference (Postgres orders by `pg_enum.enumsortorder`), so it now falls through to plain-column rendering instead. Text-backed value-sets (CHECK-constraint enums) are unaffected and keep the `array_position` rewrite. Fixes #30163 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe Postgres SQL renderer now leaves native enum columns as plain ORDER BY expressions. Integration tests cover declaration-order sorting, references, direction, NULL values, and DISTINCT ON. ChangesNative enum ordering
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The change renders native PostgreSQL enum ordering as a plain column reference while preserving the existing rewrite for text-backed value sets, preventing the reported runtime query failure. No actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The changes satisfy issue
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
@prisma/orm-extension-arktype-json
@prisma/orm-extension-middleware-cache
@prisma/orm-extension-paradedb
@prisma/orm-extension-pgvector
@prisma/orm-extension-postgis
@prisma/orm-extension-supabase
@prisma/orm-family-mongo
@prisma/orm-family-sql
@prisma/orm-framework
@prisma/orm-mongo
@prisma/orm-postgres
@prisma/orm-sqlite
@prisma/orm-target-mongo
@prisma/orm-target-postgres
@prisma/orm-target-sqlite
@prisma/orm-toolchain
commit: |
size-limit report 📦
|
Summary
ORDER BY(andDISTINCT ON, which shares the same renderer) on apg.enum(...)column rewrote toarray_position(ARRAY[...]::text[], <col>)to sort by declaration order, but only cast the array literal totext[]— never the column argument. Against a real Postgres native enum column, noarray_position(text[], <enum type>)overload exists, so Postgres rejected the query with42883on rc.8. Ordering (orDISTINCT ON-ing) by anypg.enumcolumn failed at runtime; there was no workaround short of retyping the column toStringin the contract.This PR gates the rewrite on the column's codec (
pg/enum@1) rather than on the mere presence of a value-set. A native enum column now renders as a plain column reference inORDER BY/DISTINCT ON— no cast, no rewrite, no 42883.Fixes #30163
The design choice
The issue's own analysis offered two directions:
array_position(ARRAY[...]::text[], <col>::text).This PR takes option 2. Rationale:
ORDER BY/DISTINCT ON— Postgres orders enum values bypg_enum.enumsortorder, which is exactly the contract's declared member order. The rewrite is unnecessary for these columns, not merely differently-cast.array_position(...)is a per-row function call over an array literal, evaluated for every row — it defeats a plain index on the enum column. A bareORDER BY "col"does not.renderWherealready renders comparisons on the raw column, uncast, foreq/gt/etc. A keyset/cursorWHEREpredicate on an enum column therefore already compares by the enum's native ordering. Castingarray_position's column argument totextwould still sort correctly (it preserves declaration order, not alphabetical order — casting insidearray_positionis not the same asORDER BY col::text), but it would leaveORDER BYandWHEREreasoning about the same column via two different orderings in spirit, where the plain-column rewrite keeps them trivially consistent.Text-backed value-sets (a plain
text/varcharcolumn with a generated CHECK constraint) are unaffected — they still get thearray_positionrewrite, since Postgres would otherwise sort them alphabetically. The fullorder-by-enum.integration.test.tssuite (7 tests covering that path) passes unchanged.Why the gate is safe
The gate assumes the contract's declared enum member order and the database's actual
pg_enum.enumsortordernever diverge. That holds today because the migration planner:ALTER TYPE ... ADD VALUE '<value>', with noBEFORE/AFTER, in the native-enum add-value op), andnativeEnumMemberChangeRefusal, which raises an operator-worded error instead of emitting DDL.If that refusal is ever relaxed to allow reordering, this gate needs to be revisited — it would then be possible for the contract's declared order to diverge from the database's actual enum sort order with no signal from this code path. A forward-warning comment on
sortsByDeclarationOrderNatively(insql-renderer.ts) records this.Interaction with #30099
#30099 ("Fix: enum ORDER BY / DISTINCT ON loses declaration order behind a derived table") touches the exact same function,
renderOrderByExpr, and its two resolver helpers (resolveEnumOrderValues/resolveEnumOrderValuesForIdentifier) insql-renderer.ts. It's still open.Recommended landing order: this PR first. It's an independent, narrow fix for a live runtime error; #30099 is larger, still under review, and touches the same region regardless of which merges first.
The gate this PR adds lives at two call sites (inside
resolveEnumOrderValuesandresolveEnumOrderValuesForIdentifier), matching those functions' current shape. #30099 deletes both functions andcollectTableSources/TableSourceCoordinateoutright, replacing them withfindFromSource+resolveColumnValueSetFromSource(source, column, contract)returning{ found, values }. The correct reconciliation on rebase is not to reinsert this gate at the two old call sites — that would pass this PR's five tests, but it would miss #30099's new derived-table recursion, leaving apg.enumcolumn behind adistinct()/groupBy()wrap as a new, untested 42883 site.The correct reconciliation is a single gate, inside #30099's
table-sourcebranch ofresolveColumnValueSetFromSource, immediately afterstorageColumnis resolved:found: true, notfalse— the column exists, it is simply not rewritten, and the identifier resolver's ambiguity counter depends on that distinction. That single site covers both the direct path and #30099's derived-table recursion. This is recorded as aMERGE NOTEonsortsByDeclarationOrderNativelyinsql-renderer.tsso it isn't lost in a conflict resolution.Test coverage
test/migrations/order-by-native-enum.integration.test.ts(new), against a live PGlite-backed Postgres with a realCREATE TYPE ... AS ENUMcolumn, 5 cases:column-ref, ASCidentifier-ref, ASCcolumn-ref, DESCDISTINCT ONa native-enum column, matching itsORDER BY(Postgres requires theORDER BYprefix to matchDISTINCT ON)Every case asserts both the rendered SQL (
not.toContain('array_position')+ the exact bareORDER BY/DISTINCT ONtext) and the resulting row order — the render assertions specifically rule out option 1 (casting), which would pass the row-order checks alone without fixing the actual mechanism this PR changes.test/migrations/order-by-enum.integration.test.ts(existing, text-backed value-set / CHECK-constraint suite) — 7/7 unchanged, confirming thearray_positionrewrite still applies where it should.Full
@internal/adapter-postgressuite: 871 tests passed (868 pre-existing + 3 new beyond the two originally-planned cases), 3 expected fail, 1 skipped, when run in isolation / without contention. Under heavy parallel load in this environment,render-typescript.roundtrip.test.ts,planner.fk-config.test.ts, andrls-migration-plan.integration.test.tsintermittently hit their per-test timeouts (documented pre-existing flakiness, unrelated to this change) — each verified to pass cleanly in isolation every time it was re-run.pnpm typecheck,pnpm lint(package), andpnpm lint:deps(root) all clean.Boundary
The gate keys on the column's
codecId(pg/enum@1), not onnativeType. A hand-authored contract could in principle carry a plaintextcodec (pg/text@1) over a column whose adopted/unmanaged physical type happens to be a native Postgres enum — that column would not be caught by this gate and would still hit the rewrite (and, if never exercised before, the same 42883). This is reachable only by hand-adopting an existing enum type astextin the contract, not by anythingpg.enum(...)authoring produces — a corner, not a defect.Release note
docs/releases/v8.0.0-rc.9.mddoes not exist yet. The release-note entry follows once that file exists, matching #30099's precedent forv8.0.0-rc.5.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq
Summary by CodeRabbit
Bug Fixes
DISTINCT ONqueries and NULL values.Tests