From ac083d957a397cfe5f01e2f73dbc7df28da678f3 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:44:21 +0100 Subject: [PATCH 1/6] Drive: the final retro of the convenience date and time types project Three failure modes and two slice DoD items: review a merge of main that resolves an overlapping change, keep released upgrade sources as they shipped, and check hand-written sources when a name changes meaning. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- drive/calibration/dod.md | 2 ++ drive/calibration/failure-modes.md | 39 ++++++++++++++++++++++++++++++ drive/retro/README.md | 1 + 3 files changed, 42 insertions(+) diff --git a/drive/calibration/dod.md b/drive/calibration/dod.md index 6ce283d10c81..2b99a90b3e31 100644 --- a/drive/calibration/dod.md +++ b/drive/calibration/dod.md @@ -81,6 +81,8 @@ In addition to the canonical slice DoD: - [ ] No `projects/` references in long-lived files added by the slice (per the doc-maintenance rule; grep gate in [`grep-library.md`](./grep-library.md)). - [ ] Before enabling auto-merge, after the last merge of `main`: run the checks that only the merge queue or CI's Lint job runs. That is the PSL print sweeps (`test/integration/test/psl-print/every-postgres-contract-roundtrip.integration.test.ts` and the other `psl-print` files) when the slice changes contracts, columns or printing; every journey that runs a changed command ([`failure-modes.md § F44`](./failure-modes.md#f44-a-change-in-what-a-command-does-passes-pr-ci-and-fails-in-the-merge-queue-in-integration-tests-the-slice-never-touched)); and `pnpm lint:ratchets` (the cast, throw and framework vocabulary ratchets) and `pnpm check:upgrade-coverage --mode pr --prev --head HEAD`, which `lint:agent` skips. `grep -rhoE "run: pnpm [a-z:-]+" .github/workflows/*.yml | sort -u` lists the single-line `pnpm` steps in the GitHub workflows; read the workflows for steps it misses, such as multi-line `run` blocks. (Added 2026-10-21, raw-columns: the queue dropped one slice for an unrun print sweep and another for an unrun journey and the vocabulary lint.) +- [ ] After merging `main`: if the merge resolved conflicts with a change `main` made to the same code, run a review round on `git diff
` before asking for review ([`failure-modes.md § F51`](./failure-modes.md#f51-a-merge-of-main-that-resolves-a-large-overlapping-change-goes-to-review-as-if-it-were-not-new-work)); if `main` had a release bump, `git diff HEAD -- upgrade-instructions/releases/` is empty ([`F52`](./failure-modes.md#f52-a-release-bump-archives-pending-upgrade-instructions-and-the-branchs-later-edits-land-in-the-released-copy)). (Added 2026-10-28, datetime-convenience-types.) +- [ ] If the slice changes what an existing type name means: every PSL and TypeScript source under `test/` and `examples/` that writes the name is checked, and the integration files that emit them are run ([`F53`](./failure-modes.md#f53-a-name-changes-meaning-and-a-hand-written-source-that-fixturescheck-does-not-emit-keeps-the-old-meaning)). (Added 2026-10-28, datetime-convenience-types.) - [ ] Before enabling auto-merge: every review thread on the PR is resolved, by a fix or by a reply that says why not. List the open ones with `gh api graphql --paginate -F number= -f query='query($number:Int!,$endCursor:String){repository(owner:"prisma",name:"orm"){pullRequest(number:$number){reviewThreads(first:100,after:$endCursor){pageInfo{hasNextPage endCursor} nodes{id isResolved path}}}}}' --jq '.data.repository.pullRequest.reviewThreads.nodes[]|select(.isResolved==false)|"\(.id) \(.path)"'`, which prints nothing when every thread is resolved; bot reviewers count. (Added 2026-10-27, mongo-authoring-structure: slice 1 merged with four open CodeRabbit threads, found only at close-out.) - [ ] If the slice changes code that `defineContract`, a pack descriptor or the runtime reaches: run `pnpm size:build` in `examples/bundle-size` (it builds the workspace, then bundles) on the branch and on the PR base ref (`main`, or the base branch of a stacked PR), compare the metafiles, and explain any growth in the PR description before CI's size report asks. (Added 2026-10-27, mongo-authoring-structure: both slices grew a client bundle and the cause was traced only after CI reported it.) diff --git a/drive/calibration/failure-modes.md b/drive/calibration/failure-modes.md index 3707e51a3fee..5bf224408d15 100644 --- a/drive/calibration/failure-modes.md +++ b/drive/calibration/failure-modes.md @@ -907,3 +907,42 @@ Per-repo stop conditions beyond the canonical ones: - The parity suite gets a case per assumption, accepted and refused, not only a case per existing rule. **Reference incident.** One MongoDB definition and builder (prisma/orm#30846, 2026-10): the principal-engineer review found, in two rounds, that a variant field could be stored under a name its base used, and that a variant could restate a base field with another type or stored name. Both built, and MongoDB then refused the variant's documents, because the validator is written from the base. Both were on `main` before the refactor. + +### F51. A merge of `main` that resolves a large overlapping change goes to review as if it were not new work + +**Symptom.** A branch that review has already passed merges `main`, and `main` has a change that rewrote the same code. The merge resolves dozens of conflicts with design choices of its own, such as which side's concept survives. The branch is then handed to the operator for review with no review round on the merge, and the next round finds many defects in it. + +**Root cause.** The slice DoD treats review as done once a round has passed, and a merge of `main` reads as routine upkeep. A conflict resolution that keeps one side's concept, rewrites the other side's new code to new names, or edits docs both sides changed is implementation work, and nothing in the DoD asks for it to be reviewed. + +**Detection signal.** + +- The merge commit touches more than a handful of conflicted files, or `git diff
` shows the branch editing code `main` just added. +- The resolver's report contains decisions ("kept X only for Y", "moved Z into W"). + +**Mitigation.** After a merge of `main` like this, run a review round on what the branch adds on top of `main` (`git diff
`) before asking the operator to review, and fix its findings first. + +**Reference incident.** Convenience date and time types (prisma/orm#30847, 2026-10): the merge of the field-presets removal resolved 42 conflicted files and kept the `option` argument kind for type choices only. The operator had to ask whether it had been reviewed; the round that followed found twelve defects, including refusal messages that did not parse and upgrade text written into a released guide. + +### F52. A release bump archives pending upgrade instructions, and the branch's later edits land in the released copy + +**Symptom.** A branch edits a pending upgrade instruction that another change created. A release then moves the pending instructions into `upgrade-instructions/releases/-to-/sources/`. Merging `main` carries the branch's edits into the released sources, which now describe a later release and disagree with the assembled guide that shipped. + +**Root cause.** Git follows the rename, so the branch's edit applies cleanly to the moved file. Nothing checks that released sources stay as they shipped. + +**Detection signal.** After merging a release bump, `git diff HEAD -- upgrade-instructions/releases/` is not empty. + +**Mitigation.** After merging a release bump, run that diff. Restore any released file the branch changed (`git checkout -- `) and put the branch's guidance in its own pending instruction, with a sentence for readers who followed the released guide. + +**Reference incident.** Convenience date and time types (prisma/orm#30847, 2026-10): the 8.0.0-rc.21 bump archived `remove-field-presets` and `prisma7-now-default-on-create` while the branch was rewriting them; the code review found the released sources describing rc.22 behaviour. + +### F53. A name changes meaning, and a hand-written source that `fixtures:check` does not emit keeps the old meaning + +**Symptom.** A change gives an existing type name a new meaning (here `DateTime`, from a Temporal value to a JavaScript `Date`) and rewrites every emitted fixture so no contract changes. `pnpm fixtures:check` passes. The merge queue then fails a test whose own PSL or TypeScript source is written inside a test fixture directory and emitted only by that test. + +**Root cause.** The rewrite was driven by the emitted fixtures `fixtures:check` knows about. Sources that a test emits at run time are not in that list, and PR CI skips the integration tests that read them. + +**Detection signal.** `git grep -n "" -- 'test/**/*.psl' 'test/**/*.prisma' 'test/**/fixtures/**'` lists files the branch did not change. + +**Mitigation.** When a name changes meaning, grep every PSL and TypeScript source under `test/` and `examples/` for the name, rewrite each one that relied on the old meaning, and run the integration file that emits it before enabling auto-merge. + +**Reference incident.** Convenience date and time types (prisma/orm#30847, 2026-10): `cli.emit-command.test.ts` emits `fixtures/emit-command/schema.parity.psl`, which still wrote bare `DateTime` beside a TypeScript side written with the Temporal type; the queue dropped the PR on the hash mismatch. diff --git a/drive/retro/README.md b/drive/retro/README.md index 035a7558a339..e25223fcb368 100644 --- a/drive/retro/README.md +++ b/drive/retro/README.md @@ -19,6 +19,7 @@ When a retro lesson could land in multiple surfaces, the team's preference: Patterns the team has seen multiple times. Each entry: pattern → severity → mitigation surface (where it landed). +- **2026-10-28 — Final datetime-convenience-types retro:** The merge of `main`'s field-presets removal into the convenience-types branch resolved 42 conflicted files with design choices of its own, and went to the operator for review with no review round on it. The operator had to ask, and the round that followed found twelve defects (landed in [`failure-modes.md § F51`](../calibration/failure-modes.md#f51-a-merge-of-main-that-resolves-a-large-overlapping-change-goes-to-review-as-if-it-were-not-new-work) and a PR-side item in [`dod.md`](../calibration/dod.md)). Among them, the 8.0.0-rc.21 release bump had carried the branch's edits into the released upgrade sources (landed in [`F52`](../calibration/failure-modes.md#f52-a-release-bump-archives-pending-upgrade-instructions-and-the-branchs-later-edits-land-in-the-released-copy)). The merge queue then dropped the PR twice, on a test fixture that still used `DateTime`'s old meaning and on a journey that `main`'s newest commit changed. The second is the raw-columns lesson again: the PR-side DoD was not walked before auto-merge, and the fourth project in a row hit it (the fixture case landed in [`F53`](../calibration/failure-modes.md#f53-a-name-changes-meaning-and-a-hand-written-source-that-fixturescheck-does-not-emit-keeps-the-old-meaning) and a PR-side item; walking the list before auto-merge is in the orchestrator's memory). When infer wrote a `Date` for a column whose default is `infinity`, the orchestrator offered the operator two options, one of which kept the failure, instead of fixing it (landed in the orchestrator's memory). What worked: folding four approved PRs into one at the operator's suggestion sent them through the queue as one entry; a colleague's review of ADR 279 led to the editor hover naming the explicit type each call becomes; and the independent system-design and code reviews found real defects every round, such as hover text that was not valid PSL and refusal messages whose `?` landed after the attributes. ADR-worthy decision: ADR 279, a convenience type picks one explicit type from a table. One-sentence summary: PostgreSQL schemas can write `DateTime`, `Date`, `Time` and `Duration`, each meaning exactly one explicit type, with defaults that need no Temporal polyfill, and `contract infer`, `contract print`, the Prisma 7 reader and `prisma orm init` write them. - **2026-10-27 — Final mongo-authoring-structure retro:** The close-out was first done from the plan's five-line checklist instead of the close-out procedure, so the retro, the DoD evidence, the file classification and two references were skipped until the operator sent it back; close-out now names the procedure (landed in [`drive/project/README.md`](../project/README.md#close-out-runs-the-close-out-procedure)). Slice 1 merged with four open CodeRabbit threads, because nothing before auto-merge listed open threads; one was a real ADR inconsistency (landed as a PR-side item in [`dod.md`](../calibration/dod.md)). Both slices grew a client bundle and the cause was traced only after CI's size report; measuring the bundle against `main` is now a PR-side item for slices that change code the runtime or `defineContract` reaches (landed in `dod.md`). Moving every MongoDB rule into one builder did not catch two invariants the collection validator relied on and no rule enforced (a variant field stored under a base field's name, and a variant restating a base field differently); the principal-engineer review found them, and both were on `main` too (landed in [`failure-modes.md § F50`](../calibration/failure-modes.md#f50-rules-are-moved-into-one-place-and-an-invariant-that-a-derived-artefact-relies-on-was-never-a-rule)). What worked: the operator rejected a one-case fix and asked for research against the architecture docs first, which found the structural cause and gave two slices instead of a string of patches; independent system-design and code reviews and manual QA each found defects the implementation tests passed over (details in prisma/orm#30846 and F50); and a parity suite that writes each case in both sources caught regressions in every later round. ADR-worthy decision: ADR 306, one plain-data definition and one builder per family. One-sentence summary: MongoDB contracts written in TypeScript, PSL or Prisma 6 now read written values through ADR 254's cast rule and build through one builder, so the same contract written two ways emits the same `contract.json` and is refused for the same reasons. - **2026-10-21 — Final complete-client-surfaces retro:** A mechanical rewrite of call sites in docs needs a check per site that the replacement means the same thing, because README samples are not type-checked and `db.execute(plan)` accepts a plan that returns rows: one PostGIS sample moved from `db.runtime().execute` to `db.execute` while reading rows, and only CodeRabbit caught it after three review rounds (landed in this entry). When a branch is built locally on an approved, queued pull request and that request lands as a squash commit, the merge of `main` conflicts on every file both touched; compare `main`'s copy of each conflicted file with the base branch's tip, and if they match, keep the branch's side (landed in this entry). The session gets no event when a pull request merges, so a slice that waits on a merge must be started by someone; here slice 3 started hours after slice 2 merged (landed in the orchestrator's memory). What worked: framing the static half of a client as the contract value (the operator's idea) removed the explicit type argument instead of working around it, and a spike on typing the JSON import directly was written up as ADR 277 before it was rejected; independent system-design and code reviews found that `toSql` skipped the runtime's plan check and that Supabase `close()` ended a pool the caller passed in; manual QA against a dev database confirmed `mode` and found TML-3655. One-sentence summary: an application's client is now one complete object, built from `contractFromJson(json)` with every other option inferred, carrying its custom collection classes into every transaction, loadable without a database, and able to render and run any plan. - **2026-10-21 — Final raw-columns retro:** The merge queue dropped this project's pull requests three times for a check that CI does not run on the pull request: slice 2 for the PSL print sweep tests, and slice 0 for a CLI journey that runs a changed command, then for the framework vocabulary lint that only CI's Lint job runs. Slice 0 also left the queue once for merge conflicts with `main`. This is the third project in a row with the same lesson, so it is now an item in the slice DoD: after the last merge of `main`, run the print sweeps, the journeys of changed commands and the CI-only lints before enabling auto-merge (landed in [`dod.md`](../calibration/dod.md) PR-side items). Slice 1 took seven merges of `main` and repeated conflicts before it merged, because its branch lived while other projects changed the same authoring and migration code; the operator ended that by merging it with the interim `Raw("...")` syntax and moving the breaking change to its own slice. Ship the part of a slice that depends on nothing unmerged, and put the part that waits on another project in a later slice. Slice 0 was planned first and built last, because it depended on TML-3607, another project's unmerged change: a plan orders slices by what they wait on, so a slice blocked on unmerged work goes last from the start (landed in this entry). A reviewer checked out another commit in the shared worktree in the middle of a review, which changes the code the other agents in that worktree build and test (landed in [`failure-modes.md § F22`](../calibration/failure-modes.md#f22-reviewer-runs-git-stash-to-debug-a-worktree-local-red-and-pops-an-unrelated-worktrees-stash-entry)). What worked: every review round found a real defect the implementation's tests passed over: a crash on an empty named argument, SQLite length parameters rounded instead of refused, and a `contract infer` message that pointed users at the wrong fix. One-sentence summary: a column of any type the database has can now be declared with ``Raw(sql`...`)``, created, verified and read, and a Prisma 7 schema with `Unsupported("...")` fields loads into Prisma 8. From 123bd27d21d1b6c60873a3ec71d05e7d72b34b39 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:44:21 +0100 Subject: [PATCH 2/6] Docs: ADR 257 records that the date and time renames had no deprecated aliases, and the rename project no longer links the deleted folder Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- ... Scalar types are named after the target on every surface.md | 2 +- projects/target-named-scalars-sql/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md b/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md index 864dfbcb1bd4..6774b9c22014 100644 --- a/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md +++ b/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md @@ -2,7 +2,7 @@ Status: **Accepted** -**Amended by [ADR 279](ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) (2026-10-22):** where a stored type has several codecs, every one of them carries a suffix naming its kind of value, not only the second (`TimestamptzTemporal`, `TimestamptzJsDate`, `TimestamptzString`). The unsuffixed name of the concept, such as `DateTime`, `Date`, `Time` and `Duration` on PostgreSQL, belongs to a convenience type, a type choice that becomes one of the suffixed types. +**Amended by [ADR 279](ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) (2026-10-22):** where a stored type has several codecs, every one of them carries a suffix naming its kind of value, not only the second (`TimestamptzTemporal`, `TimestamptzJsDate`, `TimestamptzString`). The unsuffixed name of the concept, such as `DateTime`, `Date`, `Time` and `Duration` on PostgreSQL, belongs to a convenience type, a type choice that becomes one of the suffixed types. Those renames were made before the release candidate, so the old names were removed at once, with no deprecated alias for a release line; the upgrade instructions give each rewrite. ## Decision diff --git a/projects/target-named-scalars-sql/README.md b/projects/target-named-scalars-sql/README.md index bf2d09ee41d9..6781f78be5e3 100644 --- a/projects/target-named-scalars-sql/README.md +++ b/projects/target-named-scalars-sql/README.md @@ -2,6 +2,6 @@ Shaped 2026-09-25, not started; spec input from the Mongo defaults, codecs and Prisma 6 source project (merged 2026-09-25 to 2026-09-27). See [`spec.md`](./spec.md) for the rename tables, the `Json` preset split, `Json` refusing non-JSON values on PostgreSQL and SQLite, the enum storage-type rule moving into the framework, the upgrade fragments and the documentation changes. The decision it applies is [ADR 257](../../docs/architecture%20docs/adrs/ADR%20257%20-%20Scalar%20types%20are%20named%20after%20the%20target%20on%20every%20surface.md). -> **Changed by the Convenience date and time types project (2026-10-22).** [ADR 279](../../docs/architecture%20docs/adrs/ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) (Proposed) keeps `DateTime` as a convenience type and renames the PostgreSQL date and time types itself: `Timestamptz` → `TimestamptzTemporal`, `Timestamp` → `TimestampTemporal`, `Date` → `DateTemporal`, `Time` → `TimeTemporal`, `Interval` → `IntervalParts`. The `DateTime` and `Timestamptz` rows of this spec's Postgres table, and `field.dateTime()` → `field.timestamptz()`, are superseded; see `projects/datetime-convenience-types/`. +> **Changed by the Convenience date and time types project (2026-10-22).** [ADR 279](../../docs/architecture%20docs/adrs/ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) keeps `DateTime` as a convenience type and renames the PostgreSQL date and time types itself: `Timestamptz` → `TimestamptzTemporal`, `Timestamp` → `TimestampTemporal`, `Date` → `DateTemporal`, `Time` → `TimeTemporal`, `Interval` → `IntervalParts`. The `DateTime` and `Timestamptz` rows of this spec's Postgres table, and `field.dateTime()` → `field.timestamptz()`, are superseded; ADR 279 lists the PostgreSQL explicit and convenience types. > Everything under `projects/` is transient — migrate long-lived architecture and upgrade documentation to `docs/`, remove repo-wide references to this workspace, and delete it at project close-out per [`projects/README.md`](../README.md). From 8068757a88a0a53140a0c1ae141e7d711a18edb4 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:44:22 +0100 Subject: [PATCH 3/6] Delete projects/datetime-convenience-types/ Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- projects/datetime-convenience-types/README.md | 12 -- .../design-notes.md | 71 ----------- .../datetime-convenience-types/handover.md | 83 ------------ projects/datetime-convenience-types/plan.md | 60 --------- .../slices/01-iso-strings/spec.md | 55 -------- .../slices/02-prisma7-now-defaults/spec.md | 50 -------- .../slices/03-explicit-types/spec.md | 62 --------- .../slices/04-convenience-types/spec.md | 68 ---------- .../05-tools-write-convenience-types/spec.md | 77 ------------ projects/datetime-convenience-types/spec.md | 118 ------------------ 10 files changed, 656 deletions(-) delete mode 100644 projects/datetime-convenience-types/README.md delete mode 100644 projects/datetime-convenience-types/design-notes.md delete mode 100644 projects/datetime-convenience-types/handover.md delete mode 100644 projects/datetime-convenience-types/plan.md delete mode 100644 projects/datetime-convenience-types/slices/01-iso-strings/spec.md delete mode 100644 projects/datetime-convenience-types/slices/02-prisma7-now-defaults/spec.md delete mode 100644 projects/datetime-convenience-types/slices/03-explicit-types/spec.md delete mode 100644 projects/datetime-convenience-types/slices/04-convenience-types/spec.md delete mode 100644 projects/datetime-convenience-types/slices/05-tools-write-convenience-types/spec.md delete mode 100644 projects/datetime-convenience-types/spec.md diff --git a/projects/datetime-convenience-types/README.md b/projects/datetime-convenience-types/README.md deleted file mode 100644 index 643b12a0c75d..000000000000 --- a/projects/datetime-convenience-types/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Convenience date and time types - -Project folder for [Convenience date and time types](https://linear.app/prisma-company/project/convenience-date-and-time-types-6a969ca1c76e). PSL gets `DateTime`, `Date`, `Time` and `Duration`, one type per concept in the application, each choosing one of PostgreSQL's explicit date and time types from its `as:` and `storage:` arguments, with defaults that need no Temporal polyfill. - -- [`spec.md`](./spec.md) — purpose, settled decisions, non-goals, cross-cutting requirements, project DoD. -- [`design-notes.md`](./design-notes.md) — the design discussion of 2026-10-21 and 2026-10-22, with the alternatives rejected. -- [`plan.md`](./plan.md) — slice plan. -- `slices//` — per-slice spec, plan and reviews. - -The decision record is [ADR 279](../../docs/architecture%20docs/adrs/ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md). - -Everything in this folder is transient. At close-out the durable parts move to `docs/` and the ADR, and the folder is deleted. diff --git a/projects/datetime-convenience-types/design-notes.md b/projects/datetime-convenience-types/design-notes.md deleted file mode 100644 index 4e0257d47cab..000000000000 --- a/projects/datetime-convenience-types/design-notes.md +++ /dev/null @@ -1,71 +0,0 @@ -# Design notes — Convenience date and time types - -Record of the design discussion between the operator and the orchestrator on 2026-10-21 and 2026-10-22. It started from a research report comparing how a user declares and reads date and time fields on PostgreSQL in Prisma 7 and Prisma 8. - -## What the research found - -- **Prisma 7** had one type, `DateTime`, whose value was always a JavaScript `Date`. `@db.*` attributes changed the column type. It was easy to learn, but a `Date` is the wrong value for a calendar date or a time of day. A birthday created at local midnight east of UTC was stored as the day before, and a `timetz` offset was lost on read. -- **Prisma 8** has eleven type names for dates and times. Each fixes both the column type and the kind of value. Nothing says which to start with, and each kind of value had a problem: - - The Temporal types fail on Node without a polyfill. The failure comes at the first read, not at startup. A `create` can commit and then throw, and without the type declarations the fields are typed `any`. - - The string types return PostgreSQL's display text, which depends on the server's `TimeZone` and `DateStyle`. Every schema Prisma 8 writes for the user uses them. - - A `Date` is available only for `timestamptz`. - - An upgraded Prisma 7 app goes from `Date` values to strings. - -## Principles - -- **Explicit controls first, convenience on top.** Prisma 8 gives users explicit, predictable controls, then makes the common cases easy by composing convenience APIs over them, as the client facades compose the runtime, adapter and driver. The date and time types had the first half only. -- **The schema describes the application's data in the application's terms;** database details are configuration (architecture overview, "Domain-first surfaces"). -- **A convenience API never hides a choice the explicit layer exposes.** Every convenience form becomes one explicit type, and the contract records only that. - -## Decisions and why - -**Convenience types over explicit types, not a renaming.** An early version of the proposal renamed every type after the application's meaning, which would have reversed ADR 257. The operator reframed it as layering: the database-named types stay as the precise form, and the new types are the short form built on them. Nothing is reversed, and a user who needs a specific column type and value kind keeps writing the explicit type. - -**A type choice names the explicit type.** (Refined when slice 4 was grounded: the first plan let a type constructor's codec be a select template over option arguments, but selects have no defaults and no refusals, and that machinery is being removed with the presets. A type choice is a table from argument values to explicit constructor names.) One codec for `DateTime` with the database type as a parameter was rejected: a codec represents values of exactly one data type, and a column stores the data type its codec represents (ADR 254). A type alias with arguments was rejected: an alias attaches attributes to one fixed type, while choosing a type from arguments is what a type constructor does. Type constructors already take arguments. - -**Names follow the application's concepts: `DateTime`, `Date`, `Time`, `Duration`.** Naming them after the Temporal types (`Instant`, `PlainDate`, `PlainTime`) was rejected as library vocabulary, not domain vocabulary: a user thinks "a birthday", not "a plain date". The mapping from each name to its value must be obvious. - -**The explicit Temporal types get a suffix.** `Date` and `Time` were explicit types (Temporal-valued), and the convenience layer needed the names. Renaming only those two would leave `Timestamptz` as an unmarked name for one kind of value next to suffixed neighbours. So all four Temporal-valued explicit types carry `Temporal`, as the others carry `String` and `JsDate`. The orchestrator first claimed that today's `Date` and `Time` broke ADR 257's naming rule; that was wrong. ADR 257 suffixes only the second codec of a stored type, so this is a refinement of the rule, recorded as an amendment. - -**Defaults need no setup.** The operator's rule: pick a default, and the other options must be possible. `DateTime` defaults to a JavaScript `Date`: it works everywhere and matches Prisma 7. `Date`, `Time` and `Duration` default to ISO 8601 strings, because JavaScript has no built-in calendar-date or time-of-day value, and a `Date` there causes the wrong-day bug. - -**Plain strings, not a Prisma value class or a branded string.** A class owned by Prisma was rejected: users would learn another date API, it would not survive JSON across a server-client boundary, and Temporal will make it redundant. A branded string (`string & { __brand }`) was rejected: almost every written value starts as a plain string from a form or a request, so every write would need a helper or a cast, to catch a mistake the codec already reports at runtime. - -**`Duration` defaults to an ISO duration string.** Today's `{ months, days, micros: bigint }` breaks `JSON.stringify`, and the contract already stores interval defaults as ISO durations. The object stays available as `as: parts`. - -**`@db.*` does not come back.** Storage options are arguments in the type position, which agrees with ADR 241. - -**`contract infer` and `contract print` write convenience types,** with arguments only where they differ from the defaults, so a schema the tools write reads like one a user would write. Only `Temporal.PlainDateTime`, which no convenience type covers, prints as its explicit type. - -**The Prisma 7 reader gives back `Date`.** Prisma 7's `DateTime` is a `timestamp(3)` column read as a `Date`. Mapping it to `DateTime(storage: timestamp)` keeps an upgraded app's code compiling. This reverses the choice made when inferred and Prisma 7 columns moved to string types, which rejected `Date` only because no `timestamp` `Date` codec existed. - -**TypeScript helpers take one object.** `type.DateTime({ as: 'string', precision: 6 })`, with one overload per row of the table, so a call is typed as the explicit type it picks and an unlisted combination does not compile (settled 2026-10-23). Explicit constructors stay positional. - -**PostgreSQL only.** A convenience name must mean the same on every target. SQLite's `DateTime` and MongoDB's `Date` already use the names for explicit types, and MongoDB's `Date` is a moment in time. Resolving them is TML-3714. - -## Alternatives rejected - -- Rename every scalar after its application meaning, with no explicit layer (reverses ADR 257; loses the precise form). -- One parameterized codec per concept (breaks the one-data-type-per-codec rule). -- Type aliases with arguments (aliases attach attributes, they don't choose types). -- Bring back `@db.*` for storage options (the decision to select storage only in type position stands). -- Names from the Temporal API (`Instant`, `PlainDate`): library vocabulary, not domain vocabulary. -- A Prisma-owned date and time value class, or a branded string (see above). -- Document the explicit types and add no convenience layer (no default; every user must understand every choice first). -- Keep `Timestamptz` and `Timestamp` unsuffixed and rename only `Date` and `Time` (two naming patterns in one layer). - -## Open questions - -None. Settled on 2026-10-22: - -- The string codecs keep their ids when they switch to ISO; it is a bug fix, and Prisma 8 keeps no backward compatibility before RC. -- Prisma requires `DateStyle=ISO` on its connections ("we get to dictate our connection requirements"); the codecs convert any time zone offset themselves. -- Renamed explicit types get no deprecated aliases before RC. -- No startup check for a missing Temporal: Temporal is an opt-in, and the first-use error names the fix. Fields are typed from `temporal-spec`, a types-only package, never from the polyfill, which is too large to include. -- `prisma7Schema` must map Prisma 7's `@default(now())` to `@default(now()) @onCreate(now())`; omitting the client-side default was a bug. - -## References - -- ADR 279 and ADR 241, 254, 257, 275. -- Architecture overview: "Domain-first surfaces", "Explicit over implicit". -- The research report (private artifact, 2026-10-21). diff --git a/projects/datetime-convenience-types/handover.md b/projects/datetime-convenience-types/handover.md deleted file mode 100644 index 67279707b25e..000000000000 --- a/projects/datetime-convenience-types/handover.md +++ /dev/null @@ -1,83 +0,0 @@ -# Handover — Convenience date and time types - -Written 2026-10-23 by the agent `thales-11`, before a rate limit. Read this whole file, then the transcript, before acting. - -**Transcript (full context, including every ruling of Will's in his own words):** `/Users/will/.claude/projects/-Users-will-Projects-prisma-orm--claude-worktrees-prisma8-datetime-usability-5a9ffb/a52854a5-d212-46e3-996d-71bf5a31c118.jsonl`. It is long; filter Will's own messages with `jq -r 'select(.type=="user" and (.message.content|type)=="string") | .message.content' `. - -Linear: project "Convenience date and time types" (TML-3709). Repository `prisma/orm`. Push only through the `bot` remote (`git@github-wmadden-electric:...`); commit with `git commit -s --trailer "Signed-off-by: Will Madden "` and end messages with `Co-Authored-By: Claude Opus 5.5 `. - -## What the project is - -PostgreSQL date and time fields in Prisma 8 had a dozen database-named "explicit" types (`TimestamptzJsDate`, `TimestamptzTemporal`, `DateString`, …), each naming the database type and the JavaScript value read, and no obvious one to start with. The project adds **convenience types**: a general mechanism where a target declares a type named for what a field means (`DateTime`, `Date`, `Time`, `Duration`), whose options pick exactly one explicit type from a table, with defaults that need no setup. The contract stores only the explicit type. The tools (`contract infer`, `contract print`, the Prisma 7 schema reader, `orm init`) write the convenience form. - -The design record is **ADR 279 — Convenience types choose an explicit type from their arguments** (`docs/architecture docs/adrs/`). It was just rewritten as a general mechanism with PostgreSQL dates as the first use. Read it first. The project spec, plan and per-PR specs are in this folder. - -```prisma -model Order { - paidAt DateTime // exactly TimestamptzJsDate(3) - settledAt DateTime(as: temporal) // exactly TimestamptzTemporal - legacyAt DateTime(storage: timestamp) // exactly TimestampJsDate(3) - shipBy Date // exactly DateString -} -``` - -## Settled decisions — do not reopen - -Will ruled each of these; quotes are in the transcript. - -- Two kinds of type: explicit types (one codec each, named after the database, suffix for the value when a database type has several codecs) and convenience types (a framework descriptor `kind: 'typeChoice'` with option arguments, handed-on arguments and a `choices` table). Not a parameterized codec, not an alias, not the preset select machinery. `@db.*` never comes back. -- Options are **`as:`** (value read: `jsDate`, `temporal`, `string`, `parts`) and **`storage:`** (database type). Renamed from `value:`/`column:` on 2026-10-23; values unchanged. TypeScript helpers take **one object**: `type.DateTime({ as: 'string', precision: 6 })`. Explicit constructors stay positional. -- Defaults: `DateTime` = `TimestamptzJsDate(3)`; `Date`, `Time`, `Duration` = ISO 8601 strings. No polyfill needed. Temporal TypeScript types come from `temporal-spec` (types only); the polyfill is never bundled. -- No backward compatibility before RC: string codecs keep their ids when they switch to ISO; renames are outright. -- `DateStyle=ISO`: the PostgreSQL target declares it as `sessionSettings`; the driver ensures it on every connection (startup option on connections it opens, `set_config` on supplied pools/clients before first use and after exclusive release). Recorded in ADR 159. -- Prisma 7 reader: `@default(now())` becomes `@default(now()) @onCreate(now())`. Prisma 7 `DateTime` becomes `TimestampJsDate(3)` (printed `DateTime(storage: timestamp)`). -- `contract infer` always takes the default path where possible. An omitted precision compares equal to 6 (data type normal form); planner postcheck catalog text uses the precision as written (`catalogParams`). -- The `Date` codecs (`pg/timestamptz-date@1`, `pg/timestamp-date@1`, MySQL datetime) refuse in `fromDataTypeValue` a stored value with digits below a millisecond (ADR 254's rule that a codec refuses what it cannot hold exactly). Rows (`fromWire`) keep dropping the digits. Infer keeps an exactness check (beside its JSON one) and prints such a default as raw SQL; the Prisma 7 binding writes refused defaults via `refusedLiteralDefault`. `db verify` cannot see a sub-millisecond default difference on a `Date` column — accepted and documented. -- Scope: PostgreSQL only. SQLite, MySQL, MongoDB convenience types are TML-3714; other scalars TML-3715. - -## The PR stack (all awaiting Will's review — open no new PRs until these are cleared) - -| PR | Branch | Base | State | -| --- | --- | --- | --- | -| #30834 design | `tml-3709-plan-datetime-convenience-types` | `main` | pushed; ADR 279 rewritten (general); description rewritten | -| #30851 ISO strings (TML-3418) | `tml-3418-iso-string-date-time-codecs` | design | pushed, last CI green before the latest DateStyle commit 5c16f01456; check CI | -| #30839 Prisma 7 now() (TML-3719) | `tml-3719-prisma7-now-default-generator` | design | pushed, green | -| #30844 explicit types (TML-3711) | `tml-3711-explicit-date-time-types` | design | pushed, green | -| #30847 convenience types (TML-3712) | `tml-3712-convenience-date-time-types` | #30844 | pushed PR branch still has `value:`/`column:`; rename in progress (below) | -| #30858 tools (TML-3713) | `tml-3713-tools-write-convenience-types` | #30847 (merges #30839 in) | pushed PR branch still has old names; rename and three fixes in progress | - -Worktrees live under the shaping worktree: `wip/slices/`. `wip/slices/final-review` is a local-only branch merging everything (never push it). - -## In flight when this was written - -An implementer was renaming `value:`→`as:`, `column:`→`storage:` and making the TypeScript type-choice helpers take an object, first on #30847's branch, then merging the design branch (new ADR 279 file name: take it as is, delete the old file, update every link to the old name/title), then merging into #30858's branch and updating infer/print/Prisma 7/init expectations, fixtures, docs and upgrade instructions. Then three fixes on #30858's branch: -1. `timestampNow` for the Date codecs rounds down to the column precision (Prisma 7 `@db.Timestamp(0) @updatedAt` generated `.700`, stored `:01`); PGlite test for precision 0–2. -2. `RUNTIME.TEMPORAL_UNAVAILABLE` fix text (Postgres target `errors.ts` ~line 91) and its error-reference entry: remove `temporal.createdAtString()`, list every Temporal codec incl. `pg/timestamp-instant@1`, point to the convenience defaults. -3. `upgrade-instructions/pending/tools-write-convenience-types/app/instructions.md` ~37–39: don't say Prisma 7 date/time strings are "unchanged" (the ISO PR changes them). - -Its work was checkpointed to backup branches **`bot/wip/tml-3712-as-storage`** and **`bot/wip/tml-3713-as-storage`** (see "Status of the backup branches" at the end). Resume from those: verify, finish, run gates, then fast-forward or push the PR branches. - -## Remaining work, in order - -1. Finish the rename and the three fixes above; run the gates (package tests with coverage/lint/typecheck for touched packages, the affected integration files and journeys, `pnpm fixtures:check`, `lint:deps`, `lint:ratchets`, `lint:docs`, `check:upgrade-coverage --mode pr --prev `). Push #30847 and #30858; watch CI. -2. Update the descriptions of #30834, #30847 and #30858 for `as:`/`storage:`, the object form and the general ADR (current bodies are in the gitignored `wip/pr-rewrite/*.new.md` of the shaping worktree; regenerate from the PRs if gone: `gh pr view --repo prisma/orm --json body`). `gh pr edit` fails with the bot token; use `gh api -X PATCH repos/prisma/orm/pulls/ -F body=@file`. -3. Run `/drive-code-review` (no walkthrough) on the final combined result again after the rename, as Will asked; the last one found 7 issues, all now fixed or queued (see transcript). -4. When Will approves a PR: turn on auto-merge (squash); stacked branches rebase with `--onto` after a squash merge; close the Linear ticket with a comment. Merge conflict to expect between #30851 and #30844 in `date-codecs.ts` `decodeDate`: keep `requireIsoDateStyle` inside the shared `decodeDate`; `date-style-every-codec.test.ts` fails otherwise. The convenience journey's `textAt`/`localAt` values become `2024-02-29T10:00:00.123456Z` and `09:30:00+02:00` when #30851 and #30847 meet. -5. Close-out after all merge: retro, docs migration, delete this `projects/` folder. - -## How Will wants you to work (from his CLAUDE.md and this session) - -- Name yourself first (`bash ~/.claude/scripts/agent-name.sh`), prefix the session title, put `Agent: ` in PRs. -- **Read the design docs before any design opinion** (ADRs in full including Alternatives, glossary, vision). Before instructing an implementer on a design change, research the code and records yourself; say when a proposal reverses a recorded decision. Will was furious when I relayed a rework ADR 254 had rejected. -- Plain, short English; no ADR numbers or internal labels without saying what they mean; no invented jargon; banned words in his CLAUDE.md. -- ADRs and PR descriptions: open with a grounding example, lead with the decision, build up step by step, end with alternatives; ADRs carry no tickets, slices or history. -- Write docs, ADRs and PR text yourself; subagents implement and review. Subagents always `model: "opus"`. -- Decide routine and representation calls yourself; ask Will only for real decisions, with a recommendation. -- Don't open new PRs while the project's PRs await review. No session-proposal cards; follow-ups go to Linear. -- Never kill processes by pattern. Never re-run CI without Will's go-ahead; report flakes. Stay inside the worktree. - -## Status of the backup branches - -- **`bot/wip/tml-3712-as-storage` at `b6bb9ffad6`** (WIP commit, signed off; local `tml-3712-convenience-date-time-types` is the same). Done: the `as:`/`storage:` rename everywhere on that branch (declarations, tests in every package, the integration file, parity test, journey, `scalar-types.md`, `error-reference.md`, Serverless guide, `gotchas.md`, ADR 275 note, `skills/prisma-8` contract and debug references, pending upgrade instruction, READMEs), and the TypeScript object form (one overload per row; options required only when not the default; unknown key, unlisted combination, unaccepted argument and positional calls fail to compile, with negative type tests; runtime `typeChoiceArgumentsFromObject` in framework-components converts the object to the argument list). The no-row refusal now reads `Argument "storage" of Time cannot be timetz with as: temporal; write one of: time.` Package tests, lint, typecheck, coverage, integration, journeys, `lint:deps`, `lint:ratchets`, `lint:docs` and `check:upgrade-coverage` passed (a few timing tests in language-server and the facade timed out under a load average of 70–140; unrelated files). -- **Not done on it:** `pnpm fixtures:check` fails: its diff is in `skills/prisma-8/references/contract.md` (the "Date and time types" and "Temporal columns" paragraphs), so some generated copy of the skill reference disagrees with the edited source; investigate (`pnpm fixtures:check` output was in the gitignored `wip/gates/` of that worktree). Then amend the WIP commit into a proper one, merge the design branch (new ADR 279 file), fix links, push the PR branch. -- **`bot/wip/tml-3713-as-storage` does not exist:** the tools branch work (merge, rename in what the tools write, and the three fixes) was never started. `bot/tml-3713-tools-write-convenience-types` is unchanged. diff --git a/projects/datetime-convenience-types/plan.md b/projects/datetime-convenience-types/plan.md deleted file mode 100644 index 45090e7cf6ab..000000000000 --- a/projects/datetime-convenience-types/plan.md +++ /dev/null @@ -1,60 +0,0 @@ -# Plan — Convenience date and time types - -**Spec:** [`spec.md`](./spec.md) · **Linear:** [Convenience date and time types](https://linear.app/prisma-company/project/convenience-date-and-time-types-6a969ca1c76e) - -## Summary - -Five slices. Slices 1, 2 and 3 are independent and can run in parallel. Slice 4 builds on slice 3, and slice 5 on slices 2 and 4. - -Five is one over the usual ceiling. Slices 1 and 2 are bug fixes the research found that any naming would need; they share the project because they serve the same purpose (date and time values that are predictable), not because the other slices depend on them. A startup check for a missing Temporal was planned as slice 2 and dropped (2026-10-22): Temporal values are now an explicit opt-in, and its type half moved into slice 3. - -## Slices - -| # | Slice | Linear | Builds on | Hands to | -| --- | --- | --- | --- | --- | -| 1 | String date and time types read ISO 8601 that does not depend on the server | [TML-3418](https://linear.app/prisma-company/issue/TML-3418) | — | String codecs whose value is the ADR 254 stored form, for `Date`, `Time` and `Duration` to default to | -| 2 | `prisma7Schema` maps `@default(now())` to `@default(now()) @onCreate(now())` | [TML-3719](https://linear.app/prisma-company/issue/TML-3719) | — | Adopted Prisma 7 schemas write the client's UTC time on create, as Prisma 7 did | -| 3 | Explicit types carry a `Temporal` suffix; `timestamp` and `interval` get the missing codecs | [TML-3711](https://linear.app/prisma-company/issue/TML-3711) | — | A complete explicit layer: every cell of the spec's decision 3 table has an explicit type, and `TimestamptzTemporal`, `TimestampTemporal`, `DateTemporal`, `TimeTemporal`, `IntervalParts`, `IntervalString` exist | -| 4 | PSL convenience types `DateTime`, `Date`, `Time` and `Duration` | [TML-3712](https://linear.app/prisma-company/issue/TML-3712) | 3 | Type constructors that choose their codec; the four convenience types; the TypeScript resolver evaluating the choice | -| 5 | `contract infer`, `contract print`, the Prisma 7 reader and `orm init` write the convenience types | [TML-3713](https://linear.app/prisma-company/issue/TML-3713) | 2, 4 | Adopted and new projects read `Date` timestamps with no polyfill | - -### Slice 1 — ISO strings - -`pg/timestamptz-string@1`, `pg/timestamp-string@1`, `pg/date-string@1`, `pg/time-string@1` and `pg/timetz@1` return the ISO 8601 form ADR 254 stores, on any server `TimeZone` and `DateStyle`, BC years and years past 9999 included; `infinity` stays `infinity`. Covers the related case in the ticket: a database `now()` default on a `timestamp` column and the client's `now()` generator store the same clock. Amends ADR 254's "Date and time types" paragraph on text codecs. - -### Slice 2 — Prisma 7 `now()` defaults - -`prisma7Schema` reads Prisma 7's `@default(now())` as the database default `now()` plus the client generator `@onCreate(now())` resolved from the column's codec. Today it writes the database default only, so on a server whose `TimeZone` is not UTC a create through Prisma 8 stores local time where Prisma 7 stored UTC. A bug fix independent of the naming work. - -### Slice 3 — The explicit layer - -Renames outright, with no deprecated aliases before RC: `Timestamptz` → `TimestamptzTemporal`, `Timestamp` → `TimestampTemporal`, `Interval` → `IntervalParts`. New explicit names `DateTemporal` and `TimeTemporal` beside today's `Date` and `Time`, which keep their meaning until slice 4. New codecs and explicit types: `TimestampJsDate` (`pg/timestamp-date@1`, a `timestamp` read as a `Date`, UTC), `TimestampInstant` (`pg/timestamp-instant@1`, a `timestamp` read as a `Temporal.Instant`, UTC) and `IntervalString` (`pg/interval-string@1`). The Temporal codecs and generators take their types from `temporal-spec`, so a Temporal field is never typed `any`; no polyfill code is added. Each new codec passes the codec conformance kit and the three codec probes (dod.md). - -### Slice 4 — The convenience types - -A new framework declaration, the type choice, picks one explicit type constructor from a table by its `as:` and `storage:` options; every reader of a field's codec resolves the call first. The four types with the spec's choices and defaults; an unlisted combination is an error at the argument. `DateTime`, `Date` and `Time` take over their names (breaking, with upgrade instructions). The TypeScript `type.*` helpers take one object and are typed per row. Hover text and completion list each type's choices. ADR 254, 257 and 275 get notes pointing to ADR 279. - -### Slice 5 — The tools write convenience types - -`contract infer` and `contract print` write convenience types, with arguments only where they differ from the defaults, and the explicit type only where no convenience type fits. The Prisma 7 reader maps `DateTime` to `DateTime(storage: timestamp)` and `@db.*` date and time columns to the matching convenience form. `prisma orm init` writes `DateTime`. The contributed `temporal.CreatedAt` and `temporal.UpdatedAt` aliases are rebuilt on `DateTime` (`DateTime @onCreate(now())`, and `@onUpdate(now())` for the second), and their `String` and `JsDate` variants are removed, per ADR 279; contracts that use them change codec, so the upgrade instruction says to re-emit and `db sign`. Upgrade instructions: re-emit and `db sign` for adopted projects. Update slice 2's upgrade instruction (`upgrade-instructions/pending/prisma7-now-default-on-create/`), which names the type the reader picks today (`TimestampString(3)`). - -## Cross-slice notes - -- Slices 1 and 4 both touch what string date and time values look like. The convenience-types journey (`convenience-date-time-types-without-temporal.e2e.test.ts`) asserts today's display text for `textAt` (`2024-02-29 10:00:00.123456+00`) and `localAt` (`09:30:00+02`). Whichever of the two slices merges second changes them to `2024-02-29T10:00:00.123456Z` and `09:30:00+02:00`. - -## Cross-slice notes - -- Slices 1 and 4 both touch what string date and time values look like. The convenience-types journey (`convenience-date-time-types-without-temporal.e2e.test.ts`) asserts today's display text for `textAt` (`2024-02-29 10:00:00.123456+00`) and `localAt` (`09:30:00+02`). Whichever of the two slices merges second changes them to `2024-02-29T10:00:00.123456Z` and `09:30:00+02:00`. - -## Dependencies - -- **Field-preset removal project.** Its remaining slices remove the presets; slice 5 here rebuilds its contributed `temporal.CreatedAt` and `temporal.UpdatedAt` on `DateTime`. -- **PostgreSQL and SQLite rename project** (not started). Must not remove `DateTime` and must take slice 3's names. Its spec gets a note in this project's shaping PR. -- **Release.** Users get the convenience types with the first `@prisma/orm-postgres` release after slice 4 merges. - -## Close-out (required) - -- [ ] Verify the project DoD in [`spec.md`](./spec.md). -- [ ] ADR 279 accepted; `docs/reference/scalar-types.md` describes both layers. -- [ ] Strip repo-wide references to `projects/datetime-convenience-types/**`. -- [ ] Delete `projects/datetime-convenience-types/`. diff --git a/projects/datetime-convenience-types/slices/01-iso-strings/spec.md b/projects/datetime-convenience-types/slices/01-iso-strings/spec.md deleted file mode 100644 index 1abc7d6f62e4..000000000000 --- a/projects/datetime-convenience-types/slices/01-iso-strings/spec.md +++ /dev/null @@ -1,55 +0,0 @@ -# Slice 1 — String date and time types read ISO 8601, the same on every server - -**Linear:** [TML-3418](https://linear.app/prisma-company/issue/TML-3418) · **Parent:** [project spec](../../spec.md), decision 8 · **Design record:** ADR 279, item 9 - -## At a glance - -A `TimestamptzString` column holding the instant `2024-01-01T00:00:00Z`: - -| | Today | After | -| --- | --- | --- | -| Read on a UTC server | `2024-01-01 00:00:00+00` | `2024-01-01T00:00:00Z` | -| Read on a server set to `Europe/Berlin` | `2024-01-01 01:00:00+01` | `2024-01-01T00:00:00Z` | -| Year 44 BC | `0044-03-15 12:00:00+00 BC` | `-000043-03-15T12:00:00Z` | -| Inside an `include` (JSON row) | the same display text | the same ISO text | - -Every string codec's application value is the stored form ADR 254 defines for its data type ("Date and time types"): `pg/timestamptz-string@1`, `pg/timestamp-string@1`, `pg/date-string@1`, `pg/time-string@1` and `pg/timetz@1`. What the application writes in that form reads back unchanged. - -## Chosen design - -1. **The codecs read with the stored-form readers.** `fromWire` turns PostgreSQL's ISO-DateStyle text into the stored form with the existing readers in `packages/3-targets/3-targets/postgres/src/core/data-types.ts` (`pgDateStoredText`, `pgTimeStoredText`, `pgTimetzStoredText`, `pgTimestampStoredText`, `pgTimestamptzStoredText`), which already convert any time zone offset to UTC for `timestamptz`, ` BC` years and long years to ISO, and keep `infinity`. `pg/timestamptz-date@1` already reads wire text this way. `toWire` keeps converting the stored ISO year into PostgreSQL's year text (`postgresYearText`). `fromDataTypeValue` is the stored value; `toDataTypeValue` requires the stored form. -2. **`DateStyle=ISO` is a connection requirement the target declares and the driver ensures** (operator ruling, 2026-10-23). The PostgreSQL target states, as data on its descriptor, the session settings its codecs need (`DateStyle=ISO`). The PostgreSQL driver ensures every connection it hands to the runtime satisfies them, whatever its binding: a URL, a `pgPool` or a `pgClient` the user supplies, and the Supabase runtime's connections after its `RESET ALL`. How the driver ensures it (a startup option, a setting applied when a connection is first acquired or after a reset, or both) is the driver's own implementation detail; the codecs never depend on how. The codecs still refuse non-ISO text with `RUNTIME.DATE_STYLE_NOT_ISO`, which now means the driver could not ensure the requirement. No `TimeZone` setting is needed: the `timestamptz` codecs convert any offset to UTC and `timetz` keeps its offset. -3. **The `now()` generators for these codecs write the stored form** (`timestampStringNow`, `timestamptzStringNow`, `dateStringNow`, `timeStringNow`, `timetzNow` in `date-time-now-generators.ts`), so a generated value reads back as written. -4. **Codec ids, the contract and stored defaults do not change.** It is a bug fix; nothing keeps the old format before RC. -5. **Values PostgreSQL holds beyond the stored form's range** (time `24:00:00`, dates after `+275760-09-13`, seven-digit years) are read as ISO-shaped text, not refused: the string types exist to carry values the other codecs cannot. The implementer documents the exact text for each. -6. **`enumRefusal` on these codecs is re-examined.** Where it existed only because a query returned display text, it goes; the enum test that places every codec in exactly one group decides. - -## Coherence rationale - -One reviewable change: the five string codecs, their generators, the runtime connection option and the docs that describe the old text. The JSON path is covered by the same `fromWire`, because the projections already cast to text. - -## Scope - -**In:** the five codecs, their `now` generators, the PostgreSQL runtime driver's connection setup, the error for a non-ISO DateStyle (error-reference entry), tests, the codec conformance cases, ADR 254's "Date and time types" and "Codecs" paragraphs that describe the old text, `docs/reference/scalar-types.md`, the codec authoring guide, `gotchas.md`, the `prisma-8` skill references, and an `app` upgrade instruction. - -**Out:** renaming any type (slice 3); the Prisma 7 `now()` mapping (slice 2); a database `now()` default on a `timestamp` column, which PostgreSQL evaluates in the session's time zone (slice 2 makes adopted schemas fill the value on the client instead). - -## Pre-investigated edge cases - -| Case | Disposition | -| --- | --- | -| Supabase runs `RESET ALL` when it releases a role scope | Startup option survives it; test through the Supabase runtime | -| `@prisma/dev` / PGlite shares one session between clients | A startup option or per-connection setting must not leak a changed setting to other clients; the tests already assume UTC and ISO | -| A user-supplied pool whose server uses `DateStyle=German` | Refused on read with the new error, not mis-parsed | -| Transaction-mode poolers that reject startup options | Out of scope to work around; the requirement is documented | -| `infinity` / `-infinity` | Unchanged | -| Tests that assert display text (`temporal-string-codec-roundtrip.integration.test.ts`, `date-time-text.integration.test.ts`, `now-generators.test.ts`, `prisma7-dates-without-temporal.e2e.test.ts`, conformance `cases.ts`) | Updated to the ISO form; the two tests that assert session leakage become tests that it no longer leaks | - -## Slice-specific done conditions - -- A `TimestamptzString` value written in ISO form reads back identical on a session with `TimeZone=Asia/Tokyo`, flat and inside an `include`, and a German-DateStyle user connection fails with the new error. - -## Dispatch plan - -1. **Codecs, generators and connection requirement** — outcome: the at-a-glance table holds on PostgreSQL, including JSON rows, BC years, long years and a non-UTC session; a non-ISO DateStyle is refused. Gates: `pnpm --filter @internal/target-postgres test`, `pnpm --filter @internal/adapter-postgres test`, `pnpm --filter @internal/driver-postgres test`, the testkit's conformance and date-time-text integration files, `pnpm typecheck` for the touched packages, `pnpm lint` for each touched package, `pnpm fixtures:check`. -2. **Docs and upgrade instruction** — outcome: every doc listed in Scope describes ISO values; ADR 254 amended in place; upgrade instruction recorded per `skills-contrib/record-upgrade-instructions`. Gates: `pnpm lint:docs`, `pnpm check:upgrade-coverage --mode pr`. diff --git a/projects/datetime-convenience-types/slices/02-prisma7-now-defaults/spec.md b/projects/datetime-convenience-types/slices/02-prisma7-now-defaults/spec.md deleted file mode 100644 index 18e9f0ebb65e..000000000000 --- a/projects/datetime-convenience-types/slices/02-prisma7-now-defaults/spec.md +++ /dev/null @@ -1,50 +0,0 @@ -# Slice 2 — `prisma7Schema` maps `@default(now())` as Prisma 7 behaved - -**Linear:** [TML-3719](https://linear.app/prisma-company/issue/TML-3719) · **Parent:** [project spec](../../spec.md), decision 10 - -## At a glance - -```prisma -// Prisma 7 schema, read by prisma7Schema('./schema.prisma') -model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) -} -``` - -| | Today | After | -| --- | --- | --- | -| Contract | database default `now()` only | database default `now()` and the `onCreate` generator `now` resolved from the column's codec | -| `create` through Prisma 8, server `TimeZone=Asia/Tokyo` | the database fills `createdAt` with Tokyo wall-clock time | the client writes the current UTC time | -| `INSERT` from outside Prisma | the database default | the database default (unchanged) | - -Prisma 7 filled `@default(now())` on the client and also wrote the database default. Omitting the client generator is a bug. - -## Chosen design - -- The Prisma 7 reader (`packages/2-sql/2-authoring/contract-prisma7`) lowers `@default(now())` on a date or time field to both the database default and an `onCreate` generator, through the generator function registry, exactly as Prisma 8 PSL's `@default(now()) @onCreate(now())` lowers. It resolves the generator by the field's codec, so it is correct for whatever codec the reader picks today and after slice 5 changes it. -- `@default(now())` together with `@updatedAt` keeps the database default and both generators. -- The storage hash does not change (the column and its default are the same); the execution hash does, because the contract gains a generator. Adopted projects re-emit and commit the contract; there is no migration and no `db sign`, because the database marker records only the storage and profile hashes. - -## Scope - -**In:** the Prisma 7 reader's default lowering, its tests, the Prisma 7 adoption journey test and `examples/prisma7-adoption` if its contract changes, the `contract-prisma7` README, an `app` upgrade instruction, and an empty `extension` declaration for the Postgres README edit. - -**Out:** any change to which codec the reader picks (slice 5); `contract infer`, which reads a database and cannot know the default came from Prisma 7. - -## Pre-investigated edge cases - -| Case | Disposition | -| --- | --- | -| `@default(now())` on a `@db.Date` or `@db.Time` field | Generator resolved by codec, like any other date or time codec | -| Optional field with `@default(now())` | Same mapping | -| `@default(now())` and `@updatedAt` on one field | Database default plus `onCreate` and `onUpdate` generators | -| A create that supplies `createdAt` explicitly | The supplied value wins, as for any generator | - -## Slice-specific done conditions - -- A Prisma 7 schema adopted with `prisma7Schema`, on a session with `TimeZone=Asia/Tokyo`, stores UTC for a create through Prisma 8, and a raw `INSERT` still gets the database default. - -## Dispatch plan - -1. **Reader mapping, tests, journey and upgrade instruction** — outcome: the at-a-glance table holds. Gates: `pnpm --filter @internal/contract-prisma7 test` (or the package's real name), the Prisma 7 journey test files, `pnpm typecheck` and `pnpm lint` for touched packages, `pnpm fixtures:check`, `pnpm check:upgrade-coverage --mode pr`. diff --git a/projects/datetime-convenience-types/slices/03-explicit-types/spec.md b/projects/datetime-convenience-types/slices/03-explicit-types/spec.md deleted file mode 100644 index 95f147f0f4b8..000000000000 --- a/projects/datetime-convenience-types/slices/03-explicit-types/spec.md +++ /dev/null @@ -1,62 +0,0 @@ -# Slice 3 — The explicit date and time types: Temporal suffix, missing codecs, Temporal types from `temporal-spec` - -**Linear:** [TML-3711](https://linear.app/prisma-company/issue/TML-3711) · **Parent:** [project spec](../../spec.md), decisions 6, 7 and 9 · **Design record:** ADR 279, items 6 and 7 - -## At a glance - -```prisma -model Event { - id Int @id - at TimestamptzTemporal(3) // was Timestamptz(3): timestamptz(3), Temporal.Instant - local TimestampTemporal // was Timestamp: timestamp, Temporal.PlainDateTime - legacyAt TimestampJsDate(3) // new: timestamp(3), a JavaScript Date, stored as UTC - utcAt TimestampInstant(6) // new: timestamp(6), a Temporal.Instant, stored as UTC - day DateTemporal // new name beside Date: date, Temporal.PlainDate - opens TimeTemporal(0) // new name beside Time: time(0), Temporal.PlainTime - period IntervalParts // was Interval: interval, { months, days, micros } - span IntervalString // new: interval, "P1Y2M" -} -``` - -`Timestamptz`, `Timestamp` and `Interval` are errors after this slice, with the upgrade instruction giving the rewrite. `DateTime`, `Date` and `Time` keep today's meaning until slice 4. - -## Chosen design - -1. **Renames are outright.** No deprecated aliases before RC. The old names are removed from `postgresPslTypeConstructors` (`packages/3-targets/3-targets/postgres/src/core/type-constructors.ts`); every `.prisma` file, PSL string in tests, doc and skill reference moves to the new names. `DateTemporal` and `TimeTemporal` are added; `Date` and `Time` stay as second names until slice 4. -2. **Three codecs, each a complete citizen** (every file the codec anatomy needs: id constant, codec class, descriptor with data type, traits, JSON projection, registration, `codec-type-map`, `OutputTypeMap` where precision-refined, aggregates, `now` binding, PSL constructor, adapter column helper, conformance and aggregate-matrix cases, enum eligibility): - - `pg/timestamp-date@1`, `TimestampJsDate(p)`: data type `pg/timestamp`; reads `timestamp` text as UTC (append `Z` before `new Date`), writes the stored form without an offset; refuses `infinity` naming `TimestampString`; `now` → `timestampNow`. - - `pg/timestamp-instant@1`, `TimestampInstant(p)`: data type `pg/timestamp`; reads as `Temporal.Instant` in UTC, writes the UTC wall-clock time without an offset; refuses `infinity`; needs a global `Temporal` like the other Temporal codecs; `now` → `instantNow`. - - `pg/interval-string@1`, `IntervalString(p)`: data type `pg/interval`; application value is the ISO 8601 duration ADR 254 stores; reads every `IntervalStyle` the existing interval reader handles; `keepsWrittenForm: true` (equal intervals can differ in form); `sum`/`avg` rows like `IntervalParts`; no `now`. -3. **`inferred` marks do not move** except `Interval`'s, which moves to `IntervalParts`. No new codec is inferred. -4. **Fix the `Interval(p)` output type** while renaming: today a precision makes it a branded string although the codec reads `PgInterval` (`packages/3-targets/3-targets/postgres/src/exports/codec-types.ts`). -5. **Temporal types from `temporal-spec`.** The Temporal codecs, the Temporal `now` generators and every exported signature that names a `Temporal` type import it with `import type { Temporal } from 'temporal-spec'`; `temporal-spec` becomes a dependency of the PostgreSQL target (types only). The emitted `.d.mts` then names the type through a real import, so a project without the polyfill's global declarations gets precise types, not `any`. No runtime import of `temporal-polyfill` is added anywhere; verify the built runtime entries contain none. -6. **`contract print`** writes the new names; check it never prints a removed name. - -## Coherence rationale - -One reviewable change: the explicit layer becomes complete and uniformly named. The three codecs follow one template each, and the renames are mechanical. The large fixture regeneration (about 268 `contract.d.ts` files gain aggregate rows) is mechanical and called out in the PR. - -## Scope - -**In:** everything above, the TS adapter column helpers (`timestampJsDateColumn`, `timestampInstantColumn`, `intervalStringColumn`, renamed helpers where they carry an old type name), docs (`docs/reference/scalar-types.md` tables, the codec authoring guide, package READMEs, the `prisma-8` skill references), and an `app` upgrade instruction with the rename table. - -**Out:** the convenience types and codec choice by arguments (slice 4); `DateTime`, `Date`, `Time` changing meaning (slice 4); `contract infer` / Prisma 7 reader output (slice 5); the `field.temporal.*` presets (being removed by the field-preset removal project). - -## Pre-investigated edge cases - -| Case | Disposition | -| --- | --- | -| Precision above 3 on `TimestampJsDate` | Refused at authoring by the parameter bound, since a `Date` holds milliseconds; decide and test | -| Years before 1 and after 9999 | Through `isoYearText` / `postgresYearText`, as the `timestamptz` codecs do | -| A `timestamp` value PostgreSQL holds beyond `Date`/`Temporal` range | Refused on read, naming `TimestampString` | -| Supabase's contract (`packages/3-extensions/supabase/src/contract/contract.prisma`, 78 `Timestamptz` lines) | Renamed; its committed contract regenerated if it changes; storage hash must not change | -| `temporal-spec` version | Match the version `temporal-polyfill` 1.0.x re-exports | - -## Slice-specific done conditions - -- A project without the polyfill's global type declarations type-checks a `TimestamptzTemporal` field as `Temporal.Instant` (a `not.toBeAny()` type test), and each new codec round-trips on PostgreSQL through the conformance kit. - -## Dispatch plan - -1. **Renames and the Temporal type source** — outcome: old names gone, new names resolve, `temporal-spec` types in every exported signature, `Interval(p)` typed correctly, all call sites and docs moved, fixtures regenerated. Gates: `pnpm build`, `pnpm --filter @internal/target-postgres test`, `pnpm --filter @internal/adapter-postgres test`, the SQL `contract-psl` and postgres extension package tests, `pnpm typecheck`, per-package `pnpm lint`, `pnpm fixtures:check`, `pnpm lint:deps`. -2. **The three codecs** — builds on 1. Outcome: each codec complete with conformance and aggregate cases, and the three codec probes from `drive/calibration/dod.md`. Gates: as 1, plus the codec testkit's conformance and aggregate-resolution files, plus `pnpm check:upgrade-coverage --mode pr` after the upgrade instruction lands. diff --git a/projects/datetime-convenience-types/slices/04-convenience-types/spec.md b/projects/datetime-convenience-types/slices/04-convenience-types/spec.md deleted file mode 100644 index a0b1982bede8..000000000000 --- a/projects/datetime-convenience-types/slices/04-convenience-types/spec.md +++ /dev/null @@ -1,68 +0,0 @@ -# Slice 4 — PSL convenience types `DateTime`, `Date`, `Time` and `Duration` - -**Linear:** [TML-3712](https://linear.app/prisma-company/issue/TML-3712) · **Parent:** [project spec](../../spec.md), decisions 1–5 · **Design record:** ADR 279 · **Builds on:** slice 3 (the explicit types) - -## At a glance - -```prisma -model Order { - id Int @id - paidAt DateTime // TimestamptzJsDate(3): timestamptz(3), a JavaScript Date - settledAt DateTime(as: temporal) // TimestamptzTemporal: timestamptz, Temporal.Instant - legacyAt DateTime(storage: timestamp) // TimestampJsDate(3): timestamp(3), a Date stored as UTC - textAt DateTime(as: string, precision: 6) // TimestamptzString(6) - shipBy Date // DateString: date, "2024-02-29" - opensAt Time(precision: 0) // TimeString(0): time(0), "09:30:00" - localAt Time(storage: timetz) // Timetz: timetz, "09:30:00+02:00" - period Duration // IntervalString: interval, "P1M" - parts Duration(as: parts) // IntervalParts: interval, { months, days, micros } - updatedAt DateTime @onCreate(now()) @onUpdate(now()) // generator chosen for TimestamptzJsDate -} -``` - -`Time(as: temporal, storage: timetz)` and `Date(as: jsDate)` are refused at the argument, naming the accepted choices. The contract stores only the explicit type each field becomes. - -## Chosen design - -1. **A convenience type is a new authoring descriptor kind, a type choice.** It declares its arguments (option arguments with a fixed set of bare-name values and a default each, plus value arguments passed through such as `precision`) and a table from each allowed combination of option values to the **name of an explicit type constructor** in the same namespace, with any parameter defaults for that combination (for example precision 3 when `as: jsDate`). Resolving a call looks up the combination, then instantiates the named explicit constructor with the passed-through arguments. A combination missing from the table is refused at the argument that makes it invalid, listing the values that would be accepted. - - Rationale: ADR 279 says each convenience type becomes exactly one explicit type; naming the explicit constructor makes that literal, supports defaults and refusals directly, and needs none of the field-preset `option`/`select` template machinery that the field-preset removal project deletes. ADR 279's "How it works" is updated to this shape (an orchestrator representation decision). -2. **Every reader resolves the field's call.** The PSL column resolution, the TypeScript builder's runtime and type-level resolution (`type.*` helpers), the contract-prisma7 values path, and every place that today reads a constructor's fixed `output.codecId` without the call's arguments: `@default` literal admission (`receivingDataType`) and the `@onCreate`/`@onUpdate` generator lookup (`generatorCallArm`) must use the codec the field's call resolves to, including through a `types {}` alias. The control stack checks every reachable explicit constructor at assembly. -3. **Bare names are scalars.** `DateTime`, `Date`, `Time` and `Duration` work bare and called; the scalar view treats them like any all-optional constructor. -4. **`inferred` marks stay on the explicit constructors** in this slice; a type choice is never inferred. `contract print` and `contract infer` keep writing explicit names until slice 5. -5. **The four PostgreSQL types and their tables** are exactly ADR 279's convenience types table and the project spec's decision 3, with defaults: `DateTime` → `jsDate`, `timestamptz`, precision 3 for `jsDate` else none; `Date` → `string`; `Time` → `string`, `time`; `Duration` → `string`. -6. **The old meanings move to explicit names.** Every existing Prisma 8 PostgreSQL schema in the repository that writes `DateTime`, `Date` or `Time` (examples, apps, integration fixtures, test PSL strings, docs) is rewritten mechanically to the explicit type it means today (`TimestamptzTemporal`, `DateTemporal`, `TimeTemporal`), so no committed `contract.json` or hash changes. The `Date` and `Time` second names and `DateTime`'s current constructor are removed. The upgrade instruction gives users the same rewrite and says the bare names now choose different values. -7. **TypeScript builder.** `type.DateTime(...)`, `type.Date(...)`, `type.Time(...)`, `type.Duration(...)` exist through the `type.*` helpers and take one object (`type.DateTime({ as: 'string', precision: 6 })`), with one overload per row and a type-level resolution that yields the explicit type's codec and application type for literal arguments (a type test per table row). -8. **Language server.** Hover on a convenience type shows each argument's choices as PSL writes them (bare names) and which explicit type each combination becomes; completion of a bare convenience type inserts the bare name. -9. **ADR updates in the same PR:** ADR 279 to Accepted, its "How it works" describing the type-choice descriptor; notes in ADR 254 (a type constructor may resolve to another constructor), ADR 257 (unsuffixed names belong to convenience types), ADR 275 (the contributed `temporal.*` aliases are built on `DateTime`; the `String`/`JsDate` variants are not needed). - -## Coherence rationale - -One PR: the descriptor kind and its resolution in every reader exist only to carry these four types, and the four types are untestable without it. The fixture rewrite is mechanical and keeps every contract byte-identical, which the reviewer can verify with `pnpm fixtures:check`. - -## Scope - -**In:** the type-choice descriptor (framework), its assembly checks, resolution in every reader listed above, the four PostgreSQL types, the TypeScript type-level resolution and `type.*` helpers, hover and completion, the fixture and doc rewrite, upgrade instructions, the ADR updates. - -**Out:** what `contract infer`, `contract print`, the Prisma 7 reader and `orm init` write (slice 5); SQLite and MongoDB (TML-3714); other scalars (TML-3715); completion inside a constructor's argument list (no constructor has it today). - -## Pre-investigated edge cases - -| Case | Disposition | -| --- | --- | -| Field typed by a `types {}` alias whose right-hand side is a convenience call | Resolves through the alias; `@default` and `@onCreate` use the resolved codec | -| `DateTime[]`, `DateTime?` | Same resolution as any constructor | -| `@default(timestamptz\`…\`)` on `DateTime(storage: timestamp)` | Refused by the existing cast rule (wrong tag), naming `timestamp` | -| `@onCreate(now())` on each convenience default | Generator resolved for the explicit codec (`timestampNow` for `TimestamptzJsDate`, `dateStringNow` for `DateString`, …) | -| Positional arguments, e.g. `DateTime(temporal)` | Accepted in declaration order, like any constructor | -| Precision on `Date` or `Duration` with `as: string` | Refused unless the explicit type takes it (`IntervalString(p)` does, `DateString` does not) | -| A pack declaring a choice that names a constructor no component registers | Assembly error naming the pack and the name | - -## Slice-specific done conditions - -- Every row of the at-a-glance block emits the stated explicit type's contract column (asserted equal to writing the explicit type), and a query on PostgreSQL reads the stated value type, with the defaults working in a process without a global `Temporal`. - -## Dispatch plan - -1. **The type-choice mechanism** — outcome: a test-only pack declaring a choice type resolves bare, positional and named calls, refuses an unlisted combination at the argument, and every reader in decision 2 uses the resolved codec (`@default` admission and `@onCreate` arm tests included); TypeScript type-level resolution covered by type tests. Gates: framework-components, psl-parser, contract-psl, contract-ts, family-sql package tests; typecheck; lint; `pnpm lint:deps`. -2. **The four PostgreSQL types and the rewrite** — builds on 1. Outcome: the at-a-glance block holds; existing schemas rewritten to explicit names with `pnpm fixtures:check` clean and no `contract.json` changed; upgrade instruction. Gates: target-postgres, adapter-postgres, the postgres extension, contract-prisma7 package tests; the date-time integration and print tests; `pnpm build`; `pnpm typecheck`; `pnpm fixtures:check`; `pnpm check:upgrade-coverage --mode pr`. -3. **Language server, docs and ADRs** — builds on 2. Outcome: hover shows choices and resulting explicit types; `docs/reference/scalar-types.md` describes both layers; skill references updated; ADR updates per decision 9. Gates: language-server package tests; `pnpm lint:docs`. diff --git a/projects/datetime-convenience-types/slices/05-tools-write-convenience-types/spec.md b/projects/datetime-convenience-types/slices/05-tools-write-convenience-types/spec.md deleted file mode 100644 index cfc3936b2b85..000000000000 --- a/projects/datetime-convenience-types/slices/05-tools-write-convenience-types/spec.md +++ /dev/null @@ -1,77 +0,0 @@ -# Slice 5 — `contract infer`, `contract print`, the Prisma 7 reader and `orm init` write the convenience types - -**Linear:** [TML-3713](https://linear.app/prisma-company/issue/TML-3713) · **Parent:** [project spec](../../spec.md), decision 10 · **Design record:** ADR 279, "What the tools write" · **Builds on:** slice 4 (the type choices) and slice 2 (the Prisma 7 `now()` mapping) - -## At a glance - -A database the user adopts with `contract infer`: - -```sql -CREATE TABLE event ( - at timestamptz, -- no declared precision - paid_at timestamptz(3), - legacy timestamp(3), - day date, - opens time(0), - local_at timetz, - span interval -); -``` - -```prisma -model Event { - at DateTime(precision: 6) // TimestamptzJsDate: a JavaScript Date - paidAt DateTime // TimestamptzJsDate(3) - legacy DateTime(storage: timestamp) // TimestampJsDate(3) - day Date // DateString - opens Time(precision: 0) // TimeString(0) - localAt Time(storage: timetz) // Timetz - span Duration // IntervalString -} -``` - -`contract print` writes the same forms for the same codecs; a `timestamp` column read as `Temporal.PlainDateTime` (`TimestampTemporal`), which no convenience type reaches, prints as its explicit type. - -A Prisma 7 schema read by `prisma7Schema(...)`: `DateTime` becomes `TimestampJsDate(3)` (prints `DateTime(storage: timestamp)`), `@db.Timestamptz(p)` becomes `TimestamptzJsDate(p)`, `@db.Date` `DateString`, `@db.Time(p)` `TimeString(p)`, `@db.Timetz(p)` `Timetz(p)`. - -`prisma orm init` writes `createdAt DateTime @default(now()) @onCreate(now())` and `updatedAt temporal.UpdatedAt`. - -## Chosen design - -1. **Infer always takes the default path where it can** (operator ruling, 2026-10-23: "infer should always choose the default path, where possible, since that's the simplest to see in your contract, as a user, and ought to represent the well-trodden path"). For each date and time column, infer picks the codec of the convenience type's default value for that column type, and writes the convenience call with only the arguments that differ from its defaults. The explicit type is written only where no convenience call reproduces the column. - - Consequence the docs state: a `Date` holds milliseconds, so a `timestamptz` or `timestamp` column with more than 3 digits of precision reads with the extra digits dropped, and `infinity` is refused on read. The upgrade and infer docs say to write `DateTime(as: temporal)` or `DateTime(as: string)` to keep the digits, and `DateTime(as: string)` for a column whose rows hold `infinity`. - - The one exception (operator ruling, 2026-10-27): when the default-path type cannot read the column's own default, such as `infinity` for a `Date`, infer takes the first row of the same convenience type and storage whose type can, because every row created with that default would otherwise fail to read. -2. **An omitted precision equals precision 6** on `pg/timestamptz`, `pg/timestamp`, `pg/time`, `pg/timetz` and `pg/interval` when columns are compared (the data type's normal form, ADR 254), because PostgreSQL stores microseconds either way. That lets infer write `DateTime(precision: 6)` for a column with no declared precision without `db verify` or the planner seeing a difference. The contract keeps parameters as written; stored contracts and hashes do not change. If this normal-form change would change any existing contract, hash or plan in the repository's fixtures, stop and report. -3. **The `inferred` marks move to the default explicit types:** `pg/timestamptz` → `TimestamptzJsDate`, `pg/timestamp` → `TimestampJsDate`, `pg/interval` → `IntervalString`; `DateString`, `TimeString`, `Timetz` keep theirs. Both infer and the Prisma 7 reader follow the marks, which gives the Prisma 7 table above. The guard test that forbids Temporal codecs in these maps keeps passing and gains `pg/timestamp-instant@1`. -4. **Print and infer write convenience calls through one framework helper** beside `resolveAuthoringTypeChoice`: given an explicit constructor call, find the type choice row (at the same namespace level) that names that constructor, build named arguments from the row (options from its `when`, passed-on arguments by name), drop arguments equal to the defaults, re-resolve to prove it reproduces the same codec and parameters, and otherwise return nothing so the caller writes the explicit call. Arguments are always written by name (positional `DateTime(6)` would land in `as`). -5. **The Prisma 7 reader's generators follow the codec**: `@updatedAt` and slice 2's `@onCreate(now())` resolve `timestampNow` for the `Date` codecs. -6. **`temporal.CreatedAt` and `temporal.UpdatedAt`** are rebuilt on `TimestamptzJsDate(3)` (exactly what bare `DateTime` resolves to, pinned by a test comparing them), with `@onCreate(now())`, and `@onUpdate(now())` for the second; the `String` and `JsDate` variants are removed. A contributed alias stays a literal type, so `type.temporal.CreatedAt()` keeps a precise codec type. -7. **`orm init`** writes the convenience forms in both its PSL and TypeScript templates. -8. **Upgrade instructions:** adopted projects re-emit (the reader and infer pick new codecs; the storage hash changes) and run `prisma db sign`; code that read strings from those columns now reads `Date`. Projects using the removed alias variants rewrite them. Slice 2's upgrade instruction (`prisma7-now-default-on-create`), which names `TimestampString(3)`, is updated. - -## Scope - -**In:** the framework helper, print and infer (`psl-print/column-types.ts`, `psl-build/column-types.ts`, `infer-model-blocks.ts`, `infer-default-codec.ts`, `inferred-type-constructors.ts`), the data types' normal form, the marks, the Prisma 7 reader, the aliases, `orm init` templates, `examples/prisma7-adoption` (contract, snapshot, `src/main.ts`, README, tests), every fixture and test pinning today's output, docs (`scalar-types.md`, `gotchas.md`, the contract-prisma7 and contract-ts READMEs, skills), ADR 275's alias line. ADR 279 already describes this design; it is not edited unless the built behaviour differs from it. - -**Out:** SQLite, MySQL and MongoDB (TML-3714); regenerating the Supabase pack contract (TML-3417). - -## Pre-investigated edge cases - -| Case | Disposition | -| --- | --- | -| `timestamptz` with no declared precision | `DateTime(precision: 6)`, by decision 2 | -| `timestamp` column read as `Temporal.PlainDateTime` | Explicit `TimestampTemporal` | -| An inferred `infinity` default on a `timestamptz` or `timestamp` column | The `Date` codec cannot read it, so infer writes the first row that can: `DateTime(as: string, ...)` with the default as written (decision 1's exception) | -| `time` with no precision | `Time` (string default; omitted precision equals 6 by decision 2) | -| `examples/prisma7-adoption` reading `updatedAt.replace(' ', 'T')` | Code updated to read a `Date` | -| Two type choices naming one constructor | The helper takes the first in namespace order; no PostgreSQL constructor is named twice | - -## Slice-specific done conditions - -- `contract infer` on the at-a-glance table writes exactly the at-a-glance schema, which emits, verifies clean against the same database with `db verify`, and plans no migration; a Prisma 7 adoption journey reads `Date` values for its `DateTime` columns. - -## Dispatch plan - -1. **The helper, the normal form and the marks** — outcome: print and infer write convenience calls for every reachable codec, the normal form treats omitted precision as 6, marks moved; unit and round-trip print/infer tests. Gates: framework-components, family-sql, target-postgres package tests; the psl-print and psl-infer integration tests; typecheck; lint; `pnpm lint:deps`; `pnpm fixtures:check`. -2. **The Prisma 7 reader, the aliases, `orm init` and the example** — builds on 1. Outcome: the at-a-glance Prisma 7 table, the aliases, the init templates, the adoption example and journeys. Gates: contract-prisma7 and CLI package tests; the Prisma 7 and infer journeys; `examples/prisma7-adoption` tests; `pnpm fixtures:check`; `pnpm check:upgrade-coverage --mode pr`. -3. **Docs and upgrade instructions** — builds on 2. Gates: `pnpm lint:docs`; `pnpm check:upgrade-coverage --mode pr`. diff --git a/projects/datetime-convenience-types/spec.md b/projects/datetime-convenience-types/spec.md deleted file mode 100644 index 258c3d2312b8..000000000000 --- a/projects/datetime-convenience-types/spec.md +++ /dev/null @@ -1,118 +0,0 @@ -# Project spec — Convenience date and time types - -**Linear:** [Convenience date and time types](https://linear.app/prisma-company/project/convenience-date-and-time-types-6a969ca1c76e) ([TML-3709](https://linear.app/prisma-company/issue/TML-3709)) · **Branch:** `tml-3709-plan-datetime-convenience-types` · **Design record:** [ADR 279 — Convenience types choose an explicit type from their arguments](../../docs/architecture%20docs/adrs/ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) (Accepted) - -## Purpose - -Prisma 8 gives users explicit, predictable controls first, then makes the common cases easy with convenience APIs built on top. PostgreSQL date and time columns have the explicit half only. Each of eleven type names fixes both the column type and the value the application reads, and nothing says which one to start with. The obvious name, `DateTime`, reads a `Temporal.Instant`, which fails on Node without a polyfill. The string types return text whose format depends on the database server's settings. And an app upgraded from Prisma 7 goes from `Date` values to those strings. - -This project gives a user one type per concept in their application (a moment in time, a calendar date, a time of day, a length of time), with defaults that work on stock Node. Every explicit choice stays available. Along the way, the values every date and time type returns become predictable. - -## At a glance - -```prisma -model Order { - id Int @id - paidAt DateTime // timestamptz(3), read as a JavaScript Date - shipBy Date // date, read as "2024-02-29" - opensAt Time(precision: 0) // time(0), read as "09:30:00" - period Duration // interval, read as "P1M" - settledAt DateTime(as: temporal) // timestamptz, read as a Temporal.Instant - legacyAt DateTime(storage: timestamp) // timestamp(3), read as a JavaScript Date, stored as UTC - exactAt TimestamptzTemporal(6) // an explicit type, written directly -} -``` - -`DateTime`, `Date`, `Time` and `Duration` are convenience types. Each becomes one explicit type, chosen by its optional `as:` and `storage:` arguments. The contract stores only the explicit type, so `settledAt` is stored exactly as `settledAt TimestamptzTemporal` would be. - -## Decisions settled in discussion (2026-10-21 and 2026-10-22) - -The reasoning and the alternatives rejected are in [`design-notes.md`](./design-notes.md) and ADR 279. - -1. **Two layers.** The explicit types stay, named after the database as ADR 257 says, each fixing one codec. The convenience types are named for what a field means in the application, and each becomes exactly one explicit type. Nothing is reversed: the explicit layer is the precise form, the convenience layer is the short form. -2. **A convenience type is a type choice: a declaration whose arguments name the explicit type constructor it becomes**, through a table with defaults and refusals. Not a codec parameterized by database type (a codec represents one data type, ADR 254), not a type alias (an alias attaches attributes to one fixed type), and not a select template over option arguments (no defaults or refusals; being removed with the presets). Settled 2026-10-22 when slice 4 was grounded. -3. **The four PostgreSQL convenience types and their choices:** - - | Type | `as:` | `storage:` | Default | - | --- | --- | --- | --- | - | `DateTime` | `jsDate`, `temporal` (`Temporal.Instant`), `string` | `timestamptz`, `timestamp` | `jsDate`, `timestamptz`; precision 3 when the value is `jsDate`, PostgreSQL's default otherwise | - | `Date` | `string`, `temporal` (`Temporal.PlainDate`) | `date` | `string` | - | `Time` | `string`, `temporal` (`Temporal.PlainTime`) | `time`, `timetz` (with `string` only) | `string`, `time` | - | `Duration` | `string` (ISO 8601 duration), `parts` (`{ months, days, micros }`) | `interval` | `string` | - - The value names repeat the suffixes of the explicit types (`TimestamptzJsDate`, `TimestamptzTemporal`, `TimestamptzString`). -4. **Defaults need no setup.** No default needs a Temporal polyfill. `DateTime` defaults to a JavaScript `Date`, which is also what Prisma 7 users have. `Date`, `Time` and `Duration` default to plain ISO 8601 strings. A JavaScript `Date` is never offered for a calendar date or a time of day. -5. **Plain strings, no brand and no Prisma value class.** A brand or class adds friction on every write and does not survive JSON. The codec validates the format on write. -6. **Every explicit type of a column with several codecs carries a suffix.** `Timestamptz`, `Timestamp`, `Date` and `Time` become `TimestamptzTemporal`, `TimestampTemporal`, `DateTemporal` and `TimeTemporal`. `Interval` becomes `IntervalParts`, next to the new `IntervalString`. -7. **Four codecs are added:** a `timestamp` column read as a `Date` and as a `Temporal.Instant`, both treating the stored value as UTC; and an `interval` column read as an ISO 8601 duration string. (The `parts` codec already exists.) -8. **String values are ISO 8601, the same on every server.** Every string codec returns the form ADR 254 stores, not PostgreSQL's display text. The codec ids do not change: this is a bug fix, and nothing keeps the old format before RC. The PostgreSQL target requires `DateStyle=ISO`, and the driver ensures it on every connection it hands out: as a startup option on connections it opens, and with a statement on a pool or client the user supplies. A connection that still prints another style is refused with an error naming the setting. Time zones need no setting: the `timestamptz` codec converts any session offset to UTC, and the `timetz` codec keeps the offset stored with the value. -9. **Temporal fields are never typed `any`.** The Temporal codecs and generators take their types from `temporal-spec`, a types-only package, instead of an undeclared global. No polyfill code is added anywhere. A missing global `Temporal` at runtime is still reported on first use, as today; a startup check was considered and dropped, because Temporal values are now an explicit opt-in. -10. **`contract infer` and `contract print` write convenience types**, with arguments only where they differ from the defaults; a column no convenience type covers (`Temporal.PlainDateTime`) is written as its explicit type. **The Prisma 7 reader** maps Prisma 7's `DateTime` to `TimestampJsDate(3)`, printed `DateTime(storage: timestamp)`, so an upgraded app reads `Date` values again, and maps Prisma 7's `@default(now())` to `@default(now()) @onCreate(now())`, as Prisma 7 behaved ([TML-3719](https://linear.app/prisma-company/issue/TML-3719)). -11. **PostgreSQL only.** A convenience name means the same thing on every target; SQLite and MongoDB follow in [TML-3714](https://linear.app/prisma-company/issue/TML-3714). -12. **TypeScript:** `type.DateTime(...)`, `type.Date(...)`, `type.Time(...)` and `type.Duration(...)` take their arguments as one object, `type.DateTime({ as: 'string', precision: 6 })`, with one overload per row, so a call is typed as the explicit type it picks and a combination with no row does not compile. Explicit constructors stay positional. Settled 2026-10-23 with the option names `as:` and `storage:`. - -## Non-goals - -- SQLite and MongoDB convenience types ([TML-3714](https://linear.app/prisma-company/issue/TML-3714)). -- Convenience types for other scalars, such as an `Int` choosing between `Int2`, `Int4` and `Int8` ([TML-3715](https://linear.app/prisma-company/issue/TML-3715)). -- The rest of the field-preset removal. This project rebuilds only the contributed `temporal.CreatedAt` and `temporal.UpdatedAt` aliases on `DateTime` (slice 5). -- A `Temporal.PlainDateTime` convenience form. It stays reachable as the explicit `TimestampTemporal`. -- Bundling a Temporal polyfill. -- Any change to how values are stored: the contract form of a value (ADR 254) and every existing codec's stored form stay as they are. - -## Place in the larger world - -- **ADR 254, data types and casts.** Codecs, data types and stored forms are unchanged. A type constructor still names exactly one codec; a type choice is a second kind of declaration that resolves to a type constructor. The text codecs today keep PostgreSQL's year text (` BC`); decision 8 changes that to the ISO form, which amends the "Date and time types" section. -- **ADR 257, scalar names.** The explicit layer follows it. The `Temporal` suffix applies the suffix rule to every codec of a multi-codec column. Its one-release deprecation rule is not applied before RC: renamed names become errors at once. -- **ADR 275.** The `option` argument kind and the `select` template stay removed with the presets; a type choice has its own option arguments. The contributed `temporal.*` aliases are built on `DateTime`. -- **The PostgreSQL and SQLite rename project** (`projects/target-named-scalars-sql/`, not started) must keep `DateTime` and take this project's renames instead of its own `DateTime` and `Timestamptz` rows. -- **The field-preset removal project** (`projects/remove-field-presets/`, slice 1 merged): its contributed aliases are built on `DateTime` (slice 5), and their `String` and `JsDate` variants are removed. -- **`prisma orm init`** writes `TimestamptzString` and `temporal.updatedAtString()` today. It moves to the convenience types in slice 5. - -## Cross-cutting requirements - -1. **The contract records only explicit types.** A schema written with a convenience type and the same schema written with the explicit type it becomes emit byte-identical `contract.json` and equal storage and execution hashes. -2. **Every argument combination is either a listed choice or an error at the argument** that names the accepted choices. No combination silently picks a codec. -3. **Every default works on Node 24 with no polyfill.** A test runs each convenience type's default in a process with no global `Temporal`. -4. **A string value round-trips.** A string written through a string codec reads back as the same string, on a server whose `TimeZone` is not UTC and whose `DateStyle` is not ISO. -5. **Renamed explicit names are renamed outright.** Before RC there are no deprecated aliases: `Timestamptz`, `Timestamp` and `Interval` become errors, and the upgrade instructions give the rewrite. `DateTime`, `Date` and `Time` change meaning when the convenience types take them. -6. **Every user-visible change ships upgrade instructions** in the same PR (`skills-contrib/record-upgrade-instructions`). -7. **Docs move with the code.** `docs/reference/scalar-types.md`, the error reference, the editor hover text and the codec authoring guide are updated in the slice that changes what they describe. - -## Transitional-shape constraints - -- Until slice 4 lands, `DateTime`, `Date` and `Time` keep today's explicit meaning. Slice 3 adds the `Temporal`-suffixed names beside them, so slice 3 alone breaks nothing; slice 4 takes the old names over. -- Slice 5 changes what `contract infer` and the Prisma 7 reader write, and so the codec of adopted columns. It ships with the upgrade instruction to re-emit and `db sign`. - -## Contract-impact - -No new contract entities or kinds. New codec ids: `pg/timestamp-date@1`, `pg/timestamp-instant@1` and `pg/interval-string@1`. Contracts that use the existing codecs do not change. Contracts emitted by `contract infer` and `prisma7Schema` after slice 5 use different codecs for date and time columns. - -## Adapter-impact - -PostgreSQL target and adapter only: data-type-bound codecs, type constructors, the runtime connection setup (slice 1), the Prisma 7 reader and `contract infer`'s type map. The framework authoring layer changes (codec choice in a type constructor); the SQL family's PSL column resolution and the TypeScript builder's type-level resolver follow. SQLite and MongoDB are untouched. - -## ADR pointer - -ADR 279, Accepted. It builds on ADR 241, 254 and 257 and amends ADR 275; ADR 254, 257 and 275 get a note in the slice that implements the change. - -## Project DoD - -Inherits the team floor in [`drive/calibration/dod.md`](../../drive/calibration/dod.md). In addition: - -- [ ] Every row of the decision 3 table works end to end: a PSL schema emits, `migration plan` produces the expected column DDL, and a query on PostgreSQL reads and writes the expected value type, with and without a global `Temporal` as each row requires. -- [ ] A new project from `prisma orm init`, and a Prisma 7 project adopted with `prisma7Schema(...)`, run their first query on Node with no polyfill and read `Date` values for their timestamps; a create through Prisma 8 on an adopted project, on a server whose `TimeZone` is not UTC, stores UTC. -- [ ] The report's use-case table (see References) re-run against the shipped surface, with each row's Prisma 8 cell updated. -- [ ] ADR 279 accepted; ADR 254, 257 and 275 carry notes; `docs/reference/scalar-types.md` describes both layers. -- [ ] TML-3714 and TML-3715 remain open with their context. - -## Open questions - -- None open. The new codecs are `pg/timestamp-date@1` (`TimestampJsDate`), `pg/timestamp-instant@1` (`TimestampInstant`) and `pg/interval-string@1` (`IntervalString`). - -## References - -- ADR 279 and the ADRs it builds on or amends: 241, 254, 257, 275. -- The research report comparing Prisma 7 and Prisma 8 date and time fields on PostgreSQL (2026-10-21, published as a private artifact; ask the operator). -- [TML-3418](https://linear.app/prisma-company/issue/TML-3418), the server-dependent string bug, now slice 1. -- The pull request that moved inferred and Prisma 7 date columns to string types: prisma/orm#30553. From 561811aa84b90e97652bca04e15fff84da3860ca Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:51:21 +0100 Subject: [PATCH 4/6] ADR 257 names the pre-release rename exception where it states the alias rule; F53's grep covers TypeScript and examples; the retro stays at process level Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- ...calar types are named after the target on every surface.md | 4 ++-- drive/calibration/failure-modes.md | 2 +- drive/retro/README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md b/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md index 6774b9c22014..8a52171b52ed 100644 --- a/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md +++ b/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md @@ -86,13 +86,13 @@ On PostgreSQL and SQLite, `Json` columns store JSON text, so what is stored is a ## Renaming a surface name -A renamed PSL name stays available as a deprecated alias of the new one for one release line, then is removed. The alias resolves to the same codec, so the contract does not change. A type constructor marks itself with `deprecated: { replacement }` on its `AuthoringTypeConstructorDescriptor`, and the PSL interpreter reports `PSL_DEPRECATED_SCALAR_NAME` at the type's span through the contract source's warning channel: a `ContractSourceDiagnostic` with `severity: 'warning'`, delivered to `ContractSourceContext.reportWarning`. `contract emit` prints it and still emits; the language server shows it as a warning and lists the deprecated names last in completion, struck through. +A renamed PSL name stays available as a deprecated alias of the new one for one release line, then is removed. The one exception is the PostgreSQL date and time renames of ADR 279, made before the release candidate, which removed the old names at once. The alias resolves to the same codec, so the contract does not change. A type constructor marks itself with `deprecated: { replacement }` on its `AuthoringTypeConstructorDescriptor`, and the PSL interpreter reports `PSL_DEPRECATED_SCALAR_NAME` at the type's span through the contract source's warning channel: a `ContractSourceDiagnostic` with `severity: 'warning'`, delivered to `ContractSourceContext.reportWarning`. `contract emit` prints it and still emits; the language server shows it as a warning and lists the deprecated names last in completion, struck through. A codec id is never renamed under this decision. If one ever has to change, the reserved mechanism is: the codec registry registers the same codec under both ids, the emitter always writes the new id, the contract validator accepts both, `CodecTypes` carries both keys mapped to the same type, and an upgrade instruction tells users to re-emit and re-sign; after one release line the old id is removed with an error naming the new one. ## Consequences -- Every target follows the same rule and the same deprecation path for a renamed PSL name. The current per-target names and the cross-target concept table are listed in [Scalar types](../../reference/scalar-types.md); other docs link there instead of repeating them. +- Every target follows the same rule and the same deprecation path for a renamed PSL name, apart from the pre-release exception above. The current per-target names and the cross-target concept table are listed in [Scalar types](../../reference/scalar-types.md); other docs link there instead of repeating them. - Readers of Prisma 6 and Prisma 7 schemas map the old names to codec ids through their target bindings and are unaffected by PSL names. - An extension that contributes a codec names its PSL type and TypeScript helper after the codec id's token, and declares every BSON type its codec stores in the `bsonTypes` of the codec's data type; the MongoDB validator derivation reads the whole list. diff --git a/drive/calibration/failure-modes.md b/drive/calibration/failure-modes.md index 5bf224408d15..4b214deb6906 100644 --- a/drive/calibration/failure-modes.md +++ b/drive/calibration/failure-modes.md @@ -941,7 +941,7 @@ Per-repo stop conditions beyond the canonical ones: **Root cause.** The rewrite was driven by the emitted fixtures `fixtures:check` knows about. Sources that a test emits at run time are not in that list, and PR CI skips the integration tests that read them. -**Detection signal.** `git grep -n "" -- 'test/**/*.psl' 'test/**/*.prisma' 'test/**/fixtures/**'` lists files the branch did not change. +**Detection signal.** `git grep -n "" -- 'test/**/*.psl' 'test/**/*.prisma' 'test/**/*.ts' 'examples/**/*.prisma' 'examples/**/*.ts'` lists files the branch did not change. **Mitigation.** When a name changes meaning, grep every PSL and TypeScript source under `test/` and `examples/` for the name, rewrite each one that relied on the old meaning, and run the integration file that emits it before enabling auto-merge. diff --git a/drive/retro/README.md b/drive/retro/README.md index e25223fcb368..6f998a96acfb 100644 --- a/drive/retro/README.md +++ b/drive/retro/README.md @@ -19,7 +19,7 @@ When a retro lesson could land in multiple surfaces, the team's preference: Patterns the team has seen multiple times. Each entry: pattern → severity → mitigation surface (where it landed). -- **2026-10-28 — Final datetime-convenience-types retro:** The merge of `main`'s field-presets removal into the convenience-types branch resolved 42 conflicted files with design choices of its own, and went to the operator for review with no review round on it. The operator had to ask, and the round that followed found twelve defects (landed in [`failure-modes.md § F51`](../calibration/failure-modes.md#f51-a-merge-of-main-that-resolves-a-large-overlapping-change-goes-to-review-as-if-it-were-not-new-work) and a PR-side item in [`dod.md`](../calibration/dod.md)). Among them, the 8.0.0-rc.21 release bump had carried the branch's edits into the released upgrade sources (landed in [`F52`](../calibration/failure-modes.md#f52-a-release-bump-archives-pending-upgrade-instructions-and-the-branchs-later-edits-land-in-the-released-copy)). The merge queue then dropped the PR twice, on a test fixture that still used `DateTime`'s old meaning and on a journey that `main`'s newest commit changed. The second is the raw-columns lesson again: the PR-side DoD was not walked before auto-merge, and the fourth project in a row hit it (the fixture case landed in [`F53`](../calibration/failure-modes.md#f53-a-name-changes-meaning-and-a-hand-written-source-that-fixturescheck-does-not-emit-keeps-the-old-meaning) and a PR-side item; walking the list before auto-merge is in the orchestrator's memory). When infer wrote a `Date` for a column whose default is `infinity`, the orchestrator offered the operator two options, one of which kept the failure, instead of fixing it (landed in the orchestrator's memory). What worked: folding four approved PRs into one at the operator's suggestion sent them through the queue as one entry; a colleague's review of ADR 279 led to the editor hover naming the explicit type each call becomes; and the independent system-design and code reviews found real defects every round, such as hover text that was not valid PSL and refusal messages whose `?` landed after the attributes. ADR-worthy decision: ADR 279, a convenience type picks one explicit type from a table. One-sentence summary: PostgreSQL schemas can write `DateTime`, `Date`, `Time` and `Duration`, each meaning exactly one explicit type, with defaults that need no Temporal polyfill, and `contract infer`, `contract print`, the Prisma 7 reader and `prisma orm init` write them. +- **2026-10-28 — Final datetime-convenience-types retro:** The merge of `main`'s field-presets removal into the convenience-types branch resolved 42 conflicted files with design choices of its own, and went to the operator for review with no review round on it. The operator had to ask, and the round that followed found twelve defects (landed in [`failure-modes.md § F51`](../calibration/failure-modes.md#f51-a-merge-of-main-that-resolves-a-large-overlapping-change-goes-to-review-as-if-it-were-not-new-work) and a PR-side item in [`dod.md`](../calibration/dod.md)). One came from the process: the 8.0.0-rc.21 release bump had carried the branch's edits into the released upgrade sources (landed in [`F52`](../calibration/failure-modes.md#f52-a-release-bump-archives-pending-upgrade-instructions-and-the-branchs-later-edits-land-in-the-released-copy)). The merge queue then dropped the PR twice, on a test fixture that still used `DateTime`'s old meaning and on a journey that `main`'s newest commit changed. The second is the raw-columns lesson again: the PR-side DoD was not walked before auto-merge, and the fourth project in a row hit it (the fixture case landed in [`F53`](../calibration/failure-modes.md#f53-a-name-changes-meaning-and-a-hand-written-source-that-fixturescheck-does-not-emit-keeps-the-old-meaning) and a PR-side item; walking the list before auto-merge is in the orchestrator's memory). When infer wrote a `Date` for a column whose default is `infinity`, the orchestrator offered the operator two options, one of which kept the failure, instead of fixing it (landed in the orchestrator's memory). What worked: folding four approved PRs into one at the operator's suggestion sent them through the queue as one entry; a colleague's review of ADR 279 led to the editor hover naming the explicit type each call becomes; and the independent system-design and code reviews found real defects every round. ADR-worthy decision: ADR 279, a convenience type picks one explicit type from a table. One-sentence summary: PostgreSQL schemas can write `DateTime`, `Date`, `Time` and `Duration`, each meaning exactly one explicit type, with defaults that need no Temporal polyfill, and `contract infer`, `contract print`, the Prisma 7 reader and `prisma orm init` write them. - **2026-10-27 — Final mongo-authoring-structure retro:** The close-out was first done from the plan's five-line checklist instead of the close-out procedure, so the retro, the DoD evidence, the file classification and two references were skipped until the operator sent it back; close-out now names the procedure (landed in [`drive/project/README.md`](../project/README.md#close-out-runs-the-close-out-procedure)). Slice 1 merged with four open CodeRabbit threads, because nothing before auto-merge listed open threads; one was a real ADR inconsistency (landed as a PR-side item in [`dod.md`](../calibration/dod.md)). Both slices grew a client bundle and the cause was traced only after CI's size report; measuring the bundle against `main` is now a PR-side item for slices that change code the runtime or `defineContract` reaches (landed in `dod.md`). Moving every MongoDB rule into one builder did not catch two invariants the collection validator relied on and no rule enforced (a variant field stored under a base field's name, and a variant restating a base field differently); the principal-engineer review found them, and both were on `main` too (landed in [`failure-modes.md § F50`](../calibration/failure-modes.md#f50-rules-are-moved-into-one-place-and-an-invariant-that-a-derived-artefact-relies-on-was-never-a-rule)). What worked: the operator rejected a one-case fix and asked for research against the architecture docs first, which found the structural cause and gave two slices instead of a string of patches; independent system-design and code reviews and manual QA each found defects the implementation tests passed over (details in prisma/orm#30846 and F50); and a parity suite that writes each case in both sources caught regressions in every later round. ADR-worthy decision: ADR 306, one plain-data definition and one builder per family. One-sentence summary: MongoDB contracts written in TypeScript, PSL or Prisma 6 now read written values through ADR 254's cast rule and build through one builder, so the same contract written two ways emits the same `contract.json` and is refused for the same reasons. - **2026-10-21 — Final complete-client-surfaces retro:** A mechanical rewrite of call sites in docs needs a check per site that the replacement means the same thing, because README samples are not type-checked and `db.execute(plan)` accepts a plan that returns rows: one PostGIS sample moved from `db.runtime().execute` to `db.execute` while reading rows, and only CodeRabbit caught it after three review rounds (landed in this entry). When a branch is built locally on an approved, queued pull request and that request lands as a squash commit, the merge of `main` conflicts on every file both touched; compare `main`'s copy of each conflicted file with the base branch's tip, and if they match, keep the branch's side (landed in this entry). The session gets no event when a pull request merges, so a slice that waits on a merge must be started by someone; here slice 3 started hours after slice 2 merged (landed in the orchestrator's memory). What worked: framing the static half of a client as the contract value (the operator's idea) removed the explicit type argument instead of working around it, and a spike on typing the JSON import directly was written up as ADR 277 before it was rejected; independent system-design and code reviews found that `toSql` skipped the runtime's plan check and that Supabase `close()` ended a pool the caller passed in; manual QA against a dev database confirmed `mode` and found TML-3655. One-sentence summary: an application's client is now one complete object, built from `contractFromJson(json)` with every other option inferred, carrying its custom collection classes into every transaction, loadable without a database, and able to render and run any plan. - **2026-10-21 — Final raw-columns retro:** The merge queue dropped this project's pull requests three times for a check that CI does not run on the pull request: slice 2 for the PSL print sweep tests, and slice 0 for a CLI journey that runs a changed command, then for the framework vocabulary lint that only CI's Lint job runs. Slice 0 also left the queue once for merge conflicts with `main`. This is the third project in a row with the same lesson, so it is now an item in the slice DoD: after the last merge of `main`, run the print sweeps, the journeys of changed commands and the CI-only lints before enabling auto-merge (landed in [`dod.md`](../calibration/dod.md) PR-side items). Slice 1 took seven merges of `main` and repeated conflicts before it merged, because its branch lived while other projects changed the same authoring and migration code; the operator ended that by merging it with the interim `Raw("...")` syntax and moving the breaking change to its own slice. Ship the part of a slice that depends on nothing unmerged, and put the part that waits on another project in a later slice. Slice 0 was planned first and built last, because it depended on TML-3607, another project's unmerged change: a plan orders slices by what they wait on, so a slice blocked on unmerged work goes last from the start (landed in this entry). A reviewer checked out another commit in the shared worktree in the middle of a review, which changes the code the other agents in that worktree build and test (landed in [`failure-modes.md § F22`](../calibration/failure-modes.md#f22-reviewer-runs-git-stash-to-debug-a-worktree-local-red-and-pops-an-unrelated-worktrees-stash-entry)). What worked: every review round found a real defect the implementation's tests passed over: a crash on an empty named argument, SQLite length parameters rounded instead of refused, and a `contract infer` message that pointed users at the wrong fix. One-sentence summary: a column of any type the database has can now be declared with ``Raw(sql`...`)``, created, verified and read, and a Prisma 7 schema with `Unsupported("...")` fields loads into Prisma 8. From b1588b0b5f4d962e2a3bd8b433a1b27d13d2766f Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:53:07 +0100 Subject: [PATCH 5/6] ADR 257's note says a type named before the suffix rule, such as MongoDB's Int64, keeps its name until renamed Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- ... Scalar types are named after the target on every surface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md b/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md index 8a52171b52ed..23699ef39e97 100644 --- a/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md +++ b/docs/architecture docs/adrs/ADR 257 - Scalar types are named after the target on every surface.md @@ -2,7 +2,7 @@ Status: **Accepted** -**Amended by [ADR 279](ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) (2026-10-22):** where a stored type has several codecs, every one of them carries a suffix naming its kind of value, not only the second (`TimestamptzTemporal`, `TimestamptzJsDate`, `TimestamptzString`). The unsuffixed name of the concept, such as `DateTime`, `Date`, `Time` and `Duration` on PostgreSQL, belongs to a convenience type, a type choice that becomes one of the suffixed types. Those renames were made before the release candidate, so the old names were removed at once, with no deprecated alias for a release line; the upgrade instructions give each rewrite. +**Amended by [ADR 279](ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md) (2026-10-22):** where a stored type has several codecs, every one of them carries a suffix naming its kind of value, not only the second (`TimestamptzTemporal`, `TimestamptzJsDate`, `TimestamptzString`). The unsuffixed name of the concept, such as `DateTime`, `Date`, `Time` and `Duration` on PostgreSQL, belongs to a convenience type, a type choice that becomes one of the suffixed types. Those renames were made before the release candidate, so the old names were removed at once, with no deprecated alias for a release line; the upgrade instructions give each rewrite. A type named before this amendment with no suffix beside a suffixed one, such as MongoDB's `Int64` beside `Int64Number` below, keeps its name until it is renamed under the alias rule. ## Decision From 9a925d162646c420dda4de9df09cc8b9f26629d4 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:59:19 +0100 Subject: [PATCH 6/6] The scalar reference says the PostgreSQL date and time renames are done, with no deprecated alias Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/reference/scalar-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/scalar-types.md b/docs/reference/scalar-types.md index 802cf075e0c0..5b859fc95fa9 100644 --- a/docs/reference/scalar-types.md +++ b/docs/reference/scalar-types.md @@ -63,7 +63,7 @@ One limitation to know about: ## PostgreSQL -These are the current names. The Postgres and SQLite rename project will change several of them to the target's own type names and update this table. +These are the current names. The date and time types below already follow ADR 257's suffix rule, and their old names `Timestamptz`, `Timestamp` and `Interval` were removed with no deprecated alias. The Postgres and SQLite rename project will change several of the other names to the target's own type names and update this table. The date and time types have two layers ([ADR 279](../architecture%20docs/adrs/ADR%20279%20-%20Convenience%20types%20choose%20an%20explicit%20type%20from%20their%20arguments.md)). Each explicit type in the table below names one column type and one kind of value. The convenience types `DateTime`, `Date`, `Time` and `Duration` are named for what a field means, and each becomes one explicit type, chosen by its optional `as` and `storage` arguments: `as` is the value the application reads, and `storage` is the database type that stores it; `precision` is passed on to the explicit type. The contract stores only the explicit type, so `DateTime(as: temporal)` and `TimestamptzTemporal` emit the same contract.