Repository navigation
fix: preserve outer join equivalences for null rows - #26042
Prajwal-k-tech wants to merge 5 commits into
Conversation
|
I pushed a regression fix for the CI failure: zero-column batches now preserve their row count, with a focused Full-join test. GitHub marked the new fork workflow runs as |
|
I also addressed the semver bot feedback by restoring the original public |
|
Thanks @Prajwal-k-tech Which of these Prs would you like us to review first?
I think it will be eaier to focus and get one done / in rather than trying to do a bunch in parallel |
kumarUjjawal
left a comment
There was a problem hiding this comment.
Thank you @Prajwal-k-tech
Left few comments please take a look.
| let expressions = class.into_iter().filter(|expr| { | ||
| !is_volatile(expr) | ||
| && expr | ||
| .evaluate(null_batch) |
There was a problem hiding this comment.
Planning now runs every non-volatile expression in an equivalence class, including user and third-party ScalarUDFs, on a synthetic all-NULL batch; it does this every time join properties are recomputed, and a panic inside evaluate is not caught.
| && expr | ||
| .evaluate(null_batch) | ||
| .and_then(|value| value.into_array(1)) | ||
| .is_ok_and(|array| array.null_count() == 1) |
There was a problem hiding this comment.
array.null_count() == 1 reads the physical null buffer, so arrays with only logical nulls report 0 and their valid equivalences are dropped.
| /// that may no longer hold after an outer join. | ||
| fn with_null_preserving_expressions(self, null_batch: &RecordBatch) -> Self { | ||
| let classes = self.classes.into_iter().filter_map(|class| { | ||
| let expressions = class.into_iter().filter(|expr| { |
There was a problem hiding this comment.
The filter keeps only members that evaluate to NULL, so it drops members that still agree on null-extended rows because they evaluate to the same non-NULL value.
| )?); | ||
| } | ||
| JoinType::Full => { | ||
| let batch = null_batch(join_schema)?; |
There was a problem hiding this comment.
null_batch allocates a new schema and one null array per output column on every outer-join property computation, even when the nullable side has no equivalence classes.**
| schema | ||
| .fields() | ||
| .iter() | ||
| .map(|field| Field::new(field.name(), field.data_type().clone(), true)) |
There was a problem hiding this comment.
null_batch rebuilds each field with Field::new(name, type, true), which drops field metadata such as extension-type annotations.
| statement ok | ||
| CREATE TABLE outer_join_sort_right (a BIGINT, b BIGINT) AS VALUES (2, 2), (3, 3); | ||
|
|
||
| query III |
There was a problem hiding this comment.
The regression tests cover only LEFT and FULL OUTER JOIN; the new JoinType::Right branch (class.rs:874) and the cross-join path are never exercised.
| Ok(()) | ||
| } | ||
|
|
||
| use datafusion_expr::Operator; |
There was a problem hiding this comment.
The new test full_join_equivalences_support_empty_schema was inserted between the test module's use lines, leaving use datafusion_expr::Operator; stranded after a function.
There was a problem hiding this comment.
The public EquivalenceGroup::join (no schema) keeps the old unsound outer-join behavior with no doc warning or deprecation, so external callers still get wrong equivalences.
|
Thanks for the detailed review. I pushed the follow-up to the existing fork branch in commits
Validation: |
Which issue does this PR close?
coalesce/CASE#26036.Rationale for this change
After a LEFT, RIGHT, or FULL OUTER JOIN, the nullable side can contain rows whose columns were extended with NULLs. An equivalence such as
b = coalesce(a, 0)is true on the input rows but does not remain true for those null-extended rows. Retaining it can lead to incorrect ordering and LIMIT results.What changes are included in this PR?
What is the testing strategy for this PR?
cargo test -p datafusion-sqllogictest --test sqllogictests -- joinscargo test -p datafusion-physical-expr(1,685 passed, 2 ignored; 13 doctests passed)RUST_BACKTRACE=1 cargo test -p datafusion(passed)RUST_BACKTRACE=1 cargo test -p datafusion-cli(passed)cargo clippy --all-targets --all-features -- -D warnings(passed)cargo +1.99.0 fmt --all -- --checkandgit diff --check(passed)This contribution was prepared with AI assistance.
Are there any user-facing changes?
This fixes incorrect query results for affected outer joins. There are no API changes.