Skip to content

fix(datafusion): map column-less MERGE INSERT VALUES by position - #984

Open
jackylee-ch wants to merge 2 commits into
apache:mainfrom
jackylee-ch:fix/merge-into-insert-without-columns
Open

jackylee-ch wants to merge 2 commits into
apache:mainfrom
jackylee-ch:fix/merge-into-insert-without-columns

Conversation

@jackylee-ch

@jackylee-ch jackylee-ch commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

WHEN NOT MATCHED THEN INSERT VALUES (s.a, s.b, ...) without an explicit column list built an empty column->expression map, so insert_select_clause emitted NULL for every target column and dropped the source values. The merge then inserted all-NULL rows (or failed on a non-null column) instead of the source data — silent corruption for a column-less positional INSERT. This form is valid standard SQL (the MERGE INSERT column list is optional) and DataFusion's parser accepts it; Spark's own MERGE grammar, by contrast, requires an explicit column list or INSERT *. The existing MERGE INSERT tests all pass an explicit column list, so this went uncovered.

Map the VALUES to the table's columns by position when no column list is given, and reject a value/column count mismatch. Explicit-column and INSERT * clauses are unchanged.

Added an end-to-end MERGE test using a column-less INSERT; it inserts all-NULL rows before the fix.

@jackylee-ch
jackylee-ch force-pushed the fix/merge-into-insert-without-columns branch from 0db0c22 to bdde90b Compare September 30, 2026 14:07
@JingsongLi

Copy link
Copy Markdown
Contributor

Requirement fit: SUPPORTED — retaining positional MERGE INSERT values fixes a real data-correctness issue. Implementation: FINDINGS at bdde90be.

[P2] Validate positional INSERT arity before executing MERGE (crates/integrations/datafusion/src/merge_into.rs:1012-1018). The added value-count check runs only while building unmatched-row batches. CoW skips that step when all source rows match, and data evolution returns through the empty-batch guard; the upfront validate_merge_insert_columns validates names only. The new promise to reject positional value-count mismatches therefore depends on the current data.

I reproduced this through SQL in both modes with a three-column (id, name, value) target: WHEN MATCHED THEN UPDATE SET value=s.value WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.name) succeeds when the source row matches, and changes the target value from 10 to 99. Please move the arity check into the existing upfront validation used by both modes, preserving INSERT ROW/*, and add a regression that an invalid fully matched MERGE fails without changing the target. This all-matched behavior predates the PR; the finding is an incomplete part of the explicitly stated validation requirement, rather than a newly introduced mutation regression.

Verification: 15/15 MERGE module tests and 29/29 MERGE SQL integration tests passed. The temporary invalid-arity SQL regression failed in both CoW and data-evolution mode with a successful commit. Diff check and current-main merge-tree passed; all 14 head CI checks are green. Temporary edits were restored.

`WHEN NOT MATCHED THEN INSERT VALUES (s.a, s.b, ...)` without an explicit
column list built an empty column->expression map, so `insert_select_clause`
emitted `NULL` for every target column and dropped the source values. The
merge then inserted all-NULL rows (or failed on a non-null column) instead of
the source data — silent corruption for a column-less positional INSERT, which
is valid standard SQL (the MERGE INSERT column list is optional) and accepted
by DataFusion's parser.

Map the VALUES to the table's columns by position when no column list is
given, and reject a value/column count mismatch. Explicit-column and
`INSERT *` clauses are unchanged.

Added an end-to-end MERGE test using a column-less INSERT; it inserts all-NULL
rows before the fix.
The value-count check for a column-less positional INSERT lived in
`insert_select_clause`, which only runs while building unmatched-row batches.
A fully matched CoW merge (and the data-evolution empty-batch path) skips that
step, so an invalid `INSERT VALUES (s.a, s.b)` against a three-column target
was accepted and the matched UPDATE committed.

Move the arity check into `validate_merge_insert_columns`, which both execute
paths call before any batch is built. `INSERT *`/`INSERT ROW` (no columns and
no values) is unaffected. Added a regression: an all-matched MERGE with a
short positional INSERT now fails without changing the target.
@jackylee-ch
jackylee-ch force-pushed the fix/merge-into-insert-without-columns branch from bdde90b to 8178f94 Compare October 2, 2026 01:47
@jackylee-ch

Copy link
Copy Markdown
Contributor Author

Addressed.

The positional value-count check moved from insert_select_clause (which only runs while building unmatched-row batches) into validate_merge_insert_columns, the upfront validation that both execute_merge_into (CoW) and execute_merge_into_once (data evolution) call before any batch is built. So a column-less INSERT VALUES (...) whose value count does not match the table is rejected regardless of whether any row is unmatched. INSERT * / INSERT ROW (no columns and no values) is left untouched.

Regression test_cow_merge_insert_arity_validated_even_when_all_matched: with a source row that matches on id, WHEN MATCHED THEN UPDATE SET value = s.value WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.name) against a three-column target now fails, and the target is unchanged — the matched UPDATE is not applied. I verified non-vacuity: disabling the upfront check makes the all-matched merge commit the UPDATE and the test fail; restoring it passes.

Rebased onto current main. The merge_into module tests (16) pass; clippy -p paimon-datafusion --all-targets --features fulltext,vortex -D warnings is clean.

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