Skip to content

TML-2852: enums become first-class in application code — typed I/O, db.enums, declaration-order ORDER BY#769

Merged
wmadden-electric merged 47 commits into
mainfrom
tml-2852-application-read-surface
Jun 10, 2026
Merged

TML-2852: enums become first-class in application code — typed I/O, db.enums, declaration-order ORDER BY#769
wmadden-electric merged 47 commits into
mainfrom
tml-2852-application-read-surface

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

At a glance

In the demo's own contract, Post carries a Priority enum — and the column is typed as its value union everywhere you touch it:

const Priority = enumType('Priority', pgText,
  member('Low', 'low'), member('High', 'high'), member('Urgent', 'urgent'));   // declaration order ≠ lexical

// on a real model:  Post { …, priority: field.namedType(Priority) }

// READ — priority is 'low' | 'high' | 'urgent' (not string), sorted by DECLARATION order
getPostsByPriority()   // → rows ordered low, low, high, urgent  (not lexical)

// WRITE — only member values compile; 'nope' is a compile error, and the CHECK constraint rejects it at the DB

// INTROSPECT at runtime
db.enums.public.Priority.values        // ['low', 'high', 'urgent']  (ordered, literal-typed)
db.enums.public.Priority.members.High  // 'high'

This runs end-to-end against PGlite in examples/prisma-next-demo — typed read, db.enums.<ns>, declaration-order ORDER BY, and the slice-2 CHECK rejecting out-of-union writes.

What this decides

An enum becomes a first-class application concept: its value union flows into the static read/write types of both query lanes, db.enums.<ns>.<Name> exposes it at runtime, and ORDER BY on an enum column sorts by declaration order. It works for text and int-backed enums and in both authoring forms (definition and factory defineContract). Built on the merged substrate (slice 1) and check-constraint enforcement (slice 2), and lands additively — PSL enum stays native until the cutover, so only enumType-authored contracts exercise it.

How it builds up

  1. Typed I/O (R4/R5) — narrow the codec type by the field's valueSet to the value union, on both paths: the authored Definition (no-emit) and the emitted contract.d.ts — the emitter resolves a field's valueSet ref to the enum's member-value union, codec-agnostically (text and int). Both lanes inherit it through the field-output typemap; non-enum fields are unchanged.
  2. db.enums.<ns>.<Name> (R6) — a runtime, literal-typed accessor (.values / .members.X / .has / .nameOf / .ordinalOf) built from that namespace's domain enums. Enums are lane-agnostic contract metadata, so db.enums lives on the db facade alongside transaction / prepare / raw / context (decided with the query team) — a namespace-keyed map projected per target exactly like db.sql / db.orm. It matches the IR (domain.namespaces[ns].enum) and lets the same enum name in two namespaces resolve independently. Unbound-namespace targets (sqlite/mongo) get db.enums.<Name> via the existing per-facade projection. Because enums sit on the facade rather than adjacent to models, no reserved-name guard is needed — a model named enums no longer collides.
  3. Declaration-order ORDER BY (R8, Postgres) — renders array_position(ARRAY[…]::text[], col) over the value-set's ordered values.
  4. Factory-form authoring — the new enum is authorable in the demo's real factory-form contract (a top-level enums key threaded through the factory overload, mirroring the definition form), not only the definition form.

Scope

Additive / dark. PSL enum keeps lowering to native (the repoint is the cutover, TML-2853); member defaults are TML-2855; non-Postgres ORDER BY (MySQL FIELD(...) / SQLite CASE) is future. Existing fixtures are byte-identical apart from the demo.

CI note

The repo-wide pnpm typecheck is red on inherited Cannot find module subpath errors (/contract-builder, /migration, /adapter, /aggregate, /constants) that reproduce on clean origin/main — a separate main-health issue, not introduced here.

Alternatives considered

  • Narrow from the emitted contract JSON rather than the authored Definition — rejected: emission widens the value-set to string[], erasing the literals.
  • A bespoke "enum codec" — rejected: every field already carries a codec; the enum is a valueSet restriction layered on top.
  • A dedicated orderByDeclarationOrder() API — rejected: ordering an enum column by declaration order should just work; the renderer handles it implicitly.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added enum type definitions with first-class support for enum-backed model fields in SQL contracts.
    • Enum fields now narrow to literal value unions (e.g., 'user' | 'admin' instead of generic string) in both read and write operations, with compile-time type checking.
    • Exposed enum metadata and lookup utilities (values list, names, member maps, membership/ordinal checks) on the client facade under db.enums.<namespace>.<EnumName> (lane-agnostic namespace-keyed map).
    • Enum-backed ORDER BY now respects enum declaration order rather than lexical order.
    • Added support for both string and numeric enum values.

@wmadden-electric wmadden-electric requested a review from a team as a code owner June 8, 2026 12:26
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Parameterizes enum generics in contract authoring, widens enum member/value typing and encodes values during contract build, exposes typed enum accessors on the ORM client, adds enum-aware ORDER BY lowering in Postgres renderer, and adds type/runtime tests and demo integration across the stack.

Changes

Enum Support Across the Stack

