From b7dd2548559ce9221e57fe07dd2b467930884e8e Mon Sep 17 00:00:00 2001 From: Tim Suchanek Date: Tue, 22 Dec 2020 13:48:19 +0100 Subject: [PATCH] fix(client): raw transactions (#4759) --- .../errors/missing-relation/test.ts | 24 - .../happy/raw-transactions/.gitignore | 1 + .../integration/happy/raw-transactions/dev.db | Bin 0 -> 45056 bytes .../happy/raw-transactions/schema.prisma | 29 ++ .../happy/raw-transactions/test.ts | 443 ++++++++++++++++++ .../src/generation/TSClient/constants.ts | 37 +- .../client/src/runtime/getPrismaClient.ts | 143 ++++-- src/packages/engine-core/src/NodeEngine.ts | 15 +- 8 files changed, 605 insertions(+), 87 deletions(-) create mode 100644 src/packages/client/src/__tests__/integration/happy/raw-transactions/.gitignore create mode 100644 src/packages/client/src/__tests__/integration/happy/raw-transactions/dev.db create mode 100644 src/packages/client/src/__tests__/integration/happy/raw-transactions/schema.prisma create mode 100644 src/packages/client/src/__tests__/integration/happy/raw-transactions/test.ts diff --git a/src/packages/client/src/__tests__/integration/errors/missing-relation/test.ts b/src/packages/client/src/__tests__/integration/errors/missing-relation/test.ts index 2207e5d1913a..6847df5a96bb 100644 --- a/src/packages/client/src/__tests__/integration/errors/missing-relation/test.ts +++ b/src/packages/client/src/__tests__/integration/errors/missing-relation/test.ts @@ -32,30 +32,6 @@ test('missing-relation', async () => { how you used Prisma Client in the issue. `) - // try { - // } catch (e) { - // expect(e).toMatchInlineSnapshot(` - - // Invalid \`prisma.post.findMany()\` invocation in - // /client/src/__tests__/integration/errors/missing-relation/test.ts:9:23 - - // 6 const prisma = new PrismaClient() - // 7 - // 8 try { - // → 9 await prisma.post.findMany( - // PANIC in query-engine/core/src/response_ir/internal.rs:348:26 - // Application logic invariant error: received null value for field author which may not be null - - // This is a non-recoverable error which probably happens when the Prisma Query Engine has a panic. - - // TEST_GITHUB_LINK - - // If you want the Prisma team to look into it, please open the link above 🙏 - // To increase the chance of success, please post your schema and a snippet of - // how you used Prisma Client in the issue. - - // `) - // } prisma.$disconnect() }) diff --git a/src/packages/client/src/__tests__/integration/happy/raw-transactions/.gitignore b/src/packages/client/src/__tests__/integration/happy/raw-transactions/.gitignore new file mode 100644 index 000000000000..97952752a72b --- /dev/null +++ b/src/packages/client/src/__tests__/integration/happy/raw-transactions/.gitignore @@ -0,0 +1 @@ +!dev.db \ No newline at end of file diff --git a/src/packages/client/src/__tests__/integration/happy/raw-transactions/dev.db b/src/packages/client/src/__tests__/integration/happy/raw-transactions/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..f5f8f9c048d4dcaa51a32900b4b7e65e43316913 GIT binary patch literal 45056 zcmeHQOK%&=5hho*B~hz*cMU=C!N9n4umw-qlK^%<)uhq7q&nQIZb)s z;S4n#%Bu)OU;|EoAm^L|ZaWRlRqiG}OC*zt$BL3jB(A}023}X-^@;p`2|oQt(*JVI=e5M0 zNB^3g{afP7%uk75{r=OhKK)|$f3yFb`Dw1<#tMe2Qzf0l+^D_N?{vb z<;~5smtJ$Y=5jsjG9?e?OB-8ikVVO>>)G;VNy(LqMYT|>;MJW{cI&Au+-~Xv3(G9c zcGWamFF25Yvc0{jW(z^}GKXupCLho^d*;uHKVh$59|pZHx447d!_UdyUSsmmUh{@# znEh#5T0^$RYv0r2##XlYgYvEVgK`^TTu3dzO876i8_9%W>HNpmz6opZiq>{*@w?(T zuCnZZn}Pz)&ADVtWWW~Q;f?1rlk;EBC0{3_iO{S{+cNgs9CJ;`?v)E0-<4HmqmWm> zSC}jxW@XBD!AoYhnb)<2TT@r&znUA=M3kJ$GQLm0=}b<}&&?&@?h1r44PP;N!la?7 zzT>iFx!i(I;3$xy2RwUWI~#Inq*G!@tZx_9jc*E);X#%KrKql}Fbs0)P5?HF1A?Dd zH&qyXJE}LfFsb|$$CHxF?&Pw0b+9afM!1gnzquyxN9N+|QtnpjI1XZ-$}+xBy`G#F z9DKdsRYPoSVAi1=JmI)_bC8^zzj-tH_JL?gWy{!eG}o}L@Y^TdBR0GMZDS5UFj|nE zfvA_%Z`5L-E0oAEOoMt025FE3Qb8GM4O{0uBU@oDS99B~_=s1S)@+&vDyKL>sLruX zlk1hLR(sj^dO)tya(S~)<(Q$WwRolBIr3M!l zW5XlOBWsAwUQa0)zk|KnR=%0{?K5R}-th(-xCcf1UXEuRp(h`IY9@UM#LG ztt>4qFRd-FtgNj* zojc0cuznMpECvPIj)@yu1yk#W$ul}XxT9C`lFsW|+jQ}g>$Ex6Dwcx^u6;$7hK?n7 zT*t8X6y>ojaJ$_$^xF#y%40)^YQ!Qq%;asyARQN-e&I*(y9s-umV!H@XS*RS{2iec zr56&cbrB5qO-dfN(2= zi)~PlgdOqrIG7EAdX(w}>-L>?t;SoeiDyU5)%IA1<;3j*6*$S#3>gY($Wr)qpqXvn zVwqC|m<-oFZbJpYy?Caqv)69mb}h|JyC(J!EaTTRpa!P-g)+>S z9lu>b<+&81=j_y}flPK0ipb>|P?J&{p@`Dn)2S`76G}60N0bq_4SF)I8{E{PrmjqB z7+9E`!P^~a<{maVeD_8T{MH=9J%ofJ-)|cZY@XuG%R_3-H4acJqTFk0?u(F%!%272 zOb@C8G4X~s>%{3DD=g8*oK^!611X;w2Ave54~TerbK zsn@Z4BJKbT?#Ol%Z#!sa4=;uQBy@Tx^Qg=t62ED{&$#r($C)S-&lD#sE3TiyID-lP zathv(SO1%UCp`!OLVyq;1PB2_fDj-A2mwNX5Fi8y0Ycz|LSTC0`n2{~yQ6dQ|Nqs$ zC*VmBLVyq;1PB2_fDj-A2mwNX5Fi8y0YZQfc=r*wo4jccA6`SSg`zP6e*jT(n;&5Ubr(`$kdv&=Yadz;Z4Fv#AhExOFaIyRiu^7~jL2MQ_kUd-;Vp0>> zMqVKFVH;vyj$9biQYbL<47(-mJx39!A((ltn1ijV1`6tsdZp26*yJ8t!jbK@V#4j2 zu7(v0FSpP{F|2m?h*denM*&MlG$REMS~OKX@;u2hEaHcjW;NSvH>|GfzMmLigT|0Y z%+X$f!JqUR1NwEXVVGzYX$YfL-$v32Z6pKx#f;T9O=u%HjBZ#kDukVDrmmSSj=~@; z+rZkf*Lf^P932^>lv~2A8B$J%xMvc20W_sM-T6_LK6vLxFzUb;ql!+WQKo_z2#fFt zTTy&afI%aUDO0}DWK@LDP>KR~&R8E$anzvYb7as&T{h2-Xnd$fd^KZ4N8si!*AUXD zHAeE(`MH}0ou{(Ds zRMUYx=sqCp^s{@{K!ep?@K)}tsilVRqoV`k=BRi_$I@sD5)HbC=w{C>{$ylEngsd{ z*kgM%u5JJ}gv<@*9>f?T6kv>m=xius3qTn$2{PYw8xjTz1waLtef|yaWWGH?%zt+`#{ZZ5e{mCl-UtCgfDj-A2mwNX5Fi8y0YZQfAOr{jLg2$h z0HQ}jq|GpzC;T{BR89=;YQf!74MJKMw1$l3+7%E~hU5fN4zF_uBfjb-5t}%}V~B1gQ$X3#LIZb=@%MW& ziKvMUOXoiZk;P%okrM;Vn_hZkz8FK72?|+VUk}2ZM``%hJKn|0OFwk_1 { + test('queryRaw', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }) + const queries: any[] = [] + prisma.$on('query', (q) => { + queries.push(q) + }) + + const res = await prisma.$transaction([ + prisma.$queryRaw`SELECT * FROM "User"`, + prisma.$queryRaw`SELECT * FROM "Post"`, + ]) + await prisma.$disconnect() + + expect(sanitizeEvents(queries)).toMatchInlineSnapshot(` + Array [ + Object { + duration: 0, + params: [], + query: BEGIN, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "User", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "Post", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: COMMIT, + target: quaint::connector::metrics, + }, + ] + `) + + expect(res).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + email: a@a.de, + id: 576eddf9-2434-421f-9a86-58bede16fd95, + name: Alice, + }, + ], + Array [], + ] + `) + }) + + test('queryRaw & updateMany 1', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }) + const queries: any[] = [] + prisma.$on('query', (q) => { + queries.push(q) + }) + + const updateUsers = prisma.user.updateMany({ + where: { + name: 'A', + }, + data: { + name: 'B', + }, + }) + + const res = await prisma.$transaction([ + prisma.$queryRaw`SELECT * FROM "User"`, + updateUsers, + prisma.$queryRaw`SELECT * FROM "Post"`, + ]) + await prisma.$disconnect() + + expect(sanitizeEvents(queries)).toMatchInlineSnapshot(` + Array [ + Object { + duration: 0, + params: [], + query: BEGIN, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "User", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: ["A"], + query: SELECT \`dev\`.\`User\`.\`id\` FROM \`dev\`.\`User\` WHERE \`dev\`.\`User\`.\`name\` = ?, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "Post", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: COMMIT, + target: quaint::connector::metrics, + }, + ] + `) + + expect(res).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + email: a@a.de, + id: 576eddf9-2434-421f-9a86-58bede16fd95, + name: Alice, + }, + ], + Object { + count: 0, + }, + Array [], + ] + `) + }) + + test('queryRaw & updateMany 2', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }) + const queries: any[] = [] + prisma.$on('query', (q) => { + queries.push(q) + }) + + const updateUsers = prisma.user.updateMany({ + where: { + name: 'A', + }, + data: { + name: 'B', + }, + }) + + const res = await prisma.$transaction([ + updateUsers, + prisma.$queryRaw`SELECT * FROM "User"`, + prisma.$queryRaw`SELECT * FROM "Post"`, + ]) + await prisma.$disconnect() + + expect(sanitizeEvents(queries)).toMatchInlineSnapshot(` + Array [ + Object { + duration: 0, + params: [], + query: BEGIN, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: ["A"], + query: SELECT \`dev\`.\`User\`.\`id\` FROM \`dev\`.\`User\` WHERE \`dev\`.\`User\`.\`name\` = ?, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "User", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "Post", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: COMMIT, + target: quaint::connector::metrics, + }, + ] + `) + + expect(res).toMatchInlineSnapshot(` + Array [ + Object { + count: 0, + }, + Array [ + Object { + email: a@a.de, + id: 576eddf9-2434-421f-9a86-58bede16fd95, + name: Alice, + }, + ], + Array [], + ] + `) + }) + + test('executeRaw', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }) + const queries: any[] = [] + prisma.$on('query', (q) => { + queries.push(q) + }) + + const res = await prisma.$transaction([ + prisma.$executeRaw`UPDATE User SET name = ${'blub1'} WHERE id = ${'THIS_DOES_NOT_EXIT1'};`, + prisma.$executeRaw`UPDATE User SET name = ${'blub2'} WHERE id = ${'THIS_DOES_NOT_EXIT2'};`, + ]) + await prisma.$disconnect() + + expect(sanitizeEvents(queries)).toMatchInlineSnapshot(` + Array [ + Object { + duration: 0, + params: [], + query: BEGIN, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: ["blub1","THIS_DOES_NOT_EXIT1"], + query: UPDATE User SET name = ? WHERE id = ?;, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: ["blub2","THIS_DOES_NOT_EXIT2"], + query: UPDATE User SET name = ? WHERE id = ?;, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: COMMIT, + target: quaint::connector::metrics, + }, + ] + `) + + expect(res).toMatchInlineSnapshot(` + Array [ + 0, + 0, + ] + `) + }) + + test('queryRaw & executeRaw in separate transactions', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }) + const queries: any[] = [] + prisma.$on('query', (q) => { + queries.push(q) + }) + + const res = await Promise.all([ + prisma.$transaction([ + prisma.$queryRaw`SELECT * FROM "User"`, + prisma.$queryRaw`SELECT * FROM "Post"`, + ]), + prisma.$transaction([prisma.user.findFirst(), prisma.post.findFirst()]), + ]) + await prisma.$disconnect() + + // as Promise.all does things in parallel, the order is not clear + // one query finishes first, but we don't know, which one that is + expect(queries.filter((q) => q.query === 'BEGIN').length).toBe(2) + expect(queries.filter((q) => q.query === 'COMMIT').length).toBe(2) + + expect(res).toMatchInlineSnapshot(` + Array [ + Array [ + Array [ + Object { + email: a@a.de, + id: 576eddf9-2434-421f-9a86-58bede16fd95, + name: Alice, + }, + ], + Array [], + ], + Array [ + Object { + email: a@a.de, + id: 576eddf9-2434-421f-9a86-58bede16fd95, + name: Alice, + }, + null, + ], + ] + `) + }) + + // TODO: Enable this test, once query engine allows it + // will be fixed in https://github.com/prisma/prisma-engines/issues/1481 + /* eslint-disable-next-line jest/no-disabled-tests */ + test.skip('all mixed', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient({ + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }) + const queries: any[] = [] + prisma.$on('query', (q) => { + queries.push(q) + }) + + const updateUsers = prisma.user.updateMany({ + where: { + name: 'A', + }, + data: { + name: 'B', + }, + }) + + const res = await prisma.$transaction([ + // prisma.$queryRaw`SELECT * FROM "Post"`, + prisma.$executeRaw`UPDATE User SET name = ${'blub1'} WHERE id = ${'THIS_DOES_NOT_EXIT1'};`, + updateUsers, + // prisma.$queryRaw`SELECT * FROM "User"`, + // prisma.$executeRaw`UPDATE User SET name = ${'blub2'} WHERE id = ${'THIS_DOES_NOT_EXIT2'};`, + ]) + await prisma.$disconnect() + + expect(sanitizeEvents(queries)).toMatchInlineSnapshot(` + Array [ + Object { + duration: 0, + params: [], + query: BEGIN, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "Post", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: ["blub1","THIS_DOES_NOT_EXIT1"], + query: UPDATE User SET name = ? WHERE id = ?;, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: SELECT * FROM "User", + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: ["blub2","THIS_DOES_NOT_EXIT2"], + query: UPDATE User SET name = ? WHERE id = ?;, + target: quaint::connector::metrics, + }, + Object { + duration: 0, + params: [], + query: COMMIT, + target: quaint::connector::metrics, + }, + ] + `) + + expect(res).toMatchInlineSnapshot(` + Array [ + Array [], + 0, + Array [ + Object { + email: a@a.de, + id: 576eddf9-2434-421f-9a86-58bede16fd95, + name: Alice, + }, + ], + 0, + ] + `) + }) +}) + +function sanitizeEvents(e: any[]) { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + return e.map(({ timestamp, ...event }) => event) +} diff --git a/src/packages/client/src/generation/TSClient/constants.ts b/src/packages/client/src/generation/TSClient/constants.ts index 3e81dcf68e58..5f0a74eeb9a5 100644 --- a/src/packages/client/src/generation/TSClient/constants.ts +++ b/src/packages/client/src/generation/TSClient/constants.ts @@ -42,16 +42,14 @@ const JSDocFields = { addLinkToDocs(`Skip the first \`n\` ${plural}.`, 'pagination'), count: (singular, plural) => addLinkToDocs(`Count returned ${plural}`, 'aggregations'), - avg: (singular, plural) => - addLinkToDocs(`Select which fields to average`, 'aggregations'), - sum: (singular, plural) => - addLinkToDocs(`Select which fields to sum`, 'aggregations'), - min: (singular, plural) => + avg: () => addLinkToDocs(`Select which fields to average`, 'aggregations'), + sum: () => addLinkToDocs(`Select which fields to sum`, 'aggregations'), + min: () => addLinkToDocs( `Select which fields to find the minimum value`, 'aggregations', ), - max: (singular, plural) => + max: () => addLinkToDocs( `Select which fields to find the maximum value`, 'aggregations', @@ -63,7 +61,7 @@ const JSDocFields = { } export const JSDocs: JSDocsType = { groupBy: { - body: (ctx) => `Group By`, + body: () => `Group By`, fields: {}, }, create: { @@ -81,7 +79,7 @@ const ${ctx.singular} = await ${ctx.method}({ }) `, fields: { - data: (singular, plural) => `The data needed to create a ${singular}.`, + data: (singular) => `The data needed to create a ${singular}.`, }, }, findOne: { @@ -102,7 +100,7 @@ const ${lowerCase(ctx.mapping.model)} = await ${ctx.method}({ } })`, fields: { - where: (singular, plural) => `Filter, which ${singular} to fetch.`, + where: (singular) => `Filter, which ${singular} to fetch.`, }, }, findUnique: { @@ -120,7 +118,7 @@ const ${lowerCase(ctx.mapping.model)} = await ${ctx.method}({ } })`, fields: { - where: (singular, plural) => `Filter, which ${singular} to fetch.`, + where: (singular) => `Filter, which ${singular} to fetch.`, }, }, findFirst: { @@ -138,7 +136,7 @@ const ${lowerCase(ctx.mapping.model)} = await ${ctx.method}({ } })`, fields: { - where: (singular, plural) => `Filter, which ${singular} to fetch.`, + where: (singular) => `Filter, which ${singular} to fetch.`, orderBy: JSDocFields.orderBy, cursor: (singular, plural) => addLinkToDocs( @@ -203,8 +201,8 @@ const ${lowerCase(ctx.mapping.model)} = await ${ctx.method}({ }) `, fields: { - data: (singular, plural) => `The data needed to update a ${singular}.`, - where: (singular, plural) => `Choose, which ${singular} to update.`, + data: (singular) => `The data needed to update a ${singular}.`, + where: (singular) => `Choose, which ${singular} to update.`, }, }, upsert: { @@ -228,11 +226,11 @@ const ${lowerCase(ctx.mapping.model)} = await ${ctx.method}({ } })`, fields: { - where: (singular, plural) => + where: (singular) => `The filter to search for the ${singular} to update in case it exists.`, - create: (singular, plural) => + create: (singular) => `In case the ${singular} found by the \`where\` argument doesn't exist, create a new ${singular} with this data.`, - update: (singular, plural) => + update: (singular) => `In case the ${singular} was found with the provided \`where\` argument, update it with this data.`, }, }, @@ -252,7 +250,7 @@ const ${ctx.singular} = await ${ctx.method}({ }) `, fields: { - where: (singular, plural) => `Filter which ${singular} to delete.`, + where: (singular) => `Filter which ${singular} to delete.`, }, }, aggregate: { @@ -281,10 +279,9 @@ const aggregations = await prisma.user.aggregate({ take: 10, })`, fields: { - where: (singular, plural) => `Filter which ${singular} to aggregate.`, + where: (singular) => `Filter which ${singular} to aggregate.`, orderBy: JSDocFields.orderBy, - cursor: (singular, plural) => - addLinkToDocs(`Sets the start position`, 'cursor'), + cursor: () => addLinkToDocs(`Sets the start position`, 'cursor'), take: JSDocFields.take, skip: JSDocFields.skip, count: JSDocFields.count, diff --git a/src/packages/client/src/runtime/getPrismaClient.ts b/src/packages/client/src/runtime/getPrismaClient.ts index 1e0dcff8f839..99581ce6689e 100644 --- a/src/packages/client/src/runtime/getPrismaClient.ts +++ b/src/packages/client/src/runtime/getPrismaClient.ts @@ -36,7 +36,6 @@ import { Dataloader } from './Dataloader' import { printStack } from './utils/printStack' import stripAnsi from 'strip-ansi' import { printJsonWithErrors } from './utils/printJsonErrors' -import { ConnectorType } from './utils/printDatasources' import { mapPreviewFeatures } from '@prisma/sdk/dist/utils/mapPreviewFeatures' import { serializeRawParameters } from './utils/serializeRawParameters' import { AsyncResource } from 'async_hooks' @@ -136,6 +135,7 @@ export interface InternalRequestParams extends MiddlewareParams { clientMethod: string callsite?: string headers?: Record + transactionId?: number } export type HookPoint = 'all' | 'engine' @@ -274,12 +274,15 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { private _engineMiddlewares: EngineMiddleware[] = [] private _clientVersion: string private _previewFeatures: string[] + private _activeProvider: string + private _transactionId = 1 constructor(optionsArg?: PrismaClientOptions) { if (optionsArg) { validatePrismaClientOptions(optionsArg, config.datasourceNames) } this._clientVersion = config.clientVersion ?? clientVersion + this._activeProvider = config.activeProvider const envPaths = { rootEnvPath: config.relativeEnvPaths.rootEnvPath && @@ -408,13 +411,14 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { } this._bootstrapClient() + void this._getActiveProvider() } catch (e) { e.clientVersion = this._clientVersion throw e } } get [Symbol.toStringTag]() { - return 'NewPrismaClient' + return 'PrismaClient' } $use(cb: Middleware) $use(namespace: 'all', cb: Middleware) @@ -495,15 +499,21 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { } } - private async _getActiveProvider(): Promise { - const configResult = await this._engine.getConfig() - return configResult.datasources[0].activeProvider + private async _getActiveProvider(): Promise { + try { + const configResult = await this._engine.getConfig() + this._activeProvider = configResult.datasources[0].activeProvider + } catch (e) { + // it's ok to silently fail + } } /** * Executes a raw query. Always returns a number */ - private async $executeRawInternal( + private $executeRawInternal( + runInTransaction: boolean, + transactionId: number | null, stringOrTemplateStringsArray: | ReadonlyArray | string @@ -514,8 +524,6 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { let query = '' let parameters: any = undefined - const activeProvider = await this._getActiveProvider() - if (typeof stringOrTemplateStringsArray === 'string') { // If this was called as prisma.$executeRaw(, [...values]), assume it is a pre-prepared SQL statement, and forward it without any changes query = stringOrTemplateStringsArray @@ -525,7 +533,7 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { } } else if (Array.isArray(stringOrTemplateStringsArray)) { // If this was called as prisma.$executeRaw``, try to generate a SQL prepared statement - switch (activeProvider) { + switch (this._activeProvider) { case 'sqlite': case 'mysql': { const queryInstance = sqlTemplateTag.sqltag( @@ -566,7 +574,7 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { } } else { // If this was called as prisma.raw(sql``), use prepared statements from sql-template-tag - switch (activeProvider) { + switch (this._activeProvider) { case 'sqlite': case 'mysql': query = stringOrTemplateStringsArray.sql @@ -599,7 +607,8 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { dataPath: [], action: 'executeRaw', callsite: this._getCallsite(), - runInTransaction: false, + runInTransaction, + transactionId: transactionId ?? undefined, }) } @@ -613,16 +622,34 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { | sqlTemplateTag.Sql, ...values: sqlTemplateTag.RawValue[] ) { - try { - const promise = this.$executeRawInternal( - stringOrTemplateStringsArray, - ...values, - ) - ;(promise as any).isExecuteRaw = true - return promise - } catch (e) { - e.clientVersion = this._clientVersion - throw e + const doRequest = (runInTransaction = false, transactionId?: number) => { + try { + const promise = this.$executeRawInternal( + runInTransaction, + transactionId ?? null, + stringOrTemplateStringsArray, + ...values, + ) + ;(promise as any).isExecuteRaw = true + return promise + } catch (e) { + e.clientVersion = this._clientVersion + throw e + } + } + return { + then(onfulfilled, onrejected) { + return doRequest().then(onfulfilled, onrejected) + }, + requestTransaction(transactionId: number) { + return doRequest(true, transactionId) + }, + catch(onrejected) { + return doRequest().catch(onrejected) + }, + finally(onfinally) { + return doRequest().finally(onfinally) + }, } } @@ -636,7 +663,9 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { /** * Executes a raw query. Always returns a number */ - private async $queryRawInternal( + private $queryRawInternal( + runInTransaction: boolean, + transactionId: number | null, stringOrTemplateStringsArray: | string | TemplateStringsArray @@ -646,8 +675,6 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { let query = '' let parameters: any = undefined - const activeProvider = await this._getActiveProvider() - if (typeof stringOrTemplateStringsArray === 'string') { // If this was called as prisma.$queryRaw(, [...values]), assume it is a pre-prepared SQL statement, and forward it without any changes query = stringOrTemplateStringsArray @@ -657,7 +684,7 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { } } else if (Array.isArray(stringOrTemplateStringsArray)) { // If this was called as prisma.$queryRaw``, try to generate a SQL prepared statement - switch (activeProvider) { + switch (this._activeProvider) { case 'sqlite': case 'mysql': { const queryInstance = sqlTemplateTag.sqltag( @@ -698,7 +725,7 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { } } else { // If this was called as prisma.raw(sql``), use prepared statements from sql-template-tag - switch (activeProvider) { + switch (this._activeProvider) { case 'sqlite': case 'mysql': query = stringOrTemplateStringsArray.sql @@ -725,13 +752,15 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { const args = { query, parameters } debug(`Prisma Client call:`) + // const doRequest = (runInTransaction = false) => { return this._request({ args, clientMethod: 'queryRaw', dataPath: [], action: 'queryRaw', callsite: this._getCallsite(), - runInTransaction: false, + runInTransaction, + transactionId: transactionId ?? undefined, }) } @@ -742,16 +771,34 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { stringOrTemplateStringsArray, ...values: sqlTemplateTag.RawValue[] ) { - try { - const promise = this.$queryRawInternal( - stringOrTemplateStringsArray, - ...values, - ) - ;(promise as any).isQueryRaw = true - return promise - } catch (e) { - e.clientVersion = this._clientVersion - throw e + const doRequest = (runInTransaction = false, transactionId?: number) => { + try { + const promise = this.$queryRawInternal( + runInTransaction, + transactionId ?? null, + stringOrTemplateStringsArray, + ...values, + ) + ;(promise as any).isQueryRaw = true + return promise + } catch (e) { + e.clientVersion = this._clientVersion + throw e + } + } + return { + then(onfulfilled, onrejected) { + return doRequest().then(onfulfilled, onrejected) + }, + requestTransaction(transactionId: number) { + return doRequest(true, transactionId) + }, + catch(onrejected) { + return doRequest().catch(onrejected) + }, + finally(onfinally) { + return doRequest().finally(onfinally) + }, } } @@ -787,6 +834,10 @@ new PrismaClient({ }) } + private getTransactionId() { + return this._transactionId++ + } + private async $transactionInternal(promises: Array): Promise { for (const p of promises) { if (!p) { @@ -805,10 +856,14 @@ new PrismaClient({ ) } } + + const transactionId = this.getTransactionId() + return Promise.all( promises.map((p) => { if (p.requestTransaction) { - return p.requestTransaction() + return p.requestTransaction(transactionId) + } else { } return p }), @@ -893,6 +948,7 @@ new PrismaClient({ action, model, headers, + transactionId, }: InternalRequestParams) { if (action !== 'executeRaw' && action !== 'queryRaw' && !model) { throw new Error(`Model missing for action ${action}`) @@ -978,6 +1034,7 @@ new PrismaClient({ engineHook: this._engineMiddlewares[0], runInTransaction, headers, + transactionId, }) } @@ -1032,7 +1089,7 @@ new PrismaClient({ return requestPromise.then(onfulfilled, onrejected) }, - requestTransaction: () => { + requestTransaction: (transactionId: number) => { if (!requestPromise) { requestPromise = this._request({ args, @@ -1042,6 +1099,7 @@ new PrismaClient({ clientMethod, callsite, runInTransaction: true, + transactionId, }) } @@ -1249,6 +1307,7 @@ export class PrismaClientFetcher { dataloader: Dataloader<{ document: Document runInTransaction?: boolean + transactionId?: number headers?: Record }> @@ -1268,6 +1327,9 @@ export class PrismaClientFetcher { }, batchBy: (request) => { if (request.runInTransaction) { + if (request.transactionId) { + return `transaction-batch-${request.transactionId}` + } return 'transaction-batch' } @@ -1311,6 +1373,7 @@ export class PrismaClientFetcher { engineHook, args, headers, + transactionId, }: { document: Document dataPath: string[] @@ -1324,6 +1387,7 @@ export class PrismaClientFetcher { engineHook?: EngineMiddleware args: any headers?: Record + transactionId?: number }) { if (this.hooks && this.hooks.beforeRequest) { const query = String(document) @@ -1358,6 +1422,7 @@ export class PrismaClientFetcher { document, runInTransaction, headers, + transactionId, }) data = result.data elapsed = result.elapsed diff --git a/src/packages/engine-core/src/NodeEngine.ts b/src/packages/engine-core/src/NodeEngine.ts index a3abe8d08b61..efff6408327f 100644 --- a/src/packages/engine-core/src/NodeEngine.ts +++ b/src/packages/engine-core/src/NodeEngine.ts @@ -199,6 +199,9 @@ export class NodeEngine { this.generator = generator this.datasources = datasources this.logEmitter = new EventEmitter() + this.logEmitter.on('error', () => { + // to prevent unhandled error events + }) this.showColors = showColors ?? false this.logLevel = logLevel this.logQueries = logQueries ?? false @@ -1115,7 +1118,7 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. * different place, not the request itself. This different place can either be * this.lastRustError or this.lastErrorLog */ - private throwAsyncErrorIfExists() { + private throwAsyncErrorIfExists(forceThrow = false) { logger('throwAsyncErrorIfExists', this.startCount, this.hasMaxRestarts) if (this.lastRustError) { const err = new PrismaClientRustPanicError( @@ -1125,7 +1128,7 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. if (this.lastRustError.is_panic) { this.lastPanic = err } - if (this.hasMaxRestarts) { + if (this.hasMaxRestarts || forceThrow) { throw err } } @@ -1140,7 +1143,7 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. this.lastPanic = err } - if (this.hasMaxRestarts) { + if (this.hasMaxRestarts || forceThrow) { throw err } } @@ -1166,6 +1169,9 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. if (this.startPromise) { await this.startPromise } + + this.throwAsyncErrorIfExists() + // A currentRequestPromise is only being canceled by the sendPanic function if (this.currentRequestPromise.isCanceled) { this.throwAsyncErrorIfExists() @@ -1200,7 +1206,7 @@ We recommend using the \`wtfnode\` package to debug open handles.`, // to get an error first for (let i = 0; i < 5; i++) { await new Promise((r) => setTimeout(r, 50)) - this.throwAsyncErrorIfExists() + this.throwAsyncErrorIfExists(true) } throw new Error(`Query engine is trying to restart, but can't. Please look into the logs or turn on the env var DEBUG=* to debug the constantly restarting query engine.`) @@ -1208,6 +1214,7 @@ Please look into the logs or turn on the env var DEBUG=* to debug the constantly } if (!graceful) { + this.throwAsyncErrorIfExists(true) throw error }