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