Skip to content

TML-2886: derive enum column types by following the contract’s references, not a baked lookup table#833

Closed
wmadden-electric wants to merge 9 commits into
mainfrom
tml-2886-slice-type-columns-from-their-own-valueset-ref-storage-plane
Closed

TML-2886: derive enum column types by following the contract’s references, not a baked lookup table#833
wmadden-electric wants to merge 9 commits into
mainfrom
tml-2886-slice-type-columns-from-their-own-valueset-ref-storage-plane

Conversation

@wmadden-electric

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

Copy link
Copy Markdown
Contributor

The decision

When you read an enum column, the type you get — the exact union of allowed values — is derived by following the reference that column carries in the contract, not frozen into a lookup table at code-generation time. The literal types you see are identical to before; what changes is where they come from. The reference becomes the single source, so the type can never drift from the values it points at.

At a glance

You declare an enum and read a column. The read is typed as the precise value union — in the SQL query builder, in the ORM, and via the exported field-type map. All of these are unchanged by this PR:

// schema: `priority` is an enum column
enum Priority { Low = 'low'  High = 'high'  Urgent = 'urgent' }
model Post { priority Priority }

const row  = await db.sql.public.post.select('priority').build()
row.priority                                  // 'low' | 'high' | 'urgent'   ✅

const post = await db.orm.public.Post.first()
post.priority                                 // 'low' | 'high' | 'urgent'   ✅

type Post = FieldOutputTypes['public']['Post'] // exported field-type map
type P = Post['priority']                      // 'low' | 'high' | 'urgent'   ✅

What changes is how that union is computed.

Before — the generator computed the union once and froze it as a literal in the generated types, and each read surface reached across to that lookup table:

// contract.d.ts (generated)
FieldOutputTypes['Post']['priority']   // 'low' | 'high' | 'urgent'   ← a literal, frozen at emit time
// to type a column, the query builder walked: column → its model field → this table

After — the generated types carry the reference each column and field has, and the enum type is computed by following that reference to the values it names:

// contract.d.ts (generated): the field/column now carry their reference…
column.priority.valueSet   // { plane: 'storage', entityKind: 'valueSet', entityName: 'Priority' }
field.priority.valueSet    // { plane: 'domain',  entityKind: 'enum',     entityName: 'Priority' }

// …and the types resolve the union by indexing through that reference, e.g.:
FieldOutputTypes['public']['Post']['priority']
//  = Contract['domain']['namespaces']['public']['enum']['Priority']['members'][number]['value']
//  → 'low' | 'high' | 'urgent'     (same union — now derived from the reference, not frozen)

Why this is the right shape

A little vocabulary first, since it makes the rest obvious:

  • The contract is the generated description of a database — a JSON file plus matching TypeScript types (contract.d.ts).
  • It describes that database in two layers. The domain layer is the application's view (models, fields, and enums — an enum being a named set of allowed values). The storage layer is the physical database (tables, columns, and "value-sets" — the bare list of literal values a column may hold). A domain enum is realized as a storage value-set.
  • A field or column doesn't inline its allowed values; it carries a small reference naming the value-set (or enum) it belongs to. The values live in one place and are referenced from each site that uses them.
  • There are two read surfaces: the SQL query builder (sql().from(...).select(...)) and the ORM (db.orm.<ns>.<Model>...). Plus an exported per-field type map (FieldOutputTypes / FieldInputTypes) that application code can index directly to get a model's row/input types.

The intended design is that each layer derives its enum types from its own reference — the ORM follows the domain field's reference to the domain enum, the query builder follows the storage column's reference to the storage value-set. Each layer is self-contained.

The old implementation didn't do that. The generator flattened each enum's values into a frozen literal in one lookup table, and the read surfaces typed a column by reaching from the storage column, across to its domain model field, into that table. That reach crossed the layer boundary the design is meant to keep clean, and — more concretely — the generated types never carried the references at all, so the type system couldn't follow them even though the JSON contract recorded them.

