Skip to content

fix(qc): keep the where filter on updateMany when relations are emulated - #5868

Open
JEONGJAEIK wants to merge 1 commit into
prisma:mainfrom
JEONGJAEIK:fix/qc-update-many-emulated-filter
Open

JEONGJAEIK wants to merge 1 commit into
prisma:mainfrom
JEONGJAEIK:fix/qc-update-many-emulated-filter

Conversation

@JEONGJAEIK

Copy link
Copy Markdown

Fixes prisma/orm#30758. Also reported in prisma/orm#21867, and likely prisma/orm#28840 (same AND 1=1 in its log).

Problem

With relationMode = "prisma", update_many_records reads the matching ids first and then runs the UPDATE with Filter::empty() (7.10.1 query log, schema and table qualifiers removed):

SELECT "id" FROM "Product" WHERE ("id" = $1 AND "stock" > $2) OFFSET $3
UPDATE "Product" SET "stock" = ("stock" - $1) WHERE ("id" IN ($2) AND 1=1)

A row that stops matching between the two statements is still updated and counted. So conditional updates that check count, like the optimistic concurrency control example in the docs, don't work under concurrent writes. With foreignKeys the filter is part of the UPDATE (#3175), and deleteMany keeps it in both modes.

Solution

Apply the filter to the UPDATE as well, ANDed with the pre-read ids, the same way deleteMany does. This way the UPDATE itself checks the condition on the row it writes.

The exception is an update that changes fields referenced by a relation with onUpdate: Cascade or SetNull. Those emulated actions write to the related rows of every pre-read row before the UPDATE runs, so a filtered UPDATE could skip a row whose related rows were already changed. These updates keep the current plan. Restrict and NoAction only read the related rows, so the filter is applied for them too. The new helper utils::emulated_on_update_writes makes the same check as emulate_on_update_cascade and emulate_on_update_set_null.

Tests

  • Added a second snapshot suite in query-compiler/query-compiler/tests/queries.rs with a relationMode = "prisma" schema (data/relation-mode-prisma/). I moved the test body into a helper. Existing snapshots are unchanged.
    • update-many and update-many-restrict: the UPDATE now includes the filter. Without the fix both fail: the UPDATE has AND 1=1 instead of the filter.
    • update-many-cascade and update-many-set-null: an update that changes a field referenced with Cascade or SetNull keeps the current plan.
    • delete-many: deleteMany in the same mode, for comparison.
  • Added occ_update_many_relation_mode_prisma_test: the existing occ_update_many_test with relation_mode = "prisma". On Postgres (make dev-pg-qc) it fails without the fix with left: 6, right: 1 and passes with it. It also passes on better-sqlite3, libsql, MariaDB and SQL Server.
  • Removed the PlanetScale exclusion from occ_update_many_test. The test kit runs PlanetScale with relationMode = "prisma", and the left: 6, right: 1 failure in the removed comment is this bug. With make dev-planetscale-qc, both OCC updateMany tests fail without the fix and pass with it. I left the D1 exclusion as it was.
  • The full query-engine-tests suite passes on Postgres and PlanetScale with the fix (--test-threads 1).
  • End to end: built the query compiler Wasm for PostgreSQL from this branch, swapped it into @prisma/client@7.10.1, and ran the reproduction from updateMany with relationMode = "prisma" drops the where filter from the UPDATE, breaking optimistic concurrency orm#30758. With relationMode = "prisma", the racing updateMany now returns count: 0 and stock stays 0 on every run (before: count: 1, stock -1). updateManyAndReturn and updateMany with limit also get the filter.

Notes

  • With a negated filter (not, notIn, ...) the UPDATE is no longer chunkable, because Chunkable comes from the record filter. On adapters with a bind value limit, a large updateMany like that can now go over it. Prisma-mode deleteMany already behaves this way.
  • Not changed here: updates that change fields referenced with Cascade or SetNull still race. Closing that would mean, for example, running the emulated actions only for rows the UPDATE actually wrote. Nested updateMany also reads with the filter and updates with Filter::empty(), in both relation modes. I can look into either in a separate PR if you think it's worth it.

The 6.19.x branch has the same code in query-engine/core. Let me know if you want a backport.

With relationMode = "prisma", updateMany read the matching ids and then ran
the UPDATE with an empty filter, so a row that stopped matching in between was
still updated and counted. Apply the filter to the UPDATE as well, unless the
update changes fields referenced by a relation with onUpdate Cascade or
SetNull. In that case the emulated actions have already written to the related
rows of every pre-read row.

The OCC updateMany test now also runs with relation_mode = "prisma" and is no
longer excluded on PlanetScale, where it failed because of this.
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: ca77b888-3e45-4daf-b43a-186194fbf15a
📥 Commits

Reviewing files that changed from the base of the PR and between d1b3f37 and 7ede3f6.

