@prisma/client-engine-runtime 7.5.0-dev.3 → 7.5.0-dev.31

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/dist/events.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type QueryEvent = {
2
2
  timestamp: Date;
3
3
  query: string;
4
- params: unknown[];
4
+ params: readonly unknown[];
5
5
  duration: number;
6
6
  };
package/dist/index.d.mts CHANGED
@@ -80,7 +80,11 @@ export declare type DecimalTaggedValue = {
80
80
  value: string;
81
81
  };
82
82
 
83
- export declare function deserializeJsonResponse(result: unknown): unknown;
83
+ declare type DeepReadonly<T> = T extends undefined | null | boolean | string | number | symbol | Function | Date ? T : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : unknown extends T ? unknown : {
84
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
85
+ };
86
+
87
+ export declare function deserializeJsonObject(result: unknown): unknown;
84
88
 
85
89
  /**
86
90
  * Checks if two objects representing the names and values of key columns match. A match is
@@ -155,6 +159,9 @@ export declare type Fragment = {
155
159
  type: 'parameter';
156
160
  } | {
157
161
  type: 'parameterTuple';
162
+ itemPrefix: string;
163
+ itemSeparator: string;
164
+ itemSuffix: string;
158
165
  } | {
159
166
  type: 'parameterTupleList';
160
167
  itemPrefix: string;
@@ -187,7 +194,7 @@ export declare type JoinExpression = {
187
194
  isRelationUnique: boolean;
188
195
  };
189
196
 
190
- export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue;
197
+ export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue | RawTaggedValue;
191
198
 
192
199
  export declare type JsonOutputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | JsonTaggedValue;
193
200
 
@@ -243,7 +250,7 @@ export declare type PrismaValuePlaceholder = {
243
250
  export declare type QueryEvent = {
244
251
  timestamp: Date;
245
252
  query: string;
246
- params: unknown[];
253
+ params: readonly unknown[];
247
254
  duration: number;
248
255
  };
249
256
 
@@ -256,7 +263,7 @@ export declare class QueryInterpreter {
256
263
  provider?: SchemaProvider;
257
264
  connectionInfo?: ConnectionInfo;
258
265
  }): QueryInterpreter;
259
- run(queryPlan: QueryPlanNode, options: QueryRuntimeOptions): Promise<unknown>;
266
+ run(queryPlan: DeepReadonly<QueryPlanNode>, options: QueryRuntimeOptions): Promise<unknown>;
260
267
  private interpretNode;
261
268
  }
262
269
 
@@ -348,6 +355,7 @@ export declare type QueryPlanNode = {
348
355
  args: {
349
356
  parent: QueryPlanNode;
350
357
  children: JoinExpression[];
358
+ canAssumeStrictEquality: boolean;
351
359
  };
352
360
  } | {
353
361
  type: 'mapField';
@@ -421,6 +429,11 @@ export declare type RawResponse = {
421
429
  rows: unknown[][];
422
430
  };
423
431
 
432
+ export declare type RawTaggedValue = {
433
+ $type: 'Raw';
434
+ value: unknown;
435
+ };
436
+
424
437
  export declare type ResultNode = {
425
438
  type: 'affectedRows';
426
439
  } | {
@@ -492,6 +505,7 @@ export declare type TransactionOptions = {
492
505
  maxWait?: number;
493
506
  timeout?: number;
494
507
  isolationLevel?: IsolationLevel;
508
+ newTxId?: string;
495
509
  };
496
510
 
497
511
  export declare class UserFacingError extends Error {
@@ -511,14 +525,14 @@ export declare class UserFacingError extends Error {
511
525
  }
512
526
 
513
527
  export declare type ValidationError = {
514
- error_identifier: 'RELATION_VIOLATION';
528
+ errorIdentifier: 'RELATION_VIOLATION';
515
529
  context: {
516
530
  relation: string;
517
531
  modelA: string;
518
532
  modelB: string;
519
533
  };
520
534
  } | {
521
- error_identifier: 'MISSING_RELATED_RECORD';
535
+ errorIdentifier: 'MISSING_RELATED_RECORD';
522
536
  context: {
523
537
  model: string;
524
538
  relation: string;
@@ -527,24 +541,24 @@ export declare type ValidationError = {
527
541
  neededFor?: string;
528
542
  };
529
543
  } | {
530
- error_identifier: 'MISSING_RECORD';
544
+ errorIdentifier: 'MISSING_RECORD';
531
545
  context: {
532
546
  operation: string;
533
547
  };
534
548
  } | {
535
- error_identifier: 'INCOMPLETE_CONNECT_INPUT';
549
+ errorIdentifier: 'INCOMPLETE_CONNECT_INPUT';
536
550
  context: {
537
551
  expectedRows: number;
538
552
  };
539
553
  } | {
540
- error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
554
+ errorIdentifier: 'INCOMPLETE_CONNECT_OUTPUT';
541
555
  context: {
542
556
  expectedRows: number;
543
557
  relation: string;
544
558
  relationType: string;
545
559
  };
546
560
  } | {
547
- error_identifier: 'RECORDS_NOT_CONNECTED';
561
+ errorIdentifier: 'RECORDS_NOT_CONNECTED';
548
562
  context: {
549
563
  relation: string;
550
564
  parent: string;
package/dist/index.d.ts CHANGED
@@ -80,7 +80,11 @@ export declare type DecimalTaggedValue = {
80
80
  value: string;
81
81
  };
82
82
 
83
- export declare function deserializeJsonResponse(result: unknown): unknown;
83
+ declare type DeepReadonly<T> = T extends undefined | null | boolean | string | number | symbol | Function | Date ? T : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : unknown extends T ? unknown : {
84
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
85
+ };
86
+
87
+ export declare function deserializeJsonObject(result: unknown): unknown;
84
88
 
85
89
  /**
86
90
  * Checks if two objects representing the names and values of key columns match. A match is
@@ -155,6 +159,9 @@ export declare type Fragment = {
155
159
  type: 'parameter';
156
160
  } | {
157
161
  type: 'parameterTuple';
162
+ itemPrefix: string;
163
+ itemSeparator: string;
164
+ itemSuffix: string;
158
165
  } | {
159
166
  type: 'parameterTupleList';
160
167
  itemPrefix: string;
@@ -187,7 +194,7 @@ export declare type JoinExpression = {
187
194
  isRelationUnique: boolean;
188
195
  };
189
196
 
190
- export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue;
197
+ export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue | RawTaggedValue;
191
198
 
192
199
  export declare type JsonOutputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | JsonTaggedValue;
193
200
 
@@ -243,7 +250,7 @@ export declare type PrismaValuePlaceholder = {
243
250
  export declare type QueryEvent = {
244
251
  timestamp: Date;
245
252
  query: string;
246
- params: unknown[];
253
+ params: readonly unknown[];
247
254
  duration: number;
248
255
  };
249
256
 
@@ -256,7 +263,7 @@ export declare class QueryInterpreter {
256
263
  provider?: SchemaProvider;
257
264
  connectionInfo?: ConnectionInfo;
258
265
  }): QueryInterpreter;
259
- run(queryPlan: QueryPlanNode, options: QueryRuntimeOptions): Promise<unknown>;
266
+ run(queryPlan: DeepReadonly<QueryPlanNode>, options: QueryRuntimeOptions): Promise<unknown>;
260
267
  private interpretNode;
261
268
  }
262
269
 
@@ -348,6 +355,7 @@ export declare type QueryPlanNode = {
348
355
  args: {
349
356
  parent: QueryPlanNode;
350
357
  children: JoinExpression[];
358
+ canAssumeStrictEquality: boolean;
351
359
  };
352
360
  } | {
353
361
  type: 'mapField';
@@ -421,6 +429,11 @@ export declare type RawResponse = {
421
429
  rows: unknown[][];
422
430
  };
423
431
 
432
+ export declare type RawTaggedValue = {
433
+ $type: 'Raw';
434
+ value: unknown;
435
+ };
436
+
424
437
  export declare type ResultNode = {
425
438
  type: 'affectedRows';
426
439
  } | {
@@ -492,6 +505,7 @@ export declare type TransactionOptions = {
492
505
  maxWait?: number;
493
506
  timeout?: number;
494
507
  isolationLevel?: IsolationLevel;
508
+ newTxId?: string;
495
509
  };
496
510
 
497
511
  export declare class UserFacingError extends Error {
@@ -511,14 +525,14 @@ export declare class UserFacingError extends Error {
511
525
  }
512
526
 
513
527
  export declare type ValidationError = {
514
- error_identifier: 'RELATION_VIOLATION';
528
+ errorIdentifier: 'RELATION_VIOLATION';
515
529
  context: {
516
530
  relation: string;
517
531
  modelA: string;
518
532
  modelB: string;
519
533
  };
520
534
  } | {
521
- error_identifier: 'MISSING_RELATED_RECORD';
535
+ errorIdentifier: 'MISSING_RELATED_RECORD';
522
536
  context: {
523
537
  model: string;
524
538
  relation: string;
@@ -527,24 +541,24 @@ export declare type ValidationError = {
527
541
  neededFor?: string;
528
542
  };
529
543
  } | {
530
- error_identifier: 'MISSING_RECORD';
544
+ errorIdentifier: 'MISSING_RECORD';
531
545
  context: {
532
546
  operation: string;
533
547
  };
534
548
  } | {
535
- error_identifier: 'INCOMPLETE_CONNECT_INPUT';
549
+ errorIdentifier: 'INCOMPLETE_CONNECT_INPUT';
536
550
  context: {
537
551
  expectedRows: number;
538
552
  };
539
553
  } | {
540
- error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
554
+ errorIdentifier: 'INCOMPLETE_CONNECT_OUTPUT';
541
555
  context: {
542
556
  expectedRows: number;
543
557
  relation: string;
544
558
  relationType: string;
545
559
  };
546
560
  } | {
547
- error_identifier: 'RECORDS_NOT_CONNECTED';
561
+ errorIdentifier: 'RECORDS_NOT_CONNECTED';
548
562
  context: {
549
563
  relation: string;
550
564
  parent: string;
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ __export(index_exports, {
37
37
  UserFacingError: () => UserFacingError,
38
38
  applySqlCommenters: () => applySqlCommenters,
39
39
  convertCompactedRows: () => convertCompactedRows,
40
- deserializeJsonResponse: () => deserializeJsonResponse,
40
+ deserializeJsonObject: () => deserializeJsonObject,
41
41
  doKeysMatch: () => doKeysMatch,
42
42
  isDeepStrictEqual: () => isDeepStrictEqual,
43
43
  isPrismaValueGenerator: () => isPrismaValueGenerator,
@@ -170,7 +170,10 @@ function normalizeJsonProtocolValues(result) {
170
170
  function isTaggedValue(value) {
171
171
  return value !== null && typeof value == "object" && typeof value["$type"] === "string";
172
172
  }
173
- function normalizeTaggedValue({ $type, value }) {
173
+ function normalizeTaggedValue({
174
+ $type,
175
+ value
176
+ }) {
174
177
  switch ($type) {
175
178
  case "BigInt":
176
179
  return { $type, value: String(value) };
@@ -182,6 +185,12 @@ function normalizeTaggedValue({ $type, value }) {
182
185
  return { $type, value: String(new import_client_runtime_utils2.Decimal(value)) };
183
186
  case "Json":
184
187
  return { $type, value: JSON.stringify(JSON.parse(value)) };
188
+ case "Raw":
189
+ return { $type, value };
190
+ case "FieldRef":
191
+ return { $type, value };
192
+ case "Enum":
193
+ return { $type, value };
185
194
  default:
186
195
  assertNever(value, "Unknown tagged value");
187
196
  }
@@ -193,12 +202,12 @@ function mapObjectValues(object, mapper) {
193
202
  }
194
203
  return result;
195
204
  }
196
- function deserializeJsonResponse(result) {
205
+ function deserializeJsonObject(result) {
197
206
  if (result === null) {
198
207
  return result;
199
208
  }
200
209
  if (Array.isArray(result)) {
201
- return result.map(deserializeJsonResponse);
210
+ return result.map(deserializeJsonObject);
202
211
  }
203
212
  if (typeof result === "object") {
204
213
  if (isTaggedValue(result)) {
@@ -207,7 +216,7 @@ function deserializeJsonResponse(result) {
207
216
  if (result.constructor !== null && result.constructor.name !== "Object") {
208
217
  return result;
209
218
  }
210
- return mapObjectValues(result, deserializeJsonResponse);
219
+ return mapObjectValues(result, deserializeJsonObject);
211
220
  }
212
221
  return result;
213
222
  }
@@ -225,6 +234,12 @@ function deserializeTaggedValue({ $type, value }) {
225
234
  return new import_client_runtime_utils2.Decimal(value);
226
235
  case "Json":
227
236
  return JSON.parse(value);
237
+ case "Raw":
238
+ return value;
239
+ case "FieldRef":
240
+ throw new Error("FieldRef tagged values cannot be deserialized to JavaScript values");
241
+ case "Enum":
242
+ return value;
228
243
  default:
229
244
  assertNever(value, "Unknown tagged value");
230
245
  }
@@ -456,7 +471,7 @@ function resolveArgPlaceholders(args, placeholderValues) {
456
471
  function convertCompactedRows(rows, compiledBatch, placeholderValues = {}) {
457
472
  const keysPerRow = rows.map(
458
473
  (item) => compiledBatch.keys.reduce((acc, key) => {
459
- acc[key] = deserializeJsonResponse(item[key]);
474
+ acc[key] = deserializeJsonObject(item[key]);
460
475
  return acc;
461
476
  }, {})
462
477
  );
@@ -742,6 +757,9 @@ function normalizeDateTime(dt) {
742
757
  return dtWithTz;
743
758
  }
744
759
 
760
+ // src/interpreter/query-interpreter.ts
761
+ var import_klona2 = require("klona");
762
+
745
763
  // src/sql-commenter.ts
746
764
  var import_klona = require("klona");
747
765
  function formatSqlComment(tags) {
@@ -1025,8 +1043,11 @@ function paginateSingleList(list, { cursor, skip, take }) {
1025
1043
  const end = take !== null ? start + take : list.length;
1026
1044
  return list.slice(start, end);
1027
1045
  }
1028
- function getRecordKey(record, fields) {
1029
- return JSON.stringify(fields.map((field) => record[field]));
1046
+ function getRecordKey(record, fields, mappers) {
1047
+ const array = fields.map(
1048
+ (field, index) => mappers?.[index] ? record[field] !== null ? mappers[index](record[field]) : null : record[field]
1049
+ );
1050
+ return JSON.stringify(array);
1030
1051
  }
1031
1052
 
1032
1053
  // src/query-plan.ts
@@ -1121,7 +1142,10 @@ function renderFragment(fragment, placeholderFormat, ctx) {
1121
1142
  case "stringChunk":
1122
1143
  return fragment.chunk;
1123
1144
  case "parameterTuple": {
1124
- const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => formatPlaceholder(placeholderFormat, ctx.placeholderNumber++)).join(",");
1145
+ const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => {
1146
+ const item = formatPlaceholder(placeholderFormat, ctx.placeholderNumber++);
1147
+ return `${fragment.itemPrefix}${item}${fragment.itemSuffix}`;
1148
+ }).join(fragment.itemSeparator);
1125
1149
  return `(${placeholders})`;
1126
1150
  }
1127
1151
  case "parameterTupleList": {
@@ -1480,7 +1504,7 @@ function doesSatisfyRule(data, rule) {
1480
1504
  }
1481
1505
  }
1482
1506
  function renderMessage(data, error) {
1483
- switch (error.error_identifier) {
1507
+ switch (error.errorIdentifier) {
1484
1508
  case "RELATION_VIOLATION":
1485
1509
  return `The change you are trying to make would violate the required relation '${error.context.relation}' between the \`${error.context.modelA}\` and \`${error.context.modelB}\` models.`;
1486
1510
  case "MISSING_RECORD":
@@ -1500,7 +1524,7 @@ function renderMessage(data, error) {
1500
1524
  }
1501
1525
  }
1502
1526
  function getErrorCode2(error) {
1503
- switch (error.error_identifier) {
1527
+ switch (error.errorIdentifier) {
1504
1528
  case "RELATION_VIOLATION":
1505
1529
  return "P2014";
1506
1530
  case "RECORDS_NOT_CONNECTED":
@@ -1615,7 +1639,7 @@ var QueryInterpreter = class _QueryInterpreter {
1615
1639
  sum += await this.#withQuerySpanAndEvent(
1616
1640
  commentedQuery,
1617
1641
  context.queryable,
1618
- () => context.queryable.executeRaw(commentedQuery).catch(
1642
+ () => context.queryable.executeRaw(cloneObject(commentedQuery)).catch(
1619
1643
  (err) => node.args.type === "rawSql" ? rethrowAsUserFacingRawError(err) : rethrowAsUserFacing(err)
1620
1644
  )
1621
1645
  );
@@ -1630,7 +1654,7 @@ var QueryInterpreter = class _QueryInterpreter {
1630
1654
  const result = await this.#withQuerySpanAndEvent(
1631
1655
  commentedQuery,
1632
1656
  context.queryable,
1633
- () => context.queryable.queryRaw(commentedQuery).catch(
1657
+ () => context.queryable.queryRaw(cloneObject(commentedQuery)).catch(
1634
1658
  (err) => node.args.type === "rawSql" ? rethrowAsUserFacingRawError(err) : rethrowAsUserFacing(err)
1635
1659
  )
1636
1660
  );
@@ -1682,7 +1706,7 @@ var QueryInterpreter = class _QueryInterpreter {
1682
1706
  childRecords: (await this.interpretNode(joinExpr.child, context)).value
1683
1707
  }))
1684
1708
  );
1685
- return { value: attachChildrenToParents(parent, children), lastInsertId };
1709
+ return { value: attachChildrenToParents(parent, children, node.args.canAssumeStrictEquality), lastInsertId };
1686
1710
  }
1687
1711
  case "transaction": {
1688
1712
  if (!context.transactionManager.enabled) {
@@ -1729,8 +1753,9 @@ var QueryInterpreter = class _QueryInterpreter {
1729
1753
  }
1730
1754
  case "process": {
1731
1755
  const { value, lastInsertId } = await this.interpretNode(node.args.expr, context);
1732
- evaluateProcessingParameters(node.args.operations, context.scope, context.generators);
1733
- return { value: processRecords(value, node.args.operations), lastInsertId };
1756
+ const ops = cloneObject(node.args.operations);
1757
+ evaluateProcessingParameters(ops, context.scope, context.generators);
1758
+ return { value: processRecords(value, ops), lastInsertId };
1734
1759
  }
1735
1760
  case "initializeRecord": {
1736
1761
  const { lastInsertId } = await this.interpretNode(node.args.expr, context);
@@ -1823,12 +1848,13 @@ function mapField2(value, field) {
1823
1848
  }
1824
1849
  return value;
1825
1850
  }
1826
- function attachChildrenToParents(parentRecords, children) {
1851
+ function attachChildrenToParents(parentRecords, children, canAssumeStrictEquality) {
1827
1852
  for (const { joinExpr, childRecords } of children) {
1828
1853
  const parentKeys = joinExpr.on.map(([k]) => k);
1829
1854
  const childKeys = joinExpr.on.map(([, k]) => k);
1830
1855
  const parentMap = {};
1831
- for (const parent of Array.isArray(parentRecords) ? parentRecords : [parentRecords]) {
1856
+ const parentArray = Array.isArray(parentRecords) ? parentRecords : [parentRecords];
1857
+ for (const parent of parentArray) {
1832
1858
  const parentRecord = asRecord(parent);
1833
1859
  const key = getRecordKey(parentRecord, parentKeys);
1834
1860
  if (!parentMap[key]) {
@@ -1841,11 +1867,12 @@ function attachChildrenToParents(parentRecords, children) {
1841
1867
  parentRecord[joinExpr.parentField] = [];
1842
1868
  }
1843
1869
  }
1870
+ const mappers = canAssumeStrictEquality ? void 0 : inferKeyCasts(parentArray, parentKeys);
1844
1871
  for (const childRecord of Array.isArray(childRecords) ? childRecords : [childRecords]) {
1845
1872
  if (childRecord === null) {
1846
1873
  continue;
1847
1874
  }
1848
- const key = getRecordKey(asRecord(childRecord), childKeys);
1875
+ const key = getRecordKey(asRecord(childRecord), childKeys, mappers);
1849
1876
  for (const parentRecord of parentMap[key] ?? []) {
1850
1877
  if (joinExpr.isRelationUnique) {
1851
1878
  parentRecord[joinExpr.parentField] = childRecord;
@@ -1857,6 +1884,40 @@ function attachChildrenToParents(parentRecords, children) {
1857
1884
  }
1858
1885
  return parentRecords;
1859
1886
  }
1887
+ function inferKeyCasts(rows, keys) {
1888
+ function getKeyCast(type) {
1889
+ switch (type) {
1890
+ case "number":
1891
+ return Number;
1892
+ case "string":
1893
+ return String;
1894
+ case "boolean":
1895
+ return Boolean;
1896
+ case "bigint":
1897
+ return BigInt;
1898
+ default:
1899
+ return;
1900
+ }
1901
+ }
1902
+ const keyCasts = Array.from({ length: keys.length });
1903
+ let keysFound = 0;
1904
+ for (const parent of rows) {
1905
+ const parentRecord = asRecord(parent);
1906
+ for (const [i, key] of keys.entries()) {
1907
+ if (parentRecord[key] !== null && keyCasts[i] === void 0) {
1908
+ const keyCast = getKeyCast(typeof parentRecord[key]);
1909
+ if (keyCast !== void 0) {
1910
+ keyCasts[i] = keyCast;
1911
+ }
1912
+ keysFound++;
1913
+ }
1914
+ }
1915
+ if (keysFound === keys.length) {
1916
+ break;
1917
+ }
1918
+ }
1919
+ return keyCasts;
1920
+ }
1860
1921
  function evalFieldInitializer(initializer, lastInsertId, scope, generators) {
1861
1922
  switch (initializer.type) {
1862
1923
  case "value":
@@ -1916,6 +1977,9 @@ function evaluateProcessingParameters(ops, scope, generators) {
1916
1977
  evaluateProcessingParameters(nested, scope, generators);
1917
1978
  }
1918
1979
  }
1980
+ function cloneObject(value) {
1981
+ return (0, import_klona2.klona)(value);
1982
+ }
1919
1983
 
1920
1984
  // src/raw-json-protocol.ts
1921
1985
  var import_client_runtime_utils4 = require("@prisma/client-runtime-utils");
@@ -2072,13 +2136,42 @@ var TransactionManager = class {
2072
2136
  );
2073
2137
  }
2074
2138
  async #startTransactionImpl(options) {
2139
+ if (options.newTxId) {
2140
+ return await this.#withActiveTransactionLock(options.newTxId, "start", async (existing) => {
2141
+ if (existing.status !== "running") {
2142
+ throw new TransactionInternalConsistencyError(
2143
+ `Transaction in invalid state ${existing.status} when starting a nested transaction.`
2144
+ );
2145
+ }
2146
+ if (!existing.transaction) {
2147
+ throw new TransactionInternalConsistencyError(
2148
+ `Transaction missing underlying driver transaction when starting a nested transaction.`
2149
+ );
2150
+ }
2151
+ existing.depth += 1;
2152
+ const savepointName = this.#nextSavepointName(existing);
2153
+ existing.savepoints.push(savepointName);
2154
+ try {
2155
+ await this.#requiredCreateSavepoint(existing.transaction)(savepointName);
2156
+ } catch (e) {
2157
+ existing.depth -= 1;
2158
+ existing.savepoints.pop();
2159
+ throw e;
2160
+ }
2161
+ return { id: existing.id };
2162
+ });
2163
+ }
2075
2164
  const transaction = {
2076
2165
  id: await randomUUID(),
2077
2166
  status: "waiting",
2078
2167
  timer: void 0,
2079
2168
  timeout: options.timeout,
2080
2169
  startedAt: Date.now(),
2081
- transaction: void 0
2170
+ transaction: void 0,
2171
+ operationQueue: Promise.resolve(),
2172
+ depth: 1,
2173
+ savepoints: [],
2174
+ savepointCounter: 0
2082
2175
  };
2083
2176
  const abortController = new AbortController();
2084
2177
  const startTimer = createTimeoutIfDefined(() => abortController.abort(), options.maxWait);
@@ -2112,14 +2205,49 @@ var TransactionManager = class {
2112
2205
  }
2113
2206
  async commitTransaction(transactionId) {
2114
2207
  return await this.tracingHelper.runInChildSpan("commit_transaction", async () => {
2115
- const txw = this.#getActiveOrClosingTransaction(transactionId, "commit");
2116
- await this.#closeTransaction(txw, "committed");
2208
+ await this.#withActiveTransactionLock(transactionId, "commit", async (txw) => {
2209
+ if (txw.depth > 1) {
2210
+ if (!txw.transaction) throw new TransactionNotFoundError();
2211
+ const savepointName = txw.savepoints.at(-1);
2212
+ if (!savepointName) {
2213
+ throw new TransactionInternalConsistencyError(
2214
+ `Missing savepoint for nested commit. Depth: ${txw.depth}, transactionId: ${txw.id}`
2215
+ );
2216
+ }
2217
+ try {
2218
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2219
+ } finally {
2220
+ txw.savepoints.pop();
2221
+ txw.depth -= 1;
2222
+ }
2223
+ return;
2224
+ }
2225
+ await this.#closeTransaction(txw, "committed");
2226
+ });
2117
2227
  });
2118
2228
  }
2119
2229
  async rollbackTransaction(transactionId) {
2120
2230
  return await this.tracingHelper.runInChildSpan("rollback_transaction", async () => {
2121
- const txw = this.#getActiveOrClosingTransaction(transactionId, "rollback");
2122
- await this.#closeTransaction(txw, "rolled_back");
2231
+ await this.#withActiveTransactionLock(transactionId, "rollback", async (txw) => {
2232
+ if (txw.depth > 1) {
2233
+ if (!txw.transaction) throw new TransactionNotFoundError();
2234
+ const savepointName = txw.savepoints.at(-1);
2235
+ if (!savepointName) {
2236
+ throw new TransactionInternalConsistencyError(
2237
+ `Missing savepoint for nested rollback. Depth: ${txw.depth}, transactionId: ${txw.id}`
2238
+ );
2239
+ }
2240
+ try {
2241
+ await this.#requiredRollbackToSavepoint(txw.transaction)(savepointName);
2242
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2243
+ } finally {
2244
+ txw.savepoints.pop();
2245
+ txw.depth -= 1;
2246
+ }
2247
+ return;
2248
+ }
2249
+ await this.#closeTransaction(txw, "rolled_back");
2250
+ });
2123
2251
  });
2124
2252
  }
2125
2253
  async getTransaction(txInfo, operation) {
@@ -2163,22 +2291,90 @@ var TransactionManager = class {
2163
2291
  return transaction;
2164
2292
  }
2165
2293
  async cancelAllTransactions() {
2166
- await Promise.allSettled([...this.transactions.values()].map((tx) => this.#closeTransaction(tx, "rolled_back")));
2294
+ await Promise.allSettled(
2295
+ [...this.transactions.values()].map(
2296
+ (tx) => this.#runSerialized(tx, async () => {
2297
+ const current = this.transactions.get(tx.id);
2298
+ if (current) {
2299
+ await this.#closeTransaction(current, "rolled_back");
2300
+ }
2301
+ })
2302
+ )
2303
+ );
2304
+ }
2305
+ #nextSavepointName(transaction) {
2306
+ return `prisma_sp_${transaction.savepointCounter++}`;
2307
+ }
2308
+ #requiredCreateSavepoint(transaction) {
2309
+ if (transaction.createSavepoint) {
2310
+ return transaction.createSavepoint.bind(transaction);
2311
+ }
2312
+ throw new TransactionManagerError(
2313
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): createSavepoint is not implemented.`
2314
+ );
2315
+ }
2316
+ #requiredRollbackToSavepoint(transaction) {
2317
+ if (transaction.rollbackToSavepoint) {
2318
+ return transaction.rollbackToSavepoint.bind(transaction);
2319
+ }
2320
+ throw new TransactionManagerError(
2321
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): rollbackToSavepoint is not implemented.`
2322
+ );
2323
+ }
2324
+ async #releaseSavepoint(transaction, name) {
2325
+ if (transaction.releaseSavepoint) {
2326
+ await transaction.releaseSavepoint(name);
2327
+ }
2328
+ }
2329
+ #debugTransactionAlreadyClosedOnTimeout(transactionId) {
2330
+ debug("Transaction already committed or rolled back when timeout happened.", transactionId);
2167
2331
  }
2168
2332
  #startTransactionTimeout(transactionId, timeout) {
2169
2333
  const timeoutStartedAt = Date.now();
2170
2334
  const timer = createTimeoutIfDefined(async () => {
2171
2335
  debug("Transaction timed out.", { transactionId, timeoutStartedAt, timeout });
2172
2336
  const tx = this.transactions.get(transactionId);
2173
- if (tx && ["running", "waiting"].includes(tx.status)) {
2174
- await this.#closeTransaction(tx, "timed_out");
2175
- } else {
2176
- debug("Transaction already committed or rolled back when timeout happened.", transactionId);
2177
- }
2337
+ if (!tx) {
2338
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2339
+ return;
2340
+ }
2341
+ await this.#runSerialized(tx, async () => {
2342
+ const current = this.transactions.get(transactionId);
2343
+ if (current && ["running", "waiting"].includes(current.status)) {
2344
+ await this.#closeTransaction(current, "timed_out");
2345
+ } else {
2346
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2347
+ }
2348
+ });
2178
2349
  }, timeout);
2179
2350
  timer?.unref?.();
2180
2351
  return timer;
2181
2352
  }
2353
+ // Any operation that mutates or closes a transaction must run through this lock so
2354
+ // status/savepoint/depth checks and updates happen against a stable view of state.
2355
+ async #withActiveTransactionLock(transactionId, operation, callback) {
2356
+ const tx = this.#getActiveOrClosingTransaction(transactionId, operation);
2357
+ return await this.#runSerialized(tx, async () => {
2358
+ const current = this.#getActiveOrClosingTransaction(transactionId, operation);
2359
+ return await callback(current);
2360
+ });
2361
+ }
2362
+ // Serializes operations per transaction id to prevent interleaving across awaits.
2363
+ // This avoids races where one operation mutates savepoint/depth state while another
2364
+ // operation is suspended, which could otherwise corrupt cleanup logic.
2365
+ async #runSerialized(tx, callback) {
2366
+ const previousOperation = tx.operationQueue;
2367
+ let releaseOperationLock;
2368
+ tx.operationQueue = new Promise((resolve) => {
2369
+ releaseOperationLock = resolve;
2370
+ });
2371
+ await previousOperation;
2372
+ try {
2373
+ return await callback();
2374
+ } finally {
2375
+ releaseOperationLock();
2376
+ }
2377
+ }
2182
2378
  async #closeTransaction(tx, status) {
2183
2379
  const createClosingPromise = async () => {
2184
2380
  debug("Closing transaction.", { transactionId: tx.id, status });
@@ -2266,7 +2462,7 @@ function createTimeoutIfDefined(cb, ms) {
2266
2462
  UserFacingError,
2267
2463
  applySqlCommenters,
2268
2464
  convertCompactedRows,
2269
- deserializeJsonResponse,
2465
+ deserializeJsonObject,
2270
2466
  doKeysMatch,
2271
2467
  isDeepStrictEqual,
2272
2468
  isPrismaValueGenerator,
package/dist/index.mjs CHANGED
@@ -119,7 +119,10 @@ function normalizeJsonProtocolValues(result) {
119
119
  function isTaggedValue(value) {
120
120
  return value !== null && typeof value == "object" && typeof value["$type"] === "string";
121
121
  }
122
- function normalizeTaggedValue({ $type, value }) {
122
+ function normalizeTaggedValue({
123
+ $type,
124
+ value
125
+ }) {
123
126
  switch ($type) {
124
127
  case "BigInt":
125
128
  return { $type, value: String(value) };
@@ -131,6 +134,12 @@ function normalizeTaggedValue({ $type, value }) {
131
134
  return { $type, value: String(new Decimal2(value)) };
132
135
  case "Json":
133
136
  return { $type, value: JSON.stringify(JSON.parse(value)) };
137
+ case "Raw":
138
+ return { $type, value };
139
+ case "FieldRef":
140
+ return { $type, value };
141
+ case "Enum":
142
+ return { $type, value };
134
143
  default:
135
144
  assertNever(value, "Unknown tagged value");
136
145
  }
@@ -142,12 +151,12 @@ function mapObjectValues(object, mapper) {
142
151
  }
143
152
  return result;
144
153
  }
145
- function deserializeJsonResponse(result) {
154
+ function deserializeJsonObject(result) {
146
155
  if (result === null) {
147
156
  return result;
148
157
  }
149
158
  if (Array.isArray(result)) {
150
- return result.map(deserializeJsonResponse);
159
+ return result.map(deserializeJsonObject);
151
160
  }
152
161
  if (typeof result === "object") {
153
162
  if (isTaggedValue(result)) {
@@ -156,7 +165,7 @@ function deserializeJsonResponse(result) {
156
165
  if (result.constructor !== null && result.constructor.name !== "Object") {
157
166
  return result;
158
167
  }
159
- return mapObjectValues(result, deserializeJsonResponse);
168
+ return mapObjectValues(result, deserializeJsonObject);
160
169
  }
161
170
  return result;
162
171
  }
@@ -174,6 +183,12 @@ function deserializeTaggedValue({ $type, value }) {
174
183
  return new Decimal2(value);
175
184
  case "Json":
176
185
  return JSON.parse(value);
186
+ case "Raw":
187
+ return value;
188
+ case "FieldRef":
189
+ throw new Error("FieldRef tagged values cannot be deserialized to JavaScript values");
190
+ case "Enum":
191
+ return value;
177
192
  default:
178
193
  assertNever(value, "Unknown tagged value");
179
194
  }
@@ -405,7 +420,7 @@ function resolveArgPlaceholders(args, placeholderValues) {
405
420
  function convertCompactedRows(rows, compiledBatch, placeholderValues = {}) {
406
421
  const keysPerRow = rows.map(
407
422
  (item) => compiledBatch.keys.reduce((acc, key) => {
408
- acc[key] = deserializeJsonResponse(item[key]);
423
+ acc[key] = deserializeJsonObject(item[key]);
409
424
  return acc;
410
425
  }, {})
411
426
  );
@@ -691,6 +706,9 @@ function normalizeDateTime(dt) {
691
706
  return dtWithTz;
692
707
  }
693
708
 
709
+ // src/interpreter/query-interpreter.ts
710
+ import { klona as klona2 } from "klona";
711
+
694
712
  // src/sql-commenter.ts
695
713
  import { klona } from "klona";
696
714
  function formatSqlComment(tags) {
@@ -974,8 +992,11 @@ function paginateSingleList(list, { cursor, skip, take }) {
974
992
  const end = take !== null ? start + take : list.length;
975
993
  return list.slice(start, end);
976
994
  }
977
- function getRecordKey(record, fields) {
978
- return JSON.stringify(fields.map((field) => record[field]));
995
+ function getRecordKey(record, fields, mappers) {
996
+ const array = fields.map(
997
+ (field, index) => mappers?.[index] ? record[field] !== null ? mappers[index](record[field]) : null : record[field]
998
+ );
999
+ return JSON.stringify(array);
979
1000
  }
980
1001
 
981
1002
  // src/query-plan.ts
@@ -1070,7 +1091,10 @@ function renderFragment(fragment, placeholderFormat, ctx) {
1070
1091
  case "stringChunk":
1071
1092
  return fragment.chunk;
1072
1093
  case "parameterTuple": {
1073
- const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => formatPlaceholder(placeholderFormat, ctx.placeholderNumber++)).join(",");
1094
+ const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => {
1095
+ const item = formatPlaceholder(placeholderFormat, ctx.placeholderNumber++);
1096
+ return `${fragment.itemPrefix}${item}${fragment.itemSuffix}`;
1097
+ }).join(fragment.itemSeparator);
1074
1098
  return `(${placeholders})`;
1075
1099
  }
1076
1100
  case "parameterTupleList": {
@@ -1429,7 +1453,7 @@ function doesSatisfyRule(data, rule) {
1429
1453
  }
1430
1454
  }
1431
1455
  function renderMessage(data, error) {
1432
- switch (error.error_identifier) {
1456
+ switch (error.errorIdentifier) {
1433
1457
  case "RELATION_VIOLATION":
1434
1458
  return `The change you are trying to make would violate the required relation '${error.context.relation}' between the \`${error.context.modelA}\` and \`${error.context.modelB}\` models.`;
1435
1459
  case "MISSING_RECORD":
@@ -1449,7 +1473,7 @@ function renderMessage(data, error) {
1449
1473
  }
1450
1474
  }
1451
1475
  function getErrorCode2(error) {
1452
- switch (error.error_identifier) {
1476
+ switch (error.errorIdentifier) {
1453
1477
  case "RELATION_VIOLATION":
1454
1478
  return "P2014";
1455
1479
  case "RECORDS_NOT_CONNECTED":
@@ -1564,7 +1588,7 @@ var QueryInterpreter = class _QueryInterpreter {
1564
1588
  sum += await this.#withQuerySpanAndEvent(
1565
1589
  commentedQuery,
1566
1590
  context.queryable,
1567
- () => context.queryable.executeRaw(commentedQuery).catch(
1591
+ () => context.queryable.executeRaw(cloneObject(commentedQuery)).catch(
1568
1592
  (err) => node.args.type === "rawSql" ? rethrowAsUserFacingRawError(err) : rethrowAsUserFacing(err)
1569
1593
  )
1570
1594
  );
@@ -1579,7 +1603,7 @@ var QueryInterpreter = class _QueryInterpreter {
1579
1603
  const result = await this.#withQuerySpanAndEvent(
1580
1604
  commentedQuery,
1581
1605
  context.queryable,
1582
- () => context.queryable.queryRaw(commentedQuery).catch(
1606
+ () => context.queryable.queryRaw(cloneObject(commentedQuery)).catch(
1583
1607
  (err) => node.args.type === "rawSql" ? rethrowAsUserFacingRawError(err) : rethrowAsUserFacing(err)
1584
1608
  )
1585
1609
  );
@@ -1631,7 +1655,7 @@ var QueryInterpreter = class _QueryInterpreter {
1631
1655
  childRecords: (await this.interpretNode(joinExpr.child, context)).value
1632
1656
  }))
1633
1657
  );
1634
- return { value: attachChildrenToParents(parent, children), lastInsertId };
1658
+ return { value: attachChildrenToParents(parent, children, node.args.canAssumeStrictEquality), lastInsertId };
1635
1659
  }
1636
1660
  case "transaction": {
1637
1661
  if (!context.transactionManager.enabled) {
@@ -1678,8 +1702,9 @@ var QueryInterpreter = class _QueryInterpreter {
1678
1702
  }
1679
1703
  case "process": {
1680
1704
  const { value, lastInsertId } = await this.interpretNode(node.args.expr, context);
1681
- evaluateProcessingParameters(node.args.operations, context.scope, context.generators);
1682
- return { value: processRecords(value, node.args.operations), lastInsertId };
1705
+ const ops = cloneObject(node.args.operations);
1706
+ evaluateProcessingParameters(ops, context.scope, context.generators);
1707
+ return { value: processRecords(value, ops), lastInsertId };
1683
1708
  }
1684
1709
  case "initializeRecord": {
1685
1710
  const { lastInsertId } = await this.interpretNode(node.args.expr, context);
@@ -1772,12 +1797,13 @@ function mapField2(value, field) {
1772
1797
  }
1773
1798
  return value;
1774
1799
  }
1775
- function attachChildrenToParents(parentRecords, children) {
1800
+ function attachChildrenToParents(parentRecords, children, canAssumeStrictEquality) {
1776
1801
  for (const { joinExpr, childRecords } of children) {
1777
1802
  const parentKeys = joinExpr.on.map(([k]) => k);
1778
1803
  const childKeys = joinExpr.on.map(([, k]) => k);
1779
1804
  const parentMap = {};
1780
- for (const parent of Array.isArray(parentRecords) ? parentRecords : [parentRecords]) {
1805
+ const parentArray = Array.isArray(parentRecords) ? parentRecords : [parentRecords];
1806
+ for (const parent of parentArray) {
1781
1807
  const parentRecord = asRecord(parent);
1782
1808
  const key = getRecordKey(parentRecord, parentKeys);
1783
1809
  if (!parentMap[key]) {
@@ -1790,11 +1816,12 @@ function attachChildrenToParents(parentRecords, children) {
1790
1816
  parentRecord[joinExpr.parentField] = [];
1791
1817
  }
1792
1818
  }
1819
+ const mappers = canAssumeStrictEquality ? void 0 : inferKeyCasts(parentArray, parentKeys);
1793
1820
  for (const childRecord of Array.isArray(childRecords) ? childRecords : [childRecords]) {
1794
1821
  if (childRecord === null) {
1795
1822
  continue;
1796
1823
  }
1797
- const key = getRecordKey(asRecord(childRecord), childKeys);
1824
+ const key = getRecordKey(asRecord(childRecord), childKeys, mappers);
1798
1825
  for (const parentRecord of parentMap[key] ?? []) {
1799
1826
  if (joinExpr.isRelationUnique) {
1800
1827
  parentRecord[joinExpr.parentField] = childRecord;
@@ -1806,6 +1833,40 @@ function attachChildrenToParents(parentRecords, children) {
1806
1833
  }
1807
1834
  return parentRecords;
1808
1835
  }
1836
+ function inferKeyCasts(rows, keys) {
1837
+ function getKeyCast(type) {
1838
+ switch (type) {
1839
+ case "number":
1840
+ return Number;
1841
+ case "string":
1842
+ return String;
1843
+ case "boolean":
1844
+ return Boolean;
1845
+ case "bigint":
1846
+ return BigInt;
1847
+ default:
1848
+ return;
1849
+ }
1850
+ }
1851
+ const keyCasts = Array.from({ length: keys.length });
1852
+ let keysFound = 0;
1853
+ for (const parent of rows) {
1854
+ const parentRecord = asRecord(parent);
1855
+ for (const [i, key] of keys.entries()) {
1856
+ if (parentRecord[key] !== null && keyCasts[i] === void 0) {
1857
+ const keyCast = getKeyCast(typeof parentRecord[key]);
1858
+ if (keyCast !== void 0) {
1859
+ keyCasts[i] = keyCast;
1860
+ }
1861
+ keysFound++;
1862
+ }
1863
+ }
1864
+ if (keysFound === keys.length) {
1865
+ break;
1866
+ }
1867
+ }
1868
+ return keyCasts;
1869
+ }
1809
1870
  function evalFieldInitializer(initializer, lastInsertId, scope, generators) {
1810
1871
  switch (initializer.type) {
1811
1872
  case "value":
@@ -1865,6 +1926,9 @@ function evaluateProcessingParameters(ops, scope, generators) {
1865
1926
  evaluateProcessingParameters(nested, scope, generators);
1866
1927
  }
1867
1928
  }
1929
+ function cloneObject(value) {
1930
+ return klona2(value);
1931
+ }
1868
1932
 
1869
1933
  // src/raw-json-protocol.ts
1870
1934
  import { Decimal as Decimal4 } from "@prisma/client-runtime-utils";
@@ -2021,13 +2085,42 @@ var TransactionManager = class {
2021
2085
  );
2022
2086
  }
2023
2087
  async #startTransactionImpl(options) {
2088
+ if (options.newTxId) {
2089
+ return await this.#withActiveTransactionLock(options.newTxId, "start", async (existing) => {
2090
+ if (existing.status !== "running") {
2091
+ throw new TransactionInternalConsistencyError(
2092
+ `Transaction in invalid state ${existing.status} when starting a nested transaction.`
2093
+ );
2094
+ }
2095
+ if (!existing.transaction) {
2096
+ throw new TransactionInternalConsistencyError(
2097
+ `Transaction missing underlying driver transaction when starting a nested transaction.`
2098
+ );
2099
+ }
2100
+ existing.depth += 1;
2101
+ const savepointName = this.#nextSavepointName(existing);
2102
+ existing.savepoints.push(savepointName);
2103
+ try {
2104
+ await this.#requiredCreateSavepoint(existing.transaction)(savepointName);
2105
+ } catch (e) {
2106
+ existing.depth -= 1;
2107
+ existing.savepoints.pop();
2108
+ throw e;
2109
+ }
2110
+ return { id: existing.id };
2111
+ });
2112
+ }
2024
2113
  const transaction = {
2025
2114
  id: await randomUUID(),
2026
2115
  status: "waiting",
2027
2116
  timer: void 0,
2028
2117
  timeout: options.timeout,
2029
2118
  startedAt: Date.now(),
2030
- transaction: void 0
2119
+ transaction: void 0,
2120
+ operationQueue: Promise.resolve(),
2121
+ depth: 1,
2122
+ savepoints: [],
2123
+ savepointCounter: 0
2031
2124
  };
2032
2125
  const abortController = new AbortController();
2033
2126
  const startTimer = createTimeoutIfDefined(() => abortController.abort(), options.maxWait);
@@ -2061,14 +2154,49 @@ var TransactionManager = class {
2061
2154
  }
2062
2155
  async commitTransaction(transactionId) {
2063
2156
  return await this.tracingHelper.runInChildSpan("commit_transaction", async () => {
2064
- const txw = this.#getActiveOrClosingTransaction(transactionId, "commit");
2065
- await this.#closeTransaction(txw, "committed");
2157
+ await this.#withActiveTransactionLock(transactionId, "commit", async (txw) => {
2158
+ if (txw.depth > 1) {
2159
+ if (!txw.transaction) throw new TransactionNotFoundError();
2160
+ const savepointName = txw.savepoints.at(-1);
2161
+ if (!savepointName) {
2162
+ throw new TransactionInternalConsistencyError(
2163
+ `Missing savepoint for nested commit. Depth: ${txw.depth}, transactionId: ${txw.id}`
2164
+ );
2165
+ }
2166
+ try {
2167
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2168
+ } finally {
2169
+ txw.savepoints.pop();
2170
+ txw.depth -= 1;
2171
+ }
2172
+ return;
2173
+ }
2174
+ await this.#closeTransaction(txw, "committed");
2175
+ });
2066
2176
  });
2067
2177
  }
2068
2178
  async rollbackTransaction(transactionId) {
2069
2179
  return await this.tracingHelper.runInChildSpan("rollback_transaction", async () => {
2070
- const txw = this.#getActiveOrClosingTransaction(transactionId, "rollback");
2071
- await this.#closeTransaction(txw, "rolled_back");
2180
+ await this.#withActiveTransactionLock(transactionId, "rollback", async (txw) => {
2181
+ if (txw.depth > 1) {
2182
+ if (!txw.transaction) throw new TransactionNotFoundError();
2183
+ const savepointName = txw.savepoints.at(-1);
2184
+ if (!savepointName) {
2185
+ throw new TransactionInternalConsistencyError(
2186
+ `Missing savepoint for nested rollback. Depth: ${txw.depth}, transactionId: ${txw.id}`
2187
+ );
2188
+ }
2189
+ try {
2190
+ await this.#requiredRollbackToSavepoint(txw.transaction)(savepointName);
2191
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2192
+ } finally {
2193
+ txw.savepoints.pop();
2194
+ txw.depth -= 1;
2195
+ }
2196
+ return;
2197
+ }
2198
+ await this.#closeTransaction(txw, "rolled_back");
2199
+ });
2072
2200
  });
2073
2201
  }
2074
2202
  async getTransaction(txInfo, operation) {
@@ -2112,22 +2240,90 @@ var TransactionManager = class {
2112
2240
  return transaction;
2113
2241
  }
2114
2242
  async cancelAllTransactions() {
2115
- await Promise.allSettled([...this.transactions.values()].map((tx) => this.#closeTransaction(tx, "rolled_back")));
2243
+ await Promise.allSettled(
2244
+ [...this.transactions.values()].map(
2245
+ (tx) => this.#runSerialized(tx, async () => {
2246
+ const current = this.transactions.get(tx.id);
2247
+ if (current) {
2248
+ await this.#closeTransaction(current, "rolled_back");
2249
+ }
2250
+ })
2251
+ )
2252
+ );
2253
+ }
2254
+ #nextSavepointName(transaction) {
2255
+ return `prisma_sp_${transaction.savepointCounter++}`;
2256
+ }
2257
+ #requiredCreateSavepoint(transaction) {
2258
+ if (transaction.createSavepoint) {
2259
+ return transaction.createSavepoint.bind(transaction);
2260
+ }
2261
+ throw new TransactionManagerError(
2262
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): createSavepoint is not implemented.`
2263
+ );
2264
+ }
2265
+ #requiredRollbackToSavepoint(transaction) {
2266
+ if (transaction.rollbackToSavepoint) {
2267
+ return transaction.rollbackToSavepoint.bind(transaction);
2268
+ }
2269
+ throw new TransactionManagerError(
2270
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): rollbackToSavepoint is not implemented.`
2271
+ );
2272
+ }
2273
+ async #releaseSavepoint(transaction, name) {
2274
+ if (transaction.releaseSavepoint) {
2275
+ await transaction.releaseSavepoint(name);
2276
+ }
2277
+ }
2278
+ #debugTransactionAlreadyClosedOnTimeout(transactionId) {
2279
+ debug("Transaction already committed or rolled back when timeout happened.", transactionId);
2116
2280
  }
2117
2281
  #startTransactionTimeout(transactionId, timeout) {
2118
2282
  const timeoutStartedAt = Date.now();
2119
2283
  const timer = createTimeoutIfDefined(async () => {
2120
2284
  debug("Transaction timed out.", { transactionId, timeoutStartedAt, timeout });
2121
2285
  const tx = this.transactions.get(transactionId);
2122
- if (tx && ["running", "waiting"].includes(tx.status)) {
2123
- await this.#closeTransaction(tx, "timed_out");
2124
- } else {
2125
- debug("Transaction already committed or rolled back when timeout happened.", transactionId);
2126
- }
2286
+ if (!tx) {
2287
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2288
+ return;
2289
+ }
2290
+ await this.#runSerialized(tx, async () => {
2291
+ const current = this.transactions.get(transactionId);
2292
+ if (current && ["running", "waiting"].includes(current.status)) {
2293
+ await this.#closeTransaction(current, "timed_out");
2294
+ } else {
2295
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2296
+ }
2297
+ });
2127
2298
  }, timeout);
2128
2299
  timer?.unref?.();
2129
2300
  return timer;
2130
2301
  }
2302
+ // Any operation that mutates or closes a transaction must run through this lock so
2303
+ // status/savepoint/depth checks and updates happen against a stable view of state.
2304
+ async #withActiveTransactionLock(transactionId, operation, callback) {
2305
+ const tx = this.#getActiveOrClosingTransaction(transactionId, operation);
2306
+ return await this.#runSerialized(tx, async () => {
2307
+ const current = this.#getActiveOrClosingTransaction(transactionId, operation);
2308
+ return await callback(current);
2309
+ });
2310
+ }
2311
+ // Serializes operations per transaction id to prevent interleaving across awaits.
2312
+ // This avoids races where one operation mutates savepoint/depth state while another
2313
+ // operation is suspended, which could otherwise corrupt cleanup logic.
2314
+ async #runSerialized(tx, callback) {
2315
+ const previousOperation = tx.operationQueue;
2316
+ let releaseOperationLock;
2317
+ tx.operationQueue = new Promise((resolve) => {
2318
+ releaseOperationLock = resolve;
2319
+ });
2320
+ await previousOperation;
2321
+ try {
2322
+ return await callback();
2323
+ } finally {
2324
+ releaseOperationLock();
2325
+ }
2326
+ }
2131
2327
  async #closeTransaction(tx, status) {
2132
2328
  const createClosingPromise = async () => {
2133
2329
  debug("Closing transaction.", { transactionId: tx.id, status });
@@ -2214,7 +2410,7 @@ export {
2214
2410
  UserFacingError,
2215
2411
  applySqlCommenters,
2216
2412
  convertCompactedRows,
2217
- deserializeJsonResponse,
2413
+ deserializeJsonObject,
2218
2414
  doKeysMatch,
2219
2415
  isDeepStrictEqual,
2220
2416
  isPrismaValueGenerator,
@@ -1,3 +1,3 @@
1
1
  import { InMemoryOps } from '../query-plan';
2
2
  export declare function processRecords(value: unknown, ops: InMemoryOps): unknown;
3
- export declare function getRecordKey(record: {}, fields: string[]): string;
3
+ export declare function getRecordKey(record: {}, fields: readonly string[], mappers?: ((value: unknown) => unknown)[]): string;
@@ -5,6 +5,7 @@ import { QueryPlanNode } from '../query-plan';
5
5
  import { type SchemaProvider } from '../schema';
6
6
  import { type TracingHelper } from '../tracing';
7
7
  import { type TransactionManager } from '../transaction-manager/transaction-manager';
8
+ import { DeepReadonly } from '../utils';
8
9
  import { Value } from './scope';
9
10
  export type QueryInterpreterTransactionManager = {
10
11
  enabled: true;
@@ -39,6 +40,6 @@ export declare class QueryInterpreter {
39
40
  provider?: SchemaProvider;
40
41
  connectionInfo?: ConnectionInfo;
41
42
  }): QueryInterpreter;
42
- run(queryPlan: QueryPlanNode, options: QueryRuntimeOptions): Promise<unknown>;
43
+ run(queryPlan: DeepReadonly<QueryPlanNode>, options: QueryRuntimeOptions): Promise<unknown>;
43
44
  private interpretNode;
44
45
  }
@@ -1,6 +1,7 @@
1
1
  import { SqlQuery } from '@prisma/driver-adapter-utils';
2
2
  import { type QueryPlanDbQuery } from '../query-plan';
3
+ import { DeepReadonly } from '../utils';
3
4
  import { GeneratorRegistrySnapshot } from './generators';
4
5
  import { ScopeBindings } from './scope';
5
- export declare function renderQuery(dbQuery: QueryPlanDbQuery, scope: ScopeBindings, generators: GeneratorRegistrySnapshot, maxChunkSize?: number): SqlQuery[];
6
+ export declare function renderQuery(dbQuery: DeepReadonly<QueryPlanDbQuery>, scope: ScopeBindings, generators: GeneratorRegistrySnapshot, maxChunkSize?: number): DeepReadonly<SqlQuery>[];
6
7
  export declare function evaluateArg(arg: unknown, scope: ScopeBindings, generators: GeneratorRegistrySnapshot): unknown;
@@ -1,3 +1,4 @@
1
1
  import { DataRule, ValidationError } from '../query-plan';
2
- export declare function performValidation(data: unknown, rules: DataRule[], error: ValidationError): void;
2
+ import { DeepReadonly } from '../utils';
3
+ export declare function performValidation(data: unknown, rules: DeepReadonly<DataRule[]>, error: ValidationError): void;
3
4
  export declare function doesSatisfyRule(data: unknown, rule: DataRule): boolean;
@@ -29,10 +29,14 @@ export type JsonTaggedValue = {
29
29
  $type: 'Json';
30
30
  value: string;
31
31
  };
32
- export type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue;
32
+ export type RawTaggedValue = {
33
+ $type: 'Raw';
34
+ value: unknown;
35
+ };
36
+ export type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue | RawTaggedValue;
33
37
  export type JsonOutputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | JsonTaggedValue;
34
38
  export type JsOutputValue = null | string | number | boolean | bigint | Uint8Array | Date | Decimal | JsOutputValue[] | {
35
39
  [key: string]: JsOutputValue;
36
40
  };
37
41
  export declare function normalizeJsonProtocolValues(result: unknown): unknown;
38
- export declare function deserializeJsonResponse(result: unknown): unknown;
42
+ export declare function deserializeJsonObject(result: unknown): unknown;
@@ -56,6 +56,9 @@ export type Fragment = {
56
56
  type: 'parameter';
57
57
  } | {
58
58
  type: 'parameterTuple';
59
+ itemPrefix: string;
60
+ itemSeparator: string;
61
+ itemSuffix: string;
59
62
  } | {
60
63
  type: 'parameterTupleList';
61
64
  itemPrefix: string;
@@ -121,6 +124,7 @@ export type QueryPlanNode = {
121
124
  args: {
122
125
  parent: QueryPlanNode;
123
126
  children: JoinExpression[];
127
+ canAssumeStrictEquality: boolean;
124
128
  };
125
129
  } | {
126
130
  type: 'mapField';
@@ -227,14 +231,14 @@ export type DataRule = {
227
231
  type: 'never';
228
232
  };
229
233
  export type ValidationError = {
230
- error_identifier: 'RELATION_VIOLATION';
234
+ errorIdentifier: 'RELATION_VIOLATION';
231
235
  context: {
232
236
  relation: string;
233
237
  modelA: string;
234
238
  modelB: string;
235
239
  };
236
240
  } | {
237
- error_identifier: 'MISSING_RELATED_RECORD';
241
+ errorIdentifier: 'MISSING_RELATED_RECORD';
238
242
  context: {
239
243
  model: string;
240
244
  relation: string;
@@ -243,24 +247,24 @@ export type ValidationError = {
243
247
  neededFor?: string;
244
248
  };
245
249
  } | {
246
- error_identifier: 'MISSING_RECORD';
250
+ errorIdentifier: 'MISSING_RECORD';
247
251
  context: {
248
252
  operation: string;
249
253
  };
250
254
  } | {
251
- error_identifier: 'INCOMPLETE_CONNECT_INPUT';
255
+ errorIdentifier: 'INCOMPLETE_CONNECT_INPUT';
252
256
  context: {
253
257
  expectedRows: number;
254
258
  };
255
259
  } | {
256
- error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
260
+ errorIdentifier: 'INCOMPLETE_CONNECT_OUTPUT';
257
261
  context: {
258
262
  expectedRows: number;
259
263
  relation: string;
260
264
  relationType: string;
261
265
  };
262
266
  } | {
263
- error_identifier: 'RECORDS_NOT_CONNECTED';
267
+ errorIdentifier: 'RECORDS_NOT_CONNECTED';
264
268
  context: {
265
269
  relation: string;
266
270
  parent: string;
package/dist/tracing.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type Context, type Span, type SpanOptions } from '@opentelemetry/api';
2
2
  import type { SqlQuery } from '@prisma/driver-adapter-utils';
3
3
  import { QueryEvent } from './events';
4
4
  import type { SchemaProvider } from './schema';
5
+ import { DeepReadonly } from './utils';
5
6
  export type SpanCallback<R> = (span?: Span, context?: Context) => R;
6
7
  export type ExtendedSpanOptions = SpanOptions & {
7
8
  name: string;
@@ -13,7 +14,7 @@ export interface TracingHelper {
13
14
  export declare const noopTracingHelper: TracingHelper;
14
15
  export declare function providerToOtelSystem(provider: SchemaProvider): string;
15
16
  export declare function withQuerySpanAndEvent<T>({ query, tracingHelper, provider, onQuery, execute, }: {
16
- query: SqlQuery;
17
+ query: DeepReadonly<SqlQuery>;
17
18
  tracingHelper: TracingHelper;
18
19
  provider: SchemaProvider;
19
20
  onQuery?: (event: QueryEvent) => void;
@@ -3,6 +3,7 @@ export type Options = {
3
3
  maxWait?: number;
4
4
  timeout?: number;
5
5
  isolationLevel?: IsolationLevel;
6
+ newTxId?: string;
6
7
  };
7
8
  export type TransactionInfo = {
8
9
  id: string;
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export type DeepReadonly<T> = T extends undefined | null | boolean | string | number | symbol | Function | Date ? T : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : unknown extends T ? unknown : {
2
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
3
+ };
4
+ export type DeepUnreadonly<T> = T extends undefined | null | boolean | string | number | symbol | Function | Date ? T : T extends ReadonlyArray<infer U> ? Array<DeepUnreadonly<U>> : unknown extends T ? unknown : {
5
+ -readonly [K in keyof T]: DeepUnreadonly<T[K]>;
6
+ };
1
7
  export declare function assertNever(_: never, message: string): never;
2
8
  /**
3
9
  * Checks if two objects are deeply equal, recursively checking all properties for strict equality.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/client-engine-runtime",
3
- "version": "7.5.0-dev.3",
3
+ "version": "7.5.0-dev.31",
4
4
  "description": "This package is intended for Prisma's internal use",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -31,19 +31,16 @@
31
31
  "nanoid": "5.1.5",
32
32
  "ulid": "3.0.0",
33
33
  "uuid": "11.1.0",
34
- "@prisma/debug": "7.5.0-dev.3",
35
- "@prisma/sqlcommenter": "7.5.0-dev.3",
36
- "@prisma/driver-adapter-utils": "7.5.0-dev.3",
37
- "@prisma/client-runtime-utils": "7.5.0-dev.3"
34
+ "@prisma/client-runtime-utils": "7.5.0-dev.31",
35
+ "@prisma/driver-adapter-utils": "7.5.0-dev.31",
36
+ "@prisma/sqlcommenter": "7.5.0-dev.31",
37
+ "@prisma/debug": "7.5.0-dev.31"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@codspeed/benchmark.js-plugin": "4.0.0",
41
41
  "@types/benchmark": "2.1.5",
42
- "@types/jest": "29.5.14",
43
42
  "@types/node": "~20.19.24",
44
- "benchmark": "2.1.4",
45
- "jest": "29.7.0",
46
- "jest-junit": "16.0.0"
43
+ "benchmark": "2.1.4"
47
44
  },
48
45
  "files": [
49
46
  "dist"
@@ -52,6 +49,6 @@
52
49
  "scripts": {
53
50
  "dev": "DEV=true tsx helpers/build.ts",
54
51
  "build": "tsx helpers/build.ts",
55
- "test": "jest"
52
+ "test": "vitest run"
56
53
  }
57
54
  }