Skip to content

Commit bd3306c

Browse files
committed
feature(physical-plan): add NullAwareMode to SortMergeJoinExec
1 parent 6f3f7b7 commit bd3306c

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

  • datafusion/physical-plan/src/joins/sort_merge_join

‎datafusion/physical-plan/src/joins/sort_merge_join/exec.rs‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,67 @@ use datafusion_physical_expr_common::physical_expr::{PhysicalExprRef, fmt_sql};
6464
use datafusion_physical_expr_common::sort_expr::{LexOrdering, OrderingRequirements};
6565
use futures::StreamExt;
6666

67+
68+
/// Null-aware (`NOT IN`) semantics of a soft merge join, derived from
69+
/// [`SortMergeJoinExec::null_aware`] and the join type.
70+
///
71+
/// Only these combinations are legal (see [`Self::try_new`]), so the
72+
/// stream matches on this instead of re-checking `null_aware && join_type == ..`.
73+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74+
pub(super) enum NullAwareMode {
75+
/// `left.key NOT IN (right.key)`: emits left rows.
76+
LeftAnti
77+
}
78+
79+
impl NullAwareMode {
80+
/// Validates that `null_aware` may be set for this join and returns its mode.
81+
pub(super) fn try_new(
82+
join_type: JoinType,
83+
num_keys: usize,
84+
has_filter: bool,
85+
null_equality: NullEquality,
86+
sort_options: &[SortOptions],
87+
) -> Result<Self> {
88+
if num_keys != 1 || has_filter {
89+
return plan_err!(
90+
"null-aware SortMergeJoin requires one key and no filter"
91+
);
92+
}
93+
94+
// NULL keys must not match each other.
95+
// The null-aware execution path handles UNKNOWN results separately.
96+
// UNKNOWN happens when `WHERE x NOT IN (NULL)`
97+
if null_equality != NullEquality::NullEqualsNothing {
98+
return plan_err!(
99+
"null-aware SortMergeJoin requires NullEqualsNothing"
100+
);
101+
}
102+
103+
let mode = match join_type {
104+
JoinType::LeftAnti => Self::LeftAnti,
105+
_ => {
106+
return plan_err!(
107+
"null-aware SortMergeJoin only supports LeftAnti, got {join_type}"
108+
);
109+
}
110+
};
111+
112+
match mode {
113+
Self::LeftAnti => {
114+
// With a single right-side partition and NULLS FIRST,
115+
// the first row reveals whether the subquery contains NULL.
116+
if sort_options.len() != 1 || !sort_options[0].nulls_first {
117+
return plan_err!(
118+
"null-aware {join_type} SortMergeJoin requires NULLS FIRST ordering"
119+
);
120+
}
121+
Ok(mode)
122+
}
123+
}
124+
}
125+
}
126+
127+
67128
/// Join execution plan that executes equi-join predicates on multiple partitions using Sort-Merge
68129
/// join algorithm and applies an optional filter post join. Can be used to join arbitrarily large
69130
/// inputs where one or both of the inputs don't fit in the available memory.

0 commit comments

Comments
 (0)