⛔ Files ignored due to path filters: 5
  • query-compiler/query-compiler/tests/snapshots/queries__queries_relation_mode_prisma@delete-many.json.snap is excluded by !**/*.snap
  • query-compiler/query-compiler/tests/snapshots/queries__queries_relation_mode_prisma@update-many-cascade.json.snap is excluded by !**/*.snap
  • query-compiler/query-compiler/tests/snapshots/queries__queries_relation_mode_prisma@update-many-restrict.json.snap is excluded by !**/*.snap
  • query-compiler/query-compiler/tests/snapshots/queries__queries_relation_mode_prisma@update-many-set-null.json.snap is excluded by !**/*.snap
  • query-compiler/query-compiler/tests/snapshots/queries__queries_relation_mode_prisma@update-many.json.snap is excluded by !**/*.snap
📒 Files selected for processing: 10
  • query-compiler/core/src/query_graph_builder/write/update.rs
  • query-compiler/core/src/query_graph_builder/write/utils.rs
  • query-compiler/query-compiler/tests/data/relation-mode-prisma/delete-many.json
  • query-compiler/query-compiler/tests/data/relation-mode-prisma/schema.prisma
  • query-compiler/query-compiler/tests/data/relation-mode-prisma/update-many-cascade.json
  • query-compiler/query-compiler/tests/data/relation-mode-prisma/update-many-restrict.json
  • query-compiler/query-compiler/tests/data/relation-mode-prisma/update-many-set-null.json
  • query-compiler/query-compiler/tests/data/relation-mode-prisma/update-many.json
  • query-compiler/query-compiler/tests/queries.rs
  • query-engine/connector-test-kit-rs/query-engine-tests/tests/new/occ.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where bulk updates using Prisma relation mode could apply inconsistently when related records were updated through cascading or nullifying relations. Updates now remain aligned with the records selected by the original filter, while continuing to process the related records affected by those updates. This makes bulk update results more predictable for these relation behaviors.

Walkthrough

The update-many query graph now retains the original filter in the generated update, except when emulated on-update actions write related records. A helper checks whether Cascade or SetNull actions affect fields in the update arguments. New query-compiler fixtures cover relation-mode Prisma operations and relation actions. The optimistic concurrency test now runs on PlanetScale and excludes D1.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 7ede3

In Prisma relation mode, updateMany now rechecks its where filter when performing the update. A row that stops matching after the IDs are read is therefore not updated or counted. Updates that change fields used by Cascade or SetNull relations keep the previous behavior. No confirmed defect blocks merging. The author has acknowledged that large bulk updates with negated filters could exceed bind-value limits on some databases; owners may want to confirm that case separately.

Security architecture risk: 🔵 Low · up to 7ede3

The change strengthens conditional updates without establishing a new security vulnerability or broader write authority. Updates that trigger related-row changes intentionally retain their previous behavior. Connector-specific failure and concurrency guarantees remain only partially verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The examined write scope consists of the model's pre-read identifiers and related rows reached through schema-defined emulated actions. The change narrows the parent update where the predicate is restored; it does not establish broader tenant, database, or credential authority. Application-level authorization was not evaluated.

Trust Boundaries and Controls

  • observed — The request's where argument is parsed into a model filter, preserved through graph construction, and rendered alongside identifier constraints as parameterized SQL in the inspected snapshot. This strengthens predicate enforcement at the write rather than relying solely on a preceding read.

Resilience and Maintainability Implications

  • observed — The updateMany graph requests transactional execution, and translation converts that request into a transaction expression. This is evidence of intended failure containment, not independent proof of every connector's rollback or isolation behavior. The added concurrency test excludes MongoDB, CockroachDB, and D1, limiting connector coverage.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 4 files. (6 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: preserving the where filter for updateMany when relations are emulated.
Description check Passed The description is directly related to the changes. It explains the race condition, implementation, exceptions, tests, and known limitations.
Linked Issues check Passed The PR implements #30758 in the non-foreign-key updateMany path. It clones the pre-read filter and applies it to the UPDATE with the selected IDs. The filter is omitted only when emulated Cascade …
Out of Scope Changes check Passed The changes stay within #30758. The update-path helper, relation-mode schema, query fixtures, query-test refactoring, and OCC test updates directly implement or verify the filter-preservation fix. The…
Full details: Linked Issues check

Explanation

The PR implements #30758 in the non-foreign-key updateMany path. It clones the pre-read filter and applies it to the UPDATE with the selected IDs. The filter is omitted only when emulated Cascade or SetNull actions write related rows, as required by the stated plan. Restrict and other read-only actions retain the filter. Query fixtures cover normal, Restrict, Cascade, SetNull, and deleteMany behavior. OCC coverage adds the Prisma relation-mode case and removes the obsolete PlanetScale exclusion. The shared update-many path also covers the reported updateManyAndReturn and limit variants. The snapshot files are excluded from review, but the source, fixtures, and test changes provide independent evidence of the implementation and coverage.

Full details: Out of Scope Changes check

Explanation

The changes stay within #30758. The update-path helper, relation-mode schema, query fixtures, query-test refactoring, and OCC test updates directly implement or verify the filter-preservation fix. The retained D1 exclusion and the documented Cascade/SetNull limitation do not add unrelated behavior.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 4 files. (6 skipped: 6 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
✨ Simplify code
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@CLAassistant

CLAassistant commented Oct 17, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

updateMany with relationMode = "prisma" drops the where filter from the UPDATE, breaking optimistic concurrency

2 participants