| // 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. | |
| use arrow::array::RecordBatch; | |
| use arrow::array::{ | |
| new_null_array, Array, ArrayRef, Capacities, ListArray, MutableArrayData, StructArray, | |
| }; | |
| use arrow::buffer::{NullBuffer, OffsetBuffer}; | |
| use arrow::datatypes::DataType::{FixedSizeList, LargeList, List, Null}; | |
| use arrow::datatypes::Schema; | |
| use arrow::datatypes::{DataType, Field, Fields}; | |
| use datafusion::common::cast::{as_fixed_size_list_array, as_large_list_array, as_list_array}; | |
| use datafusion::common::{exec_err, Result, ScalarValue}; | |
| use datafusion::logical_expr::ColumnarValue; | |
| use datafusion::physical_expr::PhysicalExpr; | |
| use std::fmt::{Display, Formatter}; | |
| use std::sync::Arc; | |
| // TODO: Reuse functions from DF | |
| // use datafusion::functions_nested::utils::make_scalar_function; | |
| // use datafusion::functions_nested::arrays_zip::arrays_zip_inner; | |
| #[derive(Debug, Eq, Hash, PartialEq)] | |
| pub struct SparkArraysZipFunc { | |
| values: Vec<Arc<dyn PhysicalExpr>>, | |
| names: Vec<String>, | |
| } | |
| impl SparkArraysZipFunc { | |
| pub fn new(values: Vec<Arc<dyn PhysicalExpr>>, names: Vec<String>) -> Self { | |
| Self { values, names } | |
| } | |
| fn fields(&self, schema: &Schema) -> Result<Vec<Field>> { | |
| let mut fields: Vec<Field> = Vec::with_capacity(self.values.len()); | |
| for (i, v) in self.values.iter().enumerate() { | |
| let element_type = match (*v).as_ref().data_type(schema)? { | |
| List(field) | LargeList(field) | FixedSizeList(field, _) => { | |
| field.data_type().clone() | |
| } | |
| Null => Null, | |
| dt => { | |
| return exec_err!("arrays_zip expects array arguments, got {dt}"); | |
| } | |
| }; | |
| fields.push(Field::new(self.names[i].to_string(), element_type, true)); | |
| } | |
| Ok(fields) | |
| } | |
| } | |
| impl Display for SparkArraysZipFunc { | |
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| write!( | |
| f, | |
| "ArraysZip [values: {:?}, names: {:?}]", | |
| self.values, self.names | |
| ) | |
| } | |
| } | |
| impl PhysicalExpr for SparkArraysZipFunc { | |
| fn data_type(&self, input_schema: &Schema) -> Result<DataType> { | |
| let fields = self.fields(input_schema)?; | |
| Ok(List(Arc::new(Field::new_list_field( | |
| DataType::Struct(Fields::from(fields)), | |
| false, | |
| )))) | |
| } | |
| fn nullable(&self, _input_schema: &Schema) -> Result<bool> { | |
| Ok(true) | |
| } | |
| fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> { | |
| let values = self | |
| .values | |
| .iter() | |
| .map(|e| e.evaluate(batch)) | |
| .collect::<datafusion::common::Result<Vec<_>>>()?; | |
| make_scalar_function(|arr| arrays_zip_inner(arr, self.names.clone()))(&values) | |
| } | |
| fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> { | |
| self.values.iter().collect() | |
| } | |
| fn with_new_children( | |
| self: Arc<Self>, | |
| children: Vec<Arc<dyn PhysicalExpr>>, | |
| ) -> Result<Arc<dyn PhysicalExpr>> { | |
| Ok(Arc::new(SparkArraysZipFunc::new( | |
| children.clone(), | |
| self.names.clone(), | |
| ))) | |
| } | |
| fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | |
| Display::fmt(self, f) | |
| } | |
| } | |
| /// This function is copied from https://github.com/apache/datafusion/blob/53.0.0/datafusion/spark/src/function/functions_nested_utils.rs#L23 | |
| /// b/c the original function is public to crate only | |
| pub fn make_scalar_function<F>(inner: F) -> impl Fn(&[ColumnarValue]) -> Result<ColumnarValue> | |
| where | |
| F: Fn(&[ArrayRef]) -> Result<ArrayRef>, | |
| { | |
| move |args: &[ColumnarValue]| { | |
| // first, identify if any of the arguments is an Array. If yes, store its `len`, | |
| // as any scalar will need to be converted to an array of len `len`. | |
| let len = args | |
| .iter() | |
| .fold(Option::<usize>::None, |acc, arg| match arg { | |
| ColumnarValue::Scalar(_) => acc, | |
| ColumnarValue::Array(a) => Some(a.len()), | |
| }); | |
| let is_scalar = len.is_none(); | |
| let args = ColumnarValue::values_to_arrays(args)?; | |
| let result = (inner)(&args); | |
| if is_scalar { | |
| // If all inputs are scalar, keeps output as scalar | |
| let result = result.and_then(|arr| ScalarValue::try_from_array(&arr, 0)); | |
| result.map(ColumnarValue::Scalar) | |
| } else { | |
| result.map(ColumnarValue::Array) | |
| } | |
| } | |
| } | |
| /// This struct is copied from https://github.com/apache/datafusion/blob/53.0.0/datafusion/functions-nested/src/arrays_zip.rs#L40 | |
| struct ListColumnView { | |
| /// The flat values array backing this list column. | |
| values: ArrayRef, | |
| /// Pre-computed per-row start offsets (length = num_rows + 1). | |
| offsets: Vec<usize>, | |
| /// Pre-computed null bitmap: true means the row is null. | |
| is_null: Vec<bool>, | |
| } | |
| /// This function is copied from https://github.com/apache/datafusion/blob/53.0.0/datafusion/functions-nested/src/arrays_zip.rs#L159 | |
| /// with an additional names argument to parameterized struct keys like Spark does | |
| pub fn arrays_zip_inner(args: &[ArrayRef], names: Vec<String>) -> Result<ArrayRef> { | |
| if args.is_empty() { | |
| return exec_err!("arrays_zip requires at least one argument"); | |
| } | |
| let num_rows = args[0].len(); | |
| // Build a type-erased ListColumnView for each argument. | |
| // None means the argument is Null-typed (all nulls, no backing data). | |
| let mut views: Vec<Option<ListColumnView>> = Vec::with_capacity(args.len()); | |
| let mut element_types: Vec<DataType> = Vec::with_capacity(args.len()); | |
| for (i, arg) in args.iter().enumerate() { | |
| match arg.data_type() { | |
| List(field) => { | |
| let arr = as_list_array(arg)?; | |
| let raw_offsets = arr.value_offsets(); | |
| let offsets: Vec<usize> = raw_offsets.iter().map(|&o| o as usize).collect(); | |
| let is_null = (0..num_rows).map(|row| arr.is_null(row)).collect(); | |
| element_types.push(field.data_type().clone()); | |
| views.push(Some(ListColumnView { | |
| values: Arc::clone(arr.values()), | |
| offsets, | |
| is_null, | |
| })); | |
| } | |
| LargeList(field) => { | |
| let arr = as_large_list_array(arg)?; | |
| let raw_offsets = arr.value_offsets(); | |
| let offsets: Vec<usize> = raw_offsets.iter().map(|&o| o as usize).collect(); | |
| let is_null = (0..num_rows).map(|row| arr.is_null(row)).collect(); | |
| element_types.push(field.data_type().clone()); | |
| views.push(Some(ListColumnView { | |
| values: Arc::clone(arr.values()), | |
| offsets, | |
| is_null, | |
| })); | |
| } | |
| FixedSizeList(field, size) => { | |
| let arr = as_fixed_size_list_array(arg)?; | |
| let size = *size as usize; | |
| let offsets: Vec<usize> = (0..=num_rows).map(|row| row * size).collect(); | |
| let is_null = (0..num_rows).map(|row| arr.is_null(row)).collect(); | |
| element_types.push(field.data_type().clone()); | |
| views.push(Some(ListColumnView { | |
| values: Arc::clone(arr.values()), | |
| offsets, | |
| is_null, | |
| })); | |
| } | |
| Null => { | |
| element_types.push(Null); | |
| views.push(None); | |
| } | |
| dt => { | |
| return exec_err!("arrays_zip argument {i} expected list type, got {dt}"); | |
| } | |
| } | |
| } | |
| // Collect per-column values data for MutableArrayData builders. | |
| let values_data: Vec<_> = views | |
| .iter() | |
| .map(|v| v.as_ref().map(|view| view.values.to_data())) | |
| .collect(); | |
| let struct_fields: Fields = element_types | |
| .iter() | |
| .enumerate() | |
| .map(|(i, dt)| Field::new(names[i].to_string(), dt.clone(), true)) | |
| .collect::<Vec<_>>() | |
| .into(); | |
| // Create a MutableArrayData builder per column. For None (Null-typed) | |
| // args we only need extend_nulls, so we track them separately. | |
| let mut builders: Vec<Option<MutableArrayData>> = values_data | |
| .iter() | |
| .map(|vd| { | |
| vd.as_ref().map(|data| { | |
| MutableArrayData::with_capacities(vec![data], true, Capacities::Array(0)) | |
| }) | |
| }) | |
| .collect(); | |
| let mut offsets: Vec<i32> = Vec::with_capacity(num_rows + 1); | |
| offsets.push(0); | |
| let mut null_mask: Vec<bool> = Vec::with_capacity(num_rows); | |
| let mut total_values: usize = 0; | |
| // Process each row: compute per-array lengths, then copy values | |
| // and pad shorter arrays with NULLs. | |
| for row_idx in 0..num_rows { | |
| let mut max_len: usize = 0; | |
| let mut all_null = true; | |
| for view in views.iter().flatten() { | |
| if !view.is_null[row_idx] { | |
| all_null = false; | |
| let len = view.offsets[row_idx + 1] - view.offsets[row_idx]; | |
| max_len = max_len.max(len); | |
| } | |
| } | |
| if all_null { | |
| null_mask.push(true); | |
| offsets.push(*offsets.last().unwrap()); | |
| continue; | |
| } | |
| null_mask.push(false); | |
| // Extend each column builder for this row. | |
| for (col_idx, view) in views.iter().enumerate() { | |
| match view { | |
| Some(v) if !v.is_null[row_idx] => { | |
| let start = v.offsets[row_idx]; | |
| let end = v.offsets[row_idx + 1]; | |
| let len = end - start; | |
| let builder = builders[col_idx].as_mut().unwrap(); | |
| builder.try_extend(0, start, end)?; | |
| if len < max_len { | |
| builder.try_extend_nulls(max_len - len)?; | |
| } | |
| } | |
| _ => { | |
| // Null list entry or None (Null-typed) arg — all nulls. | |
| if let Some(builder) = builders[col_idx].as_mut() { | |
| builder.try_extend_nulls(max_len)?; | |
| } | |
| } | |
| } | |
| } | |
| total_values += max_len; | |
| let last = *offsets.last().unwrap(); | |
| offsets.push(last + max_len as i32); | |
| } | |
| // Assemble struct columns from builders. | |
| let struct_columns: Vec<ArrayRef> = builders | |
| .into_iter() | |
| .zip(element_types.iter()) | |
| .map(|(builder, elem_type)| match builder { | |
| Some(b) => arrow::array::make_array(b.freeze()), | |
| None => new_null_array( | |
| if elem_type.is_null() { | |
| &Null | |
| } else { | |
| elem_type | |
| }, | |
| total_values, | |
| ), | |
| }) | |
| .collect(); | |
| let struct_array = StructArray::try_new(struct_fields, struct_columns, None)?; | |
| let null_buffer = if null_mask.iter().any(|&v| v) { | |
| Some(NullBuffer::from( | |
| null_mask.iter().map(|v| !v).collect::<Vec<bool>>(), | |
| )) | |
| } else { | |
| None | |
| }; | |
| let result = ListArray::try_new( | |
| Arc::new(Field::new_list_field( | |
| struct_array.data_type().clone(), | |
| false, | |
| )), | |
| OffsetBuffer::new(offsets.into()), | |
| Arc::new(struct_array), | |
| null_buffer, | |
| )?; | |
| Ok(Arc::new(result)) | |
| } |