Skip to content

Commit 8460a80

Browse files
committed
added NullHandling Enum matching Spark explode_outer
1 parent 8741a77 commit 8460a80

10 files changed

Lines changed: 425 additions & 56 deletions

File tree

‎datafusion/common/src/lib.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ pub use schema_reference::SchemaReference;
9595
pub use spans::{Location, Span, Spans};
9696
pub use stats::{ColumnStatistics, Statistics};
9797
pub use table_reference::{ResolvedTableReference, TableReference};
98-
pub use unnest::{RecursionUnnestOption, UnnestOptions};
98+
pub use unnest::{NullHandling, RecursionUnnestOption, UnnestOptions};
9999
pub use utils::project_schema;
100100

101101
// These are hidden from docs purely to avoid polluting the public view of what this crate exports.

‎datafusion/common/src/unnest.rs‎

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,38 @@
1919
2020
use crate::Column;
2121

22+
/// How [`UnnestOptions`] handles `NULL` and empty list values in the input column.
23+
///
24+
/// The variants enumerate the three observable behaviors so that callers do
25+
/// not have to compose multiple boolean flags to express what they want.
26+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Hash)]
27+
pub enum NullHandling {
28+
/// Drop rows where the input list is `NULL` or empty. Matches the
29+
/// default behavior of systems such as DuckDB and ClickHouse.
30+
Drop,
31+
/// Preserve `NULL` input rows as a single output row containing `NULL`.
32+
/// Empty lists still produce zero output rows. This is the default and
33+
/// matches DataFusion's historical `preserve_nulls = true` behavior.
34+
#[default]
35+
Preserve,
36+
/// Like [`Self::Preserve`], and additionally treat an empty list
37+
/// identically to a `NULL` list, producing a single output row
38+
/// containing `NULL`. Matches Spark's `explode_outer` semantics.
39+
PreserveAndExpandEmpty,
40+
}
41+
2242
/// Options for unnesting a column that contains a list type,
2343
/// replicating values in the other, non nested rows.
2444
///
2545
/// Conceptually this operation is like joining each row with all the
2646
/// values in the list column.
2747
///
28-
/// If `preserve_nulls` is false, nulls and empty lists
29-
/// from the input column are not carried through to the output. This
30-
/// is the default behavior for other systems such as ClickHouse and
31-
/// DuckDB
32-
///
33-
/// If `preserve_nulls` is true (the default), nulls from the input
34-
/// column are carried through to the output.
48+
/// The behavior with `NULL` and empty input lists is controlled by
49+
/// [`NullHandling`]. See its variants for full details.
3550
///
3651
/// # Examples
3752
///
38-
/// ## `Unnest(c1)`, preserve_nulls: false
53+
/// ## `Unnest(c1)`, null_handling: NullHandling::Drop
3954
/// ```text
4055
/// ┌─────────┐ ┌─────┐ ┌─────────┐ ┌─────┐
4156
/// │ {1, 2} │ │ A │ Unnest │ 1 │ │ A │
@@ -49,7 +64,7 @@ use crate::Column;
4964
/// c1 c2
5065
/// ```
5166
///
52-
/// ## `Unnest(c1)`, preserve_nulls: true
67+
/// ## `Unnest(c1)`, null_handling: NullHandling::Preserve
5368
/// ```text
5469
/// ┌─────────┐ ┌─────┐ ┌─────────┐ ┌─────┐
5570
/// │ {1, 2} │ │ A │ Unnest │ 1 │ │ A │
@@ -63,13 +78,30 @@ use crate::Column;
6378
/// c1 c2 c1 c2
6479
/// ```
6580
///
81+
/// ## `Unnest(c1)`, null_handling: NullHandling::PreserveAndExpandEmpty
82+
/// ```text
83+
/// ┌─────────┐ ┌─────┐ ┌─────────┐ ┌─────┐
84+
/// │ {1, 2} │ │ A │ Unnest │ 1 │ │ A │
85+
/// ├─────────┤ ├─────┤ ├─────────┤ ├─────┤
86+
/// │ null │ │ B │ │ 2 │ │ A │
87+
/// ├─────────┤ ├─────┤ ────────────▶ ├─────────┤ ├─────┤
88+
/// │ {} │ │ D │ │ null │ │ B │
89+
/// ├─────────┤ ├─────┤ ├─────────┤ ├─────┤
90+
/// │ {3} │ │ E │ │ null │ │ D │
91+
/// └─────────┘ └─────┘ ├─────────┤ ├─────┤
92+
/// c1 c2 │ 3 │ │ E │
93+
/// └─────────┘ └─────┘
94+
/// c1 c2
95+
/// ```
96+
///
6697
/// `recursions` instruct how a column should be unnested (e.g unnesting a column multiple
6798
/// time, with depth = 1 and depth = 2). Any unnested column not being mentioned inside this
6899
/// options is inferred to be unnested with depth = 1
69100
#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq)]
70101
pub struct UnnestOptions {
71-
/// Should nulls in the input be preserved? Defaults to true
72-
pub preserve_nulls: bool,
102+
/// How to handle `NULL` and empty list values in the input column.
103+
/// Defaults to [`NullHandling::Preserve`].
104+
pub null_handling: NullHandling,
73105
/// If specific columns need to be unnested multiple times (e.g at different depth),
74106
/// declare them here. Any unnested columns not being mentioned inside this option
75107
/// will be unnested with depth = 1
@@ -88,8 +120,7 @@ pub struct RecursionUnnestOption {
88120
impl Default for UnnestOptions {
89121
fn default() -> Self {
90122
Self {
91-
// default to true to maintain backwards compatible behavior
92-
preserve_nulls: true,
123+
null_handling: NullHandling::Preserve,
93124
recursions: vec![],
94125
}
95126
}
@@ -101,13 +132,41 @@ impl UnnestOptions {
101132
Default::default()
102133
}
103134

104-
/// Set the behavior with nulls in the input as described on
105-
/// [`Self`]
106-
pub fn with_preserve_nulls(mut self, preserve_nulls: bool) -> Self {
107-
self.preserve_nulls = preserve_nulls;
135+
/// Set the [`NullHandling`] mode used when unnesting `NULL` or empty
136+
/// input lists.
137+
pub fn with_null_handling(mut self, null_handling: NullHandling) -> Self {
138+
self.null_handling = null_handling;
108139
self
109140
}
110141

142+
/// Backward-compatible setter that maps the previous boolean
143+
/// `preserve_nulls` flag onto [`NullHandling`].
144+
///
145+
/// `true` maps to [`NullHandling::Preserve`]; `false` maps to
146+
/// [`NullHandling::Drop`]. To opt into Spark `explode_outer`
147+
/// semantics, call [`Self::with_null_handling`] directly with
148+
/// [`NullHandling::PreserveAndExpandEmpty`].
149+
pub fn with_preserve_nulls(self, preserve_nulls: bool) -> Self {
150+
let null_handling = if preserve_nulls {
151+
NullHandling::Preserve
152+
} else {
153+
NullHandling::Drop
154+
};
155+
self.with_null_handling(null_handling)
156+
}
157+
158+
/// Returns true if `NULL` input rows produce a single output row
159+
/// containing `NULL`.
160+
pub fn preserve_nulls(&self) -> bool {
161+
!matches!(self.null_handling, NullHandling::Drop)
162+
}
163+
164+
/// Returns true if empty input lists should produce a single
165+
/// output row containing `NULL` (Spark `explode_outer` semantics).
166+
pub fn expand_empty_as_null(&self) -> bool {
167+
matches!(self.null_handling, NullHandling::PreserveAndExpandEmpty)
168+
}
169+
111170
/// Set the recursions for the unnest operation
112171
pub fn with_recursions(mut self, recursion: RecursionUnnestOption) -> Self {
113172
self.recursions.push(recursion);

‎datafusion/core/tests/dataframe/mod.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4357,6 +4357,7 @@ async fn unnest_column_nulls() -> Result<()> {
43574357

43584358
let options = UnnestOptions::new().with_preserve_nulls(false);
43594359
let results = df
4360+
.clone()
43604361
.unnest_columns_with_options(&["list"], options)?
43614362
.collect()
43624363
.await?;
@@ -4373,6 +4374,29 @@ async fn unnest_column_nulls() -> Result<()> {
43734374
"
43744375
);
43754376

4377+
// Spark `explode_outer` semantics: NULL and empty lists both produce a
4378+
// single output row containing NULL.
4379+
let options = UnnestOptions::new()
4380+
.with_null_handling(datafusion_common::NullHandling::PreserveAndExpandEmpty);
4381+
let results = df
4382+
.unnest_columns_with_options(&["list"], options)?
4383+
.collect()
4384+
.await?;
4385+
assert_snapshot!(
4386+
batches_to_string(&results),
4387+
@r"
4388+
+------+----+
4389+
| list | id |
4390+
+------+----+
4391+
| 1 | A |
4392+
| 2 | A |
4393+
| | B |
4394+
| | C |
4395+
| 3 | D |
4396+
+------+----+
4397+
"
4398+
);
4399+
43764400
Ok(())
43774401
}
43784402

0 commit comments

Comments
 (0)