diff --git a/packages/client/src/generation/TSClient/PrismaClient.ts b/packages/client/src/generation/TSClient/PrismaClient.ts
index 4e32671bf406..b553485a8359 100644
--- a/packages/client/src/generation/TSClient/PrismaClient.ts
+++ b/packages/client/src/generation/TSClient/PrismaClient.ts
@@ -11,6 +11,11 @@ import { Datasources } from './Datasources'
import type { Generatable } from './Generatable'
function batchingTransactionDefinition(this: PrismaClientClass) {
+ const args = ['arg: [...P]']
+ if (this.dmmf.hasEnumInNamespace('TransactionIsolationLevel', 'prisma')) {
+ args.push('options?: { isolationLevel?: Prisma.TransactionIsolationLevel }')
+ }
+ const argsString = args.join(', ')
return `
/**
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
@@ -25,7 +30,7 @@ function batchingTransactionDefinition(this: PrismaClientClass) {
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
*/
- $transaction
[]>(arg: [...P]): Promise>;`
+ $transaction[]>(${argsString}): Promise>;`
}
function interactiveTransactionDefinition(this: PrismaClientClass) {
diff --git a/packages/client/src/runtime/RequestHandler.ts b/packages/client/src/runtime/RequestHandler.ts
index 722b98e413e4..5dfafc97404c 100644
--- a/packages/client/src/runtime/RequestHandler.ts
+++ b/packages/client/src/runtime/RequestHandler.ts
@@ -9,6 +9,7 @@ import {
PrismaClientRustPanicError,
PrismaClientUnknownRequestError,
} from '.'
+import { PrismaPromiseTransaction } from './core/request/PrismaPromise'
import { DataLoader } from './DataLoader'
import type { Client, Unpacker } from './getPrismaClient'
import type { EngineMiddleware } from './MiddlewareHandler'
@@ -30,11 +31,10 @@ export type RequestParams = {
clientMethod: string
callsite?: CallSite
rejectOnNotFound?: RejectOnNotFound
- runInTransaction?: boolean
+ transaction?: PrismaPromiseTransaction
engineHook?: EngineMiddleware
args: any
headers?: Record
- transactionId?: string | number
unpacker?: Unpacker
otelParentCtx?: Context
otelChildCtx?: Context
@@ -48,8 +48,7 @@ export type HandleErrorParams = {
export type Request = {
document: Document
- runInTransaction?: boolean
- transactionId?: string | number
+ transaction?: PrismaPromiseTransaction
headers?: Record
otelParentCtx?: Context
otelChildCtx?: Context
@@ -57,20 +56,22 @@ export type Request = {
}
function getRequestInfo(request: Request) {
- const txId = request.transactionId
- const inTx = request.runInTransaction
+ const transaction = request.transaction
const headers = request.headers ?? {}
const traceparent = getTraceParent({ tracingConfig: request.tracingConfig })
- // if the tx has a number for an id, then it's a regular batch tx
- const _inTx = typeof txId === 'number' && inTx ? true : undefined
- // if the tx has a string for id, it's an interactive transaction
- const _txId = typeof txId === 'string' && inTx ? txId : undefined
+ if (transaction?.kind === 'itx') {
+ headers.transactionId = transaction.id
+ }
- if (_txId !== undefined) headers.transactionId = _txId
- if (traceparent !== undefined) headers.traceparent = traceparent
+ if (traceparent !== undefined) {
+ headers.traceparent = traceparent
+ }
- return { inTx: _inTx, headers }
+ return {
+ batchTransaction: transaction?.kind === 'batch' ? transaction : undefined,
+ headers,
+ }
}
export class RequestHandler {
@@ -91,7 +92,9 @@ export class RequestHandler {
// TODO: pass the child information to QE for it to issue links to queries
// const links = requests.map((r) => trace.getSpanContext(r.otelChildCtx!))
- return this.client._engine.requestBatch(queries, info.headers, info.inTx)
+ return this.client._engine.requestBatch(queries, info.headers, {
+ isolationLevel: info.batchTransaction?.isolationLevel,
+ })
},
singleLoader: (request) => {
const info = getRequestInfo(request)
@@ -100,8 +103,8 @@ export class RequestHandler {
return this.client._engine.request(query, info.headers)
},
batchBy: (request) => {
- if (request.transactionId) {
- return `transaction-${request.transactionId}`
+ if (request.transaction?.id) {
+ return `transaction-${request.transaction.id}`
}
return batchFindUniqueBy(request)
@@ -118,11 +121,10 @@ export class RequestHandler {
callsite,
rejectOnNotFound,
clientMethod,
- runInTransaction,
engineHook,
args,
headers,
- transactionId,
+ transaction,
unpacker,
otelParentCtx,
otelChildCtx,
@@ -149,18 +151,19 @@ export class RequestHandler {
const result = await engineHook(
{
document,
- runInTransaction,
+ runInTransaction: Boolean(transaction),
+ },
+ (params) => {
+ return this.dataloader.request({ ...params, tracingConfig: this.client._tracingConfig })
},
- (params) => this.dataloader.request({ ...params, tracingConfig: this.client._tracingConfig }),
)
data = result.data
elapsed = result.elapsed
} else {
const result = await this.dataloader.request({
document,
- runInTransaction,
headers,
- transactionId,
+ transaction,
otelParentCtx,
otelChildCtx,
tracingConfig: this.client._tracingConfig,
diff --git a/packages/client/src/runtime/core/model/applyModel.ts b/packages/client/src/runtime/core/model/applyModel.ts
index a355fafaf7fc..04bb6cd4729c 100644
--- a/packages/client/src/runtime/core/model/applyModel.ts
+++ b/packages/client/src/runtime/core/model/applyModel.ts
@@ -61,13 +61,27 @@ export function applyModel(client: Client, dmmfModelName: string) {
const action = (paramOverrides: O.Optional) => (userArgs?: UserArgs) => {
const callSite = getCallSite(client._errorFormat) // used for showing better errors
- return createPrismaPromise((txId, lock) => {
- const data = { args: userArgs, dataPath: [] } // data and its dataPath for nested results
- const action = { action: dmmfActionName, model: dmmfModelName } // action name and its related model
- const method = { clientMethod: `${jsModelName}.${prop}`, jsModelName } // method name for display only
- const tx = { runInTransaction: !!txId, transactionId: txId, lock } // transaction information
- const trace = { callsite: callSite } // stack trace
- const params = { ...data, ...action, ...method, ...tx, ...trace }
+ return createPrismaPromise((transaction, lock) => {
+ const params: InternalRequestParams = {
+ // data and its dataPath for nested results
+ args: userArgs,
+ dataPath: [],
+
+ // action name and its related model
+ action: dmmfActionName,
+ model: dmmfModelName,
+
+ // method name for display only
+ clientMethod: `${jsModelName}.${prop}`,
+ jsModelName,
+
+ // transaction information
+ transaction,
+ lock,
+
+ // stack trace
+ callsite: callSite,
+ }
return requestFn({ ...params, ...paramOverrides })
})
diff --git a/packages/client/src/runtime/core/request/PrismaPromise.ts b/packages/client/src/runtime/core/request/PrismaPromise.ts
index 0334384955f8..fa62d2b53775 100644
--- a/packages/client/src/runtime/core/request/PrismaPromise.ts
+++ b/packages/client/src/runtime/core/request/PrismaPromise.ts
@@ -1,3 +1,21 @@
+import { IsolationLevel } from '@prisma/engine-core'
+
+export type PrismaPromiseBatchTransaction = {
+ kind: 'batch'
+ id: number
+ isolationLevel?: IsolationLevel
+}
+
+export type PrismaPromiseInteractiveTransaction = {
+ kind: 'itx'
+ id: string
+}
+
+export type PrismaPromiseTransaction = PrismaPromiseBatchTransaction | PrismaPromiseInteractiveTransaction
+
+export type BatchTransactionOptions = Omit
+export type InteractiveTransactionOptions = Omit
+
/**
* Prisma's `Promise` that is backwards-compatible. All additions on top of the
* original `Promise` are optional so that it can be backwards-compatible.
@@ -8,31 +26,34 @@ export interface PrismaPromise extends Promise {
* Extension of the original `.then` function
* @param onfulfilled same as regular promises
* @param onrejected same as regular promises
- * @param txId GUID for interactive txs
+ * @param transaction interactive transaction options
*/
then(
onfulfilled?: (value: A) => R1 | PromiseLike,
onrejected?: (error: unknown) => R2 | PromiseLike,
- txId?: string,
+ transaction?: InteractiveTransactionOptions,
): Promise
/**
* Extension of the original `.catch` function
* @param onrejected same as regular promises
- * @param txId GUID for interactive txs
+ * @param transaction interactive transaction options
*/
- catch(onrejected?: ((reason: any) => R | PromiseLike) | undefined | null, txId?: string): Promise
+ catch(
+ onrejected?: ((reason: any) => R | PromiseLike) | undefined | null,
+ transaction?: InteractiveTransactionOptions,
+ ): Promise
/**
* Extension of the original `.finally` function
* @param onfinally same as regular promises
- * @param txId GUID for interactive txs
+ * @param transaction interactive transaction options
*/
- finally(onfinally?: (() => void) | undefined | null, txId?: string): Promise
+ finally(onfinally?: (() => void) | undefined | null, transaction?: InteractiveTransactionOptions): Promise
/**
* Called when executing a batch of regular tx
- * @param txId for regular tx ids
+ * @param transaction transaction options for regular tx
*/
- requestTransaction?(txId: number, lock?: PromiseLike): PromiseLike
+ requestTransaction?(transaction: BatchTransactionOptions, lock?: PromiseLike): PromiseLike
}
diff --git a/packages/client/src/runtime/core/request/createPrismaPromise.ts b/packages/client/src/runtime/core/request/createPrismaPromise.ts
index de2401aeb5d1..2f2746b628ea 100644
--- a/packages/client/src/runtime/core/request/createPrismaPromise.ts
+++ b/packages/client/src/runtime/core/request/createPrismaPromise.ts
@@ -1,4 +1,4 @@
-import type { PrismaPromise } from './PrismaPromise'
+import type { InteractiveTransactionOptions, PrismaPromise, PrismaPromiseTransaction } from './PrismaPromise'
/**
* Creates a [[PrismaPromise]]. It is Prisma's implementation of `Promise` which
@@ -10,18 +10,18 @@ import type { PrismaPromise } from './PrismaPromise'
* @returns
*/
export function createPrismaPromise(
- callback: (txId?: string | number, lock?: PromiseLike) => PrismaPromise,
+ callback: (transaction?: PrismaPromiseTransaction, lock?: PromiseLike) => PrismaPromise,
): PrismaPromise {
let promise: PrismaPromise | undefined
- const _callback = (txId?: string | number, lock?: PromiseLike, cached = true) => {
+ const _callback = (transaction?: PrismaPromiseTransaction, lock?: PromiseLike, cached = true) => {
try {
// promises cannot be triggered twice after resolving
if (cached === true) {
- return (promise ??= callback(txId, lock))
+ return (promise ??= callback(transaction, lock))
}
// but for for batch tx we need to trigger them again
- return callback(txId, lock)
+ return callback(transaction, lock)
} catch (error) {
// if the callback throws, then we reject the promise
// and that is because exceptions are not always async
@@ -30,21 +30,23 @@ export function createPrismaPromise(
}
return {
- then(onFulfilled, onRejected, txId?: string) {
- return _callback(txId, undefined).then(onFulfilled, onRejected, txId)
+ then(onFulfilled, onRejected, transaction?) {
+ return _callback(createItx(transaction), undefined).then(onFulfilled, onRejected, transaction)
},
- catch(onRejected, txId?: string) {
- return _callback(txId, undefined).catch(onRejected, txId)
+ catch(onRejected, transaction?) {
+ return _callback(createItx(transaction), undefined).catch(onRejected, transaction)
},
- finally(onFinally, txId?: string) {
- return _callback(txId, undefined).finally(onFinally, txId)
+ finally(onFinally, transaction?) {
+ return _callback(createItx(transaction), undefined).finally(onFinally, transaction)
},
- requestTransaction(txId: number, lock?: PromiseLike) {
- const promise = _callback(txId, lock, false)
+
+ requestTransaction(transactionOptions, lock?: PromiseLike) {
+ const transaction = { kind: 'batch' as const, ...transactionOptions }
+ const promise = _callback(transaction, lock, false)
if (promise.requestTransaction) {
// we want to have support for nested promises
- return promise.requestTransaction(txId, lock)
+ return promise.requestTransaction(transaction, lock)
}
return promise
@@ -52,3 +54,10 @@ export function createPrismaPromise(
[Symbol.toStringTag]: 'PrismaPromise',
}
}
+
+function createItx(transaction: InteractiveTransactionOptions | undefined): PrismaPromiseTransaction | undefined {
+ if (transaction) {
+ return { kind: 'itx', ...transaction }
+ }
+ return undefined
+}
diff --git a/packages/client/src/runtime/getPrismaClient.ts b/packages/client/src/runtime/getPrismaClient.ts
index 423f970c8a9f..86c91251d46b 100644
--- a/packages/client/src/runtime/getPrismaClient.ts
+++ b/packages/client/src/runtime/getPrismaClient.ts
@@ -1,6 +1,7 @@
import { Context, context } from '@opentelemetry/api'
import Debug from '@prisma/debug'
import {
+ BatchTransactionOptions,
BinaryEngine,
DataProxyEngine,
DatasourceOverwrite,
@@ -29,7 +30,11 @@ import { PrismaClientValidationError } from '.'
import { MetricsClient } from './core/metrics/MetricsClient'
import { applyModels } from './core/model/applyModels'
import { createPrismaPromise } from './core/request/createPrismaPromise'
-import type { PrismaPromise } from './core/request/PrismaPromise'
+import type {
+ InteractiveTransactionOptions,
+ PrismaPromise,
+ PrismaPromiseTransaction,
+} from './core/request/PrismaPromise'
import { getLockCountPromise } from './core/transaction/utils/createLockCountPromise'
import { BaseDMMFHelper, DMMFHelper } from './dmmf'
import type { DMMF } from './dmmf-types'
@@ -171,11 +176,11 @@ export type InternalRequestParams = {
callsite?: CallSite
/** Headers metadata that will be passed to the Engine */
headers?: Record // TODO what is this
- transactionId?: string | number
+ transaction?: PrismaPromiseTransaction
unpacker?: Unpacker // TODO what is this
lock?: PromiseLike
otelParentCtx?: Context
-} & QueryMiddlewareParams
+} & Omit
// only used by the .use() hooks
export type AllHookArgs = {
@@ -607,7 +612,7 @@ export function getPrismaClient(config: GetPrismaClientConfig) {
* Executes a raw query and always returns a number
*/
private $executeRawInternal(
- txId: string | number | undefined,
+ transaction: PrismaPromiseTransaction | undefined,
lock: PromiseLike | undefined,
query: string | TemplateStringsArray | Sql,
...values: RawValue[]
@@ -702,8 +707,7 @@ export function getPrismaClient(config: GetPrismaClientConfig) {
dataPath: [],
action: 'executeRaw',
callsite: getCallSite(this._errorFormat),
- runInTransaction: !!txId,
- transactionId: txId,
+ transaction,
lock,
})
}
@@ -717,9 +721,9 @@ export function getPrismaClient(config: GetPrismaClientConfig) {
* @returns
*/
$executeRaw(query: TemplateStringsArray | Sql, ...values: any[]) {
- return createPrismaPromise((txId, lock) => {
+ return createPrismaPromise((transaction, lock) => {
if ((query as TemplateStringsArray).raw !== undefined || (query as Sql).sql !== undefined) {
- return this.$executeRawInternal(txId, lock, query, ...values)
+ return this.$executeRawInternal(transaction, lock, query, ...values)
}
throw new PrismaClientValidationError(`\`$executeRaw\` is a tag function, please use it like the following:
@@ -741,8 +745,8 @@ Or read our docs at https://www.prisma.io/docs/concepts/components/prisma-client
* @returns
*/
$executeRawUnsafe(query: string, ...values: RawValue[]) {
- return createPrismaPromise((txId, lock) => {
- return this.$executeRawInternal(txId, lock, query, ...values)
+ return createPrismaPromise((transaction, lock) => {
+ return this.$executeRawInternal(transaction, lock, query, ...values)
})
}
@@ -759,15 +763,14 @@ Or read our docs at https://www.prisma.io/docs/concepts/components/prisma-client
)
}
- return createPrismaPromise((txId, lock) => {
+ return createPrismaPromise((transaction, lock) => {
return this._request({
args: { command: command },
clientMethod: '$runCommandRaw',
dataPath: [],
action: 'runCommandRaw',
callsite: getCallSite(this._errorFormat),
- runInTransaction: !!txId,
- transactionId: txId,
+ transaction: transaction,
lock,
})
})
@@ -777,7 +780,7 @@ Or read our docs at https://www.prisma.io/docs/concepts/components/prisma-client
* Executes a raw query and returns selected data
*/
private $queryRawInternal(
- txId: string | number | undefined,
+ transaction: PrismaPromiseTransaction | undefined,
lock: PromiseLike | undefined,
query: string | TemplateStringsArray | Sql,
...values: RawValue[]
@@ -875,8 +878,7 @@ Or read our docs at https://www.prisma.io/docs/concepts/components/prisma-client
dataPath: [],
action: 'queryRaw',
callsite: getCallSite(this._errorFormat),
- runInTransaction: !!txId,
- transactionId: txId,
+ transaction,
lock,
}).then(deserializeRawResults)
}
@@ -943,7 +945,6 @@ new PrismaClient({
},
clientMethod: 'queryRaw',
dataPath: [],
- runInTransaction: false,
headers,
callsite: getCallSite(this._errorFormat),
})
@@ -954,7 +955,13 @@ new PrismaClient({
* @param requests
* @param options
*/
- private _transactionWithArray(promises: Array>): Promise {
+ private _transactionWithArray({
+ promises,
+ options,
+ }: {
+ promises: Array>
+ options?: BatchTransactionOptions
+ }): Promise {
const txId = this._transactionId++
const lock = getLockCountPromise(promises.length)
@@ -965,7 +972,7 @@ new PrismaClient({
)
}
- return request.requestTransaction?.(txId, lock)
+ return request.requestTransaction?.({ id: txId, isolationLevel: options?.isolationLevel }, lock)
})
return Promise.all(requests)
@@ -990,7 +997,7 @@ new PrismaClient({
let result: unknown
try {
// execute user logic with a proxied the client
- result = await callback(transactionProxy(this, info.id))
+ result = await callback(transactionProxy(this, { id: info.id }))
// it went well, then we commit the transaction
await this._engine.transaction('commit', headers, info)
@@ -1016,7 +1023,7 @@ new PrismaClient({
if (typeof input === 'function' && this._hasPreviewFlag('interactiveTransactions')) {
callback = () => this._transactionWithCallback({ callback: input, options })
} else {
- callback = () => this._transactionWithArray(input)
+ callback = () => this._transactionWithArray({ promises: input, options })
}
const spanOptions = {
@@ -1042,7 +1049,7 @@ new PrismaClient({
const params: QueryMiddlewareParams = {
args: internalParams.args,
dataPath: internalParams.dataPath,
- runInTransaction: internalParams.runInTransaction,
+ runInTransaction: Boolean(internalParams.transaction),
action: internalParams.action,
model: internalParams.model,
}
@@ -1068,7 +1075,7 @@ new PrismaClient({
let index = -1
// prepare recursive fn that will pipe params through middlewares
- const consumer = (changedParams: QueryMiddlewareParams) => {
+ const consumer = (changedMiddlewareParams: QueryMiddlewareParams) => {
// if this `next` was called and there's some more middlewares
const nextMiddleware = this._middlewares.query.get(++index)
@@ -1077,13 +1084,25 @@ new PrismaClient({
// calling `next` calls the consumer again with the new params
return runInChildSpan(spanOptions.middleware, async (span) => {
// we call `span.end()` _before_ calling the next middleware
- return nextMiddleware(changedParams, (p) => (span?.end(), consumer(p)))
+ return nextMiddleware(changedMiddlewareParams, (p) => (span?.end(), consumer(p)))
})
}
// no middleware? then we just proceed with request execution
// before we send the execution request, we use the changed params
- return this._executeRequest({ ...internalParams, ...changedParams })
+ const { runInTransaction, ...changedRequestParams } = changedMiddlewareParams
+ const requestParams = {
+ ...internalParams,
+ ...changedRequestParams,
+ }
+
+ // if middleware switched off `runInTransaction`, unset
+ // `transaction` property on request as well so it will be executed outside
+ // of transaction
+ if (!runInTransaction) {
+ requestParams.transaction = undefined
+ }
+ return this._executeRequest(requestParams)
}
return await runInChildSpan(spanOptions.operation, () => {
@@ -1107,11 +1126,10 @@ new PrismaClient({
jsModelName,
dataPath,
callsite,
- runInTransaction,
action,
model,
headers,
- transactionId,
+ transaction,
lock,
unpacker,
otelParentCtx,
@@ -1205,9 +1223,8 @@ new PrismaClient({
callsite,
args,
engineHook: this._middlewares.engine.get(0),
- runInTransaction,
headers,
- transactionId,
+ transaction,
unpacker,
otelParentCtx,
otelChildCtx: context.active(),
@@ -1250,10 +1267,10 @@ const forbidden = ['$connect', '$disconnect', '$on', '$transaction', '$use']
/**
* Proxy that takes over the client promises to pass `txId`
* @param thing to be proxied
- * @param txId to be passed down to {@link RequestHandler}
+ * @param transaction to be passed down to {@link RequestHandler}
* @returns
*/
-function transactionProxy(thing: T, txId: string): T {
+function transactionProxy(thing: T, transaction: InteractiveTransactionOptions): T {
// we only wrap within a proxy if it's possible: if it's an object
if (typeof thing !== 'object') return thing
@@ -1262,23 +1279,23 @@ function transactionProxy(thing: T, txId: string): T {
// we don't want to allow any calls to our `forbidden` methods
if (forbidden.includes(prop as string)) return undefined
- if (prop === TX_ID) return txId // secret accessor to the txId
+ if (prop === TX_ID) return transaction?.id // secret accessor to the txId
// we override and handle every function call within the proxy
if (typeof target[prop] === 'function') {
return (...args: unknown[]) => {
// we hijack promise calls to pass txId to prisma promises
- if (prop === 'then') return target[prop](args[0], args[1], txId)
- if (prop === 'catch') return target[prop](args[0], txId)
- if (prop === 'finally') return target[prop](args[0], txId)
+ if (prop === 'then') return target[prop](args[0], args[1], transaction)
+ if (prop === 'catch') return target[prop](args[0], transaction)
+ if (prop === 'finally') return target[prop](args[0], transaction)
// if it's not the end promise, result is also tx-proxied
- return transactionProxy(target[prop](...args), txId)
+ return transactionProxy(target[prop](...args), transaction)
}
}
// if it's an object prop, then we keep on making it proxied
- return transactionProxy(target[prop], txId)
+ return transactionProxy(target[prop], transaction)
},
}) as any as T
}
diff --git a/packages/client/tests/functional/batch-transaction-isolation-level/_matrix.ts b/packages/client/tests/functional/batch-transaction-isolation-level/_matrix.ts
new file mode 100644
index 000000000000..09ff5e9d6352
--- /dev/null
+++ b/packages/client/tests/functional/batch-transaction-isolation-level/_matrix.ts
@@ -0,0 +1,15 @@
+import { defineMatrix } from '../_utils/defineMatrix'
+
+export default defineMatrix(() => [
+ [
+ {
+ provider: 'postgresql',
+ },
+ {
+ provider: 'mysql',
+ },
+ {
+ provider: 'sqlserver',
+ },
+ ],
+])
diff --git a/packages/client/tests/functional/batch-transaction-isolation-level/prisma/_schema.ts b/packages/client/tests/functional/batch-transaction-isolation-level/prisma/_schema.ts
new file mode 100644
index 000000000000..8ec523bbc6b6
--- /dev/null
+++ b/packages/client/tests/functional/batch-transaction-isolation-level/prisma/_schema.ts
@@ -0,0 +1,20 @@
+import { idForProvider } from '../../_utils/idForProvider'
+import testMatrix from '../_matrix'
+
+export default testMatrix.setupSchema(({ provider }) => {
+ return /* Prisma */ `
+ generator client {
+ provider = "prisma-client-js"
+ }
+
+ datasource db {
+ provider = "${provider}"
+ url = env("DATABASE_URI_${provider}")
+ }
+
+ model User {
+ id ${idForProvider(provider)}
+ email String
+ }
+ `
+})
diff --git a/packages/client/tests/functional/batch-transaction-isolation-level/tests.ts b/packages/client/tests/functional/batch-transaction-isolation-level/tests.ts
new file mode 100644
index 000000000000..bfcf7d77d47d
--- /dev/null
+++ b/packages/client/tests/functional/batch-transaction-isolation-level/tests.ts
@@ -0,0 +1,102 @@
+import { NewPrismaClient } from '../_utils/types'
+import testMatrix from './_matrix'
+// @ts-ignore
+import type { Prisma as PrismaNamespace, PrismaClient } from './node_modules/@prisma/client'
+
+declare let newPrismaClient: NewPrismaClient
+declare let Prisma: typeof PrismaNamespace
+
+testMatrix.setupTestSuite(
+ () => {
+ const queries: string[] = []
+ let prisma: PrismaClient
+
+ beforeAll(() => {
+ prisma = newPrismaClient({
+ log: [
+ {
+ emit: 'event',
+ level: 'query',
+ },
+ ],
+ })
+
+ prisma.$on('query', (event) => {
+ queries.push(event.query)
+ })
+ })
+
+ afterEach(() => {
+ queries.length = 0
+ })
+
+ const testIsolationLevel = (
+ name: string,
+ { level, expectSql }: { level: () => PrismaNamespace.TransactionIsolationLevel; expectSql: string },
+ ) => {
+ test(name, async () => {
+ await prisma.$transaction([prisma.user.findFirst({}), prisma.user.findFirst({})], {
+ isolationLevel: level(),
+ })
+
+ // eslint-disable-next-line jest/no-standalone-expect
+ expect(queries).toContain(expectSql)
+ })
+ }
+
+ testIsolationLevel('ReadUncommitted', {
+ level: () => Prisma.TransactionIsolationLevel.ReadUncommitted,
+ expectSql: 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED',
+ })
+
+ testIsolationLevel('ReadCommitted', {
+ level: () => Prisma.TransactionIsolationLevel.ReadCommitted,
+ expectSql: 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED',
+ })
+
+ testIsolationLevel('RepeatableRead', {
+ level: () => Prisma.TransactionIsolationLevel.RepeatableRead,
+ expectSql: 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ',
+ })
+
+ testIsolationLevel('Serializable', {
+ level: () => Prisma.TransactionIsolationLevel.Serializable,
+ expectSql: 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE',
+ })
+
+ test('default value generates no SET TRANSACTION ISOLATION LEVEL statements', async () => {
+ await prisma.$transaction([prisma.user.findFirst({}), prisma.user.findFirst({})])
+
+ expect(queries.find((q) => q.includes('SET TRANSACTION ISOLATION LEVEL'))).toBeUndefined()
+ })
+
+ test('invalid level generates run- and compile- time error', async () => {
+ const result = prisma.$transaction([prisma.user.findFirst({}), prisma.user.findFirst({})], {
+ // @ts-expect-error
+ isolationLevel: 'yes',
+ })
+
+ await expect(result).rejects.toMatchPrismaErrorInlineSnapshot(`
+
+ Invalid \`prisma.$transaction([prisma.user.findFirst()\` invocation in
+ /client/tests/functional/batch-transaction-isolalation-level/tests.ts:0:0
+
+ XX })
+ XX
+ XX test('invalid level generates run- and compile- time error', async () => {
+ → XX const result = prisma.$transaction([prisma.user.findFirst(
+ Inconsistent column data: Conversion failed: Invalid isolation level \`yes\`
+ `)
+ })
+ },
+ {
+ optOut: {
+ from: ['mongodb', 'sqlite', 'cockroachdb'],
+ reason: `
+ mongo - Not supported
+ sqlite, cockroach - Support only serializable level, never generate sql for setting isolation level
+ `,
+ },
+ skipDefaultClientInstance: true,
+ },
+)
diff --git a/packages/client/tests/functional/interactive-transactions/tests.ts b/packages/client/tests/functional/interactive-transactions/tests.ts
index af8135bde827..fcd223dbc45a 100644
--- a/packages/client/tests/functional/interactive-transactions/tests.ts
+++ b/packages/client/tests/functional/interactive-transactions/tests.ts
@@ -394,6 +394,33 @@ testMatrix.setupTestSuite(
expect(users.length).toBe(2)
})
+
+ test('middleware exclude from transaction', async () => {
+ const isolatedPrisma = newPrismaClient()
+
+ isolatedPrisma.$use((params, next) => {
+ return next({ ...params, runInTransaction: false })
+ })
+
+ await isolatedPrisma
+ .$transaction(async (prisma) => {
+ await prisma.user.create({
+ data: {
+ email: 'user_1@website.com',
+ },
+ })
+
+ await prisma.user.create({
+ data: {
+ email: 'user_1@website.com',
+ },
+ })
+ })
+ .catch((e) => {})
+
+ const users = await isolatedPrisma.user.findMany()
+ expect(users).toHaveLength(1)
+ })
})
/**
@@ -696,12 +723,13 @@ testMatrix.setupTestSuite(
// })
test('invalid value', async () => {
+ // @ts-test-if: provider === 'mongodb'
const result = prisma.$transaction(
async (tx) => {
await tx.user.create({ data: { email: 'user@example.com' } })
},
{
- // @ts-expect-error
+ // @ts-test-if: provider !== 'mongodb'
isolationLevel: 'NotAValidLevel',
},
)
@@ -719,12 +747,13 @@ testMatrix.setupTestSuite(
})
testIf(provider === 'mongodb')('attempt to set isolation level on mongo', async () => {
+ // @ts-test-if: provider === 'mongodb'
const result = prisma.$transaction(
async (tx) => {
await tx.user.create({ data: { email: 'user@example.com' } })
},
{
- // @ts-expect-error
+ // @ts-test-if: provider !== 'mongodb'
isolationLevel: 'CanBeAnything',
},
)
diff --git a/packages/engine-core/src/binary/BinaryEngine.ts b/packages/engine-core/src/binary/BinaryEngine.ts
index 845155d2b8ca..bf28a3c60f2f 100644
--- a/packages/engine-core/src/binary/BinaryEngine.ts
+++ b/packages/engine-core/src/binary/BinaryEngine.ts
@@ -17,7 +17,13 @@ import type { Readable } from 'stream'
import { URL } from 'url'
import { promisify } from 'util'
-import type { DatasourceOverwrite, EngineConfig, EngineEventType, GetConfigResult } from '../common/Engine'
+import type {
+ BatchTransactionOptions,
+ DatasourceOverwrite,
+ EngineConfig,
+ EngineEventType,
+ GetConfigResult,
+} from '../common/Engine'
import { Engine } from '../common/Engine'
import { PrismaClientInitializationError } from '../common/errors/PrismaClientInitializationError'
import { PrismaClientKnownRequestError } from '../common/errors/PrismaClientKnownRequestError'
@@ -29,7 +35,12 @@ import type { RustError, RustLog } from '../common/errors/utils/log'
import { convertLog, getMessage, isRustError, isRustErrorLog } from '../common/errors/utils/log'
import { prismaGraphQLToJSError } from '../common/errors/utils/prismaGraphQLToJSError'
import { EngineMetricsOptions, Metrics, MetricsOptionsJson, MetricsOptionsPrometheus } from '../common/types/Metrics'
-import type { EngineSpanEvent, QueryEngineRequestHeaders, QueryEngineResult } from '../common/types/QueryEngine'
+import type {
+ EngineSpanEvent,
+ QueryEngineBatchRequest,
+ QueryEngineRequestHeaders,
+ QueryEngineResult,
+} from '../common/types/QueryEngine'
import type * as Tx from '../common/types/Transaction'
import { printGeneratorConfig } from '../common/utils/printGeneratorConfig'
import { fixBinaryTargets, plusX } from '../common/utils/util'
@@ -964,14 +975,15 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file.
async requestBatch(
queries: string[],
headers: QueryEngineRequestHeaders = {},
- transaction = false,
+ transaction?: BatchTransactionOptions,
numTry = 1,
): Promise[]> {
await this.start()
- const request = {
+ const request: QueryEngineBatchRequest = {
batch: queries.map((query) => ({ query, variables: {} })),
- transaction,
+ transaction: Boolean(transaction),
+ isolationLevel: transaction?.isolationLevel,
}
this.lastQuery = JSON.stringify(request)
diff --git a/packages/engine-core/src/common/Engine.ts b/packages/engine-core/src/common/Engine.ts
index 23d4158721cd..c6dca9462ff1 100644
--- a/packages/engine-core/src/common/Engine.ts
+++ b/packages/engine-core/src/common/Engine.ts
@@ -18,6 +18,10 @@ export type InlineDatasource = {
url: NullableEnvValue
}
+export type BatchTransactionOptions = {
+ isolationLevel?: Transaction.IsolationLevel
+}
+
// TODO Move shared logic in here
export abstract class Engine {
abstract on(event: EngineEventType, listener: (args?: any) => any): void
@@ -34,7 +38,7 @@ export abstract class Engine {
abstract requestBatch(
queries: string[],
headers?: QueryEngineRequestHeaders,
- transaction?: boolean,
+ transaction?: BatchTransactionOptions,
numTry?: number,
): Promise[]>
abstract transaction(
diff --git a/packages/engine-core/src/common/types/QueryEngine.ts b/packages/engine-core/src/common/types/QueryEngine.ts
index 49787f0b6bdf..6b6cb0dbca59 100644
--- a/packages/engine-core/src/common/types/QueryEngine.ts
+++ b/packages/engine-core/src/common/types/QueryEngine.ts
@@ -1,5 +1,7 @@
import type { DataSource, GeneratorConfig } from '@prisma/generator-helper'
+import * as Transaction from './Transaction'
+
// Events
export type QueryEngineEvent = QueryEngineLogEvent | QueryEngineQueryEvent | QueryEnginePanicEvent | EngineSpanEvent
@@ -86,6 +88,7 @@ export type QueryEngineRequestHeaders = {
export type QueryEngineBatchRequest = {
batch: QueryEngineRequest[]
transaction: boolean
+ isolationLevel?: Transaction.IsolationLevel
}
export type GetConfigOptions = {
diff --git a/packages/engine-core/src/data-proxy/DataProxyEngine.ts b/packages/engine-core/src/data-proxy/DataProxyEngine.ts
index fa8db7130ffd..57e148e7f5df 100644
--- a/packages/engine-core/src/data-proxy/DataProxyEngine.ts
+++ b/packages/engine-core/src/data-proxy/DataProxyEngine.ts
@@ -2,10 +2,17 @@ import Debug from '@prisma/debug'
import { DMMF } from '@prisma/generator-helper'
import EventEmitter from 'events'
-import type { EngineConfig, EngineEventType, GetConfigResult, InlineDatasource } from '../common/Engine'
+import type {
+ BatchTransactionOptions,
+ EngineConfig,
+ EngineEventType,
+ GetConfigResult,
+ InlineDatasource,
+} from '../common/Engine'
import { Engine } from '../common/Engine'
import { prismaGraphQLToJSError } from '../common/errors/utils/prismaGraphQLToJSError'
import { EngineMetricsOptions, Metrics, MetricsOptionsJson, MetricsOptionsPrometheus } from '../common/types/Metrics'
+import { QueryEngineBatchRequest } from '../common/types/QueryEngine'
import { DataProxyError } from './errors/DataProxyError'
import { ForcedRetryError } from './errors/ForcedRetryError'
import { InvalidDatasourceError } from './errors/InvalidDatasourceError'
@@ -129,14 +136,21 @@ export class DataProxyEngine extends Engine {
return this.requestInternal({ query, variables: {} }, headers, attempt)
}
- async requestBatch(queries: string[], headers: Record, isTransaction = false, attempt = 0) {
+ async requestBatch(
+ queries: string[],
+ headers: Record,
+ transaction?: BatchTransactionOptions,
+ attempt = 0,
+ ) {
+ const isTransaction = Boolean(transaction)
this.logEmitter.emit('query', {
query: `Batch${isTransaction ? ' in transaction' : ''} (${queries.length}):\n${queries.join('\n')}`,
})
- const body = {
+ const body: QueryEngineBatchRequest = {
batch: queries.map((query) => ({ query, variables: {} })),
transaction: isTransaction,
+ isolationLevel: transaction?.isolationLevel,
}
const { batchResult } = await this.requestInternal(body, headers, attempt)
diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts
index 5c3a20a12b9e..2159e33929ae 100644
--- a/packages/engine-core/src/index.ts
+++ b/packages/engine-core/src/index.ts
@@ -2,6 +2,7 @@ export { BinaryEngine } from './binary/BinaryEngine'
export type { EngineConfig } from './common/Engine'
export type { EngineEventType } from './common/Engine'
export type { DatasourceOverwrite } from './common/Engine'
+export type { BatchTransactionOptions } from './common/Engine'
export { Engine } from './common/Engine'
export { PrismaClientInitializationError } from './common/errors/PrismaClientInitializationError'
export { PrismaClientKnownRequestError } from './common/errors/PrismaClientKnownRequestError'
diff --git a/packages/engine-core/src/library/LibraryEngine.ts b/packages/engine-core/src/library/LibraryEngine.ts
index a0ba9c0876e9..0c82ede916fa 100644
--- a/packages/engine-core/src/library/LibraryEngine.ts
+++ b/packages/engine-core/src/library/LibraryEngine.ts
@@ -6,7 +6,7 @@ import chalk from 'chalk'
import EventEmitter from 'events'
import fs from 'fs'
-import type { DatasourceOverwrite, EngineConfig, EngineEventType } from '../common/Engine'
+import type { BatchTransactionOptions, DatasourceOverwrite, EngineConfig, EngineEventType } from '../common/Engine'
import { Engine } from '../common/Engine'
import { PrismaClientInitializationError } from '../common/errors/PrismaClientInitializationError'
import { PrismaClientKnownRequestError } from '../common/errors/PrismaClientKnownRequestError'
@@ -489,13 +489,13 @@ You may have to run ${chalk.greenBright('prisma generate')} for your changes to
async requestBatch(
queries: string[],
headers: QueryEngineRequestHeaders = {},
- transaction = false,
- numTry = 1,
+ transaction?: BatchTransactionOptions,
): Promise[]> {
debug('requestBatch')
const request: QueryEngineBatchRequest = {
batch: queries.map((query) => ({ query, variables: {} })),
- transaction,
+ transaction: Boolean(transaction),
+ isolationLevel: transaction?.isolationLevel,
}
await this.start()