This PR closes that gap: (1) emit the references into contract.d.ts (they already existed in the JSON, so this adds nothing to the data — only to the types); (2) derive every enum type — in both read surfaces and in the exported field-type map — by following the reference to the values it names, instead of reading a frozen literal. The literal type is identical; its source is now the reference, which is the single place the values are defined.

What stays the same

  • Observable types are identical — 'low' | 'high' | 'urgent' before and after, on every surface (query builder, ORM, and the exported FieldOutputTypes / FieldInputTypes maps).
  • contract.json and its content hashes are byte-identical. Every change here is in the generated .d.ts text only; the migration and verification path (which already reads references from the JSON) is untouched.
  • Non-enum fields are unchanged (plain codec output, parameterized codecs, value objects, unions).

One wrinkle worth knowing

There are two ways to use a contract: from the emitted contract.d.ts (the normal path, and this project's source of truth), and from an in-memory typeof contract value with no emit step. Only the emitted .d.ts carries the reference at the type level. So the read surfaces keep a reference → precomputed-map → codec fallback that still serves the no-emit path. Retiring that fallback entirely means teaching the authoring types to carry the reference too — a separate change, filed as a follow-up.

Verification

  • Green: pnpm build, pnpm typecheck 138/138, vitest run 10,793 passed / 0 failures.
  • contract.json / storageHash / profileHash byte-identical throughout.
  • Per-hop type tests in each read surface — and now a through-emit ORM read test and direct FieldOutputTypes/FieldInputTypes assertions — fail if a reference is dropped or a value widens (verified non-vacuous: they go red when the reference is broken). Two of these tests were previously never compiled at all (a tsconfig/vitest misconfiguration excluded them); that's fixed here, so they actually run.
  • The no-emit tests and a direct consumer of the field-type map (retail-store, which does type Product = FieldOutputTypes[...]['Product']) stay green.

Alternatives considered

  • Drop the enum type from the exported field-type map entirely (rely only on the read-surface types). Rejected — FieldOutputTypes / FieldInputTypes are exported and indexed directly by application code to get row/input types, so dropping the enum union there silently degrades a user-facing type to string. (An earlier revision of this PR did exactly that; it's corrected here — the map resolves the union by following the reference, same as the read surfaces.)
  • Keep the frozen literal in the map as the source of enum types. Rejected — it requires the cross-layer reach the design forbids, and the frozen copy can drift from the referenced value-set; the reference is the single source.
  • Make the no-emit (typeof contract) path follow references too, and delete the fallback. Deferred, not rejected — it needs the authoring types (a different package) to carry the reference, which is genuinely separate scope. Filed as a follow-up; the emitted path — the one that matters — is fully reference-following today.
  • Inline each column's allowed values instead of referencing a named value-set. Rejected — it duplicates the value list at every using site and across both layers; the named, referenced value-set keeps the values in one place per layer.

Summary by CodeRabbit

  • Chores
    • Refactored enum type resolution in contract generation to follow valueSet references, improving consistency across generated contract definitions across multiple migration and example projects.
    • Updated type-generation machinery to resolve enum column types through storage and domain plane valueSet references rather than baked type maps.
    • Updated Vitest configuration to enable explicit typecheck validation.

wmadden-electric and others added 4 commits June 15, 2026 15:19
…act.d.ts

Adds `readonly valueSet: { ... }` members to both the storage column
literal (generateTableLiteralType) and the domain field literal
(generateModelFieldEntry) when the column or field carries a valueSet
ref. Absent refs emit nothing. Rendered via the existing serializeValue
helper so quoting is consistent with all other literal members.

Fixture .d.ts files regenerated; contract.json, storageHash, and
profileHash are byte-identical.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… lanes)

Replace the baked-map cross-plane reach with reference-following in both
query lanes:

- query-builder (selection.ts): ExtractOutputType follows the storage
  column's own valueSet ref to storage.namespaces[ns].entries.valueSet
  [name].values; deletes FieldOutputOverride and its model->field walk.
- relational-core (ComputeColumnJsType): follows the domain field's
  valueSet ref to domain.namespaces[ns].enum[name].members[*].value.
- sql-builder (table-proxy.ts): ResolvedColumnTypes / insert / update
  follow the storage column's valueSet ref.

Each lane keeps a fallback to the still-baked FieldOutputTypes /
FieldInputTypes map when no valueSet ref is present, then to codec
output. The fallback is required because (a) the maps still carry
value-object / parameterized-codec / union narrowings, and (b) in the
no-emit (`typeof contract`) flow the authored object's storage column
and domain field carry no literal valueSet ref at the type level, so the
baked map is the only carrier of the enum union there. The storage
column ref only exists as a literal in the emitted contract.d.ts.

Also makes the query-builder and sql-builder enum-type type-tests real:
the query-builder tsconfig.json excluded *.test-d.ts from its include,
and sql-builder's vitest typecheck block lacked `enabled: true`, so both
files were silently never type-checked (false green). Fixed both so the
per-hop type-tests actually run and go red if a ref is dropped.

contract.json, storageHash, and profileHash unchanged; demo anchor
(demo-dx.types.test.ts) passes unmodified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ng is ref-only

Remove the enum-union narrowing from the EMITTER's FieldOutputTypes /
FieldInputTypes generation. An enum-backed field's typemap entry is now
the plain codec channel; the emitted contract.d.ts no longer bakes the
value union. The emitted-path lanes now supply the union solely by
following the valueSet ref wired in U2 (storage column ref for the query
lane, domain enum block for the ORM lane), so the emitted demo anchor is
an honest ref-following proof rather than a baked-map false-green.

Removed (emitter only):
- generate-contract-dts.ts: the resolveEnumValues closure and its
  EnumValuesResolver import; no enum resolver is passed into
  generateBothFieldTypesMaps.
- domain-type-generation.ts: the EnumValuesResolver type, the
  renderEnumValueUnion / renderEnumMemberLiteral helpers, the
  field.valueSet?.entityKind === 'enum' fork in resolveFieldType, and the
  resolveEnumValues param threaded through resolveFieldType /
  generateBothFieldTypesMaps / generateFieldOutputTypesMap /
  generateFieldInputTypesMap.

Kept: every other generateBothFieldTypesMaps branch — parameterized-codec
rendering (renderOutputTypeFor / FieldTypeParamsResolver), value-object
aliasing, union-kind fields, and plain codec output. retail-store and the
parameterized-codec path depend on these.

Untouched: the no-emit contract-ts FieldChannelType / EnumValueUnion baker
and its tests (enum-surface.* and contract-ts enum-type.field-output) stay
green — that path is a separate code base and keeps the no-emit enum union
working.

Tests: emitter mechanism tests that asserted the union in the emitted
FieldOutputTypes text now assert the codec channel there plus the emitted
ref + enum block. The demo anchor's one direct FieldOutputTypes-carries-
the-union mechanism assertion is inverted to assert the codec channel,
with a note; its lane-output behavioral assertions are unchanged and now
ref-follow honestly.

contract.json, storageHash, profileHash byte-identical (.d.ts text only).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…om-valueset-ref

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric requested a review from a team as a code owner June 15, 2026 15:15
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@wmadden-electric, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 16 minutes and 37 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: da8339ad-9d26-401e-b371-dfe566889ea9

📥 Commits

Reviewing files that changed from the base of the PR and between d0357aa and 72b895b.

⛔ Files ignored due to path filters (1)
  • projects/enums-as-domain-concept/slices/type-columns-from-valueset-ref/plan.md is excluded by !projects/**
📒 Files selected for processing (2)
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.13-to-0.14/instructions.md
📝 Walkthrough

Walkthrough

The emitter's EnumValuesResolver mechanism is removed: enum-backed fields in FieldOutputTypes/FieldInputTypes are no longer narrowed to string-literal unions. Instead, the emitter adds explicit valueSet descriptor metadata to storage column and domain field contract shapes, and runtime type helpers (relational-core, sql-builder, query-builder) resolve enum unions by following those refs. All example contract.d.ts fixtures are regenerated accordingly.

Changes

valueSet-ref-based Enum Type Resolution

Layer / File(s) Summary
Domain emitter: drop EnumValuesResolver, emit valueSet on field/column descriptors
packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts, packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts, packages/2-sql/3-tooling/emitter/src/index.ts
Removes the EnumValuesResolver export and resolveEnumValues parameter from resolveFieldType, generateBothFieldTypesMaps, generateFieldTypesMapsByNamespace, generateFieldOutputTypesMap, and generateFieldInputTypesMap. generateModelFieldEntry now appends readonly valueSet: ... when field.valueSet is present. The SQL storage emitter conditionally appends a readonly valueSet property to emitted column literals.
Runtime type helpers: resolve enum unions via valueSet refs
packages/2-sql/4-lanes/relational-core/src/types.ts, packages/2-sql/4-lanes/sql-builder/src/types/table-proxy.ts, packages/2-sql/4-lanes/query-builder/src/selection.ts
ComputeColumnJsType gains new type-level helpers to resolve a domain field's valueSet ref to the referenced enum's value union as the first resolution priority. ResolvedColumnTypes branches on ColumnValueSetUnion for output; new ResolvedInsertColumn handles write-path input with the same priority chain. ExtractOutputType replaces FieldOutputOverride with LiteralColumnOf/ColumnValueSetUnion/BakedColumnOutput; TableToSelection uses the literal column map.
Emitter unit and integration tests updated
packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts, packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts, packages/2-sql/3-tooling/emitter/test/emitter-hook.valueset-ref.test.ts
domain-type-generation.test.ts adds generateModelFieldEntry valueSet rendering tests and rewires resolveFieldType assertions to expect codec channel instead of literal unions. Integration test adds a valueSet ref to the storage fixture and replaces union assertions with codec-channel checks. New sql emitter tests cover valueSet ref rendering presence/absence, __unbound__ namespaceId, and int-codec value-agnostic behavior.
Query-builder, sql-builder, and ORM client type-level tests
packages/2-sql/4-lanes/query-builder/test/enum-type.field-output.test-d.ts, packages/2-sql/4-lanes/query-builder/tsconfig.json, packages/2-sql/4-lanes/sql-builder/test/enum-type.field-output.test-d.ts, packages/2-sql/4-lanes/sql-builder/vitest.config.ts, packages/3-extensions/sql-orm-client/test/enum-type.field-output.test-d.ts
Fixtures extended with int4 enum codec, level/Audit.kind columns with valueSet refs, and non-enum name column. Assertions verify ExtractOutputType produces numeric-literal unions for enum columns and string for non-enum. tsconfig.json includes test-d.ts glob; vitest.config.ts enables typecheck. ORM client test adds Level domain enum members and extends read/write assertions.
Regenerated contract.d.ts fixtures
examples/prisma-next-cloudflare-worker/src/prisma/contract.d.ts, examples/prisma-next-demo/src/prisma/contract.d.ts, examples/prisma-next-demo/migrations/app/*/..., examples/prisma-next-demo/test/demo-dx.types.test.ts
All example contract files are regenerated: FieldOutputTypes/FieldInputTypes enum fields switch from string-literal unions to CodecTypes['pg/text@1']['output'/'input'], and both storage and domain contract metadata gain explicit valueSet descriptors. The demo type test now expects string instead of the literal union for Post.priority.
Upgrade instructions updated
skills/upgrade/prisma-next-upgrade/upgrades/0.13-to-0.14/instructions.md, skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
Both upgrade instruction files receive a TML-2886 note documenting that enum column types now resolve via valueSet ref following instead of baked TypeMap unions, with no observable change to resolved enum union types and no required action.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#797: Both PRs change enum field typing in generated contract.d.ts by modifying resolveFieldType/codec input-type rendering so User.kind (and similar enum-backed fields) resolve from enum member literals rather than the generic codec input.
  • prisma/prisma-next#829: Regenerates the Prisma demo migration chain to shift user.kind from a native user_type enum to the value-set/check-constraint representation, which aligns with the main PR's end-contract.d.ts changes.
  • prisma/prisma-next#769: Both PRs are tied to the same enum "valueSet/value-union ref-following" work, regenerating contract.d.ts field types and updating Postgres operations to use valueSet-ordered values.

Poem

🐇 Hop hop, no more union bake,
The valueSet ref is the path I take!
'admin' | 'user' — goodbye, farewell,
Now codec channels do the tell.
Through storage refs the enum flows,
And every type the framework knows. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main change: refactoring enum column type derivation to use contract references instead of a precomputed lookup table.
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.

✏️ 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-2886-slice-type-columns-from-their-own-valueset-ref-storage-plane

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.

…olumns-from-their-own-valueset-ref-storage-plane

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>

# Conflicts:
#	examples/prisma-next-cloudflare-worker/src/prisma/contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260422T0720_initial/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260422T0742_migration/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260422T0742_migration/start-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260422T0748_migration/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260422T0748_migration/start-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260605T1145_mti_variant_link_columns/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260605T1145_mti_variant_link_columns/start-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/start-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260610T2216_set_priority_default/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260610T2216_set_priority_default/start-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260611T1856_convert_user_type_to_value_set/end-contract.d.ts
#	examples/prisma-next-demo/migrations/app/20260611T1856_convert_user_type_to_value_set/start-contract.d.ts
#	examples/prisma-next-demo/src/prisma/contract.d.ts
#	examples/prisma-next-demo/test/demo-dx.types.test.ts
#	packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
#	packages/2-sql/4-lanes/query-builder/src/selection.ts
#	packages/2-sql/4-lanes/query-builder/test/enum-type.field-output.test-d.ts
#	packages/2-sql/4-lanes/relational-core/src/types.ts
#	packages/2-sql/4-lanes/sql-builder/src/types/table-proxy.ts
#	packages/2-sql/4-lanes/sql-builder/test/enum-type.field-output.test-d.ts
#	packages/3-extensions/sql-orm-client/test/enum-type.field-output.test-d.ts
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 152.74 KB (+0.01% 🔺)
postgres / emit 121.05 KB (0%)
mongo / no-emit 78 KB (0%)
mongo / emit 72.09 KB (0%)
cf-worker / no-emit 181.21 KB (+0.01% 🔺)
cf-worker / emit 146.2 KB (0%)

…hanged)

