TML-2882: transitional enum2 block makes the new enum PSL-authorable#805
Conversation
…lock Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
Parses `enum2 <Name> { @@type("<codec-id>") Name = <literal> … }` into a
distinct PslEnum2 AST node kind alongside the existing native enum parse.
- PslEnum2 / PslEnum2Value types added to framework-components psl-ast,
including flatPslEnum2s helper and enum2s field on PslNamespace
- PSL_INVALID_ENUM2_MEMBER diagnostic code added to PslDiagnosticCode
- parseEnum2Block dedicated parse in psl-parser: bare members, = value
members (raw captured text + span), @@type block attribute, @Map
rejected with diagnostic
- Missing @@type parses through cleanly (required-ness is D2 interpreter
validation, not grammar)
- Native enum parsing byte-identical untouched
- Exports updated: PslEnum2, PslEnum2Value, flatPslEnum2s from parser index
- parser-enum2.test.ts: 13 cases covering all dispatch requirements
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Move the = value match before the @Map( check so codec-legal string values containing that substring (e.g. Foo = "@Map(x)") are captured as raw text rather than rejected with PSL_INVALID_ENUM2_MEMBER. The @Map rejection now only fires for lines without an assignment, which is its intended purpose (catching native-enum Member @Map("…") syntax). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
- Add processEnum2Declarations to interpreter.ts: validates @@type attribute, resolves codec via codecLookup, validates member values through decodeJson, calls enumType() directly to build EnumTypeHandle - Thread enum2Handles into buildModelNodeFromPsl and buildSqlContractFromDefinition - Preserve namespaceSlice.enum in patchedContract reconstruction (was silently dropped) - Register enum2 presence-signal factory in postgresAuthoringEntityTypes - Thread codecLookup from provider context into interpretPslDocumentToSqlContract - Add 13 tests: parity, 9 diagnostics, mixed doc, non-string codec Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…esolution
`resolveContractColumnTypeMetadata` in verify-sql-schema threw when a
column's `typeRef` pointed to a value-set name rather than a
`storage.types` entry. Added an early return that uses the column's
own `codecId`/`nativeType`/`typeParams` when `valueSet` is present.
`codecRefForStorageColumn` in codec-ref-for-column returned `undefined`
for the same case, causing ParamRef-without-codec errors at SQL render
time. Added a fallback that returns `{ codecId: columnDef.codecId }`
when the column has a `valueSet` reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… (D3)
Authors `enum2 Priority { @@type("pg/text@1") Low High Urgent }` in the
demo schema and re-emits the contract. The emitted contract.d.ts narrows
Post.priority to `'low' | 'high' | 'urgent'` — not string.
Adds migration 20260610T0000_add_priority_enum with addColumn,
setNotNull, and addCheckConstraint ops for the priority column.
Adds the `enum-priority` subcommand to main.ts that exercises the
enum accessor and queries posts by priority through the emitted contract.
Adds four type tests in demo-dx.types.test.ts:
- FieldOutputTypes['Post']['priority'] equals the value union
- SELECT priority yields the union in ResultType
- getPostsByPriority rows have priority typed as the union
- INSERT rejects values outside the union via @ts-expect-error
Updates integration test fixtures in repositories and sql-dsl tests to
supply `priority: 'low'` on all post inserts (required after adding the
NOT NULL column).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… revert band-aids (D3-R2)
The PSL enum2 path set typeRef: decl.name on the ColumnDescriptor so the
handle-lookup site in buildModelNodeFromPsl could find the EnumTypeHandle.
buildStorageColumn copies descriptor.typeRef onto the emitted column, so the
internal key leaked into the contract as a dangling typeRef with no matching
storage.types entry, producing a different column shape than the TS path.
Fix: remove typeRef from the enum2 descriptor. Use resolvedField.field.typeName
(the PSL field's unqualified type name, already equal to the enum2 name) at
the lookup site instead. The emitted column is now {codecId, nativeType,
nullable, valueSet} — byte-identical to the TS enumType path.
Harden the D2 parity test: replace toMatchObject (subset) with toEqual
(strict) for all five asserted shapes and add a storageHash equality check.
The test was red before the fix, green after.
Revert both ead068f SQL-family band-aids (verify-sql-schema.ts early-return
on valueSet; codec-ref-for-column.ts valueSet fallback) — they are dead code
now that no enum2 column carries a typeRef. Both files are byte-identical to
their pre-ead068f57 state.
Re-emitted demo contract.json/contract.d.ts and regenerated migration
end-contract.* + migration.json/migration.ts for the add_priority_enum
migration via the existing regen script. Migration ops unchanged:
addColumn → setNotNull → addCheckConstraint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds enum2 extension-block parsing and a codec-aware interpreter pass; renames value-set discriminators/refs to camelCase; and introduces a demo Priority enum migration plus queries, CLI, seed, and tests. ChangesExtension Block Infrastructure and enum2 Language
ValueSetRef and StorageValueSet Naming Normalization
Demo Priority Enum End-to-End
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
@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: |
size-limit report 📦
|
wmadden
left a comment
There was a problem hiding this comment.
There's a surprising amount of custom logic in the parser for a target-contributed block type. I think this is a bit of a mess. Lots of parser logic which should be in the interpreter, and only exposed in Postgres despite being database-independent (check constraints).
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (1)
1516-1527: 💤 Low valueConsider using
ifDefinedfor consistency.Line 1525 uses a conditional spread
...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {})to conditionally include theenumTypeHandleproperty. Per the codebase learning onifDefinedusage, prefer...ifDefined('enumTypeHandle', enumHandle)for consistency with other conditional spreads in this repo.♻️ Proposed refactor
fields: resolvedFields.map((resolvedField) => { const enumHandle = input.enum2Handles?.get(resolvedField.field.typeName); return { fieldName: resolvedField.field.name, columnName: resolvedField.columnName, descriptor: resolvedField.descriptor, nullable: resolvedField.field.optional, ...ifDefined('default', resolvedField.defaultValue), ...ifDefined('executionDefaults', resolvedField.executionDefaults), - ...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}), + ...ifDefined('enumTypeHandle', enumHandle), }; }),🤖 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-psl/src/interpreter.ts` around lines 1516 - 1527, The conditional spread uses a ternary to add enumTypeHandle which is inconsistent with the project's ifDefined helper; inside the fields mapping for resolvedFields (the arrow mapping that references enumHandle and resolvedField), replace the ternary spread ...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}) with the canonical spread ...ifDefined('enumTypeHandle', enumHandle) so the property is only included when enumHandle is defined and to match other uses of ifDefined in the same block.Source: Learnings
🤖 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 `@examples/prisma-next-demo/src/queries/get-posts-by-priority.ts`:
- Around line 19-21: The current query orders the text column 'priority'
lexicographically, producing wrong order; replace the lexicographic
.orderBy('priority') with an explicit rank-based ordering (e.g., a
CASE/priority_rank mapping for enum values low=1, high=2, urgent=3) and order by
that computed rank before the existing .orderBy('id'); update the query in
get-posts-by-priority (the chain using .select('id','title','priority') and
.orderBy('priority')) to compute or select the rank (or use an orderByRaw/CASE)
and then .orderBy(rank) then .orderBy('id') so ties use id as before.
In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Line 453: When nativeType is obtained from
input.codecLookup?.targetTypesFor(codecId)?.[0] but codec is undefined (from
codec = input.codecLookup?.get(codecId)), emit a diagnostic instead of allowing
enum2 members to be validated without a codec; update the interpreter.ts flow
around the const codec = input.codecLookup?.get(codecId) line to detect if
nativeType is truthy and codec is falsy and call the existing
diagnostic/reporting utility with a clear message referencing codecId (and the
enum2 member/context) so inconsistent CodecLookup implementations
(targetTypesFor() vs get()) are surfaced and validation short-circuited.
---
Nitpick comments:
In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 1516-1527: The conditional spread uses a ternary to add
enumTypeHandle which is inconsistent with the project's ifDefined helper; inside
the fields mapping for resolvedFields (the arrow mapping that references
enumHandle and resolvedField), replace the ternary spread ...(enumHandle !==
undefined ? { enumTypeHandle: enumHandle } : {}) with the canonical spread
...ifDefined('enumTypeHandle', enumHandle) so the property is only included when
enumHandle is defined and to match other uses of ifDefined in the same block.
🪄 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: ea6ea943-bfdf-48e9-b633-e71314a15829
⛔ Files ignored due to path filters (2)
projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/plan.mdis excluded by!projects/**projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/spec.mdis excluded by!projects/**
📒 Files selected for processing (26)
examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/contract.prismaexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.tsexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.jsonexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.jsonexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.tsexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/ops.jsonexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/start-contract.d.tsexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/start-contract.jsonexamples/prisma-next-demo/scripts/seed.tsexamples/prisma-next-demo/src/main.tsexamples/prisma-next-demo/src/prisma/contract.d.tsexamples/prisma-next-demo/src/prisma/contract.jsonexamples/prisma-next-demo/src/prisma/contract.prismaexamples/prisma-next-demo/src/queries/get-posts-by-priority.tsexamples/prisma-next-demo/test/demo-dx.types.test.tsexamples/prisma-next-demo/test/repositories.integration.test.tsexamples/prisma-next-demo/test/sql-dsl.integration.test.tspackages/1-framework/1-core/framework-components/src/control/psl-ast.tspackages/1-framework/1-core/framework-components/src/shared/psl-extension-block.tspackages/1-framework/2-authoring/psl-parser/src/exports/index.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.tspackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/src/provider.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.tspackages/3-targets/3-targets/postgres/src/core/authoring.ts
Delete all enum2-specific parser logic (parseEnum2Block, PslEnum2, PslEnum2Value, PslNamespace.enum2s, flatPslEnum2s, PSL_INVALID_ENUM2_MEMBER). The generic parseExtensionBlock now handles enum2 when the descriptor is registered. Generic extension-block grammar gains: - @@attr(…) block-attribute lines (PslExtensionBlockAttribute, stored in blockAttributes) - Bare-identifier entries (PslExtensionBlockParamBare, kind:'bare'), guarded by allowAdditionalParameters on the descriptor - PSL_EXTENSION_DUPLICATE_PARAMETER diagnostic for repeated keys - PSL_INVALID_EXTENSION_BLOCK_ATTRIBUTE diagnostic for malformed @@ lines Register enum2 as a SQL-family-level pslBlockDescriptor (not per-target) in packages/2-sql/9-family, with a stub entityType entry to satisfy assertPslBlocksHaveFactories. allowAdditionalParameters:true lets members flow through without unknown-parameter diagnostics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
processEnum2Declarations now accepts PslExtensionBlock[] instead of PslEnum2[]. - @@type attribute is found in decl.blockAttributes (not decl.attributes) - Members are iterated via Object.entries(decl.parameters); bare entries use the member name as the codec input, value entries JSON.parse the raw string - enum2EntityDescriptor presence check removed: support falls out of codec resolution (missing @@type or unknown codec id gives a diagnostic, not a gated absence) - Duplicate member names are now PSL_EXTENSION_DUPLICATE_PARAMETER from the parser; duplicate values remain PSL_ENUM2_DUPLICATE_MEMBER_VALUE from the interpreter - provider.ts passes pslBlockDescriptors from authoringContributions to parsePslDocument - Delete postgres stub entityTypes.enum2 (moved to SQL-family level in previous commit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…ber query
- Export Priority from prisma/contract.ts so tests and seed can reference it
- Replace 'low' as const / 'high' as const / 'urgent' as const literals in
seed.ts, repositories.integration.test.ts, and sql-dsl.integration.test.ts
with Priority.members.{Low,High,Urgent} from the TS-authored EnumTypeHandle
- Add getPostsByPriorityMember() to get-posts-by-priority.ts: filters posts by
a named member using Priority.members[memberName] and a typed WHERE clause
- Wire enum-priority-filter [member] [limit] command into main.ts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…PSL blocks Add `interpreterLowered?: boolean` to `AuthoringPslBlockDescriptor`. When set, `assertPslBlocksHaveFactories` skips the factory-matching check for that block. Delete the dead `sqlFamilyAuthoringEntityTypes` stub (factory: () => null) from the SQL family. The `enum2` block is handled by `processEnum2Declarations` in contract-psl; it has no factory and doesn't need one. Mark it `interpreterLowered: true` so the assertion is satisfied without the stub. Add tests for both the relaxed (pass) and un-relaxed (reject) paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…iles The four demo files (seed.ts, repositories test, sql-dsl test, get-posts-by-priority.ts) were importing Priority from prisma/contract.ts (the TS authoring definition) and using its .members values directly. Replace with a priorityValue(name) helper that reads from getPriorityEnumFromEmit().members[name] at runtime. EnumAccessor.members returns Readonly<Record<string, JsonValue>>, which is wider than the required 'low' | 'high' | 'urgent' union — the blindCast is intentional and the reason string explains the widening. Remove the unused `export const Priority` from prisma/contract.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Rework for the CHANGES_REQUESTED review is pushed (5 commits,
Parity guarantee unchanged: PSL emit == TS emit incl. |
…885) The PR #805 review surfaced that the emitter omits the domain enum block from contract.d.ts, so emitted-path db.enums members type-widen to JsonValue. R6 now requires literal typing through the emitted contract; TML-2885 tracks the work and becomes a cutover prerequisite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…member defaults
Carrier (spec §2): the entity coordinate is carried, never derived — plane +
entityKind ('enum' | 'valueSet', equal to the entries slot key) + namespaceId +
entityName + optional spaceId. One kind vocabulary, no translation (ADR 224).
Directional invariant corrected project-wide: domain may reference storage;
storage may never reference domain (storage plannable in isolation). ADR 221
§115's parenthetical is transposed — erratum deferred to the ADR batch. The
enumMember ColumnDefault carrier violates the invariant; TML-2855 respecced to
resolved-literal-in-storage + domain-side member intent (spec §9, alternatives,
design-notes, plan).
D5 proposal settled with planner deltas, incl. StorageValueSet node tag rename
'value-set' -> 'valueSet' folded into D5 scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
- Rename `name` → `entityName` and `entityKind: 'value-set'` → `'valueSet'`
in `ValueSetRef` to achieve field-name identity with `EntityCoordinate`
(one-vocabulary rule, ADR 224)
- Rename `StorageValueSet.kind` literal `'value-set'` → `'valueSet'` and
update `StorageValueSetInput` accordingly
- Rewrite `ValueSetRef` doc comment: one-vocabulary rule, intra-plane
constraint, correct directional invariant (domain→storage allowed;
storage→domain forbidden)
- Add `ValueSetRefSchema` plane-consistency assertion: plane must match
entityKind ('enum'→'domain', 'valueSet'→'storage')
- Mechanical follow-through across all construction and read sites:
build-contract.ts, validators.ts, contract-to-schema-ir.ts,
sql-renderer.ts, postgres-schema.ts, generate-contract-dts.ts
- Update all test files (~40 occurrences) and add new tests:
(a) kind equals entries slot key; (b) plane-consistency passing + violating
- Re-emit demo contract.json/contract.d.ts + migration end-contract.*
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
|
D5 pushed (
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/2-sql/1-core/contract/test/validators.test.ts (1)
1304-1326: ⚡ Quick winAdd a regression for storage checks pointing to domain enums.
Line 1304-Line 1326 currently covers only
entityKind: 'valueSet'plane matching/mismatching. Please add a case asserting a storage check ref with{ plane: 'domain', entityKind: 'enum' }is rejected, so the storage→domain invariant stays locked.Suggested test case
it('rejects a storage ref where plane contradicts entityKind', () => { const result = storageSchema( makeStorageWithCheckRef({ plane: 'domain', entityKind: 'valueSet', namespaceId: 'public', entityName: 'Role', }), ); expect(result).toBeInstanceOf(type.errors); }); + + it('rejects a storage ref pointing to a domain enum', () => { + const result = storageSchema( + makeStorageWithCheckRef({ + plane: 'domain', + entityKind: 'enum', + namespaceId: 'public', + entityName: 'Role', + }), + ); + expect(result).toBeInstanceOf(type.errors); + });🤖 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/1-core/contract/test/validators.test.ts` around lines 1304 - 1326, Add a regression test that ensures storage checks cannot point to domain enums: create a new it-block alongside the existing tests that calls storageSchema(makeStorageWithCheckRef({ plane: 'domain', entityKind: 'enum', namespaceId: 'public', entityName: 'Role' })) and assert the result is an instance of type.errors; follow the pattern used in the 'rejects a storage ref where plane contradicts entityKind' test to keep style consistent and ensure the storage→domain invariant is enforced for enums.
🤖 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/1-framework/2-authoring/psl-parser/src/parser.ts`:
- Around line 1662-1685: The parser currently accepts bare identifiers into
parameters when descriptor.allowAdditionalParameters is true, which lets callers
bypass declared parameter validation; before assigning a bare entry (the block
that handles bareMatch and sets parameters[key] = { kind: 'bare', ... }), check
whether key is declared in descriptor.parameters and if so push a diagnostic
(e.g. reuse PSL_INVALID_EXTENSION_BLOCK_MEMBER or a new code) indicating
declared parameters must be provided as "key = value" and skip adding the bare
entry; ensure you still allow bare entries only for truly additional parameters
and do not overwrite or mask descriptor.parameters validation.
In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 80-94: ValueSetRefSchema currently only enforces internal
consistency (plane matches entityKind) which allows domain enums to be passed
where storage-only refs are required; create a specialized schema (e.g.
StorageValueSetRefSchema) that narrows ValueSetRefSchema to plane === 'storage'
and entityKind === 'valueSet', and replace occurrences that represent
storage-carriers (the usages around the current checks at the sites noted — the
refs validated on lines ~105 and ~221) to validate against
StorageValueSetRefSchema instead of ValueSetRefSchema so storage columns/checks
cannot reference domain enums.
---
Nitpick comments:
In `@packages/2-sql/1-core/contract/test/validators.test.ts`:
- Around line 1304-1326: Add a regression test that ensures storage checks
cannot point to domain enums: create a new it-block alongside the existing tests
that calls storageSchema(makeStorageWithCheckRef({ plane: 'domain', entityKind:
'enum', namespaceId: 'public', entityName: 'Role' })) and assert the result is
an instance of type.errors; follow the pattern used in the 'rejects a storage
ref where plane contradicts entityKind' test to keep style consistent and ensure
the storage→domain invariant is enforced for enums.
🪄 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: e9299003-6fa1-45a5-82ce-7529e4946e4b
⛔ Files ignored due to path filters (5)
projects/enums-as-domain-concept/design-notes.mdis excluded by!projects/**projects/enums-as-domain-concept/plan.mdis excluded by!projects/**projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/d5-carrier-alignment-proposal.mdis excluded by!projects/**projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/spec.mdis excluded by!projects/**projects/enums-as-domain-concept/spec.mdis excluded by!projects/**
📒 Files selected for processing (45)
examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.tsexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.jsonexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.jsonexamples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.tsexamples/prisma-next-demo/scripts/seed.tsexamples/prisma-next-demo/src/main.tsexamples/prisma-next-demo/src/prisma/contract.d.tsexamples/prisma-next-demo/src/prisma/contract.jsonexamples/prisma-next-demo/src/queries/get-posts-by-priority.tsexamples/prisma-next-demo/test/repositories.integration.test.tsexamples/prisma-next-demo/test/sql-dsl.integration.test.tspackages/1-framework/0-foundation/contract/src/value-set-ref.tspackages/1-framework/1-core/framework-components/src/control/psl-ast.tspackages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.tspackages/1-framework/1-core/framework-components/src/shared/framework-authoring.tspackages/1-framework/1-core/framework-components/src/shared/psl-extension-block.tspackages/1-framework/1-core/framework-components/test/control-stack.test.tspackages/1-framework/1-core/framework-components/test/psl-ast.test.tspackages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.tspackages/1-framework/2-authoring/psl-parser/src/exports/index.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.tspackages/1-framework/3-tooling/emitter/src/generate-contract-dts.tspackages/1-framework/3-tooling/emitter/test/domain-type-generation.test.tspackages/1-framework/3-tooling/emitter/test/emitter.integration.test.tspackages/2-sql/1-core/contract/src/ir/storage-value-set.tspackages/2-sql/1-core/contract/src/validators.tspackages/2-sql/1-core/contract/test/check-constraint.test.tspackages/2-sql/1-core/contract/test/storage-value-set.test.tspackages/2-sql/1-core/contract/test/validators.test.tspackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/src/provider.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.tspackages/2-sql/2-authoring/contract-ts/src/build-contract.tspackages/2-sql/2-authoring/contract-ts/test/check-constraint.authoring.test.tspackages/2-sql/2-authoring/contract-ts/test/enum-type.authoring.test.tspackages/2-sql/9-family/src/core/authoring-entity-types.tspackages/2-sql/9-family/src/core/control-descriptor.tspackages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.tspackages/2-sql/9-family/src/exports/pack.tspackages/2-sql/9-family/test/schema-verify.check-constraints.test.tspackages/2-sql/9-family/test/value-set-roundtrip.test.tspackages/3-targets/3-targets/postgres/src/core/postgres-schema.tspackages/3-targets/3-targets/postgres/test/migrations/planner.check-constraints.test.tspackages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
✅ Files skipped from review due to trivial changes (5)
- packages/1-framework/1-core/framework-components/test/psl-ast.test.ts
- examples/prisma-next-demo/src/prisma/contract.json
- examples/prisma-next-demo/src/prisma/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/end-contract.json
🚧 Files skipped from review as they are similar to previous changes (5)
- examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.json
- examples/prisma-next-demo/test/repositories.integration.test.ts
- packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
- examples/prisma-next-demo/test/sql-dsl.integration.test.ts
- examples/prisma-next-demo/scripts/seed.ts
|
Heads-up from the stacked TML-2855 work (#808): this branch's |
aad4e15 made blockAttributes required on PslExtensionBlock; this hand-built fixture predates it. Surfaced as a typecheck failure once fresh dist propagated (earlier full-typecheck greens were stale-dist). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Re D5 — one correction before merge: the ref-plane check is at the wrong layer D5 verification came back clean on the branch itself: the carrier shape, the The problem. The new check in const expectedPlane = ref.entityKind === 'enum' ? 'domain' : 'storage';Two things wrong with it:
The fix (small, two spots):
Out of scope for #805, noted for completeness: whether a ref actually resolves ( Cosmetic, your call: the new carrier doc comment says "entries slot key" — the settled vocabulary is entity kind / entries key; "slot" is retired. And the branch is currently CONFLICTING with |
…tional-psl-enum2-block-author-the-new-enum Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # examples/prisma-next-demo/test/repositories.integration.test.ts # examples/prisma-next-demo/test/sql-dsl.integration.test.ts
There was a problem hiding this comment.
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 `@examples/prisma-next-demo/test/sql-dsl.integration.test.ts`:
- Line 15: The import path uses a partial file extension; update the type import
for Contract to remove the `.d` suffix so TypeScript doesn't include file
extensions — change the import statement referencing '../src/prisma/contract.d'
to import from '../src/prisma/contract' (keeping the `import type { Contract }`
form) so the declaration file `contract.d.ts` is referenced without an explicit
extension.
🪄 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: fe4ff605-da9e-4f9a-b60d-207e0b85e0fb
📒 Files selected for processing (4)
examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.tsexamples/prisma-next-demo/src/prisma/contract.d.tsexamples/prisma-next-demo/test/repositories.integration.test.tsexamples/prisma-next-demo/test/sql-dsl.integration.test.ts
✅ Files skipped from review due to trivial changes (2)
- examples/prisma-next-demo/src/prisma/contract.d.ts
- examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- examples/prisma-next-demo/test/repositories.integration.test.ts
…ces interpreterLowered; §5 typing split to TML-2886 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…al drop) Migration JSON files dropped the empty `"params": []` from execute steps in line with TML-2867's canonicalization. These were left uncommitted from the prior merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…ore canonical migration ops Two fixes: 1. contract.d.ts and the migration end-contract.d.ts were emitted with the TML-2885 emitter (from two branches up the stack) and contained a domain.namespaces.public.enum block that this branch's emitter does not produce. Re-emit with pnpm emit:psl rebuilds them cleanly. 2. The migration ops.json files were in a state without "params": [] (matching a future branch's canonical form) but this branch's migration system still emits empty params arrays. regen-example-migrations.mjs restores them to the correct state and updates migration hashes accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
The ref is a coordinate: both planes are representable. Whether a given
plane/entityKind pairing is valid depends on where the ref is carried, not
on the ref itself.
- StorageColumnSchema and CheckConstraintSchema use StorageValueSetRefSchema
(plane:'storage', entityKind:'valueSet') — the only valid shape at these sites.
- ModelFieldSchema uses DomainEnumRefSchema (plane:'domain', entityKind:'enum').
- ValueSetRefSchema loses the cross-field narrow() — it remains only as the
general coordinate type (currently unused internally; callers use the
call-site-narrowed schemas directly).
Tests: rework the existing plane-consistency tests to the call-site form,
add CodeRabbit's regression (a storage check ref with {plane:'domain',
entityKind:'enum'} rejects), add a domain field ref acceptance test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…re-declared-key bypass; consolidate parseExtensionBlockAttribute - Renames `allowAdditionalParameters` → `variadicParameters` across descriptor interface, validator, parser, and all call sites. - Updates the doc comment to describe the general block-body model, including the invariant that a declared key used bare is a diagnostic. - Bare occurrence of a key declared in `descriptor.parameters` now emits PSL_INVALID_EXTENSION_BLOCK_MEMBER regardless of variadicParameters. - Replaces the ad-hoc `parseExtensionBlockAttribute` implementation with a consolidated version built on `extractAttributeTokensWithSpans` + `parseAttributeToken`. - Adds parser test for bare-declared-key diagnostic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…; Task 5 smalls
- Deletes `interpreterLowered` from `AuthoringPslBlockDescriptor` and
`assertPslBlocksHaveFactories`; every PSL block descriptor now requires
a matching `entityTypes` factory with the same discriminator.
- Adds optional `codecLookup`, `sourceId`, and `diagnostics` to
`AuthoringEntityContext` so factories can validate values without
needing a separate channel.
- Registers a real `enum2` entity type factory in the SQL family pack
(`sqlFamilyEnum2EntityDescriptor`); `processEnum2Declarations` in the
PSL interpreter shrinks to factory dispatch + handle collection.
- Renames internal "leaf" vocabulary in `framework-authoring.ts`:
`AuthoringLeafEntry` → `DescriptorEntry`, `collectAuthoringLeafPaths`
→ `collectDescriptorPaths`, `collectAuthoringLeafDiscriminators` →
`collectDescriptorEntries`, `PslBlockLeafEntry` → (removed, merged
into `DescriptorEntry`), `collectPslBlockLeafEntries` →
`collectPslBlockDescriptorEntries`.
- Adds test for `PSL_ENUM2_MISSING_FACTORY` when no factory is registered.
- Uses `ifDefined('enumTypeHandle', enumHandle)` at the model field
build site.
- Fixes `contract.d` → `contract` import in sql-dsl integration test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
|
Fix round for the 2026-06-11 review is pushed (
Spec §5 typing-mechanism divergence from the review discussion is tracked as TML-2886 (post-stack). All threads have replies; gates green (framework-components 397 / psl-parser 244 / contract-psl 242 / family-sql 369 / sql-contract 188; fixtures:check byte-identical; lint:deps clean). |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/2-sql/1-core/contract/src/validators.ts (1)
80-94: 💤 Low valueConsider adding strict property checking for consistency.
Both
StorageValueSetRefSchemaandDomainEnumRefSchemalack'+': 'reject', while similar reference schemas in this file (ForeignKeyReferenceSchemaat line 188,ForeignKeySourceSchemaat line 196) include it. Adding strict checking would prevent silently accepting malformed refs with extra properties.♻️ Suggested change
const StorageValueSetRefSchema = type({ + '+': 'reject', plane: "'storage'", namespaceId: 'string', entityKind: "'valueSet'", entityName: 'string', 'spaceId?': 'string', }); const DomainEnumRefSchema = type({ + '+': 'reject', plane: "'domain'", namespaceId: 'string', entityKind: "'enum'", entityName: 'string', 'spaceId?': 'string', });🤖 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/1-core/contract/src/validators.ts` around lines 80 - 94, Add strict property checking to the two reference schemas by updating StorageValueSetRefSchema and DomainEnumRefSchema to include the same strictness marker used elsewhere (e.g., '+': 'reject'); locate the definitions for StorageValueSetRefSchema and DomainEnumRefSchema and add the '+': 'reject' property to each schema object so extra properties are rejected consistently with ForeignKeyReferenceSchema and ForeignKeySourceSchema.
🤖 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/2-sql/2-authoring/contract-psl/test/fixtures.ts`:
- Around line 180-188: The empty-enum branch currently reuses the diagnostic
code 'PSL_ENUM2_MISSING_TYPE' which collides with other checks; change the
diagnostic.code in the members.length === 0 branch to a distinct value (e.g.,
'PSL_ENUM2_EMPTY_MEMBERS' or 'PSL_ENUM2_MISSING_MEMBER') and update any
tests/expectations that assert on this diagnostic; locate the branch that pushes
into diagnostics (uses variables diagnostics, block, sourceId, span) and only
change the code string so other fields (message, span, sourceId) remain
unchanged.
In `@packages/2-sql/9-family/src/core/authoring-entity-types.ts`:
- Line 85: Replace the bare cast on the value passed to codec.decodeJson: locate
the call to codec.decodeJson(memberName as unknown as JsonValue) and either
remove the cast entirely if memberName (a string) is already assignable to
JsonValue, or replace the bare cast with a safe production cast using
blindCast<JsonValue, "MemberNameAsJson"> from `@prisma-next/utils/casts` (import
it if missing) so the call becomes codec.decodeJson(blindCast<JsonValue,
"MemberNameAsJson">(memberName)); ensure you modify the same usage in
authoring-entity-types (the memberName variable and codec.decodeJson
invocation).
---
Nitpick comments:
In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 80-94: Add strict property checking to the two reference schemas
by updating StorageValueSetRefSchema and DomainEnumRefSchema to include the same
strictness marker used elsewhere (e.g., '+': 'reject'); locate the definitions
for StorageValueSetRefSchema and DomainEnumRefSchema and add the '+': 'reject'
property to each schema object so extra properties are rejected consistently
with ForeignKeyReferenceSchema and ForeignKeySourceSchema.
🪄 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: 6fa420a9-9c94-48a2-a2b6-1ac4d0580560
⛔ Files ignored due to path filters (1)
projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/spec.mdis excluded by!projects/**
📒 Files selected for processing (17)
examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.tsexamples/prisma-next-demo/src/prisma/contract.d.tsexamples/prisma-next-demo/test/sql-dsl.integration.test.tspackages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.tspackages/1-framework/1-core/framework-components/src/exports/authoring.tspackages/1-framework/1-core/framework-components/src/shared/framework-authoring.tspackages/1-framework/1-core/framework-components/test/control-stack.test.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.tspackages/2-sql/1-core/contract/src/validators.tspackages/2-sql/1-core/contract/test/validators.test.tspackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/test/fixtures.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.tspackages/2-sql/9-family/src/core/authoring-entity-types.tspackages/2-sql/9-family/src/core/control-descriptor.tspackages/2-sql/9-family/src/exports/pack.ts
💤 Files with no reviewable changes (2)
- examples/prisma-next-demo/src/prisma/contract.d.ts
- examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
✅ Files skipped from review due to trivial changes (1)
- packages/1-framework/1-core/framework-components/src/exports/authoring.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.ts
- packages/2-sql/9-family/src/core/control-descriptor.ts
- packages/2-sql/9-family/src/exports/pack.ts
- examples/prisma-next-demo/test/sql-dsl.integration.test.ts
- packages/2-sql/1-core/contract/test/validators.test.ts
- packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
The self-edge guard copies the whole migrations tree; the suite default (timeouts.default, 100ms) no longer covers it as migrations accumulate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
processEnum2Declarations input admits undefined contributions (matching the interpreter's optional input); the entity-context diagnostics sink wraps the ContractSourceDiagnostic array; codecLookup via ifDefined (exactOptionalPropertyTypes); optional-chain lint fix in test fixture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…ate CLI member arg at runtime string is directly assignable to JsonValue, so the enum2 factory's bare-member decode needs no cast; the demo's enum-priority-filter validates the requested member against the emitted accessor's names instead of asserting a union. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…LT 'low' (prisma#808) ## Linked issue Refs [TML-2855](https://linear.app/prisma-company/issue/TML-2855/slice-enum-member-defaults-defaultadmin-renders-default-value). **Was stacked on prisma#805 (TML-2882)** — now merged; this PR targets `main` directly (rebased, four commits). With TML-2885, the last parity prerequisite before the cutover (TML-2853). ## At a glance An enum field declares its default by naming a **member**, on both authoring surfaces, and it lands as an ordinary literal: ```prisma enum2 Priority { @@type("pg/text@1") Low = "low" High = "high" Urgent = "urgent" } model Post { priority Priority @default(Low) // names a member — not the raw string "low" } ``` ```ts field.namedType(Priority).default(Priority.members.Low) // members only — .default('lwo') is a compile error ``` ```sql priority text NOT NULL DEFAULT 'low' ``` Before this PR, `.default()` on an enum-typed field accepted any literal, and PSL `@default` had no member awareness. ## Decision Member-to-literal resolution happens **at lowering, per authoring surface** — there is **no new `ColumnDefault` variant** and no machinery below the lowering. The storage column carries the resolved literal (`{ "kind": "literal", "value": "low" }`), so the storage plane stays plannable in isolation per the directional invariant settled on prisma#805 (domain may reference storage; storage may never reference domain). The `enumMember` variant the original ticket described predates that correction and was never shipped; this PR supersedes it (see the ticket's 2026-06-10 respec comment and project spec §9). ## Reviewer notes - **The negative type-tests are the heart of the TS surface:** `EnumScalarFieldBuilder.default()` narrows to the handle's member value union (`Handle['values'][number]`); the package typecheck passes with the `@ts-expect-error` directives present, so each rejected literal genuinely errors. `defaultSql` is a type error (and runtime throw) on enum builders. - **A latent stale artifact from the base branch is re-emitted here:** commit `fe9ce0f53` on the base branch canonicalized empty `typeParams` out of emitted contracts, but the demo's copied pgvector space was never re-emitted and still carried the old shape + hash. Running the migration tooling surfaced it; this PR re-emits it (byte-identical to the pgvector package's own contract). Tree-wide there are zero `"typeParams":{}` survivors now. - **Known follow-up (not exercised by any current usage):** the member-only `.default()` narrowing is lost if `.nullable()`/`.id()` is chained before it (base builder methods return the base type). Recorded in the review ledger; closes when nullable-with-default enum fields are needed. - The spec's "At a glance" shows `"default": "low"` as shorthand; the persisted shape is the existing object form (`{"kind":"literal","value":"low"}`), identical to every other literal default on the wire (e.g. `task.status`). ## How it fits together 1. **TS DSL** ([contract-dsl.ts](packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts)): the `EnumTypeHandle` overload of `namedTypeField` returns an `EnumScalarFieldBuilder` whose `.default()` accepts only the member value union and lowers to the existing literal default. 2. **PSL** ([psl-field-resolution.ts](packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts)): when a field's type resolves to an `enum2`, `@default(<identifier>)` resolves the member name to its value (literal default); non-member identifiers, quoted raw values, and function defaults are span-accurate diagnostics. Non-enum and native-enum `@default` lowering is untouched. 3. **Nothing below the lowering changes** — validator, planner, `buildColumnDefaultSql`, verification all see an ordinary literal default. 4. **Demo** ([examples/prisma-next-demo](examples/prisma-next-demo)): `@default(Low)` in the PSL schema (mirrored in the TS no-emit contract), re-emitted artifacts, migration `20260610T2216_set_priority_default` (a single `setDefault` op), and an integration test inserting a post **without** `priority` and reading back `'low'` — including the type-level proof that the insert payload's `priority` became optional. ## Behavior changes & evidence - **TS `.default()` is members-only on enum fields** — [contract-dsl.ts](packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts); evidence: [enum-type.member-defaults.test.ts](packages/2-sql/2-authoring/contract-ts/test/enum-type.member-defaults.test.ts) (string + int codecs, positive/negative type tests, lowering shape). - **PSL `@default(member)` resolves on enum2 fields, with three rejection diagnostics** — [psl-field-resolution.ts](packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts); evidence: [interpreter.enum2.test.ts](packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts), including the strict PSL/TS parity test (deep-equal + `storageHash`) extended with a defaulted field. - **The demo proves the default end-to-end** — schema, migration, and the omit-`priority` insert reading back `'low'`; evidence: [enum-surface.integration.test.ts](examples/prisma-next-demo/test/enum-surface.integration.test.ts). ## Testing performed - `pnpm test:packages` — 800 files / 10,352 tests green on final HEAD - Full `pnpm typecheck` green for all packages this diff can reach (recurring stale-dist environment artifacts in unrelated packages adjudicated in review, same shape as on the base branch) - `pnpm fixtures:check` — clean outside the demo's deliberate changes - `pnpm lint:deps` — clean ## Skill update n/a — the `@default(member)` surface rides the transitional `enum2` keyword, which is retired at the cutover; user-facing docs land with the cutover rename (same policy as prisma#805). ## Follow-ups - Builder-chaining: preserve member-only `.default()` narrowing through `.nullable()`/`.id()` chains when nullable-with-default enum fields land. - [TML-2885](https://linear.app/prisma-company/issue/TML-2885) — emit-typed `db.enums` (the other cutover prerequisite). ## Alternatives considered - **An `enumMember` `ColumnDefault` variant recording the member in storage** (the original ticket design). Rejected by the 2026-06-10 directional-invariant settlement: it is a storage → domain reference, and storage must be plannable in isolation. The resolved literal carries everything DDL needs; the authored source names the member, so intent is recoverable by re-emit. - **Default by raw value (`@default("low")`)**. Rejected (per the ticket): severs the domain link, can't be membership-checked at authoring time; it is an explicit diagnostic now. - **Recording member intent on the domain field in this slice.** Deferred — additive later if introspection/diffing wants it; no observable behavior needs it today. ## Checklist - [x] All commits are signed off (`git commit -s`) per the [DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO status check will block merge if any commit is missing a `Signed-off-by:` trailer. - [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is scoped to one logical concern. - [x] Tests are updated (or `n/a` if the change is doc-only / refactor with no behavioural delta). - [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear ticket prefix + concise title naming the concrete deliverable). See `.claude/skills/create-pr/SKILL.md` for the full convention. - [x] The **Skill update** section above is filled in (or stated `n/a — internal only`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enum field defaults: You can now specify default values for enum-typed fields using `@default(EnumMember)` syntax. This allows enum fields to be optional on insert operations, with the database automatically applying the specified default value when omitted. * **Chores** * Added upgrade guidance and migration path for enum field defaults support in v0.14. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…gh emit (prisma#809) ## Linked issue Refs [TML-2885](https://linear.app/prisma-company/issue/TML-2885). **Was stacked on prisma#808 → prisma#805** — both now merged; this PR targets `main` directly (rebased, four commits). With TML-2855, completes the cutover's (TML-2853) parity prerequisites. ## At a glance The emitted `contract.d.ts` now types the domain `enum` block, so `db.enums` is literal-typed **through the emitted contract** — closing the R6 emit-parity gap found in prisma#805's review: ```ts // before (emitted path): runtime correct, types collapsed db.enums.public.Priority.values // JsonValue[] db.enums.public.Priority.members.High // JsonValue // after db.enums.public.Priority.values // readonly ['low', 'high', 'urgent'] db.enums.public.Priority.members.High // 'high' ``` ```ts // emitted contract.d.ts — the public namespace gains: readonly enum: { readonly Priority: { readonly codecId: 'pg/text@1'; readonly members: readonly [ { readonly name: 'Low'; readonly value: 'low' }, { readonly name: 'High'; readonly value: 'high' }, { readonly name: 'Urgent'; readonly value: 'urgent' } ]; }; }; ``` ## Decision One change site: the emitter renders the per-namespace domain `enum` block with literal member tuples (order preserved — it's semantic). The **existing** accessor type chain (`NamespaceEnumAccessors` / `NamespacedEnums` in `enum-accessor.ts`) already extracts literal `values`/`members` from exactly this shape, so it is untouched — as are the runtime and the TML-2852 field narrowing. `contract.json` and every hash are byte-identical: this is a types-only emission change. The acceptance evidence is deletion: the demo's `getPriorityEnumFromEmit()` workaround and its `blindCast`s — whose reason strings documented exactly this gap — are gone; every consumer reads `db.enums.public.Priority` directly, cast-free (net −4 `blindCast` sites). ## Reviewer notes - **Non-vacuity is compile-level:** without the emitted enum block, `db.enums.public.Priority` does not exist at the type level, so the demo type tests fail to compile (verified by deleting the block and watching six files fail; restored cleanly). Stronger than a runtime assert. - **One genuinely new `blindCast`** in [generate-contract-dts.ts](packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts) (`ns.enum`), byte-identical in idiom to the two pre-existing sibling casts (`ns.models`, `ns.valueObjects`) it sits beside. - **A real stack regression was caught and fixed during this slice:** TML-2882's `aad4e15b3` made `blockAttributes` required on `PslExtensionBlock`; a cli test fixture predated it. Earlier "full typecheck green" runs were stale-dist false greens. Fixed on the base branch (`53680da2e` on prisma#805) and the stack rebased. - The JSON→TS literal rendering reuses the TML-2852 D4 helpers (`serializeValue`/`serializeObjectKey`) — no duplication. ## Behavior changes & evidence - **The emitter renders the domain enum block** — [generate-contract-dts.ts](packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts); evidence: [emitter.integration.test.ts](packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts) (string codec, int-codec bare-number literals, quoted non-identifier member keys, enum-less namespaces emit no block; unbound-namespace coverage folded in). - **`db.enums` is literal-typed through emit, cast-free in the demo** — [get-posts-by-priority.ts](examples/prisma-next-demo/src/queries/get-posts-by-priority.ts) (workaround deleted), [demo-dx.types.test.ts](examples/prisma-next-demo/test/demo-dx.types.test.ts) (literal `members.High` / `values` through the emitted artifacts). - **Types-only:** no `.json` file changes; four `.d.ts` files regenerate with only the additive enum block. ## Testing performed - `pnpm test:packages` — green on final HEAD (one adapter-postgres DB-infra timeout, diff-unreachable) - `pnpm typecheck` — green incl. `@prisma-next/cli` against fresh dist (see reviewer note on the fixed stack regression) - `pnpm fixtures:check` — clean (full re-emit against fresh dist, no drift) - `pnpm lint:deps` — clean ## Skill update n/a — types-only emission change; the user-facing `db.enums` surface was documented with TML-2852, and the transitional-keyword docs policy (skill lands at cutover) carries the rest. ## Follow-ups - TML-2853 (cutover) — parity prerequisites now complete once this stack merges. ## Alternatives considered - **Narrowing at the accessor instead of emitting the block** (parameterize `EnumAccessor` per call site). Rejected: the accessor chain was already built to extract from the typed contract shape; emitting the shape fixes every consumer at once and keeps "the `.d.ts` is the contract" honest. - **Emitting only the value tuples (not full member pairs).** Rejected: `members.<Name>` needs the name→value literal pairs; the full tuple is what `EnumEntryMembers` consumes, and it matches the runtime JSON one-for-one. ## Checklist - [x] All commits are signed off (`git commit -s`) per the [DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO status check will block merge if any commit is missing a `Signed-off-by:` trailer. - [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is scoped to one logical concern. - [x] Tests are updated (or `n/a` if the change is doc-only / refactor with no behavioural delta). - [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear ticket prefix + concise title naming the concrete deliverable). See `.claude/skills/create-pr/SKILL.md` for the full convention. - [x] The **Skill update** section above is filled in (or stated `n/a — internal only`). --------- Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ers (ADR 224) The enum2 test added by PR #805 used closed-shape `.entries.table?.` and `.entries.valueSet?.` dot access, which fails TS4111 after D1 widened `SqlNamespace.entries` to an index-signature record. Migrate to `namespaceTables()` / `namespaceValueSets()` helpers, matching the canonical style established in D1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io>
…(ADR 224/225) (prisma#812) **Decision:** a storage namespace stores its entities in one open dictionary — keyed by entity kind, then by entity name — and that path is the *only* way the framework addresses, validates, and hydrates them. This PR makes the runtime types and the validation/hydration machinery match that model (ADR 224/225). Nothing persisted changes: the wire format was already this shape, byte for byte. Here is a namespace exactly as it sits in a committed `contract.json` today (unchanged by this PR): ```jsonc // storage.namespaces.public { "id": "public", "entries": { "table": { "user": { /* StorageTable */ }, "post": { /* … */ } }, "type": { "user_type": { "kind": "postgres-enum", "values": ["admin", "user"] } }, "valueSet": { "Priority": { "kind": "valueSet", "values": ["low", "high"] } } } } ``` Every entity has a coordinate — `(plane, namespaceId, entityKind, entityName)` — and one expression resolves any coordinate, for any entity kind, including kinds a target or extension pack contributes that the framework has never heard of: ```ts entityAt(storage, { namespaceId, entityKind, entityName }) // ≡ storage.namespaces[namespaceId].entries[entityKind][entityName] ``` ## The problem The persisted JSON was already open, but the runtime types were not. Each namespace class declared a closed object with one named field per kind it knew about: ```ts // before — PostgresSchema readonly entries: { readonly table: Readonly<Record<string, StorageTable>>; readonly type: Readonly<Record<string, PostgresEnumType>>; readonly valueSet?: Readonly<Record<string, StorageValueSet>>; }; ``` A closed type forces every consumer to know every kind. The validator hardcoded `table?`/`type?`/`valueSet?` schema fields; the postgres serializer hydrated the `type` map through a lookup keyed by the node tag `'postgres-enum'`; generic code that received a coordinate had to switch on the kind to pick a property. Adding a new entity kind (the RLS branch needs policies and roles) meant editing framework and family code — or smuggling the kind past the closed types, which is what that branch currently does. ## The change **The entity kind is the `entries` key** — `table`, `type`, `valueSet`, `collection` — exactly the strings already persisted. Everything dispatches on that one string. **Open types that keep typed property access.** `entries` is typed as the open dictionary *intersected* with its known kinds as optional keys: ```ts // after — the SQL family shape (Postgres adds `type?`, Mongo has `collection?`) readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>> & { readonly table?: Readonly<Record<string, StorageTable>>; readonly valueSet?: Readonly<Record<string, StorageValueSet>>; }; ``` Unknown kinds remain valid (walkers iterate `Object.entries` as before), but `ns.entries.table` is ordinary typed property access — no helper layer, no per-call-site casts. Class instances additionally expose non-enumerable per-kind getters (`schema.type`, `db.collection`), so `JSON.stringify` emits only `id` + `entries`. Repo-wide there are exactly two read styles: generic walkers use `entries[kind][name]`; typed code uses property access or the getters. ~50 production and ~190 test call sites moved. **Construction is permissive-carry; the JSON boundary fails closed.** This split is the heart of the open model, so be explicit about where each rule applies: - **In-memory construction (builders/constructors) is permissive.** A builder constructs the kinds it owns a factory for (`table`, `collection`, …) into IR instances and **freezes-and-carries any other kind's map untouched**. It does *not* enforce a closed kind list. This is required by the open model: a pack-contributed kind (an RLS policy, a Postgres role) must pass through `buildSqlNamespace` / `buildMongoNamespace` without the family layer knowing its name — if construction threw on unknown kinds, pack kinds would be un-authorable, which is the closed-list behavior this PR exists to remove. The carry tests that assert a synthetic unknown kind survives construction (frozen, present in `JSON.stringify`, yielded by `elementCoordinates`) are pinning this promise on purpose. - **The JSON boundary (validation + hydration) fails closed.** When a contract is *deserialized*, the kind→schema registry validates every kind and the hydration dispatch constructs every kind; an unregistered kind is rejected with an error naming the kind and the namespace id. This is where corruption and typos are caught — at the point a contract enters from disk, not at every in-memory construction. The accepted trade-off: a typo'd kind in hand-authored TypeScript surfaces at emit-time boundary validation rather than at the `build*` call. Construction also accepts *input literals only* — the `Instance | Input` unions and their `v instanceof X ? v : new X(v)` normalization are gone (hydration constructs from validated JSON; authoring passes literals; the one caller that fed constructed instances was migrated). **Validation dispatches through one kind→schema registry.** This *is* the fail-closed boundary above. Validation walks `Object.entries(entries)` and validates each inner map against the schema registered for that kind. The SQL core registers `table` and `valueSet` into the same registry the postgres pack registers `type` into — one tier, no privileged built-in fallback — and the postgres pack owns `PostgresEnumTypeSchema` outright, so the family layer contains zero target-kind knowledge. A kind nobody registered fails validation with an error naming the kind and namespace id. Hydration is also kind-dispatched and fail-closed (the `hydrateEntriesKind` hook, which a target overrides per kind), and namespace **construction** carries unknown kinds and builds the kinds it owns — but both of those still use per-kind code (a `hydrateEntriesKind` hook and an `if (kind === …)` construction switch) rather than the validation registry. **Unifying construction and hydration onto one shared kind→factory registry is the explicit follow-up [TML-2890](https://linear.app/prisma-company/issue/TML-2890); this PR delivers the registry on the validation path only.** **One canonical lookup.** `entityAt(storage, coordinate)` lives beside `elementCoordinates` in the framework; the hand-rolled two-step lookups (emitter FK validation and siblings) are gone. **Docs follow the code**, and one ADR bug is fixed on the way: ADR 221 §115 stated the cross-plane reference invariant backwards. The correct direction — a domain entity may reference a storage entity, never the reverse, because the migration planner/runner must consume the storage plane in isolation — already misled one project design; the parenthetical now matches reality. ## One breaking change for downstream code The family-generic `SqlContractSerializer` no longer knows the postgres `type` kind (per ADR 225, the family carries no target knowledge), so it rejects postgres contracts with a clear error naming the kind. The fix is one line, and contract deserialization is now properly generic — no cast: ```ts // before const contract = new SqlContractSerializer(…).deserializeContract(json) as Contract; // after const contract = new PostgresContractSerializer(…).deserializeContract<Contract>(json); ``` SQLite and table-only contracts are unaffected. Upgrade instructions are recorded via the repo's upgrade-instructions process; the examples in this PR show the migration. The `as Contract` casts are swept from the demo, retail-store, and mongo-demo apps (three sites in `multi-extension-monorepo` are a recorded follow-up). ## What deliberately does not change - **Every persisted byte.** `storageHash` is content-addressed over the entries subtree, so the persisted shape — keys, key order, presence semantics — is preserved exactly. `pnpm fixtures:check` passes with zero committed-artifact diffs (and earned its keep: it caught the one hash-drift regression introduced mid-review and forced the correct fix). - **Node-body `kind` tags**, including the two that don't match their entries key (`'postgres-enum'` under `type`, `'mongo-collection'` under `collection`). They sit inside hash-covered node bodies, and nothing dispatches on them anymore — see Alternatives. - **`elementCoordinates`** — the framework's coordinate walker already read `entries` structurally; it is the consumer this refactor had to keep working unchanged, and it did. ## Verification All acceptance criteria carry committed tests: exact-shape serialization per concretion (getters absent from JSON), deep-freeze, unknown-kind rejection at construction/validation/hydration, round-trip unchanged, and a coordinate-resolution test asserting every `elementCoordinates` tuple resolves through `entries[entityKind][entityName]` for representative postgres, sqlite, and mongo contracts. Local gates: build, typecheck (138/138), lint (79/79), `lint:deps`, `fixtures:check` (clean tree, zero contract diffs); cast ratchet net negative vs merge-base. Integration suites run on this PR's CI. ## Known follow-ups (tracked, deliberately not in this PR) Review surfaced three refinements that are real but are each a distinct, higher-blast-radius change; bundling them into this already-large branch is what produced the one regression this PR hit, so they ship as focused PRs: - **[TML-2890](https://linear.app/prisma-company/issue/TML-2890) — Uniform kind dispatch in construction & hydration.** One shared kind→factory registry replacing the per-kind `if (kind === …)` switches in the 8 construction/hydration files, so dispatch is uniform everywhere, not just in validation. (RLS adds its `policy` construction as a registration rather than a switch branch once this lands — it is not blocked on it.) - **[TML-2891](https://linear.app/prisma-company/issue/TML-2891) — Eliminate the SQL family placeholder concretion.** Delete `SqlBoundNamespace`/`buildSqlNamespace`/`SqlNamespaceTablesInput`; SQL namespaces become always-target concretions; supabase emits `postgres-schema`. This is a wire-format change (committed contracts regenerate) and must land in isolation; the reverted item-G `instanceof` guard resolves here. - **[TML-2892](https://linear.app/prisma-company/issue/TML-2892) — Migration-author accessor.** A `Contract`/`Storage` accessor for collections/tables by name, so migrations stop reaching through `namespaces.__unbound__.entries.collection`. ## Out of scope (deliberate) RLS (its branch rebases onto this and deletes its workarounds); the migration planner; `ValueSetRef` (settled in prisma#805); the domain plane's flat `models`/`valueObjects` shape and the namespace-kind serializer strings (recorded follow-ups); the ADR 225/126 rewrite batch. ## Alternatives considered - **A helper-function layer instead of intersection typing.** An earlier round of this PR shipped `namespaceTables()`/`namespaceValueSets()`/`namespaceCollections()` to give typed reads over a fully-widened `entries`. Rejected in review and deleted: the helpers were a parallel per-kind API duplicating the getters, and they existed only because the type had been over-widened — the intersection keeps the dictionary open while making plain property access type-safe. - **Keep the `Instance | Input` construction unions.** Also shipped early, also deleted: the `instanceof`-normalization they force is unreadable and hides which paths actually carry what. One shape per path needs no normalization. - **Rename the legacy node tags to match their entries key** (`'postgres-enum'` → `'type'`, `'mongo-collection'` → `'collection'`) so the kind is literally one string everywhere. Rejected: the tags are persisted inside hash-covered node bodies, so the rename would churn every committed contract and `storageHash` — and the postgres-enum machinery is deleted by the enums cutover (TML-2853) regardless. New kinds (e.g. `valueSet`, post-prisma#805) use one string across coordinate, entries key, node tag, and registration from day one. - **Rename the entries keys to match the node tags.** Rejected: the keys are also persisted — strictly worse churn for the same goal. - **Keep the closed types and teach each consumer the kind list.** Rejected: that moves the kind→property mapping into every consumer and keeps pack-contributed kinds unreachable — the exact model ADR 224 rejects. - **Translate between kind and key at the serializer boundary, or carry a mapping field on contributions** (the `entrySlotName` approach the RLS branch had to invent). Rejected: both are the same translation table in disguise; ADR 224 explicitly rejects boundary reshaping, and the open dictionary makes the mapping unnecessary. **Linear:** TML-2887 --------- Signed-off-by: Will Madden <madden@prisma.io> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Co-authored-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Linked issue
Refs TML-2882.
Builds on the merged enum substrate (TML-2850, #750), check enforcement (TML-2851, #755), and typed read surface (TML-2852, #769).
Hands to TML-2855 (member defaults) and TML-2853 (cutover, now reduced to rename + migrate + delete).
At a glance
The demo's PSL schema now authors the new domain-concept enum directly — while native
enumkeeps working untouched:enum2 Priority { @@type("pg/text@1") Low = "low" High = "high" Urgent = "urgent" } model Post { priority Priority }Emit it and the real app uses it end-to-end through the emitted contract —
post.priorityreads as'low' | 'high' | 'urgent',db.enums.public.Priority.valuesreturns the ordered tuple,ORDER BY prioritysorts by declaration order, and the migration adds theCHECK:Before this PR, the new enum mechanism was reachable only through the TS
enumTypeDSL — unreachable from PSL, and therefore from the demo's real emitted-contract app.Decision
Add a parallel transitional PSL keyword (
enum2) that lowers to the new enum mechanism — domainenumentity + storagevalueSetentity + field/columnvalueSetrestriction refs + check constraint — reusing the lowering the TSenumTypepath already has. Nativeenumis byte-for-byte untouched.The point is to dissolve the constraint that forced an atomic cutover:
enumis one keyword and can't mean both native and the new shape at once, so the new mechanism was unreachable through PSL until the very end of the project. The second keyword makes it PSL-authorable additively, takes it live through the product path (PSL → emit → app) now, and shrinks the cutover (TML-2853) to a rename + migrate + delete with the risky lowering already proven in live use.The keyword is deliberately disposable: it is retired at the cutover, when
enumitself repoints at this lowering.Reviewer notes
enum2path and the TSenumTypepath emit identical contract JSON, includingstorageHash. The parity test (interpreter.enum2.test.ts) asserts strict deep equality on every emitted slice plus hash equality — it started life astoMatchObjectand was hardened totoEqualafter subset matching let a divergence escape (next bullet).ead068f57patched the SQL-family schema verifier and codec-ref resolver to tolerate a straytypeRefthe PSL path was leaking onto emitted columns; review caught that the leak was the bug, so3ec205e9efixed it at the source and reverted both patches byte-identically. (2) The first round built a dedicatedparseEnum2Blockgrammar production and a per-targetentityTypes.enum2presence stub; review (correctly) rejected both, andaad4e15b3–99fc662dcrework enum2 onto the generic extension-block grammar and delete the stub mechanism entirely.interpretPslDocumentToSqlContractrebuilt domain namespace slices copying onlymodels/valueObjects, silently droppingenumentities. Nothing PSL-authored produced domain enums until now, so it was unobservable before this PR.enum2blocks. Verified no in-scope flow round-trips authored PSL through the printer (it runs only in the schema-inference direction), so printing support is deferred to the cutover, where the keyword is renamed anyway.contract.d.tsnarrows enum column/field types to the value union, but does not type the domainenumblock itself — so on the emitted path,db.enums.<ns>.<Name>.membersvalues areJsonValue-typed at the type level (runtime values are correct). The demo's literal-union type proofs therefore live on the column types (SELECT/INSERT), which the emitter does narrow. Project spec R6 amended to require emit parity; TML-2885 is now a cutover prerequisite.pnpm lintshows two pre-existingno-bare-castinfo notices in files this branch never touches; no new casts are added.How it fits together
enum2 Priority { … }rides the existing generic extension-block grammar (<keyword> <Name> { key = value … }); members are ordinary parameter lines captured raw. The parser gains three generic extension-block capabilities (each independently tested, usable by any contributed block):@@attr(...)block-attribute lines, duplicate-parameter diagnostics, and bare-identifier parameters behind a descriptor opt-in flag.processEnum2Declarationsconsumes theenum2-keyword extension blocks — validates@@typepresence (required, never inferred), validates each member's raw value withJSON.parse+codec.decodeJson(bare members default to their name for string-accepting codecs), and enforces well-formedness. Per enum it builds anEnumTypeHandlevia the existingenumType()and aColumnDescriptorin the existing by-name map — so field resolution (priority Priority) is unchanged and doesn't care which block declared the name. Support requires no per-target registration: a target that lacks the codec fails with the unknown-codec diagnostic; the block descriptor lives at the SQL-family level, markedinterpreterLowered(the factory invariant now admits blocks lowered by the interpreter rather than an entity-type factory).buildSqlContractFromDefinitionasenums, and the domain enum, storage value-set, field/columnvalueSetrefs, and the table check all fall out of the already-merged build-contract code. Zero new lowering.enum2 Priorityin the PSL schema, re-emittedcontract.json/contract.d.ts, migration20260610T0000_add_priority_enum(addColumn → setNotNull → addCheckConstraint), andenum-priority/enum-priority-filtersubcommands consuming the enum through the emitted contract — including filtering by a live member value via the emitted accessor; seeds and integration fixtures source values from the emitted accessor too, never raw literals.Behavior changes & evidence
@@attr(...)block attributes, duplicate-parameter diagnostics, and opt-in bare-identifier parameters — andenum2blocks parse through it with zero block-specific grammar. Implementation: parser.ts, psl-extension-block.ts. Evidence: parser-enum2.test.ts.enum2to the new two-plane shape, identically to the TS path — plus ten validation diagnostics (missing@@type, unknown codec, non-JSON / codec-rejected values, bare member under non-string codec, duplicate names/values, native-name collision, namespacedenum2, unsupported target). Implementation: interpreter.ts. Evidence: interpreter.enum2.test.ts — parity incl.storageHashequality, all diagnostics, native+enum2 mixed document, non-string codec.db.enums.public.Priority, declaration-orderORDER BY, and the check-adding migration. Implementation: contract.prisma, get-posts-by-priority.ts, main.ts. Evidence: demo-dx.types.test.ts (non-vacuous: paired.not.toEqualTypeOf<string>()+@ts-expect-erroron an out-of-union insert), integration suites.user_typeemits byte-identically; no native fixture changes; the mixed-document test asserts the native slice is unchanged.Testing performed
pnpm test:packages— 799 files, 10,328 tests green on final HEADpnpm typecheck— 138 tasks greenpnpm fixtures:check— zero diff outside the demo's deliberately re-emitted artifactspnpm lint:deps— cleanenum-prioritysubcommand run end-to-end (declaration-order sort observed: low, high, urgent). Occasional PGlite portal flakes reproduced and ruled pre-existing.Skill update
n/a — the
enum2spelling is deliberately transitional and is retired at the cutover (TML-2853), whenenumitself gains this meaning. Documenting a keyword scheduled for deletion in user-facing skills would teach syntax users should never adopt long-term; the skill update lands with the cutover rename.Follow-ups
enumblock sodb.enumsis literal-typed through the emitted contract (R6 emit parity; surfaced by this PR's review; cutover prerequisite).@default(Low)on a real PSL enum field).enum2→enum, migrate remaining native enums, delete the native machinery, add printer support for the final keyword.ORDER BYfor non-text value-sets) stays guarded — tracked separately.Alternatives considered
enumon@@typepresence. The new syntax is already distinguishable from native, soenumcould mean the new shape when@@typeis present. Rejected: it re-overloads one keyword with two meanings depending on an attribute — exactly the dual-meaning ambiguity this project removes. An explicit second keyword is unambiguous, and the cutover rename is trivial.parseEnum2Blockgrammar production instead of the generic extension-block path. Built in the first round (on the premise that the missing@@-attribute support justified a bespoke parse), rejected in review: the right fix for the gap was generic block-attribute support in the extension-block grammar, landed in the rework.entityTypes.enum2presence registration. Built in the first round mirroring native enum's Postgres entry, rejected in review: native enums are genuinely Postgres-specific, the domain enum is target-agnostic. Support now falls out of codec resolution; the factory invariant admits interpreter-lowered blocks.@map(...)for member values. Rejected:@mapeverywhere else in PSL means a physical storage mapping; the member value is a first-class domain value, so it's assigned with=.typeRefleak. Rejected: they masked a parity violation (PSL emit ≠ TS emit, divergentstorageHash). Fixed at the source; the patches reverted as dead code.Checklist
git commit -s) per the DCO. The DCO status check will block merge if any commit is missing aSigned-off-by:trailer.n/aif the change is doc-only / refactor with no behavioural delta).TML-NNNN: <sentence-case title>form (Linear ticket prefix + concise title naming the concrete deliverable). See.claude/skills/create-pr/SKILL.mdfor the full convention.n/a — internal only).Summary by CodeRabbit
New Features
Migrations
CLI / Demo
Authoring
Tests