Layer / File(s) Summary
Enum authoring API: value widening & encoding
packages/2-sql/2-authoring/contract-ts/src/enum-type.ts, packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
Enum members accept non-string JSON values tied to codec input types; duplicate-value checks stringify for messages; build-time encodeEnumValue encodes member/value JSON for domain/storage.
Contract builder generics & merge
packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts, packages/2-sql/2-authoring/contract-ts/src/exports/contract-builder.ts
ContractDefinition/ContractScaffold/ContractFactory accept Enums generics; LiteralEnums/MergeEnums combine scaffold+factory enum shapes; buildBoundContract merges runtime enums and emits enums only when non-empty.
Contract types & field channel typing
packages/2-sql/2-authoring/contract-ts/src/contract-types.ts, packages/2-sql/2-authoring/contract-ts/src/exports/contract-builder.ts
Detect EnumTypeHandle type-refs, short-circuit descriptor lookup, add enumAccessors on SqlContractResult, and compute `FieldChannelTypes<Definition, 'output'
Query & SQL builder propagation
packages/2-sql/4-lanes/query-builder/src/selection.ts, packages/2-sql/4-lanes/sql-builder/test/*
ExtractOutputType now prefers field-level enum output overrides (from contract) over codec fallback; sql-builder tests validate non-nullable enum fields narrow to literal unions and nullable include null.
ORM enum accessors
packages/3-extensions/sql-orm-client/src/enum-accessor.ts, packages/3-extensions/sql-orm-client/src/orm.ts, packages/3-extensions/sql-orm-client/test/*
New EnumAccessor interface and createEnumAccessor/buildEnumsMap; orm() exposes lazy, frozen db.enums with per-enum accessors (values, names, members, has, nameOf, ordinalOf); tests and d.ts assertions added.
Postgres extension: defineContract & enum binding
packages/3-extensions/postgres/src/contract/define-contract.ts, packages/3-extensions/postgres/src/contract/enum-type.ts, packages/3-extensions/postgres/src/exports/contract-builder.ts
Adds Enums generics and EnumsConstraint to Postgres defineContract overloads, exposes a Postgres-bound enumType via bindEnumType, and re-exports enum builder types/values.
Postgres schema valueSet support
packages/3-targets/3-targets/postgres/src/core/postgres-schema.ts, packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts
Adds optional valueSet entries to PostgresSchema input/entries, constructs StorageValueSet nodes, and serializes/deserializes valueSet when present.
Postgres enum-order SQL rendering
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
renderSelect uses renderOrderByExpr; enum-backed string columns lower to array_position(ARRAY[...]::text[], column) for declaration-order sorting; ambiguous unqualified refs and non-string value-sets fall back to plain rendering; integration tests added.
Examples & tests
examples/prisma-next-demo/*, various test/* and *.test-d.ts files across lanes
Demo adds Priority enum, query helper getPostsByPriority, and runtime/type tests validating enum surface, ordering, CHECK constraints, and type narrowing.
Minor test typing adjustments
packages/3-extensions/postgres/test/postgres.test.ts, packages/3-extensions/sqlite/test/transaction.test.ts
Adjusted mock cast patterns (via unknown) for TypeScript compatibility in tests.

Sequence Diagram (high-level flow)

sequenceDiagram
  participant Authoring as ContractAuthoring
  participant Builder as ContractBuilder
  participant Runtime as ORMRuntime
  participant Postgres as PostgresRenderer
  ContractAuthoring->>ContractBuilder: defineContract/enums + enumType
  ContractBuilder->>ContractBuilder: encodeEnumValue, merge scaffold+factory enums
  ContractBuilder->>ORMRuntime: produce SqlContractResult (enumAccessors)
  ORMRuntime->>ORMRuntime: buildEnumsMap/createEnumAccessor (lazy on db.enums)
  ORMRuntime->>PostgresRenderer: query AST with enum-backed columns
  PostgresRenderer->>PostgresRenderer: renderOrderByExpr -> array_position over valueSet
Loading

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#731: Related changes to buildBoundContract / defineContract wiring; overlaps in contract-builder typing.
  • prisma/prisma-next#750: Related enum/value-set propagation and contract builder work in the SQL authoring lane.
  • prisma/prisma-next#499: Prior work on enum entity modeling that this PR extends with typed enum generics and downstream handling.

"I'm a rabbit, I hop and I write,
Enums in order, values just right.
From builder to DB, helpers parade,
Arrays and accessors neatly made.
🐇🥕"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the primary changes: adding enums as first-class in application code with typed I/O, runtime surface (db.enums), and declaration-order ORDER BY support.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2852-application-read-surface

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 and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@769

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@769

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@769

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@769

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@769

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@769

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@769

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@769

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@769

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@769

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@769

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@769

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@769

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@769

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@769

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@769

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@769

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@769

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@769

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@769

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@769

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@769

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@769

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@769

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@769

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@769

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@769

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@769

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@769

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@769

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@769

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@769

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@769

prisma-next

npm i https://pkg.pr.new/prisma-next@769

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@769

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@769

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@769

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@769

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@769

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@769

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@769

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@769

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@769

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@769

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@769

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@769

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@769

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@769

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@769

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@769

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@769

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@769

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@769

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@769

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@769

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@769

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@769

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@769

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@769

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@769

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@769

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@769

commit: d90ea31

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 151.62 KB (+0.71% 🔺)
postgres / emit 119.99 KB (+0.57% 🔺)
mongo / no-emit 76.67 KB (0%)
mongo / emit 70.96 KB (0%)
cf-worker / no-emit 180.95 KB (+0.54% 🔺)
cf-worker / emit 145.99 KB (+0.35% 🔺)

wmadden and others added 5 commits June 8, 2026 15:20
…ut types (TML-2852 Dispatch 1)

FieldOutputType and FieldInputType in contract-types.ts now resolve to the
literal value union (e.g. 'user' | 'admin') when the field was authored with
field.namedType(enumHandle), instead of falling back to the codec's string
output.  The key fix was replacing the ModelStorageColumn conditional path
(which TypeScript defers because it contains a type-variable extends check)
with a direct ModelFieldState index access for the nullable and codec-id
lookups.

ExtractOutputType in query-builder/selection.ts now consults
ExtractFieldOutputTypes via a FindModelForTable/FindFieldForColumn lookup
before falling back to the codec type, so enum columns propagate the literal
union through TableToSelection as well.

Type-tests added in all four affected packages (contract-ts, sql-orm-client,
sql-builder, query-builder) covering both read output and write input, plus a
negative case that rejects out-of-union literals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ient

Adds `db.enums.<Name>` to the sql-orm-client: a runtime accessor map built
from the contract domain enum entity, plus the type-level connection so
`db.enums.Role.values` resolves to the literal value tuple and
`.members.User` to the value literal.

- enum-accessor.ts: `createEnumAccessor` / `buildEnumsMap` (no bare casts;
  `Object.freeze` already yields `readonly`).
- orm.ts: Proxy `enums` branch + `enums` typed off the contract`s
  `enumAccessors`.
- contract-builder.ts: thread a `const Enums` generic through
  `defineContract` so authored enum handles keep their literal types.
- contract-types.ts: `BuiltEnumAccessors` derives the accessor shape from
  the authored `EnumTypeHandle`; widened (enum-less) contracts stay empty.
- Tests: runtime accessors + Proxy branch; a type-test asserting the
  literal `values` tuple (not `string[]`) and `.members.User === 'user'`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… BY (TML-2852 Dispatch 3)

An ORDER BY on an enum-restricted column now renders
array_position(ARRAY[v1, v2, …]::text[], <col>) over the value-set's
ordered values, so rows sort by declaration order rather than lexically.

