| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| // Spark-compatible flatten(array<array<T>>). |
| // |
| // DataFusion's flatten preserves the outer array null bitmap only. Spark returns NULL for a row |
| // when any sub-array inside that row is NULL, so Comet needs a Spark-specific null bitmap. |
| |
| use arrow::array::{Array, ArrayRef, GenericListArray, NullBufferBuilder, OffsetSizeTrait}; |
| use arrow::buffer::{NullBuffer, OffsetBuffer}; |
| use arrow::datatypes::{ |
| DataType, |
| DataType::{FixedSizeList, LargeList, List, Null}, |
| Field, FieldRef, |
| }; |
| use datafusion::common::cast::{as_large_list_array, as_list_array}; |
| use datafusion::common::{exec_err, utils::take_function_args, Result}; |
| use datafusion::logical_expr::{ |
| ColumnarValue, ReturnFieldArgs, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility, |
| }; |
| use std::sync::Arc; |
| |
| use super::arrays_zip::make_scalar_function; |
| |
| #[derive(Debug, PartialEq, Eq, Hash)] |
| pub struct SparkFlatten { |
| signature: Signature, |
| } |
| |
| impl Default for SparkFlatten { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl SparkFlatten { |
| pub fn new() -> Self { |
| Self { |
| signature: Signature::array(Volatility::Immutable), |
| } |
| } |
| } |
| |
| impl ScalarUDFImpl for SparkFlatten { |
| fn name(&self) -> &str { |
| "flatten" |
| } |
| |
| fn signature(&self) -> &Signature { |
| &self.signature |
| } |
| |
| fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> { |
| spark_flatten_return_type(&arg_types[0]) |
| } |
| |
| fn return_field_from_args(&self, args: ReturnFieldArgs) -> Result<FieldRef> { |
| let [arg_field] = take_function_args(self.name(), args.arg_fields)?; |
| let data_type = spark_flatten_return_type(arg_field.data_type())?; |
| let nullable = match arg_field.data_type() { |
| List(field) | LargeList(field) => arg_field.is_nullable() || field.is_nullable(), |
| Null => true, |
| _ => { |
| return exec_err!( |
| "Not reachable, data_type should be List, LargeList or FixedSizeList" |
| ) |
| } |
| }; |
| |
| Ok(Arc::new(Field::new(self.name(), data_type, nullable))) |
| } |
| |
| fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> { |
| make_scalar_function(spark_flatten_inner)(&args.args) |
| } |
| } |
| |
| fn spark_flatten_return_type(arg_type: &DataType) -> Result<DataType> { |
| let data_type = match arg_type { |
| List(field) => match field.data_type() { |
| List(field) | FixedSizeList(field, _) => List(Arc::clone(field)), |
| LargeList(field) => LargeList(Arc::clone(field)), |
| _ => arg_type.clone(), |
| }, |
| LargeList(field) => match field.data_type() { |
| List(field) | LargeList(field) | FixedSizeList(field, _) => { |
| LargeList(Arc::clone(field)) |
| } |
| _ => arg_type.clone(), |
| }, |
| Null => Null, |
| _ => exec_err!("Not reachable, data_type should be List, LargeList or FixedSizeList")?, |
| }; |
| |
| Ok(data_type) |
| } |
| |
| fn spark_flatten_inner(args: &[ArrayRef]) -> Result<ArrayRef> { |
| let [array] = take_function_args("flatten", args)?; |
| |
| match array.data_type() { |
| List(_) => { |
| let outer = as_list_array(array)?; |
| let (_field, offsets, values, _outer_nulls) = outer.clone().into_parts(); |
| let values = cast_fsl_to_list(values)?; |
| |
| match values.data_type() { |
| List(_) => { |
| let inner = as_list_array(&values)?; |
| let nulls = spark_flatten_nulls(outer, inner); |
| let (inner_field, inner_offsets, inner_values, _) = inner.clone().into_parts(); |
| let offsets = get_offsets_for_flatten::<i32, i32>(inner_offsets, &offsets); |
| let flattened_array = |
| GenericListArray::<i32>::new(inner_field, offsets, inner_values, nulls); |
| |
| Ok(Arc::new(flattened_array) as ArrayRef) |
| } |
| LargeList(_) => { |
| let inner = as_large_list_array(&values)?; |
| let nulls = spark_flatten_nulls(outer, inner); |
| let (inner_field, inner_offsets, inner_values, _) = inner.clone().into_parts(); |
| let offsets = get_offsets_for_flatten::<i64, i32>(inner_offsets, &offsets); |
| let flattened_array = |
| GenericListArray::<i64>::new(inner_field, offsets, inner_values, nulls); |
| |
| Ok(Arc::new(flattened_array) as ArrayRef) |
| } |
| _ => Ok(Arc::clone(array) as ArrayRef), |
| } |
| } |
| LargeList(_) => { |
| let outer = as_large_list_array(array)?; |
| let (_field, offsets, values, _outer_nulls) = outer.clone().into_parts(); |
| let values = cast_fsl_to_list(values)?; |
| |
| match values.data_type() { |
| List(_) => { |
| let inner = as_list_array(&values)?; |
| let nulls = spark_flatten_nulls(outer, inner); |
| let (inner_field, inner_offsets, inner_values, _) = inner.clone().into_parts(); |
| let offsets = get_large_offsets_for_flatten(inner_offsets, &offsets); |
| let flattened_array = |
| GenericListArray::<i64>::new(inner_field, offsets, inner_values, nulls); |
| |
| Ok(Arc::new(flattened_array) as ArrayRef) |
| } |
| LargeList(_) => { |
| let inner = as_large_list_array(&values)?; |
| let nulls = spark_flatten_nulls(outer, inner); |
| let (inner_field, inner_offsets, inner_values, _) = inner.clone().into_parts(); |
| let offsets = get_offsets_for_flatten::<i64, i64>(inner_offsets, &offsets); |
| let flattened_array = |
| GenericListArray::<i64>::new(inner_field, offsets, inner_values, nulls); |
| |
| Ok(Arc::new(flattened_array) as ArrayRef) |
| } |
| _ => Ok(Arc::clone(array) as ArrayRef), |
| } |
| } |
| Null => Ok(Arc::clone(array)), |
| _ => { |
| exec_err!("flatten does not support type '{}'", array.data_type()) |
| } |
| } |
| } |
| |
| fn spark_flatten_nulls<O: OffsetSizeTrait, P: OffsetSizeTrait>( |
| outer: &GenericListArray<P>, |
| inner: &GenericListArray<O>, |
| ) -> Option<NullBuffer> { |
| let mut nulls = NullBufferBuilder::new(outer.len()); |
| let inner_nulls = inner.nulls(); |
| |
| for (row, offset_window) in outer.offsets().windows(2).enumerate() { |
| if outer.is_null(row) { |
| nulls.append_null(); |
| continue; |
| } |
| |
| let start = offset_window[0].to_usize().unwrap(); |
| let end = offset_window[1].to_usize().unwrap(); |
| let has_null_subarray = |
| inner_nulls.is_some_and(|n| (start..end).any(|inner_row| n.is_null(inner_row))); |
| |
| if has_null_subarray { |
| nulls.append_null(); |
| } else { |
| nulls.append_non_null(); |
| } |
| } |
| |
| nulls.finish() |
| } |
| |
| fn get_offsets_for_flatten<O: OffsetSizeTrait, P: OffsetSizeTrait>( |
| inner_offsets: OffsetBuffer<O>, |
| outer_offsets: &OffsetBuffer<P>, |
| ) -> OffsetBuffer<O> { |
| let buffer = inner_offsets.into_inner(); |
| let offsets: Vec<O> = outer_offsets |
| .iter() |
| .map(|i| buffer[i.to_usize().unwrap()]) |
| .collect(); |
| OffsetBuffer::new(offsets.into()) |
| } |
| |
| fn get_large_offsets_for_flatten<O: OffsetSizeTrait, P: OffsetSizeTrait>( |
| inner_offsets: OffsetBuffer<O>, |
| outer_offsets: &OffsetBuffer<P>, |
| ) -> OffsetBuffer<i64> { |
| let buffer = inner_offsets.into_inner(); |
| let offsets: Vec<i64> = outer_offsets |
| .iter() |
| .map(|i| buffer[i.to_usize().unwrap()].to_i64().unwrap()) |
| .collect(); |
| OffsetBuffer::new(offsets.into()) |
| } |
| |
| fn cast_fsl_to_list(array: ArrayRef) -> Result<ArrayRef> { |
| match array.data_type() { |
| FixedSizeList(field, _) => Ok(arrow::compute::cast(&array, &List(Arc::clone(field)))?), |
| _ => Ok(array), |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use arrow::array::{Int32Array, Int32Builder, ListArray, ListBuilder}; |
| use datafusion::common::ScalarValue; |
| |
| type IntElement = Option<i32>; |
| type InnerArray = Option<Vec<IntElement>>; |
| type OuterArray = Option<Vec<InnerArray>>; |
| |
| #[test] |
| fn test_flatten_null_subarray_returns_null_row() { |
| let input = nested_int_list_array(vec![ |
| Some(vec![ |
| Some(vec![Some(1), Some(2), Some(3)]), |
| Some(vec![Some(4), Some(5)]), |
| ]), |
| Some(vec![Some(vec![Some(1)]), None]), |
| Some(vec![None, None]), |
| ]); |
| |
| let result = spark_flatten_inner(&[Arc::new(input)]).unwrap(); |
| let result = result.as_any().downcast_ref::<ListArray>().unwrap(); |
| |
| assert_eq!(result.len(), 3); |
| assert_list_value(result, 0, &[Some(1), Some(2), Some(3), Some(4), Some(5)]); |
| assert!(result.is_null(1)); |
| assert!(result.is_null(2)); |
| } |
| |
| #[test] |
| fn test_flatten_preserves_null_elements() { |
| let input = nested_int_list_array(vec![Some(vec![ |
| Some(vec![Some(1), None]), |
| Some(vec![None, Some(2)]), |
| ])]); |
| |
| let result = spark_flatten_inner(&[Arc::new(input)]).unwrap(); |
| let result = result.as_any().downcast_ref::<ListArray>().unwrap(); |
| |
| assert_eq!(result.len(), 1); |
| assert_list_value(result, 0, &[Some(1), None, None, Some(2)]); |
| } |
| |
| #[test] |
| fn test_flatten_allows_scalar_input() { |
| let input = nested_int_list_array(vec![Some(vec![Some(vec![Some(1), Some(2)])])]); |
| let scalar = ColumnarValue::Scalar(ScalarValue::try_from_array(&input, 0).unwrap()); |
| |
| let result = SparkFlatten::new() |
| .invoke_with_args(ScalarFunctionArgs { |
| args: vec![scalar], |
| arg_fields: vec![], |
| number_rows: 1, |
| return_field: Arc::new(arrow::datatypes::Field::new("flatten", Null, true)), |
| config_options: Arc::new(datafusion::config::ConfigOptions::default()), |
| }) |
| .unwrap(); |
| |
| assert!(matches!(result, ColumnarValue::Scalar(_))); |
| } |
| |
| fn nested_int_list_array(rows: Vec<OuterArray>) -> ListArray { |
| let inner_builder = ListBuilder::new(Int32Builder::new()); |
| let mut outer_builder = ListBuilder::new(inner_builder); |
| |
| for row in rows { |
| match row { |
| Some(subarrays) => { |
| let inner_builder = outer_builder.values(); |
| for subarray in subarrays { |
| match subarray { |
| Some(values) => { |
| for value in values { |
| match value { |
| Some(value) => inner_builder.values().append_value(value), |
| None => inner_builder.values().append_null(), |
| } |
| } |
| inner_builder.append(true); |
| } |
| None => inner_builder.append(false), |
| } |
| } |
| outer_builder.append(true); |
| } |
| None => outer_builder.append(false), |
| } |
| } |
| |
| outer_builder.finish() |
| } |
| |
| fn assert_list_value(list_array: &ListArray, row: usize, expected: &[Option<i32>]) { |
| let values = list_array.value(row); |
| let values = values.as_any().downcast_ref::<Int32Array>().unwrap(); |
| let actual = values.iter().collect::<Vec<_>>(); |
| assert_eq!(actual, expected); |
| } |
| |
| #[test] |
| fn test_return_type() { |
| let input_type = List(Arc::new(arrow::datatypes::Field::new_list_field( |
| List(Arc::new(arrow::datatypes::Field::new_list_field( |
| DataType::Int32, |
| true, |
| ))), |
| true, |
| ))); |
| assert_eq!( |
| SparkFlatten::new().return_type(&[input_type]).unwrap(), |
| List(Arc::new(arrow::datatypes::Field::new_list_field( |
| DataType::Int32, |
| true, |
| ))) |
| ); |
| } |
| |
| #[test] |
| fn test_return_field_nullability_matches_spark() { |
| let array_of_arrays = |outer_nullable, contains_null_subarray| { |
| let element_field = Arc::new(arrow::datatypes::Field::new_list_field( |
| DataType::Int32, |
| true, |
| )); |
| Arc::new(arrow::datatypes::Field::new( |
| "arg", |
| List(Arc::new(arrow::datatypes::Field::new_list_field( |
| List(element_field), |
| contains_null_subarray, |
| ))), |
| outer_nullable, |
| )) |
| }; |
| |
| let scalar_args: [Option<&ScalarValue>; 1] = [None]; |
| let arg_fields = [array_of_arrays(false, false)]; |
| let field = SparkFlatten::new() |
| .return_field_from_args(ReturnFieldArgs { |
| arg_fields: &arg_fields, |
| scalar_arguments: &scalar_args, |
| }) |
| .unwrap(); |
| assert!(!field.is_nullable()); |
| |
| let arg_fields = [array_of_arrays(false, true)]; |
| let field = SparkFlatten::new() |
| .return_field_from_args(ReturnFieldArgs { |
| arg_fields: &arg_fields, |
| scalar_arguments: &scalar_args, |
| }) |
| .unwrap(); |
| assert!(field.is_nullable()); |
| |
| let arg_fields = [array_of_arrays(true, false)]; |
| let field = SparkFlatten::new() |
| .return_field_from_args(ReturnFieldArgs { |
| arg_fields: &arg_fields, |
| scalar_arguments: &scalar_args, |
| }) |
| .unwrap(); |
| assert!(field.is_nullable()); |
| } |
| } |