Skip to content

[Spark] Merge Into: When Not Matched By Source - #2517

Merged
JingsongLi merged 1 commit into
apache:masterfrom
YannByron:master_merge_bysource
Dec 22, 2023
Merged

[Spark] Merge Into: When Not Matched By Source#2517
JingsongLi merged 1 commit into
apache:masterfrom
YannByron:master_merge_bysource

Conversation

@YannByron

Copy link
Copy Markdown
Contributor

Purpose

Linked issue: close #xxx

Tests

API and Format

Documentation

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Before this PR, can we create an abstraction for MergeIntoTableTest. Just like TestTrinoITCase, every spark module has a class to extend this, test merge into feature for every Spark versions.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

@JingsongLi
JingsongLi merged commit 554ae64 into apache:master Dec 22, 2023
kerwin-zk added a commit to kerwin-zk/paimon that referenced this pull request Aug 10, 2026
…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 added a commit to kerwin-zk/paimon that referenced this pull request Aug 10, 2026
…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 added a commit to kerwin-zk/paimon that referenced this pull request Aug 11, 2026
…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.
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.

2 participants