@prisma/orm-postgres 8.0.0-rc.17-dev.42 → 8.0.0-rc.18
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.
- package/package.json +8 -8
- package/skills/prisma-8/SKILL.md +1 -1
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.17-to-8.0.0-rc.18/instructions.md +973 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.17-to-8.0.0-rc.18/scripts/sql-expression-literals-psl/rewrite-sql-strings.mjs +271 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.17-to-8.0.0-rc.18/instructions.md +1008 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: "8.0.0-rc.17"
|
|
3
|
+
to: "8.0.0-rc.18"
|
|
4
|
+
changes:
|
|
5
|
+
- id: psl-default-namespace-duplicate-declaration
|
|
6
|
+
summary: |
|
|
7
|
+
A declaration at the top level of a PSL schema and one inside `namespace public { … }` (Postgres) now share one set of names. The same name declared both ways is `PSL_DUPLICATE_DECLARATION`, and so is a member of `namespace public { … }` named like a `types { }` binding or a namespace. Remove or rename one.
|
|
8
|
+
detection:
|
|
9
|
+
glob: "**/*.{prisma,psl}"
|
|
10
|
+
matches:
|
|
11
|
+
- '(^|\n)\s*namespace\s+public\s*\{'
|
|
12
|
+
- id: psl-default-namespace-name-is-reserved
|
|
13
|
+
summary: |
|
|
14
|
+
A top-level model, composite type, block or `types { }` binding cannot be named like the target's default namespace: `public` on Postgres; `unbound` or `__unbound__` on SQLite and MongoDB. This also holds for a schema read with `prisma7Schema(...)` or `prisma6Schema(...)`. Rename the declaration.
|
|
15
|
+
detection:
|
|
16
|
+
glob: "**/*.{prisma,psl}"
|
|
17
|
+
matches:
|
|
18
|
+
- '(^|\n)[ \t]*(?!namespace[ \t])[A-Za-z_]\w*[ \t]+(public|unbound|__unbound__)[ \t]*\{'
|
|
19
|
+
- '(^|\n)\s*(public|unbound|__unbound__)\s*=\s*\S'
|
|
20
|
+
- id: raw-sql-is-a-sql-literal
|
|
21
|
+
summary: |
|
|
22
|
+
`@@index(where:)`, `@@index(expression:)`, `@@fullTextIndex(where:)`, `@@check(expression:)` and a policy's `using` and `withCheck` take `sql` literals. A quoted string there is refused with `PSL_VALUE_TYPE_INCOMPATIBLE`.
|
|
23
|
+
detection:
|
|
24
|
+
glob: "**/*.prisma"
|
|
25
|
+
matches:
|
|
26
|
+
- '\b(where|expression)\s*:\s*["'']'
|
|
27
|
+
- '^\s*(using|withCheck)\s*=\s*["'']'
|
|
28
|
+
script: ./scripts/sql-expression-literals-psl/rewrite-sql-strings.mjs
|
|
29
|
+
- id: psl-storage-hash-may-change-once
|
|
30
|
+
summary: |
|
|
31
|
+
A raw SQL text with indentation shared by every line, blank lines at the start or end, a whitespace-only line or CRLF line breaks is stored as its canonical text once it is written as a `sql` literal, which changes the contract's storage hash once. For an index or check named with `map:`, `migration plan` then stops with a conflict and asks for a migration written with `migration new`. For a policy named with `@@map`, `migration plan` writes a migration that drops the policy and creates it again.
|
|
32
|
+
detection:
|
|
33
|
+
glob: "**/*.prisma"
|
|
34
|
+
matches:
|
|
35
|
+
- '\b(where|expression)\s*:\s*["'']'
|
|
36
|
+
- '^\s*(using|withCheck)\s*=\s*["'']'
|
|
37
|
+
- id: builder-raw-sql-is-a-sql-value
|
|
38
|
+
summary: |
|
|
39
|
+
In a TypeScript contract, `index({ where, expression })`, `check({ expression })`, `fullTextIndex({ where })` and the `using` and `withCheck` of `policySelect`, `policyInsert`, `policyUpdate`, `policyDelete` and `policyAll` take a `sql` value. A string there does not compile. Migration files keep strings.
|
|
40
|
+
detection:
|
|
41
|
+
glob: "**/*.{ts,mts,cts}"
|
|
42
|
+
matches:
|
|
43
|
+
- '(?<!readonly |''function'', )\b(where|expression|using|withCheck)\s*:\s*[''"\x60]'
|
|
44
|
+
- id: sql-tag-interpolates-sql-values
|
|
45
|
+
summary: |
|
|
46
|
+
The `sql` tag interpolates other `sql` values: `` sql`${owner} AND deleted_at IS NULL` ``. Compose predicates this way instead of joining strings.
|
|
47
|
+
detection:
|
|
48
|
+
glob: "**/*.{ts,mts,cts}"
|
|
49
|
+
matches:
|
|
50
|
+
- '(?<!\bsql)\x60\$\{[\w.]+\}\s+(AND|OR)\b'
|
|
51
|
+
- id: sql-tag-error-codes-renamed
|
|
52
|
+
summary: |
|
|
53
|
+
`CONTRACT.DEFAULT_SQL_INTERPOLATION` is renamed `CONTRACT.SQL_EXPRESSION_INTERPOLATION`. A NUL character or an oversize text in a `sql` value is `CONTRACT.SQL_EXPRESSION_INVALID`, not `CONTRACT.DEFAULT_INVALID`. `.default()` raises `CONTRACT.DEFAULT_INVALID` for `` sql`now()` ``, `` sql`autoincrement()` `` and unsafe SQL.
|
|
54
|
+
detection:
|
|
55
|
+
glob: "**/*.{ts,mts,cts}"
|
|
56
|
+
matches:
|
|
57
|
+
- '\bDEFAULT_SQL_INTERPOLATION\b'
|
|
58
|
+
- '\bCONTRACT\.DEFAULT_INVALID\b'
|
|
59
|
+
- id: ts-storage-hash-may-change-once
|
|
60
|
+
summary: |
|
|
61
|
+
A raw SQL string with indentation shared by every line, blank lines at the start or end, a whitespace-only line or CRLF line breaks is stored as its canonical text once it is written as a `sql` value, which changes the contract's storage hash once.
|
|
62
|
+
detection:
|
|
63
|
+
glob: "**/*.{ts,mts,cts}"
|
|
64
|
+
matches:
|
|
65
|
+
- '(?<!readonly )\b(where|expression|using|withCheck)\s*:\s*(\x60[ \t]*$|[''"\x60][ \t\r\n])'
|
|
66
|
+
- id: infer-notes-defaults-that-do-not-read-back
|
|
67
|
+
summary: |
|
|
68
|
+
`contract infer` writes a column default whose text a `sql` literal cannot write back unchanged and adds a note to the model; `contract print` refuses such a default with `CONTRACT.PRINT_UNSUPPORTED`.
|
|
69
|
+
detection:
|
|
70
|
+
glob: "**/contract.prisma"
|
|
71
|
+
matches:
|
|
72
|
+
- '@default\(\s*sql\x60[^\x60]*$'
|
|
73
|
+
- '@default\(\s*sql"[^"]*\\[rn]'
|
|
74
|
+
- id: re-emit-contracts-with-a-full-text-index
|
|
75
|
+
summary: |
|
|
76
|
+
A full-text index declared before this change is stored in `contract.json` in a new form: an index of type `fullText` over its columns whose `options` hold the weight groups and the language, with no `expression`. Its storage hash and its index name change, so the first migration after upgrading changes the index: `migration plan` renames it, and `db update` drops and recreates it. Re-emit the contract and plan a migration.
|
|
77
|
+
detection:
|
|
78
|
+
glob: "**/*.{prisma,ts,mts}"
|
|
79
|
+
matches:
|
|
80
|
+
- '@@fullTextIndex\s*\('
|
|
81
|
+
- '(?<![\w$.])fullTextIndex\s*\('
|
|
82
|
+
- id: full-text-index-one-field-diagnostic-removed
|
|
83
|
+
summary: |
|
|
84
|
+
`@@fullTextIndex` now takes several fields, so the diagnostic `PSL_FULL_TEXT_INDEX_ONE_FIELD` is gone. Code that asserts it asserts a diagnostic that is never raised.
|
|
85
|
+
detection:
|
|
86
|
+
glob: "**/*.{ts,mts,cts,js,mjs}"
|
|
87
|
+
matches:
|
|
88
|
+
- '\bPSL_FULL_TEXT_INDEX_ONE_FIELD\b'
|
|
89
|
+
- id: re-emit-contracts-with-polymorphic-models
|
|
90
|
+
summary: |
|
|
91
|
+
A polymorphic base model's `variants` is stored in a new form in `contract.json` and `contract.d.ts`: keyed by discriminator value, each entry naming the variant model and its namespace. A contract in the previous form is rejected with `CONTRACT.VALIDATION_FAILED`. Re-emit the contract, and rewrite `variants` in the contract snapshots under `migrations/snapshots/`.
|
|
92
|
+
detection:
|
|
93
|
+
glob: "**/contract*.json"
|
|
94
|
+
matches:
|
|
95
|
+
- '"variants"\s*:\s*\{\s*"[^"]+"\s*:\s*\{\s*"value"\s*:'
|
|
96
|
+
- id: read-variants-by-discriminator-value
|
|
97
|
+
summary: |
|
|
98
|
+
Code that reads a base model's `variants` from a contract finds the variant model at `variants[<value>].model`, where it used to find the value at `variants[<Model>].value`.
|
|
99
|
+
detection:
|
|
100
|
+
glob: "**/*.{ts,tsx,mts,cts,js,mjs,cjs}"
|
|
101
|
+
matches:
|
|
102
|
+
- '(?<![\w$])variants(?![\w$])'
|
|
103
|
+
- id: prisma7-ignore-keeps-storage
|
|
104
|
+
summary: |
|
|
105
|
+
A Prisma 7 schema read with `prisma7Schema()` keeps the columns, tables, keys, indexes and foreign keys of `@ignore` fields and `@@ignore` models in the contract's storage, as storage with no model. Ignored objects now meet every rule of the reader, so a schema that loaded only because they were dropped can be refused. A project whose Prisma 7 schema uses `@ignore` or `@@ignore` gets a new storage hash: it emits its contract again and brings each existing database to the new hash, after recording a migration if Prisma 8 already plans its migrations. The detection matches a `.prisma` file that uses `@ignore` or `@@ignore`.
|
|
106
|
+
detection:
|
|
107
|
+
glob: "**/*.prisma"
|
|
108
|
+
matches:
|
|
109
|
+
- '@ignore\b'
|
|
110
|
+
- id: mongo-enum-member-written-as-stored
|
|
111
|
+
summary: |
|
|
112
|
+
A Mongo contract now refuses an enum member that its codec stores as a value it reads back differently, with `CONTRACT.ENUM_INVALID` in TypeScript and `PSL_EXTENSION_INVALID_VALUE` in PSL and Prisma 6 schemas. The built-in Mongo codecs store every enum member as written, so only an enum typed by your own codec can be refused. Write each refused member as the message says, or fix the codec so `decodeJson(encodeJson(value))` gives the value back.
|
|
113
|
+
- id: typescript-contract-namespace-keys
|
|
114
|
+
summary: |
|
|
115
|
+
A contract built with `defineContract` from TypeScript now types `contract.domain.namespaces`, `contract.storage.namespaces` and `db.orm` with the same namespace keys the built contract has at runtime, each holding only its own models and tables. Before, any string key typechecked and every namespace listed every model. Code that reads a namespace the contract does not have, or indexes `domain.namespaces` with a `string` variable, now fails to typecheck. ORM rows of such a contract, which were typed `unknown` field by field, now have each field's codec output type, so a cast to a type that disagrees with the codec type now fails with TS2352. The detection matches dot and bracket access to `domain.namespaces`.
|
|
116
|
+
detection:
|
|
117
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
118
|
+
matches:
|
|
119
|
+
- '\.domain\.namespaces\b'
|
|
120
|
+
- '\[\s*[''"]domain[''"]\s*\]\s*\[\s*[''"]namespaces[''"]\s*\]'
|
|
121
|
+
- id: model-with-no-fields-has-no-keys
|
|
122
|
+
summary: |
|
|
123
|
+
A model with no fields now has no keys at the type level: reading a property of its row, such as `row.legacy_key`, fails to compile. In an emitted `contract.d.ts`, the fields and output types of such a model change from `Record<string, never>` to `{}`. In the SQL ORM client, writing a key in the create or update input of such a model, such as `create({ legacy_key: 'x' })`, also fails to compile now; before, both the read and the write compiled, for an emitted contract and for a contract built with `defineContract`. In the Mongo ORM, writes behave as before: an emitted contract already rejected such a key, and the update methods now take the type `UpdateInput` (see the next entry).
|
|
124
|
+
- id: generic-update-data-uses-update-input-type
|
|
125
|
+
summary: |
|
|
126
|
+
Code that is generic over the model name and passes a value typed `Partial<DefaultModelRow<Contract, M>>` to `updateAll`, `updateAndCount` or the `update` of `upsert` no longer compiles. Type the value as `ScalarUpdateInput<Contract, M>` from the SQL ORM client (`orm-client` entry point of the SQL facade, such as `@prisma/orm-postgres/orm-client`), or `UpdateInput<Contract, M>` from the Mongo ORM, instead.
|
|
127
|
+
detection:
|
|
128
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
129
|
+
matches:
|
|
130
|
+
- 'Partial<\s*DefaultModelRow<'
|
|
131
|
+
- id: orm-refuses-names-that-are-not-fields
|
|
132
|
+
summary: |
|
|
133
|
+
The SQL ORM client refuses any name a caller passes that is not a field of the model, or of the variant the collection is narrowed to, with `ORM.FIELD_UNKNOWN`. That covers `where` shorthand and callbacks, `orderBy`, `select`, `distinct`, `distinctOn`, `groupBy`, `cursor`, aggregates, `having`, create, update and upsert data, `conflictOn`, include nested selects and relation filters. A column name that differs from its field name is no longer accepted, and rows no longer carry a column no field maps. `conflictOn` with a name that is not a field raised `ORM.ARGUMENT_INVALID` and now raises `ORM.FIELD_UNKNOWN`. The detection matches a file that names both `conflictOn` and `ORM.ARGUMENT_INVALID`.
|
|
134
|
+
detection:
|
|
135
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
136
|
+
matches:
|
|
137
|
+
- '(?<![\s\S])(?=[\s\S]*\bconflictOn\b)[\s\S]*\bORM\.ARGUMENT_INVALID\b'
|
|
138
|
+
- id: fragment-is-made-from-the-root-collection
|
|
139
|
+
summary: "The SQL ORM client's collection.fragment(body) refuses a collection with chained calls, such as db.Post.where(...).fragment(body), which used to ignore those calls; chain on the collection directly instead, or define a shared fragment on the model's root collection. A receiver typed by a type parameter, such as this in a custom collection class method, is refused at compile time. The detection matches .fragment( right after a closing parenthesis, as in a chained call, and this.fragment(."
|
|
140
|
+
detection:
|
|
141
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
142
|
+
matches:
|
|
143
|
+
- '\)\s*\.fragment\('
|
|
144
|
+
- '\bthis\s*\.fragment\('
|
|
145
|
+
- id: fragment-types-are-renamed
|
|
146
|
+
summary: |
|
|
147
|
+
The SQL ORM client's types `Fragment` and `FieldFragment`, which 8.0.0-rc.17 exports, are renamed to `QueryFragment` and `DeclaredFieldsFragment`. Code still on the 8.0.0-rc.16 names renames `Scope` to `QueryFragment`, `FieldScope` to `DeclaredFieldsFragment` and `ScopeFacts` to `FragmentFacts` directly. `FragmentFacts`, `DeclaredField`, `db.orm.fragment` and `collection.fragment` keep their names. The detection matches `Fragment` and `Scope` only in an import or re-export from an `orm-client` entry, after a namespace import of one, or in an inline `import(...)` type of one, so React's `Fragment` and the SQL builder's `Scope` do not match.
|
|
148
|
+
detection:
|
|
149
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
150
|
+
matches:
|
|
151
|
+
- '\b(?:FieldFragment|FieldScope|ScopeFacts)\b'
|
|
152
|
+
- '(?:import|export)\s+(?:type\s+)?\{[^}]*\b(?:Fragment|Scope)\b[^}]*\}\s*from\s*[''"]@prisma/[\w-]+/orm-client[''"]'
|
|
153
|
+
- 'import\s+(?:type\s+)?\*\s+as\s+([\w$]+)\s+from\s*[''"]@prisma/[\w-]+/orm-client[''"][\s\S]*\b\1\.(?:Fragment|Scope)\b'
|
|
154
|
+
- 'import\(\s*[''"]@prisma/[\w-]+/orm-client[''"]\s*\)\.(?:Fragment|FieldFragment|Scope|FieldScope)\b'
|
|
155
|
+
- id: unique-criterion-rejects-null
|
|
156
|
+
summary: |
|
|
157
|
+
`UniqueConstraintCriterion` in the SQL ORM client (`@prisma/orm-postgres/orm-client` and the other facades' `orm-client` entries) no longer accepts `null` for a nullable unique column, so the `conflictOn` of `upsert` and the criteria of a relation `connect` or `disconnect` stop compiling when a value is `null` or may be `null`. Pass a non-null value, or handle the null case before the call. The detection matches every `conflictOn`, `.connect(` and `.disconnect(`; leave as they are the calls on other objects, such as `postgres.connect(...)`, and the `conflictOn` of `createAll` and `createAndCount`, which is an array of field names.
|
|
158
|
+
detection:
|
|
159
|
+
glob: "**/*.{ts,tsx,mts,cts}"
|
|
160
|
+
matches:
|
|
161
|
+
- '\bconflictOn\b|\.(?:connect|disconnect)\s*\(|\bUniqueConstraintCriterion\b'
|
|
162
|
+
- id: prepared-all-and-aggregate-need-their-receiver
|
|
163
|
+
summary: |
|
|
164
|
+
On every collection of the SQL ORM client, `prepared.all` and `prepared.aggregate` no longer compile when called without the `prepared` object as their receiver, such as after `const { all } = collection.prepared`. Call them on the `prepared` object. `prepared.first` and `prepared.firstOrThrow` are not affected, and nothing changes at run time.
|
|
165
|
+
detection:
|
|
166
|
+
glob: "**/*.{ts,tsx,mts,cts}"
|
|
167
|
+
matches:
|
|
168
|
+
- '\{[^}]*\b(?:all|aggregate)\b[^}]*\}\s*=\s*[^;\n]*\.prepared\b'
|
|
169
|
+
- id: native-enums-leave-out-domain-enums
|
|
170
|
+
summary: |
|
|
171
|
+
On Postgres, `db.nativeEnums` now holds only native enum types (`native_enum` blocks and `nativeEnum()`). It used to also hold every domain enum (`enum` blocks and `enumType()`), with its values as stored, such as `['1', '10']` for an enum over `pg/int8`. Read a domain enum through `db.enums`, which holds its values as a query returns them.
|
|
172
|
+
detection:
|
|
173
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
174
|
+
matches:
|
|
175
|
+
- '\.nativeEnums\b'
|
|
176
|
+
- id: middleware-hooks-are-activity-hooks
|
|
177
|
+
summary: |
|
|
178
|
+
The per-statement middleware hooks are replaced. `beforeQuery` and `beforeExecute` become `beforeEncode`; `interceptQuery` and `interceptExecute` become `intercept.onQuery` and `intercept.onExecute`; `afterQuery` and `afterExecute` become `activityEnded` on a statement; `afterTransaction` becomes `activityFinalized` on a statement. `onRow` and `beforeCompile` are unchanged.
|
|
179
|
+
detection:
|
|
180
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
181
|
+
matches:
|
|
182
|
+
- '\b(beforeQuery|beforeExecute|interceptQuery|interceptExecute|afterQuery|afterExecute|afterTransaction)\b'
|
|
183
|
+
- id: middleware-context-is-activity-context
|
|
184
|
+
summary: |
|
|
185
|
+
`RuntimeMiddlewareContext` is renamed `ActivityContext`. It gains `parent`, `state(key)` and `inTransaction()`, and loses `planExecutionId`; use `activity.id` instead.
|
|
186
|
+
detection:
|
|
187
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
188
|
+
matches:
|
|
189
|
+
- '\bRuntimeMiddlewareContext\b'
|
|
190
|
+
- '\bplanExecutionId\b'
|
|
191
|
+
- id: after-result-types-removed
|
|
192
|
+
summary: |
|
|
193
|
+
`AfterQueryResult`, `AfterExecuteResult` and `AfterTransactionResult` are removed. Use `ActivityEndedResult` (narrow it with `result.kind === 'statement'` and `result.returns`) and `ActivityFinalizedResult`.
|
|
194
|
+
detection:
|
|
195
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
196
|
+
matches:
|
|
197
|
+
- '\bAfter(Query|Execute|Transaction)Result\b'
|
|
198
|
+
- id: aborted-phase-before-encode
|
|
199
|
+
summary: |
|
|
200
|
+
A `RUNTIME.ABORTED` error raised during the middleware stage before encoding reports `details.phase: 'beforeEncode'`, not `'beforeQuery'` or `'beforeExecute'`. The phases `'afterQuery'` and `'afterExecute'` no longer occur. Mongo's `RUNTIME.CONTENT_HASH_REQUIRES_RESOLVED_COMMAND` also reports `details.phase: 'beforeEncode'`, not `'beforeExecute'`.
|
|
201
|
+
detection:
|
|
202
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
203
|
+
matches:
|
|
204
|
+
- '[''"`](beforeQuery|beforeExecute|afterQuery|afterExecute)[''"`]'
|
|
205
|
+
- id: every-started-statement-is-ended-and-finalized
|
|
206
|
+
summary: |
|
|
207
|
+
Every statement the runtime accepts now starts an activity, and every started statement ends and is finalized exactly once. A call with an already-aborted signal fires `activityStarted` before it fails. A statement that fails before it runs, in `beforeCompile`, `beforeEncode` or parameter encoding, now ends `failed` and is finalized, where it got no `afterTransaction` before. `activityEnded` also fires for a statement the caller stopped reading (outcome `'stopped'`) and for a statement that failed before it ran (outcome `'failed'`, `latencyMs: 0`, `source: 'driver'`); `afterQuery` and `afterExecute` fired for neither, so a metrics or slow-query middleware now counts them unless it checks `result.outcome`. When a middleware's `activityStarted` throws, the middleware after it receive no `activityEnded` or `activityFinalized` for that activity. A statement still running when its transaction ends, such as a row stream still being read, is finalized when it ends, with the transaction's outcome, and keeps its `ctx.state` until then.
|
|
208
|
+
- id: observed-row-budget-runs
|
|
209
|
+
summary: |
|
|
210
|
+
The `budgets()` middleware now counts the rows a query returns and enforces `maxRows` on them. Before, the observed-row check never ran. With the default row severity `'error'`, or in `strict` mode, a query that returns more than `maxRows` rows fails with `BUDGET.ROWS_EXCEEDED`; with `'warn'` in `permissive` mode it logs one warning for the statement and keeps streaming.
|
|
211
|
+
detection:
|
|
212
|
+
glob: "**/*.{ts,mts,cts,tsx}"
|
|
213
|
+
matches:
|
|
214
|
+
- '\bbudgets\('
|
|
215
|
+
- id: lossy-type-changes-ask
|
|
216
|
+
summary: |
|
|
217
|
+
On Postgres and SQLite, `migration plan` and `db update` no longer write a type change that can lose values without asking. A script that runs them without a terminal fails with `CLI.CONSENT_REQUIRED` until it passes `--convert <Model.field>` (`migration plan` only: write the conversion into the slot) or `--delete <Model.field>` (the database's own cast runs: it may cut or round values, such as a longer text cut to a shorter `VarChar` on Postgres, and a value it cannot cast fails the apply).
|
|
218
|
+
- id: placeholders-only-when-asked
|
|
219
|
+
summary: |
|
|
220
|
+
`migration plan` writes `placeholder(...)` slots on Postgres and SQLite only for a field that `--convert` or `--backfill` names. A new required field now gets a temporary value in the existing rows (`''`, `0`, …), which the output names, and an optional field made required is tightened directly. Pass `--backfill <Model.field>` to get the backfill step to fill instead.
|
|
221
|
+
- id: required-field-failures-count-rows
|
|
222
|
+
summary: |
|
|
223
|
+
A migration that makes a field required while rows have no value for it fails with `MIGRATION.RUNNER_FAILED`, whose summary counts the rows and whose advice names the fix. Scripts that matched the old summary text (`… failed during precheck: ensure no NULL values in …`) must match the new one, or the `rowsWithoutValue` failure field.
|
|
224
|
+
- id: postgres-alter-column-type-plain-options
|
|
225
|
+
summary: |
|
|
226
|
+
`this.alterColumnType(...)` in a Postgres `migration.ts` now takes `{ schema, table, column, type, using?, operationClass? }`. The `options` object with `qualifiedTargetType`, `formatTypeExpected`, `rawTargetTypeForLabel` and `using` is gone. Move `options.qualifiedTargetType` to `type` and `options.using` to `using`, and drop the other two.
|
|
227
|
+
detection:
|
|
228
|
+
glob: "**/migration.ts"
|
|
229
|
+
matches:
|
|
230
|
+
- '\bqualifiedTargetType\s*:'
|
|
231
|
+
- id: mongo-rename-statements-carried-out
|
|
232
|
+
summary: |
|
|
233
|
+
On MongoDB, `migration plan` and `db update` carry out `--rename`: a model rename renames its collection, keeping the documents, indexes and validator, and a field rename renames the field in every document that holds it. They used to refuse every statement with `MIGRATION.PLANNING_FAILED`. A question about a dropped collection now offers `--rename` as well as `--delete`.
|
|
234
|
+
- id: mongo-removed-field-asks
|
|
235
|
+
summary: |
|
|
236
|
+
On MongoDB, removing a field from a model is now data loss. `migration plan` and `db update` ask about it, and fail with `CLI.CONSENT_REQUIRED` where nobody can answer, until `--delete <Model>.<field>` or a `--rename` answers it. `--delete` removes the field from every document that holds it.
|
|
237
|
+
- id: mongo-preview-get-collection
|
|
238
|
+
summary: |
|
|
239
|
+
A MongoDB plan's preview writes each collection as `db.getCollection("users")`, for example `db.getCollection("users").createIndex(...)` where it wrote `db.users.createIndex(...)`. `ops.json` does not change.
|
|
240
|
+
detection:
|
|
241
|
+
glob: "**/*.{ts,mts,cts,js,mjs,cjs,snap}"
|
|
242
|
+
matches:
|
|
243
|
+
- 'db\.[A-Za-z_][\w]*\.(?:createIndex|dropIndex|drop)\('
|
|
244
|
+
- id: mongo-time-series-update
|
|
245
|
+
summary: |
|
|
246
|
+
`db update` on a database with a MongoDB time-series collection no longer fails with "Cannot change immutable collection option 'timeseries'" when nothing changed.
|
|
247
|
+
- id: storage-question-wording
|
|
248
|
+
summary: |
|
|
249
|
+
A data-loss question about data named by its storage name, because the origin contract is unknown, ends with `--delete loses that data` instead of `--delete loses its rows`.
|
|
250
|
+
detection:
|
|
251
|
+
glob: "**/*.{ts,mts,cts,js,mjs,cjs}"
|
|
252
|
+
matches:
|
|
253
|
+
- '--delete loses its rows'
|
|
254
|
+
- id: migration-ts-finds-its-config
|
|
255
|
+
summary: |
|
|
256
|
+
Running a `migration.ts` with node and no `--config` uses the config that owns the migration, whose migrations directory holds the file: the nearest `prisma.config.ts` above the migration's directory, else the one in the working directory. If neither owns it, it fails with `CLI.MIGRATION_CONFIG_NOT_FOUND`. It used to load only the working directory's config, whichever project it belonged to.
|
|
257
|
+
- id: contract-artifacts-restamp
|
|
258
|
+
summary: |
|
|
259
|
+
An extension that writes its own package version into the contracts it emits, such as the
|
|
260
|
+
Supabase extension, now writes 8.0.0-rc.18. Run `contract emit` once after upgrading so the
|
|
261
|
+
emitted `contract.json` and `contract.d.ts` match the installed extension.
|
|
262
|
+
detection:
|
|
263
|
+
glob: "**/contract.json"
|
|
264
|
+
contains:
|
|
265
|
+
- '"version": "8.0.0-rc.17"'
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
# 8.0.0-rc.17 → 8.0.0-rc.18 — User upgrade instructions
|
|
269
|
+
|
|
270
|
+
## Top-level declarations belong to the default namespace
|
|
271
|
+
|
|
272
|
+
A declaration written at the top level of a schema is a member of the target's default namespace: `public` on Postgres, the late-bound namespace (`unbound`, contract id `__unbound__`) on SQLite and MongoDB. Emitted contracts do not change.
|
|
273
|
+
|
|
274
|
+
### `psl-default-namespace-duplicate-declaration`
|
|
275
|
+
|
|
276
|
+
```prisma
|
|
277
|
+
model Account {
|
|
278
|
+
id Int @id
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
namespace public {
|
|
282
|
+
model Account {
|
|
283
|
+
id Int @id
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
This is `PSL_DUPLICATE_DECLARATION: Duplicate declaration of "Account"` on the second declaration. Remove one or rename it. The same holds for a member of `namespace public { … }` named like a `types { }` binding, like another namespace, or `public`.
|
|
289
|
+
|
|
290
|
+
### `psl-default-namespace-name-is-reserved`
|
|
291
|
+
|
|
292
|
+
A top-level declaration named `public` (Postgres), or `unbound` or `__unbound__` (SQLite, MongoDB), is rejected, even when the schema has the name once, because the default namespace already has it:
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
PSL_DUPLICATE_DECLARATION: Duplicate declaration of "public"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
On Postgres, `unbound` and `__unbound__` are rejected only when the schema also has a `namespace unbound { … }` or `namespace __unbound__ { … }` block. Rename the declaration; on a model, `@@map("public")` keeps the table or collection name. A schema read with `prisma7Schema(...)` or `prisma6Schema(...)` is the file the Prisma 7 or Prisma 6 client reads, so the rename applies there too.
|
|
299
|
+
|
|
300
|
+
## Raw SQL in PSL is a `sql` literal
|
|
301
|
+
|
|
302
|
+
### Every place that holds raw SQL takes a `sql` literal
|
|
303
|
+
|
|
304
|
+
Raw SQL in a PSL schema is written as a `sql` literal, the form `@default` already uses. A quoted string is refused in these places:
|
|
305
|
+
|
|
306
|
+
| Place | Before | After |
|
|
307
|
+
| --- | --- | --- |
|
|
308
|
+
| `@@index(where:)` | `@@index([email], where: "(archived_at IS NULL)", name: "users_email_active")` | ``@@index([email], where: sql`(archived_at IS NULL)`, name: "users_email_active")`` |
|
|
309
|
+
| `@@index(expression:)` | `@@index(expression: "lower(email)", name: "users_email_lower")` | ``@@index(expression: sql`lower(email)`, name: "users_email_lower")`` |
|
|
310
|
+
| `@@fullTextIndex(where:)` | `@@fullTextIndex([text], where: "archived_at IS NULL", name: "message_text_search_live")` | ``@@fullTextIndex([text], where: sql`archived_at IS NULL`, name: "message_text_search_live")`` |
|
|
311
|
+
| `@@check(expression:)` | `@@check(expression: "total > 0", name: "order_total_positive")` | ``@@check(expression: sql`total > 0`, name: "order_total_positive")`` |
|
|
312
|
+
| a policy's `using` | `using = "\"userId\"::uuid = auth.uid()"` | ``using = sql`"userId"::uuid = auth.uid()` `` |
|
|
313
|
+
| a policy's `withCheck` | `withCheck = "\"userId\"::uuid = auth.uid()"` | ``withCheck = sql`"userId"::uuid = auth.uid()` `` |
|
|
314
|
+
|
|
315
|
+
Inside a `sql` literal no quote is escaped: `\"` becomes `"`. A literal may span lines.
|
|
316
|
+
|
|
317
|
+
Run the script from the project root to rewrite every `.prisma` file:
|
|
318
|
+
|
|
319
|
+
```sh
|
|
320
|
+
node <skill>/upgrading/app/upgrades/8.0.0-rc.17-to-8.0.0-rc.18/scripts/sql-expression-literals-psl/rewrite-sql-strings.mjs '**/*.prisma'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
It finds each quoted string in those places, outside `//` and `///` comments, decodes its escapes, and writes the same text as a `sql` literal. A text that holds a backtick is written in the double-quote form, `sql"..."`. A text that spans lines is written with the text on its own lines. The script prints each changed file and its number of rewrites, and running it twice changes nothing. Check the diff by hand.
|
|
324
|
+
|
|
325
|
+
A place the script did not rewrite, for example a string the script cannot read, is refused when the schema is emitted:
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
PSL_VALUE_TYPE_INCOMPATIBLE: Expected sql`...`; write sql`(archived_at IS NULL)`
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The message ends with the literal to write. When the string's text would read back from a `sql` literal as different text (it has indentation shared by every line, blank lines at the start or end, a whitespace-only line, or a carriage return), the message is ``Expected sql`...` `` alone; write the SQL in a `sql` literal, and see the next section. A number, `true` or `false` is refused the same way, an identifier is `PSL_INVALID_ATTRIBUTE_SYNTAX` (``Expected sql`...`; got an identifier``), and `pg.sql` is `PSL_UNKNOWN_LITERAL_TAG`.
|
|
332
|
+
|
|
333
|
+
`contract infer` now writes these places as `sql` literals. When an index's SQL would read back from a `sql` literal as different text and the index has a Prisma-generated name, `contract infer` writes the canonical text, which keeps the same name. It skips an index, check or policy named with `map:` whose SQL would read back as different text, and writes a comment in its place, such as `// prisma: skipped index "users_email_active": its SQL cannot be written as a sql literal that reads back unchanged. It is not in this schema, so migration plan will drop it. A sql literal written by hand holds different text, so migration plan then stops with a conflict for an index or check, or drops and recreates a policy. Either change the SQL in the database to the text of the literal, or add the object without map: or @@map so Prisma names it.` Such an object is still in the database. If you run `migration plan` without adding it to the schema, the plan drops it; for a policy, that removes a row-level security rule. Adding it by hand as written does not fix this: a `sql` literal holds canonical text, which is not the text in the database, so `migration plan` stops with a conflict for an index or check, and drops and recreates a policy. Do one of these instead:
|
|
334
|
+
|
|
335
|
+
- Change the SQL in the database to the canonical text, for example with a migration written with `migration new`, then add the object with that text.
|
|
336
|
+
- Add the object without `map:` (for a policy, without `@@map`) so Prisma names it. `migration plan` then drops the old object and creates it again under its new name, once.
|
|
337
|
+
|
|
338
|
+
`contract print` refuses such an object with `CONTRACT.PRINT_UNSUPPORTED`; write its SQL in the canonical form in the contract's source.
|
|
339
|
+
|
|
340
|
+
This supersedes the PSL `@@index(expression: "to_tsvector(…)", type: "gin", …)` example of the `postgres-full-text-search` app instructions in the upgrade from 8.0.0-rc.11 to 8.0.0-rc.12: write it as ``@@index(expression: sql`to_tsvector(…)`, type: "gin", …)``.
|
|
341
|
+
|
|
342
|
+
### `psl-storage-hash-may-change-once`
|
|
343
|
+
|
|
344
|
+
A `sql` literal stores its canonical text: indentation shared by every line, blank lines at the start and end, whitespace-only lines and carriage returns are removed. Every blank line at the start and end is now removed, not only the first and the last, so a `sql` literal in PSL or a `sql` template in TypeScript that starts or ends with two or more blank lines also stores a different text once. A quoted string whose text had any of these is stored differently once it is rewritten, so the contract's storage hash changes once. Prisma-generated index, check and policy names do not change, because their hash is computed from text with its whitespace collapsed.
|
|
345
|
+
|
|
346
|
+
After emitting the rewritten schema, run `prisma migration plan` once and commit the migration it writes. For objects with Prisma-generated names that migration has no operations; it records the new storage hash. For an index or check named with `map:` whose text changed, `migration plan` stops with a conflict for that object and says to write a custom migration with `migration new`. The database needs no change, so write that migration with no operations. For a policy named with `@@map` whose text changed, `migration plan` writes a migration that drops the policy and creates it again with the canonical text. A schema whose texts had none of these forms emits the same contract as before, and `migration plan` reports no change.
|
|
347
|
+
|
|
348
|
+
## Raw SQL in a TypeScript contract is a `sql` value
|
|
349
|
+
|
|
350
|
+
### `builder-raw-sql-is-a-sql-value`
|
|
351
|
+
|
|
352
|
+
Every field of the TypeScript contract builder that holds raw SQL takes a `sql` value, as `.default()` already did. Wrap each string in the `sql` tag, imported from the same contract-builder module as the other helpers:
|
|
353
|
+
|
|
354
|
+
| Place | Before | After |
|
|
355
|
+
| --- | --- | --- |
|
|
356
|
+
| `index` `where` | `constraints.index([cols.email], { where: '(archived_at IS NULL)', name: 'users_email_active' })` | ``constraints.index([cols.email], { where: sql`(archived_at IS NULL)`, name: 'users_email_active' })`` |
|
|
357
|
+
| `index` `expression` | `constraints.index({ expression: 'lower(email)', name: 'users_email_lower' })` | ``constraints.index({ expression: sql`lower(email)`, name: 'users_email_lower' })`` |
|
|
358
|
+
| `fullTextIndex` `where` | `fullTextIndex(cols.text, { where: 'archived_at IS NULL', name: 'message_text_search_live' })` | ``fullTextIndex(cols.text, { where: sql`archived_at IS NULL`, name: 'message_text_search_live' })`` |
|
|
359
|
+
| `check` `expression` | `check({ expression: 'total > 0', name: 'order_total_positive' })` | ``check({ expression: sql`total > 0`, name: 'order_total_positive' })`` |
|
|
360
|
+
| a policy's `using` and `withCheck` | `policySelect(Profile, { name: 'profile_owner_read', roles: [authenticated], using: '"userId"::uuid = auth.uid()' })` | ``policySelect(Profile, { name: 'profile_owner_read', roles: [authenticated], using: sql`"userId"::uuid = auth.uid()` })`` |
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { check, policySelect, sql } from '@prisma/orm-postgres/contract-builder';
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Copy the string's value, not its source: undo the TypeScript string's own escaping first, so `'"userId"'` and `"\"userId\""` both become `` sql`"userId"` ``. Inside the template, write each backtick as `` \` ``, each `${` as `\${`, and a backslash that precedes a backtick, a dollar sign or another backslash as `\\`; every other backslash is kept as written, so `E'\n'` stays `E'\n'`. An index or check object written by hand in `.sql({ indexes, checks })` takes a `sql` value in the same fields. A shared predicate held in a `const` becomes a `sql` value too: `` const owner = sql`"userId"::uuid = auth.uid()`; ``.
|
|
367
|
+
|
|
368
|
+
Skip files under a `migrations/` folder and leave them as they are: migration functions take a string or a `sql` value, and a string keeps its exact text, while wrapping it in `sql` canonicalizes it, which can change the SQL that migration runs.
|
|
369
|
+
|
|
370
|
+
The string form is a type error. JavaScript that is not type-checked and passes a string fails when the contract is built, with `CONTRACT.ARGUMENT_INVALID`, for example ``Index "users_email_active" where must be a sql`...` value.`` The message names the index, check or policy; an index or check with no `name` or `map` is named by its model, as in `Index on "User" where`. `.default('draft')` is not raw SQL: a string there is still a literal default. The `{ fields, render }` form of an index expression still returns a string from `render`, because that text is generated by code; lowering canonicalizes that text as it canonicalizes a `sql` value.
|
|
371
|
+
|
|
372
|
+
This supersedes the TypeScript `fullTextIndex(cols.text, { where: 'archived_at IS NULL', … })` example of the `postgres-full-text-search` app instructions in the upgrade from 8.0.0-rc.11 to 8.0.0-rc.12: write it as ``fullTextIndex(cols.text, { where: sql`archived_at IS NULL`, … })``.
|
|
373
|
+
|
|
374
|
+
### `sql-tag-interpolates-sql-values`
|
|
375
|
+
|
|
376
|
+
The `sql` tag accepts other `sql` values inside `${…}`. It joins their text into the template and canonicalizes the whole text once. A predicate that was built by joining strings is written as one `sql` template:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const owner = sql`owner_id = current_setting('app.uid')::int`;
|
|
380
|
+
const ownerAlive = sql`${owner} AND deleted_at IS NULL`;
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Anything else inside `${…}`, such as a string or a number, is a type error, and throws `CONTRACT.SQL_EXPRESSION_INTERPOLATION` from JavaScript that is not type-checked. Prisma never quotes a value into SQL, so write any other text inside the template. Each line of an interpolated value after its first takes the indentation of the template line the `${…}` sits on, so a multi-line value inside an indented template stores the same text as the same SQL written out in one template.
|
|
384
|
+
|
|
385
|
+
### `sql-tag-error-codes-renamed`
|
|
386
|
+
|
|
387
|
+
- `CONTRACT.DEFAULT_SQL_INTERPOLATION` is now `CONTRACT.SQL_EXPRESSION_INTERPOLATION`, and its message is ``sql`...` only interpolates other sql`...` values; write any other text inside the template.``
|
|
388
|
+
- A `sql` value whose text holds a NUL character or is larger than 65536 bytes throws `CONTRACT.SQL_EXPRESSION_INVALID` when it is made. It was `CONTRACT.DEFAULT_INVALID`.
|
|
389
|
+
- `` sql`now()` ``, `` sql`autoincrement()` `` and unsafe SQL (`;`, a comment token, `$$` or `SELECT`) are no longer refused by the tag. `.default()` refuses them with `CONTRACT.DEFAULT_INVALID` and the same messages, so `` const current = sql`now()` `` compiles and runs, and `.default(current)` throws. In every other place such text is passed to the database unchanged.
|
|
390
|
+
|
|
391
|
+
Update code that matches on these codes. Code that catches `CONTRACT.DEFAULT_INVALID` around a `sql` template that is not passed to `.default()` no longer sees it.
|
|
392
|
+
|
|
393
|
+
### `ts-storage-hash-may-change-once`
|
|
394
|
+
|
|
395
|
+
A `sql` value stores its canonical text, as a PSL `sql` literal does: indentation shared by every line, blank lines at the start and end, whitespace-only lines and carriage returns are removed. A raw SQL string whose text had any of these, for example a multi-line template string, is stored differently once it is written as a `sql` value, so the contract's storage hash changes once. Prisma-generated index, check and policy names do not change. Skip files under a `migrations/` folder and leave them as they are: migration functions take a string or a `sql` value, and a string keeps its exact text, while wrapping it in `sql` canonicalizes it, which can change the SQL that migration runs. Follow the steps of `psl-storage-hash-may-change-once` in this guide: run `prisma migration plan` once and commit the migration it writes. For an index or check named with `map:` whose text changed, `migration plan` stops with a conflict and asks for a migration written with `migration new`, which needs no operations. The TypeScript policy helpers take no exact name, so a policy never needs this. A `{ fields, render }` index expression whose `render` returns text that is not canonical, such as indented multi-line text, also stores different text once, with the same steps; `fullTextIndex` stores its fields and language as data, renders no SQL, and is not affected.
|
|
396
|
+
|
|
397
|
+
### `infer-notes-defaults-that-do-not-read-back`
|
|
398
|
+
|
|
399
|
+
A column default whose text a `sql` literal cannot write back unchanged, for example a string constant holding a carriage return or a whitespace-only line, is treated differently by the two commands that write PSL, for the reason ADR 268 gives ("Column defaults that do not read back"):
|
|
400
|
+
|
|
401
|
+
- `contract infer` still writes the default as a `sql` literal and adds the comment `// prisma: default of "<column>" holds text a sql literal cannot write back unchanged; check its string constants before applying a migration` to the model. The literal reads back as a different constant, so a database created from the inferred schema gets that constant. Check the constant, and fix it in the schema or in the database before you apply a migration planned from it.
|
|
402
|
+
- `contract print` refuses the default with `CONTRACT.PRINT_UNSUPPORTED` (``default of column "public"."post"."slug" holds SQL that a sql literal cannot write back unchanged``). Write the default in its canonical form in the contract's source. A contract holds such a default only when it was built before this release, or through `.default({ kind: 'function', expression })` or `.defaultSql('...')`.
|
|
403
|
+
|
|
404
|
+
This applies to projects that write only PSL as well as TypeScript projects.
|
|
405
|
+
|
|
406
|
+
## A full-text index is stored as data
|
|
407
|
+
|
|
408
|
+
### `re-emit-contracts-with-a-full-text-index`
|
|
409
|
+
|
|
410
|
+
`@@fullTextIndex` and the TypeScript `fullTextIndex` helper used to store a SQL expression in the contract. They now store the index's definition as an index of type `fullText`, and the SQL is rendered from it when the index is created and when a query searches it. The database index is still a `gin` index over the same expression. For `@@fullTextIndex([title], name: "post_title_search")` the emitted index changes like this:
|
|
411
|
+
|
|
412
|
+
```diff
|
|
413
|
+
{
|
|
414
|
+
- "expression": "to_tsvector('english', \"title\")",
|
|
415
|
+
- "name": "post_title_search_724b05e5",
|
|
416
|
+
+ "columns": ["title"],
|
|
417
|
+
+ "name": "post_title_search_e0dd1131",
|
|
418
|
+
+ "options": { "language": "english", "weightGroups": [["title"]] },
|
|
419
|
+
"prefix": "post_title_search",
|
|
420
|
+
- "type": "gin",
|
|
421
|
+
+ "type": "fullText",
|
|
422
|
+
"unique": false
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The schema source does not change. The index's name ends in a hash of its contract entry, so the name changes, and the database needs one rename. Do this:
|
|
427
|
+
|
|
428
|
+
1. Run `prisma contract emit` and commit the regenerated `contract.json` and `contract.d.ts`. The storage hash changes. Do not hand-edit the generated files.
|
|
429
|
+
2. Run `prisma migration plan` and apply the migration as usual. Planned from the previous contract, it renames each full-text index: `ALTER INDEX "post_title_search_724b05e5" RENAME TO "post_title_search_e0dd1131"`. A rename is instant and keeps the index.
|
|
430
|
+
|
|
431
|
+
Do not use `prisma db update` for this step if a full-text index is large. `db update` compares the contract with the live database, where Postgres prints the expression in its own form, so it cannot tell that the old index is the new one under another name. It drops the index and builds it again. For the index above it plans:
|
|
432
|
+
|
|
433
|
+
```
|
|
434
|
+
├─ ⚠ Drop index "post_title_search_724b05e5"
|
|
435
|
+
└─ Create index "post_title_search_e0dd1131" on "post"
|
|
436
|
+
|
|
437
|
+
DROP INDEX "public"."post_title_search_724b05e5";
|
|
438
|
+
CREATE INDEX "post_title_search_e0dd1131" ON "public"."post" USING "gin" (to_tsvector('english', "title"));
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Building a GIN index reads the whole table. If you must use `db update`, rename each index by hand first, with the old name from the database and the new one from the re-emitted contract (`ALTER INDEX "post_title_search_724b05e5" RENAME TO "post_title_search_e0dd1131"`). `db update` then has nothing to do for it.
|
|
442
|
+
|
|
443
|
+
Until the rename, `prisma db verify` reports the new index name as missing. With `--strict` it also reports the old name as an extra index.
|
|
444
|
+
|
|
445
|
+
If code reads a full-text index's `expression` from the contract, recognise the index by `type: "fullText"` and read `options.weightGroups` and `options.language` instead. `weightGroups` lists the weight groups, each a list of storage column names, and `columns` is the same columns in the same order. The search document is no longer stored anywhere in the contract.
|
|
446
|
+
|
|
447
|
+
An index declared with `map:` keeps its exact name, so neither the name nor the database changes. Only the storage hash moves. `db verify` compares a `map:` index's expression with the text Postgres prints for it, character for character, so it reports a `map:` full-text index as changed, as it did before.
|
|
448
|
+
|
|
449
|
+
A full-text index can now cover several fields, each top-level item a weight group, strongest first. This is optional:
|
|
450
|
+
|
|
451
|
+
```prisma
|
|
452
|
+
@@fullTextIndex([[title, subtitle], body], name: "post_search")
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
In an index of several fields, every column is wrapped in `coalesce(column, '')`, so the index does not change when a column becomes optional or required. To search it, pass the index from the table's `indexes` to `fns.fullTextMatches` and `fns.fullTextRank` in the SQL builder, which then search its weight groups in its language: `fns.fullTextMatches(post.indexes.post_search, q)`. To search several columns without an index, wrap the groups in `fullTextDocument` from `@prisma/orm-postgres/target/full-text`.
|
|
456
|
+
|
|
457
|
+
### `full-text-index-one-field-diagnostic-removed`
|
|
458
|
+
|
|
459
|
+
`@@fullTextIndex([a, b])` was refused with `PSL_FULL_TEXT_INDEX_ONE_FIELD`. It is now a valid index with two weight groups, and that code is never raised. Remove assertions on it. The new refusals are `PSL_FULL_TEXT_INDEX_TOO_MANY_GROUPS` (more than four groups), `PSL_FULL_TEXT_INDEX_EMPTY_GROUP` (an empty group) and `PSL_FULL_TEXT_INDEX_DUPLICATE_FIELD` (a field named twice). `PSL_FULL_TEXT_INDEX_TEXT_FIELD`, `PSL_FULL_TEXT_INDEX_REQUIRES_NAME` and `PSL_FULL_TEXT_INDEX_NAME_XOR_MAP` are unchanged.
|
|
460
|
+
|
|
461
|
+
## A base model lists its variants by discriminator value
|
|
462
|
+
|
|
463
|
+
### `re-emit-contracts-with-polymorphic-models`
|
|
464
|
+
|
|
465
|
+
For `@@base(Task, "bug")` and `@@base(Task, "feature")` the base model's entry changes like this:
|
|
466
|
+
|
|
467
|
+
```diff
|
|
468
|
+
"Task": {
|
|
469
|
+
"discriminator": { "field": "type" },
|
|
470
|
+
"variants": {
|
|
471
|
+
- "Bug": { "value": "bug" },
|
|
472
|
+
- "Feature": { "value": "feature" }
|
|
473
|
+
+ "bug": { "model": "Bug", "namespace": "public" },
|
|
474
|
+
+ "feature": { "model": "Feature", "namespace": "public" }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The schema source does not change, and neither does a variant model's `base`. The storage hash does not change, so no migration is needed and the database marker stays valid.
|
|
480
|
+
|
|
481
|
+
1. Run `prisma contract emit` and commit the regenerated `contract.json` and `contract.d.ts`.
|
|
482
|
+
2. Rewrite each contract snapshot under `migrations/snapshots/<hash>/` whose `contract.json` matches `detection`. No command regenerates a snapshot, and a snapshot in the previous form stops `prisma migration plan` when it is the plan's origin. The directory names stay as they are.
|
|
483
|
+
|
|
484
|
+
In a snapshot's `contract.json`, for every model that has `variants`:
|
|
485
|
+
|
|
486
|
+
- replace each entry `"<Model>": { "value": "<value>" }` with `"<value>": { "model": "<Model>", "namespace": "<namespace>" }`, where `<namespace>` is the key under `domain.namespaces` that holds the base model;
|
|
487
|
+
- order the entries by key;
|
|
488
|
+
- keep the file on one line with no added whitespace.
|
|
489
|
+
|
|
490
|
+
In the snapshot's `contract.d.ts`, make the same change to the type of `variants`:
|
|
491
|
+
|
|
492
|
+
```diff
|
|
493
|
+
readonly variants: {
|
|
494
|
+
- readonly Bug: { readonly value: 'bug' };
|
|
495
|
+
- readonly Feature: { readonly value: 'feature' };
|
|
496
|
+
+ readonly bug: { readonly namespace: 'public' & NamespaceId; readonly model: 'Bug' };
|
|
497
|
+
+ readonly feature: { readonly namespace: 'public' & NamespaceId; readonly model: 'Feature' };
|
|
498
|
+
};
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Quote a key that is not an identifier, such as `'view-product'`. Change nothing else in either file.
|
|
502
|
+
|
|
503
|
+
A MongoDB contract has one namespace, `__unbound__`; use it as `<namespace>`.
|
|
504
|
+
|
|
505
|
+
### `read-variants-by-discriminator-value`
|
|
506
|
+
|
|
507
|
+
This affects only code that reads `variants` from a contract object or from `contract.json`. The `variants` option of the MongoDB TypeScript contract builder's `discriminator` is unchanged: it still lists `<Model>: { value }`.
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
// before
|
|
511
|
+
const value = models.Task.variants.Bug.value;
|
|
512
|
+
const variantNames = Object.keys(models.Task.variants);
|
|
513
|
+
|
|
514
|
+
// after
|
|
515
|
+
const model = models.Task.variants.bug.model;
|
|
516
|
+
const values = Object.keys(models.Task.variants);
|
|
517
|
+
const variantNames = Object.values(models.Task.variants).map((variant) => variant.model);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
To find the value of a known variant model, search the entries for the one whose `model` and `namespace` are that model's.
|
|
521
|
+
|
|
522
|
+
## A Prisma 7 schema's `@ignore` and `@@ignore` objects stay in the contract
|
|
523
|
+
|
|
524
|
+
The contract read from a Prisma 7 schema now describes every table and column Prisma 7 created, including those of `@ignore` fields and `@@ignore` models. The ORM still leaves them out of its models. The contract's storage hash therefore changes for a schema that uses `@ignore` or `@@ignore`, and no existing database's signature matches it any more.
|
|
525
|
+
|
|
526
|
+
### A schema that loaded before can be refused
|
|
527
|
+
|
|
528
|
+
An ignored field or model now becomes part of the contract, so it meets every rule the reader applies, not only to the fields Prisma 8 maps. `prisma contract emit` can refuse a schema that loaded before only because its ignored objects were dropped. Each diagnostic says what to change in the Prisma 7 schema. Common examples, not every case:
|
|
529
|
+
|
|
530
|
+
- `PSL.PRISMA7_NATIVE_TYPE_UNSUPPORTED`: a `@db.*` column type with no Prisma 8 codec, on an `@ignore` field or in an `@@ignore` model. `PSL.PRISMA7_UNSUPPORTED_TYPE`: an `Unsupported("...")` field in an `@@ignore` model. `@ignore` and `@@ignore` no longer work around a missing codec; the schema cannot be read until Prisma 8 supports the type.
|
|
531
|
+
- `PSL.PRISMA7_INDEX_ARGUMENT_UNSUPPORTED`: an index on an `@@ignore` model with `sort`, `length` or `ops` arguments, such as the `sort: Desc` that `prisma db pull` writes.
|
|
532
|
+
- `PSL.PRISMA7_REFERENTIAL_ACTION_UNSUPPORTED`: an `@ignore` relation with `onDelete: SetNull` or `SetDefault` over a required field that cannot take it.
|
|
533
|
+
- `PSL.PRISMA7_JSON_NULL_DEFAULT_UNSUPPORTED`: an ignored `Json` field with `@default("null")`.
|
|
534
|
+
|
|
535
|
+
### While Prisma 7 still applies the migrations
|
|
536
|
+
|
|
537
|
+
If you have no `migrations/app` folder of Prisma 8 migrations yet, emit the contract and sign each existing database again:
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
prisma contract emit
|
|
541
|
+
prisma db sign
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### After Prisma 8 took over the migrations
|
|
545
|
+
|
|
546
|
+
Emit the contract, then plan a migration before anything else. The migration creates the ignored objects, which a database rebuilt from `migrations/` needs:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
prisma contract emit
|
|
550
|
+
prisma migration plan --name keep-ignored-objects
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Then bring each existing database (production, staging, every developer's) to the new hash. Either sign it:
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
prisma db sign
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
or run your usual deploy step with the `db` ref:
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
prisma db migrate --advance-ref db
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
`db migrate` skips each create because the object already exists. Plain `prisma db migrate` also brings the database to the new hash, but leaves the `db` ref behind, so the next `migration plan` would plan the same migration again; run `prisma db sign` afterwards to move the ref.
|
|
566
|
+
|
|
567
|
+
If you signed before planning, the `db` ref points at a hash that is not in the migration graph, and `prisma migration plan` fails with `MIGRATION.HASH_NOT_IN_GRAPH`. Plan from the hash the database had before, which `prisma migration list` shows as the last migration's destination:
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
prisma migration plan --from <previous hash> --name keep-ignored-objects
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## `mongo-enum-member-written-as-stored`
|
|
574
|
+
|
|
575
|
+
`contract.json` holds each enum member in the form its codec stores, and `db.enums` reads it back through the codec. Both must give the member as written, so a Mongo contract now refuses a member whose stored form the codec reads back as another value, and says what to write:
|
|
576
|
+
|
|
577
|
+
```text
|
|
578
|
+
CONTRACT.ENUM_INVALID: enumType("Volume"): member "Quiet" is written "quiet", but the field stores "QUIET". Write the member as "QUIET".
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
In a PSL or Prisma 6 schema the same member is reported at the member:
|
|
582
|
+
|
|
583
|
+
```text
|
|
584
|
+
enum "Volume" member "Quiet" is written "quiet", but its codec stores "QUIET". Write the member as "QUIET".
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
In TypeScript, a codec that throws on a member is now reported as `CONTRACT.ENUM_INVALID` naming the member, with the codec's error as the cause.
|
|
588
|
+
|
|
589
|
+
1. Run `prisma contract emit`. Each refused member is reported with the value to write.
|
|
590
|
+
2. If the codec is yours and should store the value as written, fix its `encodeJson` or `decodeJson` so `decodeJson(encodeJson(value))` returns `value`. Otherwise write each refused member as the message says.
|
|
591
|
+
3. Re-emit.
|
|
592
|
+
|
|
593
|
+
## A TypeScript contract's namespaces and rows have precise types
|
|
594
|
+
|
|
595
|
+
### Namespace keys equal the runtime keys
|
|
596
|
+
|
|
597
|
+
A contract built with `defineContract` from TypeScript typed `contract.domain.namespaces` as a record with any string key, and every key listed every model. The types now have exactly the keys the built contract has at runtime:
|
|
598
|
+
|
|
599
|
+
- `contract.domain.namespaces` and `db.orm` have a key for each namespace that holds a model. They also have the default namespace (`public` on Postgres, `__unbound__` on SQLite) when the contract has no models or declares enums. Each namespace lists only its own models.
|
|
600
|
+
- `contract.storage.namespaces` has the default namespace, each namespace listed in `namespaces`, and each model's namespace. Each lists only the tables of its own models.
|
|
601
|
+
|
|
602
|
+
A Postgres contract whose models all have `namespace: 'auth'` has no `public` domain namespace, so `db.orm.public` no longer typechecks. At runtime it was always `undefined`. Code that reads a namespace the contract does not have now fails to typecheck. Use the namespace the model is in:
|
|
603
|
+
|
|
604
|
+
```diff
|
|
605
|
+
// A Postgres contract built with defineContract, with User in the default namespace
|
|
606
|
+
- type User = (typeof contract.domain.namespaces)['__unbound__']['models']['User'];
|
|
607
|
+
+ type User = (typeof contract.domain.namespaces)['public']['models']['User'];
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Indexing `domain.namespaces` with a `string` variable no longer typechecks, because the type has no index signature. Narrow the key to the contract's keys first:
|
|
611
|
+
|
|
612
|
+
```diff
|
|
613
|
+
- const namespace = contract.domain.namespaces[name];
|
|
614
|
+
+ const namespace = Object.hasOwn(contract.domain.namespaces, name)
|
|
615
|
+
+ ? contract.domain.namespaces[name as keyof typeof contract.domain.namespaces]
|
|
616
|
+
+ : undefined;
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
A model is placed in its namespace only when `namespace` is a string literal. When any model's `namespace` comes from a `string` variable, the contract's namespace types fall back to a `string` index, and every namespace lists every model of the contract, as before this change. Access a namespace with brackets and check it for `undefined`, for example `db.orm['billing']?.Invoice`.
|
|
620
|
+
|
|
621
|
+
On SQLite, `defineContract` no longer accepts `namespaces`. The build always rejected it at runtime; the type now rejects it too. Remove the option.
|
|
622
|
+
|
|
623
|
+
### Rows have their codec output types
|
|
624
|
+
|
|
625
|
+
The ORM client of such a contract now types each row field as its codec's output type, narrowed to the enum's values for an enum field. Before, every field was `unknown`. This also applies to `field.column({ codecId, nativeType })` with an inline codec id, which no longer needs `as const`.
|
|
626
|
+
|
|
627
|
+
A cast that agrees with the codec type still typechecks and can be removed. A cast that disagrees with the codec type now fails with TS2352, and shows a real mismatch: fix the code that expected the other type.
|
|
628
|
+
|
|
629
|
+
```diff
|
|
630
|
+
const row = await db.orm.Sample.first();
|
|
631
|
+
- const when = row.when as string;
|
|
632
|
+
+ if (row === null) throw new Error('No sample');
|
|
633
|
+
+ const when = row.when.toISOString();
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
`first()` returns `null` when no row matches, which its type now says, so code that read a field of the result without checking for `null` fails to typecheck too.
|
|
637
|
+
|
|
638
|
+
Rows of an included relation are still typed `unknown`; casts on those still typecheck.
|
|
639
|
+
|
|
640
|
+
## A model with no fields has no keys
|
|
641
|
+
|
|
642
|
+
Re-emit the contract. If code reads a property of a row of a model with no fields, it now fails to compile, because the model has no field of that name. With the SQL ORM client, passing a key in the create or update input of such a model fails to compile too. Remove the read or the key, or add a field to the model that maps the column.
|
|
643
|
+
|
|
644
|
+
## Generic update data uses the update input type
|
|
645
|
+
|
|
646
|
+
The update methods of the SQL and Mongo ORM clients now take the update input type of the model, which rejects every key for a model with no fields. While the model name is a type parameter, TypeScript cannot tell that `Partial<DefaultModelRow<Contract, M>>` is that type. Use the update input type instead.
|
|
647
|
+
|
|
648
|
+
SQL ORM client:
|
|
649
|
+
|
|
650
|
+
```diff
|
|
651
|
+
- import type { DefaultModelRow } from '@prisma/orm-postgres/orm-client';
|
|
652
|
+
- function touchAll<M extends 'User' | 'Post'>(rows: Collection<Contract, M>, data: Partial<DefaultModelRow<Contract, M>>) {
|
|
653
|
+
+ import type { ScalarUpdateInput } from '@prisma/orm-postgres/orm-client';
|
|
654
|
+
+ function touchAll<M extends 'User' | 'Post'>(rows: Collection<Contract, M>, data: ScalarUpdateInput<Contract, M>) {
|
|
655
|
+
return rows.where({}).updateAndCount(data);
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Mongo ORM: replace `Partial<DefaultModelRow<Contract, M>>` with `UpdateInput<Contract, M>`, imported from `@prisma/orm-mongo/orm`, the same entry point as `DefaultModelRow`.
|
|
660
|
+
|
|
661
|
+
Where the model name is a concrete name, such as `Partial<DefaultModelRow<Contract, 'User'>>`, nothing changes.
|
|
662
|
+
|
|
663
|
+
## The SQL ORM client accepts only field names
|
|
664
|
+
|
|
665
|
+
Every name passed to the SQL ORM client must be the name of a field of the model, or of the variant the collection is narrowed to. A name that is not a field throws `ORM.FIELD_UNKNOWN` with the message `Model "<model>" has no field "<name>"`. A column name is not a field name: pass the field instead.
|
|
666
|
+
|
|
667
|
+
```diff
|
|
668
|
+
- await db.orm.Post.where({ user_id: 1 }).all();
|
|
669
|
+
+ await db.orm.Post.where({ userId: 1 }).all();
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Code that catches the error a `conflictOn` name that is not a field raises (in a create with `onConflict: 'skip'`) matched `ORM.ARGUMENT_INVALID`. Match `ORM.FIELD_UNKNOWN` instead.
|
|
673
|
+
|
|
674
|
+
```diff
|
|
675
|
+
- if (error.code === 'ORM.ARGUMENT_INVALID') {
|
|
676
|
+
+ if (error.code === 'ORM.FIELD_UNKNOWN') {
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Leave other checks of `ORM.ARGUMENT_INVALID` alone; only the `conflictOn` refusal changed code.
|
|
680
|
+
|
|
681
|
+
## `fragment` is called on the model's root collection
|
|
682
|
+
|
|
683
|
+
A fragment for one model, made with `collection.fragment(body)`, is built from the model alone. Calls chained before `.fragment(...)`, such as `where`, `orderBy`, `include`, `limit` or `forUpdate`, were ignored without a word. Now `fragment` throws `ORM.ARGUMENT_INVALID` on such a collection, and a `where`, an `orderBy` or an `include` before it is also a compile error. A receiver typed by a type parameter, such as `this` in a custom collection class method or `C` in a generic function, is a compile error too, because it may be a chained collection.
|
|
684
|
+
|
|
685
|
+
In your code, chain on the collection directly instead of defining a fragment from it. Inside a class method, chain on `this`:
|
|
686
|
+
|
|
687
|
+
```diff
|
|
688
|
+
titles() {
|
|
689
|
+
- return this.with(this.fragment((posts) => posts.select('id', 'title')));
|
|
690
|
+
+ return this.select('id', 'title');
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
If the fragment is shared between places, define it once as a module-level constant on the model's root collection, and move the chained calls that were meant to be part of it into its body:
|
|
695
|
+
|
|
696
|
+
```diff
|
|
697
|
+
- const recentSummary = db.orm.public.Post.orderBy((p) => p.id.desc()).fragment((posts) =>
|
|
698
|
+
- posts.select('id', 'title'),
|
|
699
|
+
- );
|
|
700
|
+
+ const recentSummary = db.orm.public.Post.fragment((posts) =>
|
|
701
|
+
+ posts.orderBy((p) => p.id.desc()).select('id', 'title'),
|
|
702
|
+
+ );
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
The detection does not find a `.fragment(` call whose receiver is a variable or a parameter holding a chained collection, such as `const published = db.orm.public.Post.where(...)` followed by `published.fragment(...)`, or a generic receiver. Check those `.fragment(` calls by hand.
|
|
706
|
+
|
|
707
|
+
Skip a match on the fragment for any model, `fragment(fields, body)`, called on a client returned by a call, such as `orm(...).fragment(fields, body)`, which did not change, and a match in code that uses the MongoDB ORM client, which did not change either.
|
|
708
|
+
|
|
709
|
+
## `Fragment` is now `QueryFragment`, and `FieldFragment` is now `DeclaredFieldsFragment`
|
|
710
|
+
|
|
711
|
+
A function from a collection to a collection, run with `collection.with(fn)`, is a query fragment, and its type is now named `QueryFragment<In, Out>`. "Fragment" alone also covers a row fragment, a function of the model accessor that `where` and `orderBy` take, and it is the name of React's `Fragment`. What `db.orm.fragment(fields, body)` returns, a fragment declared over the fields it needs, is now typed `DeclaredFieldsFragment`. The types work as before.
|
|
712
|
+
|
|
713
|
+
These changes apply to the SQL ORM client only. The MongoDB ORM client did not change; skip matches in code that uses it.
|
|
714
|
+
|
|
715
|
+
Rename `Fragment` to `QueryFragment` and `FieldFragment` to `DeclaredFieldsFragment`. These are the names 8.0.0-rc.17 exports.
|
|
716
|
+
|
|
717
|
+
```diff
|
|
718
|
+
- import type { FieldFragment, Fragment } from '@prisma/orm-postgres/orm-client';
|
|
719
|
+
+ import type { DeclaredFieldsFragment, QueryFragment } from '@prisma/orm-postgres/orm-client';
|
|
720
|
+
|
|
721
|
+
- const newest: Fragment<PostCollection, Ordered<PostCollection>> = (posts) => posts.newestFirst();
|
|
722
|
+
+ const newest: QueryFragment<PostCollection, Ordered<PostCollection>> = (posts) => posts.newestFirst();
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Code that still uses the 8.0.0-rc.16 names renames them to the new names directly: `Scope` to `QueryFragment`, `FieldScope` to `DeclaredFieldsFragment` and `ScopeFacts` to `FragmentFacts`.
|
|
726
|
+
|
|
727
|
+
Rename every use of these types in the file, including a re-export such as `export type { Fragment } from '@prisma/orm-postgres/orm-client'`, a qualified name such as `Orm.Fragment` after `import * as Orm from '@prisma/orm-postgres/orm-client'`, and an inline type such as `import('@prisma/orm-postgres/orm-client').Fragment`. After renaming, sort the named imports again, since `QueryFragment` sorts in a different place than `Fragment` or `Scope`, and wrap any line the longer names make too long for the formatter.
|
|
728
|
+
|
|
729
|
+
Do not rename React's `Fragment`, the SQL builder's `Scope` and `ScopeField` (imported from a `builder` entry), or any `Fragment` or `Scope` that is not imported from the ORM client. Do not rename `FragmentFacts` or `DeclaredField`, which keep their names.
|
|
730
|
+
|
|
731
|
+
## `unique-criterion-rejects-null`
|
|
732
|
+
|
|
733
|
+
`UniqueConstraintCriterion<TContract, ModelName>`, exported by the SQL ORM client (`@prisma/orm-postgres/orm-client` and the other facades' `orm-client` entries), is the object that names one row by its primary key or by one of its unique constraints. Each of its values is now the field's type without `null`. A `null` value cannot identify one row: a unique constraint allows any number of rows whose column is `NULL`.
|
|
734
|
+
|
|
735
|
+
The type is the argument of three calls, and each of them now refuses `null` and a value whose type includes `null`:
|
|
736
|
+
|
|
737
|
+
- `conflictOn` in `collection.upsert({ create, update, conflictOn })`
|
|
738
|
+
- `mutator.connect(criterion)` and `mutator.connect([criterion, ...])` in a relation callback of `create` or `update`
|
|
739
|
+
- `mutator.disconnect([criterion, ...])` in a relation callback of `update`
|
|
740
|
+
|
|
741
|
+
Only a model with a nullable unique column is affected. Given `Account.handle`, a nullable column with a unique constraint, these no longer compile:
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
await db.orm.Account.upsert({ create, update, conflictOn: { handle: null } });
|
|
745
|
+
await db.orm.Account.upsert({ create, update, conflictOn: { handle: maybeHandle } }); // maybeHandle: string | null
|
|
746
|
+
|
|
747
|
+
await db.orm.Owner.where({ id }).update({
|
|
748
|
+
accounts: (accounts) => accounts.connect({ handle: maybeHandle }),
|
|
749
|
+
});
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
The compiler reports `Type 'null' is not assignable to type 'string'` on the field.
|
|
753
|
+
|
|
754
|
+
The detection also matches code this change does not affect. Leave as they are: a `.connect(` or `.disconnect(` call on another object, such as `postgres.connect({ url })`; and the `conflictOn` of `createAll` and `createAndCount`, which is an array of field names, such as `conflictOn: ['handle']`.
|
|
755
|
+
|
|
756
|
+
What to write instead:
|
|
757
|
+
|
|
758
|
+
- Where the value is known to be present, narrow it before the call, so that its type has no `null`:
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
if (maybeHandle === null) {
|
|
762
|
+
throw new Error('an account needs a handle to be connected');
|
|
763
|
+
}
|
|
764
|
+
await db.orm.Owner.where({ id }).update({
|
|
765
|
+
accounts: (accounts) => accounts.connect({ handle: maybeHandle }),
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
- Where the value can be absent, decide what that case means and write it as its own branch. For `upsert`, create the row with `create(...)` in that branch, or use a constraint whose columns all have values, such as the primary key: `conflictOn: { id }`. For `connect` and `disconnect`, leave the criterion out of the list in that branch, or name the row by another key.
|
|
770
|
+
|
|
771
|
+
- To read or write the rows whose column is `NULL`, use `where({ handle: null })`. It can match more than one row.
|
|
772
|
+
|
|
773
|
+
- Where your own code declares a variable, a parameter or a return value as `UniqueConstraintCriterion<...>`, the same rule applies to the values assigned to it.
|
|
774
|
+
|
|
775
|
+
## `prepared-all-and-aggregate-need-their-receiver`
|
|
776
|
+
|
|
777
|
+
`all` and `aggregate` on a collection's `prepared` object now declare the object they are called on. This applies to every collection, not only one filtered with `whereUnique`. A call without that receiver no longer compiles:
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
const { all, aggregate } = db.orm.Post.prepared;
|
|
781
|
+
all(); // error: The 'this' context of type 'void' is not assignable to method's 'this'
|
|
782
|
+
aggregate((a) => ({ posts: a.count() })); // the same error
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
Call the method on the `prepared` object instead:
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
const prepared = db.orm.Post.prepared;
|
|
789
|
+
prepared.all();
|
|
790
|
+
prepared.aggregate((a) => ({ posts: a.count() }));
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
These still compile and need no change: a destructured `first` or `firstOrThrow`, as in `const { first, firstOrThrow } = db.orm.Post.prepared; first(); firstOrThrow()`, and `prepared.all` passed as a callback without being called. What the methods do at run time is unchanged.
|
|
794
|
+
|
|
795
|
+
## `native-enums-leave-out-domain-enums`
|
|
796
|
+
|
|
797
|
+
`db.nativeEnums.<namespace>` lists the native Postgres enum types of the namespace, keyed as before by the name in the schema. It no longer lists a domain enum, which is an `enum` block in PSL or an `enumType()` in TypeScript. `db.enums.<namespace>` holds each domain enum, with its values decoded as a query returns them.
|
|
798
|
+
|
|
799
|
+
For each `db.nativeEnums.<namespace>.<Name>` in the code, check whether `<Name>` is a native enum (`native_enum <Name>` in PSL, `nativeEnum('<Name>', …)` in TypeScript). If it is a domain enum, read it through `db.enums` instead:
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
// before
|
|
803
|
+
db.nativeEnums.public.Priority.values; // ['1', '10'], the stored text
|
|
804
|
+
// after
|
|
805
|
+
db.enums.public.Priority.values; // [1n, 10n], the values a query returns
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
A domain enum's values in `db.enums` are the values a query returns, which can differ from the stored text that `db.nativeEnums` held: a `pg/int8` member is a `bigint`, a `pg/timestamptz-temporal` member a `Temporal.Instant`. Change code that compared them with stored text. A member's name is its name in the enum, such as `Low`, where `db.nativeEnums` named each member by its value.
|
|
809
|
+
|
|
810
|
+
## Middleware hooks are activity hooks
|
|
811
|
+
|
|
812
|
+
The runtime now runs every statement and every transaction as an **activity**. A middleware observes an activity through three lifecycle events, `activityStarted`, `activityEnded` and `activityFinalized`, and changes or stops a statement through interceptors: `beforeEncode`, `intercept.onQuery`, `intercept.onExecute` and `onRow`. The per-statement hooks are gone.
|
|
813
|
+
|
|
814
|
+
### Rename the hooks
|
|
815
|
+
|
|
816
|
+
| Before | After |
|
|
817
|
+
|---|---|
|
|
818
|
+
| `beforeQuery`, `beforeExecute` | `beforeEncode` |
|
|
819
|
+
| `interceptQuery`, `interceptExecute` | `intercept.onQuery`, `intercept.onExecute` |
|
|
820
|
+
| `onRow` | `onRow` |
|
|
821
|
+
| `afterQuery`, `afterExecute` | `activityEnded` on a statement |
|
|
822
|
+
| `afterTransaction` | `activityFinalized` on a statement |
|
|
823
|
+
| `beforeCompile` | `beforeCompile` |
|
|
824
|
+
|
|
825
|
+
`beforeEncode` keeps the arguments of `beforeQuery` and `beforeExecute`: the lowered plan, the context and the parameter mutator. It runs for both `query()` and `execute()`. A middleware that defined both `beforeQuery` and `beforeExecute` with the same body defines `beforeEncode` once. A middleware that must tell them apart reads `activity.returns` (`'rows'` or `'stats'`) in `activityStarted` and keeps it in `ctx.state`.
|
|
826
|
+
|
|
827
|
+
The interceptors move under one `intercept` object:
|
|
828
|
+
|
|
829
|
+
```diff
|
|
830
|
+
- async interceptQuery(plan, ctx) {
|
|
831
|
+
- return { rows: cached };
|
|
832
|
+
- },
|
|
833
|
+
+ intercept: {
|
|
834
|
+
+ async onQuery(plan, ctx) {
|
|
835
|
+
+ return { rows: cached };
|
|
836
|
+
+ },
|
|
837
|
+
+ },
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### Read the result from `activityEnded`
|
|
841
|
+
|
|
842
|
+
`afterQuery(plan, result, ctx)` and `afterExecute(plan, result, ctx)` become one hook, `activityEnded(activity, result, ctx)`. It fires for transactions too, so check the kind first. `result.completed` is replaced by `result.outcome`, which is `'succeeded'`, `'failed'` or `'stopped'` (the caller stopped reading the rows). `latencyMs` and `source` are unchanged; `rowCount` or `stats` is there according to `result.returns`. The plan the caller sent is `activity.plan`; the lowered SQL is not on the activity, so a middleware that logs it keeps it from `beforeEncode` in `ctx.state`:
|
|
843
|
+
|
|
844
|
+
```diff
|
|
845
|
+
- async afterQuery(plan, result, ctx) {
|
|
846
|
+
- if (result.latencyMs > 250) ctx.log.warn({ sql: plan.sql, rowCount: result.rowCount });
|
|
847
|
+
- },
|
|
848
|
+
+ async beforeEncode(plan, ctx) {
|
|
849
|
+
+ ctx.state(statementSql)['sql'] = plan.sql;
|
|
850
|
+
+ },
|
|
851
|
+
+ async activityEnded(activity, result, ctx) {
|
|
852
|
+
+ if (result.kind !== 'statement' || result.latencyMs <= 250) return;
|
|
853
|
+
+ ctx.log.warn({
|
|
854
|
+
+ sql: ctx.state(statementSql)['sql'],
|
|
855
|
+
+ ...(result.returns === 'rows' ? { rowCount: result.rowCount } : {}),
|
|
856
|
+
+ });
|
|
857
|
+
+ },
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
`statementSql` is a module-level `Symbol('…')`, for example `const statementSql = Symbol('slow-query-warning.sql')`. An error thrown from `activityEnded` after a successful statement still fails it and reaches the caller, as an error from `afterQuery` did; every other middleware still receives `activityEnded`, and the caller receives the first error. A transaction also ends: `succeeded` when its `commit()` or `rollback()` resolved, `failed` when it rejected. So a rolled-back transaction ends `succeeded` and is finalized `rolled-back`. `activityEnded` also fires where `afterQuery` and `afterExecute` did not: for a statement the caller stopped reading, with `result.outcome` `'stopped'`, and for a statement that failed before it ran, in `beforeCompile`, `beforeEncode` or parameter encoding, with `result.outcome` `'failed'`, `latencyMs: 0` and `source: 'driver'`. A middleware that counts statements or latencies checks `result.outcome` if it should skip them.
|
|
861
|
+
|
|
862
|
+
### Replace `afterTransaction` with `activityFinalized`
|
|
863
|
+
|
|
864
|
+
`afterTransaction(plan, result, ctx)` becomes `activityFinalized(activity, result, ctx)`. A statement still running when its transaction ends is finalized when it ends, after its `activityEnded`, where `afterTransaction` fired at the transaction's end, before the statement's after-hook. `result.outcome` keeps its three values, `'committed'`, `'rolled-back'` and `'unknown'`, and the same rules decide it. An error thrown from it is still logged and swallowed. It also fires for transactions, after their statements, so a middleware that acts on statements checks `activity.kind === 'statement'`. A statement that fails before it runs, in `beforeCompile`, `beforeEncode` or parameter encoding, is now finalized too: `unknown` outside a transaction, and with the transaction's outcome inside one.
|
|
865
|
+
|
|
866
|
+
### Rename the context type and the execution id
|
|
867
|
+
|
|
868
|
+
`RuntimeMiddlewareContext` is now `ActivityContext`, and `SqlMiddlewareContext` and `MongoMiddlewareContext` extend it. A SQL middleware imports `SqlMiddleware` and `SqlMiddlewareContext` from `@prisma/orm-postgres/family-runtime` (or `@prisma/orm-sqlite/family-runtime`), a Mongo middleware imports `MongoMiddleware` and `MongoMiddlewareContext` from `@prisma/orm-mongo/family-runtime`, and a cross-family middleware imports `CrossFamilyMiddleware` and `ActivityContext` from `@prisma/orm-framework/components/runtime`. `ctx.planExecutionId` is removed; use `activity.id`, a fresh id for each activity that every hook of the activity sees. A log detail or field that carried `planExecutionId: ctx.planExecutionId` becomes `activityId: activity.id`. Keep per-statement state in `ctx.state(key)` instead of a `WeakMap` keyed by the plan: each activity has its own state, and middleware that use the same key share the object. `AfterQueryResult`, `AfterExecuteResult` and `AfterTransactionResult` are removed; use `ActivityEndedResult` and `ActivityFinalizedResult`.
|
|
869
|
+
|
|
870
|
+
### Abort phases and already-aborted calls
|
|
871
|
+
|
|
872
|
+
A `RUNTIME.ABORTED` error raised during the middleware stage before encoding has `details.phase` `'beforeEncode'`. Code that matches `'beforeQuery'` or `'beforeExecute'` matches `'beforeEncode'` instead. The same holds for the Mongo error `RUNTIME.CONTENT_HASH_REQUIRES_RESOLVED_COMMAND`, raised when a Mongo `beforeEncode` hook calls `ctx.contentHash`: its `details.phase` is `'beforeEncode'`. A call whose signal was aborted before it was made now fires `activityStarted`, then ends `failed`, before it rejects with `RUNTIME.ABORTED`.
|
|
873
|
+
|
|
874
|
+
### The observed-row budget now runs
|
|
875
|
+
|
|
876
|
+
`budgets()` now counts the rows each query returns. Before, this check never ran, so a query that returned more than `maxRows` rows succeeded. Now, with the default row severity `'error'` or in `strict` mode, such a query fails with `BUDGET.ROWS_EXCEEDED` once the count passes `maxRows`; with `severities: { rowCount: 'warn' }` in `permissive` mode it logs one warning for the statement and keeps streaming. The `postgres()` client runs its runtime in `strict` mode, where every `budgets()` violation throws whatever its severity, so the warn path does not apply there until the client exposes `mode`. Raise `maxRows` for reads that are expected to return more rows. `tableRows` and `defaultTableRows` only change the estimate checked before a read runs; they do not change the limit on rows a read actually returns.
|
|
877
|
+
|
|
878
|
+
## Statements decide placeholders and type changes
|
|
879
|
+
|
|
880
|
+
A Postgres or SQLite project that changes a field's type so that values may not survive, adds a required field to a table with rows, or makes an optional field required, plans differently.
|
|
881
|
+
|
|
882
|
+
For each script, CI job or documented workflow that runs `prisma migration plan` or `prisma db update` without a terminal:
|
|
883
|
+
|
|
884
|
+
1. If it changes a field's type, add `--convert <Model.field>` to `migration plan` and write the conversion into the `using: placeholder('<Model.field>:convert')` slot of the written `migration.ts` as SQL text over the column, such as `using: '"age"::integer'` (on SQLite, the column's copy expression in `recreateTable`, such as `copyExpressions: { age: 'CAST("age" AS INTEGER)' }`), then run `node <package dir>/migration.ts`. Or add `--delete <Model.field>` to accept the database's cast. `db update` accepts only `--delete`.
|
|
885
|
+
2. If it adds a required field to a table with rows, or makes an optional field required, and relied on the backfill step `migration plan` used to write, add `--backfill <Model.field>`. Without it the existing rows get a temporary value, or, for a field made required, the apply fails when rows hold NULL. A new unique, primary-key or foreign-key field, or one whose values come from a value set, has no temporary value: use `--backfill` when the table has rows. Fill the slots the plan writes, then run `node <package dir>/migration.ts`. On Postgres, `check` returns a query that finds a row still without a value, and `run` is one closure, or an array of closures, each returning one update (not one closure returning an array): `check: () => db.public.user.select('id').where((f, fns) => fns.eq(f.name, null)).limit(1)` and `run: () => db.public.user.update({ name: '' }).where((f, fns) => fns.eq(f.name, null))`, with `db` the `sql` of `postgres<End>({ contractJson: endContract })`. On SQLite there is only `run`, a closure that returns the SQL text of one statement: ``run: () => `UPDATE "User" SET name = 'user ' || id WHERE name IS NULL` ``.
|
|
886
|
+
3. If it parses the failure of such an apply, match the new summary, for example `3 rows of "Item" hold NULL in "note", so it cannot become required.`
|
|
887
|
+
|
|
888
|
+
## `alterColumnType` takes the new type and the conversion directly
|
|
889
|
+
|
|
890
|
+
A hand-written Postgres migration that changes a column's type called `alterColumnType` with an `options` object that carried internal names:
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
this.alterColumnType({
|
|
894
|
+
schema: 'public',
|
|
895
|
+
table: 'user',
|
|
896
|
+
column: 'age',
|
|
897
|
+
options: {
|
|
898
|
+
qualifiedTargetType: 'int4',
|
|
899
|
+
formatTypeExpected: 'integer',
|
|
900
|
+
rawTargetTypeForLabel: 'int4',
|
|
901
|
+
using: '"age"::integer',
|
|
902
|
+
},
|
|
903
|
+
}),
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
It now takes the new type and the conversion directly:
|
|
907
|
+
|
|
908
|
+
```ts
|
|
909
|
+
this.alterColumnType({
|
|
910
|
+
schema: 'public',
|
|
911
|
+
table: 'user',
|
|
912
|
+
column: 'age',
|
|
913
|
+
type: 'int4',
|
|
914
|
+
using: '"age"::integer',
|
|
915
|
+
}),
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
A `migration.ts` that still passes `options` stops with `MIGRATION.OPERATION_OPTION_REMOVED` when it runs, naming the column and the new shape.
|
|
919
|
+
|
|
920
|
+
For each `migration.ts` the detection matches:
|
|
921
|
+
|
|
922
|
+
1. Replace `options: { ... }` with `type: <the value of qualifiedTargetType>`, and `using: <the value of options.using>` when the old call had one.
|
|
923
|
+
2. Delete `formatTypeExpected` and `rawTargetTypeForLabel`. The operation now works out the type the database prints from `type`, through the data types of the configured stack. A type it cannot read fails with `MIGRATION.COLUMN_TYPE_UNREADABLE`, which names the call. Write the type as a migration writes it, such as `int4`, `varchar(20)`, or `"Mood"` for an enum, or add the extension that provides it to the configuration.
|
|
924
|
+
3. Check `operationClass`. Left out, it is now `data` when the call has a `using`, and `destructive` otherwise. A call with a `using` that said `operationClass: 'destructive'` keeps that class only if you keep the option; delete it to class the conversion as `data`. A call that said `operationClass: 'widening'` keeps it.
|
|
925
|
+
4. Check `using`. A semicolon, a SQL comment or dollar quoting outside a quoted literal, or a comma or a keyword such as `FROM` or `UNION` outside parentheses, now fails with `MIGRATION.SQL_EXPRESSION_INVALID`, which names the call. `FROM` directly after `DISTINCT`, as in `IS DISTINCT FROM`, is accepted. Remove a trailing comment. If the expression holds a comma or a keyword such as `FROM` at the top level, wrap it in parentheses.
|
|
926
|
+
5. If the migration has not been applied to any database, run it (`node <package dir>/migration.ts`) to write `ops.json` and `migration.json` again. If it has been applied, leave those two files as they are: the operation's label now names the type as written in `type`, so writing them again would change the migration's hash.
|
|
927
|
+
|
|
928
|
+
## MongoDB carries out `--rename` and `--delete`
|
|
929
|
+
|
|
930
|
+
### `mongo-rename-statements-carried-out`
|
|
931
|
+
|
|
932
|
+
`--rename` now works on MongoDB, with the same syntax as on SQL. A field is named by its contract key, which on MongoDB is its stored name: `id @map("_id")` is `User._id`.
|
|
933
|
+
|
|
934
|
+
```sh
|
|
935
|
+
prisma migration plan --name tidy --rename Profile:User --rename User.name:User.fullName
|
|
936
|
+
prisma db update --rename Profile:User --rename User.name:User.fullName
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
- A model rename whose collection name changes renames the collection (`renameCollection`), keeping its documents, indexes and validator.
|
|
940
|
+
- A field rename renames the field in every document that holds it (`renameField`), skipping validation. A variant's field is renamed only in that variant's documents.
|
|
941
|
+
- The planner refuses, with the reason and what to do instead: a variant rename whose discriminator value changes, `_id`, a field whose stored name contains `.` or starts with `$`, a time-series collection, a target collection that already exists or that MongoDB rejects, and a collection whose control policy is not `managed`. A field removed from a model whose collection is not `managed` is not asked about and not removed.
|
|
942
|
+
|
|
943
|
+
If you renamed a collection or a field by hand in `mongosh` because `--rename` was refused, nothing changes for migrations you already applied. From now on, give the `--rename` statement instead.
|
|
944
|
+
|
|
945
|
+
A script or test that expects `--rename` on MongoDB to fail with `MIGRATION.PLANNING_FAILED` now gets a plan. A script that reads `meta.unanswered` of `CLI.CONSENT_REQUIRED` for a dropped MongoDB collection now finds `verbs: ['rename', 'delete']` instead of `['delete']`.
|
|
946
|
+
|
|
947
|
+
### `mongo-removed-field-asks`
|
|
948
|
+
|
|
949
|
+
Removing a field from a MongoDB model used to update the collection's validator and ask nothing. Documents that still held the field then failed every update with `Document failed validation`. Now the plan removes the field from those documents (`unsetField`, a `destructive` operation) and asks first. Where nobody can answer, the command fails with `CLI.CONSENT_REQUIRED`, whose next actions name `--delete User.nickname` and `--rename 'User.nickname:User.<new name>'`.
|
|
950
|
+
|
|
951
|
+
In a script or CI job that runs `prisma db update --no-interactive` or `prisma migration plan` after a field was removed, add `--delete <Model>.<field>` for each removed field, or `--rename` if the field was renamed. When `db update` has no snapshot of the contract the database is at, the field is named by its collection: `--delete profiles.nickname`.
|
|
952
|
+
|
|
953
|
+
### `mongo-preview-get-collection`
|
|
954
|
+
|
|
955
|
+
A test or a script that compares the preview of a MongoDB plan (`migration plan`, `db update --dry-run`, `migration show`) expects `db.getCollection("<name>")` in place of `db.<name>`, so a collection name such as `my-coll` is valid `mongosh`. Plans and `ops.json` are unchanged.
|
|
956
|
+
|
|
957
|
+
### `mongo-time-series-update`
|
|
958
|
+
|
|
959
|
+
A repeated `db update` on a database whose contract declares a time-series collection used to fail with "Cannot change immutable collection option 'timeseries'", because MongoDB adds defaults such as `bucketMaxSpanSeconds` that the contract leaves out. The planner now compares only the options the contract declares, so the command succeeds and plans nothing. A workaround that skipped `db update` for such a database is no longer needed.
|
|
960
|
+
|
|
961
|
+
### `storage-question-wording`
|
|
962
|
+
|
|
963
|
+
A test that asserts the text of a data-loss question about data named by its storage name expects `--delete loses that data` where it expected `--delete loses its rows`.
|
|
964
|
+
|
|
965
|
+
### `migration-ts-finds-its-config`
|
|
966
|
+
|
|
967
|
+
Without `--config`, `node migration.ts` now picks the config that owns the migration: a config owns it when the migration file is inside that config's `migrations.dir`. It tries the nearest `prisma.config.ts` above the migration's directory first, then the `prisma.config.ts` in the working directory. So the file runs from inside its own directory, from the project directory, or from any other directory, and with `migrations.dir` outside the project, such as `../shared-db/migrations`, when run from the project directory. If neither config owns the migration, it fails with `CLI.MIGRATION_CONFIG_NOT_FOUND`, naming the configs it checked; run it with `--config <path>`. `--config` still overrides the lookup.
|
|
968
|
+
|
|
969
|
+
A command that ran the file from the project directory keeps working. A script that ran it from a directory whose `prisma.config.ts` belongs to another project, and relied on that config, now fails with `CLI.MIGRATION_CONFIG_NOT_FOUND`; pass `--config`. `migration new` and `migration plan` given `--config` now repeat it in the `node` command they print. Messages that tell you to run a `migration.ts` say to run it with node, without a path or a working directory.
|
|
970
|
+
|
|
971
|
+
## `contract-artifacts-restamp`
|
|
972
|
+
|
|
973
|
+
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.18`; any other difference in the emitted files comes from an earlier entry in this guide.
|