The slice's downstream-facing diff is regenerated `.d.ts` only: enum columns
are now typed by following their own `valueSet` ref instead of a baked
`FieldOutputTypes`/`FieldInputTypes` entry, so an enum field's TypeMap entry
becomes its plain codec channel while the value union moves to lane-level
ref-following. Observable types are unchanged — `select(...).priority` and ORM
reads still resolve to the enum value union — so a consumer re-emit round-trips
with no code action.

Record both surfaces as incidental:
- skills/upgrade/prisma-next-upgrade/upgrades/0.13-to-0.14/instructions.md
  (examples/: regenerated demo contract.d.ts + migration *-contract.d.ts).
- skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
  (packages/3-extensions/: a single sql-orm-client type-test file; no extension
  reads the raw FieldOutputTypes enum entry, and the observable lane types are
  unchanged).

The native-enum SPI deletion and the namespace-nested TypeMaps shape are already
covered by the existing `enum-becomes-domain-concept` and
`namespaced-type-resolution` changes[] entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 72b895b

@wmadden-electric wmadden-electric changed the title TML-2886: type enum columns from their own valueSet ref (storage-plane typed emission) TML-2886: derive enum column types by following the contract’s references, not a baked lookup table Jun 15, 2026
expectTypeOf<PriorityOutput>().toEqualTypeOf<'low' | 'high' | 'urgent'>();
expectTypeOf<PriorityOutput>().not.toEqualTypeOf<string>();
expectTypeOf<PriorityOutput>().toEqualTypeOf<string>();
expectTypeOf<PriorityOutput>().not.toEqualTypeOf<'low' | 'high' | 'urgent'>();

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.

