Skip to main content

datafusion_functions_nested/
arrays_zip.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//! [`ScalarUDFImpl`] definitions for arrays_zip function.
19
20use crate::utils::make_scalar_function;
21use arrow::array::{
22    Array, ArrayRef, Capacities, ListArray, MutableArrayData, NullBufferBuilder,
23    StructArray, new_null_array,
24};
25use arrow::buffer::{NullBuffer, OffsetBuffer};
26use arrow::datatypes::DataType::{FixedSizeList, LargeList, List, Null};
27use arrow::datatypes::{DataType, Field, Fields};
28use datafusion_common::cast::{
29    as_fixed_size_list_array, as_large_list_array, as_list_array,
30};
31use datafusion_common::{Result, exec_err};
32use datafusion_expr::{
33    ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
34    Volatility,
35};
36use datafusion_macros::user_doc;
37use std::sync::Arc;
38
39/// Type-erased view of a list column (works for both List and LargeList).
40/// Stores the information needed to iterate rows without re-downcasting.
41struct ListColumnView {
42    /// The flat values array backing this list column.
43    values: ArrayRef,
44    /// Pre-computed per-row start offsets (length = num_rows + 1).
45    offsets: Vec<usize>,
46    /// Null bitmap from the input array (None means no nulls).
47    nulls: Option<NullBuffer>,
48}
49
50impl ListColumnView {
51    fn is_null(&self, idx: usize) -> bool {
52        self.nulls.as_ref().is_some_and(|n| n.is_null(idx))
53    }
54}
55
56make_udf_expr_and_func!(
57    ArraysZip,
58    arrays_zip,
59    "combines one or multiple arrays into a single array of structs.",
60    arrays_zip_udf
61);
62
63#[user_doc(
64    doc_section(label = "Array Functions"),
65    description = "Returns an array of structs created by combining the elements of each input array at the same index. If the arrays have different lengths, shorter arrays are padded with NULLs.",
66    syntax_example = "arrays_zip(array1[, ..., array_n])",
67    sql_example = r#"```sql
68> select arrays_zip([1, 2, 3]);
69+---------------------------------------------------+
70| arrays_zip([1, 2, 3])                             |
71+---------------------------------------------------+
72| [{1: 1}, {1: 2}, {1: 3}]                          |
73+---------------------------------------------------+
74> select arrays_zip([1, 2], [3, 4, 5]);
75+---------------------------------------------------+
76| arrays_zip([1, 2], [3, 4, 5])                     |
77+---------------------------------------------------+
78| [{1: 1, 2: 3}, {1: 2, 2: 4}, {1: NULL, 2: 5}]     |
79+---------------------------------------------------+
80```"#,
81    argument(name = "array1", description = "First array expression."),
82    argument(
83        name = "array_n",
84        description = "Optional additional array expressions."
85    )
86)]
87#[derive(Debug, PartialEq, Eq, Hash)]
88pub struct ArraysZip {
89    signature: Signature,
90    aliases: Vec<String>,
91}
92
93impl Default for ArraysZip {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl ArraysZip {
100    pub fn new() -> Self {
101        Self {
102            signature: Signature::variadic_any(Volatility::Immutable),
103            aliases: vec![String::from("list_zip")],
104        }
105    }
106}
107
108impl ScalarUDFImpl for ArraysZip {
109    fn name(&self) -> &str {
110        "arrays_zip"
111    }
112
113    fn signature(&self) -> &Signature {
114        &self.signature
115    }
116
117    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
118        if arg_types.is_empty() {
119            return exec_err!("arrays_zip requires at least one argument");
120        }
121
122        let mut fields = Vec::with_capacity(arg_types.len());
123        for (i, arg_type) in arg_types.iter().enumerate() {
124            let element_type = match arg_type {
125                List(field) | LargeList(field) | FixedSizeList(field, _) => {
126                    field.data_type().clone()
127                }
128                Null => Null,
129                dt => {
130                    return exec_err!("arrays_zip expects array arguments, got {dt}");
131                }
132            };
133            fields.push(Field::new(arrays_zip_field_name(i), element_type, true));
134        }
135
136        Ok(List(Arc::new(Field::new_list_field(
137            DataType::Struct(Fields::from(fields)),
138            true,
139        ))))
140    }
141
142    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
143        make_scalar_function(arrays_zip_inner)(&args.args)
144    }
145
146    fn aliases(&self) -> &[String] {
147        &self.aliases
148    }
149
150    fn documentation(&self) -> Option<&Documentation> {
151        self.doc()
152    }
153}
154
155/// Core implementation for arrays_zip.
156///
157/// Takes N list arrays and produces a list of structs where each struct
158/// has one field per input array. If arrays within a row have different
159/// lengths, shorter arrays are padded with NULLs.
160/// Supports List, LargeList, and Null input types.
161fn arrays_zip_inner(args: &[ArrayRef]) -> Result<ArrayRef> {
162    if args.is_empty() {
163        return exec_err!("arrays_zip requires at least one argument");
164    }
165
166    let field_names = arrays_zip_field_names(args.len());
167    let num_rows = args[0].len();
168
169    if let Some(result) = try_perfect_list_zip(args, &field_names)? {
170        return Ok(result);
171    }
172
173    // Build a type-erased ListColumnView for each argument.
174    // None means the argument is Null-typed (all nulls, no backing data).
175    let mut views: Vec<Option<ListColumnView>> = Vec::with_capacity(args.len());
176    let mut element_types: Vec<DataType> = Vec::with_capacity(args.len());
177
178    for (i, arg) in args.iter().enumerate() {
179        match arg.data_type() {
180            List(field) => {
181                let arr = as_list_array(arg)?;
182                let raw_offsets = arr.value_offsets();
183                let offsets: Vec<usize> =
184                    raw_offsets.iter().map(|&o| o as usize).collect();
185                element_types.push(field.data_type().clone());
186                views.push(Some(ListColumnView {
187                    values: Arc::clone(arr.values()),
188                    offsets,
189                    nulls: arr.nulls().cloned(),
190                }));
191            }
192            LargeList(field) => {
193                let arr = as_large_list_array(arg)?;
194                let raw_offsets = arr.value_offsets();
195                let offsets: Vec<usize> =
196                    raw_offsets.iter().map(|&o| o as usize).collect();
197                element_types.push(field.data_type().clone());
198                views.push(Some(ListColumnView {
199                    values: Arc::clone(arr.values()),
200                    offsets,
201                    nulls: arr.nulls().cloned(),
202                }));
203            }
204            FixedSizeList(field, size) => {
205                let arr = as_fixed_size_list_array(arg)?;
206                let size = *size as usize;
207                let offsets: Vec<usize> = (0..=num_rows).map(|row| row * size).collect();
208                element_types.push(field.data_type().clone());
209                views.push(Some(ListColumnView {
210                    values: Arc::clone(arr.values()),
211                    offsets,
212                    nulls: arr.nulls().cloned(),
213                }));
214            }
215            Null => {
216                element_types.push(Null);
217                views.push(None);
218            }
219            dt => {
220                return exec_err!("arrays_zip argument {i} expected list type, got {dt}");
221            }
222        }
223    }
224
225    // Collect per-column values data for MutableArrayData builders.
226    let values_data: Vec<_> = views
227        .iter()
228        .map(|v| v.as_ref().map(|view| view.values.to_data()))
229        .collect();
230
231    let struct_fields: Fields = element_types
232        .iter()
233        .zip(field_names.iter())
234        .map(|(dt, name)| Field::new(name.clone(), dt.clone(), true))
235        .collect::<Vec<_>>()
236        .into();
237
238    // Create a MutableArrayData builder per column. For None (Null-typed)
239    // args we only need extend_nulls, so we track them separately.
240    let mut builders: Vec<Option<MutableArrayData>> = values_data
241        .iter()
242        .map(|vd| {
243            vd.as_ref().map(|data| {
244                MutableArrayData::with_capacities(vec![data], true, Capacities::Array(0))
245            })
246        })
247        .collect();
248
249    let mut offsets: Vec<i32> = Vec::with_capacity(num_rows + 1);
250    offsets.push(0);
251    let mut null_builder = NullBufferBuilder::new(num_rows);
252    let mut total_values: usize = 0;
253
254    // Process each row: compute per-array lengths, then copy values
255    // and pad shorter arrays with NULLs.
256    for row_idx in 0..num_rows {
257        let mut max_len: usize = 0;
258        let mut all_null = true;
259
260        for view in views.iter().flatten() {
261            if !view.is_null(row_idx) {
262                all_null = false;
263                let len = view.offsets[row_idx + 1] - view.offsets[row_idx];
264                max_len = max_len.max(len);
265            }
266        }
267
268        if all_null {
269            null_builder.append_null();
270            offsets.push(*offsets.last().unwrap());
271            continue;
272        }
273        null_builder.append_non_null();
274
275        // Extend each column builder for this row.
276        for (col_idx, view) in views.iter().enumerate() {
277            match view {
278                Some(v) if !v.is_null(row_idx) => {
279                    let start = v.offsets[row_idx];
280                    let end = v.offsets[row_idx + 1];
281                    let len = end - start;
282                    let builder = builders[col_idx].as_mut().unwrap();
283                    builder.try_extend(0, start, end)?;
284                    if len < max_len {
285                        builder.try_extend_nulls(max_len - len)?;
286                    }
287                }
288                _ => {
289                    // Null list entry or None (Null-typed) arg — all nulls.
290                    if let Some(builder) = builders[col_idx].as_mut() {
291                        builder.try_extend_nulls(max_len)?;
292                    }
293                }
294            }
295        }
296
297        total_values += max_len;
298        let last = *offsets.last().unwrap();
299        offsets.push(last + max_len as i32);
300    }
301
302    // Assemble struct columns from builders.
303    let struct_columns: Vec<ArrayRef> = builders
304        .into_iter()
305        .zip(element_types.iter())
306        .map(|(builder, elem_type)| match builder {
307            Some(b) => arrow::array::make_array(b.freeze()),
308            None => new_null_array(
309                if elem_type.is_null() {
310                    &Null
311                } else {
312                    elem_type
313                },
314                total_values,
315            ),
316        })
317        .collect();
318
319    let struct_array = StructArray::try_new(struct_fields, struct_columns, None)?;
320
321    let null_buffer = null_builder.finish();
322
323    let result = ListArray::try_new(
324        Arc::new(Field::new_list_field(
325            struct_array.data_type().clone(),
326            true,
327        )),
328        OffsetBuffer::new(offsets.into()),
329        Arc::new(struct_array),
330        null_buffer,
331    )?;
332
333    Ok(Arc::new(result))
334}
335
336fn arrays_zip_field_name(index: usize) -> String {
337    (index + 1).to_string()
338}
339
340fn arrays_zip_field_names(len: usize) -> Vec<String> {
341    (0..len).map(arrays_zip_field_name).collect()
342}
343
344/// Fast path for regular List inputs whose existing buffers already match the
345/// zipped output: all offsets and values lengths match, and null rows cover no
346/// values. This lets us reuse offsets and child values instead of rebuilding.
347fn try_perfect_list_zip(
348    args: &[ArrayRef],
349    field_names: &[String],
350) -> Result<Option<ArrayRef>> {
351    debug_assert_eq!(args.len(), field_names.len());
352
353    let mut list_arrays = Vec::with_capacity(args.len());
354    let mut struct_fields = Vec::with_capacity(args.len());
355
356    for (arg, field_name) in args.iter().zip(field_names) {
357        let arr = match arg.data_type() {
358            List(field) => {
359                struct_fields.push(Field::new(
360                    field_name.clone(),
361                    field.data_type().clone(),
362                    true,
363                ));
364                as_list_array(arg)?
365            }
366            _ => return Ok(None),
367        };
368
369        list_arrays.push(arr);
370    }
371
372    let first = list_arrays[0];
373    let num_rows = first.len();
374    let offsets = first.offsets().clone();
375    let values_len = first.values().len();
376
377    // Reusing the child arrays is only valid when every list uses the exact
378    // same row boundaries and exposes the same total number of child values.
379    for arr in &list_arrays {
380        if arr.values().len() != values_len || arr.offsets() != &offsets {
381            return Ok(None);
382        }
383    }
384
385    let nulls = if list_arrays.iter().any(|arr| arr.null_count() != 0) {
386        let first_nulls = first.nulls();
387        if list_arrays.iter().all(|arr| arr.nulls() == first_nulls) {
388            first_nulls.cloned()
389        } else {
390            // Match the general path: arrays_zip only marks an output row null
391            // when every concrete input list is null. Mixed null and non-null
392            // empty lists still produce a non-null empty list, but mixed null
393            // rows with values must fall back to preserve field-level nulls.
394            let mut null_builder = NullBufferBuilder::new(num_rows);
395            for row_idx in 0..num_rows {
396                let mut all_null = true;
397
398                for arr in &list_arrays {
399                    if arr.is_null(row_idx) {
400                        if arr.offsets()[row_idx + 1] != arr.offsets()[row_idx] {
401                            return Ok(None);
402                        }
403                    } else {
404                        all_null = false;
405                    }
406                }
407
408                if all_null {
409                    null_builder.append_null();
410                } else {
411                    null_builder.append_non_null();
412                }
413            }
414
415            null_builder.finish()
416        }
417    } else {
418        None
419    };
420
421    let struct_columns = list_arrays
422        .iter()
423        .map(|arr| Arc::clone(arr.values()))
424        .collect::<Vec<_>>();
425    let struct_array =
426        StructArray::try_new(Fields::from(struct_fields), struct_columns, None)?;
427    let result = ListArray::try_new(
428        Arc::new(Field::new_list_field(
429            struct_array.data_type().clone(),
430            true,
431        )),
432        offsets,
433        Arc::new(struct_array),
434        nulls,
435    )?;
436
437    Ok(Some(Arc::new(result)))
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use arrow::array::Int64Array;
444    use arrow::buffer::NullBuffer;
445
446    fn list(values: Vec<i64>, offsets: Vec<i32>) -> Arc<ListArray> {
447        list_with_validity(values, offsets, None)
448    }
449
450    fn list_with_validity(
451        values: Vec<i64>,
452        offsets: Vec<i32>,
453        valid: Option<Vec<bool>>,
454    ) -> Arc<ListArray> {
455        Arc::new(
456            ListArray::try_new(
457                Arc::new(Field::new_list_field(DataType::Int64, true)),
458                OffsetBuffer::new(offsets.into()),
459                Arc::new(Int64Array::from(values)),
460                valid.map(NullBuffer::from),
461            )
462            .unwrap(),
463        )
464    }
465
466    #[test]
467    fn perfect_zip_reuses_input_values_and_offsets() {
468        let left = list(vec![1, 2, 3, 4, 5, 6], vec![0, 2, 3, 6]);
469        let right = list(vec![10, 20, 30, 40, 50, 60], vec![0, 2, 3, 6]);
470
471        let result = arrays_zip_inner(&[
472            Arc::clone(&left) as ArrayRef,
473            Arc::clone(&right) as ArrayRef,
474        ])
475        .unwrap();
476        let result = result.as_any().downcast_ref::<ListArray>().unwrap();
477        let values = result
478            .values()
479            .as_any()
480            .downcast_ref::<StructArray>()
481            .unwrap();
482
483        assert!(result.offsets().ptr_eq(left.offsets()));
484        assert!(Arc::ptr_eq(values.column(0), left.values()));
485        assert!(Arc::ptr_eq(values.column(1), right.values()));
486    }
487
488    #[test]
489    fn perfect_zip_uses_supplied_field_names() {
490        let left = list(vec![1, 2, 3], vec![0, 1, 3]);
491        let right = list(vec![10, 20, 30], vec![0, 1, 3]);
492        let field_names = vec!["left".to_string(), "right".to_string()];
493
494        let result = try_perfect_list_zip(
495            &[
496                Arc::clone(&left) as ArrayRef,
497                Arc::clone(&right) as ArrayRef,
498            ],
499            &field_names,
500        )
501        .unwrap()
502        .unwrap();
503        let result = result.as_any().downcast_ref::<ListArray>().unwrap();
504        let values = result
505            .values()
506            .as_any()
507            .downcast_ref::<StructArray>()
508            .unwrap();
509        let names = values
510            .fields()
511            .iter()
512            .map(|field| field.name().as_str())
513            .collect::<Vec<_>>();
514
515        assert_eq!(names, vec!["left", "right"]);
516    }
517
518    #[test]
519    fn perfect_zip_reuses_zero_length_null_rows() {
520        let left = list_with_validity(
521            vec![1, 2, 3, 4],
522            vec![0, 2, 2, 4],
523            Some(vec![true, false, true]),
524        );
525        let right = list_with_validity(
526            vec![10, 20, 30, 40],
527            vec![0, 2, 2, 4],
528            Some(vec![true, false, true]),
529        );
530
531        let result = arrays_zip_inner(&[
532            Arc::clone(&left) as ArrayRef,
533            Arc::clone(&right) as ArrayRef,
534        ])
535        .unwrap();
536        let result = result.as_any().downcast_ref::<ListArray>().unwrap();
537
538        assert!(result.offsets().ptr_eq(left.offsets()));
539        assert!(result.is_null(1));
540    }
541
542    #[test]
543    fn perfect_zip_preserves_mixed_null_empty_rows() {
544        let left =
545            list_with_validity(vec![], vec![0, 0, 0, 0], Some(vec![false, true, false]));
546        let right =
547            list_with_validity(vec![], vec![0, 0, 0, 0], Some(vec![true, false, false]));
548
549        let result = arrays_zip_inner(&[
550            Arc::clone(&left) as ArrayRef,
551            Arc::clone(&right) as ArrayRef,
552        ])
553        .unwrap();
554        let result = result.as_any().downcast_ref::<ListArray>().unwrap();
555
556        assert!(result.offsets().ptr_eq(left.offsets()));
557        assert!(!result.is_null(0));
558        assert!(!result.is_null(1));
559        assert!(result.is_null(2));
560    }
561
562    #[test]
563    fn perfect_zip_reuses_null_rows_with_hidden_values() {
564        let left =
565            list_with_validity(vec![1, 2, 3, 4], vec![0, 2, 4], Some(vec![true, false]));
566        let right = list_with_validity(
567            vec![10, 20, 30, 40],
568            vec![0, 2, 4],
569            Some(vec![true, false]),
570        );
571
572        let result = arrays_zip_inner(&[
573            Arc::clone(&left) as ArrayRef,
574            Arc::clone(&right) as ArrayRef,
575        ])
576        .unwrap();
577        let result = result.as_any().downcast_ref::<ListArray>().unwrap();
578
579        assert!(result.offsets().ptr_eq(left.offsets()));
580        assert_eq!(result.value_offsets(), &[0, 2, 4]);
581        assert!(result.is_null(1));
582    }
583
584    #[test]
585    fn mixed_null_row_with_hidden_values_uses_general_path() {
586        let left =
587            list_with_validity(vec![1, 2, 3, 4], vec![0, 2, 4], Some(vec![true, false]));
588        let right = list_with_validity(
589            vec![10, 20, 30, 40],
590            vec![0, 2, 4],
591            Some(vec![true, true]),
592        );
593
594        let result = arrays_zip_inner(&[
595            Arc::clone(&left) as ArrayRef,
596            Arc::clone(&right) as ArrayRef,
597        ])
598        .unwrap();
599        let result = result.as_any().downcast_ref::<ListArray>().unwrap();
600        let values = result
601            .values()
602            .as_any()
603            .downcast_ref::<StructArray>()
604            .unwrap();
605
606        assert!(!result.offsets().ptr_eq(left.offsets()));
607        assert_eq!(result.value_offsets(), &[0, 2, 4]);
608        assert!(values.column(0).is_null(2));
609        assert!(values.column(0).is_null(3));
610        assert!(!values.column(1).is_null(2));
611        assert!(!values.column(1).is_null(3));
612    }
613}