Skip to content

fix(core): reject a DELETE or an UPDATE whose WHERE clause cannot reach the provider - #24657

Open
michaelsembwever wants to merge 3 commits into
apache:mainfrom
thelastpickle:fix/dml-where-subquery-not-supported
Open

fix(core): reject a DELETE or an UPDATE whose WHERE clause cannot reach the provider#24657
michaelsembwever wants to merge 3 commits into
apache:mainfrom
thelastpickle:fix/dml-where-subquery-not-supported

Conversation

@michaelsembwever

@michaelsembwever michaelsembwever commented Aug 25, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

#24654

Rationale for this change

See ticket.

What changes are included in this PR?

A DELETE or an UPDATE whose WHERE clause holds an IN or an EXISTS subquery changed every row of the target table, and reported the whole table as affected. The optimizer rewrites the subquery into a semi join, so the condition leaves the Filter nodes that extract_dml_filters() reads. The provider then received an empty filter list, which is the encoding for "no WHERE clause", and applied the statement to all rows.

An always-false WHERE clause reached the provider the same way. The simplifier folds the predicate into an empty relation, so again no filter survived, and a DELETE FROM t WHERE false emptied the table.

Add classify_dml_input(), which walks the input plan of a DELETE or an UPDATE before the provider hook runs:

  • an empty relation means that no row matches, so the statement reports a count of 0 and the hook is not called;
  • a join, a predicate on another table, or any other node that restricts or multiplies rows raises a "not implemented" error, and the hook is not called.

The hook stays untouched in every rejected case, so a provider that writes to durable storage cannot lose rows.

Are these changes tested?

Only with the tests provided in this patch, which are based on the assumptions made in the ticket description.

Are there any user-facing changes?

?

@codecov-commenter

codecov-commenter commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.83838% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.70%. Comparing base (a6e2d3f) to head (19a68c0).
⚠️ Report is 192 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/core/src/physical_planner.rs 83.83% 4 Missing and 12 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24657      +/-   ##
==========================================
+ Coverage   81.36%   81.70%   +0.33%     
==========================================
  Files        1117     1127      +10     
  Lines      397872   415707   +17835     
  Branches   397872   415707   +17835     
==========================================
+ Hits       323725   339648   +15923     
- Misses      55229    56112     +883     
- Partials    18918    19947    +1029     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread datafusion/core/src/physical_planner.rs Outdated
Comment thread datafusion/core/src/physical_planner.rs Outdated
Comment thread datafusion/sqllogictest/test_files/dml_delete.slt
Comment thread datafusion/core/src/physical_planner.rs Outdated
Comment thread datafusion/core/src/physical_planner.rs Outdated
michaelsembwever and others added 3 commits September 7, 2026 12:15
…ch the provider

A DELETE or an UPDATE whose WHERE clause holds an IN or an EXISTS subquery changed every row of the target table, and reported the whole table as affected. The optimizer rewrites the subquery into a semi join, so the condition leaves the `Filter` nodes that `extract_dml_filters()` reads. The provider then received an empty filter list, which is the encoding for "no WHERE clause", and applied the statement to all rows.

An always-false WHERE clause reached the provider the same way. The simplifier folds the predicate into an empty relation, so again no filter survived, and `DELETE FROM t WHERE false` emptied the table.

Add `classify_dml_input()`, which walks the input plan of a DELETE or an UPDATE before the provider hook runs. An empty relation means that no row matches, so the statement reports a count of 0. A join, a predicate on another table, or any other node that restricts or multiplies rows raises a "not implemented" error. The hook stays untouched in both cases, so a provider that writes to durable storage cannot lose rows.

`classify_dml_input()` and `extract_dml_filters()` both need the target table and its aliases, so `collect_dml_target_refs()` collects that set once and each DML arm passes it to both.

`classify_dml_input()` also states the LIMIT gap that it leaves open: `DELETE FROM t LIMIT n` deletes every matching row, because a `Limit` carries no predicate and reaches the provider as no filter. `UPDATE ... LIMIT` is already rejected by the SQL planner.

The sqllogictest cases run a second time with `datafusion.optimizer.max_passes = 0`. No rule rewrites the subquery on that path, so the predicate reaches the provider and fails when the provider compiles it to a physical expression, and an always-false predicate reaches the provider and matches no row. That path was already safe, which is what makes the bug optimizer-dependent; the cases guard it.

Assisted-by: Claude Code:claude-opus-5
Co-authored-by: Martin Grigorov <martin-g@users.noreply.github.com>
…unoptimized path

fix(core): reject a DELETE or an UPDATE whose WHERE clause cannot reach the provider

A DELETE or an UPDATE whose WHERE clause holds an IN or an EXISTS subquery changed every row of the target table, and reported the whole table as affected. The optimizer rewrites the subquery into a semi join, so the condition leaves the `Filter` nodes that `extract_dml_filters()` reads. The provider then received an empty filter list, which is the encoding for "no WHERE clause", and applied the statement to all rows.

An always-false WHERE clause reached the provider the same way. The simplifier folds the predicate into an empty relation, so again no filter survived, and `DELETE FROM t WHERE false` emptied the table.

Add `classify_dml_input()`, which walks the input plan of a DELETE or an UPDATE before the provider hook runs. An empty relation means that no row matches, so the statement reports a count of 0. A join, a predicate on another table, or any other node that restricts or multiplies rows raises a "not implemented" error. The hook stays untouched in both cases, so a provider that writes to durable storage cannot lose rows.

`classify_dml_input()` and `extract_dml_filters()` both need the target table and its aliases, so `collect_dml_target_refs()` collects that set once and each DML arm passes it to both.

`classify_dml_input()` also states the LIMIT gap that it leaves open: `DELETE FROM t LIMIT n` deletes every matching row, because a `Limit` carries no predicate and reaches the provider as no filter. `UPDATE ... LIMIT` is already rejected by the SQL planner.

The sqllogictest cases run a second time with `datafusion.optimizer.max_passes = 0`. No rule rewrites the subquery on that path, so the predicate reaches the provider and fails when the provider compiles it to a physical expression, and an always-false predicate reaches the provider and matches no row. That path was already safe, which is what makes the bug optimizer-dependent; the cases guard it.

Assisted-by: Claude Code:claude-opus-5
@michaelsembwever
michaelsembwever force-pushed the fix/dml-where-subquery-not-supported branch from 648f444 to 19a68c0 Compare September 7, 2026 10:15

@martin-g martin-g left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

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

Labels

core Core DataFusion crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants