From 3a928bd6d2fa2b28484a53a8e1a23ee97b56b823 Mon Sep 17 00:00:00 2001 From: Krist Ponpairin Date: Fri, 23 Oct 2026 02:27:03 +0700 Subject: [PATCH 1/2] fix(orm): don't build an empty IN() when zero rows match an update/updateMany on MySQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadAfterMutationEntities treated an empty preUpdateIds array as truthy, building `WHERE (id) IN ()` for any zero-match update/updateMany once an afterEntityMutation hook is registered. MySQL (no RETURNING support) rejects that as a syntax error — breaking every zero-match write app-wide, including ones a policy plugin legitimately denies down to zero rows. Fixes #2886 --- .../executor/zenstack-query-executor.ts | 7 +++++ tests/regression/test/issue-2886.test.ts | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/regression/test/issue-2886.test.ts diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index c7d9cbadc..a2b418029 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -460,6 +460,13 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { ? this.getInsertIds(mutationInfo.model, queryNode, queryResult) : preUpdateIds; + if (mutatedIds && mutatedIds.length === 0) { + // zero rows matched (e.g. a policy-denied or genuinely empty + // updateMany/deleteMany) — nothing to reload, and NOT the "can't + // determine ids at all" case below, so don't warn. + return []; + } + if (mutatedIds) { const idFields = requireIdFields(this.client.$schema, mutationInfo.model); const eb = expressionBuilder(); diff --git a/tests/regression/test/issue-2886.test.ts b/tests/regression/test/issue-2886.test.ts new file mode 100644 index 000000000..a0f2754a3 --- /dev/null +++ b/tests/regression/test/issue-2886.test.ts @@ -0,0 +1,29 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 2886', () => { + it('zero-match updateMany on MySQL does not throw when an afterEntityMutation hook is registered', async () => { + const db = await createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + email String @unique +} + `, + { provider: 'mysql', usePrismaPush: true }, + ); + + const hooked = db.$use({ + id: 'noop', + onEntityMutation: { + afterEntityMutation: async () => {}, + }, + }); + + // no row with id 999 exists -> preUpdateIds resolves to [] -> should be a + // normal zero-match update, not a thrown SQL syntax error + await expect( + hooked.user.updateMany({ where: { id: 999 }, data: { email: 'x@example.com' } }), + ).resolves.toEqual({ count: 0 }); + }); +}); From 2f5978fa3d84a07ad221b3c9eccc3cce28e8c2f3 Mon Sep 17 00:00:00 2001 From: Krist Ponpairin Date: Fri, 23 Oct 2026 02:31:26 +0700 Subject: [PATCH 2/2] rename regression test to the original issue (#2884, not the duplicate #2886) --- .../regression/test/{issue-2886.test.ts => issue-2884.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/regression/test/{issue-2886.test.ts => issue-2884.test.ts} (95%) diff --git a/tests/regression/test/issue-2886.test.ts b/tests/regression/test/issue-2884.test.ts similarity index 95% rename from tests/regression/test/issue-2886.test.ts rename to tests/regression/test/issue-2884.test.ts index a0f2754a3..91d43785d 100644 --- a/tests/regression/test/issue-2886.test.ts +++ b/tests/regression/test/issue-2884.test.ts @@ -1,7 +1,7 @@ import { createTestClient } from '@zenstackhq/testtools'; import { describe, expect, it } from 'vitest'; -describe('Regression for issue 2886', () => { +describe('Regression for issue 2884', () => { it('zero-match updateMany on MySQL does not throw when an afterEntityMutation hook is registered', async () => { const db = await createTestClient( `