blob: 6f0332d3d86207e42a57f2b4f796b1f54f1284de [file]
// 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.extend(0, start, end);
if len < max_len {
builder.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.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))
}