Skip to content

fix(target-postgres): avoid uncast array_position for pg.enum ORDER BY - #30191

Open
StevenMcClankerton wants to merge 1 commit into
mainfrom
issue-30163
Open

fix(target-postgres): avoid uncast array_position for pg.enum ORDER BY#30191
StevenMcClankerton wants to merge 1 commit into
mainfrom
issue-30163

Conversation

@StevenMcClankerton

@StevenMcClankerton StevenMcClankerton commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

ORDER BY (and DISTINCT ON, which shares the same renderer) on a pg.enum(...) column rewrote to array_position(ARRAY[...]::text[], <col>) to sort by declaration order, but only cast the array literal to text[] — never the column argument. Against a real Postgres native enum column, no array_position(text[], <enum type>) overload exists, so Postgres rejected the query with 42883 on rc.8. Ordering (or DISTINCT ON-ing) by any pg.enum column failed at runtime; there was no workaround short of retyping the column to String in 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 in ORDER BY/DISTINCT ON — no cast, no rewrite, no 42883.

Fixes #30163

The design choice

The issue's own analysis offered two directions:

  1. Cast the column argument too: array_position(ARRAY[...]::text[], <col>::text).
  2. Gate the rewrite on the column's physical type instead of casting.

This PR takes option 2. Rationale:

  • A native Postgres enum already sorts by declaration order under a plain ORDER BY/DISTINCT ON — Postgres orders enum values by pg_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 bare ORDER BY "col" does not.
  • renderWhere already renders comparisons on the raw column, uncast, for eq/gt/etc. A keyset/cursor WHERE predicate on an enum column therefore already compares by the enum's native ordering. Casting array_position's column argument to text would still sort correctly (it preserves declaration order, not alphabetical order — casting inside array_position is not the same as ORDER BY col::text), but it would leave ORDER BY and WHERE reasoning 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/varchar column with a generated CHECK constraint) are unaffected — they still get the array_position rewrite, since Postgres would otherwise sort them alphabetically. The full order-by-enum.integration.test.ts suite (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.enumsortorder never diverge. That holds today because the migration planner:

  • can only append a new member (ALTER TYPE ... ADD VALUE '<value>', with no BEFORE/AFTER, in the native-enum add-value op), and
  • refuses to plan any other member change — rename, removal, or reorder — via nativeEnumMemberChangeRefusal, 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 (in sql-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) in sql-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 resolveEnumOrderValues and resolveEnumOrderValuesForIdentifier), matching those functions' current shape. #30099 deletes both functions and collectTableSources/TableSourceCoordinate outright, replacing them with findFromSource + 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 a pg.enum column behind a distinct()/groupBy() wrap as a new, untested 42883 site.

The correct reconciliation is a single gate, inside #30099's table-source branch of resolveColumnValueSetFromSource, immediately after storageColumn is resolved:

if (sortsByDeclarationOrderNatively(storageColumn)) return { found: true, values: undefined };

found: true, not false — 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 a MERGE NOTE on sortsByDeclarationOrderNatively in sql-renderer.ts so 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 real CREATE TYPE ... AS ENUM column, 5 cases:

  • qualified column-ref, ASC
  • unqualified identifier-ref, ASC
  • column-ref, DESC
  • a NULL row (column made nullable for this case)
  • DISTINCT ON a native-enum column, matching its ORDER BY (Postgres requires the ORDER BY prefix to match DISTINCT ON)

Every case asserts both the rendered SQL (not.toContain('array_position') + the exact bare ORDER BY/DISTINCT ON text) 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 the array_position rewrite still applies where it should.

Full @internal/adapter-postgres suite: 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, and rls-migration-plan.integration.test.ts intermittently 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), and pnpm lint:deps (root) all clean.

Boundary

The gate keys on the column's codecId (pg/enum@1), not on nativeType. A hand-authored contract could in principle carry a plain text codec (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 as text in the contract, not by anything pg.enum(...) authoring produces — a corner, not a defect.

Release note

docs/releases/v8.0.0-rc.9.md does not exist yet. The release-note entry follows once that file exists, matching #30099's precedent for v8.0.0-rc.5.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq

Summary by CodeRabbit

  • Bug Fixes

    • Fixed ordering for PostgreSQL native enum columns.
    • Native enums now use their declared database order without generating incompatible SQL.
    • Corrected ordering behavior for ascending, descending, qualified, and unqualified columns, including DISTINCT ON queries and NULL values.
  • Tests

    • Added integration coverage for native enum ordering and migration scenarios.

…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>
@StevenMcClankerton
StevenMcClankerton requested a review from a team as a code owner September 1, 2026 16:35
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Team

Run ID: 04479ba5-6e40-4de9-9bd5-3645a94363fa

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0f135 and 639f961.

📒 Files selected for processing (2)
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/order-by-native-enum.integration.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The 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.

Changes

Native enum ordering

Layer / File(s) Summary
Native enum rendering guard
packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
The renderer identifies native enum codecs and skips the array_position rewrite for qualified and unqualified column references.
Native enum query validation
packages/3-targets/6-adapters/postgres/test/migrations/order-by-native-enum.integration.test.ts
Integration tests create and migrate a native enum schema, then verify SQL and result ordering for multiple ORDER BY and DISTINCT ON cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 639f9

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: wmadden-electric

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing invalid array_position SQL for PostgreSQL enum columns.
Linked Issues check ✅ Passed The changes satisfy issue #30163. Native PostgreSQL enum columns bypass the array_position rewrite, which prevents error 42883 while preserving declaration-order sorting. Text-backed value sets retain…
Out of Scope Changes check ✅ Passed The source changes, documentation updates, and integration tests directly support the native-enum ORDER BY and DISTINCT ON fix. No unrelated code changes are evident.
Full details: Linked Issues check

Explanation

The changes satisfy issue #30163. Native PostgreSQL enum columns bypass the array_position rewrite, which prevents error 42883 while preserving declaration-order sorting. Text-backed value sets retain the existing rewrite. Integration tests cover both affected reference paths and the required ordering cases.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-30163

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/orm-extension-arktype-json

npm i https://pkg.pr.new/@prisma/orm-extension-arktype-json@30191

@prisma/orm-extension-middleware-cache

npm i https://pkg.pr.new/@prisma/orm-extension-middleware-cache@30191

@prisma/orm-extension-paradedb

npm i https://pkg.pr.new/@prisma/orm-extension-paradedb@30191

@prisma/orm-extension-pgvector

npm i https://pkg.pr.new/@prisma/orm-extension-pgvector@30191

@prisma/orm-extension-postgis

npm i https://pkg.pr.new/@prisma/orm-extension-postgis@30191

@prisma/orm-extension-supabase

npm i https://pkg.pr.new/@prisma/orm-extension-supabase@30191

@prisma/orm-family-mongo

npm i https://pkg.pr.new/@prisma/orm-family-mongo@30191

@prisma/orm-family-sql

npm i https://pkg.pr.new/@prisma/orm-family-sql@30191

@prisma/orm-framework

npm i https://pkg.pr.new/@prisma/orm-framework@30191

@prisma/orm-mongo

npm i https://pkg.pr.new/@prisma/orm-mongo@30191

@prisma/orm-postgres

npm i https://pkg.pr.new/@prisma/orm-postgres@30191

@prisma/orm-sqlite

npm i https://pkg.pr.new/@prisma/orm-sqlite@30191

@prisma/orm-target-mongo

npm i https://pkg.pr.new/@prisma/orm-target-mongo@30191

@prisma/orm-target-postgres

npm i https://pkg.pr.new/@prisma/orm-target-postgres@30191

@prisma/orm-target-sqlite

npm i https://pkg.pr.new/@prisma/orm-target-sqlite@30191

@prisma/orm-toolchain

npm i https://pkg.pr.new/@prisma/orm-toolchain@30191

commit: 639f961

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
postgres / no-emit 174.91 KB (+0.02% 🔺)
postgres / emit 152.08 KB (+0.04% 🔺)
mongo / no-emit 101.09 KB (0%)
mongo / emit 90.95 KB (0%)
cf-worker / no-emit 198.83 KB (+0.03% 🔺)
cf-worker / emit 173.35 KB (+0.03% 🔺)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ORDER BY on a pg.enum column emits array_position() with an uncast column argument (Postgres 42883)

2 participants