This looks very wrong. PriorityOutput should be the string literals, not string

wmadden-electric and others added 3 commits June 15, 2026 18:33
…on) — type from the ref, not codec output

U3 wrongly degraded the user-facing FieldOutputTypes/FieldInputTypes enum
entries to codec output (string), losing the literal value union — the enum
feature's whole point. FieldOutputTypes is consumed directly by user code
(e.g. `type Post = FieldOutputTypes['public']['Post']`), so this was an
observable regression.

Mechanism: ref-following render (preferred). The emitter now renders an
enum-backed field's FieldOutputTypes/FieldInputTypes entry as a type
expression that FOLLOWS the field's domain valueSet ref into the emitted
domain enum block:
`ContractBase['domain']['namespaces'][ns]['enum'][Name]['members'][number]['value']`.
The entry resolves to the literal member-value union AND is sourced from the
ref — honoring "type from the ref" while keeping the type literal. No
circularity: FieldOutputTypes is emitted above ContractBase in the same
module (TS resolves the forward ref) and ContractBase never references the
field maps.

Non-enum fields are unchanged (codec output / parameterized codecs /
value-objects). U1 (emit refs) and U2 (lanes ref-follow) are untouched; the
lane zeroed-baked-map unit tests stay green — they prove the lanes ref-follow
independent of the emitted map.

