Skip to main content

datafusion_functions_nested/
utils.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! array function utils
19
20use std::sync::Arc;
21
22use arrow::datatypes::{DataType, Field, FieldRef, Fields};
23
24use arrow::array::{
25    Array, ArrayRef, BooleanArray, Float64Array, GenericListArray, NullBufferBuilder,
26    OffsetSizeTrait, Scalar,
27};
28use arrow::buffer::{NullBuffer, OffsetBuffer};
29use datafusion_common::cast::{
30    as_fixed_size_list_array, as_float64_array, as_generic_list_array,
31    as_large_list_array, as_large_list_view_array, as_list_array, as_list_view_array,
32};
33use datafusion_common::{Result, ScalarValue, exec_err, internal_err, plan_err};
34
35use datafusion_expr::ColumnarValue;
36use itertools::Itertools as _;
37
38/// Computes the return type of a function that produces a list with the same
39/// inner field as `array_type`, plus an element that may be null when
40/// `element_nullable` is set.
41///
42/// The inner field is carried over from `array_type` verbatim — name, metadata
43/// and all — so that the type promised at planning time is the one the kernel
44/// can actually build. Its nullability is widened when `element_nullable` is
45/// set, because a nullable new element may introduce nulls into a list whose
46/// elements were previously declared non-nullable.
47///
48/// Types other than `List`/`LargeList` are returned unchanged; callers handle
49/// `Null` themselves and the kernels reject anything else at execution time.
50pub(crate) fn list_type_with_element(
51    array_type: &DataType,
52    element_nullable: bool,
53) -> DataType {
54    match array_type {
55        DataType::List(field) => {
56            DataType::List(widen_nullability(field, element_nullable))
57        }
58        DataType::LargeList(field) => {
59            DataType::LargeList(widen_nullability(field, element_nullable))
60        }
61        other => other.clone(),
62    }
63}
64
65fn widen_nullability(field: &FieldRef, nullable: bool) -> FieldRef {
66    if nullable && !field.is_nullable() {
67        Arc::new(field.as_ref().clone().with_nullable(true))
68    } else {
69        Arc::clone(field)
70    }
71}
72
73/// Extracts the inner field of a `List`/`LargeList` type, so that a kernel can
74/// build a list array carrying exactly that field.
75///
76/// Used both on an input's type and on the type promised by
77/// [`ScalarUDFImpl::return_field_from_args`]. Anything else is a bug in the
78/// caller's dispatch, hence the internal error; `context` names the kernel so
79/// that error identifies where the bad dispatch happened.
80///
81/// [`ScalarUDFImpl::return_field_from_args`]: datafusion_expr::ScalarUDFImpl::return_field_from_args
82pub(crate) fn list_inner_field(context: &str, data_type: &DataType) -> Result<FieldRef> {
83    match data_type {
84        DataType::List(field) | DataType::LargeList(field) => Ok(Arc::clone(field)),
85        other => internal_err!("{context} got unexpected data type: {other}"),
86    }
87}
88
89pub(crate) fn check_datatypes(name: &str, args: &[&ArrayRef]) -> Result<()> {
90    let data_type = args[0].data_type();
91    if !args.iter().all(|arg| {
92        arg.data_type().equals_datatype(data_type)
93            || arg.data_type().equals_datatype(&DataType::Null)
94    }) {
95        let types = args.iter().map(|arg| arg.data_type()).collect::<Vec<_>>();
96        return plan_err!(
97            "{name} received incompatible types: {}",
98            types.iter().join(", ")
99        );
100    }
101
102    Ok(())
103}
104
105/// array function wrapper that differentiates between scalar (length 1) and array.
106pub(crate) fn make_scalar_function<F>(
107    inner: F,
108) -> impl Fn(&[ColumnarValue]) -> Result<ColumnarValue>
109where
110    F: Fn(&[ArrayRef]) -> Result<ArrayRef>,
111{
112    move |args: &[ColumnarValue]| {
113        // first, identify if any of the arguments is an Array. If yes, store its `len`,
114        // as any scalar will need to be converted to an array of len `len`.
115        let len = args
116            .iter()
117            .fold(Option::<usize>::None, |acc, arg| match arg {
118                ColumnarValue::Scalar(_) => acc,
119                ColumnarValue::Array(a) => Some(a.len()),
120            });
121
122        let is_scalar = len.is_none();
123
124        let args = ColumnarValue::values_to_arrays(args)?;
125
126        let result = (inner)(&args);
127
128        if is_scalar {
129            // If all inputs are scalar, keeps output as scalar
130            let result = result.and_then(|arr| ScalarValue::try_from_array(&arr, 0));
131            result.map(ColumnarValue::Scalar)
132        } else {
133            result.map(ColumnarValue::Array)
134        }
135    }
136}
137
138pub(crate) fn align_array_dimensions<O: OffsetSizeTrait>(
139    args: Vec<ArrayRef>,
140) -> Result<Vec<ArrayRef>> {
141    let args_ndim = args
142        .iter()
143        .map(|arg| datafusion_common::utils::list_ndims(arg.data_type()))
144        .collect::<Vec<_>>();
145    let max_ndim = args_ndim.iter().max().unwrap_or(&0);
146
147    // Align the dimensions of the arrays
148    let aligned_args: Result<Vec<ArrayRef>> = args
149        .into_iter()
150        .zip(args_ndim.iter())
151        .map(|(array, ndim)| {
152            if ndim < max_ndim {
153                let mut aligned_array = Arc::clone(&array);
154                for _ in 0..(max_ndim - ndim) {
155                    let data_type = aligned_array.data_type().to_owned();
156                    let array_lengths = vec![1; aligned_array.len()];
157                    let offsets = OffsetBuffer::<O>::from_lengths(array_lengths);
158
159                    aligned_array = Arc::new(GenericListArray::<O>::try_new(
160                        Arc::new(Field::new_list_field(data_type, true)),
161                        offsets,
162                        aligned_array,
163                        None,
164                    )?)
165                }
166                Ok(aligned_array)
167            } else {
168                Ok(Arc::clone(&array))
169            }
170        })
171        .collect();
172
173    aligned_args
174}
175
176/// Computes a BooleanArray indicating equality or inequality between elements in a list array and a specified element array.
177///
178/// # Arguments
179///
180/// * `list_array_row` - A reference to a trait object implementing the Arrow `Array` trait. It represents the list array for which the equality or inequality will be compared.
181///
182/// * `element_array` - A reference to a trait object implementing the Arrow `Array` trait. It represents the array with which each element in the `list_array_row` will be compared.
183///
184/// * `row_index` - The index of the row in the `element_array` and `list_array` to use for the comparison.
185///
186/// * `eq` - A boolean flag. If `true`, the function computes equality; if `false`, it computes inequality.
187///
188/// # Returns
189///
190/// Returns a `Result<BooleanArray>` representing the comparison results. The result may contain an error if there are issues with the computation.
191///
192/// # Example
193///
194/// ```text
195/// compare_element_to_list(
196///     [1, 2, 3], [1, 2, 3], 0, true => [true, false, false]
197///     [1, 2, 3, 3, 2, 1], [1, 2, 3], 1, true => [false, true, false, false, true, false]
198///
199///     [[1, 2, 3], [2, 3, 4], [3, 4, 5]], [[1, 2, 3], [2, 3, 4], [3, 4, 5]], 0, true => [true, false, false]
200///     [[1, 2, 3], [2, 3, 4], [2, 3, 4]], [[1, 2, 3], [2, 3, 4], [3, 4, 5]], 1, false => [true, false, false]
201/// )
202/// ```
203pub(crate) fn compare_element_to_list(
204    list_array_row: &dyn Array,
205    element_array: &dyn Array,
206    row_index: usize,
207    eq: bool,
208) -> Result<BooleanArray> {
209    if list_array_row.data_type() != element_array.data_type() {
210        return exec_err!(
211            "compare_element_to_list received incompatible types: '{:?}' and '{:?}'.",
212            list_array_row.data_type(),
213            element_array.data_type()
214        );
215    }
216
217    let element_array_row = element_array.slice(row_index, 1);
218
219    // Compute all positions in list_row_array (that is itself an
220    // array) that are equal to `from_array_row`
221    let res = match element_array_row.data_type() {
222        // arrow_ord::cmp::eq does not support ListArray, so we need to compare it by loop
223        DataType::List(_) => {
224            // compare each element of the from array
225            let element_array_row_inner = as_list_array(&element_array_row)?.value(0);
226            let list_array_row_inner = as_list_array(list_array_row)?;
227
228            list_array_row_inner
229                .iter()
230                // compare element by element the current row of list_array
231                .map(|row| {
232                    row.map(|row| {
233                        if eq {
234                            row.eq(&element_array_row_inner)
235                        } else {
236                            row.ne(&element_array_row_inner)
237                        }
238                    })
239                })
240                .collect::<BooleanArray>()
241        }
242        DataType::LargeList(_) => {
243            // compare each element of the from array
244            let element_array_row_inner =
245                as_large_list_array(&element_array_row)?.value(0);
246            let list_array_row_inner = as_large_list_array(list_array_row)?;
247
248            list_array_row_inner
249                .iter()
250                // compare element by element the current row of list_array
251                .map(|row| {
252                    row.map(|row| {
253                        if eq {
254                            row.eq(&element_array_row_inner)
255                        } else {
256                            row.ne(&element_array_row_inner)
257                        }
258                    })
259                })
260                .collect::<BooleanArray>()
261        }
262        _ => {
263            let element_arr = Scalar::new(element_array_row);
264            // use not_distinct so we can compare NULL
265            if eq {
266                arrow_ord::cmp::not_distinct(&list_array_row, &element_arr)?
267            } else {
268                arrow_ord::cmp::distinct(&list_array_row, &element_arr)?
269            }
270        }
271    };
272
273    Ok(res)
274}
275
276/// Returns the length of each array dimension
277pub(crate) fn compute_array_dims(
278    arr: Option<ArrayRef>,
279) -> Result<Option<Vec<Option<u64>>>> {
280    let mut value = match arr {
281        Some(arr) => arr,
282        None => return Ok(None),
283    };
284    if value.is_empty() {
285        return Ok(None);
286    }
287    let mut res = vec![Some(value.len() as u64)];
288
289    loop {
290        match value.data_type() {
291            DataType::List(_) => {
292                value = as_list_array(&value)?.value(0);
293                res.push(Some(value.len() as u64));
294            }
295            DataType::LargeList(_) => {
296                value = as_large_list_array(&value)?.value(0);
297                res.push(Some(value.len() as u64));
298            }
299            DataType::ListView(_) => {
300                value = as_list_view_array(&value)?.value(0);
301                res.push(Some(value.len() as u64));
302            }
303            DataType::LargeListView(_) => {
304                value = as_large_list_view_array(&value)?.value(0);
305                res.push(Some(value.len() as u64));
306            }
307            DataType::FixedSizeList(..) => {
308                value = as_fixed_size_list_array(&value)?.value(0);
309                res.push(Some(value.len() as u64));
310            }
311            _ => return Ok(Some(res)),
312        }
313    }
314}
315
316pub(crate) fn get_map_entry_field(data_type: &DataType) -> Result<&Fields> {
317    match data_type {
318        DataType::Map(field, _) => {
319            let field_data_type = field.data_type();
320            match field_data_type {
321                DataType::Struct(fields) => Ok(fields),
322                _ => {
323                    internal_err!("Expected a Struct type, got {}", field_data_type)
324                }
325            }
326        }
327        _ => internal_err!("Expected a Map type, got {data_type}"),
328    }
329}
330
331/// Shared `coerce_types` impl for array-math UDFs whose kernels expect
332/// `List<Float64>` / `LargeList<Float64>` (e.g. `array_add`, `cosine_distance`,
333/// `inner_product`, `array_normalize`).
334///
335/// Each input must be `Null`, `List`, `LargeList`, or `FixedSizeList`; otherwise
336/// returns a plan error naming `name`. `FixedSizeList` is widened to `List`,
337/// `Null` is coerced to a list of `Float64`, and if any input is `LargeList`
338/// the rest are widened to `LargeList` so the runtime sees a homogeneous pair.
339pub(crate) fn coerce_array_math_arg_types(
340    name: &str,
341    arg_types: &[DataType],
342) -> Result<Vec<DataType>> {
343    use DataType::{FixedSizeList, LargeList, List, Null};
344    use datafusion_common::utils::{ListCoercion, coerced_type_with_base_type_only};
345
346    let coercion = Some(&ListCoercion::FixedSizedListToList);
347
348    for arg_type in arg_types {
349        if !matches!(arg_type, Null | List(_) | LargeList(_) | FixedSizeList(..)) {
350            return plan_err!("{name} does not support type {arg_type}");
351        }
352    }
353
354    // If any input is `LargeList`, both sides must be widened to `LargeList`
355    // so the runtime dispatch in `inner_product_inner` sees a homogeneous
356    // pair. Follows the pattern in `ArrayConcat::coerce_types`.
357    let any_large_list = arg_types.iter().any(|t| matches!(t, LargeList(_)));
358
359    let coerced = arg_types
360        .iter()
361        .map(|arg_type| {
362            if matches!(arg_type, Null) {
363                let field = Arc::new(Field::new_list_field(DataType::Float64, true));
364                return if any_large_list {
365                    LargeList(field)
366                } else {
367                    List(field)
368                };
369            }
370            let coerced =
371                coerced_type_with_base_type_only(arg_type, &DataType::Float64, coercion);
372            match coerced {
373                List(field) if any_large_list => LargeList(field),
374                other => other,
375            }
376        })
377        .collect();
378
379    Ok(coerced)
380}
381
382/// Element-wise binary operation kernel for two `Float64` lists of equal per-row
383/// length. The caller is responsible for type-dispatching on `O` (`i32` for
384/// `List`, `i64` for `LargeList`).
385///
386/// Semantics:
387/// - whole-row NULL on either side → NULL output row, length 0
388/// - per-element NULL on either side → NULL at that output position
389/// - per-row length mismatch → exec error tagged with `op_name`
390///
391/// `op_name` flows into the error message; `op` is the per-element scalar op
392/// (e.g. `|a, b| a + b` for `array_add`, `|a, b| a - b` for `array_subtract`).
393pub(crate) fn array_math_binary_op<O, F>(
394    op_name: &str,
395    lhs: &ArrayRef,
396    rhs: &ArrayRef,
397    op: F,
398) -> Result<ArrayRef>
399where
400    O: OffsetSizeTrait,
401    F: Fn(f64, f64) -> f64,
402{
403    let lhs = as_generic_list_array::<O>(lhs)?;
404    let rhs = as_generic_list_array::<O>(rhs)?;
405
406    let lhs_values = as_float64_array(lhs.values())?;
407    let rhs_values = as_float64_array(rhs.values())?;
408    let lhs_offsets = lhs.value_offsets();
409    let rhs_offsets = rhs.value_offsets();
410
411    let row_nulls = NullBuffer::union(lhs.nulls(), rhs.nulls());
412
413    let mut out_values: Vec<f64> = Vec::with_capacity(lhs_values.len());
414    let mut out_inner_nulls = NullBufferBuilder::new(lhs_values.len());
415    let mut out_offsets = Vec::<O>::with_capacity(lhs.len() + 1);
416    out_offsets.push(O::zero());
417
418    for row in 0..lhs.len() {
419        if row_nulls.as_ref().is_some_and(|nb| nb.is_null(row)) {
420            out_offsets.push(out_offsets[row]);
421            continue;
422        }
423
424        let start1 = lhs_offsets[row].as_usize();
425        let len1 = lhs.value_length(row).as_usize();
426        let start2 = rhs_offsets[row].as_usize();
427        let len2 = rhs.value_length(row).as_usize();
428
429        if len1 != len2 {
430            return exec_err!(
431                "{op_name} requires both list inputs to have the same length per row, got {len1} and {len2} at row {row}"
432            );
433        }
434
435        let l_slice = lhs_values.slice(start1, len1);
436        let r_slice = rhs_values.slice(start2, len2);
437
438        let l_vals = l_slice.values();
439        let r_vals = r_slice.values();
440
441        for i in 0..len1 {
442            out_values.push(op(l_vals[i], r_vals[i]));
443        }
444
445        match NullBuffer::union(l_slice.nulls(), r_slice.nulls()) {
446            Some(nb) => out_inner_nulls.append_buffer(&nb),
447            None => out_inner_nulls.append_n_non_nulls(len1),
448        }
449
450        out_offsets.push(out_offsets[row] + O::usize_as(len1));
451    }
452
453    let values_array = Arc::new(Float64Array::new(
454        out_values.into(),
455        out_inner_nulls.finish(),
456    ));
457    let field = Arc::new(Field::new_list_field(DataType::Float64, true));
458
459    Ok(Arc::new(GenericListArray::<O>::try_new(
460        field,
461        OffsetBuffer::new(out_offsets.into()),
462        values_array,
463        row_nulls,
464    )?))
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use arrow::array::ListArray;
471    use arrow::datatypes::Int64Type;
472    use datafusion_common::utils::SingleRowListArrayBuilder;
473
474    /// Only test internal functions, array-related sql functions will be tested in sqllogictest `array.slt`
475    #[test]
476    fn test_align_array_dimensions() {
477        let array1d_1: ArrayRef =
478            Arc::new(ListArray::from_iter_primitive::<Int64Type, _, _>(vec![
479                Some(vec![Some(1), Some(2), Some(3)]),
480                Some(vec![Some(4), Some(5)]),
481            ]));
482        let array1d_2: ArrayRef =
483            Arc::new(ListArray::from_iter_primitive::<Int64Type, _, _>(vec![
484                Some(vec![Some(6), Some(7), Some(8)]),
485            ]));
486
487        let array2d_1: ArrayRef = Arc::new(
488            SingleRowListArrayBuilder::new(Arc::clone(&array1d_1)).build_list_array(),
489        );
490        let array2d_2 = Arc::new(
491            SingleRowListArrayBuilder::new(Arc::clone(&array1d_2)).build_list_array(),
492        );
493
494        let res = align_array_dimensions::<i32>(vec![
495            array1d_1.to_owned(),
496            array2d_2.to_owned(),
497        ])
498        .unwrap();
499
500        let expected = as_list_array(&array2d_1).unwrap();
501        let expected_dim = datafusion_common::utils::list_ndims(array2d_1.data_type());
502        assert_ne!(as_list_array(&res[0]).unwrap(), expected);
503        assert_eq!(
504            datafusion_common::utils::list_ndims(res[0].data_type()),
505            expected_dim
506        );
507
508        let array3d_1: ArrayRef =
509            Arc::new(SingleRowListArrayBuilder::new(array2d_1).build_list_array());
510        let array3d_2: ArrayRef =
511            Arc::new(SingleRowListArrayBuilder::new(array2d_2).build_list_array());
512        let res = align_array_dimensions::<i32>(vec![array1d_1, array3d_2]).unwrap();
513
514        let expected = as_list_array(&array3d_1).unwrap();
515        let expected_dim = datafusion_common::utils::list_ndims(array3d_1.data_type());
516        assert_ne!(as_list_array(&res[0]).unwrap(), expected);
517        assert_eq!(
518            datafusion_common::utils::list_ndims(res[0].data_type()),
519            expected_dim
520        );
521    }
522}