Skip to content

Fix: enum ORDER BY / DISTINCT ON loses declaration order behind a derived table - #30099

Open
StevenMcClankerton wants to merge 3 commits into
mainfrom
enum-order-by-derived-table
Open

Fix: enum ORDER BY / DISTINCT ON loses declaration order behind a derived table#30099
StevenMcClankerton wants to merge 3 commits into
mainfrom
enum-order-by-derived-table

Conversation

@StevenMcClankerton

@StevenMcClankerton StevenMcClankerton commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

collectTableSources in the Postgres renderer only recognised FROM entries whose kind is table-source, so the enum-declaration-order rewrite (array_position(ARRAY[...]::text[], col)) never applied once the FROM was a derived table — even though callers deliberately alias that derived table back to the base table name so outer column references keep resolving. ORDER BY (and DISTINCT ON, which shares the same renderOrderByExpr) silently fell back to a bare column sort, reordering an enum-keyed result by lexical text order instead of declaration order.

This is pre-existing, not specific to any one caller: any derived-table wrap loses declaration order the same way — distinct()'s ROW_NUMBER dedup wrap on the plain-select path, a grouped aggregate's pre-group scoping wrap, or DISTINCT ON sharing the ORDER BY renderer. Discovered via manual QA on groupBy-pagination (#30092), where a post-group orderBy() on an enum group key could silently return the wrong group. The operator authorised this as its own PR, sequenced before rc.5 is cut, since the fix belongs in the Postgres adapter with its own test surface, not scoped to any one slice.

Changes

  • Test surface first, red: four cases plus two controls in order-by-enum.integration.test.ts — case 0 (unwrapped, passing control), cases 1/1b (a derived-table wrap with an outer ORDER BY on the enum column, column-ref and identifier-ref forms), case 2 (a derived-table wrap with GROUP BY and a post-group ORDER BY on the group key), case 3 (DISTINCT ON behind a derived table), and a fallback control (a projected expression that isn't a plain column reference falls through to today's bare-column rendering rather than guessing).
  • The fix: replaces the flat table-source map with a resolver that looks a column reference up against the FROM/JOIN source it names. A table-source resolves directly against the contract's storage, same as before. A derived-table-source finds the matching output alias in its own projection and, when the projected expression is itself a plain ColumnRef, recurses into the derived table's own query — so nested wraps resolve too. A projected expression that isn't a plain ColumnRef has no storage column to trace back to and falls through to the pre-existing bare-column rendering, rather than guessing at an order.

Why

Position of the fix. The derived table's alias-back-to-base-table-name convention is load-bearing for GROUP BY and outer column refs across multiple callers; nothing here reintroduces a refTableName parameter to work around the gap instead of fixing it.

Fallback over guessing. A projected expression the resolver can't trace to a storage column (a function call, say) keeps today's plain-column behaviour. A wrong enum order would be a worse failure mode than the existing "no declaration-order rewrite" gap — silently confident and wrong versus silently absent.

Postgres only. SQLite has no declaration-order enum sorting to degrade — confirmed via a full sweep of the adapter's renderers (grep for the resolver's equivalent found no SQLite counterpart) — so there is nothing to fix or test there.

