1919
2020use 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 ) ]
70101pub 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 {
88120impl 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) ;
0 commit comments