diff --git a/docs/architecture docs/ADR-INDEX.md b/docs/architecture docs/ADR-INDEX.md index 5ed3adab52fd..de32cd452ffe 100644 --- a/docs/architecture docs/ADR-INDEX.md +++ b/docs/architecture docs/ADR-INDEX.md @@ -35,6 +35,7 @@ This document provides a comprehensive index of all Architectural Decision Recor | 246 | Option arguments and select templates for authoring helpers | Adds a shared `option` argument kind (bare token in PSL, literal union in TS; one type across block parameters and helper arguments) and a `select` template node — registration-validated against the option's values — so preset vocabulary never leaks generator ids. An undefined execution-defaults phase omits the phase; an empty resolved `typeParams` omits the key — the two rules carry each other, and the `updatedAt()` ≡ `timestamptz(now, now)` shorthand is test-enforced, not structural. Per-codec preset name = codec base name. Records which check protects which surface (PSL validator vs TS literal union; the TS surface has no runtime validation) and which protects which argument object (weak type vs excess-property). | [ADR 246 - Option arguments and select templates for authoring helpers.md](adrs/ADR%20246%20-%20Option%20arguments%20and%20select%20templates%20for%20authoring%20helpers.md) | | 249 | Central attribute-spec registry | Registers every model-level and field-level PSL attribute of both families in one place, keyed by level and name, as a spec *factory* over a framework-owned construction-time context (`AttributeSpecContext` / `FieldAttributeSpecContext`) — the uniform signature is what lets the language server invoke the same factories the interpreters run. `assembleAttributeSpecs` merges family built-ins with [ADR 236](adrs/ADR%20236%20-%20Target-contributed%20model%20attributes.md)'s model-attribute descriptors into frozen plain records and restores the factory types core erases (they return `AttributeSpec`, because `refine` makes `Out` contravariant and `unknown` would reject every spec that refines). Registry keys drive unknown-attribute diagnostics at field and model level in both families; block attributes are declared on `AuthoringPslBlockDescriptor.attributes` and parsed by the kit. | [ADR 249 - Central attribute-spec registry.md](adrs/ADR%20249%20-%20Central%20attribute-spec%20registry.md) | | 250 | Models and views are emitted from the contract | `contract.d.ts` gains a `Models` namespace (one member per model, `_`, plus `_Any` for each polymorphic base) and a type-only `models` constant; relations are typed as the related member with a phantom `RelationKeys` key so `Scalars` names a default fetch's row and `Shape` names an application data structure derived from the model, and both ORM collections carry `_row` so `ResultType` names any query's result. To-one nullability is a family hook (SQL reads foreign keys and column nullability; Mongo takes the nullable default); name collisions and non-identifier names are emitter errors | [ADR 250 - Models and views are emitted from the contract.md](adrs/ADR%20250%20-%20Models%20and%20views%20are%20emitted%20from%20the%20contract.md) | +| 253 | Literal types for column defaults | **Proposed.** Every literal column default has a literal type (`string`, `boolean`, `int`, `float`, `bigint`, `decimal`, `json`), cut where the codecs' stored JSON forms are cut, so a codec descriptor names the types it is compatible with and gains no methods. PSL writes a literal as a plain scalar or as an ADR 129 tagged literal (`` json`...` ``); `sql` writes a raw SQL expression instead. Codecs never see PSL syntax. Replaces the PSL half of ADR 184. Open: which literal type a plain number scalar names | [ADR 253 - Literal types for column defaults.md](adrs/ADR%20253%20-%20Literal%20types%20for%20column%20defaults.md) | ## Query System diff --git a/docs/architecture docs/adrs/ADR 184 - Codec-owned value serialization.md b/docs/architecture docs/adrs/ADR 184 - Codec-owned value serialization.md index 401a1e4a0efe..b2ce685b7082 100644 --- a/docs/architecture docs/adrs/ADR 184 - Codec-owned value serialization.md +++ b/docs/architecture docs/adrs/ADR 184 - Codec-owned value serialization.md @@ -2,6 +2,8 @@ > **Retrospective note.** This ADR's examples use the `defineCodec({...})` factory. That factory was the canonical codec-author surface at the time; it was later retired in favor of class-based authoring: concrete codecs extend `CodecImpl`, descriptors extend `CodecDescriptorImpl`, and per-codec column helpers tie helpers to descriptors with `satisfies`. The ADR's *decision* — that codecs own both wire and JSON-safe representations through `encode` / `decode` + `encodeJson` / `decodeJson` — is unchanged; only the authoring shape has moved on. See [ADR 208 — Higher-order codecs for parameterized types](ADR%20208%20-%20Higher-order%20codecs%20for%20parameterized%20types.md) and the [Codec authoring guide](../../reference/codec-authoring-guide.md) for the current shape. +> **PSL half: see [ADR 253 — Literal types for column defaults](ADR%20253%20-%20Literal%20types%20for%20column%20defaults.md) (proposed).** The `PslLiteralCodec` interface sketched below is replaced there: codec descriptors name the literal types they are compatible with, codecs gain no methods, and codecs never receive PSL syntax. The JSON half of this ADR is unaffected. + ## At a glance A column with `codecId: "pg/timestamptz@1"` has a default value of `new Date('2024-01-15')` — a JavaScript `Date`. This value has to survive a round-trip through `contract.json`, but `Date` has no JSON representation. The codec handles it: diff --git a/docs/architecture docs/adrs/ADR 253 - Literal types for column defaults.md b/docs/architecture docs/adrs/ADR 253 - Literal types for column defaults.md new file mode 100644 index 000000000000..85fef7b523cd --- /dev/null +++ b/docs/architecture docs/adrs/ADR 253 - Literal types for column defaults.md @@ -0,0 +1,197 @@ +# ADR 253 — Literal types for column defaults + +Status: **Proposed** + +## Decision + +Every literal column default has a **literal type**. A codec declares the literal types its columns are compatible with, by name and nothing more. PSL is one way of writing a literal of a given type, and a codec never sees PSL syntax. + +```prisma +model Account { + id Int @id + name String @default("anonymous") + balance BigInt @default(9007199254740993) + price Decimal @default(1.50) + active Boolean @default(true) + meta Jsonb @default(json`{ "plan": "free", "seats": 1 }`) + expires DateTime @default(sql`now() + interval '3 days'`) +} +``` + +Reading that model: + +- `"anonymous"`, `9007199254740993`, `1.50`, and `true` are plain PSL scalars. They write a `string` literal, a `bigint` literal, a `decimal` literal, and a `boolean` literal. +- `` json`...` `` is a tagged literal, the syntax [ADR 129](ADR%20129%20-%20Template-Tagged%20Literals%20for%20Extensions.md) defines: a tag followed by a string. The tag `json` names the literal type, and the string holds the JSON document. In backticks it needs no escaping and may span several lines. +- `` sql`now() + interval '3 days'` `` is also a tagged literal, but `sql` does not name a literal type. It writes a raw SQL expression, which becomes a function default the database evaluates. No codec is consulted. + +Each literal type produces exactly the value shape the codecs that name it already accept in `decodeJson`, so a codec needs no new methods. Writing `` @default(json`{}`) `` on an `Int` column is an error that says `pg/int4@1` is compatible with `int` literals. + +This ADR replaces the PSL half of [ADR 184](ADR%20184%20-%20Codec-owned%20value%20serialization.md), which sketched `encodePsl` and `decodePsl` methods on codecs. The JSON half of ADR 184 is unchanged. + +## Why literal types + +### A codec cannot be asked "is this literal yours?" without a type to answer about + +In SQL a codec can represent almost anything: a number, a document, a geometry, a vector, a timestamp. When a schema writes a default, something has to decide whether the written value suits the column. If the only information available is the characters the author typed, the codec has to try to read them and report failure by throwing. That makes compatibility a side effect of decoding, and the error the author sees is whatever the codec happened to throw. + +A literal type turns the question into a lookup. The literal says what kind of value it is. The codec says which kinds it accepts. A mismatch is reported before anything is decoded, with a message that names the codec and the literal types it accepts. + +### A codec should not depend on PSL + +Codecs live below every authoring surface. PSL is one surface; the schema language of earlier Prisma versions, read by the contract source in [ADR 252](ADR%20252%20-%20An%20earlier%20Prisma%20version's%20schema%20is%20a%20contract%20source.md), is another. If a codec received PSL text, or the PSL parser's idea of what a token is, every codec would be coupled to PSL's quoting, escaping, and tokenizer rules. A change to PSL syntax would become a change to every codec. + +With literal types, each authoring surface maps its own syntax to literals, and codecs only ever receive a literal of a type they declared. PSL can gain new tags without any codec changing. + +### The literal types are cut where the stored representations are cut + +The contract stores a literal default in the column codec's JSON form ([ADR 184](ADR%20184%20-%20Codec-owned%20value%20serialization.md)). Codecs that hold numbers do not share one JSON form: + +| Column | Codec | Written | Stored in `contract.json` | +|---|---|---|---| +| `Int` | `pg/int4@1` | `42` | `42` | +| `BigInt` | `pg/int8@1` | `9007199254740993` | `"9007199254740993"` | +| `Decimal` | `pg/numeric@1` | `1.50` | `"1.50"` | + +A single `number` literal type would have to be converted per codec, which puts conversion code on every numeric codec. Instead the literal types are cut where those representations are cut: `int`, `bigint`, and `decimal` are separate literal types, each producing what its codecs already accept. The declaration is then a list of names, and the rules for whole numbers, decimal canonicalisation, and digit preservation are written once, in the literal type, rather than once per codec. + +## The literal types + +A literal type defines what its value is, how a written literal is read into that value, and how a stored value is written back. It is defined in the framework, so a codec descriptor in any family can name it. + +| Literal type | Value it produces | Named by | +|---|---|---| +| `string` | The text, with escapes resolved | Text, uuid, bit and varbit, enum-backed text, bytes as base64, geometry as hex, intervals, timestamps and dates as their text form | +| `boolean` | `true` or `false` | Boolean codecs | +| `int` | A JSON number, whole. A fraction is refused | `pg/int4@1`, `pg/int2@1`, `pg/int8number@1`, `sqlite/integer@1`, `sql/int@1` | +| `float` | A JSON number, or the text `NaN`, `Infinity`, or `-Infinity` | `pg/float4@1`, `pg/float8@1`, `sqlite/real@1`, `sql/float@1` | +| `bigint` | The digits as text, so every digit survives | `pg/int8@1`, `pg/unboundedint@1`, `sqlite/bigint@1` | +| `decimal` | Decimal text. Trailing zeros are kept, leading zeros and the sign of zero are removed | `pg/numeric@1` | +| `json` | A JSON value | `pg/json@1`, `pg/jsonb@1`, `sqlite/json@1`, `pg/vector@1`, `arktype/json@1` | + +Two rules keep the values faithful. A number is never converted to a JavaScript number unless its literal type says so, because converting `9007199254740993` rounds it and converting `1.50` drops the trailing zero a `numeric` column keeps. And a `json` literal's body is parsed as JSON once, by the literal type, so codecs receive the JSON value rather than text they must parse. + +`sqlite/real@1` and `sql/float@1` refuse `NaN` and the infinities, as they already do for JSON values. The `float` literal type carries them, and those codecs reject them when they decode. + +## Writing a literal in PSL + +PSL has two ways to write a literal, and both produce the same literal. + +**Plain scalars** write `string`, `boolean`, and the numeric literal types, and need no tag. + +**Tagged literals** write any literal type, including ones with no plain scalar. A tagged literal is a tag followed by a string in any of PSL's three quote characters; its escapes and the canonical body (line endings normalised, the common indentation removed) are those of [ADR 129](ADR%20129%20-%20Template-Tagged%20Literals%20for%20Extensions.md). Tags are registered in `ControlMutationDefaults.defaultLiteralTagRegistry`, whose entry for a tag says which literal type it writes, and they follow ADR 129's prefixing rules. Each SQL target registers the `json` tag, with no prefixed alias. A tag that no pack in the contract's stack registers is `PSL_UNKNOWN_DEFAULT_LITERAL_TAG`, and the message lists the registered tags. + +The `sql` tag is the one tag that does not write a literal type. Its body is a SQL expression, not a value of the column's type, so it lowers to a function default (`{ kind: 'function', expression }`) on any column, and no codec compatibility applies. + +The syntax tree keeps exactly what the author wrote. The formatter and the language server work from the tree; only the interpreter turns scalars and tagged literals into literals. + +## Codecs declare compatible literal types + +A codec descriptor names the literal types its columns are compatible with. This is static metadata, next to `traits` and `targetTypes` on `CodecDescriptor`: it depends only on the codec id, never on a particular column's parameters. It carries no functions, because the literal type produces the value the codec's `decodeJson` already accepts. + +```ts +class PgJsonbDescriptor extends PostgresCodecDescriptor { + override readonly literalTypes = ['json'] as const; +} + +class PgInt4Descriptor extends PostgresCodecDescriptor { + override readonly literalTypes = ['int'] as const; +} +``` + +The declaration is optional. A codec that names no literal type accepts no literal defaults, and its columns take raw SQL defaults only. + +The codec instance keeps the checks that depend on column parameters. The interpreter passes the literal type's value to the codec's existing `decodeJson`, so a `vector(3)` column given a four-element `json` literal is refused there, with the vector codec's own message. + +## Reading a default + +Each step has one owner. + +1. **PSL parser.** Parses the `@default(...)` argument into a scalar or a tagged literal node, recording the source span. +2. **Interpreter.** Determines the literal's type: a tagged literal takes the type its tag writes; a plain scalar takes the type the column's codec declares for that scalar, which the open question below concerns. A `sql` tagged literal becomes a function default and stops here. +3. **Compatibility.** If the literal's type is not one the column's codec declares, the interpreter reports an error at the literal naming the codec and its compatible literal types. +4. **Literal type.** The literal type reads the written text into its value, refusing text it cannot read, such as a fraction written for an `int` literal. +5. **Codec instance.** `decodeJson` checks the value against the column. A value it refuses is reported at the literal with the codec's message. +6. **Contract.** The default is stored as `{ kind: 'literal', value }` in the codec's JSON form, like every literal default. + +Other text-based contract sources follow the same steps from their own syntax. The reader for the earlier Prisma schema language ([ADR 252](ADR%20252%20-%20An%20earlier%20Prisma%20version's%20schema%20is%20a%20contract%20source.md)) turns that language's defaults into literals of a type and checks them against the same declarations. The TypeScript contract builder is not a text source: `.default(value)` passes a value of the codec's own type, and TypeScript's types do the compatibility check. + +## Printing a default + +`contract infer` runs the steps in reverse for each introspected literal default: + +1. The target reads the database's default into the codec's JSON form. +2. The printer takes the literal type the column's codec declares and asks it to write that value. +3. A literal type with a plain scalar prints as that scalar. Any other prints as a tagged literal with the tag that writes it. +4. When the codec names no literal type, or the literal type cannot write the value, the printer writes the database's expression as a `sql` tagged literal. Infer never drops a default. + +A printed schema therefore reads back to the same contract, because printing and reading pass through the same literal type. + +## Responsibilities + +| Layer | Owns | +|---|---| +| PSL parser | Scalars and tagged literal nodes, spans, canonicalisation of tagged bodies | +| Tag registry | Which literal type each tag writes; which tag writes raw SQL | +| Literal types | The value each literal holds, reading a written literal into it, and writing a stored value back | +| Interpreter and other text sources | Mapping their syntax to literals; reporting incompatibility at the literal | +| Codec descriptor | The names of the compatible literal types | +| Codec instance | `decodeJson` checks that depend on column parameters | +| Contract | The JSON form, unchanged from [ADR 184](ADR%20184%20-%20Codec-owned%20value%20serialization.md) | + +## Open question: how a plain scalar picks its literal type + +**Not settled, and this ADR is not implemented for the numeric literal types until it is.** + +A PSL number scalar is written the same way everywhere, but under this design the literal type it produces depends on the column: `42` is an `int` literal on an `Int` column, a `bigint` literal on a `BigInt` column, and a `decimal` literal on a `Decimal` column. The column's codec decides, by what it declares, and that declaration is also the compatibility statement. + +That follows from cutting the literal types where the stored representations are cut, but it means one syntax does not name one literal type. Three answers are open: accept it as written here; give each numeric literal type a tag so a written literal always names its own type; or return to a single `number` literal type whose conversion each codec owns, at the cost described above. + +## Settled details + +- **The declaration is optional**, and Mongo codecs name nothing, because no Mongo contract source reads defaults from text. +- **A JSON column is not compatible with a `string` literal.** `Jsonb @default("{}")` is an error that asks for `` json`{}` ``. The reader for the earlier Prisma schema language turns that language's quoted JSON into a `json` literal itself. +- **A decimal column is not compatible with a `string` literal.** `Decimal @default("1.50")` is an error, and the default is written `1.50`. +- **`NaN`, `Infinity`, and `-Infinity` are `float` literals**, because the PSL tokenizer reads them as numbers. The integer literal types refuse them. +- **JSON null is a value.** `` Json @default(json`null`) `` stores JSON null. +- **Enum columns are unchanged.** Their default is a bare member name, and enum codecs name no literal types. +- **List columns keep PSL's list syntax.** Each element is a literal checked against the element codec's declaration, so `` Jsonb[] @default([json`{}`, json`[]`]) `` is valid and `Int[] @default([1, "x"])` is refused at its second element. +- **Diagnostics.** A literal whose type the codec does not declare is `PSL_DEFAULT_LITERAL_TYPE_INCOMPATIBLE`. Text a literal type cannot read, and a value a codec refuses, are `PSL_INVALID_DEFAULT_LITERAL` with the reason. A `json` body that is not valid JSON is `PSL_INVALID_JSON_LITERAL`. Each diagnostic points at the literal. + +## Alternatives considered + +### Codec methods that receive the parser's classification + +Codecs gain `encodePsl(value)` and `decodePsl(literal)`, where the literal is `{ kind: 'string' | 'number' | 'boolean', text }` produced by the PSL parser. This is close to the interface ADR 184 sketched. + +Rejected. The input to every codec becomes the PSL tokenizer's view of the source, which couples codecs to PSL. A JSON document can only arrive as a string with its quotes escaped. And there is still no compatibility check: a codec discovers that a literal is not for it by failing to decode it. + +### One `number` literal type, converted per codec + +A single `number` literal type carries the digits as text, and each numeric codec declares a read function to its own JSON form and a write function back. + +Rejected, and reconsidered in the open question above. Every numeric codec carries conversion code that duplicates what its `decodeJson` already does, and the rules for whole numbers, decimal canonicalisation, and digit preservation are written once per codec instead of once per literal type. + +### Codecs receive the raw argument text + +Whatever is written between `@default(` and `)` goes to the column codec as text, and the codec parses it. + +Rejected. The parser would need an unparsed argument form that exists for `@default` alone, and the formatter and language server would need to understand it. Every codec would reimplement PSL quoting and escaping. And a bare word such as `ACTIVE` could be an enum member or a string the codec reads, with nothing to tell them apart. + +### Try each JSON form until the codec accepts one + +A number literal is offered to the codec as a JSON number first, then as text, and the first form `decodeJson` accepts wins. + +Rejected. Compatibility is again discovered by failure, a value that two forms both decode is ambiguous, and the error the author sees comes from the last attempt rather than from the actual mismatch. + +### A separate registry of PSL converters keyed by codec id + +The PSL conversions live in a registry beside the codecs, as ADR 184's `PslLiteralCodec` interface suggested. + +Rejected. The codec descriptor is already the codec-id-keyed home for a codec's static metadata. A second registry would hold the same kind of information in a second place and could drift from the codecs it describes. + +## Related + +- [ADR 184 — Codec-owned value serialization](ADR%20184%20-%20Codec-owned%20value%20serialization.md): codecs own the JSON form of values. Its JSON half stands; this ADR replaces its PSL half. +- [ADR 129 — Tagged literals carry raw SQL and other pack-owned text in PSL](ADR%20129%20-%20Template-Tagged%20Literals%20for%20Extensions.md): the tagged literal syntax, the canonical body, and tag registration. This ADR adds that a tag writes either a literal of a literal type or, for `sql`, a raw SQL expression. +- [ADR 252 — An earlier Prisma version's schema is a contract source](ADR%20252%20-%20An%20earlier%20Prisma%20version's%20schema%20is%20a%20contract%20source.md): a second text contract source that maps its own syntax to literals. +- [ADR 167 — Typed default literal pipeline and extensibility](ADR%20167%20-%20Typed%20default%20literal%20pipeline%20and%20extensibility.md): historical context for typed literal defaults. diff --git a/projects/remove-dbgenerated/plan.md b/projects/remove-dbgenerated/plan.md index 0e387ac14a7d..17cf4efe36e6 100644 --- a/projects/remove-dbgenerated/plan.md +++ b/projects/remove-dbgenerated/plan.md @@ -7,7 +7,7 @@ Spec: [`spec.md`](spec.md). Deferred items: [`deferred.md`](deferred.md). **Line | Slice | Folder | Branch | Depends on | Owner | Linear | |---|---|---|---|---|---| | A — The `sql` tagged literal for raw SQL defaults | [`slices/a-sql-default-literal/`](slices/a-sql-default-literal/spec.md) | `remove-dbgenerated-sql-literal` | nothing | agent 1 | TBD | -| B — Codec-owned PSL literals (ADR 184, PSL half) | [`slices/b-codec-psl-literals/`](slices/b-codec-psl-literals/spec.md) | `remove-dbgenerated-codec-psl-literals` | nothing | agent 2 | TBD | +| B — Literal types that codecs are compatible with (ADR 184, PSL half) | [`slices/b-codec-psl-literals/`](slices/b-codec-psl-literals/spec.md) | `remove-dbgenerated-codec-psl-literals` | A merged | agent 2 | TBD | | C — Delete `dbgenerated`, regenerate Supabase, upgrade instruction | [`slices/c-remove-dbgenerated/`](slices/c-remove-dbgenerated/spec.md) | `remove-dbgenerated-delete` | A and B merged | agent 1 | TBD | Each slice is one PR against `main`. Slice plans (dispatch decomposition) are written at build time at `slices//plan.md` by the slice's implementer following the slice spec; the specs are complete enough that the plan is a sequencing document, not a design document. @@ -15,10 +15,11 @@ Each slice is one PR against `main`. Slice plans (dispatch decomposition) are wr ## Sequencing ``` -main ──┬── A (parallel) ──┐ - └── B (parallel) ──┴── C +main ── A ── B ── C ``` +- *Amended 2026-09-17.* B now builds on slice A's tag registry and tagged-literal node, so B follows A. B's first PR implemented the withdrawn `encodePsl`/`decodePsl` design and is reworked on top of A after A merges. + - A and B start together from the same `main` commit (`f3574a34a7` or later). - A and B share one file: `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts`, function `scalarDefaultArms`. A appends a `taggedLiteral(...)` arm after the function arms. B replaces `str(), numLiteral(), bool()` with `literal()`. Whichever merges second rebases and resolves that one function by hand; the result is `[literal(), ...funcArms, taggedLiteral(tags)]` for scalars and `[list(literal()), ...funcArms, taggedLiteral(tags)]` for lists. Both also touch `DefaultArgValue` in the same file, each adding its own union member. - A and B both add a file under `psl-parser/src/attribute-spec/combinators/` and a member to `ArgTypeKind` in `attribute-spec/types.ts`; distinct names, so the rebase is mechanical. @@ -29,7 +30,7 @@ main ──┬── A (parallel) ──┐ - Agent 2 (slice B) owns the `Codec` interface change and every codec class. Agent 1 (slice A) does not add or change codec members. - Agent 1 (slice A) owns the tokenizer, parser node, tag registry, TypeScript `sql` tag and helpers, `gen_random_uuid()`, and the SQLite verify-side resolver. Agent 2 does not touch those. -- Interfaces slice C relies on, which A and B must ship exactly as specified: `ControlMutationDefaults.defaultLiteralTagRegistry` (A5), `TaggedLiteralValue` (A4), `Codec.encodePsl` / `Codec.decodePsl` and `PslLiteral` (B1, B2), `mapDefault(columnDefault, { codec })` and `formatPslLiteral` (B6). A change to any of these names or shapes is reported to the orchestrator before it lands. +- Interfaces slice C relies on, which A and B must ship exactly as specified: `ControlMutationDefaults.defaultLiteralTagRegistry` (A5), `TaggedLiteralValue` (A4), the codec descriptor's compatible literal types declaration and the tag registry's mapping from tag to literal type (slice B spec, rewrite pending), `mapDefault(columnDefault, { codec })` (B6). A change to any of these names or shapes is reported to the orchestrator before it lands. - Both agents record any question the spec does not answer in their PR body under "Spec gaps" and stop on the halt conditions their spec lists. They do not choose an alternative. ## Validation gates per slice diff --git a/projects/remove-dbgenerated/slices/b-codec-psl-literals/brief.md b/projects/remove-dbgenerated/slices/b-codec-psl-literals/brief.md new file mode 100644 index 000000000000..bec7ed5233de --- /dev/null +++ b/projects/remove-dbgenerated/slices/b-codec-psl-literals/brief.md @@ -0,0 +1,233 @@ +# Implementation brief — literal types for column defaults + +You are implementing ADR 253. This brief is self-contained: it assumes you have not seen the discussion that produced the design, and it tells you everything you need to decide nothing for yourself. Where something is not settled, it says so and tells you to stop and ask rather than choose. + +## 1. One decision is blocked and must be discussed before you write code for it + +**A plain number scalar in a schema does not name a literal type of its own.** Under ADR 253, `42` written on an `Int` column is an `int` literal, on a `BigInt` column a `bigint` literal, and on a `Decimal` column a `decimal` literal. Which one it is depends on what the column's codec declares. That consequence is deliberate but unsettled, and Will has asked to discuss it before it is built. + +Three answers are open, and ADR 253 records all three: accept it as written; give each numeric literal type its own tag so a written literal always names its own type; or return to a single `number` literal type whose conversion each codec owns. + +**What this means for you:** + +- Raise the question with Will before you implement anything in section 6.4 or the numeric part of section 6.3. Ask directly, state the three options, and wait for an answer. Do not pick one, and do not proceed on an assumption. +- Everything else in this brief is settled and you can build it while you wait, in the order section 7 gives. +- Once Will answers, write the answer into the slice specification (section 5) before you write the code that depends on it. + +## 2. What this repository is, in the terms this brief uses + +Prisma 8 is a data layer whose single source of truth is a **contract**: a JSON file, `contract.json`, that describes every table, column, index and constraint of a database. Nothing generates runtime code from it; tools read it. + +Four ideas matter here. + +**Authoring surfaces.** A contract is written in one of several ways. The main one is **PSL**, the Prisma Schema Language, the `.prisma` file people write. A second is the TypeScript contract builder. A third is the schema file of an earlier Prisma version, which Prisma 8 reads directly. Every one of these is called a **contract source**, and each turns its own syntax into the same contract. + +**Codecs.** A **codec** owns one database type. It converts between the JavaScript value an application uses, the wire form the database driver exchanges, and a JSON form used inside `contract.json`. Its four methods are `encode`, `decode`, `encodeJson` and `decodeJson`. Every codec has a **codec id** such as `pg/int4@1`. A **codec descriptor** holds the codec's static metadata, keyed by codec id: `traits`, `targetTypes`, a params schema, and the factory that makes codec instances. The descriptor is where this work adds a declaration. + +**Column defaults.** A column's default in the contract is one of two shapes. `{ kind: 'literal', value }` holds a value, stored in that column's codec JSON form. `{ kind: 'function', expression }` holds SQL text the database evaluates. Nothing in this work changes those two shapes. + +**Tagged literals.** PSL can write text that Prisma does not parse, as a **tag** followed by a string: `` sql`now()` ``. The tag says which pack owns the text. This already exists and is described in ADR 129. + +## 3. What you are building, in one paragraph + +Every literal column default gains a **literal type**: `string`, `boolean`, `int`, `float`, `bigint`, `decimal`, or `json`. Each literal type is defined once in the framework and produces exactly the value shape that the codecs naming it already accept in `decodeJson`. Each codec descriptor names the literal types its columns are compatible with, as a list of names carrying no functions. A contract source turns its own syntax into a literal of a type, the interpreter checks the type against the column's codec and reports a precise error when they do not match, and the literal type produces the value that goes through `decodeJson` into the contract. Printing a schema runs the same path backwards. No codec gains a method, and no per-type code remains in the interpreter or the printer. + +## 4. Read these first + +Everything listed is committed. Fetch before you start. + +**On `main`:** + +- `docs/architecture docs/adrs/ADR 129 - Template-Tagged Literals for Extensions.md`. The tagged literal syntax, the canonical body, and how tags are registered. +- `docs/architecture docs/adrs/ADR 184 - Codec-owned value serialization.md`. Why codecs own the JSON form of values. Its PSL half is what ADR 253 replaces. +- `docs/architecture docs/adrs/ADR 252 - An earlier Prisma version's schema is a contract source.md`. The second text contract source you must keep working. +- `docs/reference/codec-authoring-guide.md`. How a codec and its descriptor are written. +- `projects/remove-dbgenerated/spec.md` and `projects/remove-dbgenerated/plan.md`. The project this slice belongs to. Its purpose is removing `@default(dbgenerated("..."))`, a construct that put raw SQL into a default as an unnamed string. Slice A replaced it with the `sql` tagged literal and is merged. Your slice is B. Slice C deletes `dbgenerated` and is not yours. +- `CLAUDE.md` at the repository root, and the rules it points at under `.agents/rules/`. + +**On branch `remove-dbgenerated-adr-253`, which is pull request 30334 and may have merged into `main` by the time you read this. Check `main` first; if the file is not there, fetch the branch:** + +- `docs/architecture docs/adrs/ADR 253 - Literal types for column defaults.md`. **The design you are implementing. It is authoritative. Where this brief and ADR 253 disagree, ADR 253 wins, and you tell Will about the disagreement.** If review changed the ADR, follow the changed ADR. +- The same branch amends `projects/remove-dbgenerated/spec.md` decisions D9 and D10 to match ADR 253. + +**On branch `remove-dbgenerated-codec-psl-literals`, whose pull request 30324 is closed.** This branch holds an earlier, withdrawn attempt at the same slice, built against a design where codecs gained `encodePsl` and `decodePsl` methods. That design is dead. Five pieces of it are worth reusing and are listed in section 8. Do not merge or rebase this branch; take the pieces by hand. + +## 5. Your first deliverable: rewrite the slice specification + +`projects/remove-dbgenerated/slices/b-codec-psl-literals/spec.md` currently describes the withdrawn design and carries a banner saying so. Rewrite it to describe the work in this brief, in the same shape as the sibling file `projects/remove-dbgenerated/slices/a-sql-default-literal/spec.md`: outcome, design sections, the tests that must exist, definition of done, halt conditions. Keep it a slice-level document. Do not restate ADR 253's rationale; point at it. + +Commit the rewritten spec before you write implementation code, so the two are reviewable apart. + +## 6. The design, in full + +### 6.1 Literal types live in the framework + +Add literal types to `packages/1-framework/1-core/framework-components`, alongside the codec surface in `src/shared/`, exported through `src/exports/codec.ts`. There are seven, and no others in this slice. + +| Literal type | The value it produces | Reading rules | Writing rules | +|---|---|---|---| +| `string` | The text | Escapes are already resolved by the source | Print as the source's string syntax | +| `boolean` | `true` or `false` | | | +| `int` | A JSON number, whole | Refuse text that is not a whole number, and refuse `NaN` and the infinities | Print the digits | +| `float` | A JSON number, or the text `NaN`, `Infinity`, or `-Infinity` | Accept a number, or one of those three words | Print a finite number plainly with no exponent; print the three special values as a quoted string | +| `bigint` | The digits as text | Refuse text that is not a whole number | Print the digits | +| `decimal` | Decimal text | Keep trailing zeros. Remove leading zeros and the sign of zero, so `007` reads as `7`, `-0` as `0`, and `-007.50` as `-7.50`. Accept `NaN`, `Infinity` and `-Infinity` | Print a finite decimal plainly with no exponent; print the three special values as a quoted string | +| `json` | A JSON value | Parse the body as JSON once | Print `JSON.stringify` of the value | + +The canonicalisation rules for `decimal` already exist on `main` in `packages/2-sql/2-authoring/contract-psl/src/number-literal-default.ts`, in `canonicalDecimalText`. Move that logic into the `decimal` literal type and delete that file, its export from `packages/2-sql/2-authoring/contract-psl/src/exports/resolution.ts`, and its test. + +A literal type must never convert a number through a JavaScript number except where the table says it produces a JSON number. `bigint` and `decimal` carry digits as text end to end. + +### 6.2 Codec descriptors name their literal types + +Add an optional member to `CodecDescriptor` in `packages/1-framework/1-core/framework-components/src/shared/codec-descriptor.ts`, and to `CodecDescriptorImpl`, naming the literal types that codec's columns are compatible with. It carries names only and no functions. A codec that names none accepts no literal defaults; its columns can still take a `sql` default. + +The complete inventory follows. Every codec id in the repository appears exactly once. Implement it exactly. + +**Postgres target, `packages/3-targets/3-targets/postgres/src/core/`:** + +| Codec ids | Literal type | +|---|---| +| `pg/text@1`, `pg/char@1`, `pg/varchar@1`, `pg/uuid@1`, `pg/inet@1`, `pg/bit@1`, `pg/varbit@1`, `pg/timetz@1`, `pg/interval@1`, `pg/bytea@1`, `pg/enum@1`, `pg/date-string@1`, `pg/time-string@1`, `pg/timestamp-string@1`, `pg/timestamptz-string@1`, `pg/date-temporal@1`, `pg/time-temporal@1`, `pg/timestamp-temporal@1`, `pg/timestamptz-temporal@1`, `pg/timestamptz-date@1` | `string` | +| `pg/int4@1`, `pg/int2@1`, `pg/int8number@1`, `pg/int@1` | `int` | +| `pg/float4@1`, `pg/float8@1`, `pg/float@1` | `float` | +| `pg/int8@1`, `pg/unboundedint@1` | `bigint` | +| `pg/numeric@1` | `decimal` | +| `pg/json@1`, `pg/jsonb@1` | `json` | +| `pg/bool@1` | `boolean` | +| `pg/text-array@1` | none. It is internal to list handling and no column authored in a schema uses it | + +`pg/enum@1` names `string` because its JSON form is the member's storage string. This does not change how enum defaults are written; see section 6.6. + +**SQLite target, `packages/3-targets/3-targets/sqlite/src/core/codecs.ts`:** `sqlite/text@1`, `sqlite/blob@1` and `sqlite/datetime@1` name `string`. `sqlite/integer@1` and `sqlite/bigintnumber@1` name `int`. `sqlite/real@1` names `float`. `sqlite/bigint@1` names `bigint`. `sqlite/json@1` names `json`. + +**SQL base codecs, `packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts`:** `sql/text@1`, `sql/char@1` and `sql/varchar@1` name `string`. `sql/int@1` names `int`. `sql/float@1` names `float`. + +**Extensions:** `pg/vector@1` and `arktype/json@1` name `json`. `pg/geometry@1` names `string`, because its JSON form is hexadecimal text. + +**Mongo:** `mongo/string@1`, `mongo/bool@1`, `mongo/date@1`, `mongo/double@1`, `mongo/int32@1`, `mongo/vector@1`, `mongo/array@1` and `mongo/document@1` name nothing and do not change. No Mongo contract source reads a default from text. + +`sqlite/real@1` and `sql/float@1` refuse `NaN` and the infinities inside `decodeJson` already. Leave that. The `float` literal type carries those values and the codec rejects them, which produces the right error on those targets. + +### 6.3 The PSL interpreter reads literals through literal types + +Files: `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts`, `psl-column-resolution.ts`. + +The `@default(...)` argument today has separate arms for a string, a number, a boolean, a list, a function call, an enum member, and, since slice A, a tagged literal. Replace the string, number and boolean arms with one arm that yields the written scalar and its syntax kind, keep the list arm wrapping it, and leave the function-call, enum-member and tagged-literal arms alone. + +Resolution order for a default that is a literal: + +1. A tagged literal takes the literal type its tag writes, from the tag registry. The `sql` tag is not a literal type and keeps slice A's behaviour of lowering to a raw SQL default. +2. A plain scalar takes its literal type from the column's codec declaration. **A string scalar takes `string` and a boolean scalar takes `boolean`, both unambiguous. A number scalar is the blocked decision in section 1: do not implement it until Will has answered.** +3. If the literal's type is not one the column's codec names, report `PSL_DEFAULT_LITERAL_TYPE_INCOMPATIBLE` at the literal, with a message naming the codec and the types it accepts, for example `Field "Account.meta": pg/int4@1 is not compatible with a json literal; it accepts int literals`. +4. Ask the literal type to read the written text. Text it refuses is `PSL_INVALID_DEFAULT_LITERAL` at the literal, with the reason. A `json` body that is not valid JSON is `PSL_INVALID_JSON_LITERAL`. +5. Pass the value to the column codec's `decodeJson`. A thrown error becomes `PSL_INVALID_DEFAULT_LITERAL` at the literal, carrying the codec's message. +6. Store the default as it is stored today. + +A missing codec in the lookup is an internal error, not a diagnostic: the lookup that resolved the column must carry its codec. + +### 6.4 The `json` tag + +Each SQL target registers a `json` tag in the registry slice A added, `ControlMutationDefaults.defaultLiteralTagRegistry`. Register it with no prefixed alias: `json` only, on Postgres and on SQLite. The registry entry says which literal type the tag writes. A tag no pack registers keeps slice A's diagnostic, `PSL_UNKNOWN_DEFAULT_LITERAL_TAG`. + +### 6.5 The earlier Prisma version's schema reader + +Files under `packages/2-sql/2-authoring/contract-prisma7/src/`, principally `defaults.ts` and `target-binding.ts`, plus `packages/3-targets/3-targets/postgres/src/core/prisma7-binding.ts`. + +That reader has its own per-type handling for defaults, including a rule that `Int` and `BigInt` defaults must be whole numbers, and a path that parses quoted JSON text. Replace both with the same route as section 6.3: map the written syntax to a literal of a type, check it against the codec's declaration, read it with the literal type, pass it to `decodeJson`. Its diagnostics keep their existing code, `PSL.PRISMA7_UNKNOWN_DEFAULT`, with the reason from the literal type or the codec. + +Two behaviours of that reader stay exactly as they are: + +- A `Bytes` or `DateTime` default is carried as a raw SQL expression rather than a value, because verification cannot yet compare those as typed values. This is decision D11 in the project spec. +- Its diagnostic for a JSON default of `null`, `PSL.PRISMA7_JSON_NULL_DEFAULT_UNSUPPORTED`, stays and keeps firing. + +That language writes a JSON default as a quoted string, such as `@default("{\"a\":1}")`. The reader turns that quoted string into a `json` literal itself. Prisma 8's own PSL does not accept that form; see section 6.7. + +### 6.6 The printer + +Files: `packages/2-sql/9-family/src/core/psl-contract-infer/default-mapping.ts` and the Postgres printer under `packages/3-targets/3-targets/postgres/src/core/psl-infer/`. + +`contract infer` reads a live database and prints a schema. For a literal default it must now take the literal type the column's codec names, ask that type to write the stored value, and print the result: as a plain scalar where the literal type has one, otherwise as a tagged literal with the tag that writes it. When the codec names no literal type, or the literal type cannot write the value, keep whatever the printer does on `main` for a default it cannot express. Slice C changes that fallback; you do not. + +Delete the per-type formatter table in the Postgres printer, `PslDefaultValueFormat`, `pslDefaultValueFormat`, `formatPslValue`, `formatPslListLiteralValue` and the helpers they use, and the equivalent literal formatting in the family's `default-mapping.ts`. + +### 6.7 Behaviour that changes for people who already wrote schemas + +Three forms that work today become errors. Each needs an entry in the app-author upgrade instructions; section 9 says where. + +- A JSON column with a quoted JSON string, `Jsonb @default("{}")`. It becomes `` Jsonb @default(json`{}`) ``. +- A decimal column with a quoted decimal, `Decimal @default("1.50")`. It becomes `Decimal @default(1.50)`. +- Any other quoted value on a column whose codec names a non-`string` literal type. + +Two forms are unchanged and must stay working: an enum column's default is a bare member name, such as `@default(ACTIVE)`; and a list column keeps PSL's list syntax, `Int[] @default([1, 2])`, where each element is checked against the element codec's declaration, so `` Jsonb[] @default([json`{}`, json`[]`]) `` is valid and `Int[] @default([1, "x"])` is refused at its second element. + +`` Json @default(json`null`) `` is allowed and stores JSON null. + +### 6.8 What you must not change + +- The contract format. Every `contract.json` already in the repository must come out byte-identical. `pnpm fixtures:check` is the proof. +- The `Codec` interface. No codec gains a method in this slice. +- The TypeScript contract builder's `.default(value)`, which passes a value of the codec's own type and is checked by TypeScript. +- Anything about `dbgenerated`, which slice C removes. +- The DDL rendering half of ADR 184, which stays future work. + +## 7. Order of work + +1. Rewrite the slice specification (section 5). +2. Literal types in the framework, with their own tests (section 6.1). +3. The descriptor member and the full inventory of declarations, with a test per pack asserting every registered codec's declaration (section 6.2). +4. The `json` tag on both targets (section 6.4). +5. The interpreter, for the `string`, `boolean` and `json` literal types only (section 6.3, steps 1 and 3 to 6). +6. The printer for the same types (section 6.6). +7. The earlier Prisma version's reader (section 6.5). +8. **Stop. The numeric literal types need the blocked decision from section 1.** If it is answered by now, implement them across the interpreter, the printer and the reader, and update the slice specification first. +9. Documentation and upgrade instructions (section 9). +10. Full gates and the pull request (sections 10 and 11). + +Steps 5 to 7 leave the repository in a state where numeric defaults do not yet work, so the branch is not ready to merge until step 8 lands. Do not open the pull request before then. + +## 8. What to reuse from the closed branch + +Branch `remove-dbgenerated-codec-psl-literals`. Take these by hand; ignore everything else on it, especially anything adding `encodePsl`, `decodePsl`, a `PslLiteral` type, or a `literal()` parser combinator. + +- **The end-to-end test** at `test/integration/test/cli-journeys/codec-psl-literal-defaults.e2e.test.ts`, which emits a schema using every literal kind, runs `db init`, verifies the database, and reads a row back through the client. Adapt its schema to the new syntax, in particular the JSON column. +- **The JSON round-trip case** in `test/integration/test/cli-journeys/infer-roundtrip-fidelity.e2e.test.ts`, which asserts that a `jsonb` default survives infer, emit and verify without a workaround. +- **The float fix** in `packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts`: `pg/float4@1` and `pg/float8@1` carry `NaN` and the infinities as text in their JSON form and on the wire, and read them back as numbers. The `float` literal type depends on this. +- **The decimal canonicalisation behaviour** for `pg/numeric@1`, as test cases. The logic itself comes from `main` as section 6.1 says. +- **The Postgres printer's codec lookup**, `packages/3-targets/3-targets/postgres/src/core/psl-infer/infer-default-codec.ts`, which finds the codec that `contract emit` binds to a printed PSL type name. The printer needs a codec and this is how it gets one. Note the reason recorded there: several codecs share one database type, so the printed type name, not the database type, picks the codec. + +One trap from that branch: when the `@default` argument arms change, the language server's completion for `@default(` loses its `true` and `false` suggestions unless the new arm offers them. There is a test for it in `packages/1-framework/3-tooling/language-server`. + +## 9. Documentation to update in the same pull request + +- `docs/reference/error-reference.md`: add `PSL_DEFAULT_LITERAL_TYPE_INCOMPATIBLE`, `PSL_INVALID_DEFAULT_LITERAL` and `PSL_INVALID_JSON_LITERAL`, in the form its neighbours use, and update the earlier-version reader's message wording where it changed. +- `docs/reference/codec-authoring-guide.md`: a codec descriptor names its literal types; show one example. +- `packages/2-sql/2-authoring/contract-psl/README.md`: one paragraph on how a literal default is written and what the column's codec accepts. +- Upgrade instructions for the three broken forms in section 6.7, following the `record-upgrade-instructions` skill in `skills-contrib/record-upgrade-instructions/`. Both audiences need one: app authors, whose schemas change; and extension authors, whose codec descriptors should name literal types. The repository check for this is `pnpm check:upgrade-coverage --mode pr`. +- If ADR 253 has merged and your implementation diverges from it in any way, update the ADR in this pull request and say so in the description. + +## 10. How to work + +- Follow the Drive process: invoke the `drive-process` skill and run the slice with one implementer and one reviewer, resumed across dispatches. +- Tests before implementation, as `CLAUDE.md` requires. Every named test must fail before the change that makes it pass. +- Use `pnpm`, never `npm` or `npx`. Use the shell's Node; do not switch versions. +- Run slow commands through their `:agent` variants and read the log file each prints, per `.agents/rules/running-tests.mdc`. +- No `any`, no bare `as` casts in production code, no comments that restate the code, and test names that omit "should". The rules under `.agents/rules/` are not optional. +- Stage files explicitly and sign off every commit, per `.agents/rules/git-staging.mdc`. Do not push until the work is ready for review. + +## 11. Definition of done + +- `pnpm typecheck`, `pnpm test:packages`, `pnpm test:integration`, `pnpm test:e2e`, `pnpm lint`, `pnpm lint:deps`, `pnpm lint:docs` and `pnpm fixtures:check` all green, the last with no contract file changed. +- `pnpm lint:throws` and `pnpm check:upgrade-coverage --mode pr` green. Neither runs under `pnpm lint`, and continuous integration fails without them. +- `git grep -n "numberLiteralDefault\|PslDefaultValueFormat\|formatPslValue\|formatPslListLiteralValue" -- packages` returns nothing. +- A test per pack asserts that every codec it registers declares the literal types this brief's inventory gives it, so a codec added later without a declaration fails. +- The end-to-end test from section 8 passes against a real database, covering every literal type. +- One pull request against `main`, with a description that follows the `create-pr` skill in `skills-contrib/create-pr/`. This project has no Linear ticket, so omit the ticket prefix from the title and say so in the checklist. + +## 12. Stop and ask Will if + +- The blocked decision in section 1 is still unanswered when you reach step 8. +- A codec's JSON form turns out not to match the literal type this brief assigns it, so the declaration would need a conversion function after all. That would reopen the ADR's central claim. +- Making a codec declaration work would require changing what a `contract.json` stores. +- A contract source other than PSL and the earlier version's reader turns out to read literal defaults. +- The rewritten slice specification would need a design decision this brief does not give you. diff --git a/projects/remove-dbgenerated/slices/b-codec-psl-literals/spec.md b/projects/remove-dbgenerated/slices/b-codec-psl-literals/spec.md index c7c9e03f2feb..733ba1d33b67 100644 --- a/projects/remove-dbgenerated/slices/b-codec-psl-literals/spec.md +++ b/projects/remove-dbgenerated/slices/b-codec-psl-literals/spec.md @@ -1,3 +1,5 @@ +> **Superseded 2026-09-17.** Project spec decisions D9 and D10 were amended: every literal default has a literal type, codec descriptors declare the literal types they are compatible with, and PSL writes a literal of a type as a PSL scalar or with a tag. This spec's `encodePsl`/`decodePsl` design (B1, B2, B3, B4) is withdrawn. The design is recorded in ADR 253 (`docs/architecture docs/adrs/ADR 253 - Literal types for column defaults.md`); this spec is rewritten to match it before implementation. + # Slice B — Codec-owned PSL literals (the PSL half of ADR 184) **Project:** [Remove `dbgenerated`](../../spec.md). **Linear:** not yet created. **Branch:** `remove-dbgenerated-codec-psl-literals` off `main`. **Shape:** one PR. **Runs in parallel with:** [slice A](../a-sql-default-literal/spec.md). **Touches nothing slice A touches** except the `@default` argument arms in `sql-attribute-specs.ts`, where each slice adds its own arm. diff --git a/projects/remove-dbgenerated/spec.md b/projects/remove-dbgenerated/spec.md index 6a1ac95e7532..5509f56209a0 100644 --- a/projects/remove-dbgenerated/spec.md +++ b/projects/remove-dbgenerated/spec.md @@ -7,7 +7,7 @@ `@default(dbgenerated("..."))` lets a schema put arbitrary SQL into a column default as an unnamed string. ADR 167 accepted it as a stopgap while typed default literals were unfinished. It was never meant to ship in Prisma 8. This project removes it and builds the two things it was standing in for: 1. A designed way to write a raw SQL default: the ADR 129 tagged literal, written `@default(sql\`...\`)` or `@default(sql"...")` in PSL, and `.default(sql\`...\`)` in TypeScript. -2. The codec-owned PSL literal layer that ADR 184 decided and nobody built, so that every typed literal default (JSON, big integers, decimals, timestamps, and so on) is read from PSL and printed back to PSL by the column's codec, with no special cases in the interpreter or the printer. +2. Typed literal defaults checked and converted by the column's codec: every literal default has a literal type (`string`, `number`, `boolean`, `json`), each codec declares the literal types it is compatible with, PSL writes a literal of a type either as a plain PSL scalar or with a tag, and the interpreter and printer have no per-type code. When both exist, `dbgenerated` is deleted everywhere, the shipped Supabase contract is regenerated without it, and users get an upgrade instruction. @@ -56,13 +56,21 @@ The SQL family contract builder exports `sql` (template tag), `now()`, and `auto Nobody can tell from a function's name whether it returns a value of the column's type, for a list column or for any other column. That is the author's responsibility and the database reports the error if it is wrong. So `tags String[] @default(sql`'{}'::text[]`)` and `tags DateTime[] @default(now())` both lower. Two defaults are still refused on a list column: client-side generators (`uuid()`, `cuid()`, `ulid()`, `nanoid()`), which generate one value, and `autoincrement()` (`PSL_LIST_AUTOINCREMENT_UNSUPPORTED`), which is a Prisma marker for a sequence-backed scalar column rather than SQL and would otherwise be rendered as a scalar `SERIAL` column with no error from the database. -### D9. Codecs own the PSL form of every literal +### D9. Every literal default has a literal type; PSL writes it as a scalar or with a tag -`encodePsl` and `decodePsl` become required members of the framework `Codec` interface and abstract members of `CodecImpl`. Every codec class in the repository implements them, including Mongo codecs. The interpreter passes a literal's normalised content and kind to the column codec's `decodePsl` and stores the result through `encodeJson` as today. The printer calls `encodePsl` and writes the result. The numbers-only shortcut in the interpreter, the per-codec formatter table in the Postgres infer printer, and the Prisma 7 source's JSON literal handling are all deleted. The full interface, the rule for what each codec's PSL form is, and the list of codecs are in [slice B](slices/b-codec-psl-literals/spec.md). ADR 184 is amended to say this was always the design and the interface is a consumer interface satisfied by the pack's codecs, not a separate entity. +*Amended 2026-09-17; replaces the 2026-09-16 D9. Full design: ADR 253.* A literal default has a literal type, and the literal type says what the value is, independent of how PSL writes it. There are seven: `string`, `boolean`, `int`, `float`, `bigint`, `decimal`, and `json`. Each one is defined in the framework, and each produces the value shape the codecs that name it already accept in `decodeJson`, so `int` gives a whole JSON number, `bigint` gives the digits as text, `decimal` gives decimal text with its trailing zeros, and `json` gives a JSON value. -### D10. The codec receives normalised content, not source text with fences +PSL writes a literal of a given type in one of two ways. The plain PSL scalars write `string`, `boolean`, and the numeric literal types. A tag writes a literal of the type the tag names, as in `` @default(json`{ "a": 1 }`) ``; the tag registry entry says which literal type a tag writes, and each SQL target registers `json` with no prefixed alias. Both ways lower to the same literal. The syntax tree keeps what was written, for the formatter and the language server. -`decodePsl` receives `{ kind, text }` where `kind` is `string`, `number`, or `boolean`, and `text` is the literal's content with the quotes removed and escape sequences resolved for a string, the digits exactly as written for a number, and `true` or `false` for a boolean. The codec never sees the fence. A number is never converted to a JavaScript number before the codec sees it. `encodePsl` returns the same shape and the printer adds quotes and escapes. +The `sql` tag is different: it writes a raw SQL expression, not a value of the column's type. It lowers to a raw SQL default as D2 describes, and no codec check applies to it. + +**Open, to discuss with the operator before implementation:** a plain number scalar names no literal type of its own, so `42` is an `int` literal on an `Int` column, a `bigint` literal on a `BigInt` column, and a `decimal` literal on a `Decimal` column, decided by what the column's codec declares. Whether to accept that, to give each numeric literal type its own tag, or to keep one `number` literal type that each codec converts, is not settled. The numeric literal types are not implemented until it is. ADR 253 records the three options. + +### D10. Codecs declare the literal types they are compatible with + +*Amended 2026-09-17; replaces the 2026-09-16 D10. Full design: ADR 253.* A codec descriptor names the literal types a column of that codec is compatible with, as static metadata beside `traits` and `targetTypes`. The declaration carries names only: the literal type produces the value the codec's existing `decodeJson` accepts, so no codec gains a method and no conversion code is written per codec. The declaration is optional, and a codec that names no literal type accepts no literal defaults. + +A default whose literal type the column's codec does not name is a diagnostic at the literal that names the codec and its compatible literal types, for example `pg/int4@1 is not compatible with a json literal; it accepts int literals`. The interpreter then passes the literal type's value to `decodeJson`, so a value the codec refuses, such as a vector of the wrong length, is reported with the codec's own message. The contract stores the JSON form as it does today (D1). ### D11. The DDL side of ADR 184 is out of scope @@ -70,7 +78,7 @@ Nobody can tell from a function's name whether it returns a value of the column' ### D12. `contract infer` prints the new forms and never a gap -When a Postgres default is a named function, infer prints the named function. When the column codec can read it as a literal, infer prints the literal through `encodePsl`. Otherwise infer prints `@default(sql\`\`)`, switching to the double-quote fence when the expression contains a backtick. Infer never emits a comment in place of a default and never stops on one. The family printer's `// Raw default:` comment fallback is deleted. +When a Postgres default is a named function, infer prints the named function. When the column's codec names a literal type that can write the value, infer prints it as a literal of that type: as a plain PSL scalar where the type has one, otherwise with the type's tag. Otherwise infer prints `@default(sql\`\`)`, switching to the double-quote fence when the expression contains a backtick. Infer never emits a comment in place of a default and never stops on one. The family printer's `// Raw default:` comment fallback is deleted. ### D13. The Prisma 7 source maps `dbgenerated` directly @@ -87,7 +95,7 @@ Slice A implements completion of registered tags inside `@default(`, because the - Migrating index expressions, check constraint bodies, and RLS predicates from plain strings to tagged literals (deferred). - `encodeDdl` and `decodeDdl` (deferred, D11). - New named storage default functions. -- Any change to Mongo authoring. Mongo codecs implement the new methods; nothing calls them yet. +- Any change to Mongo authoring or Mongo codecs. Nothing reads a Mongo default from PSL. ## Cross-cutting requirements @@ -101,21 +109,21 @@ Slice A implements completion of registered tags inside `@default(`, because the ## Contract-impact -Entities affected: `ColumnDefault` (unchanged shape, new producers). `Codec` interface (two new required members, slice B). `ControlMutationDefaults` (a new tag registry beside the function registry, slice A). No migration of stored contracts. +Entities affected: `ColumnDefault` (unchanged shape, new producers). `Codec` interface (unchanged). Codec descriptors (a new declaration naming compatible literal types, slice B). Literal types are defined in the framework and the tag registry says which one each tag writes (slice B). `ControlMutationDefaults` (a new tag registry beside the function registry, slice A). No migration of stored contracts. ## Adapter-impact - Postgres adapter: registry gains the tag registry; loses `dbgenerated`. - SQLite adapter: registry gains the tag registry; loses `dbgenerated` and the `NOW_SYNONYMS` rewrite. -- Postgres target: infer prints the new forms; codecs implement PSL methods. -- SQLite target: verify-side default resolution hook; codecs implement PSL methods. -- Mongo: codecs implement PSL methods; nothing else. -- Extensions: pgvector, postgis, arktype-json codecs implement PSL methods. Supabase contract regenerated. +- Postgres target: infer prints the new forms; codec descriptors declare their compatible literal types. +- SQLite target: verify-side default resolution hook; codec descriptors declare their compatible literal types. +- Extensions: pgvector, postgis, arktype-json codec descriptors declare their compatible literal types. Supabase contract regenerated. ## ADR pointers - ADR 129 — amended in slice A: two fences, tag registration rule (D3), canonicalization applies to both fences, the `TaggedLiteral` node's fields as built. -- ADR 184 — amended in slice B: `encodePsl` and `decodePsl` are required members of `Codec` (D9, D10); the "single interface" alternative is not rejected for PSL; DDL methods remain future work. +- ADR 184 — its PSL half is replaced by ADR 253: a codec descriptor names the literal types it is compatible with, and gains no methods (D9, D10). DDL methods remain future work. +- ADR 129 — also amended in slice B: a tag writes a literal of a literal type; `sql` writes a raw SQL expression (D9). - ADR 167 — note added in slice C: the `dbgenerated(...)` stopgap is removed and what replaced it. ## Definition of done (project) @@ -150,6 +158,10 @@ Conclusions from the shaping discussion on 2026-09-16, with reasons, assumptions **Normalised content to the codec, no pre-parsing.** Why: converting a number literal to a JavaScript number before the codec sees it loses precision for big integers and decimals. Alternative rejected: passing source text with fences (the codec has no business with PSL quoting). +**Codecs declare the literal types they are compatible with; tags are how PSL writes a literal of a type (2026-09-17).** Why: it answers how to check a default literal against a codec whose SQL type can be anything. The check is a lookup, not a trial decode. The codec deals only in literal types and never sees PSL syntax, so the PSL scalars and the tags are two ways of writing the same thing. This replaces the 2026-09-16 entries "Codec interface, not a separate registry" and "Normalised content to the codec, no pre-parsing". Alternatives rejected: `encodePsl` and `decodePsl` on the codec, taking a string, number, or boolean kind (built in slice B's first PR and withdrawn, because it made the parser's classification the codec's input); passing the raw argument text to the codec (needs an unparsed argument form for `@default` alone). + +**The literal types are cut where the stored representations are cut (2026-09-18).** Why: codecs that hold numbers store different JSON forms, `42` for `pg/int4@1`, `"9007199254740993"` for `pg/int8@1`, and `"1.50"` for `pg/numeric@1`, and existing contract JSON must not change. Separate `int`, `bigint`, and `decimal` literal types each produce one of those forms, so the codec declaration is a list of names and no codec carries conversion code. Alternative rejected: one `number` literal type with a read and a write function per codec, which duplicates what each codec's `decodeJson` already does. The consequence that a plain number scalar names no literal type of its own is open, and recorded in D9. + **DDL side out of scope.** Why: no strong reason to do it now; the Prisma 7 source's workaround for bytes and timestamps is contained and recorded. **Infer prints, never stops.** Why: infer's job is to describe the database faithfully; a user adopting a database should not be halted on a default they then have to add by hand. Alternative rejected: reporting a gap and stopping.