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