Skip to content

Commit 953381d

Browse files
[AURON #2343] Support native flatten for mixed child array nullability (#2344)
# Which issue does this PR close? Closes #2343 # Rationale for this change Auron currently excludes Spark's `flatten function` tests because native execution can fail when child arrays have different `containsNull` metadata. `flatten` is a built-in Spark array expression, so supporting it in native execution improves Spark SQL compatibility and reduces fallback coverage gaps. # What changes are included in this PR? - Adds a native `Spark_ArrayFlatten` function. - Converts Spark `Flatten` expressions to the new native function. - Preserves Spark semantics for: - null outer arrays - null child arrays - empty arrays - null elements inside child arrays - primitive and struct element arrays - Adds an Auron SQL test for mixed child array nullability. - Removes the `flatten function` excludes from Spark 3.1/3.2/3.4/3.5 test settings. # Are there any user-facing changes? No user-facing API changes. More `flatten` expressions can now be executed natively by Auron. # How was this patch tested? UT. Signed-off-by: weimingdiit <weimingdiit@gmail.com> Co-authored-by: Shilun Fan <slfan1989@apache.org>
1 parent 2087a10 commit 953381d

9 files changed

Lines changed: 268 additions & 16 deletions

File tree

auron-spark-tests/spark31/src/test/scala/org/apache/auron/utils/AuronSparkTestSettings.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ class AuronSparkTestSettings extends SparkTestSettings {
3131
// Native execution wraps SparkRuntimeException from map_concat input validation in
3232
// SparkException.
3333
.exclude("map_concat function")
34-
// Native flatten can fail when child arrays have different containsNull metadata.
35-
.exclude("flatten function")
3634
// Native execution wraps SparkRuntimeException from null map keys in SparkException.
3735
.exclude("SPARK-24734: Fix containsNull of Concat for array type")
3836

auron-spark-tests/spark32/src/test/scala/org/apache/auron/utils/AuronSparkTestSettings.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ class AuronSparkTestSettings extends SparkTestSettings {
3131
.exclude("map with arrays")
3232
.exclude("map_concat function")
3333
.exclude("SPARK-24734: Fix containsNull of Concat for array type")
34-
// Native flatten can fail when child arrays have different containsNull metadata.
35-
.exclude("flatten function")
3634

3735
enableSuite[AuronDateFunctionsSuite]
3836
// Native execution wraps Spark parsing/format validation exceptions in SparkException.

auron-spark-tests/spark34/src/test/scala/org/apache/auron/utils/AuronSparkTestSettings.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ class AuronSparkTestSettings extends SparkTestSettings {
3232
.exclude("SPARK-24734: Fix containsNull of Concat for array type")
3333
.exclude("array_insert functions")
3434
.exclude("transform keys function - Invalid lambda functions and exceptions")
35-
// Native flatten can fail when child arrays have different containsNull metadata.
36-
.exclude("flatten function")
3735

3836
enableSuite[AuronDateFunctionsSuite]
3937
// Native execution wraps Spark parsing/format validation exceptions in SparkException.

auron-spark-tests/spark35/src/test/scala/org/apache/auron/utils/AuronSparkTestSettings.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ class AuronSparkTestSettings extends SparkTestSettings {
3232
.exclude("SPARK-24734: Fix containsNull of Concat for array type")
3333
.exclude("array_insert functions")
3434
.exclude("transform keys function - Invalid lambda functions and exceptions")
35-
// Native flatten can fail when child arrays have different containsNull metadata.
36-
.exclude("flatten function")
3735

3836
enableSuite[AuronDateFunctionsSuite]
3937
// Native execution wraps Spark parsing/format validation exceptions in SparkException.

native-engine/datafusion-ext-functions/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub fn create_auron_ext_function(
6565
}
6666
"Spark_ParseJson" => Arc::new(spark_get_json_object::spark_parse_json),
6767
"Spark_ArrayReverse" => Arc::new(spark_array::array_reverse),
68+
"Spark_ArrayFlatten" => Arc::new(spark_array::array_flatten),
6869
"Spark_MakeArray" => Arc::new(spark_make_array::array),
6970
"Spark_MapConcat" => Arc::new(spark_map::map_concat),
7071
"Spark_MapFromArrays" => Arc::new(spark_map::map_from_arrays),

native-engine/datafusion-ext-functions/src/spark_array.rs

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
//! Array expressions
17+
1618
use std::sync::Arc;
1719

18-
use arrow::{array::*, datatypes::DataType};
20+
use arrow::{
21+
array::{Array, ArrayRef, ListArray, MutableArrayData, make_array},
22+
buffer::{NullBuffer, OffsetBuffer, ScalarBuffer},
23+
datatypes::{DataType, Field},
24+
};
1925
use datafusion::{
2026
common::{Result, ScalarValue},
2127
logical_expr::ColumnarValue,
@@ -80,11 +86,94 @@ fn reverse_list_array(array: &ListArray) -> Result<ArrayRef> {
8086
ScalarValue::iter_to_array(values)
8187
}
8288

89+
fn as_list_array(array: &ArrayRef) -> Result<ListArray> {
90+
downcast_any!(array, ListArray).cloned()
91+
}
92+
93+
fn columnar_value_to_list_array(arg: &ColumnarValue) -> Result<ListArray> {
94+
match arg {
95+
ColumnarValue::Array(array) if matches!(array.data_type(), DataType::Null) => {
96+
Ok(ListArray::new_null(
97+
Arc::new(Field::new_list_field(DataType::Null, true)),
98+
array.len(),
99+
))
100+
}
101+
ColumnarValue::Array(array) => as_list_array(array),
102+
ColumnarValue::Scalar(scalar) if scalar.is_null() => {
103+
let list_field = match scalar.data_type() {
104+
DataType::List(field) => field,
105+
_ => Arc::new(Field::new_list_field(DataType::Null, true)),
106+
};
107+
Ok(ListArray::new_null(list_field, 1))
108+
}
109+
ColumnarValue::Scalar(scalar) => {
110+
let array = scalar.to_array()?;
111+
as_list_array(&array)
112+
}
113+
}
114+
}
115+
116+
/// Flatten an array of arrays into a single array.
117+
pub fn array_flatten(args: &[ColumnarValue]) -> Result<ColumnarValue> {
118+
if args.len() != 1 {
119+
df_execution_err!("array_flatten expects one argument, got {}", args.len())?;
120+
}
121+
122+
let outer = columnar_value_to_list_array(&args[0])?;
123+
let inner = as_list_array(&outer.values())?;
124+
let inner_values = inner.values();
125+
let inner_values_data = inner_values.to_data();
126+
let mut mutable = MutableArrayData::new(vec![&inner_values_data], true, inner_values.len());
127+
128+
let mut offsets = Vec::with_capacity(outer.len() + 1);
129+
let mut valids = Vec::with_capacity(outer.len());
130+
let mut offset = 0i32;
131+
offsets.push(offset);
132+
133+
for row_idx in 0..outer.len() {
134+
if outer.is_null(row_idx) {
135+
valids.push(false);
136+
offsets.push(offset);
137+
continue;
138+
}
139+
140+
let outer_start = outer.value_offsets()[row_idx] as usize;
141+
let outer_end = outer.value_offsets()[row_idx + 1] as usize;
142+
let row_valid = (outer_start..outer_end).all(|inner_idx| inner.is_valid(inner_idx));
143+
144+
valids.push(row_valid);
145+
if row_valid {
146+
let mut row_len = 0i32;
147+
for inner_idx in outer_start..outer_end {
148+
let inner_start = inner.value_offsets()[inner_idx] as usize;
149+
let inner_end = inner.value_offsets()[inner_idx + 1] as usize;
150+
mutable.extend(0, inner_start, inner_end);
151+
row_len += (inner_end - inner_start) as i32;
152+
}
153+
offset += row_len;
154+
}
155+
offsets.push(offset);
156+
}
157+
158+
let values = make_array(mutable.freeze());
159+
let field = Arc::new(Field::new_list_field(values.data_type().clone(), true));
160+
Ok(ColumnarValue::Array(Arc::new(ListArray::try_new(
161+
field,
162+
OffsetBuffer::new(ScalarBuffer::from(offsets)),
163+
values,
164+
Some(NullBuffer::from(valids)),
165+
)?)))
166+
}
167+
83168
#[cfg(test)]
84169
mod test {
85-
use std::sync::Arc;
170+
use std::{error::Error, sync::Arc};
86171

87-
use arrow::datatypes::Int32Type;
172+
use arrow::{
173+
array::{ArrayRef, ListArray},
174+
buffer::{NullBuffer, OffsetBuffer, ScalarBuffer},
175+
datatypes::{Field, Int32Type},
176+
};
88177
use datafusion::common::ScalarValue;
89178

90179
use super::*;
@@ -137,4 +226,38 @@ mod test {
137226
assert_eq!(&result, &expected);
138227
Ok(())
139228
}
229+
230+
#[test]
231+
fn test_array_flatten_int() -> std::result::Result<(), Box<dyn Error>> {
232+
let inner = ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
233+
Some(vec![Some(1), None]),
234+
Some(vec![Some(3)]),
235+
Some(vec![]),
236+
Some(vec![Some(4), Some(5)]),
237+
Some(vec![Some(6)]),
238+
None,
239+
]);
240+
let inner: ArrayRef = Arc::new(inner);
241+
let input: ArrayRef = Arc::new(ListArray::try_new(
242+
Arc::new(Field::new_list_field(inner.data_type().clone(), true)),
243+
OffsetBuffer::new(ScalarBuffer::from(vec![0, 2, 4, 6, 6, 6])),
244+
inner,
245+
Some(NullBuffer::from(vec![true, true, true, true, false])),
246+
)?);
247+
248+
let result = array_flatten(&[ColumnarValue::Array(input)])?.into_array(5)?;
249+
250+
let expected = vec![
251+
Some(vec![Some(1), None, Some(3)]),
252+
Some(vec![Some(4), Some(5)]),
253+
None,
254+
Some(vec![]),
255+
None,
256+
];
257+
let expected: ArrayRef =
258+
Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(expected));
259+
260+
assert_eq!(&result, &expected);
261+
Ok(())
262+
}
140263
}

native-engine/datafusion-ext-functions/src/spark_make_array.rs

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
1818
use std::sync::Arc;
1919

20-
use arrow::{array::*, datatypes::DataType};
20+
use arrow::{
21+
array::*,
22+
datatypes::{DataType, Field, Fields},
23+
};
2124
use datafusion::{
2225
common::{Result, ScalarValue},
2326
logical_expr::ColumnarValue,
2427
};
25-
use datafusion_ext_commons::df_execution_err;
28+
use datafusion_ext_commons::{arrow::cast::cast, df_execution_err};
2629

2730
macro_rules! downcast_vec {
2831
($ARGS:expr, $ARRAY_TYPE:ident) => {{
@@ -105,9 +108,20 @@ fn array_array(args: &[ArrayRef]) -> Result<ArrayRef> {
105108
DataType::UInt16 => array!(args, UInt16Array, UInt16Builder),
106109
DataType::UInt32 => array!(args, UInt32Array, UInt32Builder),
107110
DataType::UInt64 => array!(args, UInt64Array, UInt64Builder),
108-
data_type => {
111+
_ => {
109112
// naive implementation with scalar values
110113
let num_rows = args[0].len();
114+
let data_type = common_array_element_data_type(args)?;
115+
let args = args
116+
.iter()
117+
.map(|arg| {
118+
if arg.data_type() == &data_type {
119+
Ok(arg.clone())
120+
} else {
121+
cast(arg.as_ref(), &data_type)
122+
}
123+
})
124+
.collect::<Result<Vec<_>>>()?;
111125
let mut output_scalars = Vec::with_capacity(num_rows);
112126
for i in 0..num_rows {
113127
let row_scalars: Vec<ScalarValue> = args
@@ -116,7 +130,7 @@ fn array_array(args: &[ArrayRef]) -> Result<ArrayRef> {
116130
.collect::<Result<_>>()?;
117131
output_scalars.push(ScalarValue::List(ScalarValue::new_list(
118132
&row_scalars,
119-
data_type,
133+
&data_type,
120134
true,
121135
)));
122136
}
@@ -126,6 +140,57 @@ fn array_array(args: &[ArrayRef]) -> Result<ArrayRef> {
126140
Ok(res)
127141
}
128142

143+
fn common_array_element_data_type(args: &[ArrayRef]) -> Result<DataType> {
144+
let mut data_type = args[0].data_type().clone();
145+
for arg in &args[1..] {
146+
data_type = widen_data_type(&data_type, arg.data_type())?;
147+
}
148+
Ok(data_type)
149+
}
150+
151+
fn widen_data_type(left: &DataType, right: &DataType) -> Result<DataType> {
152+
if left == right {
153+
return Ok(left.clone());
154+
}
155+
156+
match (left, right) {
157+
(DataType::List(left_field), DataType::List(right_field)) => {
158+
let item_type = widen_data_type(left_field.data_type(), right_field.data_type())?;
159+
Ok(DataType::List(Arc::new(Field::new(
160+
left_field.name().as_str(),
161+
item_type,
162+
left_field.is_nullable() || right_field.is_nullable(),
163+
))))
164+
}
165+
(DataType::Struct(left_fields), DataType::Struct(right_fields))
166+
if left_fields.len() == right_fields.len() =>
167+
{
168+
let fields = left_fields
169+
.iter()
170+
.zip(right_fields.iter())
171+
.map(|(left_field, right_field)| {
172+
if left_field.name() != right_field.name() {
173+
return df_execution_err!(
174+
"array child struct fields must have same names, got {} and {}",
175+
left_field.name(),
176+
right_field.name()
177+
);
178+
}
179+
Ok(Arc::new(Field::new(
180+
left_field.name().as_str(),
181+
widen_data_type(left_field.data_type(), right_field.data_type())?,
182+
left_field.is_nullable() || right_field.is_nullable(),
183+
)))
184+
})
185+
.collect::<Result<Vec<_>>>()?;
186+
Ok(DataType::Struct(Fields::from(fields)))
187+
}
188+
_ => df_execution_err!(
189+
"array child values must have same data type, got {left:?} and {right:?}"
190+
),
191+
}
192+
}
193+
129194
/// put values in an array.
130195
pub fn array(values: &[ColumnarValue]) -> Result<ColumnarValue> {
131196
let arrays: Vec<ArrayRef> = values
@@ -145,7 +210,8 @@ mod test {
145210

146211
use arrow::{
147212
array::{ArrayRef, Int32Array, ListArray},
148-
datatypes::{Float32Type, Int32Type},
213+
buffer::{OffsetBuffer, ScalarBuffer},
214+
datatypes::{DataType, Field, Float32Type, Int32Type},
149215
};
150216
use datafusion::{common::ScalarValue, physical_plan::ColumnarValue};
151217

@@ -215,4 +281,48 @@ mod test {
215281
assert_eq!(&result, &expected);
216282
Ok(())
217283
}
284+
285+
#[test]
286+
fn test_make_array_with_mixed_child_array_nullability() -> Result<(), Box<dyn Error>> {
287+
let non_nullable_items: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), Some(2)]));
288+
let non_nullable_list: ArrayRef = Arc::new(ListArray::try_new(
289+
Arc::new(Field::new_list_field(DataType::Int32, false)),
290+
OffsetBuffer::new(ScalarBuffer::from(vec![0, 1, 2])),
291+
non_nullable_items,
292+
None,
293+
)?);
294+
295+
let nullable_items: ArrayRef = Arc::new(Int32Array::from(vec![None, Some(3)]));
296+
let nullable_list: ArrayRef = Arc::new(ListArray::try_new(
297+
Arc::new(Field::new_list_field(DataType::Int32, true)),
298+
OffsetBuffer::new(ScalarBuffer::from(vec![0, 1, 2])),
299+
nullable_items,
300+
None,
301+
)?);
302+
303+
let result = array(&vec![
304+
ColumnarValue::Array(non_nullable_list),
305+
ColumnarValue::Array(nullable_list),
306+
])?
307+
.into_array(2)?;
308+
309+
let expected_values: ArrayRef = Arc::new(ListArray::try_new(
310+
Arc::new(Field::new_list_field(DataType::Int32, true)),
311+
OffsetBuffer::new(ScalarBuffer::from(vec![0, 1, 2, 3, 4])),
312+
Arc::new(Int32Array::from(vec![Some(1), None, Some(2), Some(3)])),
313+
None,
314+
)?);
315+
let expected: ArrayRef = Arc::new(ListArray::try_new(
316+
Arc::new(Field::new_list_field(
317+
expected_values.data_type().clone(),
318+
true,
319+
)),
320+
OffsetBuffer::new(ScalarBuffer::from(vec![0, 2, 4])),
321+
expected_values,
322+
None,
323+
)?);
324+
325+
assert_eq!(&result, &expected);
326+
Ok(())
327+
}
218328
}

spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronFunctionSuite.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,31 @@ class AuronFunctionSuite extends AuronQueryTest with BaseAuronSQLSuite {
9696
}
9797
}
9898

99+
test("flatten function with mixed child array nullability") {
100+
withTable("t1") {
101+
sql("""
102+
|create table t1 using parquet as
103+
|select
104+
| array(array(1, 2), array(3, cast(null as int)), array()) as ints,
105+
| array(array(named_struct('a', 1)), array(named_struct('a', 2))) as structs
106+
|union all
107+
|select
108+
| array(array(4), cast(null as array<int>)),
109+
| array(array(named_struct('a', 3)), cast(array() as array<struct<a:int>>))
110+
|union all
111+
|select
112+
| cast(array() as array<array<int>>),
113+
| cast(array() as array<array<struct<a:int>>>)
114+
|union all
115+
|select
116+
| cast(null as array<array<int>>),
117+
| cast(null as array<array<struct<a:int>>>)
118+
|""".stripMargin)
119+
120+
checkSparkAnswerAndOperator("select flatten(ints), flatten(structs) from t1")
121+
}
122+
}
123+
99124
test("expm1 function") {
100125
withTable("t1") {
101126
sql("create table t1(c1 double) using parquet")

0 commit comments

Comments
 (0)