fix: preserve MERGE target qualifier bindings - #24429
Conversation
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #24429 +/- ##
==========================================
- Coverage 81.34% 81.34% -0.01%
==========================================
Files 1117 1117
Lines 397528 397525 -3
Branches 397528 397525 -3
==========================================
- Hits 323385 323376 -9
- Misses 55225 55229 +4
- Partials 18918 18920 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@alamb @kosiew @timsaucer Could you take a look of this PR before the Datafusion 55.0.0 release. I think adding the target table's qualifer into the protobuf is a more elegant solution to make code succinct and recursively rewriting subquery. |
kosiew
left a comment
There was a problem hiding this comment.
Thanks for working on this. This looks good to me.
I like the approach of keeping MERGE's SQL-visible target qualifier separate from the target provider identity. Using that qualifier consistently when rebuilding the MERGE expression schema across the analyzer, optimizer, and physical planner avoids the alias canonicalization issues while preserving the correct SQL scoping semantics.
The protobuf handling also looks good. Persisting the qualifier while falling back to DmlNode.table_name for older payloads keeps the change backward compatible.
The added coverage for aliased targets, source-name collisions, correlated subqueries, qualifier shadowing, and the basic protobuf round trip gives me good confidence in the change.
One note on protobuf coverage: I think keeping the MERGE round-trip test's ON expression flat is appropriate here. A target-correlated subquery cannot currently be serialized because datafusion/proto/src/logical_plan/to_proto.rs does not support OuterReferenceColumn, Exists, or InSubquery. A MERGE round-trip test for that case would fit better with future work adding broader protobuf support for correlated subquery expressions.
Thanks again!
alamb
left a comment
There was a problem hiding this comment.
Thanks for this @wirybeaver and @kosiew
The code looks good -- I would just like to request we migrate some of these tests to use slt rather than more rust code
Keep SQL-visible target qualifiers distinct from provider identity so correlated subqueries and source-name collisions retain their intended meaning.
Remove worktree-only context and ADR files from the published change while retaining them through local excludes.
MergeIntoOp has not appeared in a release, so users do not need before-and-after upgrade guidance.
Keep only binding-specific Rust assertions while moving SQL behavior cases to the faster, more maintainable sqllogictest suite.
|
@alamb The testing code has moved to SLT. Thanks for the guide |
d359e29 to
3d19096
Compare
Which issue does this PR close?
Rationale for this change
#22988 deliberately rejected two valid MERGE forms to avoid silently changing expression meaning.
Limitation 1: target-correlated subquery with an aliased target
t.idinside the subquery becomesouter_ref(t.id). The old top-level canonicalizer only rewroteExpr::Column(t.id)totarget.id, leaving the outer reference inconsistent with the schema later rebuilt fromDmlStatement.table_name.Limitation 2: source qualifier equals the target table name
Canonicalizing target
t.idtotarget.idcollapses both operands onto the source qualifier and can turn the condition intotarget.id = target.id.A recursive string rewrite is not safe: qualifiers are scope-local, so an inner relation can legally shadow
t. It also cannot solve the second limitation because both relations would still have the same qualifier after rewriting.What changes are included in this PR?
Solution
Keep provider identity and the SQL-visible target qualifier as separate plan state:
Planning now proceeds as follows:
targetto the target provider, while retaining aliastas the visible qualifier.tonMergeIntoOp; do not canonicalize target columns or recursively rewrite subquery plans.t.*, then source fields) and use it consistently in SQL planning, analyzer/optimizer rules, physical planning, and programmatic plans.TableProvider::merge_into, where target and source columns resolve to distinct physical indices.Protobuf change and 55.0/55.1 compatibility
MergeIntoOpNodegains optionaltarget_qualifierfield 3. This field is necessary becauseDmlNode.table_namecontains provider identity and cannot also represent aliast; without it, a proto round trip loses the binding needed to resolve MERGE expressions.Compatibility is directional:
DmlNode.table_name, matching 55.0's canonicalized representation.Rust API compatibility
DataFusion 55.0 released
MergeIntoOpwith public struct-literal construction:MergeIntoOp { on, clauses }. This PR makes the struct non-exhaustive, adds private target-qualifier state, and requiresMergeIntoOp::new(target_qualifier, on, clauses). Therefore existing 55.0 downstream struct literals will not compile unchanged against 55.1. This compatibility exception should be considered explicitly for the 55.1 release.The PR also:
Are these changes tested?
cargo fmt --allcargo clippy --all-targets --all-features -- -D warnings./ci/scripts/doc_prettier_check.sh --write --allow-dirtyRUST_BACKTRACE=1 cargo test --profile ci --exclude datafusion-examples --exclude datafusion-benchmarks --exclude datafusion-cli --workspace --lib --tests --bins --features avro,json,backtrace,extended_tests,recursive_protection,parquet_encryptionAre there any user-facing changes?
Yes. Both valid MERGE forms above now plan successfully and reach
TableProvider::merge_into. The Rust and protobuf compatibility constraints for upgrading from 55.0 to 55.1 are documented above.