Verification

  • pnpm --filter @internal/adapter-postgres test — full package suite green (repeat runs on a heavily loaded shared CI-adjacent box surfaced unrelated, inconsistent timeouts in RLS/TS-roundtrip/migration-cli tests across different runs; isolated re-runs and a subsequent clean full run confirmed these are pre-existing environmental flakes, not caused by this change — this PR's own new tests passed in every run).
  • pnpm test:packages (root, matches CI's Test job) — green.
  • pnpm build && pnpm typecheck (workspace-wide) — clean.
  • pnpm check:upgrade-coverage --mode pr — no violation; packages/3-targets/** is not a tracked substrate for the upgrade-instructions mechanism (only examples/ and packages/3-extensions/ are), confirmed by reading the script rather than assuming.
  • pnpm lint:deps — clean.

Scope

Does not touch docs/releases/v8.0.0-rc.5.md — that file didn't exist on main when this branch was cut, and now that #30092 has merged and brought it, this PR's release-note entry lands as a follow-up commit once the PR number exists.

Summary by CodeRabbit

  • Breaking Changes

    • ORM commands now use the unified CLI’s mount paths; update scripts accordingly.
  • New Features

    • Added typed column references for raw query .returns definitions.
  • Bug Fixes

    • Corrected enum sorting for ORDER BY and DISTINCT ON, including derived tables and grouped queries.
    • Fixed aggregation, grouped pagination, Postgres error handling, migration paths, and orm init error messages.
    • Improved language server error handling and fallback behavior for unresolved expressions.
  • Documentation

    • Expanded release notes with upgrade guidance and command examples.

@StevenMcClankerton
StevenMcClankerton requested a review from a team as a code owner August 21, 2026 15:19
@coderabbitai

coderabbitai Bot commented Aug 21, 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: Pro Plus

Run ID: 36e5a29b-f328-46ff-a72a-23a38bc38934

📥 Commits

Reviewing files that changed from the base of the PR and between 6fdd35c and 0b19e34.

📒 Files selected for processing (1)
  • docs/releases/v8.0.0-rc.5.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/releases/v8.0.0-rc.5.md

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


📝 Walkthrough

Walkthrough

PostgreSQL enum-aware ordering now resolves projected columns through derived tables for ORDER BY and DISTINCT ON. Integration tests cover grouped queries, identifiers, aggregates, and fallback rendering. Release notes document the fix and other release changes.

Changes

Enum-aware ordering

Layer / File(s) Summary
Query-aware enum source resolution
packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
SelectAst now provides source context for enum value-set resolution. The renderer traces base and derived-table projections and leaves ambiguous or unsupported expressions unchanged.
ORDER BY and DISTINCT ON integration
packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts, packages/3-targets/6-adapters/postgres/test/migrations/order-by-enum.integration.test.ts
ORDER BY and DISTINCT ON use the query-aware resolution path. Integration tests cover derived tables, grouped aggregates, identifiers, and fallback behavior.
Release note updates
docs/releases/v8.0.0-rc.5.md
The release notes describe the enum ordering fix, command changes, typed raw-query references, and additional fixes.

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

Merge Risk: ⚪ Minimal · up to 0b19e

The Postgres renderer now preserves enum declaration order through derived-table ORDER BY and DISTINCT ON paths, with targeted tests and reported green validation checks. No actionable merge-blocking risk remains after normal review.

Sequence Diagram(s)

sequenceDiagram
  participant renderSelect
  participant renderOrderByExpr
  participant SelectAst
  participant EnumValueSets
  renderSelect->>renderOrderByExpr: pass SelectAst for ordering
  renderOrderByExpr->>SelectAst: resolve source and projected column
  SelectAst->>EnumValueSets: trace underlying enum value set
  EnumValueSets-->>renderOrderByExpr: return enum metadata or unresolved result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. (1 skipped: 1 unsupported.) 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 fix for enum ordering behind derived tables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch enum-order-by-derived-table

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/orm-extension-arktype-json

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

@prisma/orm-extension-middleware-cache

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

@prisma/orm-extension-paradedb

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

@prisma/orm-extension-pgvector

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

@prisma/orm-extension-postgis

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

@prisma/orm-extension-supabase

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

@prisma/orm-family-mongo

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

@prisma/orm-family-sql

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

@prisma/orm-framework

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

@prisma/orm-mongo

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

@prisma/orm-postgres

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

@prisma/orm-sqlite

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

@prisma/orm-target-mongo

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

@prisma/orm-target-postgres

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

@prisma/orm-target-sqlite

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

@prisma/orm-toolchain

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

commit: 0b19e34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/3-targets/6-adapters/postgres/test/migrations/order-by-enum.integration.test.ts (1)

324-333: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove or rewrite the stale implementation comment.

The comment describes collectTableSources, but the renderer now uses findFromSource and recursive source resolution. The test names already describe the cases. Remove this block, or label it explicitly as pre-fix behavior.

As per coding guidelines, avoid comments when possible.

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In
`@packages/3-targets/6-adapters/postgres/test/migrations/order-by-enum.integration.test.ts`
around lines 324 - 333, Remove the stale explanatory comment above the migration
test cases; the current renderer uses findFromSource with recursive source
resolution, and the test names already document the scenarios. Do not alter the
test behavior or add replacement commentary unless explicitly labeling
historical pre-fix behavior is necessary.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Nitpick comments:
In
`@packages/3-targets/6-adapters/postgres/test/migrations/order-by-enum.integration.test.ts`:
- Around line 324-333: Remove the stale explanatory comment above the migration
test cases; the current renderer uses findFromSource with recursive source
resolution, and the test names already document the scenarios. Do not alter the
test behavior or add replacement commentary unless explicitly labeling
historical pre-fix behavior is necessary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 08ec1f00-6e46-41e9-8ca8-a738f4a3335f

📥 Commits

Reviewing files that changed from the base of the PR and between 08bf229 and 6fdd35c.

📒 Files selected for processing (3)
  • docs/releases/v8.0.0-rc.5.md
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/order-by-enum.integration.test.ts

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

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
postgres / no-emit 173.69 KB (+0.07% 🔺)
postgres / emit 150.87 KB (+0.05% 🔺)
mongo / no-emit 101.15 KB (0%)
mongo / emit 91 KB (0%)
cf-worker / no-emit 197.43 KB (+0.05% 🔺)
cf-worker / emit 172.1 KB (+0.06% 🔺)

Thegreatsura pushed a commit to Thegreatsura/prisma that referenced this pull request Aug 22, 2026
Closes the aggregate-pagination project and removes its working
artifacts. Documentation-only — no source, no tests, no behaviour.

## ⚠️ Merge order

**This must merge after prisma#30098.** That PR lands the project's retro
learnings into `drive/calibration/dod.md` and the upgrade-instructions
skill. This project produced no long-lived documentation to migrate —
the one guide it wrote was deleted on review — so the learnings are its
only durable output. Merging this first deletes them.

## What the project delivered

`.aggregate()` silently ignored `take` / `skip` / `cursor` / `distinct`
/ `distinctOn`, reducing over every matching row and returning a
confident, wrong number with no signal. `groupBy()` had the same defect
for everything chained before it. Both are fixed, with **clause position
deciding meaning**: before a terminal, clauses shape the rows it
reduces; after `groupBy()`, they page the groups.

- prisma#30067 — root `aggregate()` honours the whole chain
- prisma#30092 — `groupBy()` carries the chain before it; `GroupedCollection`
gained `take` / `skip` / `orderBy` to page groups, with post-group
pagination requiring a prior `orderBy` at the type level

## Definition of Done

All items met, with one closed as deliberately refused:

- Root `aggregate()` honours `take`/`skip`/`cursor` including bare
`skip`, and `distinct()`/`distinctOn()` ✅
- Pre-group clauses scope rows, post-group clauses page groups, both
verified with `having()` present ✅
- Post-group pagination gated on a prior `orderBy` in the type state ✅
- CI-enforced guard that an unpaginated aggregate's compiled AST is
unchanged — the baseline snapshot is byte-identical across every commit
of both slices ✅
- Integration tests assert values, not plan shape, on PGlite **and**
SQLite for each chain position ✅
- `test/aggregate-pagination.test.ts` free of `it.fails` ✅
- No new ORM error subcode ✅
- Position rule documented where a user meets it — **closed as
refused.** Both halves were rejected on operator review: TSDoc as
restating the signatures, and a reference guide as unwarranted for what
is a bug fix. The changelog entries in `v8.0.0-rc.5.md` carry the
user-facing notice.

## Spun out, not dropped

prisma#30099 fixes enum `ORDER BY` / `DISTINCT ON` losing declaration order
behind any derived table. Manual QA found it through the grouped path,
but it is **pre-existing and wider** — `.distinct().orderBy(enumCol)`
has had it since `wrapWithRowNumberDedup` first aliased a derived table
back to its base name. It ships separately, before rc.5 is cut, so no
released version exposes the new route unfixed.

## Notes

Two findings were deliberately not ticketed, per standing direction on
QA follow-ups: an empty TSDoc hover at the `never`-narrowing error site
(`cursor()` behaves identically, so it is a house-level property, not a
slice regression), and the demo's namespaced contract requiring
`db.orm.<ns>.<Model>` where flat-namespace examples use
`db.orm.<Model>`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
Co-authored-by: Steven McClankerton <tatarintsev@prisma.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
SevInf added 3 commits August 24, 2026 07:52
…table

collectTableSources (sql-renderer.ts) only recognises `table-source`
FROM entries, so the enum-ordering hook resolves nothing once the FROM
is a derived table — even though callers deliberately alias that
derived table back to the base table name so outer references keep
resolving. This is not specific to any one caller: any derived-table
wrap loses declaration order the same way.

Four red cases plus two controls:
- case 0: unwrapped ORDER BY (passing control, must keep passing)
- case 1 / 1b: a derived-table wrap with an outer ORDER BY on the enum
  column, column-ref and identifier-ref forms — the shape distinct()'s
  ROW_NUMBER dedup wrap produces on the plain-select path
- case 2: a derived-table wrap with GROUP BY and a post-group ORDER BY
  on the group key — the exact shape a grouped aggregate produces
- case 3: DISTINCT ON an enum column behind a derived table, since it
  shares renderOrderByExpr with ORDER BY
- fallback control: a projected expression that isn't a plain column
  reference has no storage column to resolve — falls back to today's
  bare-column rendering rather than guessing, and must keep doing so

Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
…alias

collectTableSources skipped any FROM entry that wasn't a table-source,
so the enum-ordering hook (array_position over a value-set's
declaration order) never found a column's storage coordinate once the
FROM was a derived table — even though callers alias that derived
table back to the base table name specifically so outer references
keep resolving. ORDER BY (and DISTINCT ON, sharing the same
renderOrderByExpr) fell through to a bare column sort instead,
silently reordering an enum-keyed result by lexical text order.

Replaces the flat table-source map with a resolver that looks a
column reference up against the FROM/JOIN source it names: a
table-source resolves directly against the contract's storage, same
as before; a derived-table-source finds the matching output alias in
its own projection and, when that projected expression is itself a
plain ColumnRef, recurses into the derived table's own query — so a
wrap around a wrap resolves too. A projected expression that isn't a
plain column reference (a function call, say) has no storage column
to trace back to and falls through to today's bare-column rendering,
same as an unresolvable reference always has — a wrong enum order
would be worse than the pre-existing gap.

The derived table's own alias convention is untouched: it still
aliases back to the base table name, and nothing here reintroduces a
ref-table parameter.

Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
One sentence plus the PR citation, matching the two existing entries'
format.

Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
@SevInf
SevInf force-pushed the enum-order-by-derived-table branch from 6fdd35c to 0b19e34 Compare August 24, 2026 07:56
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.

2 participants