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