diff --git a/packages/client/src/runtime/RequestHandler.ts b/packages/client/src/runtime/RequestHandler.ts index fc9966422a34..09fbcdc27aba 100644 --- a/packages/client/src/runtime/RequestHandler.ts +++ b/packages/client/src/runtime/RequestHandler.ts @@ -208,7 +208,12 @@ export class RequestHandler { message = this.sanitizeMessage(message) // TODO: Do request with callsite instead, so we don't need to rethrow if (error.code) { - throw new PrismaClientKnownRequestError(message, error.code, this.client._clientVersion, error.meta) + throw new PrismaClientKnownRequestError(message, { + code: error.code, + clientVersion: this.client._clientVersion, + meta: error.meta, + batchRequestIdx: error.batchRequestIdx, + }) } else if (error.isPanic) { throw new PrismaClientRustPanicError(message, this.client._clientVersion) } else if (error instanceof PrismaClientUnknownRequestError) { diff --git a/packages/client/src/runtime/getPrismaClient.ts b/packages/client/src/runtime/getPrismaClient.ts index 3fc7f67fa5a5..193839ecfeb9 100644 --- a/packages/client/src/runtime/getPrismaClient.ts +++ b/packages/client/src/runtime/getPrismaClient.ts @@ -56,6 +56,7 @@ import type { InstanceRejectOnNotFound, RejectOnNotFound } from './utils/rejectO import { getRejectOnNotFound } from './utils/rejectOnNotFound' import { serializeRawParameters } from './utils/serializeRawParameters' import { validatePrismaClientOptions } from './utils/validatePrismaClientOptions' +import { waitForBatch } from './utils/waitForBatch' const debug = Debug('prisma:client') const ALTER_RE = /^(\s*alter\s)/i @@ -957,7 +958,7 @@ new PrismaClient({ return request.requestTransaction?.({ id: txId, isolationLevel: options?.isolationLevel }, lock) }) - return Promise.all(requests) + return waitForBatch(requests) } /** diff --git a/packages/client/src/runtime/utils/rejectOnNotFound.ts b/packages/client/src/runtime/utils/rejectOnNotFound.ts index e75384aa7dcd..5fd3f7274f5f 100644 --- a/packages/client/src/runtime/utils/rejectOnNotFound.ts +++ b/packages/client/src/runtime/utils/rejectOnNotFound.ts @@ -17,7 +17,7 @@ export type InstanceRejectOnNotFound = */ export class NotFoundError extends PrismaClientKnownRequestError { constructor(message: string) { - super(message, 'P2025', clientVersion) + super(message, { code: 'P2025', clientVersion }) this.name = 'NotFoundError' } } diff --git a/packages/client/src/runtime/utils/waitForBatch.ts b/packages/client/src/runtime/utils/waitForBatch.ts new file mode 100644 index 000000000000..3eceefef1820 --- /dev/null +++ b/packages/client/src/runtime/utils/waitForBatch.ts @@ -0,0 +1,36 @@ +import { hasBatchIndex } from '@prisma/engine-core' + +/** + * Waits for result of batch $transaction and picks the best possible error to report if any + * of the request fails. Best error is determined as follows: + * + * - if engine have reported and index of failed request in the batch, the best error is the one + * who's index matches that + * - otherwise, first error is used (like Promise.all) + * + * @param promises + * @returns + */ +export async function waitForBatch(promises: T): Promise<{ [K in keyof T]: Awaited }> { + const results = await Promise.allSettled(promises) + + let bestError: unknown = null + const successfulResults = [] as { [K in keyof T]: Awaited } + for (const [i, result] of results.entries()) { + if (result.status === 'rejected') { + const reason = result.reason + if (hasBatchIndex(reason) && reason.batchRequestIdx === i) { + bestError = reason + } else if (!bestError) { + bestError = reason + } + } else { + successfulResults.push(result.value) + } + } + + if (bestError) { + throw bestError + } + return successfulResults +} diff --git a/packages/client/tests/functional/issues/14373-batch-tx-error/_matrix.ts b/packages/client/tests/functional/issues/14373-batch-tx-error/_matrix.ts new file mode 100644 index 000000000000..7a6b8ff0075c --- /dev/null +++ b/packages/client/tests/functional/issues/14373-batch-tx-error/_matrix.ts @@ -0,0 +1,24 @@ +import { defineMatrix } from '../../_utils/defineMatrix' + +export default defineMatrix(() => [ + [ + { + provider: 'sqlite', + }, + { + provider: 'postgresql', + }, + { + provider: 'mysql', + }, + { + provider: 'mongodb', + }, + { + provider: 'cockroachdb', + }, + { + provider: 'sqlserver', + }, + ], +]) diff --git a/packages/client/tests/functional/issues/14373-batch-tx-error/prisma/_schema.ts b/packages/client/tests/functional/issues/14373-batch-tx-error/prisma/_schema.ts new file mode 100644 index 000000000000..a75c03293f54 --- /dev/null +++ b/packages/client/tests/functional/issues/14373-batch-tx-error/prisma/_schema.ts @@ -0,0 +1,22 @@ +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 @unique + memo String? + createdAt DateTime @default(now()) + } + ` +}) diff --git a/packages/client/tests/functional/issues/14373-batch-tx-error/tests.ts b/packages/client/tests/functional/issues/14373-batch-tx-error/tests.ts new file mode 100644 index 000000000000..e42a3329420e --- /dev/null +++ b/packages/client/tests/functional/issues/14373-batch-tx-error/tests.ts @@ -0,0 +1,24 @@ +// @ts-ignore +import type { PrismaClient } from '@prisma/client' + +import testMatrix from './_matrix' + +declare let prisma: PrismaClient + +testMatrix.setupTestSuite(() => { + test('correctly reports location of a batch error', async () => { + const result = prisma.$transaction([ + prisma.user.findMany({}), + prisma.user.update({ + where: { + email: 'notthere@example.com', + }, + data: { + memo: 'id is 1', + }, + }), + ]) + + await expect(result).rejects.toThrowError('Invalid `prisma.user.update()` invocation') + }) +}) diff --git a/packages/engine-core/src/binary/BinaryEngine.ts b/packages/engine-core/src/binary/BinaryEngine.ts index d81b94c34be0..e4563e156447 100644 --- a/packages/engine-core/src/binary/BinaryEngine.ts +++ b/packages/engine-core/src/binary/BinaryEngine.ts @@ -1203,12 +1203,11 @@ Please look into the logs or turn on the env var DEBUG=* to debug the constantly */ transactionHttpErrorHandler(result: Result): never { const response = result.data as { [K: string]: unknown } - throw new PrismaClientKnownRequestError( - response.message as string, - response.error_code as string, - this.clientVersion as string, - response.meta, - ) + throw new PrismaClientKnownRequestError(response.message as string, { + code: response.error_code as string, + clientVersion: this.clientVersion as string, + meta: response.meta as Record, + }) } } diff --git a/packages/engine-core/src/common/errors/ErrorWithBatchIndex.ts b/packages/engine-core/src/common/errors/ErrorWithBatchIndex.ts new file mode 100644 index 000000000000..f81da08e288b --- /dev/null +++ b/packages/engine-core/src/common/errors/ErrorWithBatchIndex.ts @@ -0,0 +1,7 @@ +export interface ErrorWithBatchIndex { + batchRequestIdx?: number +} + +export function hasBatchIndex(value: object): value is Required { + return typeof value['batchRequestIdx'] === 'number' +} diff --git a/packages/engine-core/src/common/errors/PrismaClientKnownRequestError.ts b/packages/engine-core/src/common/errors/PrismaClientKnownRequestError.ts index acffc650d832..af86d13c60bb 100644 --- a/packages/engine-core/src/common/errors/PrismaClientKnownRequestError.ts +++ b/packages/engine-core/src/common/errors/PrismaClientKnownRequestError.ts @@ -1,14 +1,25 @@ -export class PrismaClientKnownRequestError extends Error { +import { ErrorWithBatchIndex } from './ErrorWithBatchIndex' + +type KnownErrorParams = { + code: string + clientVersion: string + meta?: Record + batchRequestIdx?: number +} + +export class PrismaClientKnownRequestError extends Error implements ErrorWithBatchIndex { code: string meta?: Record clientVersion: string + batchRequestIdx?: number - constructor(message: string, code: string, clientVersion: string, meta?: any) { + constructor(message: string, { code, clientVersion, meta, batchRequestIdx }: KnownErrorParams) { super(message) this.code = code this.clientVersion = clientVersion this.meta = meta + this.batchRequestIdx = batchRequestIdx } get [Symbol.toStringTag]() { return 'PrismaClientKnownRequestError' diff --git a/packages/engine-core/src/common/errors/types/RequestError.ts b/packages/engine-core/src/common/errors/types/RequestError.ts index 41f4cecabf93..3e4f687a4439 100644 --- a/packages/engine-core/src/common/errors/types/RequestError.ts +++ b/packages/engine-core/src/common/errors/types/RequestError.ts @@ -3,7 +3,8 @@ export interface RequestError { user_facing_error: { is_panic: boolean message: string - meta?: object + meta?: Record error_code?: string + batch_request_idx?: number } } diff --git a/packages/engine-core/src/common/errors/utils/prismaGraphQLToJSError.ts b/packages/engine-core/src/common/errors/utils/prismaGraphQLToJSError.ts index ffb0e4fc1075..4e5de8bd15c1 100644 --- a/packages/engine-core/src/common/errors/utils/prismaGraphQLToJSError.ts +++ b/packages/engine-core/src/common/errors/utils/prismaGraphQLToJSError.ts @@ -3,17 +3,17 @@ import { PrismaClientUnknownRequestError } from '../PrismaClientUnknownRequestEr import type { RequestError } from '../types/RequestError' export function prismaGraphQLToJSError( - error: RequestError, + { error, user_facing_error }: RequestError, clientVersion: string, ): PrismaClientKnownRequestError | PrismaClientUnknownRequestError { - if (error.user_facing_error.error_code) { - return new PrismaClientKnownRequestError( - error.user_facing_error.message, - error.user_facing_error.error_code, + if (user_facing_error.error_code) { + return new PrismaClientKnownRequestError(user_facing_error.message, { + code: user_facing_error.error_code, clientVersion, - error.user_facing_error.meta, - ) + meta: user_facing_error.meta, + batchRequestIdx: user_facing_error.batch_request_idx, + }) } - return new PrismaClientUnknownRequestError(error.error, clientVersion) + return new PrismaClientUnknownRequestError(error, clientVersion) } diff --git a/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts b/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts index 7123092a0c9f..ad3910a6f89a 100644 --- a/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts +++ b/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts @@ -95,7 +95,7 @@ export async function responseToError( const error = await getResponseErrorBody(response) if (error.type === 'QueryEngineError') { - throw new PrismaClientKnownRequestError(error.body.message, error.body.error_code, clientVersion) + throw new PrismaClientKnownRequestError(error.body.message, { code: error.body.error_code, clientVersion }) } if (error.type === 'DataProxyError') { diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts index 2159e33929ae..6e24970d4cf9 100644 --- a/packages/engine-core/src/index.ts +++ b/packages/engine-core/src/index.ts @@ -4,6 +4,7 @@ export type { EngineEventType } from './common/Engine' export type { DatasourceOverwrite } from './common/Engine' export type { BatchTransactionOptions } from './common/Engine' export { Engine } from './common/Engine' +export { hasBatchIndex } from './common/errors/ErrorWithBatchIndex' export { PrismaClientInitializationError } from './common/errors/PrismaClientInitializationError' export { PrismaClientKnownRequestError } from './common/errors/PrismaClientKnownRequestError' export { PrismaClientRustPanicError } from './common/errors/PrismaClientRustPanicError' diff --git a/packages/engine-core/src/library/LibraryEngine.ts b/packages/engine-core/src/library/LibraryEngine.ts index 6f1ea2856a5f..9fc61b5c165e 100644 --- a/packages/engine-core/src/library/LibraryEngine.ts +++ b/packages/engine-core/src/library/LibraryEngine.ts @@ -146,12 +146,11 @@ export class LibraryEngine extends Engine { const response = this.parseEngineResponse<{ [K: string]: unknown }>(result) if (response.error_code) { - throw new PrismaClientKnownRequestError( - response.message as string, - response.error_code as string, - this.config.clientVersion as string, - response.meta, - ) + throw new PrismaClientKnownRequestError(response.message as string, { + code: response.error_code as string, + clientVersion: this.config.clientVersion as string, + meta: response.meta as Record, + }) } return response as Tx.Info | undefined