TML-3086: bind @db.Date to pg/date@1; infer 1:1 from unique indexes - #1038
Conversation
|
Warning Review limit reached
Next review available in: 2 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (36)
📝 WalkthroughWalkthroughThe change adds explicit partial-index metadata across SQL IR, database introspection, migration planning, and relation inference. PostgreSQL date mappings now use ChangesSchema metadata and date codec updates
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Database
participant ControlAdapter
participant SqlIndexIR
participant RelationInference
Database->>ControlAdapter: expose partial-index metadata
ControlAdapter->>SqlIndexIR: construct index with partial flag
SqlIndexIR->>RelationInference: provide index columns and uniqueness
RelationInference-->>Database: infer singular or list back-relation
sequenceDiagram
participant PSL
participant DateResolver
participant PostgresCodecMap
participant Runtime
PSL->>DateResolver: resolve Date or `@db.Date`
DateResolver->>PostgresCodecMap: select pg/date@1
PostgresCodecMap->>Runtime: decode date columns
Runtime-->>PSL: return UTC-midnight Date values
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
@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/config-loader
@prisma-next/emitter
@prisma-next/language-server
@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 📦
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts (1)
1187-1207: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPrefer total indexes when deduplicating identical column tuples.
If a table has both a total unique index and a partial unique index on the same columns, the current tie-breaker may retain the partial one based on its name. The downstream relation inference then cannot see the qualifying non-partial unique index and may infer a 1:N relation instead of 1:1. Prefer
partial: falsebefore the name tie-breaker and add a regression fixture covering both indexes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts` around lines 1187 - 1207, Update the bestByColumnTuple selection in the survivingIndexes deduplication loop to prefer total indexes (partial: false) over partial indexes before comparing names, while preserving the existing uniqueness preference and deterministic name tie-breaker. Add a regression fixture covering identical-column total and partial unique indexes and verify relation inference remains 1: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
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.16-to-0.17/instructions.md`:
- Around line 207-211: Replace broad Date substring matching in both upgrade
instruction registries with precise date-column detection: in
skills/extension-author/prisma-next-extension-upgrade/upgrades/0.16-to-0.17/instructions.md
lines 207-211, match affected contract/PSL declarations or codec references; in
skills/upgrade/prisma-next-upgrade/upgrades/0.16-to-0.17/instructions.md lines
193-198, ensure DateTime fields match only when annotated with `@db.Date`.
In `@test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`:
- Around line 416-418: Update the ticket reference in the comment near the
inferred singular back-relation explanation from TML-3086 to TML-3087, leaving
the surrounding rationale unchanged.
---
Outside diff comments:
In `@packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts`:
- Around line 1187-1207: Update the bestByColumnTuple selection in the
survivingIndexes deduplication loop to prefer total indexes (partial: false)
over partial indexes before comparing names, while preserving the existing
uniqueness preference and deterministic name tie-breaker. Add a regression
fixture covering identical-column total and partial unique indexes and verify
relation inference remains 1:1.
🪄 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 Plus
Run ID: 62145add-8b48-41d9-9bb9-4ce281518924
📒 Files selected for processing (36)
packages/2-sql/1-core/schema-ir/src/ir/sql-index-ir.tspackages/2-sql/1-core/schema-ir/test/sql-flat-tree-diffable.test.tspackages/2-sql/1-core/schema-ir/test/sql-index-ir.test.tspackages/2-sql/1-core/schema-ir/test/sql-schema-ir-node.test.tspackages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.types.test.tspackages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.tspackages/2-sql/9-family/src/core/psl-contract-infer/relation-inference.tspackages/2-sql/9-family/test/contract-to-schema-ir.test.tspackages/2-sql/9-family/test/psl-contract-infer/relation-inference.test.tspackages/2-sql/9-family/test/schema-verify.classify.test.tspackages/2-sql/9-family/test/schema-verify.helpers.tspackages/3-extensions/pgvector/test/migrations/planner.behavior.test.tspackages/3-extensions/postgres/test/native-type-parity.test.tspackages/3-extensions/postgres/test/scalar-type-parity.test.tspackages/3-targets/3-targets/postgres/src/core/codec-type-map.tspackages/3-targets/3-targets/postgres/src/core/migrations/contract-to-postgres-database-schema-node.tspackages/3-targets/3-targets/postgres/test/migrations/node-issue-planner.test.tspackages/3-targets/3-targets/postgres/test/postgres-table-schema-node.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.naming-and-constraints.test.tspackages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.relations.test.tspackages/3-targets/3-targets/sqlite/test/migrations/node-issue-helpers.tspackages/3-targets/6-adapters/postgres/src/core/control-adapter.tspackages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.tspackages/3-targets/6-adapters/postgres/test/control-adapter.test.tspackages/3-targets/6-adapters/postgres/test/control-mutation-defaults.test.tspackages/3-targets/6-adapters/postgres/test/migrations/index-introspection.integration.test.tspackages/3-targets/6-adapters/postgres/test/migrations/planner-schema-lookup.test.tspackages/3-targets/6-adapters/postgres/test/migrations/planner.unique-index-structural.test.tspackages/3-targets/6-adapters/sqlite/src/core/control-adapter.tspackages/3-targets/6-adapters/sqlite/test/control-adapter.test.tsskills/extension-author/prisma-next-extension-upgrade/upgrades/0.16-to-0.17/instructions.mdskills/upgrade/prisma-next-upgrade/upgrades/0.16-to-0.17/instructions.mdtest/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.tstest/integration/test/infer-roundtrip-runtime.integration.test.ts
|
Re the outside-diff CodeRabbit finding on |
… works Both spellings — legacy `DateTime @db.Date` (NATIVE_TYPE_SPECS) and the bare `Date` type constructor (postgresNativeAuthoringTypes) — previously inherited `pg/timestamptz@1`, whose decodeJson rejects the bare YYYY-MM-DD strings that json_agg renders, so relation include() over a date column failed with RUNTIME.DECODE_FAILED. Bind both to the dedicated pg/date@1 codec and add the missing `date` key to the postgres codec descriptor map. Breaking: re-emitted contracts with date columns change codec ref and storage hash; previously emitted contracts keep working (pg/timestamptz@1 still exists). Upgrade instructions recorded in both 0.16-to-0.17 skill packages. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A one-to-one enforced by a bare CREATE UNIQUE INDEX (rather than a UNIQUE constraint) inferred as a list back-relation because detectOneToOne consulted only the primary key and unique constraints. It now also accepts an exact, order-insensitive column-set match against a unique index — but only a total one: a partial unique index does not guarantee at-most-one child per parent, so introspection now reads pg_index.indpred and carries partiality on SqlIndexIR as a non-enumerable field excluded from structural equality and JSON (same treatment as dependsOn), leaving differ/verify semantics and serialized schemas unchanged. Expression unique indexes never survive introspection with a usable column set, so they keep a list back-relation; that behavior is pinned end-to-end. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…-unique-index journey skips emit (TML-3086) Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…y (TML-3086) An optional partial field silently read as "total" when absent, which hid the sqlite adapter never reporting partiality at all. Every producer now states it: the postgres adapter stamps the indpred-derived boolean unconditionally, the sqlite adapter reads the PRAGMA index_list partial column, and contract-derived indexes stamp partial: false (PSL cannot express partial indexes). The field stays non-enumerable — out of JSON, Object.keys, and isEqualTo. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…t omitted (TML-3086) name/type/options/annotations/dependsOn keep undefined-able values but become required keys, so every construction site says name: undefined rather than silently dropping the key. Instance shape is unchanged: the constructor still assigns enumerable properties only for defined values and keeps dependsOn/partial non-enumerable, so JSON output, Object.keys, and isEqualTo are byte-identical. ifDefined spreads at producer sites collapse to direct key assignments. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ate matches The contains ["@db.Date", "Date"] predicate substring-matched every DateTime field in .prisma files and ordinary Date usage in TS files, so the detection stopped filtering at all. Replace it with regex predicates that match the @db.Date attribute and the bare PSL Date type token at a word boundary, which cannot occur inside DateTime. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
128b6c7 to
89abff1
Compare
…us (#1035) + date-emission regen (#1038) Merges 2 more origin/main commits on top of the prior merge (9b99660): #1038 (TML-3086) binds @db.Date to pg/date@1 and infers 1:1 from unique indexes, re-churning date-column contract hashes; #1035 ports the 488-case prisma test corpus under test/integration/test/ports/**, whose fixture configs and generated contracts still carry this branch's pre-rename keys since they were authored against pre-rename main. The merge itself was conflict-free (git auto-resolved cleanly): the two 0.16-to-0.17 upgrade-instruction files each gained one new sibling entry from #1038 (postgres-date-rebound-to-pg-date) alongside the entries this branch and main already both carried from the prior merge — verified all entries from both sides survived. The corpus key-rename and hash regeneration for #1038's date-column change land in follow-up commits on top of this one. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… fixtures The 488-case prisma test corpus (#1035) was authored against pre-rename main, so its 43 _fixture/prisma-next.config.ts sugar configs and shared Postgres harness still used outputPath/extensionPacks. Renames outputPath to output across all 43 configs plus this corpus's own README (which documented the old key for contributors), and extensionPacks to extensions in the one hand-written harness that builds a control stack directly (_harness/postgres.ts) — the corpus's other 87 extensionPacks occurrences were all in the checked-in generated contract.json/contract.d.ts pairs, regenerated below rather than hand-edited. Regenerated all 43 fixture pairs via the corpus's documented `contract emit --config <fixture>` command (not covered by fixtures:emit/check, per test/integration/test/ports/README.md). None of the corpus schemas use the PSL `@db.Date` attribute, so none of the churn traces to #1038; it is entirely the extensions key landing in the hashed bytes. Regenerating legacy-json's fixture surfaced a real, pre-existing gap: its schema authored `requiredJson`/`optionalJson` as bare `Json`, which main's prior PSL scalar-type unification (#1022) binds to pg/json@1 (no equality comparison) rather than the jsonb-backed storage upstream Prisma actually gives its `Json` scalar. The test's own translation comment confirms it means jsonb (`where: { requiredJson: { not: ... } }` ported to `.neq(...)`), so the committed fixture predates #1022 and was never regenerated afterward -- main's own CI never caught it because the ports corpus has no fixtures:check coverage. Switched both fields to `Jsonb`, matching the already-recorded 0.16-to-0.17 upgrade guidance (postgres-json-rebound-to-native-json) exactly; scoped repo-wide and confirmed no other suite in the corpus hits the same gap. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Linked issue
Refs TML-3086. Round 2 of the external field report behind TML-3037 / #1011. The reporter's remaining feature ask (relation-generation opt-out) is deferred to TML-3087.
At a glance
Before this PR the
include()call threw at decode time:@db.Datecolumns carried no codec binding and inheritedpg/timestamptz@1, whosedecodeJsonrejects the bareYYYY-MM-DDthatjson_aggrenders. The reporter types dates asStringin production to survive this.Decision
Two defects from the re-verified v0.16.0 field report, one slice:
@db.Date(and bareDate) now bindpg/date@1. A deliberate interim, decided 2026-07-23: TML-2987 (infer prints bare types) is unscheduled and the runtime failure is hurting a production user now. The binding is deleted together with@db.*in TML-2988. Breaking: re-emitting a contract with date columns changes the column's codec ref and the contract hash (previously emitted contracts keep working —pg/timestamptz@1still exists). Upgrade instructions recorded.contract inferdetects a 1:1 enforced by a bareCREATE UNIQUE INDEX. Previously only UNIQUE constraints and primary keys counted, so index-enforced 1:1s inferred as list back-relations. Partial unique indexes prove nothing and are excluded, which required introspection to start readingpg_index.indpred.Reviewer notes
?back-relation does not survivecontract emit. A bare unique index has no PSL representation (the printer emits@unique/@@uniquefromtable.uniquesonly), so the interpreter rejects the printed PSL withPSL_NON_UNIQUE_BACKRELATION. Resolving it means either printing@unique(declares a constraint the live DB doesn't have → verify drift) or teaching printer+interpreter about unique indexes — both beyond this slice's scope. The new e2e block pins the printed PSL and carries a comment recording the gap; likely bundled with TML-3087.Daterebind looks like scope creep but isn't: the parity tests assert@db.Dateand the bareDateconstructor produce identical codec bindings, so rebinding one spelling alone is impossible. Both must pinpg/date@1(coordinates with TML-2986, mid-flight in the Remove-@db.*project).SqlIndexIR.partialuses the same non-enumerable pattern asdependsOn: absent from JSON, structural equality, and drift detection. Surfacing partial-index drift is explicitly out of scope;pnpm fixtures:checkconfirms zero fixture churn.How it fits together
db.Dateentry inNATIVE_TYPE_SPECSgoes fromcodecId: null(inherit from base type) to'pg/date@1'(psl-column-resolution.ts); the bareDateconstructor in the postgres adapter's authoring defaults follows (control-mutation-defaults.ts); the postgres target'scodecDescriptorMapgains the missingdatekey (codec-type-map.ts).pg_indexquery now selectsix.indpred IS NOT NULL AS indpartial(control-adapter.ts), stamped onto the IR only when true.SqlIndexIR.partialisdeclare readonly+defineNonEnumerable— the exactdependsOnpattern (sql-index-ir.ts).detectOneToOneadds a third arm: an exact, order-insensitive column-set match against a unique, non-partial entry intable.indexes(relation-inference.ts).pnpm check:upgrade-coverageenforces coverage.Behavior changes & evidence
.include()over a date column decodes; dates canonicalize as UTC-midnightDates in both top-level select and include paths. Implementation: psl-column-resolution.ts, codec-type-map.ts. Evidence: the previously pinned-red block in infer-roundtrip-runtime.integration.test.ts is now the green whole-shape assertion above; interpreter and parity pins flipped in interpreter.types.test.ts and native-type-parity.test.ts.?back-relation; a partial one keeps[]. Implementation: relation-inference.ts. Evidence: unit coverage in relation-inference.test.ts; end-to-end total/partial/expression tables through realcontract inferin infer-roundtrip-fidelity.e2e.test.ts.Testing performed
On final HEAD (
36a8015), full CI gate set:pnpm build(68/68),pnpm typecheck:packages+pnpm typecheck:examples(green;typecheck --forcehits a pre-existing Turbo build/typecheck scheduling race in packages untouched here — CI doesn't use--force)lint:casts,lint:deps,check:upgrade-coverage --mode pr) — greenpnpm fixtures:check— zero churnpnpm test:packages(13499 passed),pnpm test:integration(1173/1173),pnpm test:e2e(109/109). Four timeout flakes under full-suite load (emitter, adapter render-roundtrip, two integration afterAll cleanups) each pass solo and touch files outside this diff.Skill update
Upgrade skills updated:
postgres-date-rebound-to-pg-dateentries appended to skills/upgrade/prisma-next-upgrade/upgrades/0.16-to-0.17/instructions.md and skills/extension-author/prisma-next-extension-upgrade/upgrades/0.16-to-0.17/instructions.md. No CLI/API surface changed otherwise.Follow-ups
@db.*project keeps both spellings pinned topg/date@1.Alternatives considered
@db.Dateunbound until the Remove-@db.*project lands (the TML-3037 disposition). Rejected 2026-07-23: that project's bare-types step (TML-2987) is unscheduled while a production user works around a runtime decode failure today. The interim binding dies with@db.*in TML-2988.pg/timestamptz@1.decodeJsontolerate bareYYYY-MM-DD. Rejected: it would paper over a wrong codec assignment and change timestamptz semantics for everyone; a date column deserves the date codec.SqlIndexIRfield. Rejected: it would change contract JSON, structural equality, and differ/verify semantics for a property nothing downstream consumes yet. The non-enumerable pattern already exists (dependsOn) for exactly this shape of need.@uniquefor index-backed 1:1s so infer output round-trips through emit. Rejected for this slice: it would declare a constraint the live database doesn't have and surface as verify drift. Needs its own decision (see reviewer notes).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).Slice spec (orphan slice — reconstructed contract)
Round 2 of the external field report (reporter "Eugen") that produced TML-3037 / #1011. Re-verified on v0.16.0: two defects remain in the infer → emit → runtime pipeline. The relation-generation opt-out is deferred to TML-3087.
Bind
@db.Date→pg/date@1.@db.DatecarriedcodecId: null, so columns inheritedpg/timestamptz@1, whosedecodeJsonrejects the bareYYYY-MM-DDstrings thatjson_aggrenders —.include()decode failed at runtime. Change theNATIVE_TYPE_SPECSentry, add the missingdatekey to the postgres target'scodecDescriptorMap, flip the pinned red assertion in the infer-roundtrip runtime integration test, update interpreter/print-psl pins, and record upgrade instructions (breaking: codec ref + contract hash change on re-emit; old contracts keep working; no in-repo emitted fixture has a date column). Decision context: deliberately overrides TML-3037's recorded deferral (Will, 2026-07-23); deleted with@db.*in TML-2988; both spellings (@db.Date, bareDate) must pinpg/date@1.detectOneToOneaccepts total unique indexes. A 1:1 enforced by a bareCREATE UNIQUE INDEXinferred as a list back-relation because detection consulted onlyprimaryKey+uniques. Introspection first learns partiality (pg_index.indpred); partiality is carried onSqlIndexIRas a first-class optional field excluded from structural equality and JSON (thedependsOnnon-enumerable pattern) so differ/verify semantics do not change; detection then accepts an exact column-set match against a total unique index. Tests: total unique index →?; partial →[]; expression index excluded.Close-out. Full CI gate set; sweep
test/integration+test/e2efor stale assertions; PR with decision-led narrative; heads-up comments on TML-2986/TML-2988.Summary by CodeRabbit
New Features
datecolumns now use the dedicatedpg/date@1codec.Bug Fixes
Documentation