@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.
@@ -0,0 +1,1008 @@
1
+ ---
2
+ from: "8.0.0-rc.17"
3
+ to: "8.0.0-rc.18"
4
+ changes:
5
+ - id: build-symbol-table-takes-default-namespace-id
6
+ summary: |
7
+ `buildSymbolTable` from `@internal/psl-parser` requires `defaultNamespaceId`. `ContractSourceContext` (`@internal/config/config-types`) and `ControlTargetDescriptor` (`@internal/framework-components/control`) each have a required `defaultNamespaceId`.
8
+ detection:
9
+ glob: "**/*.{ts,mts,cts,tsx}"
10
+ matches:
11
+ - '\bbuildSymbolTable\s*\('
12
+ - '\bContractSourceContext\b'
13
+ - '\bControlTargetDescriptor\b'
14
+ - id: symbol-table-has-no-top-level
15
+ summary: |
16
+ `SymbolTable` is `{ defaultNamespace, namespaces, namedTypes }` with no `topLevel`; `TopLevelScope` is removed. Top-level models, composite types, blocks and mixins are members of `symbolTable.defaultNamespace`. `NamespaceSymbol` has a contract `id` and a PSL `name`, `namespaces` is keyed by `id`, and `declarations` can be empty.
17
+ detection:
18
+ glob: "**/*.{ts,mts,cts,tsx}"
19
+ matches:
20
+ - '\.topLevel\b'
21
+ - '\bTopLevelScope\b'
22
+ - '\bNamespaceSymbol\b'
23
+ - '\.declarations\[0\]'
24
+ - '\bResolvedEntityReference\b'
25
+ - id: repeated-extension-block-is-a-duplicate-declaration
26
+ summary: |
27
+ A test that expects `PSL_DUPLICATE_EXTENSION_ENTITY` for an extension block repeated at the top level and in `namespace public { … }`, or under both `namespace unbound { … }` and `namespace __unbound__ { … }`, now gets `PSL_DUPLICATE_DECLARATION` with the message `Duplicate declaration of "<name>"`.
28
+ detection:
29
+ glob: "**/*.{ts,mts,cts,tsx}"
30
+ matches:
31
+ - '\bPSL_DUPLICATE_EXTENSION_ENTITY\b'
32
+ - id: psl-default-namespace-schema-rules
33
+ summary: |
34
+ PSL shipped with or generated by an extension: the top level and `namespace public { … }` (Postgres) share one set of names, and a top-level declaration cannot be named like the target's default namespace. Rename or remove the clashing declaration.
35
+ detection:
36
+ glob: "**/*.{prisma,psl}"
37
+ matches:
38
+ - '(^|\n)\s*namespace\s+(public|unbound|__unbound__)\s*\{'
39
+ - '(^|\n)[ \t]*(?!namespace[ \t])[A-Za-z_]\w*[ \t]+(public|unbound|__unbound__)[ \t]*\{'
40
+ - '(^|\n)\s*(public|unbound|__unbound__)\s*=\s*\S'
41
+ - id: block-spec-context-carries-data-types
42
+ summary: |
43
+ `BlockSpecContext` gains `dataTypes`, and `interpretExtensionBlocks`, `interpretExtensionBlock` and `interpretExtensionBlockAttributes` take a required `dataTypes`. `ControlDefaultRegistries` is deleted: an attribute spec context, `createBinder` and the Mongo PSL interpreter take `defaultFunctionRegistry` directly. Every factory in `sqlAttributeSpecs` takes the spec context.
44
+ detection:
45
+ glob: "**/*.{ts,mts,cts}"
46
+ matches:
47
+ - '\b(BlockSpecContext|ControlDefaultRegistries|interpretExtensionBlocks|interpretExtensionBlock|interpretExtensionBlockAttributes)\b'
48
+ - 'controlMutationDefaults:\s*\{\s*defaultFunctionRegistry'
49
+ - 'sqlAttributeSpecs\.(model|field)\.\w+\(\)'
50
+ - id: tagged-literal-text-helpers
51
+ summary: |
52
+ `canonicalizeTaggedLiteralBody` and `TaggedLiteralCanonicalization` are exported from `@internal/framework-components/authoring` only, beside the new `printedTaggedLiteralReadsBack`. `printSqlExpressionLiteral` throws for a text that would read back as different text; check with `sqlTextsReadBack` first.
53
+ detection:
54
+ glob: "**/*.{ts,mts,cts}"
55
+ matches:
56
+ - '\bcanonicalizeTaggedLiteralBody\b'
57
+ - '\bTaggedLiteralCanonicalization\b'
58
+ - '\bprintSqlExpressionLiteral\b'
59
+ - id: builder-raw-sql-is-a-sql-value
60
+ summary: |
61
+ 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.
62
+ detection:
63
+ glob: "**/*.{ts,mts,cts}"
64
+ matches:
65
+ - '(?<!readonly |''function'', )\b(where|expression|using|withCheck)\s*:\s*[''"\x60]'
66
+ - id: sql-tag-interpolates-sql-values
67
+ summary: |
68
+ The `sql` tag interpolates other `sql` values: `` sql`${owner} AND deleted_at IS NULL` ``. Compose predicates this way instead of joining strings.
69
+ detection:
70
+ glob: "**/*.{ts,mts,cts}"
71
+ matches:
72
+ - '(?<!\bsql)\x60\$\{[\w.]+\}\s+(AND|OR)\b'
73
+ - id: sql-tag-error-codes-renamed
74
+ summary: |
75
+ `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.
76
+ detection:
77
+ glob: "**/*.{ts,mts,cts}"
78
+ matches:
79
+ - '\bDEFAULT_SQL_INTERPOLATION\b'
80
+ - '\bCONTRACT\.DEFAULT_INVALID\b'
81
+ - id: ts-storage-hash-may-change-once
82
+ summary: |
83
+ 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.
84
+ detection:
85
+ glob: "**/*.{ts,mts,cts}"
86
+ matches:
87
+ - '(?<!readonly )\b(where|expression|using|withCheck)\s*:\s*(\x60[ \t]*$|[''"\x60][ \t\r\n])'
88
+ - id: policy-handles-hold-sql-values
89
+ summary: |
90
+ `RlsPolicyHandle`, `RlsUsingPolicyDescriptor`, `RlsWithCheckPolicyDescriptor` and `RlsUsingWithCheckPolicyDescriptor` hold `using` and `withCheck` as `SqlExpression`; `fullTextIndex`'s `where` option, `IndexConstraint.where`, `IndexExpressionInput` and `AuthoredCheckConstraint.expression` are `SqlExpression` too. Read the text with `.text`.
91
+ detection:
92
+ glob: "**/*.{ts,mts,cts}"
93
+ matches:
94
+ - '\b(RlsPolicyHandle|RlsUsingPolicyDescriptor|RlsWithCheckPolicyDescriptor|RlsUsingWithCheckPolicyDescriptor|IndexConstraint|IndexExpressionInput|AuthoredCheckConstraint)\b'
95
+ - id: sql-tag-lives-in-sql-contract
96
+ summary: |
97
+ The `sql` tag and the `SqlExpression` type live in `@internal/sql-contract/sql-expression`; `@internal/sql-contract-ts/contract-builder` re-exports both. The tag returns a `SqlExpression`, not a `ColumnDefault`. `sql-default-literal.ts` is deleted.
98
+ detection:
99
+ glob: "**/*.{ts,mts,cts}"
100
+ matches:
101
+ - 'sql-default-literal'
102
+ - '\bsql\x60[^\x60]*\x60\s*\.(kind|expression)\b'
103
+ - ':\s*ColumnDefault\s*=\s*sql\x60'
104
+ - 'import\s*(type\s*)?\{[^}]*\bsql\b[^}]*\}\s*from\s*[''"]@internal/sql-contract-ts/contract-builder[''"]'
105
+ - id: render-full-text-index-document-takes-a-definition
106
+ summary: |
107
+ `renderFullTextIndexExpression` from `@internal/target-postgres/sql-utils` is now `renderFullTextIndexDocument`, and it takes the index definition `{ weightGroups, language }` instead of a language and one column name.
108
+ detection:
109
+ glob: "**/*.{ts,mts,cts}"
110
+ matches:
111
+ - '\brenderFullTextIndexExpression\b'
112
+ - id: full-text-indexable-codec-check-replaced
113
+ summary: |
114
+ `isFullTextIndexableCodec` is no longer exported from `@internal/target-postgres/sql-utils`. Check a full-text index with `fullTextIndexProblems`, or read a codec's traits with `postgresCodecTraitsOf`, both from `@internal/target-postgres/full-text-index-authoring`.
115
+ detection:
116
+ glob: "**/*.{ts,mts,cts}"
117
+ matches:
118
+ - '\bisFullTextIndexableCodec\b'
119
+ - id: structured-index-option-values-hash-as-json
120
+ summary: |
121
+ An index option whose value is an array or an object now enters the index name's hash as JSON rather than through `String()`. A wire-named index with such an option gets a new name; re-emit contracts that declare one.
122
+ detection:
123
+ glob: "**/*.{ts,mts,cts}"
124
+ matches:
125
+ - 'constraints\.index\([\s\S]*options:\s*\{[^}]*[\[{]'
126
+ - id: collection-state-carries-tables
127
+ summary: |
128
+ A SQL ORM `CollectionState` has a required `tables` property holding the table scope and the aliased root table and variant tables of the collection, and `emptyState()` takes it as its argument. Build it with `createCollectionTables(contract, namespaceId, modelName)`. This applies to every hand-built state, including `IncludeExpr.nested`, a `combine()` branch state and the state of an include scalar selector.
129
+ detection:
130
+ glob: "**/*.{ts,tsx,mts,cts}"
131
+ matches:
132
+ - '\bemptyState\('
133
+ - '\bCollectionState\b'
134
+ - id: state-filters-planned-as-written
135
+ summary: |
136
+ The SQL ORM planners no longer turn a literal in `CollectionState.filters` into a parameter. A filter put into a hand-built state must carry a `ParamRef` with the column's codec for each value compared with a column; a `LiteralExpr` left there is not encoded by the column's codec. Filters added through `collection.where(...)` are unaffected.
137
+ detection:
138
+ glob: "**/*.{ts,tsx,mts,cts}"
139
+ matches:
140
+ - '\bfilters\s*:'
141
+ - id: create-model-accessor-takes-tables
142
+ summary: |
143
+ `createModelAccessor` takes the collection's tables as its fourth argument, before the optional variant's discriminator value: `createModelAccessor(context, namespaceId, modelName, tables, variantValue?)`.
144
+ detection:
145
+ glob: "**/*.{ts,tsx,mts,cts}"
146
+ matches:
147
+ - '\bcreateModelAccessor\('
148
+ - id: include-expr-junction-table
149
+ summary: |
150
+ A hand-built `IncludeExpr` for a many-to-many relation needs a `junction` aliased table next to `through`; compiling a `through` include without one throws. `IncludeExpr.localTableName` is replaced by the optional `localVariantValue`, the discriminator value of the multi-table-inheritance variant whose table holds the relation's local columns.
151
+ detection:
152
+ glob: "**/*.{ts,tsx,mts,cts}"
153
+ matches:
154
+ - '\bIncludeExpr\b'
155
+ - id: read-variants-by-discriminator-value
156
+ summary: |
157
+ A polymorphic base model's `variants` in a contract is keyed by discriminator value, and each entry is a reference `{ namespace, model }` to the variant model. Code that reads it finds the variant model at `variants[<value>].model`, where it used to find the value at `variants[<Model>].value`. `ContractVariantEntry` is removed; the entry type is `CrossReference`. A variant may be in a different namespace from its base.
158
+ detection:
159
+ glob: "**/*.{ts,tsx,mts,cts,js,mjs,cjs}"
160
+ matches:
161
+ - '(?<![\w$])variants(?![\w$])'
162
+ - '\bContractVariantEntry\b'
163
+ - id: sql-orm-state-names-a-variant-by-value
164
+ summary: |
165
+ SQL ORM collection state names a variant by its discriminator value, not its model name. `CollectionState.variantName` is `variantValue`, the type-state field `variantName` is `variantValue`, and `CollectionTables.variants` is keyed by discriminator value. `IncludeExpr.localTableName` is replaced by `localVariantValue`, as `include-expr-junction-table` describes.
166
+ detection:
167
+ glob: "**/*.{ts,tsx,mts,cts}"
168
+ matches:
169
+ - '\bvariantName\b'
170
+ - '\bCollectionTables\b'
171
+ - id: orm-variant-types-take-a-value
172
+ summary: |
173
+ The Mongo ORM variant type helpers take a discriminator value where they took a variant model name: `VariantModelRow`, `VariantCreateInput`, and the fourth type parameter of `MongoCollection`. `DiscriminatorValues` is the keys of `variants`. `VariantNames` and `VariantNameForValue` are removed.
174
+ detection:
175
+ glob: "**/*.{ts,tsx,mts,cts}"
176
+ matches:
177
+ - '\bVariantNames\b'
178
+ - '\bVariantNameForValue\b'
179
+ - '\bVariantModelRow\b'
180
+ - '\bVariantCreateInput\b'
181
+ - '\bMongoCollection\s*<'
182
+ - id: orm-refuses-names-that-are-not-fields
183
+ summary: |
184
+ 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`.
185
+ detection:
186
+ glob: "**/*.{ts,mts,cts,tsx}"
187
+ matches:
188
+ - '(?<![\s\S])(?=[\s\S]*\bconflictOn\b)[\s\S]*\bORM\.ARGUMENT_INVALID\b'
189
+ - id: fragment-is-made-from-the-root-collection
190
+ 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(."
191
+ detection:
192
+ glob: "**/*.{ts,mts,cts,tsx}"
193
+ matches:
194
+ - '\)\s*\.fragment\('
195
+ - '\bthis\s*\.fragment\('
196
+ - id: fragment-types-are-renamed
197
+ summary: |
198
+ 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 the ORM client (an `orm-client` entry or `@internal/sql-orm-client`), after a namespace import of it, or in an inline `import(...)` type of it, so React's `Fragment` and the SQL builder's `Scope` do not match.
199
+ detection:
200
+ glob: "**/*.{ts,mts,cts,tsx}"
201
+ matches:
202
+ - '\b(?:FieldFragment|FieldScope|ScopeFacts)\b'
203
+ - '(?:import|export)\s+(?:type\s+)?\{[^}]*\b(?:Fragment|Scope)\b[^}]*\}\s*from\s*[''"]@(?:prisma/[\w-]+/orm-client|internal/sql-orm-client)[''"]'
204
+ - 'import\s+(?:type\s+)?\*\s+as\s+([\w$]+)\s+from\s*[''"]@(?:prisma/[\w-]+/orm-client|internal/sql-orm-client)[''"][\s\S]*\b\1\.(?:Fragment|Scope)\b'
205
+ - 'import\(\s*[''"]@(?:prisma/[\w-]+/orm-client|internal/sql-orm-client)[''"]\s*\)\.(?:Fragment|FieldFragment|Scope|FieldScope)\b'
206
+ - id: unique-criterion-rejects-null
207
+ summary: |
208
+ `UniqueConstraintCriterion` in the SQL ORM client (`@prisma/orm-postgres/orm-client`, the other facades' `orm-client` entries and `@internal/sql-orm-client`) 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.
209
+ detection:
210
+ glob: "**/*.{ts,tsx,mts,cts}"
211
+ matches:
212
+ - '\bconflictOn\b|\.(?:connect|disconnect)\s*\(|\bUniqueConstraintCriterion\b'
213
+ - id: prepared-all-and-aggregate-need-their-receiver
214
+ summary: |
215
+ 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.
216
+ detection:
217
+ glob: "**/*.{ts,tsx,mts,cts}"
218
+ matches:
219
+ - '\{[^}]*\b(?:all|aggregate)\b[^}]*\}\s*=\s*[^;\n]*\.prepared\b'
220
+ - id: build-native-enums-takes-the-domain
221
+ summary: |
222
+ `buildNamespacedNativeEnums` from `@prisma/orm-postgres/runtime` takes the contract's domain as a second argument, `buildNamespacedNativeEnums(contract.storage, contract.domain)`, and leaves out each value set that a domain enum of the same namespace produced. The accessors it builds hold native enum types only; read a domain enum through the accessor `db.enums` uses.
223
+ detection:
224
+ glob: "**/*.{ts,mts,cts,tsx}"
225
+ matches:
226
+ - '\bbuildNamespacedNativeEnums\s*\('
227
+ - id: middleware-hooks-are-activity-hooks
228
+ summary: |
229
+ 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.
230
+ detection:
231
+ glob: "**/*.{ts,mts,cts,tsx}"
232
+ matches:
233
+ - '\b(beforeQuery|beforeExecute|interceptQuery|interceptExecute|afterQuery|afterExecute|afterTransaction)\b'
234
+ - id: middleware-context-is-activity-context
235
+ summary: |
236
+ `RuntimeMiddlewareContext` is renamed `ActivityContext`. It gains `parent`, `state(key)` and `inTransaction()`, and loses `planExecutionId`; use `activity.id` instead.
237
+ detection:
238
+ glob: "**/*.{ts,mts,cts,tsx}"
239
+ matches:
240
+ - '\bRuntimeMiddlewareContext\b'
241
+ - '\bplanExecutionId\b'
242
+ - id: after-result-types-removed
243
+ summary: |
244
+ `AfterQueryResult`, `AfterExecuteResult` and `AfterTransactionResult` are removed. Use `ActivityEndedResult` (narrow it with `result.kind === 'statement'` and `result.returns`) and `ActivityFinalizedResult`.
245
+ detection:
246
+ glob: "**/*.{ts,mts,cts,tsx}"
247
+ matches:
248
+ - '\bAfter(Query|Execute|Transaction)Result\b'
249
+ - id: aborted-phase-before-encode
250
+ summary: |
251
+ 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'`.
252
+ detection:
253
+ glob: "**/*.{ts,mts,cts,tsx}"
254
+ matches:
255
+ - '[''"`](beforeQuery|beforeExecute|afterQuery|afterExecute)[''"`]'
256
+ - id: every-started-statement-is-ended-and-finalized
257
+ summary: |
258
+ 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.
259
+ - id: observed-row-budget-runs
260
+ summary: |
261
+ 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.
262
+ detection:
263
+ glob: "**/*.{ts,mts,cts,tsx}"
264
+ matches:
265
+ - '\bbudgets\('
266
+ - id: statement-runners-replace-middleware-runners
267
+ summary: |
268
+ `runBeforeQueryChain`, `runBeforeExecuteChain`, `runQueryWithMiddleware`, `runExecuteWithMiddleware`, `runAfterTransaction`, `reportQueryEnding`, `reportExecuteEnding`, `onQueryEndOutsideTransaction` and `QueryEnding` are removed from `@prisma/orm-framework`. A runtime runs a statement with `streamStatement` or `executeStatement`, whose body calls `beforeEncode`, `runQuery` and `runExecute`, and opens a transaction with `startTransactionActivity`.
269
+ detection:
270
+ glob: "**/*.{ts,mts,cts,tsx}"
271
+ matches:
272
+ - '\b(runBeforeQueryChain|runBeforeExecuteChain|runQueryWithMiddleware|runExecuteWithMiddleware|runAfterTransaction|reportQueryEnding|reportExecuteEnding|onQueryEndOutsideTransaction|QueryEnding|afterTransactionStageFor|anyMiddlewareDeclaresAfterTransaction)\b'
273
+ - id: runtime-core-context-fields
274
+ summary: |
275
+ `RuntimeCoreOptions.ctx` is an `ActivityContextFields`: the context fields without `parent`, `state` and `inTransaction`, and without `planExecutionId`.
276
+ detection:
277
+ glob: "**/*.{ts,mts,cts,tsx}"
278
+ matches:
279
+ - '\bplanExecutionId\s*:'
280
+ - id: begin-transaction-on
281
+ summary: |
282
+ `SqlRuntimeBase.wrapTransaction(driverTx, connection)` is replaced by `beginTransactionOn(connection, owner)`, which starts the transaction activity, runs `BEGIN` on the connection and returns the wrapped transaction. `owner` is the `RuntimeConnection` the subclass hands to its callers for that connection; when the transaction's `activityStarted` hooks refuse the start before `BEGIN`, `withTransaction` releases `owner`, and after any other start failure it destroys it.
283
+ detection:
284
+ glob: "**/*.{ts,mts,cts,tsx}"
285
+ matches:
286
+ - '\bwrapTransaction\s*\('
287
+ - id: middleware-debug-events-renamed
288
+ summary: |
289
+ The runtime's debug events `middleware.interceptQuery` and `middleware.interceptExecute` are now `middleware.intercept.onQuery` and `middleware.intercept.onExecute`, and the logged error event `middleware.afterTransaction.error` is now `middleware.activityFinalized.error`.
290
+ detection:
291
+ glob: "**/*.{ts,mts,cts,tsx}"
292
+ matches:
293
+ - 'middleware\.(interceptQuery|interceptExecute|afterTransaction\.error)'
294
+ - id: rename-statements-capability-removed
295
+ summary: |
296
+ `TargetMigrationsCapability.renameStatements` (with `refused` and `keepDataByHand`), added in 8.0.0-rc.17, is removed: every target's planner carries out rename statements. The CLI no longer appends a target's text to a data-loss question.
297
+ detection:
298
+ glob: "**/*.{ts,mts,cts}"
299
+ matches:
300
+ - '(?<![\w$])(?:renameStatements|keepDataByHand)\s*:'
301
+ - id: mongo-ddl-rename-collection
302
+ summary: |
303
+ `MongoDdlCommandVisitor` gains `renameCollection(command: RenameCollectionCommand)`, and `AnyMongoDdlCommand` and `AnyMongoDdlWireCommand` gain `RenameCollectionCommand` and `RenameCollectionWireCommand` (source in `collection`, new name in `to`). A visitor implementation, or a driver's `run`, handles the new kind.
304
+ detection:
305
+ glob: "**/*.{ts,mts,cts}"
306
+ matches:
307
+ - '(?<![\w$])(?:MongoDdlCommandVisitor|AnyMongoDdlCommand|AnyMongoDdlWireCommand)(?![\w$])'
308
+ - id: mongo-dml-operations
309
+ summary: |
310
+ `MongoDataTransformOperation` is renamed `MongoDmlOperation`, and `MongoDataTransformCheck` `MongoDmlCheck`. Its `operationClass` is any `MigrationOperationClass`, not only `'data'`: the planner's document rewrites (`renameField`, `unsetField`) are DML operations with class `widening` or `destructive`. Tell a DML operation from a DDL operation with `isMongoDmlOperation(operation)` (it has `run`), not with `operationClass === 'data'`.
311
+ detection:
312
+ glob: "**/*.{ts,mts,cts}"
313
+ matches:
314
+ - 'operationClass\s*===?\s*["'']data["'']'
315
+ - '(?<![\w$])MongoDataTransform(?:Operation|Check)(?![\w$])'
316
+ - id: mongo-update-many-bypass-validation
317
+ summary: |
318
+ `RawUpdateManyCommand` takes an optional fourth argument `{ bypassDocumentValidation }`; `UpdateManyWireCommand` takes it as an optional fifth constructor argument and carries `bypassDocumentValidation: boolean`; the lowered `rawUpdateMany` draft (`MongoLoweredDraft`) requires `bypassDocumentValidation: boolean`.
319
+ detection:
320
+ glob: "**/*.{ts,mts,cts}"
321
+ matches:
322
+ - 'kind:\s*["'']rawUpdateMany["'']'
323
+ - id: mongo-op-factory-call-to-op
324
+ summary: |
325
+ A Mongo `OpFactoryCall`'s `toOp()`, and `renderOps(calls)`, return `AnyMongoMigrationOperation`, since `RenameFieldCall` and `UnsetFieldCall` produce DML operations.
326
+ detection:
327
+ glob: "**/*.{ts,mts,cts}"
328
+ matches:
329
+ - '["'']@(?:prisma/orm-target-mongo|internal/target-mongo)(?:/[^"'']*)?["''][\s\S]*(?:\.toOp\(\)|(?<![\w$])renderOps\s*\()'
330
+ ---
331
+
332
+ # 8.0.0-rc.17 → 8.0.0-rc.18 — Extension author upgrade instructions
333
+
334
+ ## The PSL symbol table places top-level declarations in the default namespace
335
+
336
+ `@prisma/orm-framework` publishes these modules as `@prisma/orm-framework/psl-parser`, `/config/config-types` and `/components/control`.
337
+
338
+ ### `build-symbol-table-takes-default-namespace-id`
339
+
340
+ Pass the target's default namespace id: `context.defaultNamespaceId` in a contract source, `stack.target.defaultNamespaceId` from a control stack. A `ContractSourceContext` or `ControlTargetDescriptor` built by hand adds the same field (`'public'` for Postgres, `'__unbound__'` for SQLite and MongoDB).
341
+
342
+ ```ts
343
+ const { symbolTable } = buildSymbolTable({
344
+ documents,
345
+ sources,
346
+ defaultNamespaceId: context.defaultNamespaceId,
347
+ });
348
+ ```
349
+
350
+ ### `symbol-table-has-no-top-level`
351
+
352
+ Before:
353
+
354
+ ```ts
355
+ for (const model of Object.values(symbolTable.topLevel.models)) visit(model, undefined);
356
+ for (const namespace of Object.values(symbolTable.topLevel.namespaces)) {
357
+ for (const model of Object.values(namespace.models)) visit(model, namespace.name);
358
+ }
359
+ ```
360
+
361
+ After:
362
+
363
+ ```ts
364
+ for (const namespace of Object.values(symbolTable.namespaces)) {
365
+ for (const model of Object.values(namespace.models)) visit(model, namespace.id);
366
+ }
367
+ ```
368
+
369
+ `symbolTable.namespaces` includes the default namespace, which is also `symbolTable.defaultNamespace` and holds the top-level declarations together with the members of a `namespace public { … }` block. `topLevel.namedTypes` is `symbolTable.namedTypes`. Use `namespace.id` for a contract id; `namespace.name` is the PSL spelling, which differs only for the late-bound namespace (id `__unbound__`, name `unbound`, one symbol for both spellings). `namespace.declarations` lists the written blocks and is empty for a default namespace with none. A model, composite type or block resolution always has `namespace`, so `resolution.namespace?.name ?? fallback` becomes `resolution.namespace.id`.
370
+
371
+ ### `repeated-extension-block-is-a-duplicate-declaration`
372
+
373
+ The symbol table reports the repeated block before any entity factory runs. Change the expected code for those inputs only; `PSL_DUPLICATE_EXTENSION_ENTITY` is still reported when two different blocks place an entity under one key in one namespace.
374
+
375
+ ### `psl-default-namespace-schema-rules`
376
+
377
+ A name declared both at the top level and inside `namespace public { … }` (the default namespace on Postgres), or a `types { }` binding or namespace with the same name as a member of that block, is `PSL_DUPLICATE_DECLARATION`: remove or rename one. A top-level declaration named like the target's default namespace (`public` on Postgres; `unbound` or `__unbound__` on SQLite and MongoDB) is rejected with the same code: rename it, and on a model keep the stored name with `@@map`.
378
+
379
+ ## Raw SQL in PSL is a `sql` literal
380
+
381
+ ### Spec contexts carry the stack's data types
382
+
383
+ A block spec can now read the stack's data types, so a block parameter can receive a data type through `dataTypeValue`:
384
+
385
+ ```ts
386
+ function policyUsingParam(ctx: BlockSpecContext) {
387
+ return {
388
+ type: optional(dataTypeValue(SQL_EXPRESSION_DATA_TYPE_ID, ctx.dataTypes)),
389
+ documentation: 'A SQL predicate controlling which rows this policy permits.',
390
+ };
391
+ }
392
+ ```
393
+
394
+ - `BlockSpecContext` is `{ symbols, dataTypes }`. It carries no block: a block attribute reads the block it belongs to from `BlockAttributeCtx.selfBlock` (ADR 262). Code that builds one passes the stack's `DataTypeSupport`, or `EMPTY_DATA_TYPES` from `@internal/psl-parser` when the stack registers none.
395
+ - `interpretExtensionBlocks`, `interpretExtensionBlock` and `interpretExtensionBlockAttributes` take a required `dataTypes`. Pass the stack's data types, the same value the attribute spec context carries.
396
+ - `ControlDefaultRegistries` is deleted. `AttributeSpecContext`, `CreateBinderOptions` and `InterpretPslDocumentToMongoContractInput` take `defaultFunctionRegistry: ControlMutationDefaultRegistry` in place of `controlMutationDefaults`:
397
+
398
+ ```diff
399
+ - controlMutationDefaults: { defaultFunctionRegistry },
400
+ + defaultFunctionRegistry,
401
+ ```
402
+
403
+ A spec factory that read `ctx.controlMutationDefaults.defaultFunctionRegistry` reads `ctx.defaultFunctionRegistry`.
404
+
405
+ `blockSpecContext({ symbols, dataTypes })` from `@internal/psl-parser` builds a `BlockSpecContext`. Every factory in `sqlAttributeSpecs` (from `@internal/sql-contract-psl/attribute-specs`) takes the spec context, including those whose spec does not read it: call `sqlAttributeSpecs.model.map(ctx)`, not `sqlAttributeSpecs.model.map()`.
406
+
407
+ A spec that takes raw SQL can do what the SQL family and Postgres now do: declare `dataTypeValue(SQL_EXPRESSION_DATA_TYPE_ID, ctx.dataTypes)`, with `SQL_EXPRESSION_DATA_TYPE_ID` from `@internal/sql-contract/sql-expression`. The value it returns holds the canonical text; `sqlTextFromCanonical(value.value)` reads it.
408
+
409
+ ### Tagged literal text helpers
410
+
411
+ `canonicalizeTaggedLiteralBody` and its result type `TaggedLiteralCanonicalization` are no longer exported from `@internal/framework-components/control`. Import them from `@internal/framework-components/authoring`:
412
+
413
+ ```diff
414
+ - import { canonicalizeTaggedLiteralBody } from '@internal/framework-components/control';
415
+ - import type { TaggedLiteralCanonicalization } from '@internal/framework-components/control';
416
+ + import { canonicalizeTaggedLiteralBody } from '@internal/framework-components/authoring';
417
+ + import type { TaggedLiteralCanonicalization } from '@internal/framework-components/authoring';
418
+ ```
419
+
420
+ `canonicalizeTaggedLiteralBody` now drops every blank line at the start and end of the body, not only the first and the last, so its result is always its own canonical text.
421
+
422
+ The same entry exports `printedTaggedLiteralReadsBack(text)`, which says whether a tagged literal printed with `text` reads back as the same text. `printSqlExpressionLiteral` from `@internal/sql-contract/sql-expression` now throws an internal error for a text that would read back as different text. Check the texts first with `sqlTextsReadBack(texts)` from the same module. When it returns `false`, `contract infer` skips an object named with `map:`, prints a wire-named index with `canonicalSqlText(text)` (whose result always reads back), and `contract print` refuses the object. Default expressions are not checked: they print with `printTaggedLiteral`, because defaults are compared with case and whitespace ignored.
423
+
424
+ ## Raw SQL in a TypeScript contract is a `sql` value
425
+
426
+ ### `builder-raw-sql-is-a-sql-value`
427
+
428
+ 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:
429
+
430
+ | Place | Before | After |
431
+ | --- | --- | --- |
432
+ | `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' })`` |
433
+ | `index` `expression` | `constraints.index({ expression: 'lower(email)', name: 'users_email_lower' })` | ``constraints.index({ expression: sql`lower(email)`, name: 'users_email_lower' })`` |
434
+ | `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' })`` |
435
+ | `check` `expression` | `check({ expression: 'total > 0', name: 'order_total_positive' })` | ``check({ expression: sql`total > 0`, name: 'order_total_positive' })`` |
436
+ | 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()` })`` |
437
+
438
+ ```ts
439
+ import { check, policySelect, sql } from '@internal/postgres/contract-builder';
440
+ ```
441
+
442
+ 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()`; ``.
443
+
444
+ 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.
445
+
446
+ 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.
447
+
448
+ 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`, … })``.
449
+
450
+ ### `sql-tag-interpolates-sql-values`
451
+
452
+ 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:
453
+
454
+ ```ts
455
+ const owner = sql`owner_id = current_setting('app.uid')::int`;
456
+ const ownerAlive = sql`${owner} AND deleted_at IS NULL`;
457
+ ```
458
+
459
+ 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.
460
+
461
+ ### `sql-tag-error-codes-renamed`
462
+
463
+ - `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.``
464
+ - 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`.
465
+ - `` 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.
466
+
467
+ 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.
468
+
469
+ ### `ts-storage-hash-may-change-once`
470
+
471
+ 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. Emit the contract again, and for a pack with migrations 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.
472
+
473
+ A contract built before this release can hold a column default whose text is not canonical. `contract infer` writes such a default with a note and `contract print` refuses it, for the reason ADR 268 gives ("Column defaults that do not read back"); the app instructions of this release say what to do.
474
+
475
+ ### `policy-handles-hold-sql-values`
476
+
477
+ The types behind the builder fields changed with them. In code that builds, reads or re-exports them:
478
+
479
+ - `RlsPolicyHandle.using` and `.withCheck` are `SqlExpression | undefined`. Every `using` and `withCheck` of `RlsUsingPolicyDescriptor`, `RlsWithCheckPolicyDescriptor` and both arms of `RlsUsingWithCheckPolicyDescriptor` is `SqlExpression`, required or optional as before. A function that takes a descriptor and passes its predicates on types them as `SqlExpression` too.
480
+ - `fullTextIndex`'s option `where` is `SqlExpression` and is passed to `IndexConstraint.where` unchanged.
481
+ - `IndexConstraint.where` is `SqlExpression`, `IndexExpressionInput` is `SqlExpression | DeferredIndexExpression`, and `AuthoredCheckConstraint.expression` is `SqlExpression`. `DeferredIndexExpression.render` still returns a string; lowering canonicalizes the text it returns, so a `render` that returns text that is not canonical stores different text once, as `ts-storage-hash-may-change-once` describes.
482
+ - Import the type with `import type { SqlExpression } from '@internal/sql-contract/sql-expression';`, or from `@internal/sql-contract-ts/contract-builder`, which re-exports it.
483
+ - Read the text of a value with `.text`. Code that lowers such a value from JavaScript that is not type-checked reads it with `requireSqlExpression(value, 'Policy "p" using').text` from `@internal/sql-contract/sql-expression`, which throws `CONTRACT.ARGUMENT_INVALID` for anything else. The Postgres target lowers a policy handle's predicates this way.
484
+
485
+ The definition tree (`IndexNode`, `CheckNode`), `buildSqlContractFromDefinition`, `PostgresRlsPolicy` and the emitted contract keep strings.
486
+
487
+ ### `sql-tag-lives-in-sql-contract`
488
+
489
+ `sql` is defined in `@internal/sql-contract/sql-expression`, with `SqlExpression`, `isSqlExpression`, `readSqlExpression` and `requireSqlExpression`. Read a value through `readSqlExpression` or `requireSqlExpression`, not by checking `isSqlExpression` and reading `.text`: a value made by another installed copy of the package passes `isSqlExpression`, and only those two functions canonicalize its text. `@internal/sql-contract-ts/contract-builder` re-exports `sql` and `type SqlExpression`; `packages/2-sql/2-authoring/contract-ts/src/sql-default-literal.ts` is deleted, so import from one of those modules. A contract-builder facade that re-exports `sql` also re-exports `type SqlExpression` beside it, and never the class as a value.
490
+
491
+ The tag returns a `SqlExpression`, whose `text` is the canonical text. Code that read `` sql`...`.expression `` or typed the result as `ColumnDefault` reads `.text`, or passes the value to `.default()`, which stores `{ kind: 'function', expression: value.text }`. `describeTaggedLiteralFailure` and `resolveTemplateTagEscapes` are also exported from `@internal/framework-components/authoring`.
492
+
493
+ This supersedes the sentence of the `sql-default-literal` extension instructions in the upgrade from 8.0.0-rc.11 to 8.0.0-rc.12 that says `` sql`now()` `` and `` sql`autoincrement()` `` are refused by the TypeScript `sql` tag: `.default()` refuses them now.
494
+
495
+ ## A full-text index is stored as data
496
+
497
+ ### `render-full-text-index-document-takes-a-definition`
498
+
499
+ The renderer now draws the whole search document of a full-text index, which may cover several columns in weight groups, and is named for it. Import `renderFullTextIndexDocument` instead and pass the definition the contract stores in the index's `options`:
500
+
501
+ ```diff
502
+ -import { renderFullTextIndexExpression } from '@internal/target-postgres/sql-utils';
503
+ +import { renderFullTextIndexDocument } from '@internal/target-postgres/sql-utils';
504
+
505
+ -renderFullTextIndexExpression('english', 'body')
506
+ +renderFullTextIndexDocument({ weightGroups: [['body']], language: 'english' })
507
+ ```
508
+
509
+ One field renders exactly what it rendered before, `to_tsvector('english', "body")`. A document of several columns wraps every column in `coalesce(column, '')`. A definition that breaks a rule of a full-text index (no group, more than four groups, an empty group, or a column named twice) is refused with `CONTRACT.INDEX_INVALID`.
510
+
511
+ A Postgres full-text index in a contract is now an index of type `fullText`, registered in the Postgres index type registry, with `columns` and `options: { weightGroups, language }`, not an expression. `columns` is exactly `weightGroups.flat()`; a contract where they differ is refused with `CONTRACT.INDEX_INVALID` when it is loaded, as is a full-text index over a column whose codec Postgres does not know to be `textual`. Code that reads indexes from a contract recognises a full-text index by `type === 'fullText'` (exported as `FULL_TEXT_INDEX_TYPE` from `@internal/target-postgres/full-text-index-authoring`), and must not treat its `columns` as a plain index over those columns. In the database it is a `gin` index over the rendered document: the `fullText` entry of the Postgres index type registry declares `accessMethod: 'gin'`.
512
+
513
+ ### `full-text-indexable-codec-check-replaced`
514
+
515
+ The rules of a full-text index, including that it covers text columns only, are now one function. Call it instead of testing each codec:
516
+
517
+ ```diff
518
+ -import { isFullTextIndexableCodec } from '@internal/target-postgres/sql-utils';
519
+ +import {
520
+ + fullTextIndexProblems,
521
+ + postgresCodecTraitsOf,
522
+ +} from '@internal/target-postgres/full-text-index-authoring';
523
+
524
+ -if (!isFullTextIndexableCodec(codecId)) refuse();
525
+ +const problems = fullTextIndexProblems({
526
+ + weightGroups: [[columnName]],
527
+ + codecs: { codecIdOf: () => codecId, traitsOf: postgresCodecTraitsOf },
528
+ +});
529
+ +if (problems.length > 0) refuse();
530
+ ```
531
+
532
+ To test one codec alone, `postgresCodecTraitsOf(codecId)?.includes('textual') === true` is what `isFullTextIndexableCodec(codecId)` returned.
533
+
534
+ ### `structured-index-option-values-hash-as-json`
535
+
536
+ `normalizeIndexOptionValue` from `@internal/sql-schema-ir/naming` writes an array or object value as JSON, with object keys sorted at every depth, so `[['a', 'b']]` and `[['a'], ['b']]` no longer hash to the same index name and the order in which an object's keys were written does not change it. Scalar values hash as before. If an extension's contract declares a wire-named index whose options hold an array or an object, rebuild its contract space so the emitted index name and storage hash move together.
537
+
538
+ ## SQL ORM collection state carries its tables
539
+
540
+ These changes concern code that imports from `@prisma/orm-family-sql/orm-client` and builds SQL ORM collection state or model accessors by hand.
541
+
542
+ ### `collection-state-carries-tables`
543
+
544
+ `CollectionState.tables` is a `CollectionTables` value: the table scope plus the aliased root table and each aliased multi-table-inheritance variant table. `emptyState` takes it as a required argument. Build it from the contract with `createCollectionTables`:
545
+
546
+ ```ts
547
+ // before
548
+ import { emptyState } from '@prisma/orm-family-sql/orm-client';
549
+
550
+ const state = { ...emptyState(), limit: 10 };
551
+
552
+ // after
553
+ import { createCollectionTables, emptyState } from '@prisma/orm-family-sql/orm-client';
554
+
555
+ const tables = createCollectionTables(contract, 'public', 'User');
556
+ const state = { ...emptyState(tables), limit: 10 };
557
+ ```
558
+
559
+ `createCollectionTables(contract, namespaceId, modelName)` takes the model the state is for. For a polymorphic model pass the base model name; its variant tables are aliased in the same scope.
560
+
561
+ Every object typed `CollectionState` needs the property, not only top-level states:
562
+
563
+ - `IncludeExpr.nested`;
564
+ - the `state` of a `combine()` rows branch;
565
+ - the `state` of an include scalar selector.
566
+
567
+ A state passed to `new Collection(ctx, modelName, { state })` needs it too. A `Collection` constructed without `state` builds its own tables, so that call needs no change.
568
+
569
+ Do not build a `CollectionTables` or a table scope by hand.
570
+
571
+ A state built by hand for an include child must not reuse the parent's table aliases. Prefer building includes through `collection.include(...)` and reading `collection.state`, which aliases the child's tables from the parent's scope. Where a child state is built by hand for a relation whose target table already appears in the parent chain (a self-relation, or an include that returns to an ancestor's table), build it through `include()` instead; `createCollectionTables` starts a fresh scope and would give the child the same alias as its parent.
572
+
573
+ ### `state-filters-planned-as-written`
574
+
575
+ This affects only code that writes expressions into `CollectionState.filters` itself (a hand-built state, an `IncludeExpr.nested` state, a `combine()` branch state). Such a filter used to have each literal compared with a column replaced by a parameter carrying the column's codec when the statement was planned. The planners now use stored filters unchanged.
576
+
577
+ Build the filter through the collection, which binds it when `where()` is called, and read the state from the result:
578
+
579
+ ```ts
580
+ // before
581
+ const state = {
582
+ ...emptyState(),
583
+ filters: [BinaryExpr.eq(ColumnRef.of('users', 'email'), LiteralExpr.of(email))],
584
+ };
585
+
586
+ // after
587
+ const state = users.where(BinaryExpr.eq(ColumnRef.of('users', 'email'), LiteralExpr.of(email))).state;
588
+ ```
589
+
590
+ A filter that must be written by hand needs a `ParamRef` with the column's codec in place of the literal. A literal left in place is not encoded by the column's codec.
591
+
592
+ ### `create-model-accessor-takes-tables`
593
+
594
+ Pass the tables as the fourth argument. A variant moves to the fifth, named by its discriminator value:
595
+
596
+ ```ts
597
+ // before
598
+ createModelAccessor(context, 'public', 'User');
599
+ createModelAccessor(context, 'public', 'Task', 'Feature');
600
+
601
+ // after
602
+ createModelAccessor(context, 'public', 'User', createCollectionTables(context.contract, 'public', 'User'));
603
+ createModelAccessor(
604
+ context,
605
+ 'public',
606
+ 'Task',
607
+ createCollectionTables(context.contract, 'public', 'Task'),
608
+ 'feature',
609
+ );
610
+ ```
611
+
612
+ The accessor takes the aliases for its relation filters and relation orders from `tables.scope` and records them there. Two relation filters over the same table built from one accessor therefore get different aliases. Pass each accessor its own `createCollectionTables(...)` result unless the expressions it builds are meant for one statement.
613
+
614
+ ### `include-expr-junction-table`
615
+
616
+ This affects only an `IncludeExpr` object written out by hand with a `through` descriptor. An include built with `collection.include('<relation>')` carries the aliased junction table already.
617
+
618
+ There is no public function that aliases a junction table on its own. Replace the hand-built object with the include the collection builds:
619
+
620
+ ```ts
621
+ // before
622
+ const include: IncludeExpr = { relationName: 'tags', through: { table: 'user_tags', /* … */ }, /* … */ };
623
+
624
+ // after
625
+ const [include] = users.include('tags').state.includes;
626
+ ```
627
+
628
+ `IncludeExpr` no longer has `localTableName`. Remove the property where the relation's local columns are on the parent model's own table, which is every relation of a non-polymorphic model, a base model or a single-table-inheritance variant. Where they are on a multi-table-inheritance variant's table, set `localVariantValue` to that variant's discriminator value:
629
+
630
+ ```ts
631
+ // before
632
+ const include: IncludeExpr = { relationName: 'assignee', localTableName: 'features', /* … */ };
633
+
634
+ // after
635
+ const include: IncludeExpr = { relationName: 'assignee', localVariantValue: 'feature', /* … */ };
636
+ ```
637
+
638
+ ## A variant is named by its discriminator value
639
+
640
+ These changes concern code that reads `variants` from a contract, or that imports from `@prisma/orm-family-sql/orm-client` or `@prisma/orm-family-mongo/orm`. A contract an extension ships must be re-emitted: see the app instructions' `re-emit-contracts-with-polymorphic-models`.
641
+
642
+ ### `read-variants-by-discriminator-value`
643
+
644
+ ```ts
645
+ // before
646
+ for (const [variantName, { value }] of Object.entries(model.variants ?? {})) {
647
+ const variant = models[variantName];
648
+ }
649
+
650
+ // after
651
+ for (const [value, variantRef] of Object.entries(model.variants ?? {})) {
652
+ const variant = contract.domain.namespaces[variantRef.namespace]?.models[variantRef.model];
653
+ }
654
+ ```
655
+
656
+ Look the variant model up in the namespace its reference names, not in the base's namespace. A discriminator value is unique in a hierarchy; a variant model name is not, because two namespaces may each have a variant model of the same name.
657
+
658
+ Replace the type `ContractVariantEntry` with `CrossReference`, exported from the same module.
659
+
660
+ ### `sql-orm-state-names-a-variant-by-value`
661
+
662
+ Rename the properties, and where a value is written or compared, use the discriminator value the variant declares in place of its model name:
663
+
664
+ ```ts
665
+ // before
666
+ const narrowed = { ...state, variantName: 'Feature' };
667
+
668
+ // after, for @@base(Task, "feature")
669
+ const narrowed = { ...state, variantValue: 'feature' };
670
+ const featureTable = state.tables.variants.get('feature');
671
+ ```
672
+
673
+ A type that reads the selected variant from a collection's type state reads `variantValue`, for example `CollectionTypeStateOf<C>['variantValue']`.
674
+
675
+ ### `orm-variant-types-take-a-value`
676
+
677
+ These types are exported from `@prisma/orm-family-mongo/orm`. Pass the discriminator value where a variant model name was passed:
678
+
679
+ ```ts
680
+ // before
681
+ type BugRow = VariantModelRow<Contract, 'Task', 'Bug'>;
682
+ type BugInput = VariantCreateInput<Contract, 'Task', 'Bug'>;
683
+ type Bugs = MongoCollection<Contract, 'Task', NoIncludes, 'Bug'>;
684
+
685
+ // after, for a Bug variant declaring "bug"
686
+ type BugRow = VariantModelRow<Contract, 'Task', 'bug'>;
687
+ type BugInput = VariantCreateInput<Contract, 'Task', 'bug'>;
688
+ type Bugs = MongoCollection<Contract, 'Task', NoIncludes, 'bug'>;
689
+ ```
690
+
691
+ `VariantNameForValue<C, M, V>` mapped a value to a model name for these helpers; pass the value itself. Replace `VariantNames<C, M>` with `DiscriminatorValues<C, M>` where the code lists what `variant()` accepts.
692
+
693
+ ## The SQL ORM client accepts only field names
694
+
695
+ 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.
696
+
697
+ ```diff
698
+ - await db.orm.Post.where({ user_id: 1 }).all();
699
+ + await db.orm.Post.where({ userId: 1 }).all();
700
+ ```
701
+
702
+ 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.
703
+
704
+ ```diff
705
+ - if (error.code === 'ORM.ARGUMENT_INVALID') {
706
+ + if (error.code === 'ORM.FIELD_UNKNOWN') {
707
+ ```
708
+
709
+ Leave other checks of `ORM.ARGUMENT_INVALID` alone; only the `conflictOn` refusal changed code.
710
+
711
+ ## `fragment` is called on the model's root collection
712
+
713
+ 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.
714
+
715
+ In the extension's code, chain on the collection directly instead of defining a fragment from it. Inside a class method, chain on `this`:
716
+
717
+ ```diff
718
+ titles() {
719
+ - return this.with(this.fragment((posts) => posts.select('id', 'title')));
720
+ + return this.select('id', 'title');
721
+ }
722
+ ```
723
+
724
+ 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:
725
+
726
+ ```diff
727
+ - const recentSummary = db.orm.public.Post.orderBy((p) => p.id.desc()).fragment((posts) =>
728
+ - posts.select('id', 'title'),
729
+ - );
730
+ + const recentSummary = db.orm.public.Post.fragment((posts) =>
731
+ + posts.orderBy((p) => p.id.desc()).select('id', 'title'),
732
+ + );
733
+ ```
734
+
735
+ 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.
736
+
737
+ 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.
738
+
739
+ ## `Fragment` is now `QueryFragment`, and `FieldFragment` is now `DeclaredFieldsFragment`
740
+
741
+ 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.
742
+
743
+ These changes apply to the SQL ORM client only. The MongoDB ORM client did not change; skip matches in code that uses it.
744
+
745
+ Rename `Fragment` to `QueryFragment` and `FieldFragment` to `DeclaredFieldsFragment`. These are the names 8.0.0-rc.17 exports.
746
+
747
+ ```diff
748
+ - import type { FieldFragment, Fragment } from '@prisma/orm-postgres/orm-client';
749
+ + import type { DeclaredFieldsFragment, QueryFragment } from '@prisma/orm-postgres/orm-client';
750
+
751
+ - const newest: Fragment<PostCollection, Ordered<PostCollection>> = (posts) => posts.newestFirst();
752
+ + const newest: QueryFragment<PostCollection, Ordered<PostCollection>> = (posts) => posts.newestFirst();
753
+ ```
754
+
755
+ 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`.
756
+
757
+ Rename every use of these types in the extension's code and in the declarations it exports, 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.
758
+
759
+ Do not rename React's `Fragment`, the SQL builder's `Scope` and `ScopeField` (imported from a `builder` entry or `@internal/sql-builder`), or any `Fragment` or `Scope` that is not imported from the ORM client. Do not rename `FragmentFacts` or `DeclaredField`, which keep their names.
760
+
761
+ ## `unique-criterion-rejects-null`
762
+
763
+ `UniqueConstraintCriterion<TContract, ModelName>`, exported by the SQL ORM client (`@prisma/orm-postgres/orm-client`, the other facades' `orm-client` entries and `@internal/sql-orm-client`), 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`.
764
+
765
+ The type is the argument of three calls, and each of them now refuses `null` and a value whose type includes `null`:
766
+
767
+ - `conflictOn` in `collection.upsert({ create, update, conflictOn })`
768
+ - `mutator.connect(criterion)` and `mutator.connect([criterion, ...])` in a relation callback of `create` or `update`
769
+ - `mutator.disconnect([criterion, ...])` in a relation callback of `update`
770
+
771
+ Only a model with a nullable unique column is affected. Given `Account.handle`, a nullable column with a unique constraint, these no longer compile:
772
+
773
+ ```ts
774
+ await db.orm.Account.upsert({ create, update, conflictOn: { handle: null } });
775
+ await db.orm.Account.upsert({ create, update, conflictOn: { handle: maybeHandle } }); // maybeHandle: string | null
776
+
777
+ await db.orm.Owner.where({ id }).update({
778
+ accounts: (accounts) => accounts.connect({ handle: maybeHandle }),
779
+ });
780
+ ```
781
+
782
+ The compiler reports `Type 'null' is not assignable to type 'string'` on the field.
783
+
784
+ 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']`.
785
+
786
+ What to write instead:
787
+
788
+ - Where the value is known to be present, narrow it before the call, so that its type has no `null`:
789
+
790
+ ```ts
791
+ if (maybeHandle === null) {
792
+ throw new Error('an account needs a handle to be connected');
793
+ }
794
+ await db.orm.Owner.where({ id }).update({
795
+ accounts: (accounts) => accounts.connect({ handle: maybeHandle }),
796
+ });
797
+ ```
798
+
799
+ - 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.
800
+
801
+ - To read or write the rows whose column is `NULL`, use `where({ handle: null })`. It can match more than one row.
802
+
803
+ - 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.
804
+
805
+ ## `prepared-all-and-aggregate-need-their-receiver`
806
+
807
+ `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:
808
+
809
+ ```ts
810
+ const { all, aggregate } = db.orm.Post.prepared;
811
+ all(); // error: The 'this' context of type 'void' is not assignable to method's 'this'
812
+ aggregate((a) => ({ posts: a.count() })); // the same error
813
+ ```
814
+
815
+ Call the method on the `prepared` object instead:
816
+
817
+ ```ts
818
+ const prepared = db.orm.Post.prepared;
819
+ prepared.all();
820
+ prepared.aggregate((a) => ({ posts: a.count() }));
821
+ ```
822
+
823
+ 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.
824
+
825
+ ## `build-native-enums-takes-the-domain`
826
+
827
+ Pass the contract's domain beside its storage:
828
+
829
+ ```ts
830
+ // before
831
+ buildNamespacedNativeEnums(contract.storage);
832
+ // after
833
+ buildNamespacedNativeEnums(contract.storage, contract.domain);
834
+ ```
835
+
836
+ The result no longer has an accessor for a domain enum (an `enum` block or `enumType()`), only for native enum types. The type `NamespacedNativeEnums<Contract>` leaves out the same names. Code that read a domain enum from the result reads it from the accessor `db.enums` uses instead.
837
+
838
+ ## Middleware hooks are activity hooks
839
+
840
+ 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.
841
+
842
+ ### Rename the hooks
843
+
844
+ | Before | After |
845
+ |---|---|
846
+ | `beforeQuery`, `beforeExecute` | `beforeEncode` |
847
+ | `interceptQuery`, `interceptExecute` | `intercept.onQuery`, `intercept.onExecute` |
848
+ | `onRow` | `onRow` |
849
+ | `afterQuery`, `afterExecute` | `activityEnded` on a statement |
850
+ | `afterTransaction` | `activityFinalized` on a statement |
851
+ | `beforeCompile` | `beforeCompile` |
852
+
853
+ `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`.
854
+
855
+ The interceptors move under one `intercept` object:
856
+
857
+ ```diff
858
+ - async interceptQuery(plan, ctx) {
859
+ - return { rows: cached };
860
+ - },
861
+ + intercept: {
862
+ + async onQuery(plan, ctx) {
863
+ + return { rows: cached };
864
+ + },
865
+ + },
866
+ ```
867
+
868
+ ### Read the result from `activityEnded`
869
+
870
+ `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`:
871
+
872
+ ```diff
873
+ - async afterQuery(plan, result, ctx) {
874
+ - if (result.latencyMs > 250) ctx.log.warn({ sql: plan.sql, rowCount: result.rowCount });
875
+ - },
876
+ + async beforeEncode(plan, ctx) {
877
+ + ctx.state(statementSql)['sql'] = plan.sql;
878
+ + },
879
+ + async activityEnded(activity, result, ctx) {
880
+ + if (result.kind !== 'statement' || result.latencyMs <= 250) return;
881
+ + ctx.log.warn({
882
+ + sql: ctx.state(statementSql)['sql'],
883
+ + ...(result.returns === 'rows' ? { rowCount: result.rowCount } : {}),
884
+ + });
885
+ + },
886
+ ```
887
+
888
+ `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.
889
+
890
+ ### Replace `afterTransaction` with `activityFinalized`
891
+
892
+ `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.
893
+
894
+ ### Rename the context type and the execution id
895
+
896
+ `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`.
897
+
898
+ ### Abort phases and already-aborted calls
899
+
900
+ 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`.
901
+
902
+ ### The observed-row budget now runs
903
+
904
+ `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.
905
+
906
+ ### Move per-statement state from a `WeakMap` to `ctx.state`
907
+
908
+ A middleware that kept per-statement state in a `WeakMap` keyed by the plan keeps it in `ctx.state` instead. The state object's values are `unknown`, so read them back through a type guard, and delete the entry in `activityEnded` once it is used:
909
+
910
+ ```diff
911
+ - const pending = new WeakMap<object, PendingMiss>();
912
+ + const pendingMiss = Symbol('cache.pendingMiss');
913
+ +
914
+ + function pendingOf(ctx: ActivityContext): PendingMiss | undefined {
915
+ + const miss = ctx.state(pendingMiss)['miss'];
916
+ + return isPendingMiss(miss) ? miss : undefined;
917
+ + }
918
+
919
+ // in intercept.onQuery, on a miss
920
+ - pending.set(exec, { entry, buffer: [] });
921
+ + ctx.state(pendingMiss)['miss'] = { entry, buffer: [] };
922
+
923
+ // in onRow
924
+ - pending.get(exec)?.buffer.push(row);
925
+ + pendingOf(ctx)?.buffer.push(row);
926
+
927
+ // in activityEnded, which replaces afterQuery
928
+ - const miss = pending.get(exec);
929
+ - if (miss === undefined) return;
930
+ - pending.delete(exec);
931
+ - if (!result.completed || result.source !== 'driver') return;
932
+ + const miss = pendingOf(ctx);
933
+ + if (miss === undefined) return;
934
+ + delete ctx.state(pendingMiss)['miss'];
935
+ + if (result.kind !== 'statement' || result.outcome !== 'succeeded' || result.source !== 'driver') return;
936
+ ```
937
+
938
+ `pendingMiss` and `pendingOf` are module-level; `isPendingMiss` is a type guard for the state's shape. The middleware object then lists `intercept: { onQuery }`, `onRow` and `activityEnded` in place of `interceptQuery`, `onRow` and `afterQuery`.
939
+
940
+ ### Run statements through the statement runners
941
+
942
+ A family runtime no longer composes the middleware runners itself. It runs each statement through one call that starts the activity, runs a body, and ends and finalizes the activity:
943
+
944
+ ```diff
945
+ - await runBeforeQueryChain(exec, this.middleware, ctx, mutator);
946
+ - yield* reportQueryEnding(onQueryEnd, () =>
947
+ - runQueryWithMiddleware(exec, this.middleware, ctx, () => this.runDriver(exec)),
948
+ - );
949
+ + yield* streamStatement(
950
+ + { middleware: this.middleware, plan, context: contextFields, parent: undefined },
951
+ + async function* (run) {
952
+ + const exec = await prepare(run); // calls run.beforeEncode(draft, mutator) before encoding
953
+ + yield* run.runQuery(exec, () => self.runDriver(exec));
954
+ + },
955
+ + );
956
+ ```
957
+
958
+ `executeStatement` is the same for `execute()`, with `run.runExecute(exec, runDriver)`. `context` is an `ActivityContextFields`: the runtime-wide fields (`contract`, `mode`, `now`, `log`, `contentHash`, `scope`, and `signal` when the caller passed one); the runner adds `parent`, `state` and `inTransaction`. `parent` is the open transaction a statement was sent on, or `undefined`. `RuntimeCoreOptions.ctx` takes the same fields, so remove `planExecutionId` from it.
959
+
960
+ A runtime that opens transactions calls `startTransactionActivity({ middleware, context, holdFinishedChildren })` before `BEGIN`, where `holdFinishedChildren` is decided once, when the runtime is created: `true` only when some middleware implements `activityFinalized`. It passes the result as the `parent` of every statement sent on the transaction or its connection while it is open, and calls `end({ by: 'commit' | 'rollback', outcome, ... })` when `commit()` or `rollback()` settles. A SQL runtime subclass that wrapped a driver transaction with `wrapTransaction(driverTx, connection)` calls `beginTransactionOn(connection, owner)` instead, which does all of this.
961
+
962
+ ### Renamed log events
963
+
964
+ A test or log filter that matches the runtime's debug event `middleware.interceptQuery` or `middleware.interceptExecute` matches `middleware.intercept.onQuery` or `middleware.intercept.onExecute`. The error event `middleware.afterTransaction.error` is now `middleware.activityFinalized.error`.
965
+
966
+ ## MongoDB carries out `--rename` and `--delete`
967
+
968
+ ### `rename-statements-capability-removed`
969
+
970
+ Delete `renameStatements` from a target's `migrations` capability, and any `keepDataByHand` function it pointed at. A test double of the capability that set it drops the member. A data-loss question now offers `rename` for a model or field the destination no longer has on every target; write a planner that carries out the resolved statements it is given, or refuses each with a `statementRefused` conflict that carries it in `refusedStatement`.
971
+
972
+ ### `mongo-ddl-rename-collection`
973
+
974
+ The DDL command alphabet gains a collection rename:
975
+
976
+ ```ts
977
+ import { RenameCollectionCommand } from '@prisma/orm-family-mongo/query-ast/control';
978
+
979
+ new RenameCollectionCommand('profiles', 'users');
980
+ ```
981
+
982
+ A `MongoDdlCommandVisitor<R>` implementation adds `renameCollection(command)`. A Mongo driver's `run(wireCommand)` handles `kind: 'renameCollection'`, as the shipped driver does with `db.renameCollection(collection, to)`, without `dropTarget`. An adapter's DDL lowering maps the command to `new RenameCollectionWireCommand(command.collection, command.to)`.
983
+
984
+ ### `mongo-dml-operations`
985
+
986
+ Rename the types where code names them:
987
+
988
+ ```diff
989
+ - import type { MongoDataTransformCheck, MongoDataTransformOperation } from '@prisma/orm-family-mongo/query-ast/control';
990
+ + import type { MongoDmlCheck, MongoDmlOperation } from '@prisma/orm-family-mongo/query-ast/control';
991
+ ```
992
+
993
+ Where code narrows `AnyMongoMigrationOperation` by `operation.operationClass === 'data'`, use the guard instead:
994
+
995
+ ```diff
996
+ - if (operation.operationClass === 'data') {
997
+ + if (isMongoDmlOperation(operation)) {
998
+ ```
999
+
1000
+ `isMongoDmlOperation` comes from `@prisma/orm-family-mongo/query-ast/control`. An operation without `run` is a DDL operation. The `dataTransform` factory keeps its name: it builds a DML operation with class `data`. A hand-written `dataTransform` still has class `data`, so a policy that leaves out `data` (as `db update`'s does) still refuses it, and runs `renameField` and `unsetField`.
1001
+
1002
+ ### `mongo-update-many-bypass-validation`
1003
+
1004
+ A hand-built lowered draft of kind `rawUpdateMany` adds `bypassDocumentValidation: false`. A driver's `updateMany` passes `bypassDocumentValidation: true` to MongoDB when the wire command carries it.
1005
+
1006
+ ### `mongo-op-factory-call-to-op`
1007
+
1008
+ Code typed against the result of `toOp()` or `renderOps(...)` as `MongoMigrationPlanOperation` widens it to `AnyMongoMigrationOperation`, and narrows with `isMongoDmlOperation` where it needs `execute` or `run`.