Skip to main content

vortex_array/arrays/list/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::sync::Arc;
7
8use num_traits::AsPrimitive;
9use vortex_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_error::vortex_bail;
12use vortex_error::vortex_ensure;
13use vortex_error::vortex_ensure_eq;
14use vortex_error::vortex_panic;
15
16use crate::ArrayRef;
17use crate::ArraySlots;
18use crate::Canonical;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::VortexSessionExecute;
22use crate::aggregate_fn::NumericalAggregateOpts;
23use crate::aggregate_fn::fns::min_max::min_max;
24use crate::array::Array;
25use crate::array::ArrayParts;
26use crate::array::TypedArrayRef;
27use crate::array::child_to_validity;
28use crate::array::validity_to_child;
29use crate::array_slots;
30use crate::arrays::ConstantArray;
31use crate::arrays::List;
32use crate::arrays::ListArray;
33use crate::arrays::Primitive;
34use crate::builtins::ArrayBuiltins;
35use crate::dtype::DType;
36use crate::dtype::NativePType;
37use crate::legacy_session;
38use crate::match_each_integer_ptype;
39use crate::match_each_native_ptype;
40use crate::scalar_fn::fns::operators::Operator;
41use crate::validity::Validity;
42
43#[array_slots(List)]
44pub struct ListSlots {
45    /// The elements data array containing all list elements concatenated together.
46    #[slot(0)]
47    pub elements: ArrayRef,
48    /// The offsets array defining the start/end of each list within the elements array.
49    #[slot(1)]
50    pub offsets: ArrayRef,
51    /// The validity bitmap indicating which list elements are non-null.
52    #[slot(2)]
53    pub validity: Option<ArrayRef>,
54}
55
56/// A list array that stores variable-length lists of elements, similar to `Vec<Vec<T>>`.
57///
58/// This mirrors the Apache Arrow List array encoding and provides efficient storage
59/// for nested data where each row contains a list of elements of the same type.
60///
61/// ## Data Layout
62///
63/// The list array uses an offset-based encoding:
64/// - **Elements array**: A flat array containing all list elements concatenated together
65/// - **Offsets array**: Integer array where `offsets[i]` is an (inclusive) start index into
66///   the **elements** and `offsets[i+1]` is the (exclusive) stop index for the `i`th list.
67/// - **Validity**: Optional mask indicating which lists are null
68///
69/// This allows for excellent cascading compression of the elements and offsets, as similar values
70/// are clustered together and the offsets have a predictable pattern and small deltas between
71/// consecutive elements.
72///
73/// ## Offset Semantics
74///
75/// - Offsets must be non-nullable integers (i32, i64, etc.)
76/// - Offsets array has length `n+1` where `n` is the number of lists
77/// - List `i` contains elements from `elements[offsets[i]..offsets[i+1]]`
78/// - Offsets must be monotonically increasing
79///
80/// # Examples
81///
82/// ```
83/// use vortex_array::arrays::{ListArray, PrimitiveArray};
84/// use vortex_array::arrays::list::ListArrayExt;
85/// use vortex_array::validity::Validity;
86/// use vortex_array::IntoArray;
87/// use vortex_buffer::buffer;
88/// use std::sync::Arc;
89///
90/// // Create a list array representing [[1, 2], [3, 4, 5], []]
91/// let elements = buffer![1i32, 2, 3, 4, 5].into_array();
92/// let offsets = buffer![0u32, 2, 5, 5].into_array(); // 3 lists
93///
94/// let list_array = ListArray::try_new(
95///     elements.into_array(),
96///     offsets.into_array(),
97///     Validity::NonNullable,
98/// ).unwrap();
99///
100/// assert_eq!(list_array.len(), 3);
101///
102/// // Access individual lists
103/// let first_list = list_array.list_elements_at(0).unwrap();
104/// assert_eq!(first_list.len(), 2); // [1, 2]
105///
106/// let third_list = list_array.list_elements_at(2).unwrap();
107/// assert!(third_list.is_empty()); // []
108/// ```
109#[derive(Clone, Debug, Default)]
110pub struct ListData;
111
112impl Display for ListData {
113    fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
114        Ok(())
115    }
116}
117
118pub struct ListDataParts {
119    pub elements: ArrayRef,
120    pub offsets: ArrayRef,
121    pub validity: Validity,
122    pub dtype: DType,
123}
124
125impl ListData {
126    pub(crate) fn make_slots(
127        elements: &ArrayRef,
128        offsets: &ArrayRef,
129        validity: &Validity,
130        len: usize,
131    ) -> ArraySlots {
132        ListSlots {
133            elements: elements.clone(),
134            offsets: offsets.clone(),
135            validity: validity_to_child(validity, len),
136        }
137        .into_slots()
138    }
139
140    /// Creates a new `ListArray`.
141    ///
142    /// # Panics
143    ///
144    /// Panics if the provided components do not satisfy the invariants documented
145    /// in `ListArray::new_unchecked`.
146    pub fn build(elements: ArrayRef, offsets: ArrayRef, validity: Validity) -> Self {
147        Self::try_build(elements, offsets, validity).vortex_expect("ListArray new")
148    }
149
150    /// Constructs a new `ListArray`.
151    ///
152    /// See `ListArray::new_unchecked` for more information.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if the provided components do not satisfy the invariants documented in
157    /// `ListArray::new_unchecked`.
158    pub(crate) fn try_build(
159        elements: ArrayRef,
160        offsets: ArrayRef,
161        validity: Validity,
162    ) -> VortexResult<Self> {
163        Self::validate(&elements, &offsets, &validity)?;
164
165        // SAFETY: validate ensures all invariants are met.
166        Ok(unsafe { Self::new_unchecked() })
167    }
168
169    /// Creates a new `ListArray` without validation from these components:
170    ///
171    /// * `elements` is a flat array containing all list elements concatenated.
172    /// * `offsets` is an integer array where `offsets[i]` is the start index for list `i`.
173    /// * `validity` holds the null values.
174    ///
175    /// # Safety
176    ///
177    /// The caller must ensure all of the following invariants are satisfied:
178    ///
179    /// - Offsets must be a non-nullable integer array.
180    /// - Offsets must have at least one element (even for empty lists, it should contain \[0\]).
181    /// - Offsets must be sorted (monotonically increasing).
182    /// - All offset values must be non-negative.
183    /// - The maximum offset must not exceed `elements.len()`.
184    /// - If validity is an array, its length must equal `offsets.len() - 1`.
185    pub unsafe fn new_unchecked() -> Self {
186        Self
187    }
188
189    /// Validates the components that would be used to create a `ListArray`.
190    ///
191    /// This function checks all the invariants required by `ListArray::new_unchecked`.
192    #[allow(clippy::disallowed_methods)]
193    pub fn validate(
194        elements: &ArrayRef,
195        offsets: &ArrayRef,
196        validity: &Validity,
197    ) -> VortexResult<()> {
198        // Offsets must have at least one element
199        vortex_ensure!(
200            !offsets.is_empty(),
201            InvalidArgument: "Offsets must have at least one element, [0] for an empty list"
202        );
203
204        // Offsets must be of integer type, and cannot go lower than 0.
205        vortex_ensure!(
206            offsets.dtype().is_int() && !offsets.dtype().is_nullable(),
207            InvalidArgument: "offsets have invalid type {}",
208            offsets.dtype()
209        );
210
211        // We can safely unwrap the DType as primitive now
212        let offsets_ptype = offsets.dtype().as_ptype();
213        let mut ctx = legacy_session().create_execution_ctx();
214
215        // Offsets must be sorted (but not strictly sorted, zero-length lists are allowed)
216        if let Some(is_sorted) = offsets.statistics().compute_is_sorted(&mut ctx) {
217            vortex_ensure!(is_sorted, InvalidArgument: "offsets must be sorted");
218        } else {
219            vortex_bail!(InvalidArgument: "offsets must report is_sorted statistic");
220        }
221
222        // Validate that offsets min is non-negative, and max does not exceed the length of
223        // the elements array.
224        if let Some(min_max) = min_max(offsets, &mut ctx, NumericalAggregateOpts::default())? {
225            match_each_integer_ptype!(offsets_ptype, |P| {
226                #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)]
227                {
228                    let max = min_max
229                        .max
230                        .as_primitive()
231                        .as_::<P>()
232                        .vortex_expect("offsets type must fit offsets values");
233                    let min = min_max
234                        .min
235                        .as_primitive()
236                        .as_::<P>()
237                        .vortex_expect("offsets type must fit offsets values");
238
239                    vortex_ensure!(
240                        min >= 0,
241                        InvalidArgument: "offsets minimum {min} outside valid range [0, {max}]"
242                    );
243
244                    vortex_ensure!(
245                        max <= P::try_from(elements.len()).unwrap_or_else(|_| vortex_panic!(
246                            "Offsets type {} must be able to fit elements length {}",
247                            <P as NativePType>::PTYPE,
248                            elements.len()
249                        )),
250                        InvalidArgument: "Max offset {max} is beyond the length of the elements array {}",
251                        elements.len()
252                    );
253                }
254            })
255        } else {
256            // TODO(aduffy): fallback to slower validation pathway?
257            vortex_bail!(
258                InvalidArgument: "offsets array with encoding {} must support min_max compute function",
259                offsets.encoding_id()
260            );
261        };
262
263        // If a validity array is present, it must be the same length as the ListArray
264        if let Some(validity_len) = validity.maybe_len() {
265            vortex_ensure_eq!(
266                validity_len,
267                offsets.len() - 1,
268                InvalidArgument: "validity length does not match array size",
269            );
270        }
271
272        Ok(())
273    }
274    // TODO(connor)[ListView]: Create 2 functions `reset_offsets` and `recursive_reset_offsets`,
275    // where `reset_offsets` is infallible.
276    // Also, `reset_offsets` can be made more efficient by replacing `sub_scalar` with a match on
277    // the offset type and manual subtraction and fast path where `offsets[0] == 0`.
278}
279
280pub trait ListArrayExt: ListArraySlotsExt {
281    fn nullability(&self) -> crate::dtype::Nullability {
282        match self.as_ref().dtype() {
283            DType::List(_, nullability) => *nullability,
284            _ => unreachable!("ListArrayExt requires a list dtype"),
285        }
286    }
287
288    fn list_validity(&self) -> Validity {
289        child_to_validity(
290            self.as_ref().slots()[ListSlots::VALIDITY].as_ref(),
291            self.nullability(),
292        )
293    }
294
295    #[allow(clippy::disallowed_methods)]
296    fn offset_at(&self, index: usize) -> VortexResult<usize> {
297        vortex_ensure!(
298            index <= self.as_ref().len(),
299            "Index {index} out of bounds 0..={}",
300            self.as_ref().len()
301        );
302
303        if let Some(p) = self.offsets().as_opt::<Primitive>() {
304            Ok(match_each_native_ptype!(p.ptype(), |P| {
305                p.as_slice::<P>()[index].as_()
306            }))
307        } else {
308            self.offsets()
309                .execute_scalar(index, &mut legacy_session().create_execution_ctx())?
310                .as_primitive()
311                .as_::<usize>()
312                .ok_or_else(|| vortex_error::vortex_err!("offset value does not fit in usize"))
313        }
314    }
315
316    fn list_elements_at(&self, index: usize) -> VortexResult<ArrayRef> {
317        let start = self.offset_at(index)?;
318        let end = self.offset_at(index + 1)?;
319        self.elements().slice(start..end)
320    }
321
322    fn sliced_elements(&self) -> VortexResult<ArrayRef> {
323        let start = self.offset_at(0)?;
324        let end = self.offset_at(self.as_ref().len())?;
325        self.elements().slice(start..end)
326    }
327
328    fn element_dtype(&self) -> &DType {
329        self.elements().dtype()
330    }
331
332    fn reset_offsets(&self, recurse: bool, ctx: &mut ExecutionCtx) -> VortexResult<Array<List>> {
333        let mut elements = self.sliced_elements()?;
334        if recurse && elements.is_canonical() {
335            let compacted = elements
336                .execute::<Canonical>(ctx)?
337                .compact(ctx)?
338                .into_array();
339            elements = compacted;
340        } else if recurse && let Some(child_list_array) = elements.as_opt::<List>() {
341            elements = child_list_array
342                .into_owned()
343                .reset_offsets(recurse, ctx)?
344                .into_array();
345        }
346
347        let offsets = self.offsets();
348        let first_offset = offsets.execute_scalar(0, ctx)?;
349        let adjusted_offsets = offsets.clone().binary(
350            ConstantArray::new(first_offset, offsets.len()).into_array(),
351            Operator::Sub,
352        )?;
353
354        // SAFETY: By resetting the offsets we simply "shift" everything left and discard trailing garbage, so all invariants remain the same.
355        Ok(unsafe { ListArray::new_unchecked(elements, adjusted_offsets, self.list_validity()) })
356    }
357}
358impl<T: TypedArrayRef<List>> ListArrayExt for T {}
359
360impl Array<List> {
361    /// Creates a new `ListArray`.
362    pub fn new(elements: ArrayRef, offsets: ArrayRef, validity: Validity) -> Self {
363        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
364        let len = offsets.len().saturating_sub(1);
365        let slots = ListData::make_slots(&elements, &offsets, &validity, len);
366        let data = ListData::build(elements, offsets, validity);
367        unsafe { Array::from_parts_unchecked(ArrayParts::new(List, dtype, len, data, slots)) }
368    }
369
370    /// Constructs a new `ListArray`.
371    pub fn try_new(
372        elements: ArrayRef,
373        offsets: ArrayRef,
374        validity: Validity,
375    ) -> VortexResult<Self> {
376        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
377        let len = offsets.len().saturating_sub(1);
378        let slots = ListData::make_slots(&elements, &offsets, &validity, len);
379        let data = ListData::try_build(elements, offsets, validity)?;
380        Ok(unsafe { Array::from_parts_unchecked(ArrayParts::new(List, dtype, len, data, slots)) })
381    }
382
383    /// Creates a new `ListArray` without validation.
384    ///
385    /// # Safety
386    ///
387    /// See [`ListData::new_unchecked`].
388    pub unsafe fn new_unchecked(elements: ArrayRef, offsets: ArrayRef, validity: Validity) -> Self {
389        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
390        let len = offsets.len().saturating_sub(1);
391        let slots = ListData::make_slots(&elements, &offsets, &validity, len);
392        let data = unsafe { ListData::new_unchecked() };
393        unsafe { Array::from_parts_unchecked(ArrayParts::new(List, dtype, len, data, slots)) }
394    }
395
396    pub fn into_data_parts(self) -> ListDataParts {
397        let dtype = self.dtype().clone();
398        let elements = self.slots()[ListSlots::ELEMENTS]
399            .clone()
400            .vortex_expect("ListArray elements slot");
401        let offsets = self.slots()[ListSlots::OFFSETS]
402            .clone()
403            .vortex_expect("ListArray offsets slot");
404        let validity = self.list_validity();
405        ListDataParts {
406            elements,
407            offsets,
408            validity,
409            dtype,
410        }
411    }
412}