Tests: restored demo-dx.types.test.ts to assert
FieldOutputTypes/FieldInputTypes Post.priority is the literal union (removed
the inverted `=== string` assertions); added through-emit ORM coverage
(`db.orm.public.Post.first()` row priority is the union). Updated the emitter
mechanism tests to assert the ref-following render.

contract.json / storageHash / profileHash byte-identical (.d.ts text only).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…) in slice plan

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…havior; cover no-emit enum read

F01: the TML-2886 entries in both 0.13-to-0.14/instructions.md files described
the U3 behavior (enum field's FieldOutputTypes/FieldInputTypes entry "becomes
its plain codec channel"), which U6 reverted. Rewrite both to describe what
actually ships: the emitter renders the enum entry as a ref-following type
expression that indexes the emitted domain enum block, which STILL resolves to
the literal value union — FieldOutputTypes[ns][Model][enumField] stays
'low'|'high'|'urgent', not codec output. The source moved to the ref; the
observable type is unchanged. The "no API/SPI change, observable types
unchanged, no consumer action — incidental" conclusion is kept (correct, and
why it stays an incidental declaration).

F02: the no-emit baked-map fallback's reason-for-existing is already covered.
examples/prisma-next-demo/test/enum-surface.types.test-d.ts (committed, from
#769) asserts a no-emit (typeof contract) enum FIELD read through the
query-builder select — sql.post.select('id','priority') yields
Row['priority'] === 'low'|'high'|'urgent' and .not string — plus the no-emit
write. That exercises the no-emit query lane (the tier the baked-map fallback
backs), so no new test was added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

Closing in favor of a reworked approach. The design here (resolving enum types by following the valueSet ref at the type level) was rejected in design review for two reasons: it pushes a multi-hop type-level traversal into every enum column type — which compounds across multi-table/many-column queries and exhausts TypeScript's instantiation budget on large contracts — and it over-applied symmetry to the ORM, which is a domain consumer for which the existing per-field lookup table is the correct, simple tool.

The replacement keeps the one good idea (the storage plane types its own columns from its own value-sets) but realizes it as baked literal lookups computed in the emitter (a new StorageColumnTypes map; FieldOutputTypes derived from it at emit time), with no type-level reference-following in the emitted output. New branch: tml-2886-baked-storage-column-lookup. Spec under projects/enums-as-domain-concept/slices/type-columns-from-valueset-ref/.

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