[spark] Fix MERGE INTO silently skipping WHEN NOT MATCHED BY SOURCE actions - #9155
Open
kerwin-zk wants to merge 1 commit into
Open
[spark] Fix MERGE INTO silently skipping WHEN NOT MATCHED BY SOURCE actions#9155kerwin-zk wants to merge 1 commit into
kerwin-zk wants to merge 1 commit into
Conversation
kerwin-zk
force-pushed
the
nmbs-scope-fix
branch
2 times, most recently
from
August 10, 2026 16:08
f46f70e to
00ded7f
Compare
…ctions
`MergeIntoPaimonTable` extracts the target-only conjuncts of the merge
condition and uses them to prune the target table before the full outer
join (`filteredTargetPlan` / `targetOnlyCondition`).
That pruning is sound for `WHEN MATCHED` and `WHEN NOT MATCHED`: a target
row that fails a target-only conjunct can never satisfy the whole merge
condition, so it can never be matched, and dropping it cannot change the
outcome of those actions.
It is not sound for `WHEN NOT MATCHED BY SOURCE`. The pruned-away rows are
exactly the population that clause is defined over, so their actions are
silently skipped -- no error, no warning, just fewer rows changed.
For example, with a partitioned table and
MERGE INTO target t USING source s
ON t.a = s.a AND t.pt = 'p1'
WHEN MATCHED THEN UPDATE SET t.b = s.b
WHEN NOT MATCHED BY SOURCE THEN UPDATE SET t.c = 'stale'
every row outside `pt = 'p1'` should be updated to `stale` (no source row
can match it), but none of them is.
The pruning was introduced together with MERGE INTO itself in apache#2331, one
month before `WHEN NOT MATCHED BY SOURCE` was added in apache#2517, and its
safety argument was never revisited.
Note that the V2 row-level paths (`ReplaceData` / `WriteDelta`) are rewritten
by Spark and are not affected, so the same statement currently produces
different results depending on whether the table qualifies for
`SparkTable.supportsV2RowLevelOps`. Primary key tables never qualify, so they
always take the affected V1 path.
This disables the pruning when the merge has any `WHEN NOT MATCHED BY SOURCE`
action. Setting `targetOnlyCondition` to `None` covers all three places it
feeds: `filteredTargetPlan`, `findCandidateDataSplits` and
`targetDSWithFilePathCol`.
A follow-up can restore part of the pruning by handling the excluded rows as
a separate not-matched-by-source-only stream, which avoids joining them
against the source while still applying their actions.
kerwin-zk
force-pushed
the
nmbs-scope-fix
branch
from
August 11, 2026 03:23
00ded7f to
85a8073
Compare
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.
Purpose
MergeIntoPaimonTableextracts the target-only conjuncts of the merge condition and uses them to prune the target table before the full outer join (targetOnlyCondition/filteredTargetPlan,MergeIntoPaimonTable.scala:63-69).That pruning is sound for
WHEN MATCHEDandWHEN NOT MATCHED: a target row that fails a target-only conjunct can never satisfy the whole merge condition, so it can never be matched, and dropping it cannot change the outcome of those actions.It is not sound for
WHEN NOT MATCHED BY SOURCE. The pruned-away rows are exactly the population that clause is defined over, so their actions are silently skipped — no error, no warning, just fewer rows changed.Repro on a partitioned table holding
pt = 'p1'andpt = 'p2'rows, with a source that only containsa = 1:Every
pt = 'p2'row should be updated tostale(no source row can ever match it), but none of them is:The pruning was introduced together with MERGE INTO itself in #2331 (2023-11-17), one month before
WHEN NOT MATCHED BY SOURCEwas added in #2517 (2023-12-22), and its safety argument was never revisited.Note the V2 row-level paths (
ReplaceData/WriteDelta) are rewritten by Spark and are not affected, so today the same statement produces different results depending on whether the table qualifies forSparkTable.supportsV2RowLevelOps. Primary key tables never qualify, so they always take the affected V1 path.Fix: disable the pruning when the merge has any
WHEN NOT MATCHED BY SOURCEaction. SettingtargetOnlyConditiontoNonecovers all three places it feeds —filteredTargetPlan,findCandidateDataSplitsandtargetDSWithFilePathCol— so there is no path left that can drop those rows.Tests
CI