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

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/index.d.mts CHANGED
@@ -80,7 +80,7 @@ export declare type DecimalTaggedValue = {
80
80
  value: string;
81
81
  };
82
82
 
83
- export declare function deserializeJsonResponse(result: unknown): unknown;
83
+ export declare function deserializeJsonObject(result: unknown): unknown;
84
84
 
85
85
  /**
86
86
  * Checks if two objects representing the names and values of key columns match. A match is
@@ -155,6 +155,9 @@ export declare type Fragment = {
155
155
  type: 'parameter';
156
156
  } | {
157
157
  type: 'parameterTuple';
158
+ itemPrefix: string;
159
+ itemSeparator: string;
160
+ itemSuffix: string;
158
161
  } | {
159
162
  type: 'parameterTupleList';
160
163
  itemPrefix: string;
@@ -187,7 +190,7 @@ export declare type JoinExpression = {
187
190
  isRelationUnique: boolean;
188
191
  };
189
192
 
190
- export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue;
193
+ export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue | RawTaggedValue;
191
194
 
192
195
  export declare type JsonOutputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | JsonTaggedValue;
193
196
 
@@ -421,6 +424,11 @@ export declare type RawResponse = {
421
424
  rows: unknown[][];
422
425
  };
423
426
 
427
+ export declare type RawTaggedValue = {
428
+ $type: 'Raw';
429
+ value: unknown;
430
+ };
431
+
424
432
  export declare type ResultNode = {
425
433
  type: 'affectedRows';
426
434
  } | {
@@ -492,6 +500,7 @@ export declare type TransactionOptions = {
492
500
  maxWait?: number;
493
501
  timeout?: number;
494
502
  isolationLevel?: IsolationLevel;
503
+ newTxId?: string;
495
504
  };
496
505
 
497
506
  export declare class UserFacingError extends Error {
@@ -511,14 +520,14 @@ export declare class UserFacingError extends Error {
511
520
  }
512
521
 
513
522
  export declare type ValidationError = {
514
- error_identifier: 'RELATION_VIOLATION';
523
+ errorIdentifier: 'RELATION_VIOLATION';
515
524
  context: {
516
525
  relation: string;
517
526
  modelA: string;
518
527
  modelB: string;
519
528
  };
520
529
  } | {
521
- error_identifier: 'MISSING_RELATED_RECORD';
530
+ errorIdentifier: 'MISSING_RELATED_RECORD';
522
531
  context: {
523
532
  model: string;
524
533
  relation: string;
@@ -527,24 +536,24 @@ export declare type ValidationError = {
527
536
  neededFor?: string;
528
537
  };
529
538
  } | {
530
- error_identifier: 'MISSING_RECORD';
539
+ errorIdentifier: 'MISSING_RECORD';
531
540
  context: {
532
541
  operation: string;
533
542
  };
534
543
  } | {
535
- error_identifier: 'INCOMPLETE_CONNECT_INPUT';
544
+ errorIdentifier: 'INCOMPLETE_CONNECT_INPUT';
536
545
  context: {
537
546
  expectedRows: number;
538
547
  };
539
548
  } | {
540
- error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
549
+ errorIdentifier: 'INCOMPLETE_CONNECT_OUTPUT';
541
550
  context: {
542
551
  expectedRows: number;
543
552
  relation: string;
544
553
  relationType: string;
545
554
  };
546
555
  } | {
547
- error_identifier: 'RECORDS_NOT_CONNECTED';
556
+ errorIdentifier: 'RECORDS_NOT_CONNECTED';
548
557
  context: {
549
558
  relation: string;
550
559
  parent: string;
package/dist/index.d.ts CHANGED
@@ -80,7 +80,7 @@ export declare type DecimalTaggedValue = {
80
80
  value: string;
81
81
  };
82
82
 
83
- export declare function deserializeJsonResponse(result: unknown): unknown;
83
+ export declare function deserializeJsonObject(result: unknown): unknown;
84
84
 
85
85
  /**
86
86
  * Checks if two objects representing the names and values of key columns match. A match is
@@ -155,6 +155,9 @@ export declare type Fragment = {
155
155
  type: 'parameter';
156
156
  } | {
157
157
  type: 'parameterTuple';
158
+ itemPrefix: string;
159
+ itemSeparator: string;
160
+ itemSuffix: string;
158
161
  } | {
159
162
  type: 'parameterTupleList';
160
163
  itemPrefix: string;
@@ -187,7 +190,7 @@ export declare type JoinExpression = {
187
190
  isRelationUnique: boolean;
188
191
  };
189
192
 
190
- export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue;
193
+ export declare type JsonInputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | FieldRefTaggedValue | JsonTaggedValue | EnumTaggedValue | RawTaggedValue;
191
194
 
192
195
  export declare type JsonOutputTaggedValue = DateTaggedValue | DecimalTaggedValue | BytesTaggedValue | BigIntTaggedValue | JsonTaggedValue;
193
196
 
@@ -421,6 +424,11 @@ export declare type RawResponse = {
421
424
  rows: unknown[][];
422
425
  };
423
426
 
427
+ export declare type RawTaggedValue = {
428
+ $type: 'Raw';
429
+ value: unknown;
430
+ };
431
+
424
432
  export declare type ResultNode = {
425
433
  type: 'affectedRows';
426
434
  } | {
@@ -492,6 +500,7 @@ export declare type TransactionOptions = {
492
500
  maxWait?: number;
493
501
  timeout?: number;
494
502
  isolationLevel?: IsolationLevel;
503
+ newTxId?: string;
495
504
  };
496
505
 
497
506
  export declare class UserFacingError extends Error {
@@ -511,14 +520,14 @@ export declare class UserFacingError extends Error {
511
520
  }
512
521
 
513
522
  export declare type ValidationError = {
514
- error_identifier: 'RELATION_VIOLATION';
523
+ errorIdentifier: 'RELATION_VIOLATION';
515
524
  context: {
516
525
  relation: string;
517
526
  modelA: string;
518
527
  modelB: string;
519
528
  };
520
529
  } | {
521
- error_identifier: 'MISSING_RELATED_RECORD';
530
+ errorIdentifier: 'MISSING_RELATED_RECORD';
522
531
  context: {
523
532
  model: string;
524
533
  relation: string;
@@ -527,24 +536,24 @@ export declare type ValidationError = {
527
536
  neededFor?: string;
528
537
  };
529
538
  } | {
530
- error_identifier: 'MISSING_RECORD';
539
+ errorIdentifier: 'MISSING_RECORD';
531
540
  context: {
532
541
  operation: string;
533
542
  };
534
543
  } | {
535
- error_identifier: 'INCOMPLETE_CONNECT_INPUT';
544
+ errorIdentifier: 'INCOMPLETE_CONNECT_INPUT';
536
545
  context: {
537
546
  expectedRows: number;
538
547
  };
539
548
  } | {
540
- error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
549
+ errorIdentifier: 'INCOMPLETE_CONNECT_OUTPUT';
541
550
  context: {
542
551
  expectedRows: number;
543
552
  relation: string;
544
553
  relationType: string;
545
554
  };
546
555
  } | {
547
- error_identifier: 'RECORDS_NOT_CONNECTED';
556
+ errorIdentifier: 'RECORDS_NOT_CONNECTED';
548
557
  context: {
549
558
  relation: string;
550
559
  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
  );
@@ -1121,7 +1136,10 @@ function renderFragment(fragment, placeholderFormat, ctx) {
1121
1136
  case "stringChunk":
1122
1137
  return fragment.chunk;
1123
1138
  case "parameterTuple": {
1124
- const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => formatPlaceholder(placeholderFormat, ctx.placeholderNumber++)).join(",");
1139
+ const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => {
1140
+ const item = formatPlaceholder(placeholderFormat, ctx.placeholderNumber++);
1141
+ return `${fragment.itemPrefix}${item}${fragment.itemSuffix}`;
1142
+ }).join(fragment.itemSeparator);
1125
1143
  return `(${placeholders})`;
1126
1144
  }
1127
1145
  case "parameterTupleList": {
@@ -1480,7 +1498,7 @@ function doesSatisfyRule(data, rule) {
1480
1498
  }
1481
1499
  }
1482
1500
  function renderMessage(data, error) {
1483
- switch (error.error_identifier) {
1501
+ switch (error.errorIdentifier) {
1484
1502
  case "RELATION_VIOLATION":
1485
1503
  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
1504
  case "MISSING_RECORD":
@@ -1500,7 +1518,7 @@ function renderMessage(data, error) {
1500
1518
  }
1501
1519
  }
1502
1520
  function getErrorCode2(error) {
1503
- switch (error.error_identifier) {
1521
+ switch (error.errorIdentifier) {
1504
1522
  case "RELATION_VIOLATION":
1505
1523
  return "P2014";
1506
1524
  case "RECORDS_NOT_CONNECTED":
@@ -2072,13 +2090,42 @@ var TransactionManager = class {
2072
2090
  );
2073
2091
  }
2074
2092
  async #startTransactionImpl(options) {
2093
+ if (options.newTxId) {
2094
+ return await this.#withActiveTransactionLock(options.newTxId, "start", async (existing) => {
2095
+ if (existing.status !== "running") {
2096
+ throw new TransactionInternalConsistencyError(
2097
+ `Transaction in invalid state ${existing.status} when starting a nested transaction.`
2098
+ );
2099
+ }
2100
+ if (!existing.transaction) {
2101
+ throw new TransactionInternalConsistencyError(
2102
+ `Transaction missing underlying driver transaction when starting a nested transaction.`
2103
+ );
2104
+ }
2105
+ existing.depth += 1;
2106
+ const savepointName = this.#nextSavepointName(existing);
2107
+ existing.savepoints.push(savepointName);
2108
+ try {
2109
+ await this.#requiredCreateSavepoint(existing.transaction)(savepointName);
2110
+ } catch (e) {
2111
+ existing.depth -= 1;
2112
+ existing.savepoints.pop();
2113
+ throw e;
2114
+ }
2115
+ return { id: existing.id };
2116
+ });
2117
+ }
2075
2118
  const transaction = {
2076
2119
  id: await randomUUID(),
2077
2120
  status: "waiting",
2078
2121
  timer: void 0,
2079
2122
  timeout: options.timeout,
2080
2123
  startedAt: Date.now(),
2081
- transaction: void 0
2124
+ transaction: void 0,
2125
+ operationQueue: Promise.resolve(),
2126
+ depth: 1,
2127
+ savepoints: [],
2128
+ savepointCounter: 0
2082
2129
  };
2083
2130
  const abortController = new AbortController();
2084
2131
  const startTimer = createTimeoutIfDefined(() => abortController.abort(), options.maxWait);
@@ -2112,14 +2159,49 @@ var TransactionManager = class {
2112
2159
  }
2113
2160
  async commitTransaction(transactionId) {
2114
2161
  return await this.tracingHelper.runInChildSpan("commit_transaction", async () => {
2115
- const txw = this.#getActiveOrClosingTransaction(transactionId, "commit");
2116
- await this.#closeTransaction(txw, "committed");
2162
+ await this.#withActiveTransactionLock(transactionId, "commit", async (txw) => {
2163
+ if (txw.depth > 1) {
2164
+ if (!txw.transaction) throw new TransactionNotFoundError();
2165
+ const savepointName = txw.savepoints.at(-1);
2166
+ if (!savepointName) {
2167
+ throw new TransactionInternalConsistencyError(
2168
+ `Missing savepoint for nested commit. Depth: ${txw.depth}, transactionId: ${txw.id}`
2169
+ );
2170
+ }
2171
+ try {
2172
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2173
+ } finally {
2174
+ txw.savepoints.pop();
2175
+ txw.depth -= 1;
2176
+ }
2177
+ return;
2178
+ }
2179
+ await this.#closeTransaction(txw, "committed");
2180
+ });
2117
2181
  });
2118
2182
  }
2119
2183
  async rollbackTransaction(transactionId) {
2120
2184
  return await this.tracingHelper.runInChildSpan("rollback_transaction", async () => {
2121
- const txw = this.#getActiveOrClosingTransaction(transactionId, "rollback");
2122
- await this.#closeTransaction(txw, "rolled_back");
2185
+ await this.#withActiveTransactionLock(transactionId, "rollback", async (txw) => {
2186
+ if (txw.depth > 1) {
2187
+ if (!txw.transaction) throw new TransactionNotFoundError();
2188
+ const savepointName = txw.savepoints.at(-1);
2189
+ if (!savepointName) {
2190
+ throw new TransactionInternalConsistencyError(
2191
+ `Missing savepoint for nested rollback. Depth: ${txw.depth}, transactionId: ${txw.id}`
2192
+ );
2193
+ }
2194
+ try {
2195
+ await this.#requiredRollbackToSavepoint(txw.transaction)(savepointName);
2196
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2197
+ } finally {
2198
+ txw.savepoints.pop();
2199
+ txw.depth -= 1;
2200
+ }
2201
+ return;
2202
+ }
2203
+ await this.#closeTransaction(txw, "rolled_back");
2204
+ });
2123
2205
  });
2124
2206
  }
2125
2207
  async getTransaction(txInfo, operation) {
@@ -2163,22 +2245,90 @@ var TransactionManager = class {
2163
2245
  return transaction;
2164
2246
  }
2165
2247
  async cancelAllTransactions() {
2166
- await Promise.allSettled([...this.transactions.values()].map((tx) => this.#closeTransaction(tx, "rolled_back")));
2248
+ await Promise.allSettled(
2249
+ [...this.transactions.values()].map(
2250
+ (tx) => this.#runSerialized(tx, async () => {
2251
+ const current = this.transactions.get(tx.id);
2252
+ if (current) {
2253
+ await this.#closeTransaction(current, "rolled_back");
2254
+ }
2255
+ })
2256
+ )
2257
+ );
2258
+ }
2259
+ #nextSavepointName(transaction) {
2260
+ return `prisma_sp_${transaction.savepointCounter++}`;
2261
+ }
2262
+ #requiredCreateSavepoint(transaction) {
2263
+ if (transaction.createSavepoint) {
2264
+ return transaction.createSavepoint.bind(transaction);
2265
+ }
2266
+ throw new TransactionManagerError(
2267
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): createSavepoint is not implemented.`
2268
+ );
2269
+ }
2270
+ #requiredRollbackToSavepoint(transaction) {
2271
+ if (transaction.rollbackToSavepoint) {
2272
+ return transaction.rollbackToSavepoint.bind(transaction);
2273
+ }
2274
+ throw new TransactionManagerError(
2275
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): rollbackToSavepoint is not implemented.`
2276
+ );
2277
+ }
2278
+ async #releaseSavepoint(transaction, name) {
2279
+ if (transaction.releaseSavepoint) {
2280
+ await transaction.releaseSavepoint(name);
2281
+ }
2282
+ }
2283
+ #debugTransactionAlreadyClosedOnTimeout(transactionId) {
2284
+ debug("Transaction already committed or rolled back when timeout happened.", transactionId);
2167
2285
  }
2168
2286
  #startTransactionTimeout(transactionId, timeout) {
2169
2287
  const timeoutStartedAt = Date.now();
2170
2288
  const timer = createTimeoutIfDefined(async () => {
2171
2289
  debug("Transaction timed out.", { transactionId, timeoutStartedAt, timeout });
2172
2290
  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
- }
2291
+ if (!tx) {
2292
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2293
+ return;
2294
+ }
2295
+ await this.#runSerialized(tx, async () => {
2296
+ const current = this.transactions.get(transactionId);
2297
+ if (current && ["running", "waiting"].includes(current.status)) {
2298
+ await this.#closeTransaction(current, "timed_out");
2299
+ } else {
2300
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2301
+ }
2302
+ });
2178
2303
  }, timeout);
2179
2304
  timer?.unref?.();
2180
2305
  return timer;
2181
2306
  }
2307
+ // Any operation that mutates or closes a transaction must run through this lock so
2308
+ // status/savepoint/depth checks and updates happen against a stable view of state.
2309
+ async #withActiveTransactionLock(transactionId, operation, callback) {
2310
+ const tx = this.#getActiveOrClosingTransaction(transactionId, operation);
2311
+ return await this.#runSerialized(tx, async () => {
2312
+ const current = this.#getActiveOrClosingTransaction(transactionId, operation);
2313
+ return await callback(current);
2314
+ });
2315
+ }
2316
+ // Serializes operations per transaction id to prevent interleaving across awaits.
2317
+ // This avoids races where one operation mutates savepoint/depth state while another
2318
+ // operation is suspended, which could otherwise corrupt cleanup logic.
2319
+ async #runSerialized(tx, callback) {
2320
+ const previousOperation = tx.operationQueue;
2321
+ let releaseOperationLock;
2322
+ tx.operationQueue = new Promise((resolve) => {
2323
+ releaseOperationLock = resolve;
2324
+ });
2325
+ await previousOperation;
2326
+ try {
2327
+ return await callback();
2328
+ } finally {
2329
+ releaseOperationLock();
2330
+ }
2331
+ }
2182
2332
  async #closeTransaction(tx, status) {
2183
2333
  const createClosingPromise = async () => {
2184
2334
  debug("Closing transaction.", { transactionId: tx.id, status });
@@ -2266,7 +2416,7 @@ function createTimeoutIfDefined(cb, ms) {
2266
2416
  UserFacingError,
2267
2417
  applySqlCommenters,
2268
2418
  convertCompactedRows,
2269
- deserializeJsonResponse,
2419
+ deserializeJsonObject,
2270
2420
  doKeysMatch,
2271
2421
  isDeepStrictEqual,
2272
2422
  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
  );
@@ -1070,7 +1085,10 @@ function renderFragment(fragment, placeholderFormat, ctx) {
1070
1085
  case "stringChunk":
1071
1086
  return fragment.chunk;
1072
1087
  case "parameterTuple": {
1073
- const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => formatPlaceholder(placeholderFormat, ctx.placeholderNumber++)).join(",");
1088
+ const placeholders = fragment.value.length == 0 ? "NULL" : fragment.value.map(() => {
1089
+ const item = formatPlaceholder(placeholderFormat, ctx.placeholderNumber++);
1090
+ return `${fragment.itemPrefix}${item}${fragment.itemSuffix}`;
1091
+ }).join(fragment.itemSeparator);
1074
1092
  return `(${placeholders})`;
1075
1093
  }
1076
1094
  case "parameterTupleList": {
@@ -1429,7 +1447,7 @@ function doesSatisfyRule(data, rule) {
1429
1447
  }
1430
1448
  }
1431
1449
  function renderMessage(data, error) {
1432
- switch (error.error_identifier) {
1450
+ switch (error.errorIdentifier) {
1433
1451
  case "RELATION_VIOLATION":
1434
1452
  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
1453
  case "MISSING_RECORD":
@@ -1449,7 +1467,7 @@ function renderMessage(data, error) {
1449
1467
  }
1450
1468
  }
1451
1469
  function getErrorCode2(error) {
1452
- switch (error.error_identifier) {
1470
+ switch (error.errorIdentifier) {
1453
1471
  case "RELATION_VIOLATION":
1454
1472
  return "P2014";
1455
1473
  case "RECORDS_NOT_CONNECTED":
@@ -2021,13 +2039,42 @@ var TransactionManager = class {
2021
2039
  );
2022
2040
  }
2023
2041
  async #startTransactionImpl(options) {
2042
+ if (options.newTxId) {
2043
+ return await this.#withActiveTransactionLock(options.newTxId, "start", async (existing) => {
2044
+ if (existing.status !== "running") {
2045
+ throw new TransactionInternalConsistencyError(
2046
+ `Transaction in invalid state ${existing.status} when starting a nested transaction.`
2047
+ );
2048
+ }
2049
+ if (!existing.transaction) {
2050
+ throw new TransactionInternalConsistencyError(
2051
+ `Transaction missing underlying driver transaction when starting a nested transaction.`
2052
+ );
2053
+ }
2054
+ existing.depth += 1;
2055
+ const savepointName = this.#nextSavepointName(existing);
2056
+ existing.savepoints.push(savepointName);
2057
+ try {
2058
+ await this.#requiredCreateSavepoint(existing.transaction)(savepointName);
2059
+ } catch (e) {
2060
+ existing.depth -= 1;
2061
+ existing.savepoints.pop();
2062
+ throw e;
2063
+ }
2064
+ return { id: existing.id };
2065
+ });
2066
+ }
2024
2067
  const transaction = {
2025
2068
  id: await randomUUID(),
2026
2069
  status: "waiting",
2027
2070
  timer: void 0,
2028
2071
  timeout: options.timeout,
2029
2072
  startedAt: Date.now(),
2030
- transaction: void 0
2073
+ transaction: void 0,
2074
+ operationQueue: Promise.resolve(),
2075
+ depth: 1,
2076
+ savepoints: [],
2077
+ savepointCounter: 0
2031
2078
  };
2032
2079
  const abortController = new AbortController();
2033
2080
  const startTimer = createTimeoutIfDefined(() => abortController.abort(), options.maxWait);
@@ -2061,14 +2108,49 @@ var TransactionManager = class {
2061
2108
  }
2062
2109
  async commitTransaction(transactionId) {
2063
2110
  return await this.tracingHelper.runInChildSpan("commit_transaction", async () => {
2064
- const txw = this.#getActiveOrClosingTransaction(transactionId, "commit");
2065
- await this.#closeTransaction(txw, "committed");
2111
+ await this.#withActiveTransactionLock(transactionId, "commit", async (txw) => {
2112
+ if (txw.depth > 1) {
2113
+ if (!txw.transaction) throw new TransactionNotFoundError();
2114
+ const savepointName = txw.savepoints.at(-1);
2115
+ if (!savepointName) {
2116
+ throw new TransactionInternalConsistencyError(
2117
+ `Missing savepoint for nested commit. Depth: ${txw.depth}, transactionId: ${txw.id}`
2118
+ );
2119
+ }
2120
+ try {
2121
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2122
+ } finally {
2123
+ txw.savepoints.pop();
2124
+ txw.depth -= 1;
2125
+ }
2126
+ return;
2127
+ }
2128
+ await this.#closeTransaction(txw, "committed");
2129
+ });
2066
2130
  });
2067
2131
  }
2068
2132
  async rollbackTransaction(transactionId) {
2069
2133
  return await this.tracingHelper.runInChildSpan("rollback_transaction", async () => {
2070
- const txw = this.#getActiveOrClosingTransaction(transactionId, "rollback");
2071
- await this.#closeTransaction(txw, "rolled_back");
2134
+ await this.#withActiveTransactionLock(transactionId, "rollback", async (txw) => {
2135
+ if (txw.depth > 1) {
2136
+ if (!txw.transaction) throw new TransactionNotFoundError();
2137
+ const savepointName = txw.savepoints.at(-1);
2138
+ if (!savepointName) {
2139
+ throw new TransactionInternalConsistencyError(
2140
+ `Missing savepoint for nested rollback. Depth: ${txw.depth}, transactionId: ${txw.id}`
2141
+ );
2142
+ }
2143
+ try {
2144
+ await this.#requiredRollbackToSavepoint(txw.transaction)(savepointName);
2145
+ await this.#releaseSavepoint(txw.transaction, savepointName);
2146
+ } finally {
2147
+ txw.savepoints.pop();
2148
+ txw.depth -= 1;
2149
+ }
2150
+ return;
2151
+ }
2152
+ await this.#closeTransaction(txw, "rolled_back");
2153
+ });
2072
2154
  });
2073
2155
  }
2074
2156
  async getTransaction(txInfo, operation) {
@@ -2112,22 +2194,90 @@ var TransactionManager = class {
2112
2194
  return transaction;
2113
2195
  }
2114
2196
  async cancelAllTransactions() {
2115
- await Promise.allSettled([...this.transactions.values()].map((tx) => this.#closeTransaction(tx, "rolled_back")));
2197
+ await Promise.allSettled(
2198
+ [...this.transactions.values()].map(
2199
+ (tx) => this.#runSerialized(tx, async () => {
2200
+ const current = this.transactions.get(tx.id);
2201
+ if (current) {
2202
+ await this.#closeTransaction(current, "rolled_back");
2203
+ }
2204
+ })
2205
+ )
2206
+ );
2207
+ }
2208
+ #nextSavepointName(transaction) {
2209
+ return `prisma_sp_${transaction.savepointCounter++}`;
2210
+ }
2211
+ #requiredCreateSavepoint(transaction) {
2212
+ if (transaction.createSavepoint) {
2213
+ return transaction.createSavepoint.bind(transaction);
2214
+ }
2215
+ throw new TransactionManagerError(
2216
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): createSavepoint is not implemented.`
2217
+ );
2218
+ }
2219
+ #requiredRollbackToSavepoint(transaction) {
2220
+ if (transaction.rollbackToSavepoint) {
2221
+ return transaction.rollbackToSavepoint.bind(transaction);
2222
+ }
2223
+ throw new TransactionManagerError(
2224
+ `Nested transactions are not supported by adapter "${transaction.adapterName}" (${transaction.provider}): rollbackToSavepoint is not implemented.`
2225
+ );
2226
+ }
2227
+ async #releaseSavepoint(transaction, name) {
2228
+ if (transaction.releaseSavepoint) {
2229
+ await transaction.releaseSavepoint(name);
2230
+ }
2231
+ }
2232
+ #debugTransactionAlreadyClosedOnTimeout(transactionId) {
2233
+ debug("Transaction already committed or rolled back when timeout happened.", transactionId);
2116
2234
  }
2117
2235
  #startTransactionTimeout(transactionId, timeout) {
2118
2236
  const timeoutStartedAt = Date.now();
2119
2237
  const timer = createTimeoutIfDefined(async () => {
2120
2238
  debug("Transaction timed out.", { transactionId, timeoutStartedAt, timeout });
2121
2239
  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
- }
2240
+ if (!tx) {
2241
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2242
+ return;
2243
+ }
2244
+ await this.#runSerialized(tx, async () => {
2245
+ const current = this.transactions.get(transactionId);
2246
+ if (current && ["running", "waiting"].includes(current.status)) {
2247
+ await this.#closeTransaction(current, "timed_out");
2248
+ } else {
2249
+ this.#debugTransactionAlreadyClosedOnTimeout(transactionId);
2250
+ }
2251
+ });
2127
2252
  }, timeout);
2128
2253
  timer?.unref?.();
2129
2254
  return timer;
2130
2255
  }
2256
+ // Any operation that mutates or closes a transaction must run through this lock so
2257
+ // status/savepoint/depth checks and updates happen against a stable view of state.
2258
+ async #withActiveTransactionLock(transactionId, operation, callback) {
2259
+ const tx = this.#getActiveOrClosingTransaction(transactionId, operation);
2260
+ return await this.#runSerialized(tx, async () => {
2261
+ const current = this.#getActiveOrClosingTransaction(transactionId, operation);
2262
+ return await callback(current);
2263
+ });
2264
+ }
2265
+ // Serializes operations per transaction id to prevent interleaving across awaits.
2266
+ // This avoids races where one operation mutates savepoint/depth state while another
2267
+ // operation is suspended, which could otherwise corrupt cleanup logic.
2268
+ async #runSerialized(tx, callback) {
2269
+ const previousOperation = tx.operationQueue;
2270
+ let releaseOperationLock;
2271
+ tx.operationQueue = new Promise((resolve) => {
2272
+ releaseOperationLock = resolve;
2273
+ });
2274
+ await previousOperation;
2275
+ try {
2276
+ return await callback();
2277
+ } finally {
2278
+ releaseOperationLock();
2279
+ }
2280
+ }
2131
2281
  async #closeTransaction(tx, status) {
2132
2282
  const createClosingPromise = async () => {
2133
2283
  debug("Closing transaction.", { transactionId: tx.id, status });
@@ -2214,7 +2364,7 @@ export {
2214
2364
  UserFacingError,
2215
2365
  applySqlCommenters,
2216
2366
  convertCompactedRows,
2217
- deserializeJsonResponse,
2367
+ deserializeJsonObject,
2218
2368
  doKeysMatch,
2219
2369
  isDeepStrictEqual,
2220
2370
  isPrismaValueGenerator,
@@ -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;
@@ -227,14 +230,14 @@ export type DataRule = {
227
230
  type: 'never';
228
231
  };
229
232
  export type ValidationError = {
230
- error_identifier: 'RELATION_VIOLATION';
233
+ errorIdentifier: 'RELATION_VIOLATION';
231
234
  context: {
232
235
  relation: string;
233
236
  modelA: string;
234
237
  modelB: string;
235
238
  };
236
239
  } | {
237
- error_identifier: 'MISSING_RELATED_RECORD';
240
+ errorIdentifier: 'MISSING_RELATED_RECORD';
238
241
  context: {
239
242
  model: string;
240
243
  relation: string;
@@ -243,24 +246,24 @@ export type ValidationError = {
243
246
  neededFor?: string;
244
247
  };
245
248
  } | {
246
- error_identifier: 'MISSING_RECORD';
249
+ errorIdentifier: 'MISSING_RECORD';
247
250
  context: {
248
251
  operation: string;
249
252
  };
250
253
  } | {
251
- error_identifier: 'INCOMPLETE_CONNECT_INPUT';
254
+ errorIdentifier: 'INCOMPLETE_CONNECT_INPUT';
252
255
  context: {
253
256
  expectedRows: number;
254
257
  };
255
258
  } | {
256
- error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
259
+ errorIdentifier: 'INCOMPLETE_CONNECT_OUTPUT';
257
260
  context: {
258
261
  expectedRows: number;
259
262
  relation: string;
260
263
  relationType: string;
261
264
  };
262
265
  } | {
263
- error_identifier: 'RECORDS_NOT_CONNECTED';
266
+ errorIdentifier: 'RECORDS_NOT_CONNECTED';
264
267
  context: {
265
268
  relation: string;
266
269
  parent: string;
@@ -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/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.30",
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.30",
35
+ "@prisma/debug": "7.5.0-dev.30",
36
+ "@prisma/sqlcommenter": "7.5.0-dev.30",
37
+ "@prisma/driver-adapter-utils": "7.5.0-dev.30"
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
  }