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