The Postgres sql-renderer builds an alias→storage-coordinate map from the
SELECT's FROM and JOIN table sources, resolves an ORDER BY column-ref to
its storage column, and — when that column carries a valueSet — emits the
array_position expression from the value-set's ordered values. Non-enum
columns render unchanged. A PGlite integration test inserts rows out of
declaration order and asserts the declaration-order result, plus a
nullable-column case (array_position returns NULL for NULL rows, which
sort last under the default ASC NULL handling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric force-pushed the tml-2852-application-read-surface branch from d33e6a2 to c011d3a Compare June 8, 2026 13:34

@wmadden wmadden 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.

Only a few small comments. Overall looks good.

Comment thread packages/2-sql/2-authoring/contract-ts/src/contract-types.ts Outdated
Comment thread packages/2-sql/4-lanes/query-builder/src/selection.ts Outdated
wmadden and others added 4 commits June 8, 2026 15:48
…solution (TML-2852)

D1 rewrote FieldOutputType/FieldInputType with an inline ResolveFieldDescriptor
path that re-widened non-enum field result types to string (id, createdAt),
regressing the query-builder ResultType inference covered by integration-tests
contract-builder.test.ts and .types.test-d.ts.

Restore mains codec-based resolution verbatim as FieldCodecOutputType and
FieldCodecInputType, and layer the enum value-union narrowing on top via
EnumNarrowedType, which only narrows when the field carries an enum typeRef.

The non-enum widening came from EnumValueUnion structurally matching the default
EnumTypeHandle and inferring Values as string[]. Guard it with a tuple-wrapped
extends check plus a string[] extends Values bail-out so only fields with a
concrete enum handle narrow; everything else falls through to mains codec output.

In query-builder selection.ts, ExtractOutputType consults the field-output
typemap only when it carries a concrete literal override, otherwise falling back
to mains exact ExtractCodecTypes codec output expression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…(TML-2852)

D2 added enums to OrmClient, so the loose () => ({ lane }) mocks no longer
overlap typeof orm and the as typeof ormMock cast failed with TS2352. Route the
mock implementations through unknown so the consumer stubs satisfy the richer
client type without weakening OrmClient.enums.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…rder sort (TML-2852)

The Postgres ORDER BY enum hook only matched qualified column-ref order items.
The sql-builders .orderBy(col) string and callback forms both emit an
identifier-ref (bare column, no table qualifier), so a natural ORDER BY on an
enum column fell through to a plain column reference and sorted lexically.

Resolve identifier-ref order columns against the SELECTs FROM/JOIN sources and
render array_position over the value-sets ordered values when exactly one source
carries that enum column; an ambiguous name across joined tables falls through
to the bare column. Adds a PGlite test for the identifier-ref form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… end to end (TML-2852)

A focused example app authored with the TypeScript enumType API (not PSL enum)
that demonstrates all three slice surfaces against PGlite:

- Typed I/O: the enum field reads as the value union (not string) and a write
  only accepts the union; an out-of-union literal is a compile error
  (enum-demo.types.test-d.ts).
- db.enums: the runtime accessor exposes values/names/members/has/ordinalOf in
  declaration order (enum-demo.integration.test.ts).
- Declaration-order ORDER BY via array_position, plus the slice-2 CHECK
  constraint rejecting out-of-union values (enum-demo.integration.test.ts).

The runtime passes the TypeScript-authored contract directly so the value-union
types flow from the authored definition. The emitted contract.json / contract.d.ts
are committed and kept current via emit:check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric changed the title TML-2852: application read surface — typed enum I/O, db.enums, declaration-order ORDER BY TML-2852: enums become first-class in application code — typed I/O, db.enums, declaration-order ORDER BY Jun 8, 2026

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts`:
- Around line 282-297: The current resolution loop over sourcesByRef only treats
ambiguity when multiple enum-backed columns match, allowing a rewrite to
array_position(...) when the same column name exists on multiple sources but
only one is enum; change the logic in sql-renderer.ts so that before setting
resolved (and before performing the array_position rewrite) you first count
matches of the identifier across all sources (use sourcesByRef and
contract.storage.namespaces lookups used in the existing loop), and only proceed
to set resolved/valueSet and perform the enum rewrite when the identifier
matches exactly one source column in total (not just one enum column); apply the
same uniqueness check to the other matching block that leads to the
array_position rewrite (the code around the existing resolved variable usage and
the 317-321 region) so ambiguous column names are not rewritten.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7c1e650a-68d2-4642-b876-c4cedb51f2f4

📥 Commits

Reviewing files that changed from the base of the PR and between c011d3a and bc85f26.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • examples/enum-demo/README.md
  • examples/enum-demo/biome.jsonc
  • examples/enum-demo/package.json
  • examples/enum-demo/prisma-next.config.ts
  • examples/enum-demo/src/contract.d.ts
  • examples/enum-demo/src/contract.json
  • examples/enum-demo/src/contract.ts
  • examples/enum-demo/src/db.ts
  • examples/enum-demo/test/enum-demo.integration.test.ts
  • examples/enum-demo/test/enum-demo.types.test-d.ts
  • examples/enum-demo/test/init-db.ts
  • examples/enum-demo/tsconfig.json
  • examples/enum-demo/vitest.config.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
  • packages/2-sql/4-lanes/query-builder/src/selection.ts
  • packages/3-extensions/postgres/test/postgres.test.ts
  • packages/3-extensions/sqlite/test/transaction.test.ts
  • 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
✅ Files skipped from review due to trivial changes (8)
  • examples/enum-demo/tsconfig.json
  • examples/enum-demo/prisma-next.config.ts
  • examples/enum-demo/src/contract.ts
  • packages/3-extensions/sqlite/test/transaction.test.ts
  • examples/enum-demo/src/contract.json
  • examples/enum-demo/biome.jsonc
  • examples/enum-demo/README.md
  • examples/enum-demo/src/contract.d.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/2-sql/4-lanes/query-builder/src/selection.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-types.ts

Comment thread packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
wmadden and others added 3 commits June 8, 2026 16:50
…L-2852)

Reduce the enum value-union narrowing to a shallow, single-pass form and fix a
non-null enum column reading as `union | null`.

Behavior spec (TDD, tests first):
- A non-null enum field reads/writes as exactly its value union (no `| null`).
- A nullable enum field reads/writes as `union | null`.
- A non-text (int-backed) enum narrows to its int value union (e.g. `1 | 10`).
- Non-enum fields keep their codec output unchanged.

Implementation:
- contract-types.ts: compute a field`s non-null base type once (enum value
  union when the field is enum-typed, else the codec channel), apply nullability
  once. Replaces the deeply nested FieldCodecOutputType / EnumNarrowedType pair
  (max conditional depth ~10 -> ~4).
- contract-types.ts: resolve an enum-handle field`s storage descriptor and
  type-ref from the handle, so an enum column is a real StorageColumn with the
  correct `nullable: false` instead of collapsing to `never` (the `never extends
  true` vacuous-truth that injected the spurious `| null` in the select lane).
- query-builder selection.ts: delete FindModelForTable / FindFieldForColumn /
  EnumOutputOverride / FieldOutputTypeFor; read the FieldOutputTypes override in
  one flat mapped-type pass (max depth ~7 -> ~3).
- enum-type.ts + build-contract.ts: enum member values may be string or number
  literals (preserved in the handle for narrowing); encode to strings when
  lowering to the storage value-set / domain enum members.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…enum-demo (TML-2852)

Move the enum read-surface example out of a standalone `examples/enum-demo`
app and into the existing `prisma-next-demo`, and fix the Postgres value-set
materialization gap the move surfaced.

- Postgres value-set materialization: `postgresCreateNamespace` and the
  Postgres contract serializer dropped the `entries.valueSet` slot, so an
  `enumType`-authored enum built through the Postgres builder lost its
  storage value-set. The CHECK constraint and the declaration-order ORDER BY
  both reference that value-set, so `db init` failed ("value-set not found")
  and ORDER BY fell through to a bare column. `PostgresSchema` now carries
  `valueSet`, and the serializer round-trips it.
- postgres `defineContract`: thread an `enums` field through the wrapper so a
  Postgres contract can declare `enumType` enums; re-export `enumType` /
  `member` / `EnumTypeHandle` from `@prisma-next/postgres/contract-builder`.
- prisma-next-demo: add a TS-authored `enumType` enum (`Priority`) on a small
  definition-form contract and demonstrate the three surfaces with tests —
  typed read/write narrowing (`*.types.test-d.ts`), `db.enums` accessors, and
  declaration-order ORDER BY against a dev database. PSL `enum` stays native.
- Delete `examples/enum-demo` entirely (workspace + lockfile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…any joined source shares the name (TML-2852)

resolveEnumOrderValuesForIdentifier only fell through when two enum-backed
columns matched. A join where the same column name exists on multiple sources
but only one is enum-backed still rewrote to array_position over a bare
identifier — ambiguous SQL. Now count every source column with that name (enum
or not); rewrite only when exactly one source has it and it carries a value-set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/2-sql/2-authoring/contract-ts/src/enum-type.ts (1)

188-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate uniqueness on the lowered storage value.

seenValues now treats 1 and '1' as distinct, but build-contract.ts later normalizes both to "1". That means enumType('E', ..., member('One', 1), member('TextOne', '1')) passes here and then emits duplicate enum/value-set entries downstream. Please compare uniqueness on String(m.value) (or otherwise on the lowered representation) so authored enums stay one-to-one after serialization.

🛠️ Suggested fix
-  const seenValues = new Set<string | number>();
+  const seenLoweredValues = new Set<string>();
   for (const m of members) {
     if (seenNames.has(m.name)) {
       throw new Error(
         `enumType("${name}"): duplicate member name "${m.name}". Member names must be unique.`,
       );
     }
     seenNames.add(m.name);

-    if (seenValues.has(m.value)) {
+    const loweredValue = String(m.value);
+    if (seenLoweredValues.has(loweredValue)) {
       throw new Error(
-        `enumType("${name}"): duplicate member value "${m.value}". Member values must be unique.`,
+        `enumType("${name}"): duplicate member value "${loweredValue}" after storage encoding. Member values must be unique.`,
       );
     }
-    seenValues.add(m.value);
+    seenLoweredValues.add(loweredValue);
   }
🤖 Prompt for AI Agents
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/2-sql/2-authoring/contract-ts/src/enum-type.ts` around lines 188 -
203, The uniqueness check for enum member values in the members loop treats
numeric 1 and string "1" as distinct; change the logic in enum-type.ts (inside
the members iteration where seenValues is used) to compare and store the
lowered/serialized representation (e.g., String(m.value)) so duplication is
detected after serialization; update seenValues to be a Set<string>, perform
seenValues.has(String(m.value)) for the duplicate check, and then
seenValues.add(String(m.value)) when storing the value.
packages/2-sql/2-authoring/contract-ts/src/contract-types.ts (1)

594-603: ⚠️ Potential issue | 🟠 Major

Fix enumAccessors accessor method parameter types to use the authored enum value union

EnumHandleAccessorType still types has/nameOf/ordinalOf as v: string (even though enum handles are string | number at runtime and carry literal value tuples), so int-backed enums will incorrectly reject db.enums.<Enum>.has(1) / nameOf(10) / ordinalOf(1) at compile time.

🛠️ Suggested fix
 type EnumHandleAccessorType<Handle> =
   Handle extends EnumTypeHandle<infer _Name, infer Values, infer Names, infer MembersMap>
     ? {
         readonly values: Values;
         readonly names: Names;
         readonly members: MembersMap;
-        has(v: string): boolean;
-        nameOf(v: string): string | undefined;
-        ordinalOf(v: string): number;
+        has(v: Values[number]): boolean;
+        nameOf(v: Values[number]): string | undefined;
+        ordinalOf(v: Values[number]): number;
       }
     : never;
🤖 Prompt for AI Agents
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/2-sql/2-authoring/contract-ts/src/contract-types.ts` around lines
594 - 603, EnumHandleAccessorType currently types the has/nameOf/ordinalOf
parameters as v: string which prevents numeric enum values from compiling;
update the parameter types for has, nameOf, and ordinalOf to accept the authored
enum value union (use the inferred Values tuple to derive the value type, e.g.
Values[number] or the equivalent union) in the EnumHandleAccessorType definition
so these methods accept both string and numeric enum members.
🧹 Nitpick comments (1)
examples/prisma-next-demo/test/utils/enum-control-client.ts (1)

27-29: 💤 Low value

Optional: remove redundant mkdirSync after mkdtempSync.

mkdtempSync at line 27 already creates the directory atomically, so the mkdirSync call at line 29 is redundant. While the recursive: true flag makes this a harmless no-op, removing it would simplify the code.

♻️ Simplify by removing the redundant mkdir
 const migrationsDir = mkdtempSync(join(tmpdir(), 'prisma-next-demo-enum-migrations-'));
 try {
-  mkdirSync(migrationsDir, { recursive: true });
   const initResult = await client.dbInit({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma-next-demo/test/utils/enum-control-client.ts` around lines 27
- 29, Remove the redundant mkdirSync call: after creating migrationsDir with
mkdtempSync (symbol migrationsDir), delete the subsequent
mkdirSync(migrationsDir, { recursive: true }) invocation since mkdtempSync
already creates the directory atomically; ensure any surrounding try/catch or
error handling still makes sense after removing the mkdirSync line.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/2-sql/2-authoring/contract-ts/src/contract-types.ts`:
- Around line 594-603: EnumHandleAccessorType currently types the
has/nameOf/ordinalOf parameters as v: string which prevents numeric enum values
from compiling; update the parameter types for has, nameOf, and ordinalOf to
accept the authored enum value union (use the inferred Values tuple to derive
the value type, e.g. Values[number] or the equivalent union) in the
EnumHandleAccessorType definition so these methods accept both string and
numeric enum members.

In `@packages/2-sql/2-authoring/contract-ts/src/enum-type.ts`:
- Around line 188-203: The uniqueness check for enum member values in the
members loop treats numeric 1 and string "1" as distinct; change the logic in
enum-type.ts (inside the members iteration where seenValues is used) to compare
and store the lowered/serialized representation (e.g., String(m.value)) so
duplication is detected after serialization; update seenValues to be a
Set<string>, perform seenValues.has(String(m.value)) for the duplicate check,
and then seenValues.add(String(m.value)) when storing the value.

---

Nitpick comments:
In `@examples/prisma-next-demo/test/utils/enum-control-client.ts`:
- Around line 27-29: Remove the redundant mkdirSync call: after creating
migrationsDir with mkdtempSync (symbol migrationsDir), delete the subsequent
mkdirSync(migrationsDir, { recursive: true }) invocation since mkdtempSync
already creates the directory atomically; ensure any surrounding try/catch or
error handling still makes sense after removing the mkdirSync line.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a209978a-97a7-42f7-9907-30108d7a8de5

📥 Commits

Reviewing files that changed from the base of the PR and between bc85f26 and f642ed8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • examples/prisma-next-demo/prisma/enum-contract.ts
  • examples/prisma-next-demo/prisma/enum-db.ts
  • examples/prisma-next-demo/test/enum-surface.integration.test.ts
  • examples/prisma-next-demo/test/enum-surface.types.test-d.ts
  • examples/prisma-next-demo/test/utils/enum-control-client.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
  • packages/2-sql/2-authoring/contract-ts/src/enum-type.ts
  • packages/2-sql/2-authoring/contract-ts/test/enum-type.field-output.test.ts
  • packages/2-sql/4-lanes/query-builder/src/selection.ts
  • packages/3-extensions/postgres/src/contract/define-contract.ts
  • packages/3-extensions/postgres/src/exports/contract-builder.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-schema.ts
  • test/integration/test/contract-builder.types.test-d.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/3-extensions/postgres/src/exports/contract-builder.ts

wmadden and others added 3 commits June 8, 2026 17:30
Collapse the enum read/write field-type helpers to the flattest shape the
behavior tests require, without changing non-enum resolution:

- Replace the `IsEnumTypeRef` boolean threaded through three descriptor
  resolvers with a single `EnumFieldHandle<FieldState>` that yields the
  handle (or never). `ResolveFieldDescriptor` and `ResolveFieldColumnTypeRef`
  now short-circuit on it; for non-enum fields both reduce to mains exact
  bodies. `ResolveFieldColumnTypeParams` reverts to byte-identical-to-main
  (it already produced undefined for enum fields via the column-type-ref).
- Merge `FieldOutputType` + `FieldInputType` + `FieldNullable` + `FieldBaseType`
  into one channel-parameterised `FieldChannelType` and one `FieldChannelTypes`
  map; nullability is a flat union member, not an outer conditional.

Net -27 lines; no behavior change (enum field-output type-tests and the
full-lane ResultType spec stay green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…852)

The factory form of defineContract dropped a top-level `enums` key returned
from the factory callback: its `Built` generic carried only `types`/`models`
and the impl spread only those, so an enum authored through the factory never
reached `SqlContractResult` -> field narrowing / db.enums.

Mirror the definition form. The factory `Built` type now carries
`readonly enums?: Enums` captured with a `const Enums` generic, the impl spreads
`built.enums`, and the public factory overload merges scaffold-declared and
factory-returned enums into the narrowing `Enums` slot. A parity type-test
asserts the factory form narrows field reads/writes to the value union and
surfaces the same `enumAccessors` shape as the definition form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…L-2852)

Thread a factory-returned `enums` key through the Postgres `defineContract`
wrapper so its factory form carries enum handles (previously only the scaffold
arg did), mirroring the base builder. The demo contract (factory form) now
declares a `Priority` enum (declaration order low -> high -> urgent, which
differs from lexical) and references it on `Post.priority` via
`field.namedType`.

The no-emit app code consumes it for real: `getPostsByPriority` reads the
narrowed value union and orders by declaration order, and `getPriorityEnum`
surfaces `db.enums.Priority`. The rewritten enum-surface tests prove the read
narrows to the union (not string), `db.enums` exposes the declaration-ordered
runtime surface, declaration-order ORDER BY sorts low/low/high/urgent, and the
CHECK constraint rejects out-of-union writes.

Removes the side-contract (prisma/enum-contract.ts, prisma/enum-db.ts,
test/utils/enum-control-client.ts) it replaces. The PSL schema is untouched
(PSL enum repoint is a later slice).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/2-sql/2-authoring/contract-ts/src/contract-types.ts (1)

596-605: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Accept numeric enum values in accessor lookups.

Lines 602-604 hard-code string lookup inputs, but this PR also introduces int-backed enums (Priority in the new factory-form test is 1 | 10). That means enumAccessors.Priority.has(1), nameOf(1), and ordinalOf(1) are rejected by the public types even though numeric enum values are now first-class elsewhere in the surface.

Suggested fix
 type EnumHandleAccessorType<Handle> =
   Handle extends EnumTypeHandle<infer _Name, infer Values, infer Names, infer MembersMap>
     ? {
         readonly values: Values;
         readonly names: Names;
         readonly members: MembersMap;
-        has(v: string): boolean;
-        nameOf(v: string): string | undefined;
-        ordinalOf(v: string): number;
+        has(v: string | number): boolean;
+        nameOf(v: string | number): Names[number] | undefined;
+        ordinalOf(v: string | number): number;
       }
     : never;
🤖 Prompt for AI Agents
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/2-sql/2-authoring/contract-ts/src/contract-types.ts` around lines
596 - 605, The accessor type EnumHandleAccessorType currently restricts lookup
inputs to string, which breaks int-backed enums; update the method signatures on
EnumHandleAccessorType (the has, nameOf, and ordinalOf members) to accept both
string and number (e.g., change parameter type from string to string | number)
so numeric enum values like 1 or 10 are allowed in lookups while keeping return
types the same.
packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts (1)

435-439: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Merge scaffold and factory enums before building the contract.

Lines 435-439 currently overwrite definition.enums with built.enums. That disagrees with the factory-form type on Lines 504-529, which advertises MergeEnums<ScaffoldEnums, FactoryEnums>. In a mixed contract, any scaffold-authored enum disappears from the built contract as soon as the factory returns an enums object, so the runtime enum surface no longer matches the published type.

Suggested fix
     return buildContractFromDsl({
       ...full,
       ...ifDefined('types', built.types),
       ...ifDefined('models', built.models),
-      ...ifDefined('enums', built.enums),
+      ...ifDefined(
+        'enums',
+        built.enums === undefined
+          ? undefined
+          : {
+              ...(definition.enums ?? {}),
+              ...built.enums,
+            },
+      ),
     });
🤖 Prompt for AI Agents
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/2-sql/2-authoring/contract-ts/src/contract-builder.ts` around lines
435 - 439, The returned contract currently overwrites scaffold enums with
factory enums; change the merge so scaffold and factory enums are combined
before calling buildContractFromDsl by merging full.enums and built.enums into a
single enums object (respecting scaffold keys and letting factory keys override
where intended) instead of using ifDefined('enums', built.enums); update the
return in the function that calls buildContractFromDsl to pass the merged enums
(referencing buildContractFromDsl, ifDefined, definition.enums, built.enums and
the MergeEnums<ScaffoldEnums, FactoryEnums> contract type) so runtime enums
match the published MergeEnums type.
packages/3-extensions/postgres/src/contract/define-contract.ts (1)

24-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the Postgres wrapper's enum generics split between scaffold and factory.

This wrapper collapses both enum sources into one Enums generic. Unlike the core defineContract overload in packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts (Lines 489-530), that means defineContract({ enums: { A } }, () => ({ enums: { B } })) cannot model the combined shape through the Postgres API. The extension surface ends up narrower than the builder it delegates to.

Suggested fix
 export function defineContract<
   const Types extends TypesConstraint = Record<never, never>,
   const Models extends ModelsConstraint = Record<never, never>,
   const ExtensionPacks extends
     | Record<string, ExtensionPackRef<'sql', string>>
     | undefined = undefined,
-  const Enums extends EnumsConstraint = Record<never, never>,
+  const ScaffoldEnums extends EnumsConstraint = Record<never, never>,
+  const FactoryEnums extends EnumsConstraint = Record<never, never>,
 >(
-  scaffold: PostgresScaffold<ExtensionPacks, Enums>,
+  scaffold: PostgresScaffold<ExtensionPacks, ScaffoldEnums>,
   factory: (helpers: ComposedAuthoringHelpers<SqlFamily, PostgresPack, ExtensionPacks>) => {
     readonly types?: Types;
     readonly models?: Models;
-    readonly enums?: Enums;
+    readonly enums?: FactoryEnums;
   },
-): PostgresResult<Types, Models, ExtensionPacks, Enums>;
+): PostgresResult<
+  Types,
+  Models,
+  ExtensionPacks,
+  MergeEnums<ScaffoldEnums, FactoryEnums>
+>;

Also applies to: 84-98

🤖 Prompt for AI Agents
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-extensions/postgres/src/contract/define-contract.ts` around lines
24 - 40, The Postgres wrapper collapses scaffold and factory enums into one
Enums generic; update type PostgresResult to accept two enum generics (e.g.
EnumsScaffold and EnumsFactory) and pass them through to buildBoundContract so
both the scaffold-provided enums and the factory-provided enums are preserved.
Concretely: replace the single Enums generic with separate EnumsScaffold and
EnumsFactory generics, change the scaffold-side shape to use enums?:
EnumsScaffold and ensure the factory-side shape (the buildBoundContract
return/input) is typed to accept enums?: EnumsFactory so the wrapper delegates
both enum sources to buildBoundContract (referencing the PostgresResult type and
buildBoundContract, SqlFamily, and PostgresPack symbols).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts`:
- Around line 435-439: The returned contract currently overwrites scaffold enums
with factory enums; change the merge so scaffold and factory enums are combined
before calling buildContractFromDsl by merging full.enums and built.enums into a
single enums object (respecting scaffold keys and letting factory keys override
where intended) instead of using ifDefined('enums', built.enums); update the
return in the function that calls buildContractFromDsl to pass the merged enums
(referencing buildContractFromDsl, ifDefined, definition.enums, built.enums and
the MergeEnums<ScaffoldEnums, FactoryEnums> contract type) so runtime enums
match the published MergeEnums type.

In `@packages/2-sql/2-authoring/contract-ts/src/contract-types.ts`:
- Around line 596-605: The accessor type EnumHandleAccessorType currently
restricts lookup inputs to string, which breaks int-backed enums; update the
method signatures on EnumHandleAccessorType (the has, nameOf, and ordinalOf
members) to accept both string and number (e.g., change parameter type from
string to string | number) so numeric enum values like 1 or 10 are allowed in
lookups while keeping return types the same.

In `@packages/3-extensions/postgres/src/contract/define-contract.ts`:
- Around line 24-40: The Postgres wrapper collapses scaffold and factory enums
into one Enums generic; update type PostgresResult to accept two enum generics
(e.g. EnumsScaffold and EnumsFactory) and pass them through to
buildBoundContract so both the scaffold-provided enums and the factory-provided
enums are preserved. Concretely: replace the single Enums generic with separate
EnumsScaffold and EnumsFactory generics, change the scaffold-side shape to use
enums?: EnumsScaffold and ensure the factory-side shape (the buildBoundContract
return/input) is typed to accept enums?: EnumsFactory so the wrapper delegates
both enum sources to buildBoundContract (referencing the PostgresResult type and
buildBoundContract, SqlFamily, and PostgresPack symbols).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 62792a6c-5926-4b5e-85c2-4e0ed53c2b8e

📥 Commits

Reviewing files that changed from the base of the PR and between f642ed8 and 6e2afa1.

📒 Files selected for processing (10)
  • examples/prisma-next-demo/prisma/contract.ts
  • examples/prisma-next-demo/src/prisma-no-emit/priority-feed.ts
  • examples/prisma-next-demo/test/enum-surface.integration.test.ts
  • examples/prisma-next-demo/test/enum-surface.types.test-d.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/enum-type.factory-form.test.ts
  • packages/3-extensions/postgres/src/contract/define-contract.ts
  • 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
✅ Files skipped from review due to trivial changes (1)
  • examples/prisma-next-demo/test/enum-surface.types.test-d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts

Comment thread examples/prisma-next-demo/prisma/enum-contract.ts Outdated
Comment thread examples/prisma-next-demo/prisma/contract.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-ts/src/enum-type.ts Outdated
wmadden and others added 4 commits June 9, 2026 13:52
…g|number (TML-2852)

member<N, V> stays generic over any literal V; enumType constrains member
values to the codec descriptors input type via CodecInput<C> (falls back to
unknown when the descriptor carries no input phantom). EnumTypeHandle and the
db.enums accessor (has/nameOf/ordinalOf) now accept the value union, so
int-backed enums accept numeric values. Uniqueness now compares the lowered
String(value) form so 1 and "1" collide as build-contract later normalizes them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
buildBoundContract previously spread built.enums into the DSL input, overwriting
any scaffold-authored definition.enums. The factory-form type advertises
MergeEnums<ScaffoldEnums, FactoryEnums>, so a mixed contract silently lost the
scaffold enums at runtime. Merge both sides instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ntract (TML-2852)

The postgres wrapper collapsed scaffold + factory enums into one Enums generic,
so it could not model defineContract({ enums: { A } }, () => ({ enums: { B } }))
— narrower than the core defineContract it delegates to. Split into
ScaffoldEnums / FactoryEnums and return MergeEnums<ScaffoldEnums, FactoryEnums>,
mirroring the core overload. Exposes MergeEnums from the core builder for reuse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… demo test (TML-2852)

Remove the redundant comment on contract.ts. The enum accessor probe with a
non-member value now annotates the value union, since has() is typed to the
codec value union after making member values codec-driven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
wmadden and others added 5 commits June 10, 2026 12:23
Conflict resolutions (done before this session):
- packages/3-extensions/sql-orm-client/src/orm.ts: took main's
  namespace-facet structure; layered our enums root accessor on top
  (prop === 'enums' guard before the namespace check; OrmClient
  intersection adds `readonly enums: ContractEnumAccessors<TContract>`).
- packages/3-extensions/postgres/test/postgres.test.ts: took main's
  cast-free namespaced mock shape.
- packages/3-extensions/sqlite/test/transaction.test.ts: took main's
  cast-free namespaced mock shape.

TML-2816 adaptations (done in this session):
- examples/prisma-next-demo/src/prisma-no-emit/context.ts: createOrmClient
  now returns the full ORM client (not just client['public']) so that
  getPriorityEnum can reach db.enums.Priority at the root.
- examples/prisma-next-demo/src/queries/get-users-with-posts-no-emit.ts:
  updated to access the public namespace via db['public'] with a runtime
  guard, matching the new namespace-qualified ORM API.
- packages/2-sql/4-lanes/sql-builder/test/enum-type.field-output.test-d.ts:
  updated db.User → db.__unbound__.User; Db<C> is now namespace-qualified
  and the test contract uses __unbound__.
- packages/3-extensions/sql-orm-client/test/enum-type.field-output.test-d.ts:
  passed namespaceId: '__unbound__' to the Collection constructor; the
  constructor now requires the third options argument.
- packages/3-extensions/sqlite/src/runtime/sqlite.ts: broadened
  unboundNamespace parameter from the narrow index-signature type to
  object (using blindCast to index); the OrmClient intersection adding
  enums made the old parameter type incompatible.
- test/integration/test/contract-builder.types.test-d.ts: updated
  enumDb.account → enumDb.public.account; defineContract with flat models
  places them in the public namespace, which is now the required access path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
The flat root `db.enums` accessor violated the namespace-keyed client root
(TML-2816) and last-write-wins collapsed same-named enums across namespaces.
Move enums into each namespace facet under a reserved `enums` key:
`db.<ns>.enums.<Name>` (postgres `db.orm.public.enums`, sqlite `db.orm.enums`
via the unbound projection). `buildEnumsMapForNamespace` resolves only that
namespace's `domain.namespaces[ns].enum`, so same-named enums in two
namespaces resolve independently. A domain model named `enums` is rejected at
facet construction rather than silently shadowed.

The facet enum accessor type derives from the emitted `domain.namespaces[ns].enum`
entries (literal members) and, for the no-emit built contract, from its flat
`enumAccessors` carrier; both preserve the ordered literal `values` tuple.
Revert the sqlite `unboundNamespace` widening now that the removed root
intersection no longer breaks its narrower param type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Enums move from a flat root `db.enums` into the namespace facet under a
reserved `enums` key (`db.<ns>.enums.<Name>`). Record the decision, its
rationale (TML-2816 namespace-keyed root, IR alignment, cross-namespace
collision), the reserved-name rule (runtime guard now, authoring diagnostic at
cutover), and that this is the template for future client-side entity-accessor
maps. Repoint the slice spec's `db.enums` references to the facet path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…facet (TML-2852)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…→0.14)

The slice is purely additive — enumType-authored enums gain typed I/O,
db.<ns>.enums, and declaration-order ORDER BY; PSL enum stays native and
fixtures:check is byte-identical for existing contracts. Records the per-PR
upgrade-coverage declaration (no consumer action) for both the user and
extension-author skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric added this pull request to the merge queue Jun 10, 2026
@wmadden wmadden removed this pull request from the merge queue due to a manual request Jun 10, 2026
wmadden and others added 6 commits June 10, 2026 14:22
…cade (TML-2852)

Add buildNamespacedEnums(domain) + the NamespacedEnums<TContract> type to
enum-accessor.ts, relocating the per-namespace accessor types out of the orm
facet. Enums are contract metadata, the same whether reached through the sql
or orm lane, so they belong on the db facade, not inside the orm namespace
facet. Remove the orm facet enums key and its reserved-name guard; the orm
client returns to a pure namespace map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Add a top-level db.enums member to the postgres and sqlite clients: a
namespace-keyed map projected per target exactly like db.sql / db.orm.
Postgres exposes db.enums.<ns>.<Name>; sqlite projects the unbound namespace
so users write db.enums.<Name>. Built once at construction from
contract.domain via buildNamespacedEnums and frozen; the same map is reused on
the transaction context. Tests cover the same-named-enum-in-two-namespaces
collision fix on the facade map. Mongo is untouched (no SQL enum surface).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…L-2852)

The no-emit demo has no facade, so build the namespace-keyed enum map directly
from the validated contract (the same surface the postgres facade exposes as
db.enums) and read enums.public.Priority. getPriorityEnum no longer takes a
runtime — enums are lane-agnostic contract metadata. The types test derives
PriorityValue from the enums surface and asserts the inferred read type with no
annotation, preserving the review-comment fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Update design-notes, slice spec, plan, and the 0.13-to-0.14 upgrade-instruction
comments from db.<ns>.enums to db.enums.<ns>. Record the decision: enums are
lane-agnostic contract metadata, so they live on the db facade as a
namespace-keyed map projected per target like db.sql / db.orm; the reserved-name
guard is no longer needed because enums are not adjacent to models. This is the
template for future client-side entity-accessor maps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…-2852)

Use a literal-keyed two-namespace contract so db.enums.public.Role resolves as
a static facade proof rather than index-signature access, fixing the test-file
typecheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden enabled auto-merge June 10, 2026 13:01
@wmadden wmadden added this pull request to the merge queue Jun 10, 2026
Comment thread examples/prisma-next-demo/src/prisma-no-emit/context.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-ts/src/build-contract.ts Outdated
Comment thread packages/3-extensions/postgres/test/postgres-close.test.ts Outdated
Comment thread packages/3-extensions/postgres/test/postgres.test.ts Outdated
Comment thread packages/3-extensions/sqlite/test/transaction.test.ts Outdated
Mongo enums move from a non-goal to scope. The domain enum is framework-level
and target-agnostic; Mongo realizes the value-set restriction as a $jsonSchema
collection validator (validationLevel: strict) instead of a CHECK constraint,
needing no value-set storage IR or migration-ops parallel. Mongo has no native
enum and no prior PSL enum, so it skips the transitional keyword and the cutover
entirely. Planned as one complete vertical slice (author -> enforce -> read,
proven end-to-end against mongodb-memory-server) rather than split, since a
non-vertical slice cannot be shown to work. Independent of the SQL track.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden removed this pull request from the merge queue due to a manual request Jun 10, 2026
wmadden and others added 3 commits June 10, 2026 15:35
…aCodec

encodeEnumValue and encodeDefaultLiteralValue both did the same
codec-or-fallback dance. Merge them into one encodeViaCodec helper used
for both the column-default and enum-lowering paths. Folds the former
bare `as JsonValue` on the default path into the unified blindCast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ndation

The enum-accessor builders and types are built purely from
contract.domain.namespaces[ns].enum (framework ContractEnum), with
nothing ORM-specific. Living in sql-orm-client forced the postgres and
sqlite facade tests to wholesale-mock @prisma-next/sql-orm-client and
pass buildNamespacedEnums through the mock to avoid a construction crash.

Move the module to @prisma-next/contract (the 0-foundation layer both
SQL facades and a future Mongo facade already depend on) under a new
@prisma-next/contract/enum-accessor entrypoint, and relax the type
constraint from Contract<SqlStorage> to Contract so it stays
lane-neutral. Update the postgres/sqlite facades, the postgres read-
surface type test, and the demo no-emit context to the new path; drop
the now-unneeded buildNamespacedEnums pass-through from all four facade
test mocks. The facade tests now run against the real, unmocked accessor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
The no-emit context hand-wired stack/context/sql and hand-built the
enums map with two blindCasts, behind a comment claiming "the no-emit
path has no facade to hang db.enums on" — which is wrong: postgres()
accepts a TypeScript-authored contract with deferred binding and exposes
sql/enums/context/stack directly.

Source those from the facade (db.sql.public, db.enums, db.context,
db.stack), dropping both blindCasts, the misleading comment, and the
custom rawCodecInferer stub (the facade uses the adapter inferer).
createOrmClient still uses the orm() builder because the demo binds an
externally-created runtime; everything else now comes from db.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric added this pull request to the merge queue Jun 10, 2026
Merged via the queue into main with commit dc72201 Jun 10, 2026
29 of 30 checks passed
@wmadden-electric wmadden-electric deleted the tml-2852-application-read-surface branch June 10, 2026 14:48
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
The merge pulled in three enum tests from main (PR #769) that construct
runtimes through the deleted createRuntime / mocked-deserializer path. Port
them to this branch's design:

- postgres.test.ts: the two db.enums facade tests passed contractJson:{} with
  a mocked deserializeContract and asserted mocks.createRuntime. This branch's
  postgres.test.ts mocks only the pg boundary, so pass the real
  twoNamespaceDomain as contract and assert the pool never connects instead of
  inspecting a (deleted) createRuntime mock.
- enum-surface.integration.test.ts: openRuntime built the runtime via
  instantiateExecutionStack + createRuntime; rebuild it through the
  postgres({ contract, url, extensions }).connect() facade, matching
  repositories.integration.test.ts.
- runtime.ts: context.ts no longer exports validatedContract (took main's
  db-facade form), so pass the raw contract; the facade deserializes it.

Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SevInf added a commit that referenced this pull request Jun 10, 2026
Mains enum work (#769) renders enum value unions into the field-type maps;
our slice nests those maps by namespace id. Reconcile the enum type-tests to
the nested shape: index ExtractFieldOutputTypes/InputTypes through the default
namespace (public) or the fixtures __unbound__ coordinate, and pass the NsId
coordinate to ContractToQC. Enum value unions still resolve per-namespace; no
flat-map read reintroduced.

Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
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