fix: DELETE with JOIN deletes all rows instead of only matched rows - #25125
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
8d92328 to
9fd1b7d
Compare
…ates When DELETE...JOIN routes through MULTI_UPDATE, the join output may contain duplicate target rows when the right side has multiple matches. Use FilterRowIdForDel to deduplicate by rowid before counting affected rows, matching the plain deletion.go path. Skip allocation when no duplicates exist. BVT: tj_c JOIN tj_d with 3x duplicate matches — count(*)=1 verified.
XuPeng-SH
left a comment
There was a problem hiding this comment.
matrixone#25098 is an issue, so I reviewed the fixing PR #25125.
I found one blocker here: the new rowid-dedup path still does not reach partitioned multi-table DELETE.
compileMultiUpdate() only sets NeedDedupDelete when the scope root is *multi_update.MultiUpdate, but constructMultiUpdate() wraps partitioned targets in *multi_update.PartitionMultiUpdate instead. As a result, the raw operator behind PartitionMultiUpdate keeps NeedDedupDelete=false, and delete_table() skips FilterRowIdForDel() on the partitioned path.
There is a second propagation hole too: NewPartitionMultiUpdateFrom() does not copy raw.NeedDedupDelete, so even if the root operator is fixed later, the cloned partition operator path would still drop the flag.
That means the duplicate-join-match bug is still present for partitioned target tables.
Suggested fix:
- propagate the dedup decision to
PartitionMultiUpdate.rawincompileMultiUpdate(); - preserve
NeedDedupDeleteinNewPartitionMultiUpdateFrom(); - add a regression with a partitioned target table where the RHS has duplicate matches, so we prove each target row is deleted exactly once on the partitioned path.
The new non-partitioned BVTs are directionally right, but without the partitioned regression this fix is still incomplete.
Fixes the partition-path dedup hole flagged in review for matrixorigin#25125. - Add GetMultiUpdate() accessor on PartitionMultiUpdate (following PartitionDelete.GetDelete() convention) - compileMultiUpdate: set NeedDedupDelete on PartitionMultiUpdate.raw when the root op is a partition wrapper - NewPartitionMultiUpdateFrom: copy NeedDedupDelete from source - dupOperator MultiUpdate case: defensively copy NeedDedupDelete - Add regression test: hash-partitioned DELETE...JOIN with duplicate RHS matches (3x id=1, 2x id=2 → 2 distinct deletes) All 115 delete_multiple_table BVT tests pass at 100%. Co-Authored-By: Claude <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
Compute needDedup before constructMultiUpdate calls and pass it as a parameter. Set arg.NeedDedupDelete before NewPartitionMultiUpdate wraps it — the flag rides into the wrapper automatically, eliminating the brittle type-assertion loop. - compileMultiUpdate: move needDedup walk before construction; only UpdateWriteTable call site passes needDedup (the only path that reaches delete_table); WriteS3/FlushS3Info pass false - constructMultiUpdate: add needDedup bool parameter, set on arg - Remove GetMultiUpdate() accessor (dead code) - Remove post-hoc type-assertion loop Co-Authored-By: Claude <noreply@anthropic.com>
…me plumbing Replace the runtime NeedDedupDelete flag + FilterRowIdForDel chain with a single Node_DISTINCT injected above the select in bindDelete when joins are present. Since DELETE projects only target-table columns (including the unique Row_ID), DISTINCT on the whole row correctly deduplicates. - bind_delete.go: +6 lines — appendDistinctNode when TableRefs > 0 - delete.go: -22 lines — remove dedup block and colexec import - compile.go: -21 lines — remove needDedup walk, hasJoinDescendant, call-site args - operator.go: -3 lines — remove parameter and dup copy - multi_update_partition.go: -1 line — remove NeedDedupDelete copy - types.go: -1 line — remove NeedDedupDelete field 115/115 delete_multiple_table BVT tests pass at 100%. Net: -49 lines simpler. Co-Authored-By: Claude <noreply@anthropic.com>
|
Queued — the merge queue status continues in this comment ↓. |
Merge Queue Status
This pull request spent 2 hours 19 minutes 58 seconds in the queue, including 1 hour 9 minutes 51 seconds running CI. Required conditions to merge
|
What type of PR is this?
Which issue(s) this PR fixes:
issue #25098
What this PR does / why we need it:
DELETE ... JOINwas broken: the planner rejected these queries from the modernbindDeletepath, routing them to the legacyNode_DELETEpath that ignored the join condition and deleted all rows from the target table.Root cause
Two guards in
bindDeleteincorrectly rejected single-targetDELETE...JOIN:Truncation guard (
stmt.Where == nil && stmt.Limit == nil): for JOIN deletes the condition lives in theONclause, notWHERE. Withstmt.Where == nil, the guard fired and tried a truncate rewrite, which failed and returned an error — falling back to legacyNode_DELETE.PROJECT node check (
selectNode.NodeType != plan.Node_PROJECT): after a JOIN,bindSelectcan produce a non-PROJECT root. This guard rejected it as "malformed select node", also falling back to legacy.Fix
len(stmt.TableRefs) == 0— skip truncate rewrite when joins are present.bindSelectoutput is always usable regardless of root node type.Node_DISTINCTabove the select whenstmt.TableRefs > 0. Since DELETE projects only target-table columns (including the uniqueRow_ID), DISTINCT on the whole row eliminates exact duplicates produced by a JOIN where the RHS has multiple matches per target row. This happens at the plan level — no runtime flag plumbing, no cross-batch dedup hole.Removed
All runtime dedup plumbing from the previous iteration of this PR:
NeedDedupDeleteflag,FilterRowIdForDelcall indelete_table(),hasJoinDescendanttree walker, flag propagation throughconstructMultiUpdate/dupOperator/NewPartitionMultiUpdateFrom.Tests
Added three regression groups:
tj_a/tj_b: DELETE with ON-clause only (no WHERE) — verifies the truncation guard fixtj_c/tj_d: duplicate RHS matches (3× id=1) — verifies dedup correctness on non-partitioned pathtj_part_c/tj_part_d: HASH-partitioned table with duplicate RHS matches (3× id=1, 2× id=2) — verifies partitioned path115/115 BVT tests pass at 100%.
pkg/sql/plan/bind_delete.gopkg/sql/colexec/multi_update/delete.gopkg/sql/compile/compile.goneedDedupwalk,hasJoinDescendantpkg/sql/compile/operator.gopkg/sql/colexec/multi_update/multi_update_partition.gopkg/sql/colexec/multi_update/types.gotest/.../delete_multiple_table.sqltest/.../delete_multiple_table.resultNet: -49 lines simpler than the runtime dedup approach.
🤖 Generated with Claude Code