TML-2886: derive enum column types by following the contract’s references, not a baked lookup table#833
Conversation
…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>
|
Warning Review limit reached
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 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 configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe emitter's ChangesvalueSet-ref-based Enum Type Resolution
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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
size-limit report 📦
|
…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>
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
| expectTypeOf<PriorityOutput>().toEqualTypeOf<'low' | 'high' | 'urgent'>(); | ||
| expectTypeOf<PriorityOutput>().not.toEqualTypeOf<string>(); | ||
| expectTypeOf<PriorityOutput>().toEqualTypeOf<string>(); | ||
| expectTypeOf<PriorityOutput>().not.toEqualTypeOf<'low' | 'high' | 'urgent'>(); |
There was a problem hiding this comment.
This looks very wrong. PriorityOutput should be the string literals, not string
…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>
|
Closing in favor of a reworked approach. The design here (resolving enum types by following the 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 |
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:
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:
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:
Why this is the right shape
A little vocabulary first, since it makes the rest obvious:
contract.d.ts).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
'low' | 'high' | 'urgent'before and after, on every surface (query builder, ORM, and the exportedFieldOutputTypes/FieldInputTypesmaps).contract.jsonand its content hashes are byte-identical. Every change here is in the generated.d.tstext only; the migration and verification path (which already reads references from the JSON) is untouched.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-memorytypeof contractvalue with no emit step. Only the emitted.d.tscarries the reference at the type level. So the read surfaces keep areference → precomputed-map → codecfallback 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
pnpm build,pnpm typecheck138/138,vitest run10,793 passed / 0 failures.contract.json/storageHash/profileHashbyte-identical throughout.FieldOutputTypes/FieldInputTypesassertions — 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 (atsconfig/vitestmisconfiguration excluded them); that's fixed here, so they actually run.retail-store, which doestype Product = FieldOutputTypes[...]['Product']) stay green.Alternatives considered
FieldOutputTypes/FieldInputTypesare exported and indexed directly by application code to get row/input types, so dropping the enum union there silently degrades a user-facing type tostring. (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.)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.Summary by CodeRabbit