fix: apply MemTable DELETE and UPDATE at execution, not planning - #25040
Open
michaelsembwever wants to merge 2 commits into
Open
fix: apply MemTable DELETE and UPDATE at execution, not planning#25040michaelsembwever wants to merge 2 commits into
michaelsembwever wants to merge 2 commits into
Conversation
EXPLAIN DELETE and EXPLAIN UPDATE removed or rewrote the rows of an in-memory table. handle_explain() builds the physical plan in order to print it, and MemTable did the whole row change inside the delete_from() and update() hooks that the planner awaits. Split each hook into a planning half and an execution half. The hooks now build the physical predicates and assignments and return MemDeleteExec or MemUpdateExec. Those nodes take the partition write locks, rewrite the batches, clear the declared sort order and count the affected rows when execute() runs. DmlResultExec is removed, since the count is no longer known while the plan is built. Planning errors stay in the hooks, so EXPLAIN still reports an unknown SET column and a predicate that cannot be planned. The two nodes print the predicates and the assignments in place of the row count, so the eight physical plan expectations in delete.slt and update.slt are regenerated. Closes apache#24656 Assisted-by: Claude Code:claude-opus-5
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #25040 +/- ##
========================================
Coverage 81.72% 81.72%
========================================
Files 1127 1127
Lines 416237 416694 +457
Branches 416237 416694 +457
========================================
+ Hits 340156 340561 +405
- Misses 56092 56109 +17
- Partials 19989 20024 +35 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…rom 24655 EXPLAIN DELETE and EXPLAIN UPDATE removed or rewrote the rows of an in-memory table. handle_explain() builds the physical plan in order to print it, and MemTable did the whole row change inside the delete_from() and update() hooks that the planner awaits. Split each hook into a planning half and an execution half. The hooks now build the physical predicates and assignments and return MemDeleteExec or MemUpdateExec. Those nodes take the partition write locks, rewrite the batches, clear the declared sort order and count the affected rows when execute() runs. DmlResultExec is removed, since the count is no longer known while the plan is built. Planning errors stay in the hooks, so EXPLAIN still reports an unknown SET column and a predicate that cannot be planned. Each node declares one partition, so a request for any other partition is a caller error. execute() reports it rather than applying the statement. The two nodes print the predicates and the assignments in place of the row count, so the eight physical plan expectations in delete.slt and update.slt are regenerated. New sqllogictest cases in dml_delete.slt and dml_update.slt assert that EXPLAIN leaves the rows alone and that EXPLAIN ANALYZE changes them once. The unit tests in memory_test.rs cover what those cases cannot reach: a table that holds no partition, a batch of no row that both operations skip, a request for a partition other than 0, a WHERE clause that names an unknown column or is not a predicate, a SET clause that names an unknown column, an UPDATE of a batch that misses a column of the table, and an assignment that fails to evaluate, is of the wrong type, or is null for a NOT NULL column. The partition guard and the memory_test.rs cases come from apache#24655, which fixes the same issue by the same strategy. Closes apache#24656 Assisted-by: Claude Code:claude-opus-5
|
Watch the flyover → · 12:29 · 6 chapters · 6 files An animated walkthrough of this diff: context, the problem, the change, implementation care, code walkthrough, and impact. dfdcd91 · updated automatically by flyovers |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Rationale for this change
EXPLAIN DELETEandEXPLAIN UPDATEchanged the rows of an in-memory table. The plan was printed, and the statement had also run:Two causes combined.
handle_explain()builds the physical plan in order to print thephysical_plansection, and the planner awaitsTableProvider::delete_from()andTableProvider::update()while it builds.MemTabledid the whole row change inside those hooks: it took a write lock on each partition, overwrote it, cleared the declared sort order, and returned a constantDmlResultExeccarrying a count it had already computed. The row count baked into the plan text was the tell.EXPLAIN ANALYZE DELETEis expected to change the rows, because it runs the plan by design. It must apply the statement exactly once.What changes are included in this PR?
Each hook is split into a planning half and an execution half.
execute()batchesandsort_orderhandlesWHEREclauseSETcolumn names, build the physical assignmentscountbatchdelete_from()now returnsMemDeleteExecandupdate()returnsMemUpdateExec. Each node holds the shared partition handles plus the expressions the hook built, and applies the change on the first poll of the stream thatexecute()returns.DmlResultExecis removed, since the count is not known while the plan is built. Each run of the plan applies the statement once, asDataSinkExecdoes for anINSERT.The row logic itself moved unchanged into
delete_rows()andupdate_rows(), including the SQL three-valued logic for a NULL predicate and theevaluate_selectioncall that keeps an error such as a divide by zero away from the rows the statement does not touch.Planning errors stay in the hooks, so
EXPLAINstill reports an unknownSETcolumn and a predicate that cannot be planned.apply_expressions()now visits the expressions the nodes hold; the constant node it replaces had none.Both nodes are private to
datafusion-catalog, so there is no public API change.What is the testing strategy for this PR?
New sqllogictest cases in
dml_delete.sltanddml_update.sltassert thatEXPLAINleaves the three rows alone and thatEXPLAIN ANALYZEchanges them. Against the unfixed provider both fail. The update case adds ten to each matching value rather than deleting, so a plan that ran twice would print22and23instead of12and13; that is what pins the once-only guarantee, and it is what the old code produced.The eight physical plan expectations in
delete.sltandupdate.sltare regenerated. They can no longer carryrows_affected, and the new text names the predicates and the assignments instead:cargo test --test sqllogictests,cargo test -p datafusion --lib, andcargo test -p datafusion --test core_integrationshow identical results before and after the change. Clippy passes with-D warningsondatafusion-catalog,datafusion, anddatafusion-sqllogictest.Are there any user-facing changes?
Yes, and all three are the point of the fix or follow from it.
EXPLAIN DELETEandEXPLAIN UPDATEon aMemTableno longer change data, and they no longer clear the table's declared sort order.The physical plan text of a
DELETEor anUPDATEon aMemTablechanged.DmlResultExec: rows_affected=NbecomesMemDeleteExecorMemUpdateExec, which name the predicates and the assignments in place of the count.One behaviour change beyond the fix, noted for completeness: the early return for a
MemTablewith no partitions is gone, so anUPDATEnaming an unknown column now raises its plan error rather than reporting zero rows.MemTable::try_newrejects an empty partition list, so this is unreachable through normal construction.EXPLAIN INSERTstill clears the declared sort order, becauseinsert_into()does that inside the hook. That is a smaller instance of the same shape and is left for a follow-up.