Skip to content

Commit 6d76482

Browse files
authored
fix: Handle null-aware joins correctly in FilterNullJoinKeys when its enabled (apache#23848)
## Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes apache#123` indicates that this PR will close issue apache#123. --> - Closes apache#23847 . ## Rationale for this change Fixes a correctness bug, not sure when that config used/desirable. This is another thing I ran into during apache#21585 ## What changes are included in this PR? 1. New SLT test 2. Fix for bug in `datafusion-optimizer` ## Are these changes tested? Existing tests, and a new SLT test verifying the config change doesn't affect the query result and basic unit test. ## Are there any user-facing changes? None --------- Signed-off-by: Adam Gutglick <adamgsal@gmail.com>
1 parent 36c417b commit 6d76482

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

datafusion/optimizer/src/filter_null_join_keys.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ impl OptimizerRule for FilterNullJoinKeys {
5252
match plan {
5353
LogicalPlan::Join(mut join)
5454
if !join.on.is_empty()
55+
&& !join.null_aware
5556
&& join.null_equality == NullEquality::NullEqualsNothing =>
5657
{
5758
let (left_preserved, right_preserved) =
@@ -359,4 +360,50 @@ mod tests {
359360
let t2 = table_scan(Some("t2"), &schema, None)?.build()?;
360361
Ok((t1, t2))
361362
}
363+
364+
#[test]
365+
fn null_aware_left_mark_join_keys_not_filtered() -> Result<()> {
366+
let (t1, t2) = test_tables()?;
367+
let plan = build_null_aware_plan(t1, t2, JoinType::LeftMark)?;
368+
369+
assert_optimized_plan_equal!(plan, @r"
370+
LeftMark Join: t1.id = t2.optional_id null_aware
371+
TableScan: t1
372+
TableScan: t2
373+
")
374+
}
375+
376+
#[test]
377+
fn null_aware_left_anti_join_keys_not_filtered() -> Result<()> {
378+
let (t1, t2) = test_tables()?;
379+
let plan = build_null_aware_plan(t1, t2, JoinType::LeftAnti)?;
380+
381+
assert_optimized_plan_equal!(plan, @r"
382+
LeftAnti Join: t1.id = t2.optional_id null_aware
383+
TableScan: t1
384+
TableScan: t2
385+
")
386+
}
387+
388+
/// A join whose nullable right key would get an `IS NOT NULL` filter if it
389+
/// were not null-aware.
390+
fn build_null_aware_plan(
391+
left_table: LogicalPlan,
392+
right_table: LogicalPlan,
393+
join_type: JoinType,
394+
) -> Result<LogicalPlan> {
395+
LogicalPlanBuilder::from(left_table)
396+
.join_detailed_with_options(
397+
right_table,
398+
join_type,
399+
(
400+
vec![Column::from_qualified_name("t1.id")],
401+
vec![Column::from_qualified_name("t2.optional_id")],
402+
),
403+
None,
404+
NullEquality::NullEqualsNothing,
405+
true,
406+
)?
407+
.build()
408+
}
362409
}

datafusion/sqllogictest/test_files/null_aware_anti_join.slt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ query IT rowsort
7070
SELECT * FROM outer_table WHERE id NOT IN (SELECT id FROM inner_table_with_null);
7171
----
7272

73+
# Regression test
74+
75+
statement ok
76+
set datafusion.optimizer.filter_null_join_keys = true;
77+
78+
# The subquery NULL must reach the join: every row's NOT IN is UNKNOWN or
79+
# FALSE, so the result stays empty.
80+
query IT rowsort
81+
SELECT * FROM outer_table WHERE id NOT IN (SELECT id FROM inner_table_with_null);
82+
----
83+
84+
statement ok
85+
reset datafusion.optimizer.filter_null_join_keys;
86+
7387
# Verify the result is empty even though there are rows in outer_table
7488
# that don't match the non-NULL value (2) in the subquery.
7589
# This is correct null-aware behavior: if subquery contains NULL, result is unknown.

0 commit comments

Comments
 (0)