@prisma/orm-mongo 8.0.0-rc.14-dev.46 → 8.0.0-rc.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1365 @@
1
+ ---
2
+ from: "8.0.0-rc.14"
3
+ to: "8.0.0-rc.15"
4
+ changes:
5
+ - id: contract-stores-data-type
6
+ summary: |
7
+ A SQL contract names each column's data type in `dataType` (for example `pg/int4`) instead of
8
+ its database type name in `nativeType`. Upgrade every extension that ships migrations in the
9
+ same step, then run the colocated script on the project: it rewrites every contract, writes
10
+ its lists in this release's new form, renames the snapshot directories to the new storage
11
+ hashes, and rewrites the migrations, refs, `migration.ts` files and `contract.d.ts` files
12
+ that name them.
13
+ detection:
14
+ glob: "**/*.json"
15
+ matches:
16
+ - '"nativeType"\s*:\s*"'
17
+ script: ./scripts/data-type-in-contract/data-type-in-contract.ts
18
+ - id: sign-databases-after-upgrade
19
+ summary: |
20
+ The upgrade changes every contract's storage hash, so every database's marker names a hash
21
+ the project no longer has. Run `prisma db sign` against every database before deploying the
22
+ application built with the new contract. `db sign` now signs every contract space, and its
23
+ `--json` document is `{ ok, summary, spaces, advancedRefs }`.
24
+ - id: column-descriptors-drop-native-type
25
+ summary: |
26
+ A hand-written column descriptor names its codec only: `{ codecId: 'pg/text@1' }` instead of
27
+ `{ codecId: 'pg/text@1', nativeType: 'text' }`. Code that reads or builds a column of a stored
28
+ contract uses `dataType` (for example `pg/text`) instead of `nativeType` (`text`).
29
+ detection:
30
+ glob: "**/*.{ts,mts,cts,tsx}"
31
+ matches:
32
+ - '(?<![\w$])(?<!readonly\s+)nativeType\s*:\s*[''"]'
33
+ - '\.nativeType\b'
34
+ - id: parameter-casts-use-base-names
35
+ summary: |
36
+ PostgreSQL parameter casts are written with the data type's base name: `$1::int4` instead of
37
+ `$1::integer`, and likewise `int2`, `int8`, `float4`, `float8` and `bool` instead of
38
+ `smallint`, `bigint`, `real`, `double precision` and `boolean`. Logged SQL and SQL snapshots
39
+ in tests change to match.
40
+ detection:
41
+ glob: "**/*.{ts,mts,cts,sql,json,snap}"
42
+ matches:
43
+ - '\$\d+::(?:integer|smallint|bigint|real|double precision|boolean)\b'
44
+ - id: ts-contract-lists-extension-codecs
45
+ summary: |
46
+ A TypeScript contract built with `defineContract` from `@prisma/orm-postgres/contract-builder`
47
+ or `@prisma/orm-sqlite/contract-builder` names each column's database type from the data type
48
+ its codec represents. A contract that uses an extension's codec without listing the extension
49
+ in `extensions` now fails with `CONTRACT.CODEC_DESCRIPTOR_MISSING`. List the extension.
50
+ detection:
51
+ glob: "**/*.{ts,mts,cts}"
52
+ matches:
53
+ - '(?<![\s\S])(?![\s\S]*GENERATED FILE - DO NOT EDIT)(?=[\s\S]*(?<![\w$])defineContract(?![\w$]))[\s\S]*[''"]@prisma/orm-extension-(?:pgvector|postgis|arktype-json)/column-types[''"]'
54
+ - '(?<![\s\S])(?![\s\S]*GENERATED FILE - DO NOT EDIT)(?=[\s\S]*(?<![\w$])defineContract(?![\w$]))[\s\S]*(?<![\w$])codecId\s*:\s*[''"](?:pg/vector|pg/geometry|arktype/json)@\d+[''"]'
55
+ - id: sqlite-contract-d-ts-char-aggregates
56
+ summary: |
57
+ On SQLite, `sql/char@1` and `sql/varchar@1` are registered codecs. Re-emit the contract:
58
+ `contract.d.ts` gains `min` and `max` aggregate rows for both codecs. `contract.json`, its
59
+ hashes and the migration SQL do not change.
60
+ detection:
61
+ glob: "**/contract.d.ts"
62
+ matches:
63
+ - '^(?![\s\S]*[''"]sql/char@1[''"]\s*:\s*\{\s*readonly output)[\s\S]*@prisma/orm-(?:target-)?sqlite/'
64
+ - id: column-helpers-raise-type-params-invalid
65
+ summary: |
66
+ pgvector's `vector(length)` and PostGIS's `geometry({ srid })` and `pgGeometryColumn({ srid })`
67
+ no longer check their arguments. Building the contract checks every column's parameters
68
+ against its data type: an argument outside its bounds now fails `defineContract` with
69
+ `CONTRACT.TYPE_PARAMS_INVALID` instead of failing the helper call with
70
+ `CONTRACT.ARGUMENT_INVALID`, and `srid: 0` is refused when the contract is built, not later
71
+ when a migration is planned.
72
+ detection:
73
+ glob: "**/*.{ts,mts,cts}"
74
+ matches:
75
+ - '\bCONTRACT\.ARGUMENT_INVALID\b'
76
+ - '\bsrid\s*:\s*0\b'
77
+ - id: reemit-explicit-list-cardinality
78
+ summary: Re-emit list contracts with nested element nullability while preserving scalar JSON.
79
+ - id: refresh-historical-list-contracts
80
+ summary: Refresh historical MongoDB contract snapshots and their migration references together; for SQL contracts, `contract-stores-data-type` rewrites them.
81
+ - id: mongo-nullable-list-containers
82
+ summary: Re-emit Mongo contracts and migrate validators to accept nullable list containers.
83
+ - id: domain-types-match-their-columns
84
+ summary: |
85
+ The domain half of an emitted SQL contract now carries the type parameters and enum value sets the schema declares: on fields typed by a named type, on enum list fields, and on composite type members. In `contract.d.ts`, a composite type member with type parameters now has the parameterized output type. Re-emit the contract. This change leaves the storage half, every hash and migration snapshots unchanged.
86
+ detection:
87
+ glob: "**/contract.json"
88
+ matches:
89
+ - '"typeRef"\s*:'
90
+ - '"valueObjects"\s*:'
91
+ - '"valueSet"\s*:'
92
+ - id: value-object-default-matches-composite-type
93
+ summary: |
94
+ A literal default on a field typed by a composite type must now match the composite type, with each member value read by the member's codec and each enum member value one of the enum's values, or the schema is refused. Fix the default the diagnostic names.
95
+ detection:
96
+ glob: "**/*.prisma"
97
+ matches:
98
+ - '^\s*type\s+\w+\s*\{'
99
+ - id: composite-type-attributes-refused
100
+ summary: |
101
+ An attribute on a composite type or on one of its members is now refused, where it used to be ignored. Remove it.
102
+ detection:
103
+ glob: "**/*.prisma"
104
+ matches:
105
+ - '^\s*type\s+\w+\s*\{'
106
+ - id: codecs-check-stored-json
107
+ summary: |
108
+ A TypeScript `.default()` value or `enumType` member that its column's codec does not take is now refused when the contract is built, with `CONTRACT.DEFAULT_INVALID` or `CONTRACT.ENUM_INVALID`. A `contract.json` that holds such a default stops `db init`, `db update` and `migration plan` with `CONTRACT.DEFAULT_INVALID`, and a `migration.ts` that holds one fails when it runs. Correct the value the error names.
109
+ detection:
110
+ glob: "**/*.{ts,mts,cts,tsx}"
111
+ matches:
112
+ - '\.default\(\s*(?!now\(|sql`|autoincrement\()'
113
+ - '\benumType\s*\('
114
+ - id: psl-values-checked-by-codecs
115
+ summary: |
116
+ A PSL schema whose SQL enum member its codec does not take, or whose literal default its column's type does not hold, is now refused at `contract emit`, where it used to load. Correct the member or the default.
117
+ detection:
118
+ glob: "**/*.prisma"
119
+ matches:
120
+ - '@@type\(\s*"(?:pg|sql|sqlite)/'
121
+ - '^\s*\w+\s*=\s*-?\d{10,}\s*$'
122
+ - '@default\(\s*(?:"|-?\d|\[)'
123
+ - id: uuid-defaults-stored-as-postgresql-writes
124
+ summary: |
125
+ A uuid default written in upper case, in braces or without hyphens, in PSL or in a TypeScript `.default()`, is now stored as PostgreSQL writes it, so emitting the contract again changes its storage hash. Earlier versions could not apply such a contract: the command that applied it failed and changed nothing. Emit the contract again, then run that command again. With migrations, first delete the migration package that never applied.
126
+ detection:
127
+ glob: "**/*.{prisma,ts,mts,cts,tsx}"
128
+ matches:
129
+ - '\bUuid\b[^\n]*@default\(\s*"(?![0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")\{?[0-9A-Fa-f]{4}'
130
+ - '\b(?:uuidNative|pgUuidColumn)\s*\([^\n]*\.default\(\s*[''"`](?![0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[''"`])\{?[0-9A-Fa-f]{4}'
131
+ - '^\s*\.default\(\s*[''"`](?![0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[''"`])(?=[^''"`\n]*[A-F{-])\{?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}\}?[''"`]'
132
+ - id: ts-enum-member-written-as-stored
133
+ summary: |
134
+ `defineContract` from the Postgres and SQLite packages now refuses an `enumType` member that its codec takes but stores as a different value, with `CONTRACT.ENUM_INVALID`. A uuid member written with an upper-case hex digit, in braces, or with hyphens anywhere other than the 8-4-4-4-12 positions Postgres prints (including none) is refused, because `pg/uuid@1` stores lower-case 8-4-4-4-12 text. Write each refused member as the error message says, re-emit, and apply a migration that replaces the enum's CHECK constraint.
135
+ detection:
136
+ glob: "**/*.{ts,tsx,mts,cts}"
137
+ matches:
138
+ - '\benumType\('
139
+ - id: mongo-codecs-check-json
140
+ summary: |
141
+ The built-in Mongo codecs now refuse a JSON value that is not the JSON form of their type, where most passed it through: a PSL enum member whose value its `@@type` codec does not take is now refused at `contract emit`, and a TypeScript `enumType` member that `mongo/objectId@1` or `mongo/int32@1` does not hold is refused when the contract is built. Correct the member.
142
+ detection:
143
+ glob: "**/*.{prisma,ts,mts,cts,tsx}"
144
+ matches:
145
+ - '@@type\(\s*"mongo/'
146
+ - '[''"]mongo/(?:objectId|int32)@1[''"]'
147
+ - '\bMONGO_(?:OBJECTID|INT32)_CODEC_ID\b'
148
+ - id: text-array-elements-nullable
149
+ summary: |
150
+ A `textArray()` column's elements are now typed `string | null`, because a `text[]` holds NULL elements, which it reads as `null`. Handle the `null`.
151
+ detection:
152
+ glob: "**/*.{ts,mts,cts,tsx}"
153
+ matches:
154
+ - '\btextArray\s*\('
155
+ - '[''"]pg/text-array@1[''"]'
156
+ - id: char-reads-drop-only-padding
157
+ summary: |
158
+ A `char(n)` column now reads the same through `.include()` as through a flat read: without the trailing spaces that pad it, where an include used to return them, and keeping a trailing tab or newline, which a flat read used to drop. Compare `char` values without their padding.
159
+ detection:
160
+ glob: "**/contract.json"
161
+ matches:
162
+ - '"codecId"\s*:\s*"(?:sql|pg)/char@1"'
163
+ - id: sqlite-nan-parameters-refused
164
+ summary: |
165
+ On SQLite, NaN written to a float column or used as a filter value now throws `RUNTIME.ENCODE_FAILED` naming the codec, where SQLite stored NULL or matched nothing. Write `null` for no value.
166
+ detection:
167
+ glob: "**/contract.json"
168
+ matches:
169
+ - '"target"\s*:\s*"sqlite"'
170
+ - id: sqlite-int-include-refuses-inexact-values
171
+ summary: |
172
+ On SQLite, an `.include()` of a row whose `sql/int@1` column holds an INTEGER past 2^53, or a REAL, now throws `RUNTIME.DECODE_FAILED`, where it read the value rounded or with a fraction. Store such values in a `BigInt` or `Float` column.
173
+ detection:
174
+ glob: "**/contract.json"
175
+ matches:
176
+ - '"codecId"\s*:\s*"sql/int@1"'
177
+ - id: prefixed-sql-tags-are-removed
178
+ summary: |
179
+ The tags `pg.sql` and `sqlite.sql` are removed. Write `sql`.
180
+ detection:
181
+ glob: "**/*.{prisma,ts}"
182
+ matches:
183
+ - '(?<![\w./-])(pg|sqlite)\s*\.\s*sql\s*\\?[\x60"'']'
184
+ - id: default-diagnostic-codes-changed
185
+ summary: |
186
+ Four PSL diagnostic codes for written values changed: `PSL_UNKNOWN_DEFAULT_LITERAL_TAG` is now `PSL_UNKNOWN_LITERAL_TAG`, `PSL_INVALID_JSON_LITERAL` is now `PSL_INVALID_LITERAL`, `PSL_DEFAULT_TYPE_INCOMPATIBLE` is now `PSL_VALUE_TYPE_INCOMPATIBLE`, or `PSL_DEFAULT_LIST_EXPECTED` for a single value on a list column, and most cases of `PSL_INVALID_DEFAULT_LITERAL` moved to `PSL_INVALID_LITERAL`.
187
+ detection:
188
+ glob: "**/*.{ts,mts,cts,js,mjs}"
189
+ matches:
190
+ - '\bPSL_INVALID_JSON_LITERAL\b'
191
+ - '\bPSL_UNKNOWN_DEFAULT_LITERAL_TAG\b'
192
+ - '\bPSL_DEFAULT_TYPE_INCOMPATIBLE\b'
193
+ - '\bPSL_INVALID_DEFAULT_LITERAL\b'
194
+ - id: migration-ts-column-defaults
195
+ summary: |
196
+ In `migration.ts`, the adapter writes every column default, reading it with the column's codec. Postgres `setDefault` takes the column as `col(name, type, { default, codecRef })` instead of `column` (the name) and `defaultSql`. A SQLite `addColumn` or `recreateTable` column carries `default` and `codecRef` instead of `defaultSql`, and a `recreateTable` postcheck for a default is `{ description, columnDefault }`. An earlier `migration.ts` that uses `defaultSql` no longer compiles, and running it with `node migration.ts` stops with `MIGRATION.OPERATION_OPTION_REMOVED`; its `ops.json` still applies.
197
+ detection:
198
+ glob: "**/migration.ts"
199
+ matches:
200
+ - '\bdefaultSql\s*:'
201
+ - id: postgres-changed-default-applied
202
+ summary: |
203
+ On PostgreSQL, `db update` and `db migrate` now change a column default that is already there. They used to skip the change and then fail with `MIGRATION.SCHEMA_VERIFY_FAILED`. A migration an earlier version planned still skips it: before you apply it, delete its package and plan it again, or rewrite its `setDefault` call to the form `migration-ts-column-defaults` shows and run its `migration.ts` to write `ops.json` again.
204
+ detection:
205
+ glob: "**/ops.json"
206
+ matches:
207
+ - '"id":\s*"setDefault\.'
208
+ - id: cli-error-from-caught
209
+ summary: |
210
+ `mapCaughtMigrationError` is removed from `@prisma/orm-toolchain/cli/control-api` (`@internal/cli/control-api`). Use `errorFromCaught(error, why)`, which always returns an error: a CLI error as it is, any error with a structured code as itself, and anything else as `CLI.UNEXPECTED` with `why(message)`. It throws an `InternalError` again.
211
+ detection:
212
+ glob: "**/*.{ts,mts,cts,tsx}"
213
+ matches:
214
+ - '\bmapCaughtMigrationError\b'
215
+ - id: sql-with-a-line-comment-gets-a-new-wire-name
216
+ summary: |
217
+ An index, check or policy whose SQL body contains both `--` and a line break gets a new name in `contract.json` once. The next `migration plan` drops and recreates the object.
218
+ detection:
219
+ glob: "**/contract.json"
220
+ matches:
221
+ - '"(?:[^"\\]|\\.)*?--(?:[^"\\]|\\.)*?\\n|"(?:[^"\\]|\\.)*?\\n(?:[^"\\]|\\.)*?--'
222
+ - id: strict-verify-unclaimed-code
223
+ summary: |
224
+ `prisma db verify --strict` now reports a database holding tables no contract declares under `CONTRACT.SCHEMA_VERIFICATION_FAILED`, in both the diagnostic and the JSON result's `code`. It used to report `CONTRACT.MARKER_REQUIRED`. The exit code is still 4. `CONTRACT.MARKER_REQUIRED` now only means the database has not been signed.
225
+ detection:
226
+ glob: "**/*.{ts,mts,cts,js,mjs,cjs,sh,yml,yaml,json}"
227
+ matches:
228
+ - 'CONTRACT\.MARKER_REQUIRED'
229
+ - id: prisma7-schema-date-types-are-text
230
+ summary: |
231
+ A contract from `prisma7Schema(...)` now reads a Prisma 7 `DateTime` column and `@db.Timestamp`, `@db.Timestamptz`, `@db.Date` and `@db.Time` columns as the text PostgreSQL prints (`TimestampString(3)`, `TimestampString(p)`, `TimestamptzString(p)`, `DateString`, `TimeString(p)`), not as `Temporal` values. `@updatedAt` still writes UTC. The application needs no `Temporal` for them. Re-emit, change code that treats these fields as `Temporal` values, then run `prisma db sign`.
232
+ detection:
233
+ glob: "**/prisma.config.{ts,mts,cts,js,mjs}"
234
+ matches:
235
+ - '\bprisma7Schema\s*\('
236
+ - id: contract-infer-writes-text-date-types
237
+ summary: |
238
+ `prisma contract infer` now writes `TimestampString(p)`, `TimestamptzString(p)`, `DateString` and `TimeString(p)` for `timestamp`, `timestamptz`, `date` and `time` columns, where it wrote `Timestamp(p)`, `Timestamptz(p)`, `Date` and `Time(p)`. A contract inferred earlier keeps its types until infer runs again.
239
+ detection:
240
+ glob: "**/*.prisma"
241
+ matches:
242
+ - 'Contract inferred from the live database schema'
243
+ - id: text-timestamp-now-is-utc
244
+ summary: |
245
+ A `timestamp` column of type `TimestampString(p)` that the ORM fills with `now` now receives the UTC wall-clock time on a host outside UTC. Before, it received the host's local time. No code changes.
246
+ detection:
247
+ glob: "**/*.{prisma,ts,mts,cts}"
248
+ matches:
249
+ - '\btimestampString\s*\([^)\n]*\bnow\b'
250
+ - id: contract-infer-writes-bytea-default-literals
251
+ summary: |
252
+ `prisma contract infer` now writes a `bytea` column default as a base64 literal, `@default("aGVsbG8=")`, where it wrote ``@default(sql`'\\x68656c6c6f'::bytea`)``. A contract emitted from the new output stores a value instead of an expression, so it gets a new storage hash while the database does not change. Re-emit, then `prisma db sign`, or record an empty migration with `prisma migration new`.
253
+ detection:
254
+ glob: "**/*.prisma"
255
+ matches:
256
+ - '\bBytes(?:\[\])?\??[ \t]+[^\n]*@default\(sql[^\n]*::bytea'
257
+ - id: prisma6-int-written-as-long
258
+ summary: |
259
+ A contract read with `prisma6Schema(...)` now gives a plain Prisma 6 `Int` field, and `Int @db.Long`,
260
+ the `mongo/int64Number@1` codec: the application type stays `number`, and Prisma 8 now writes these
261
+ fields as a BSON long, as Prisma 6 does, instead of an int. A document in which such a field holds a
262
+ fractional number, which the previous contract let Prisma 8 write, now fails to read with
263
+ `RUNTIME.DECODE_FAILED`: repair those documents, then re-emit the contract.
264
+ detection:
265
+ glob: "**/prisma.config.{ts,mts,cts,js,mjs}"
266
+ matches:
267
+ - '\bprisma6Schema\s*\('
268
+ - id: prisma6-bytes-objectid-is-hex
269
+ summary: |
270
+ A Prisma 6 MongoDB schema with a native type Prisma 6 accepts now emits with `prisma6Schema(...)`
271
+ (only `DateTime @db.Timestamp` is still refused). A `Bytes @db.ObjectId` field holds an ObjectId, so
272
+ Prisma 8 reads it as a 24-digit hex string and writes only a hex string or an ObjectId; a 12-byte
273
+ `Buffer` or `Uint8Array`, which the Prisma 6 client uses for it, is refused.
274
+ detection:
275
+ glob: "**/*.prisma"
276
+ matches:
277
+ - '\bBytes(?:\[\])?\??[ \t]+[^\n]*@db\.ObjectId\b'
278
+ - id: writes-on-a-conditional-collection-are-refused
279
+ summary: |
280
+ A write (`update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`) on a collection that is filtered on some code paths and not on others no longer compiles. A pattern cannot tell which collections those are: act only where the compiler reports "The 'this' context of type '...' is not assignable to method's 'this' of type 'HasWhere'". Filter on every path, or make the write only where the filter was applied.
281
+ detection:
282
+ glob: "**/*.{ts,mts,cts,tsx}"
283
+ matches:
284
+ - '\.(?:updateAll|updateAndCount|deleteAll|deleteAndCount)\s*\('
285
+ - '\.update\s*\('
286
+ - '\.delete\s*\(\s*(?:\)|\()'
287
+ - id: writes-refuse-what-they-would-ignore
288
+ summary: |
289
+ `updateAll`, `updateAndCount`, `deleteAll` and `deleteAndCount` now throw `ORM.ARGUMENT_INVALID` on a collection that has a `limit`, an `offset`, a `cursor`, `distinct` or `distinctOn`. These writes change every row that matches the filter; their statement cannot apply any of these, so they were ignored and more rows changed than the chain asked for. Remove them before the write, or read the rows first and change them by their ids. `update` with a relation callback now throws on a collection with an order, a limit, an offset, a cursor, `distinct` or `distinctOn`, which it ignored; filter it to the one row instead. `update` and `delete` without a relation callback change the row `first()` returns, as before, except after `limit(0)`: they changed one row and now change none and return `null`.
290
+ detection:
291
+ glob: "**/*.{ts,mts,cts,tsx}"
292
+ matches:
293
+ - '\.(?:limit|offset|cursor|distinct|distinctOn)\s*\([^)]*\)[\s\S]{0,300}?\.(?:update|updateAll|updateAndCount|delete|deleteAll|deleteAndCount)\s*\('
294
+ - '\.orderBy\s*\([\s\S]{0,300}?\.update\s*\('
295
+ - id: cursor-and-distinct-on-check-the-receiver
296
+ summary: |
297
+ `cursor` and `distinctOn` now require an order on the collection they are called on, checked on the receiver. A cast on the argument, such as `cursor({ id } as never)`, no longer bypasses the check; add the `orderBy`, or cast the collection to `Ordered<C>` where the query is meant to have no order.
298
+ detection:
299
+ glob: "**/*.{ts,mts,cts,tsx}"
300
+ matches:
301
+ - '\.(?:cursor|distinctOn)\s*\([^)]*\bas\s+never\b'
302
+ - id: apply-is-a-collection-member
303
+ summary: |
304
+ Every collection now has an `apply` method. A custom collection class that declares its own `apply` member with another signature no longer compiles; rename it.
305
+ detection:
306
+ glob: "**/*.{ts,mts,cts,tsx}"
307
+ matches:
308
+ - '(?:^|\n)[ \t]*(?:(?:public|protected|private|readonly|static|async|override)\s+)*apply\s*[(<:=?]'
309
+ - id: scope-is-a-collection-member
310
+ summary: |
311
+ Every collection now has a `scope` method. A custom collection class that declares its own `scope` member with another signature no longer compiles; rename it.
312
+ detection:
313
+ glob: "**/*.{ts,mts,cts,tsx}"
314
+ matches:
315
+ - '(?:^|\n)[ \t]*(?:(?:public|protected|private|readonly|static|async|override|get|set)\s+)*scope\s*[!(<:=?]'
316
+ - id: overriding-a-chaining-method
317
+ summary: |
318
+ In a class that extends `Collection`, an override of a chaining method (`where`, `orderBy`, `limit`, `offset`, `distinct`, `distinctOn`, `cursor`, `include`) or of a method that returns rows (`all`, `first`, `create`, `createAll`, `upsert`, `update`, `updateAll`, `updateAndCount`, `delete`, `deleteAll`, `deleteAndCount`) must use the new signature, which takes a `this` parameter.
319
+ detection:
320
+ glob: "**/*.{ts,mts,cts,tsx}"
321
+ matches:
322
+ - '(?:^|\n)[ \t]*(?:(?:public|protected|override|async)\s+)*(?:where|orderBy|limit|offset|distinct|distinctOn|cursor|include|all|first|create|createAll|upsert|update|updateAll|updateAndCount|delete|deleteAll|deleteAndCount)\s*[(<]'
323
+ - id: collection-state-flags-are-boolean
324
+ summary: |
325
+ In `DefaultCollectionTypeState`, `hasWhere`, `hasOrderBy` and `hasUniqueFilter` are `boolean` (not known) instead of `false`. Code that expects `false` on a collection with no filter or order must expect `boolean`.
326
+ detection:
327
+ glob: "**/*.{ts,mts,cts,tsx}"
328
+ matches:
329
+ - '\bhas(?:Where|OrderBy|UniqueFilter)\b[''"]?\]?\s*,\s*false\b'
330
+ - '\bhas(?:Where|OrderBy|UniqueFilter)\s*:\s*false\b'
331
+ - id: read-collection-state-and-row-with-helpers
332
+ summary: |
333
+ A collection's type state and row are read with `CollectionTypeStateOf<C>` and `CollectionRowOf<C>`, not by inferring the type arguments of `Collection`. The type arguments keep what the collection started with.
334
+ detection:
335
+ glob: "**/*.{ts,mts,cts,tsx}"
336
+ matches:
337
+ - '\bCollection<[^;]*?\binfer\b'
338
+ - id: return-type-of-a-chaining-method
339
+ summary: |
340
+ `ReturnType` of `where`, `orderBy`, `limit`, `offset`, `distinct`, `distinctOn`, `cursor` or `include` no longer gives a collection. Write `Filtered<C>` after `where`, `Ordered<C>` after `orderBy`, and `C` after the others, or take `typeof` of a value.
341
+ detection:
342
+ glob: "**/*.{ts,mts,cts,tsx}"
343
+ matches:
344
+ - '\bReturnType<[^>]*\[[''"](?:where|orderBy|limit|offset|distinct|distinctOn|cursor|include)[''"]\]'
345
+ - '\bReturnType<\s*typeof\s+[\w$.]+\.(?:where|orderBy|limit|offset|distinct|distinctOn|cursor|include)\b'
346
+ - id: chaining-methods-take-no-explicit-type-arguments
347
+ summary: |
348
+ `include`, `distinct` and `distinctOn` with explicit type arguments no longer compile: `posts.include<'user'>('user')` and `posts.distinct<['title']>('title')` fail, and `ReturnType<typeof posts.include<'user'>>` is `never`. Drop the type arguments; they are inferred from the arguments, so `posts.distinct('title')` needs none.
349
+ detection:
350
+ glob: "**/*.{ts,mts,cts,tsx}"
351
+ matches:
352
+ - '\.(?:include|distinct|distinctOn)<'
353
+ - id: custom-collection-methods-chain
354
+ summary: |
355
+ Optional. Custom collection methods now stay available after the built-in chaining methods. Where code repeats a class method's body inline after a chaining call, it can call the method instead.
356
+ detection:
357
+ glob: "**/*.{ts,mts,cts,tsx}"
358
+ matches:
359
+ - '\bextends\s+Collection<'
360
+ - id: variant-takes-discriminator-value
361
+ summary: |
362
+ `.variant()` on a polymorphic SQL or Mongo ORM collection takes the discriminator value a variant declares instead of the variant's model name: `db.orm.public.Task.variant('bug')` for `@@base(Task, "bug")`, where it used to be `.variant('Bug')`. A value the model does not declare, or a call on a model with no discriminator, now throws `ORM.ARGUMENT_INVALID` instead of returning the collection unchanged. `.variant()` on a collection that already has a variant selected is now rejected: select the variant from the base collection.
363
+ detection:
364
+ glob: "**/*.{ts,tsx,mts,cts}"
365
+ matches:
366
+ - '\.variant\('
367
+ - id: imported-postgres-field-checks-defaults
368
+ summary: |
369
+ The `field` exported by the Postgres facade's `contract-builder` entry now has the Postgres presets and checks a `.default(...)` value against the Postgres target's column types, as the `defineContract` callback's `field` does; it does not know the column types an extension adds, such as pgvector's, so it does not check those. A default of the wrong type, which compiled before and failed when the contract was built, is now a compile error; give the value the column's type.
370
+ detection:
371
+ glob: "**/*.{ts,mts,cts,tsx}"
372
+ matches:
373
+ - 'import\s*\{[^}]*\bfield\b[^}]*\}\s*from\s*[''"]@(?:prisma/orm-|internal/)postgres/contract-builder[''"]'
374
+ - id: re-emit-for-the-row-locking-capabilities
375
+ summary: "The Postgres adapter reports seven new capability keys (sql.forUpdate, sql.forShare, sql.lockOf, sql.lockNowait, sql.lockSkipLocked, postgres.forNoKeyUpdate, postgres.forKeyShare), which gate the new row-locking methods on the SQL builder; a contract emitted before this release does not carry them and the methods are unavailable against it, so re-emit the contract before using them."
376
+ detection:
377
+ glob: "**/contract.json"
378
+ contains:
379
+ - '"distinctOn"'
380
+ - id: cache-annotation-ttl-removed
381
+ summary: "cacheAnnotation from @prisma/orm-extension-middleware-cache no longer takes ttl. Every annotated read is now cached, including cacheAnnotation({}) and cacheAnnotation({ key }), which used to pass through uncached; how long an entry lives is the store's policy (the default store: 60 seconds). Remove ttl from every cacheAnnotation call."
382
+ detection:
383
+ glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}"
384
+ matches:
385
+ - '\bcacheAnnotation\s*\('
386
+ - id: cache-annotation-skip-renamed-bypass
387
+ summary: "cacheAnnotation({ skip }) is now cacheAnnotation({ bypass }), and the CachePayload type is now CacheAnnotationOptions. Detection finds skip written inside a cacheAnnotation({ ... }) literal and any use of CachePayload; options built elsewhere without that type are not detected."
388
+ detection:
389
+ glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}"
390
+ matches:
391
+ - '\bCachePayload\b'
392
+ - '\bcacheAnnotation\s*\(\s*\{[^}]*\bskip\s*:'
393
+ - id: cache-middleware-store-options
394
+ summary: "createCacheMiddleware no longer takes maxEntries or clock. Pass them to createInMemoryCacheStore and hand that store to createCacheMiddleware({ store }), or drop them when they match the new defaults (1000 entries). Detection finds maxEntries or clock written in the options literal of a createCacheMiddleware call; it also matches an already-migrated createCacheMiddleware({ store: createInMemoryCacheStore({ maxEntries }) }), which needs no change."
395
+ detection:
396
+ glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}"
397
+ matches:
398
+ - '\bcreateCacheMiddleware\s*\(\s*\{[^}]*\b(maxEntries|clock)\s*:'
399
+ - id: cache-store-object-arguments
400
+ summary: "A custom CacheStore is now a cache of values with a version per key: CacheStore<TMeta, TValue>. get({ key, meta }) replaces get(key) and returns a CacheEntry { key, meta, version, data } whose data is { empty: true } or { empty: false, value }; set(entry, value) replaces set(key, entry, ttlMs), stores only if the key's version still equals entry.version, and returns whether it stored; a new required unset({ keys, meta }) removes values and increments their keys' versions. CachedEntry is replaced by CacheEntry, and the rows type by CachedRows."
401
+ detection:
402
+ glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}"
403
+ matches:
404
+ - '\bCacheStore\b'
405
+ - '\bCachedEntry\b'
406
+ - '\bcreateCacheMiddleware\s*\('
407
+ - id: contract-artifacts-restamp
408
+ summary: |
409
+ An extension that writes its own package version into the contracts it emits, such as the
410
+ Supabase extension, now writes 8.0.0-rc.15. Run `contract emit` once after upgrading so the
411
+ emitted `contract.json` and `contract.d.ts` match the installed extension.
412
+ detection:
413
+ glob: "**/contract.json"
414
+ contains:
415
+ - '"version": "8.0.0-rc.14"'
416
+ ---
417
+
418
+ # 8.0.0-rc.14 → 8.0.0-rc.15 — User upgrade instructions
419
+
420
+ ## Order of steps
421
+
422
+ Apply this guide from top to bottom. Several entries tell you to run `prisma contract emit`. In a SQL project, do the first steps in this order:
423
+
424
+ 1. Upgrade the framework packages and every extension that ships migrations in the same step (`contract-stores-data-type`).
425
+ 2. Run the `data-type-in-contract` script (`contract-stores-data-type`) before any step that re-emits a contract. The script changes only files in the old format.
426
+ 3. Run `prisma db sign` against every database (`sign-databases-after-upgrade`) before you re-emit. `db sign` must see the contract the database was created from, as the script rewrote it, before a re-emit changes it again.
427
+ 4. Make the changes the other entries describe in your schema, your TypeScript contract and your application code.
428
+ 5. Run `prisma contract emit`. One emit after all those changes covers every entry that asks you to re-emit.
429
+ 6. Where an entry says the re-emit changes the storage hash, follow that entry's database steps: sign again, or plan and apply a migration. Do this before you deploy the application built with the new contract.
430
+
431
+ A MongoDB project skips steps 2 and 3, because the script rewrites SQL contracts only.
432
+
433
+ ## `contract-stores-data-type`
434
+
435
+ Upgrade every extension that ships migrations (for example `@prisma/orm-extension-pgvector` and `@prisma/orm-extension-postgis`) in the same step as the framework, to the release its authors published for this change. An extension whose contract space is still in the old format makes the project refuse to load.
436
+
437
+ Do this before any other step of this release that re-emits a contract: the script changes only files in the old format, and `prisma db sign` (`sign-databases-after-upgrade`) must see the contract the database was created from before a re-emit changes it again. Commit your work first, so the script's changes can be reviewed and undone with git. The script follows links to files and directories, also outside the root, and rewrites the files there; commit or back up those too. Then run the script from the project root:
438
+
439
+ ```sh
440
+ node <skill>/upgrading/app/upgrades/8.0.0-rc.14-to-8.0.0-rc.15/scripts/data-type-in-contract/data-type-in-contract.ts
441
+ ```
442
+
443
+ `<skill>` is the directory of the synced `prisma-8` skill. Node 24 or later runs the TypeScript script directly; it needs no `tsx`. It reads and writes files only and needs no database. It rewrites every `*.json` file under the root that parses as a SQL contract in the old format (a column or `storage.types` entry that stores `nativeType`), skipping `node_modules`, `.git`, `dist` and `build`. That includes a test fixture of an old-format contract: if you keep such a fixture on purpose, restore it with git afterwards (`git restore <file>`), or keep it outside the project root. In each contract it rewrites, it also writes lists in this release's new form (`reemit-explicit-list-cardinality`): a list column or field stored as `many: true` becomes `many: { elementNullable: false }`, and `contract.d.ts` declares `many` on every column. This is the SQL part of `refresh-historical-list-contracts`.
444
+
445
+ On PostgreSQL, a list column of an enum keeps the membership check its database has, because the database still holds that check. Run `prisma db sign` before you emit the contract again. The next `prisma contract emit` writes the check as `array_remove(…) <@ ARRAY[…]` under a new name; `prisma migration plan` then writes a migration that drops the old check and adds the new one, and `prisma db migrate` applies it. The plan warns that dropping the old check may lose data; it does not, because only the check changes, not the column. If you already emitted before signing, `db sign` reports the new check as missing: sign each database with the storage hash the script printed instead (`prisma db sign <new hash>`), then plan and migrate as above.
446
+
447
+ If the script stops on an error, for example on a full disk, it prints the error and `the upgrade stopped partway, run the script again to finish it`, and exits 1. After an error, Ctrl-C or a crash, run it again: it finishes the upgrade. A file the script was writing at that moment is either unchanged or complete, and it removes its own temporary files (ending in `.data-type-in-contract-tmp`) on the next run.
448
+
449
+ Run your formatter afterwards. The script replaces text in `migration.ts` and `contract.d.ts`, so the import order in `migration.ts` and the line wrapping in `contract.d.ts` can differ from what a fresh emit and your formatter produce.
450
+
451
+ When it finishes, it prints how many files it rewrote and how many snapshot directories it renamed, and each storage hash it replaced (`<old> -> <new>`). A contract already in the new format is never changed, even when its stored hash does not match its content, so a project already in the new format is left unchanged, and the script says that nothing changed. If it finds no SQL contract under the root, it says so; run it again from the project root. It prints `<file>: stored hash did not recompute; rehashed from content` for an old-format contract whose stored storage hash does not match its content, and rewrites it anyway. It changes no file and exits 1 when a column uses a codec it does not know (`<file>: unknown codec <id>; name its data type with --data-type <id>=<data type id>`) or when a renamed snapshot directory already exists with different content.
452
+
453
+ The script knows every codec that Prisma and its own extensions ship. For a codec from another extension, pass the line that extension publishes in its upgrade notes, once per codec, for example `--data-type acme/shape@1=acme/shape`. The option cannot change the data type of a codec the script already knows for a contract's target, but it can name the data type of a shared `sql/*` codec on a target the script does not know.
454
+
455
+ On SQLite, the contract now stores a literal default of an `Int` column as digit text, as it already stored a `BigInt` default: an `Int` default that was the JSON number `42` becomes the text `"42"`. The script makes this change. Planned SQL does not change (`DEFAULT 42`), and a database the previous release created still verifies. A `BigInt` default of 2^53 or less, which the previous release's check after `prisma db migrate` reported as missing, now passes. The same holds for the members of an `enum` typed by an integer codec (`@@type("sqlite/integer@1")`, `@@type("sql/int@1")`): the script writes their stored values as digit text. The schema does not change: members are written as before, for example `Low = 1`. An `enum` typed by `sqlite/json@1` now stores the JSON text of each member's document, and the script rewrites its stored values to that text; in the schema, write each member as a string holding that JSON text, for example `Low = '"low"'` instead of `Low = "low"`, and `Level = "1"` instead of `Level = 1`.
456
+
457
+ ## `sign-databases-after-upgrade`
458
+
459
+ The upgrade gives every contract a new storage hash. Each database's marker still holds the old hash, so until you sign it:
460
+
461
+ - `prisma db migrate` refuses to run with `MIGRATION.MARKER_MISMATCH`;
462
+ - `prisma db verify` exits with code 4 and `CONTRACT.MARKER_MISMATCH`;
463
+ - `prisma migration status` does not label the migrations applied before the upgrade as applied.
464
+
465
+ The running application does not report the mismatch: it keeps answering queries and logs nothing, because the `postgres()` client has no logger for the marker check.
466
+
467
+ Run `prisma db sign` against every database (development, staging, production) before you deploy the application built with the new contract:
468
+
469
+ ```sh
470
+ prisma db sign --db "$DATABASE_URL"
471
+ ```
472
+
473
+ `db sign` verifies the live schema of every contract space (the application's and each extension's) against its contract, then writes the marker of every space that verified, in one transaction on PostgreSQL and SQLite. It advances each signed space's `db` ref, or the ref `--advance-ref <name>` names. Running it again changes nothing. A space that fails verification is not signed: the command prints its differences and exits with code 4. Repair that schema first (for example a Supabase database whose `auth` schema drifted from the extension's contract), then sign again.
474
+
475
+ A script that reads `db sign --json` reads one outcome per space. The document was `{ ok, summary, contract, target, marker }` for the application's space; it is now:
476
+
477
+ ```json
478
+ {
479
+ "ok": true,
480
+ "summary": "Database signed",
481
+ "spaces": [
482
+ {
483
+ "space": "app",
484
+ "status": "updated",
485
+ "contract": { "storageHash": "…", "profileHash": "…" },
486
+ "previous": { "storageHash": "…", "profileHash": "…" }
487
+ }
488
+ ],
489
+ "advancedRefs": [{ "space": "app", "name": "db", "hash": "…" }]
490
+ }
491
+ ```
492
+
493
+ Every space has `space`, `status` and `contract`, the hashes of the space's contract. `status` is `created` (the space had no marker), `updated` (with `previous`, the hashes the marker held), `unchanged` (the marker already held the contract's hashes), `failed` or `conflict`. A failed space also has `schema`, the verification result, and `ok` is `false`. A space in conflict is one whose marker another process, such as `migrate`, changed while `db sign` ran: it has `expected` and `found`, the marker hashes `db sign` read and the ones it found, it is not signed, and the command exits with code 4; run `db sign` again once that process has finished.
494
+
495
+ Code that signs through the programmatic control API calls `client.dbSign({ contract, migrationsDir })` instead of `client.sign({ contract })`, which is removed with `SignOptions` and `SignDatabaseResult`. `dbSign` verifies every contract space and signs each one that verified, as `db sign` does.
496
+
497
+ ## `column-descriptors-drop-native-type`
498
+
499
+ In `contract.ts` and every other file that builds a column descriptor by hand, delete the `nativeType` property. The contract takes the column's data type from its codec.
500
+
501
+ ```ts
502
+ // before
503
+ const pgText = { codecId: 'pg/text@1', nativeType: 'text' } as const;
504
+ const Priority = enumType('Priority', { codecId: 'pg/int4@1', nativeType: 'int4' }, member('Low', 0));
505
+
506
+ // after
507
+ const pgText = { codecId: 'pg/text@1' } as const;
508
+ const Priority = enumType('Priority', { codecId: 'pg/int4@1' }, member('Low', 0));
509
+ ```
510
+
511
+ Code that reads a column of a stored contract (`contract.storage…tables[name].columns[name]`) reads `dataType`, the data type id such as `pg/text`, instead of `nativeType`, the database type name such as `text`:
512
+
513
+ ```tsx
514
+ // before
515
+ <span className="col-type">{column.nativeType}</span>
516
+
517
+ // after
518
+ <span className="col-type">{column.dataType}</span>
519
+ ```
520
+
521
+ Code that builds a stored contract's column by hand, for example a test fixture, writes the data type id in `dataType` instead of the type name in `nativeType`:
522
+
523
+ ```ts
524
+ // before
525
+ id: { nativeType: 'uuid', nullable: false, codecId: 'pg/uuid@1' },
526
+
527
+ // after
528
+ id: { dataType: 'pg/uuid', nullable: false, codecId: 'pg/uuid@1' },
529
+ ```
530
+
531
+ A type written by hand for such a contract changes the same way: `readonly nativeType: 'int4'` becomes `readonly dataType: 'pg/int4'`.
532
+
533
+ ## `parameter-casts-use-base-names`
534
+
535
+ Update tests that assert query text or SQL snapshots:
536
+
537
+ | Before | After |
538
+ | --- | --- |
539
+ | `$1::integer` | `$1::int4` |
540
+ | `$1::smallint` | `$1::int2` |
541
+ | `$1::bigint` | `$1::int8` |
542
+ | `$1::real` | `$1::float4` |
543
+ | `$1::double precision` | `$1::float8` |
544
+ | `$1::boolean` | `$1::bool` |
545
+ | `$1::integer[]` | `$1::int4[]` |
546
+
547
+ Extension types keep their names (`$1::vector`, `$1::geometry`).
548
+
549
+ ## `ts-contract-lists-extension-codecs`
550
+
551
+ A column's stored database type name is now written from the data type of the column's codec, so the contract build needs the pack that provides the codec. Add every extension whose codec the contract uses:
552
+
553
+ ```ts
554
+ import pgvector from '@prisma/orm-extension-pgvector/pack';
555
+ import { defineContract } from '@prisma/orm-postgres/contract-builder';
556
+
557
+ export const contract = defineContract(
558
+ { extensions: { pgvector } },
559
+ ({ field, model }) => ({
560
+ // …
561
+ }),
562
+ );
563
+ ```
564
+
565
+ A contract that already lists the extension changes nothing. `contract.json` does not change.
566
+
567
+ ## `sqlite-contract-d-ts-char-aggregates`
568
+
569
+ Run `prisma contract emit` for a SQLite project. The emitted `contract.d.ts` adds these rows under both `AggregateTypes.max.byCodec` and `AggregateTypes.min.byCodec`:
570
+
571
+ ```ts
572
+ readonly 'sql/char@1': { readonly output: 'sql/char@1'; readonly nullable: true };
573
+ readonly 'sql/varchar@1': { readonly output: 'sql/varchar@1'; readonly nullable: true };
574
+ ```
575
+
576
+ `contract.json`, `storageHash`, `profileHash` and migration snapshots do not change, so no migration or re-sign is needed.
577
+
578
+ ## `column-helpers-raise-type-params-invalid`
579
+
580
+ The error now comes from `defineContract`, not from the helper call. Code that catches it by its code checks the new code, around the contract build:
581
+
582
+ ```ts
583
+ // before
584
+ if (error.code === 'CONTRACT.ARGUMENT_INVALID') { /* … */ }
585
+
586
+ // after
587
+ if (error.code === 'CONTRACT.TYPE_PARAMS_INVALID') { /* … */ }
588
+ ```
589
+
590
+ The error's `meta` is `{ dataType, parameters, modelName, fieldName }`, for example `{ dataType: 'postgis/geometry', parameters: ['srid'], modelName: 'Place', fieldName: 'location' }`, in place of `helperPath`, `expected` and `received`. A contract that passes `srid: 0` now fails when it is built; PostgreSQL refuses an SRID below 1, so such a column never migrated. Use a real SRID such as `4326`, or `geometryColumn` for a column with no SRID.
591
+
592
+ The build checks every column, so the PostgreSQL column helpers' parameters are checked too: a contract with `varcharColumn(0)` or `numericColumn(2000)` now fails when it is built, where before `migration plan` or `db verify` failed.
593
+
594
+ ## `reemit-explicit-list-cardinality`
595
+
596
+ Re-run contract emission from each application's original PSL or TypeScript authoring source using its existing Prisma configuration (`pnpm exec prisma contract emit`, with `--config` where needed). Replace both `contract.json` and `contract.d.ts`, including contracts for composed spaces. Do not patch just the generated declarations or copy old hashes into newly emitted JSON. Scalar-only contract JSON remains compatible: omitted `many` in serialized model and value-object fields still means scalar. Generated domain-field declarations also omit scalar `many`; SQL storage-column declarations retain required cardinality.
597
+
598
+ For hand-authored contract objects, replace a list's `many: true` with `many: { elementNullable: false }` to preserve its existing strict-element meaning. Serialized non-list model and value-object fields may omit `many`; deserialization normalizes omission to `many: false`, and canonical emission omits it again. Keep nested `elementNullable: false` in list descriptors. Native SQL array columns use the same descriptor; non-array storage columns use `many: false`. If adopting an intermediate representation with a sibling `elementNullable`, move that property into the `many` descriptor and remove the sibling. Explicit malformed `many` values, including the old boolean-list form and descriptors without a boolean `elementNullable`, and sibling-property representations are rejected. Do not change relation cardinality or mark JSON-backed value-object storage as a native SQL array.
599
+
600
+ Keep `nullable` unchanged: it describes the whole value, not list elements. Existing `String[]` and `String[]?` declarations and `.many()` calls retain non-null elements. Only when nullable elements are intended, use `String?[]` / `String?[]?` or `.many({ elementsNullable: true })` / `.many({ elementsNullable: true }).nullable()`. The authoring option is plural `elementsNullable`; the emitted descriptor uses singular `elementNullable`. Generated types distinguish `ReadonlyArray<T | null>` from `ReadonlyArray<T> | null`.
601
+
602
+ Keep explicit `.noCheck('elementNotNull')` / `@noCheck(elementNotNull)` waivers on strict lists. They waive enforcement without permitting null in the declared element type. Do not infer nullable elements from a waiver or replace waivers during this representation upgrade. If intentionally changing a list to nullable elements, remove that now-inapplicable waiver and review the resulting PostgreSQL check-constraint change or MongoDB validator change separately.
603
+
604
+ ## `refresh-historical-list-contracts`
605
+
606
+ For SQL contracts, the `contract-stores-data-type` script of the same release rewrites stored list columns and fields to this form and refreshes hashes and snapshot names, so historical SQL snapshots need no re-emission; MongoDB contracts still do. On PostgreSQL, the script keeps the membership check of an enum list column as the database holds it; run `prisma db sign` before you emit the contract again, and the next `prisma contract emit` writes the new check under a new name, which `prisma migration plan` and `prisma db migrate` then apply.
607
+
608
+ For MongoDB contracts, inventory every stored contract pair, not only the current application contract: include migration snapshots, fixture migration chains, composed-space snapshots, and any generated contracts imported by application tooling. Re-emit each historical state from its own authoring source and configuration, preserving that state's extensions, storage mappings, defaults, and explicit waivers. Do not emit today's schema over every historical snapshot.
609
+
610
+ Use the project's snapshot-store and migration-generation tooling to write each emitted JSON/declaration pair and update its references. The new native-list representation can change contract hashes even when existing strict-list DDL is unchanged. When a hash changes, create the corresponding content-addressed snapshot entry and update imports of both JSON and declarations, migration start/end contract hashes, and dependent migration metadata consistently. Process predecessor states before successors, including all branches and composed-space dependencies; regenerate derived migration identifiers and parent references where the tooling requires it. Preserve the recorded operations unless a separate reviewed schema change is intended.
611
+
612
+ Do not blanket-replace hexadecimal filenames, rename snapshot directories without updating references, edit hash fields by hand, or delete snapshots still referenced by a migration. Check that every reference resolves and that every stored JSON/declaration pair describes the same historical state. If the original source or a complete reference mapping is unavailable, stop and recover it rather than guessing.
613
+
614
+ For already-applied migrations, retain the original history and reconcile the database's recorded migration identities and contract markers through the project's supported migration procedure before deploying newly emitted contracts. Do not rewrite applied history or reset a database merely to satisfy the new hashes. Rehearse the transition on a disposable database and verify both the migration graph and database contract verification; a clean typecheck alone cannot establish consistency.
615
+
616
+ ## `mongo-nullable-list-containers`
617
+
618
+ For Mongo schemas with nullable list containers (`T[]?` or `T?[]?`), re-emit the current `contract.json` and `contract.d.ts` using your normal `prisma contract emit` command. The collection validator now uses `bsonType: ['null', 'array']` for these fields; element constraints and required-list behavior are unchanged. The changed validator changes the storage hash, so keep the generated contract pair together.
619
+
620
+ For an existing database, preserve applied migrations and their contract snapshots. Plan a new migration from the existing contract to the re-emitted contract, review the validator update, and apply it through your normal migration deployment flow before writing explicit null containers. Deploying new application code alone does not update a Mongo collection validator. Do not rewrite applied historical migrations or their hashes.
621
+
622
+ For disposable example or test databases whose migration fixtures are regenerated from source, regenerate that fixture chain with its existing tooling, including referenced contract snapshots and migration metadata, and recreate the disposable database. In the Prisma 8 source repository, `pnpm fixtures:emit` re-emits current contracts and regenerates example migration fixtures; retain existing content-addressed snapshots alongside any newly emitted snapshots, then run the example's formatter/import organizer if new snapshot paths change import order. This fixture-only procedure is not a production migration procedure.
623
+
624
+ ## `domain-types-match-their-columns`
625
+
626
+ Run `prisma contract emit`. `contract.json` and `contract.d.ts` gain these entries in the domain half:
627
+
628
+ | PSL | Added to the field's domain entry |
629
+ | --- | --- |
630
+ | `code Short`, with `types { Short = VarChar(10) }` (also `Short[]`) | `"typeParams": { "length": 10 }` on `type` |
631
+ | `roles Role[]`, where `Role` is an `enum` | `"valueSet": { "plane": "domain", "entityKind": "enum", "namespaceId": "public", "entityName": "Role" }`, as `role Role` already had |
632
+ | composite type member `amount Numeric(10, 2)` (also a list, or a named type) | `"typeParams": { "precision": 10, "scale": 2 }` on `type` |
633
+ | composite type member `role Role` or `roles Role[]` | the same `valueSet` as a model field of that enum |
634
+
635
+ A named type without parameters, such as `Email = String`, adds nothing.
636
+
637
+ In `contract.d.ts`, a composite type's output type (`AddressOutput`) now gives a member with type parameters the parameterized output type a model field of that type has, such as `Varchar<10>` or `Numeric<10, 2>`, instead of the codec's plain output type. These are branded strings, so code that builds such an output object from plain strings, such as a test fixture or a mock, no longer type-checks. Build the value as the ORM returns it, or type it with the composite type's input type (`AddressInput`), which is unchanged.
638
+
639
+ Migration snapshots under `migrations/snapshots/<hash>/` need no change. Migration commands read only their storage half, which is unchanged.
640
+
641
+ `prisma contract print` now expects a field typed by a parameterized named type, and an enum list field, to carry these domain entries. It refuses a contract emitted before this change that lacks them. Re-emit it first.
642
+
643
+ ## `value-object-default-matches-composite-type`
644
+
645
+ A literal default on a field typed by a composite type used to be stored whatever its shape. It is now checked, naming the path that is wrong, as in `Field "User.home.street"`:
646
+
647
+ - A single value object takes a JSON object, and a list of them a JSON array: `` homes Address[] @default(json`{"street": "x"}`) `` is refused; write `@default([])` or `` @default(json`[{"street": "x"}]`) ``. JSON `null` is taken when the field is optional. `PSL_VALUE_TYPE_INCOMPATIBLE`.
648
+ - A key that is not a member is refused, and so is a missing member that is not optional, and `null` for a member that is not optional. `PSL_VALUE_TYPE_INCOMPATIBLE`.
649
+ - The default holds each member in the form its codec stores, so the member's codec must read the value. A `Decimal`, `Numeric(p, s)` or `BigInt` member takes a decimal string, `"1.5"`, and a number is refused; a `String` member takes a JSON string, so `"street": 1` is refused; a `DateTime` member takes a date and time string; a `Json` member takes any JSON value. `PSL_INVALID_DEFAULT_LITERAL`, with the codec's message.
650
+ - A member typed by an enum takes only the enum's values: `PSL_INVALID_DEFAULT_LITERAL`, `Expected one of:` the values.
651
+ - Nested value objects are checked the same way.
652
+
653
+ Correct the value the diagnostic names.
654
+
655
+ ## `composite-type-attributes-refused`
656
+
657
+ An attribute inside a `type` block was ignored: `street String @default("x")` stored no default, and `@@map` mapped nothing. Each is now refused, `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` on a member and `PSL_UNSUPPORTED_COMPOSITE_TYPE_ATTRIBUTE` on the type. Remove the attribute. To give a value object a default, write it on the model field as a whole value, such as `` home Address @default(json`{"street": "x"}`) ``.
658
+
659
+ ## `codecs-check-stored-json`
660
+
661
+ The built-in SQL, PostgreSQL and SQLite codecs now refuse a value that is not a stored form of their type, including one its type parameters rule out, where most used to pass it through. A TypeScript `.default()` given such a value is now refused when the contract is built, with `CONTRACT.DEFAULT_INVALID` naming the model and field: a value of another kind, such as a number for a text column, or one the PostgreSQL column rules in `psl-values-checked-by-codecs` refuse, such as `'toolong'` for a `varchar(3)` column. A TypeScript `enumType` member its codec does not take is refused the same way, with `CONTRACT.ENUM_INVALID` naming the enum, the member and the codec: for example a `pg/char@1` or `sql/char@1` member longer than one character on PostgreSQL, since the enum's column is `character`, which holds one. Correct the value the error names.
662
+
663
+ A `contract.json` with such a default, emitted by an earlier version or edited by hand, still loads. `db init`, `db update` and `migration plan` used to plan the default; they now stop with `CONTRACT.DEFAULT_INVALID`, which names the table, the column, the codec and the value. Emit the contract again with this version, and correct the default in the contract source if emit refuses it. Running a `migration.ts` that an earlier version planned with such a default fails with the same message; correct the default in that file. Every statement that writes a default reads it this way: a new table or column, a changed default, and on SQLite a rebuilt table, which used to write a changed default or a rebuilt table's defaults unread. Each element of a list default is read the same way, and the message names the element's position: `Column "post"."tags" has a default (element 2) its codec pg/text@1 refuses: pg/text@1 JSON value must be a string`. A NULL element stays NULL. The codecs that carry PostgreSQL's own date and time text, those of `DateString`, `TimeString`, `TimestampString`, `TimestamptzString` and `Timetz`, read only a date or time in ISO 8601 or as PostgreSQL writes it, so such a default as `'now'`, which PostgreSQL would read once when it creates the table, is refused the same way. An `.include()` of such a column reads the text PostgreSQL writes in the ISO DateStyle, its default; on a server set to another DateStyle it now fails with `RUNTIME.DECODE_FAILED`, where it passed the text through. Set `DateStyle` to `ISO` on that server.
664
+
665
+ ## `psl-values-checked-by-codecs`
666
+
667
+ The PSL reader reads each literal default, and each member of a SQL `enum`, with the column's codec, so the stricter codecs refuse schemas that loaded before. A value of another kind is refused: a text codec takes a string, an integer codec an integer in its range, `Boolean` `true` or `false`, and `Uuid` a UUID. On PostgreSQL the codecs also check what the column stores:
668
+
669
+ - `VarChar(n)` and `Char(n)` take at most n characters, counted by code point, and a `Char` value's trailing spaces do not count. A `Char` without a length is `character(1)`, so it takes one character.
670
+ - `Bit(n)` takes exactly n bits and `VarBit(n)` at most n, and a bit column without a length is `bit(1)`.
671
+ - `Numeric(p, s)` takes a value it stores without rounding.
672
+ - `Int`, `sql/int@1` and an enum whose members are integers take an integer from -2147483648 to 2147483647, and `SmallInt` one from -32768 to 32767.
673
+ - `Real` takes a finite number only if float4 holds it, neither overflowing to an infinity nor becoming 0.
674
+
675
+ Such a default used to load, and the migration planned and applied; the first insert that used the default then failed. A `Char` or bit column without a length, which did not apply on PostgreSQL whatever its default, now applies as `character(1)` or `bit(1)`. SQLite does not enforce a declared length, so on SQLite the char and varchar codecs take text of any length. Each of these is now refused at `contract emit`:
676
+
677
+ | Schema | Diagnostic |
678
+ | --- | --- |
679
+ | `enum P { @@type("pg/int4@1") Low = "low" }` | `PSL_EXTENSION_INVALID_VALUE`: `enum "P" member "Low" was rejected by codec "pg/int4@1": pg/int4@1 JSON value must be an integer from -2147483648 to 2147483647` |
680
+ | the same enum with a bare `Low` | `PSL_ENUM_BARE_MEMBER_NON_STRING_CODEC`: `enum "P" member "Low" has no value and codec "pg/int4@1" does not accept a bare name as input` |
681
+ | `enum P { @@type("pg/text@1") Low = 1 }` | `PSL_EXTENSION_INVALID_VALUE`, `enum "P" member "Low": pg/text has no cast from pg/int2; it casts from nothing` |
682
+ | an enum without `@@type` whose integer members include one outside -2147483648 to 2147483647, such as `Low = 3000000000` | `PSL_EXTENSION_INVALID_VALUE`, `enum "P" member "Low": pg/int4 has no cast from pg/int8; it casts from pg/int2` |
683
+ | `u Uuid @default("nope")` | `PSL_INVALID_LITERAL`, `"nope" is not a UUID: PostgreSQL reads 32 hexadecimal digits, with a hyphen after any group of four and optionally in braces.` |
684
+ | `s VarChar(3) @default("toolong")` | `PSL_INVALID_DEFAULT_LITERAL`, `sql/varchar@1 JSON value must be a string of at most 3 characters` |
685
+ | `c Char @default("abc")` on PostgreSQL | `PSL_INVALID_DEFAULT_LITERAL`, `sql/char@1 JSON value must be a string of at most 1 character before any trailing spaces` |
686
+ | `enum P { @@type("sql/int@1") Low = 3000000000 }` on PostgreSQL | `PSL_EXTENSION_INVALID_VALUE`, `enum "P" member "Low": pg/int4 has no cast from pg/int8; it casts from pg/int2` |
687
+ | `n Numeric(5, 2) @default(1.555)` | `PSL_INVALID_DEFAULT_LITERAL`, `pg/numeric@1 JSON value must be a decimal string that numeric(5, 2) stores without rounding` |
688
+
689
+ Give each enum member a value its codec takes, and each default a value its column's type holds unchanged. A `Uuid` default is still read in any form PostgreSQL reads; see `uuid-defaults-stored-as-postgresql-writes`.
690
+
691
+ ## `uuid-defaults-stored-as-postgresql-writes`
692
+
693
+ A `Uuid` default may be written in any form PostgreSQL reads: either case, with or without a hyphen after any group of four digits, and optionally in braces. The contract now stores it as PostgreSQL writes it, in lower case and hyphenated 8-4-4-4-12, and so does a TypeScript `.default()` on a `pg/uuid@1` column, so the applied default verifies against the database with no difference.
694
+
695
+ Earlier versions stored such a default as written. The database stores the lower-case form, so the check that runs after the change is applied failed: `db init`, `db update` and `db migrate` stopped with `MIGRATION.RUNNER_FAILED` and rolled the change back. The database has none of the changes that contract adds, and no marker for it. With this version, a `contract.json` that still holds such a default stops `db init`, `db update` and `migration plan` with `CONTRACT.DEFAULT_INVALID`, as `codecs-check-stored-json` describes.
696
+
697
+ Emit the contract again with this version. The stored default changes, and with it the storage hash. Then:
698
+
699
+ - For a project kept with `db init` or `db update`, run the command that failed again. It applies the contract, and `db verify` then passes.
700
+ - For a project with migrations, delete the migration package that never applied: its directory under `migrations/app/`, and its contract snapshot `migrations/snapshots/<hash>/`, where `<hash>` is the `to` hash in the package's `migration.json`. Then run `prisma migration plan` and `prisma db migrate`. Left in place, the package stays in the migration graph, ending at a contract no database reaches.
701
+
702
+ ## `ts-enum-member-written-as-stored`
703
+
704
+ The types of a TypeScript contract name each `enumType` member as written, in `db.enums` and in the types of the fields that use the enum, while `contract.json` and the database hold what the column's codec stores. The two now have to be the same value, so `defineContract` refuses a member its codec stores as something else and says what to write:
705
+
706
+ ```text
707
+ CONTRACT.ENUM_INVALID: enumType("Key"): member "A" is written "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", but the column stores "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11". Write the member as "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11".
708
+ ```
709
+
710
+ A member its codec does not take at all, such as text that is not a uuid on `pg/uuid@1`, is refused with a different message; `codecs-check-stored-json` describes it.
711
+
712
+ Refused members:
713
+
714
+ - On a uuid column (`pg/uuid@1`), a member with an upper-case hex digit, in braces, or with hyphens anywhere other than the 8-4-4-4-12 positions (including none). Write it in lower case, hyphenated 8-4-4-4-12.
715
+ - A member that got past the type check with a value of another type its codec still takes, for example through a cast: the number `1` on `pg/int8@1`, which stores `"1"`, is refused with "Write the member as 1n".
716
+
717
+ 1. Run `prisma contract emit`, or run the code that calls `defineContract`. Each refused member is reported with the value to write. To find uuid members first, search the calls to `member(` in your contract for a uuid that is not lower case, hyphenated 8-4-4-4-12. The detection for this change looks for calls to `enumType(`; if you import it under another name, as in `import { enumType as defineEnum }`, it does not find them, so search for that name.
718
+ 2. Rewrite each refused member as the message says. Code that reads members through the enum, such as `Key.members.A` or `db.enums.public.Key.members.A`, needs no change. Code that compares a value with the old spelling does: values read from the database were always in the stored form.
719
+ 3. Re-emit. If a column uses the enum, its membership CHECK constraint's expression changes, and a CHECK constraint's name is derived from its expression, so the storage hash and the constraint's name both change. Plan and apply a migration: it drops the old CHECK constraint and adds the new one. Dropping a constraint is a destructive operation, so the plan needs the destructive operation class allowed.
720
+
721
+ ## `mongo-codecs-check-json`
722
+
723
+ A Mongo codec's `decodeJson` reads the JSON form of its type. The Mongo runtime reads documents through `decode` and never calls it; the PSL reader calls it for each member of an enum. `mongo/string@1`, `mongo/objectId@1`, `mongo/int32@1`, `mongo/double@1`, `mongo/bool@1`, `mongo/vector@1` and `mongo/bson@1` used to return any JSON value as it was, so an enum member of the wrong kind was stored in the contract:
724
+
725
+ ```prisma
726
+ enum Priority {
727
+ @@type("mongo/int32@1")
728
+ Low = "low"
729
+ }
730
+ ```
731
+
732
+ This is now refused with `PSL_EXTENSION_INVALID_VALUE`, naming the codec's message, `mongo/int32@1 JSON value must be an integer from -2147483648 to 2147483647`. A member written without a value, such as a bare `Low`, under a codec that does not take text is `PSL_ENUM_BARE_MEMBER_NON_STRING_CODEC`. Give each member a value of the codec's type.
733
+
734
+ Each codec now takes: `mongo/string@1` a string; `mongo/objectId@1` 24 hexadecimal digits; `mongo/int32@1` an integer from -2147483648 to 2147483647; `mongo/double@1` a number, or the text `"NaN"`, `"Infinity"` or `"-Infinity"`, which its `encodeJson` now writes for those values instead of a number JSON cannot hold; `mongo/bool@1` a boolean; `mongo/date@1` the text `Date.toISOString()` writes; `mongo/vector@1` an array of numbers; and `mongo/bson@1` canonical Extended JSON, the form its `encodeJson` writes. Another value throws `RUNTIME.DECODE_FAILED` with the codec id in `meta`. A TypeScript `enumType` member that `mongo/objectId@1` or `mongo/int32@1` does not hold now throws `RUNTIME.ENCODE_FAILED` when the contract is built.
735
+
736
+ ## `text-array-elements-nullable`
737
+
738
+ `pg/text-array@1`, the codec of a contract-free `textArray()` column, reads a `text[]` column's NULL elements as `null`, so its application type is `readonly (string | null)[]` where it was `readonly string[]`. Code typed by a `textArray()` column, or by `min` or `max` over one, sees `string | null` elements; handle the `null`. Read through an `.include()`, a two-dimensional `text[]` value now throws `RUNTIME.DECODE_FAILED`, where it read as the text `"a,b"`.
739
+
740
+ ## `char-reads-drop-only-padding`
741
+
742
+ PostgreSQL pads a `char(n)` value with spaces to its length: `'a'` in a `char(3)` column is stored as `'a '`. A flat read dropped every trailing whitespace character, so a stored `'a\t'` also read as `"a"`, while `.include()` returned the padded text, `"a "`. Both reads now return the value without the padding and nothing more: `"a"` for `'a'`, and `"a\t"` for `'a\t'`. On SQLite, which does not pad, both reads drop trailing spaces, as a flat read did. Code that compared an included `char` value with its padding, or relied on a flat read dropping a trailing tab or newline, compares the value without its padding.
743
+
744
+ ## `sqlite-nan-parameters-refused`
745
+
746
+ SQLite cannot store NaN: bound as a parameter, it becomes NULL. So `create({ value: 0 / 0 })` on an optional `Float` column stored NULL, and `where((p) => p.value.eq(Number.NaN))` matched nothing. On SQLite, `sqlite/real@1` and `sql/float@1` now refuse NaN with `RUNTIME.ENCODE_FAILED`, `<codecId> value must be a number other than NaN, which SQLite cannot store`, with `meta.codecId` and `meta.received`: when they encode a value to write or filter by, and when they encode a TypeScript `.default()`, which is still refused when the contract is built with `CONTRACT.DEFAULT_INVALID`, now with this message. Their `decodeJson` refuses the text `"NaN"`. A NaN parameter no codec encoded, such as one in raw SQL, is refused by the SQLite driver with the same code: `Parameter 2 is NaN, which SQLite cannot store: it would bind it as NULL. Pass null to store no value.`, with `meta.paramIndex`, counted from 0. On a required column SQLite already refused the NULL, so only the error changes. Where a computed value can be NaN, write `null` for no value, and filter with `isNull()` for rows that have none. Infinity and -Infinity are stored and read back as before.
747
+
748
+ ## `sqlite-int-include-refuses-inexact-values`
749
+
750
+ `sql/int@1` holds a JavaScript safe integer. On SQLite, an INTEGER column can hold a larger integer or a REAL. An `.include()` read such a value rounded or with a fraction; it now throws `RUNTIME.DECODE_FAILED`, naming the codec. A flat read is unchanged. Store an integer past 2^53 in a `BigInt` column and a fraction in a `Float` column.
751
+
752
+ ## `prefixed-sql-tags-are-removed`
753
+
754
+ `pg.sql` and `sqlite.sql` were second names for the `sql` tag. They are removed, and a schema that uses one is refused with `PSL_UNKNOWN_LITERAL_TAG`: `Unknown literal tag "pg.sql". Known tags: sql, json.`
755
+
756
+ Replace the tag with `sql` and leave the text unchanged:
757
+
758
+ ```diff
759
+ - createdAt DateTime @default(pg.sql`(now() + interval '1 hour')`)
760
+ + createdAt DateTime @default(sql`(now() + interval '1 hour')`)
761
+ ```
762
+
763
+ The stored default does not change, so no migration follows.
764
+
765
+ ## `default-diagnostic-codes-changed`
766
+
767
+ This matters only to code that reads PSL diagnostic codes, such as a test that asserts one. The messages did not change, except for a `sql` literal inside a list literal and the list of known tags, both described below the table.
768
+
769
+ | Refusal | Old code | New code |
770
+ | --- | --- | --- |
771
+ | A tag no pack registered | `PSL_UNKNOWN_DEFAULT_LITERAL_TAG` | `PSL_UNKNOWN_LITERAL_TAG` |
772
+ | A `json` literal whose text is not a JSON document | `PSL_INVALID_JSON_LITERAL` | `PSL_INVALID_LITERAL` |
773
+ | Text an authoring entry or a cast refused | `PSL_INVALID_DEFAULT_LITERAL` | `PSL_INVALID_LITERAL` |
774
+ | A value whose type the column's type has no cast from, including a list written on a column that holds one value | `PSL_DEFAULT_TYPE_INCOMPATIBLE` | `PSL_VALUE_TYPE_INCOMPATIBLE` |
775
+ | A written form the target has no data type for | `PSL_DEFAULT_TYPE_INCOMPATIBLE` | `PSL_VALUE_TYPE_INCOMPATIBLE` |
776
+ | A single value on a list column | `PSL_DEFAULT_TYPE_INCOMPATIBLE` | `PSL_DEFAULT_LIST_EXPECTED` |
777
+ | A value the column's codec refused | `PSL_INVALID_DEFAULT_LITERAL` | unchanged |
778
+ | A `sql` literal inside a list literal | `PSL_INVALID_DEFAULT_LITERAL`, at the element | `PSL_VALUE_TYPE_INCOMPATIBLE`, at the `@default` attribute |
779
+
780
+ `PSL_INVALID_JSON_LITERAL` no longer exists.
781
+
782
+ The unknown-tag message lists the known tags in the order the stack registers them. The SQL family registers `sql` before the target registers `json`, so a Postgres or SQLite stack lists `sql, json`, where it used to list `json, sql, pg.sql` or `json, sql, sqlite.sql`. The completion list and the `Expected one of` message of `@default` offer `sql` before `json` for the same reason.
783
+
784
+ A `sql` literal inside a list literal used to report `Literal tag "sql" produces a default of its own and cannot be an element of a list literal.` It is now refused by the cast rule, like any other value the column's type does not take: `Field "Post.tags" at element 1: pg/text has no cast from sql/expression; it casts from nothing`. Write the whole list as one `sql` literal instead, as in `` @default(sql`'{}'::text[]`) ``.
785
+
786
+ This supersedes the codes named in the `data-types-column-defaults` app instructions of the upgrade from 8.0.0-rc.11 to 8.0.0-rc.12: where they name `PSL_DEFAULT_TYPE_INCOMPATIBLE` for a value a column's type has no cast from, or `PSL_INVALID_JSON_LITERAL`, read the new codes above.
787
+
788
+ ## `migration-ts-column-defaults`
789
+
790
+ A `migration.ts` no longer carries a column default as SQL text. The control adapter writes the `DEFAULT …` clause for every statement, the same way for a new table, a new column, a changed default and a rebuilt SQLite table, and it reads a literal default with the column's codec first. A `migration.ts` that sets a default the codec refuses fails when it runs, with `CONTRACT.DEFAULT_INVALID` naming the table and the column.
791
+
792
+ On PostgreSQL, `setDefault` takes the column and its default:
793
+
794
+ ```typescript
795
+ // before
796
+ this.setDefault({ table: 'user', column: 'role', defaultSql: "DEFAULT 'member'" })
797
+ // after
798
+ this.setDefault({ table: 'user', column: col('role', 'text', { default: lit('member'), codecRef: { codecId: 'pg/text@1' } }) })
799
+ ```
800
+
801
+ On SQLite, a column in `addColumn` or `recreateTable` carries the default and its codec, and a `recreateTable` postcheck that checks a default names the column:
802
+
803
+ ```typescript
804
+ // before
805
+ { name: 'role', typeSql: 'TEXT', defaultSql: "DEFAULT 'member'", nullable: false }
806
+ { description: 'verify "role" default on "user"', sql: "SELECT COUNT(*) > 0 FROM pragma_table_info('user') WHERE ..." }
807
+ // after
808
+ { name: 'role', typeSql: 'TEXT', default: { kind: 'literal', value: 'member' }, codecRef: { codecId: 'sqlite/text@1' }, nullable: false }
809
+ { description: 'verify "role" default on "user"', columnDefault: 'role' }
810
+ ```
811
+
812
+ An applied migration needs nothing: `db migrate` applies `ops.json`, which holds the SQL. Change a `migration.ts` this way only when you run it again to write `ops.json`. `node migration.ts` does not check types, so an earlier file still runs; `setDefault`, `addColumn` and `recreateTable` then refuse a `defaultSql` with `MIGRATION.OPERATION_OPTION_REMOVED`, naming the table and the column, rather than leave the default out. Rewrite the call as shown, or, if the migration is not applied, delete its package and run `migration plan` again.
813
+
814
+ The `migration.ts` that `migration plan` writes for a new SQLite table now gives each column its `codecRef`, so running it writes the same `ops.json` as the plan. To apply a changed PostgreSQL default that an earlier version planned, see [`postgres-changed-default-applied`](#postgres-changed-default-applied).
815
+
816
+ ## `postgres-changed-default-applied`
817
+
818
+ A migration operation that changes an existing default on PostgreSQL checked afterwards only that the column has a default. The old default passes that check, and the runner skips an operation whose check already passes, so the default stayed as it was and verification then failed with `MIGRATION.SCHEMA_VERIFY_FAILED`. Such an operation now has no check afterwards and always runs; setting a default twice changes nothing.
819
+
820
+ A migration package an earlier version planned keeps the old check in `ops.json`. If one changes a default and you have not applied it, write its `ops.json` again before you apply it with `prisma db migrate`. Either delete the package and run `prisma migration plan` again, or rewrite its `setDefault` call in `migration.ts` to the form [`migration-ts-column-defaults`](#migration-ts-column-defaults) shows and then run the file (`node migration.ts`). Run unchanged, the file stops with `MIGRATION.OPERATION_OPTION_REMOVED`, because its `setDefault` still passes `defaultSql`. `db update` plans again each time, so it needs nothing.
821
+
822
+ ## `cli-error-from-caught`
823
+
824
+ `mapCaughtMigrationError(error)`, exported from `@prisma/orm-toolchain/cli/control-api` (`@internal/cli/control-api`), returned a CLI error unchanged and `null` for anything else, which the caller wrapped as `CLI.UNEXPECTED`. `errorFromCaught(error, why)`, exported from the same place, does the whole job: it returns a CLI error unchanged, reports any other error with a structured `NAMESPACE.SUBCODE` code as itself, reports anything else as `CLI.UNEXPECTED` with `why` given the error's message, and throws an `InternalError` again. Replace `mapCaughtMigrationError(error) ?? errorUnexpected(...)` with `errorFromCaught(error, (message) => ...)`. A caller that holds a database connection string passes it as `errorFromCaught(error, why, { connection })`, which removes it from every field of the reported error.
825
+
826
+ ## `sql-with-a-line-comment-gets-a-new-wire-name`
827
+
828
+ Prisma names an index, check or policy after a hash of its SQL body. Before hashing, it normalizes the body. A body that contains `--` now keeps its line breaks, because a line break ends the comment and so changes what the body means. Every other body normalizes as before.
829
+
830
+ This affects an index expression or predicate, a check expression, or a policy `using` or `withCheck` that contains both `--` and a line break. For such an object, re-emitting the contract changes the stored index, check or policy name in `contract.json`. The stored body does not change; only the hash input does.
831
+
832
+ Re-emit the contract and run `migration plan`. The name's hash suffix changes, so the planned migration drops and recreates each affected object once.
833
+
834
+ ## `strict-verify-unclaimed-code`
835
+
836
+ For each place that reads `CONTRACT.MARKER_REQUIRED` from a `prisma db verify --strict` run to detect tables no contract declares, read `CONTRACT.SCHEMA_VERIFICATION_FAILED` instead, or read the names from the result's `unclaimed` list. Leave code that reads `CONTRACT.MARKER_REQUIRED` to detect an unsigned database unchanged.
837
+
838
+ ## `prisma7-schema-date-types-are-text`
839
+
840
+ For each project whose `prisma.config.ts` uses `prisma7Schema(...)`:
841
+
842
+ 1. Run `prisma contract emit`. The date and time fields change type:
843
+
844
+ | Prisma 7 field | Before | Now | Example value |
845
+ | --- | --- | --- | --- |
846
+ | `DateTime`, `DateTime @db.Timestamp(p)` | `Temporal.PlainDateTime` | `string` | `"2026-09-14 10:00:00.123"` (UTC, as Prisma 7 writes it) |
847
+ | `DateTime @db.Timestamptz(p)` | `Temporal.Instant` | `string` | `"2026-09-14 10:00:00.123+00"` (on a server whose `TimeZone` is UTC) |
848
+ | `DateTime @db.Date` | `Temporal.PlainDate` | `string` | `"2026-09-14"` |
849
+ | `DateTime @db.Time(p)` | `Temporal.PlainTime` | `string` | `"10:00:00.123"` |
850
+
851
+ `@db.Timetz` fields already read as text and do not change.
852
+
853
+ 2. Change the code that reads or writes these fields:
854
+
855
+ - `DateTime` and `@db.Timestamp`: the text holds UTC wall-clock time with a space between the date and the time, where `.toString()` on a `Temporal.PlainDateTime` printed a `T`. Where code printed or stored that form, replace `value.toString()` with `value.replace(' ', 'T')`. Where it compares or computes with the value, `` new Date(`${value.replace(' ', 'T')}Z`) `` is the instant.
856
+ - `@db.Timestamptz`: the text carries the offset of the database session's `TimeZone`, `+00` on a server set to UTC. Do not apply `replace(' ', 'T')` to it. `new Date(value)` parses it in Node.js, and `new Date(value).toISOString()` prints the instant in UTC ending in `Z`, the form `.toString()` on a `Temporal.Instant` printed, with milliseconds always present.
857
+ - `@db.Date` and `@db.Time`: the text is the form `.toString()` on a `Temporal.PlainDate` or `Temporal.PlainTime` printed.
858
+
859
+ Write a string PostgreSQL reads, such as `"2026-09-14 10:00:00"` for `DateTime` or `"2026-09-14T10:00:00Z"` for `@db.Timestamptz`, instead of a `Temporal` value. `@updatedAt` still writes UTC, as it did before and as Prisma 7 does, so existing rows need no change.
860
+
861
+ 3. If no other code in the application uses `Temporal`, remove the `import 'temporal-polyfill/full/global'` it had for these fields, and remove `temporal-polyfill` from the application's `dependencies`. Keep the dependency in a project that installs with Yarn: `@prisma/orm-postgres` declares it as a peer dependency, and Yarn does not install peers on its own.
862
+
863
+ 4. Run `prisma db sign` against every database the application uses. The storage hash changed with the column types. Until a database is signed, `prisma db verify --db "$DATABASE_URL"` exits with code 4 and reports `CONTRACT.MARKER_MISMATCH`. The running application does not report the mismatch: it keeps answering queries and logs nothing, because the `postgres()` client has no logger for the marker check. The database itself needs no migration.
864
+
865
+ `prisma7Schema(...)` has no option to keep the `Temporal` types. A project that wants them writes a Prisma 8 contract, for example with `prisma contract print --output prisma/contract.prisma`, and changes the types there.
866
+
867
+ ## `contract-infer-writes-text-date-types`
868
+
869
+ Nothing changes until `prisma contract infer` runs again. When it does, the rewritten contract uses the text types for `timestamp`, `timestamptz`, `date` and `time` columns. Follow steps 1 to 4 of `prisma7-schema-date-types-are-text` for the fields that changed, or change the types back to `Timestamp(p)`, `Timestamptz(p)`, `Date` and `Time(p)` in the inferred file to keep `Temporal` values.
870
+
871
+ A default of `infinity` or `-infinity` on one of these columns now prints as `@default("infinity")` where it printed a `sql` expression.
872
+
873
+ ## `text-timestamp-now-is-utc`
874
+
875
+ This covers `temporal.timestampString(p, onCreate: now, onUpdate: now)` in PSL and `field.temporal.timestampString(...)` with `'now'` in TypeScript. Rows these fields wrote before this release on a host outside UTC hold that host's local time; rows written from now on hold UTC. A Prisma 7 `DateTime @updatedAt` read through `prisma7Schema(...)` is not affected: it wrote UTC before and still does.
876
+
877
+ ## `contract-infer-writes-bytea-default-literals`
878
+
879
+ Nothing changes until `prisma contract infer` runs again. A schema that keeps the `sql` default keeps working: schema verification reads it as the same bytes as the default in the database.
880
+
881
+ When infer runs again, it writes each `bytea` default as the base64 the `Bytes` codec stores:
882
+
883
+ | Before | Now |
884
+ | --- | --- |
885
+ | ``@default(sql`'\\x68656c6c6f'::bytea`)`` | `@default("aGVsbG8=")` |
886
+ | ``@default(sql`ARRAY['\\x68656c6c6f'::bytea]`)`` | `@default(["aGVsbG8="])` |
887
+
888
+ 1. Run `prisma contract emit`. The default is now stored as a value, so the storage hash changes. The default in the database does not change.
889
+ 2. If you create the database with `prisma db init` or `prisma db update`, run `prisma db verify --schema-only` to confirm the schema matches, then `prisma db sign` to sign the database with the re-emitted contract.
890
+ 3. If you use migrations, `prisma migration plan` refuses with "Contract changed but planner produced no operations", because nothing in the database changes. Run `prisma migration new --name bytea-default-literals` to write an empty migration from the earlier contract to the re-emitted one, then `prisma db migrate`. When `migration new` cannot tell where to start, pass `--from` with the `to` hash of your latest migration, which `prisma migration list` shows.
891
+
892
+ Sign the database or apply the migration before you deploy the re-emitted contract. Until then `prisma db verify` exits with code 4 and `CONTRACT.MARKER_MISMATCH`, because the database marker holds the earlier storage hash. The running application does not report the mismatch: it keeps answering queries and logs nothing.
893
+
894
+ ## `prisma6-int-written-as-long`
895
+
896
+ Prisma 6 stores a plain `Int` on MongoDB as a BSON long and presents it as a `number`. A contract read with `prisma6Schema(...)` used to give such a field the 32-bit int codec, so Prisma 8 wrote new values as BSON ints, and a fractional number was accepted and stored as a double. It now uses `mongo/int64Number@1`: a stored long reads as a `number`, Prisma 8 writes a `number` back as a long, and a value that is not a whole number within ±(2^53 − 1) is refused instead of rounded. The application type does not change.
897
+
898
+ 1. Repair every document in which a plain `Int` field holds a fractional number, whether the field is on the model, in an `Int[]` list, or in a composite type the model holds once or in a list. The previous contract let Prisma 8 store such a value as a BSON double, and `mongo/int64Number@1` refuses to read it (`RUNTIME.DECODE_FAILED`, "wire value is the fractional double 2.5"), so a query that returns such a document fails as a whole. For each plain `Int` field, in the collection its model is stored in, with the MongoDB shell (`mongosh`) or the driver:
899
+ - List the affected documents, to decide how to repair them. For a field on the model, this finds the fractional values:
900
+
901
+ ```js
902
+ db.Post.find({ likes: { $type: 'double' }, $expr: { $ne: ['$likes', { $trunc: '$likes' }] } })
903
+ ```
904
+
905
+ For a list or a composite value, this finds every document with a double in the field: `db.Post.find({ scores: { $type: 'double' } })` for an `Int[]` field, and `db.Post.find({ 'addresses.zip': { $type: 'double' } })` for `zip` in a composite type, whether `addresses` holds one value or a list.
906
+
907
+ - Store every double in the field as a long. Choose `$round` or `$trunc`: `$round` rounds to the nearest whole number and a half to the even one (2.5 becomes 2, 3.5 becomes 4); `$trunc` drops the fraction (2.9 becomes 2). Whole-number doubles become longs either way.
908
+
909
+ ```js
910
+ db.Post.updateMany({ likes: { $type: 'double' } }, [
911
+ { $set: { likes: { $toLong: { $round: ['$likes', 0] } } } },
912
+ ])
913
+ ```
914
+
915
+ - For an `Int[]` field, convert each double in the list:
916
+
917
+ ```js
918
+ db.Post.updateMany({ scores: { $type: 'double' } }, [
919
+ {
920
+ $set: {
921
+ scores: {
922
+ $map: {
923
+ input: '$scores',
924
+ in: {
925
+ $cond: [
926
+ { $eq: [{ $type: '$$this' }, 'double'] },
927
+ { $toLong: { $round: ['$$this', 0] } },
928
+ '$$this',
929
+ ],
930
+ },
931
+ },
932
+ },
933
+ },
934
+ },
935
+ ])
936
+ ```
937
+
938
+ - For a field of a composite type the model holds once (`address Address?`), write its dotted path (`'address.zip'` and `'$address.zip'`) in the filter and the update above.
939
+ - For a field of a composite type the model holds in a list (`addresses Address[]`), the dotted path fails with "$round only supports numeric types, not array". Rewrite each element of the list instead:
940
+
941
+ ```js
942
+ db.Post.updateMany({ 'addresses.zip': { $type: 'double' } }, [
943
+ {
944
+ $set: {
945
+ addresses: {
946
+ $map: {
947
+ input: '$addresses',
948
+ in: {
949
+ $mergeObjects: [
950
+ '$$this',
951
+ {
952
+ zip: {
953
+ $cond: [
954
+ { $eq: [{ $type: '$$this.zip' }, 'double'] },
955
+ { $toLong: { $round: ['$$this.zip', 0] } },
956
+ '$$this.zip',
957
+ ],
958
+ },
959
+ },
960
+ ],
961
+ },
962
+ },
963
+ },
964
+ },
965
+ },
966
+ ])
967
+ ```
968
+
969
+ Here `Post`, `likes`, `scores`, `address`, `addresses` and `zip` stand for the collection and the field names in the database, after `@@map` and `@map`.
970
+ 2. Run `prisma contract emit`. In `contract.d.ts`, each plain `Int` field of the Prisma 6 schema is now typed with `mongo/int64Number@1` instead of `mongo/int32@1`; both read and write a `number`, so application code needs no change. `db sign` and `db verify` need nothing new: the contract carries no validators.
971
+
972
+ ## `prisma6-bytes-objectid-is-hex`
973
+
974
+ Before this release, `prisma6Schema(...)` refused every native type other than `String @db.ObjectId`. It now reads each native type Prisma 6 accepts on MongoDB as the codec for the BSON type Prisma 6 stores. Only `DateTime @db.Timestamp` is still refused.
975
+
976
+ A `Bytes @db.ObjectId` field stores an ObjectId, so Prisma 8 treats it like `String @db.ObjectId`: reads return the 24-digit hex string, and writes take a hex string or an ObjectId. The Prisma 6 client uses 12 bytes for the same field, and Prisma 8 refuses a `Buffer` or `Uint8Array` there.
977
+
978
+ Where application code that moves to the Prisma 8 client passes or reads such a field, convert at the boundary:
979
+
980
+ - bytes to the hex string Prisma 8 takes: `bytes.toString('hex')` for a `Buffer`, `Buffer.from(bytes).toString('hex')` for a `Uint8Array`
981
+ - the hex string Prisma 8 returns to bytes: `Buffer.from(hex, 'hex')`
982
+
983
+ The stored values do not change: Prisma 6 and Prisma 8 read and write the same ObjectId.
984
+
985
+ ## `writes-on-a-conditional-collection-are-refused`
986
+
987
+ `where`, `orderBy`, `limit`, `offset`, `distinct`, `distinctOn`, `cursor` and `include` now return the collection they were called on, with what they establish added to its type as a fact. A custom collection class keeps its methods through the chain, so `db.Post.where({ userId }).withTitle('orm')` and `db.Post.include('user').withTitle('orm')` compile.
988
+
989
+ The facts have names. Write a filtered collection's type as `Filtered<C>` and an ordered one as `Ordered<C>`. `Filtered<C>` is `C & HasWhere`, and `HasWhere` is the name error messages print. Import the names from the `orm-client` entry of your facade, for example `@prisma/orm-postgres/orm-client`.
990
+
991
+ This entry and `cursor-and-distinct-on-check-the-receiver`, `apply-is-a-collection-member`, `overriding-a-chaining-method`, `collection-state-flags-are-boolean`, `read-collection-state-and-row-with-helpers`, `return-type-of-a-chaining-method`, `chaining-methods-take-no-explicit-type-arguments` and `custom-collection-methods-chain` apply to the SQL ORM client only. The MongoDB ORM client did not change; skip matches in code that uses it.
992
+
993
+ A write needs a collection that is filtered on every code path. Code that filters only on some paths compiled before and no longer does:
994
+
995
+ ```ts
996
+ const posts = search ? db.Post.withTitle(search) : db.Post;
997
+ await posts.deleteAll(); // error: The 'this' context of type 'PostCollection' is not assignable to method's 'this' of type 'HasWhere'
998
+ ```
999
+
1000
+ The same applies to an `if` with an early return, a `switch`, a loop, and a `let` reassigned in an `if`. Make the write only where the filter was applied, or filter on every path:
1001
+
1002
+ ```diff
1003
+ - const posts = search ? db.Post.withTitle(search) : db.Post;
1004
+ - await posts.deleteAll();
1005
+ + if (search) {
1006
+ + await db.Post.withTitle(search).deleteAll();
1007
+ + }
1008
+ ```
1009
+
1010
+ If the code relied on deleting or updating every row when there is no filter, that was the unsafe case the check now refuses. State the intent with an explicit filter instead.
1011
+
1012
+ ## `writes-refuse-what-they-would-ignore`
1013
+
1014
+ `updateAll`, `updateAndCount`, `deleteAll` and `deleteAndCount` change every row that matches the filter. They ignored a `limit`, an `offset`, a `cursor`, `distinct` and `distinctOn` on the collection, so a chain such as `db.orm.public.Post.where(...).limit(10).deleteAll()` deleted every matching row, not ten, and one with `.cursor({ id })` deleted the rows before the cursor too. They now throw `ORM.ARGUMENT_INVALID` instead. An order does not change which rows they change, and they still accept it. Remove what the write would ignore, or read the rows and change them by their ids:
1015
+
1016
+ ```diff
1017
+ - await db.orm.public.Post.where({ userId }).limit(10).deleteAll();
1018
+ + const ids = (await db.orm.public.Post.where({ userId }).select('id').limit(10).all()).map((p) => p.id);
1019
+ + await db.orm.public.Post.where((p) => p.id.in(ids)).deleteAll();
1020
+ ```
1021
+
1022
+ `update` and `delete` change one row, the one `first()` returns, so the order, the offset, the cursor, `distinct` and `distinctOn` choose it; they are unchanged, except after `limit(0)`. `first()` replaces the limit with 1, so after `limit(0)` they changed one row; they now change none and return `null`. A `limit(0)` that a request can set, such as a page size of zero, no longer writes.
1023
+
1024
+ The exception is `update` with a relation callback, such as `posts: (posts) => posts.connect(...)`. It finds its row by the filter alone and ignored the order, the limit, the offset, the cursor, `distinct` and `distinctOn`, so it could change another row than the chain chose. It now throws `ORM.ARGUMENT_INVALID` on a collection with any of them. Find the row first and filter to it:
1025
+
1026
+ ```diff
1027
+ - await users.where({ teamId }).orderBy((u) => u.createdAt.asc()).update({ posts: (posts) => posts.connect([{ id: postId }]) });
1028
+ + const oldest = await users.where({ teamId }).orderBy((u) => u.createdAt.asc()).select('id').first();
1029
+ + if (oldest) await users.where({ id: oldest.id }).update({ posts: (posts) => posts.connect([{ id: postId }]) });
1030
+ ```
1031
+
1032
+ ## `cursor-and-distinct-on-check-the-receiver`
1033
+
1034
+ `cursor` and `distinctOn` need an order on the collection they are called on. The check is on the receiver, so a cast on the argument no longer bypasses it:
1035
+
1036
+ ```ts
1037
+ await db.Post.cursor({ id } as never).all(); // error: The 'this' context of type 'PostCollection' is not assignable to method's 'this' of type 'HasOrderBy'
1038
+ ```
1039
+
1040
+ Add the order the query needs:
1041
+
1042
+ ```diff
1043
+ - await db.Post.cursor({ id } as never).all();
1044
+ + await db.Post.orderBy((post) => post.id.asc()).cursor({ id }).all();
1045
+ ```
1046
+
1047
+ Where the query is meant to run without an order, cast the collection instead of the argument:
1048
+
1049
+ ```ts
1050
+ import type { Ordered } from '@prisma/orm-postgres/orm-client';
1051
+
1052
+ const unordered = db.Post as Ordered<typeof db.Post>;
1053
+ await unordered.cursor({ id }).all();
1054
+ ```
1055
+
1056
+ ## `apply-is-a-collection-member`
1057
+
1058
+ Collections have a new method, `apply(fn)`, which calls `fn` with the collection and returns the result. A custom collection class that declares its own `apply` with another signature no longer compiles. Rename that member and its call sites:
1059
+
1060
+ ```diff
1061
+ class PostCollection extends Collection<Contract, 'Post'> {
1062
+ - apply(limit: number) { return this.limit(limit); }
1063
+ + firstPage(limit: number) { return this.limit(limit); }
1064
+ }
1065
+ ```
1066
+
1067
+ An aggregate operation named `apply` is now refused with `ORM.AGGREGATE_OPERATION_RESERVED` when the client is built; rename the operation.
1068
+
1069
+ ## `scope-is-a-collection-member`
1070
+
1071
+ The `scope` change applies to the SQL ORM client only. The MongoDB ORM client did not change; skip matches in code that uses it.
1072
+
1073
+ Collections have a new method, `scope(body)`, which defines a scope for the collection's model: `db.orm.public.Post.scope((posts) => posts.select('id', 'title'))`. A custom collection class that declares its own `scope` with another signature no longer compiles. Rename that member and its call sites:
1074
+
1075
+ ```diff
1076
+ class PostCollection extends Collection<Contract, 'Post'> {
1077
+ - scope(userId: string) { return this.where({ userId }); }
1078
+ + ownedBy(userId: string) { return this.where({ userId }); }
1079
+ }
1080
+ ```
1081
+
1082
+ An aggregate operation named `scope` is now refused with `ORM.AGGREGATE_OPERATION_RESERVED` when the client is built; rename the operation.
1083
+
1084
+ ## `overriding-a-chaining-method`
1085
+
1086
+ A class that extends `Collection` and overrides one of the chaining methods, or one of the methods that return rows (`all`, `first`, `create`, `createAll`, `upsert` and the writes), must declare the override with the new signature: a type parameter for the receiver, a `this` parameter of that type, and the result type the base method returns. Call the base method with `call` and explicit type arguments, so that the receiver type passes through:
1087
+
1088
+ ```diff
1089
+ class PostCollection extends Collection<Contract, 'Post'> {
1090
+ - override limit(n: number) {
1091
+ - return super.limit(Math.min(n, 100));
1092
+ - }
1093
+ + override limit<Self>(this: Self, n: number): Self {
1094
+ + return super.limit.call<Self, [number], Self>(this, Math.min(n, 100));
1095
+ + }
1096
+ }
1097
+ ```
1098
+
1099
+ Only classes that extend the SQL `Collection` are affected; skip matches in other classes.
1100
+
1101
+ ## `collection-state-flags-are-boolean`
1102
+
1103
+ `DefaultCollectionTypeState` declares `hasWhere`, `hasOrderBy` and `hasUniqueFilter` as `boolean`, meaning not known. A method that establishes a flag sets it to `true`. Where your code expects `false`, expect `boolean`:
1104
+
1105
+ ```diff
1106
+ - type Check = Equal<CollectionTypeStateOf<typeof users>['hasOrderBy'], false>;
1107
+ + type Check = Equal<CollectionTypeStateOf<typeof users>['hasOrderBy'], boolean>;
1108
+ ```
1109
+
1110
+ A type of your own that sets a flag to `false` should set it to `boolean`. The writes still need `hasWhere: true`, and `cursor` and `distinctOn` still need `hasOrderBy: true`.
1111
+
1112
+ ## `read-collection-state-and-row-with-helpers`
1113
+
1114
+ `where`, `orderBy` and `include` record what they establish in two declared properties, not in the type arguments of `Collection`. Inferring the third or fourth type argument gives the row and the state the collection started with. Read them with `CollectionRowOf` and `CollectionTypeStateOf` instead:
1115
+
1116
+ ```diff
1117
+ - type RowOf<C> = C extends Collection<infer _C, infer _M, infer Row, infer _S> ? Row : never;
1118
+ - type UsersRow = RowOf<typeof users>;
1119
+ + import type { CollectionRowOf } from '@prisma/orm-postgres/orm-client';
1120
+ + type UsersRow = CollectionRowOf<typeof users>;
1121
+ ```
1122
+
1123
+ To keep a helper of your own, constrain its parameter, because both helpers require one:
1124
+
1125
+ ```ts
1126
+ import type { CollectionRowOf, CollectionTypeStateOf, HasRow, HasTypeState } from '@prisma/orm-postgres/orm-client';
1127
+
1128
+ type RowOf<C extends HasRow> = CollectionRowOf<C>;
1129
+ type StateOf<C extends HasTypeState> = CollectionTypeStateOf<C>;
1130
+ ```
1131
+
1132
+ ## `return-type-of-a-chaining-method`
1133
+
1134
+ The chaining methods are generic in their receiver, and `ReturnType` of a generic method uses the constraint of its type parameter. `ReturnType<PostCollection['where']>` is now `HasWhere`, and `ReturnType<PostCollection['limit']>` is `unknown`. Write the type with `Filtered` after `where`, `Ordered` after `orderBy`, and the collection type itself after `limit`, `offset`, `distinct`, `distinctOn` and `cursor`. You can also take `typeof` of a value:
1135
+
1136
+ ```diff
1137
+ - type MatchingPosts = ReturnType<PostCollection['where']>;
1138
+ + import type { Filtered } from '@prisma/orm-postgres/orm-client';
1139
+ + type MatchingPosts = Filtered<PostCollection>;
1140
+ ```
1141
+
1142
+ `ReturnType` of a method of your own class, such as `ReturnType<PostCollection['withTitle']>`, still works.
1143
+
1144
+ ## `chaining-methods-take-no-explicit-type-arguments`
1145
+
1146
+ These methods infer their receiver from the call. With explicit type arguments the receiver is not inferred: `posts.include<'user'>('user')` does not compile, `posts.distinct<['title']>('title')` and `posts.distinctOn<['title']>('title')` fail with "Expected 2 type arguments, but got 1", and `ReturnType<typeof posts.include<'user'>>` is `never`. Drop the type arguments. They are inferred from the arguments, so `distinct<['title']>('title')` becomes `distinct('title')`:
1147
+
1148
+ ```diff
1149
+ - const titles = posts.distinct<['title']>('title');
1150
+ + const titles = posts.distinct('title');
1151
+ ```
1152
+
1153
+ To name the type of an include, call `include` on a value and take its type:
1154
+
1155
+ ```diff
1156
+ - type WithUser = ReturnType<typeof posts.include<'user'>>;
1157
+ + const withUser = posts.include('user');
1158
+ + type WithUser = typeof withUser;
1159
+ ```
1160
+
1161
+ ## `custom-collection-methods-chain`
1162
+
1163
+ Custom collection methods are now available after `where`, `orderBy`, `limit` and the other chaining methods. Where code on a client built with `orm({ collections })` repeats a class method's body inline after a chaining call on that class, it can call the method. A client without custom classes, such as `db.orm`, has no such methods.
1164
+
1165
+ ```diff
1166
+ return db.Post.forUser(userId)
1167
+ - .orderBy((post) => post.createdAt.desc())
1168
+ + .newestFirst()
1169
+ .limit(limit)
1170
+ .all();
1171
+ ```
1172
+
1173
+ Here `newestFirst()` is a method of the application's `PostCollection` whose body is that `orderBy`.
1174
+
1175
+ ## `variant-takes-discriminator-value`
1176
+
1177
+ For every `.variant(...)` call matched by `detection`, replace the variant's model name with the discriminator value that variant declares. Read the value from the contract: in PSL it is the second argument of the variant's `@@base(Base, "<value>")`; in the TypeScript builder it is the `value` under the base model's `discriminator.variants.<VariantName>`; in `contract.json` it is `domain.namespaces.<ns>.models.<Base>.variants.<VariantName>.value`.
1178
+
1179
+ ```ts
1180
+ // before
1181
+ db.orm.public.Task.variant('Bug');
1182
+ db.orm.events.variant('ViewProductEvent');
1183
+
1184
+ // after, for @@base(Task, "bug") and a ViewProductEvent variant declaring "view-product"
1185
+ db.orm.public.Task.variant('bug');
1186
+ db.orm.events.variant('view-product');
1187
+ ```
1188
+
1189
+ Custom collection methods that call `this.variant('<ModelName>')`, and helpers or closures that call `.variant()`, change the same way. When the argument is a variable or a forwarded parameter, follow it back to where the model name is written and replace it there, including in type annotations that list variant model names. Update code comments and READMEs that show `.variant('<ModelName>')` too.
1190
+
1191
+ The type checker catches most old call sites, because the parameter only accepts the base model's declared values. It does not catch a call whose model-name argument happens to equal a declared value, for example a variant model declared with `@@base(Base, "Admin")`. Such a call keeps compiling and now selects the variant declaring that value, so check every call site against the contract rather than relying on type errors.
1192
+
1193
+ A call that passes a value through `as never` or another cast also escapes the type checker. At runtime, a value the base model does not declare, or a `variant()` call on a model without a discriminator, throws `ORM.ARGUMENT_INVALID`. The error names the model and lists its declared values. Previously the call returned the collection unchanged, so the query read every variant. Code that relied on that fallback must stop calling `variant()` in that case.
1194
+
1195
+ A second `.variant()` call on a variant collection is rejected. It no longer replaces the first selection: it is a type error, and at runtime it throws `ORM.OPERATION_UNSUPPORTED` naming the model and the discriminator value already selected. Select each variant from the base collection instead, and keep a reference to the base collection where code needs more than one variant.
1196
+
1197
+ ```ts
1198
+ // before
1199
+ const bugs = db.orm.public.Task.variant('Bug');
1200
+ const features = bugs.variant('Feature');
1201
+
1202
+ // after
1203
+ const tasks = db.orm.public.Task;
1204
+ const bugs = tasks.variant('bug');
1205
+ const features = tasks.variant('feature');
1206
+ ```
1207
+
1208
+ ## `imported-postgres-field-checks-defaults`
1209
+
1210
+ `import { field } from '@prisma/orm-postgres/contract-builder'` now gives the same field builders the `defineContract` callback receives, without the ones an extension adds: `field.text()`, `field.temporal.timestamptz()`, `field.uuidString()`, and `field.column(columnType)` as before. Its `.default(...)` now checks the value against the Postgres target's column types; a column type an extension adds, such as pgvector's `vector`, is not checked. A default of another type no longer compiles:
1211
+
1212
+ ```diff
1213
+ - field.column(int8Column).default(1)
1214
+ + field.column(int8Column).default(1n)
1215
+ ```
1216
+
1217
+ ## `re-emit-for-the-row-locking-capabilities`
1218
+
1219
+ The typed SQL builder gains four methods that lock the rows a select reads, named after the SQL they render: `forUpdate()`, `forNoKeyUpdate()`, `forShare()` and `forKeyShare()`. Each takes an optional `{ of, nowait, skipLocked }`: `of` names the tables or aliases to lock, and `nowait` and `skipLocked` exclude each other. A lock lasts until the transaction ends, so use it inside `db.transaction(...)`:
1220
+
1221
+ ```ts
1222
+ await db.transaction(async (tx) => {
1223
+ const [job] = await tx.query(
1224
+ tx.sql.public.job
1225
+ .select('id')
1226
+ .where((f, fns) => fns.eq(f.state, 'queued'))
1227
+ .limit(1)
1228
+ .forUpdate({ skipLocked: true })
1229
+ .build(),
1230
+ );
1231
+ });
1232
+ ```
1233
+
1234
+ Each method and each option is gated on a capability key that the Postgres adapter now reports: `sql.forUpdate`, `sql.forShare`, `postgres.forNoKeyUpdate`, `postgres.forKeyShare`, and `sql.lockOf`, `sql.lockNowait`, `sql.lockSkipLocked` for the options. A `contract.json` emitted before this release carries none of them, so the methods do not exist on its builder.
1235
+
1236
+ Re-emit your contract to pick up the keys:
1237
+
1238
+ ```console
1239
+ prisma contract emit
1240
+ ```
1241
+
1242
+ Nothing else changes. The keys are additive, the storage hash does not move, and every existing query behaves exactly as before. You only need to re-emit if you want to use the new methods. SQLite reports none of the keys, because SQLite has no row locks.
1243
+
1244
+ ## `cache-annotation-ttl-removed`
1245
+
1246
+ The read annotation no longer carries a lifetime. The store decides how long an entry lives. The default store, which `createCacheMiddleware()` uses when you pass no `store`, keeps every entry for 60 seconds.
1247
+
1248
+ This also changes behaviour without a type error. Before, a read annotated without `ttl` (`cacheAnnotation({})`, `cacheAnnotation({ key })`, or a `ttl` that evaluated to `undefined`) was not cached. Now every annotated read in runtime scope is cached. If a read must stay uncached, remove its annotation, or pass `cacheAnnotation({ bypass: true })`.
1249
+
1250
+ In each file that calls `cacheAnnotation(...)`:
1251
+
1252
+ 1. Remove the `ttl` property. `cacheAnnotation({ ttl: 60_000 })` becomes `cacheAnnotation({})`, and `cacheAnnotation({ ttl, key })` becomes `cacheAnnotation({ key })`.
1253
+ 2. Remove any variable, function parameter or option field that existed only to supply that `ttl` (for example a `ttlMs` option on a helper that wraps the annotated read), and remove it from its callers.
1254
+ 3. If every read used the same lifetime and it was not 60 seconds, set it once on the store: `createCacheMiddleware({ store: createInMemoryCacheStore({ ttlMs }) })`. `ttlMs: Infinity` never expires. If reads need different lifetimes, write a custom `CacheStore` that reads the lifetime from the annotation's `meta` (see `cache-store-object-arguments`), and pass it as `cacheAnnotation({ meta: { ttlMs } })`.
1255
+ 4. Update comments and READMEs that say the annotation needs a `ttl`, that caching happens "within the TTL window" of the annotation, or that an annotation without `ttl` passes through. Say instead that an annotated read is cached and the default store keeps the entry for 60 seconds.
1256
+
1257
+ Before:
1258
+
1259
+ ```ts
1260
+ export async function getUsersCached(limit = 10, ttlMs = 60_000) {
1261
+ const plan = db.sql.public.user
1262
+ .select('id', 'email')
1263
+ .annotate(cacheAnnotation({ ttl: ttlMs }))
1264
+ .limit(limit)
1265
+ .build();
1266
+ return db.runtime().query(plan);
1267
+ }
1268
+ ```
1269
+
1270
+ After:
1271
+
1272
+ ```ts
1273
+ export async function getUsersCached(limit = 10) {
1274
+ const plan = db.sql.public.user
1275
+ .select('id', 'email')
1276
+ .annotate(cacheAnnotation({}))
1277
+ .limit(limit)
1278
+ .build();
1279
+ return db.runtime().query(plan);
1280
+ }
1281
+ ```
1282
+
1283
+ ## `cache-annotation-skip-renamed-bypass`
1284
+
1285
+ In each file that imports from `@prisma/orm-extension-middleware-cache`:
1286
+
1287
+ 1. Rename `skip` to `bypass` in every `cacheAnnotation(...)` argument: `cacheAnnotation({ skip: forceRefresh })` becomes `cacheAnnotation({ bypass: forceRefresh })`. Only rename `skip` inside a `cacheAnnotation` argument or a value typed `CachePayload`; leave other `skip` properties alone.
1288
+ 2. Rename the type `CachePayload` to `CacheAnnotationOptions`, in imports and in uses.
1289
+ 3. Update comments that describe `skip` on the annotation, or that say `skip` wins over a `ttl`.
1290
+
1291
+ ## `cache-middleware-store-options`
1292
+
1293
+ In each call to `createCacheMiddleware(...)`:
1294
+
1295
+ - If the options contain only `maxEntries: 1000` (or `1_000`), and no `clock`, call `createCacheMiddleware()` with no options.
1296
+ - Otherwise, move `maxEntries` and `clock` to a store: `createCacheMiddleware({ maxEntries: 500 })` becomes `createCacheMiddleware({ store: createInMemoryCacheStore({ maxEntries: 500 }) })`, importing `createInMemoryCacheStore` from `@prisma/orm-extension-middleware-cache`. The middleware's `clock` used to stamp `storedAt` only; it now drives the store's expiry.
1297
+
1298
+ `createInMemoryCacheStore` options are all optional now: `maxEntries` (default 1000), `ttlMs` (default 60 000) and `clock` (default `Date.now`).
1299
+
1300
+ ## `cache-store-object-arguments`
1301
+
1302
+ Only for code that implements `CacheStore`, as a typed object, a class, or an object literal passed inline to `createCacheMiddleware({ store: { ... } })`. The types reject the old shape, but an untyped JavaScript store fails only at run time, so check each one.
1303
+
1304
+ 1. Keep a version per key: an integer, 0 for a key never seen, that only `unset` changes. Keep it even for keys that hold no value, for at least as long as a read can take.
1305
+ 2. Change `get(key)` to `get({ key, meta })`, returning a `CacheEntry`: `{ key, meta, version, data }`, where `meta` is the `meta` passed in, `version` the key's current version, and `data` is `{ empty: true }` on a miss or `{ empty: false, value }` with the stored rows. `meta` is the read annotation's `meta`, or `undefined`. A store that matches `meta` folds the versions of whatever `meta` names into `version`, and does the same in `set`; a store that does not index `meta` ignores it here.
1306
+ 3. Change `set(key, entry, ttlMs)` to `set(entry, value)`, returning a boolean. `entry` is the `CacheEntry` your `get` returned; `value` is the rows. Store `value` under `entry.key` only if the key's version still equals `entry.version`, and return whether you stored. The comparison and the write must be atomic against `unset`: one synchronous step in memory, or one server-side script (for example Lua on Redis). There is no unconditional write and no `ttlMs` argument: the store sets the lifetime itself, as a fixed value or read from `entry.meta`. Code that prefilled the cache with `set(key, entry, ttlMs)` now calls `get` and then `set`.
1307
+ 4. Add `unset({ keys, meta })`. It removes the values stored under `keys` when `keys` is not `undefined`, and increments the version of every one of those keys, whether or not it holds a value. When `meta` is not `undefined`, it removes the values whose `meta` matches, compared by value, and increments their versions; a store that does not index `meta` must throw instead of ignoring it.
1308
+ 5. Replace `CachedEntry` with `CachedRows` (the rows, `readonly Record<string, unknown>[]`) for stored values and `CacheEntry` for what `get` returns. Type the store as `CacheStore<TMeta>`, which is `CacheStore<TMeta, CachedRows>`; `createCacheMiddleware` rejects a store of any other value type, `unknown` included. There is no `storedAt`.
1309
+
1310
+ Before:
1311
+
1312
+ ```ts
1313
+ const store: CacheStore = {
1314
+ async get(key) {
1315
+ const raw = await redis.get(key);
1316
+ return raw ? (JSON.parse(raw) as CachedEntry) : undefined;
1317
+ },
1318
+ async set(key, entry, ttlMs) {
1319
+ await redis.set(key, JSON.stringify(entry), 'PX', ttlMs);
1320
+ },
1321
+ };
1322
+ ```
1323
+
1324
+ After:
1325
+
1326
+ ```ts
1327
+ const SET_IF_VERSION = `
1328
+ if tonumber(redis.call('GET', KEYS[2]) or '0') ~= tonumber(ARGV[2]) then return 0 end
1329
+ redis.call('SET', KEYS[1], ARGV[1], 'PX', 60000)
1330
+ return 1`;
1331
+
1332
+ const store: CacheStore<unknown, CachedRows> = {
1333
+ async get({ key, meta }) {
1334
+ const [raw, version] = await redis.mget(`value:${key}`, `version:${key}`);
1335
+ return {
1336
+ key,
1337
+ meta,
1338
+ version: Number(version ?? 0),
1339
+ data: raw ? { empty: false, value: JSON.parse(raw) as CachedRows } : { empty: true },
1340
+ };
1341
+ },
1342
+ async set(entry, value) {
1343
+ const keys = [`value:${entry.key}`, `version:${entry.key}`];
1344
+ const json = JSON.stringify(value);
1345
+ return (await redis.eval(SET_IF_VERSION, 2, ...keys, json, entry.version)) === 1;
1346
+ },
1347
+ async unset({ keys, meta }) {
1348
+ if (meta !== undefined) {
1349
+ throw new Error('This store does not index meta');
1350
+ }
1351
+ for (const key of keys ?? []) {
1352
+ await redis
1353
+ .multi()
1354
+ .del(`value:${key}`)
1355
+ .incr(`version:${key}`)
1356
+ .pexpire(`version:${key}`, 60_000)
1357
+ .exec();
1358
+ }
1359
+ },
1360
+ };
1361
+ ```
1362
+
1363
+ ## `contract-artifacts-restamp`
1364
+
1365
+ For every `contract.json` matched by `detection`, run the project's emit command (`prisma contract emit`, or the project's `contract:emit` script) once after upgrading. This entry accounts for the extension's embedded `version` moving to `8.0.0-rc.15`; any other difference in the emitted files comes from an earlier entry in this guide.