Skip to main content

vortex_array/arrays/listview/
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_buffer::BitBufferMut;
10use vortex_error::VortexExpect;
11use vortex_error::VortexResult;
12use vortex_error::vortex_bail;
13use vortex_error::vortex_ensure;
14use vortex_error::vortex_err;
15use vortex_mask::Mask;
16
17use crate::ArrayRef;
18use crate::ArraySlots;
19use crate::ExecutionCtx;
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::ListView;
30use crate::arrays::Primitive;
31use crate::arrays::PrimitiveArray;
32use crate::arrays::bool;
33use crate::arrays::primitive::PrimitiveArrayExt;
34use crate::builtins::ArrayBuiltins;
35use crate::dtype::DType;
36use crate::dtype::IntegerPType;
37use crate::dtype::PType;
38use crate::expr::stats::Stat;
39use crate::legacy_session;
40use crate::match_each_integer_ptype;
41use crate::match_each_unsigned_integer_ptype;
42use crate::scalar_fn::fns::operators::Operator;
43use crate::validity::Validity;
44
45#[array_slots(ListView)]
46pub struct ListViewSlots {
47    /// The `elements` data array, where each list scalar is a _slice_ of the `elements` array,
48    /// and each inner list element is a _scalar_ of the `elements` array.
49    #[slot(0)]
50    pub elements: ArrayRef,
51    /// The `offsets` array indicating the start position of each list in elements.
52    ///
53    /// Since we also store `sizes`, this `offsets` field is allowed to be stored out-of-order
54    /// (which is different from [`ListArray`](crate::arrays::ListArray)).
55    #[slot(1)]
56    pub offsets: ArrayRef,
57    /// The `sizes` array indicating the length of each list.
58    ///
59    /// This field is intended to be paired with a corresponding offset to determine the list
60    /// scalar we want to access.
61    #[slot(2)]
62    pub sizes: ArrayRef,
63    /// The validity bitmap indicating which list elements are non-null.
64    #[slot(3)]
65    pub validity: Option<ArrayRef>,
66}
67
68/// The canonical encoding for variable-length list arrays.
69///
70/// The `ListViewArray` encoding differs from [`ListArray`] in that it stores a child `sizes` array
71/// in addition to a child `offsets` array (which is the _only_ child in [`ListArray`]).
72///
73/// In the past, we used [`ListArray`] as the canonical encoding for [`DType::List`], but we have
74/// since migrated to `ListViewArray` for a few reasons:
75///
76/// - Enables better SIMD vectorization (no sequential dependency when reading `offsets`)
77/// - Allows out-of-order offsets for better compression (we can shuffle the buffers)
78/// - Supports different integer types for offsets vs sizes
79///
80/// It is worth mentioning that this encoding mirrors Apache Arrow's `ListView` array type, but does
81/// not exactly mirror the similar type found in DuckDB and Velox, which stores the pair of offset
82/// and size in a row-major fashion rather than column-major. More specifically, the row-major
83/// layout has a single child array with alternating offset and size next to each other.
84///
85/// We choose the column-major layout as it allows better compressability, as well as using
86/// different (logical) integer widths for our `offsets` and `sizes` buffers (note that the
87/// compressor will likely compress to a different bit-packed width, but this is speaking strictly
88/// about flexibility in the logcial type).
89///
90/// # Examples
91///
92/// ```
93/// # fn main() -> vortex_error::VortexResult<()> {
94/// # use vortex_array::arrays::{ListViewArray, PrimitiveArray};
95/// # use vortex_array::arrays::listview::ListViewArrayExt;
96/// # use vortex_array::validity::Validity;
97/// # use vortex_array::IntoArray;
98/// # use vortex_buffer::buffer;
99/// # use std::sync::Arc;
100/// #
101/// // Create a list view array representing [[3, 4], [1], [2, 3]].
102/// // Note: Unlike `ListArray`, offsets don't need to be monotonic.
103///
104/// let elements = buffer![1i32, 2, 3, 4, 5].into_array();
105/// let offsets = buffer![2u32, 0, 1].into_array();  // Out-of-order offsets
106/// let sizes = buffer![2u32, 1, 2].into_array();  // The sizes cause overlaps
107///
108/// let list_view = ListViewArray::new(
109///     elements.into_array(),
110///     offsets.into_array(),
111///     sizes.into_array(),
112///     Validity::NonNullable,
113/// );
114///
115/// assert_eq!(list_view.len(), 3);
116///
117/// // Access individual lists
118/// let first_list = list_view.list_elements_at(0)?;
119/// assert_eq!(first_list.len(), 2);
120/// // First list contains elements[2..4] = [3, 4]
121///
122/// let first_offset = list_view.offset_at(0);
123/// let first_size = list_view.size_at(0);
124/// assert_eq!(first_offset, 2);
125/// assert_eq!(first_size, 2);
126/// # Ok(())
127/// # }
128/// ```
129///
130/// [`ListArray`]: crate::arrays::ListArray
131#[derive(Clone, Debug)]
132pub struct ListViewData {
133    // TODO(connor)[ListView]: Add the n+1 memory allocation optimization.
134    /// A flag denoting if the array is zero-copyable* to a [`ListArray`](crate::arrays::ListArray).
135    ///
136    /// We use this information to help us more efficiently rebuild / compact our data.
137    ///
138    /// When this flag is true (indicating sorted offsets with no gaps and no overlaps and all
139    /// `offsets[i] + sizes[i]` are in order), conversions can bypass the very expensive rebuild
140    /// process which must rebuild the array from scratch.
141    is_zero_copy_to_list: bool,
142}
143
144impl Display for ListViewData {
145    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146        write!(f, "is_zero_copy_to_list: {}", self.is_zero_copy_to_list)
147    }
148}
149
150pub struct ListViewDataParts {
151    pub elements_dtype: Arc<DType>,
152
153    /// See `ListViewArray::elements`
154    pub elements: ArrayRef,
155
156    /// See `ListViewArray::offsets`
157    pub offsets: ArrayRef,
158
159    /// See `ListViewArray::sizes`
160    pub sizes: ArrayRef,
161
162    /// See `ListViewArray::validity`
163    pub validity: Validity,
164}
165
166impl ListViewData {
167    pub(crate) fn make_slots(
168        elements: &ArrayRef,
169        offsets: &ArrayRef,
170        sizes: &ArrayRef,
171        validity: &Validity,
172        len: usize,
173    ) -> ArraySlots {
174        ListViewSlots {
175            elements: elements.clone(),
176            offsets: offsets.clone(),
177            sizes: sizes.clone(),
178            validity: validity_to_child(validity, len),
179        }
180        .into_slots()
181    }
182
183    /// Creates a new `ListViewArray`.
184    ///
185    /// # Panics
186    ///
187    /// Panics if the provided components do not satisfy the invariants documented
188    /// in `ListViewArray::new_unchecked`.
189    pub fn new() -> Self {
190        Self {
191            is_zero_copy_to_list: false,
192        }
193    }
194
195    /// Constructs a new `ListViewArray`.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the provided components do not satisfy the invariants documented
200    /// in `ListViewArray::new_unchecked`.
201    pub fn try_new() -> VortexResult<Self> {
202        Ok(Self::new())
203    }
204
205    /// Creates a new `ListViewArray` without validation.
206    ///
207    /// This unsafe function does not check the validity of the data. Prefer calling [`new()`] or
208    /// [`try_new()`] over this function, as they will check the validity of the data.
209    ///
210    /// [`ListArray`]: crate::arrays::ListArray
211    /// [`new()`]: Self::new
212    /// [`try_new()`]: Self::try_new
213    ///
214    /// # Safety
215    ///
216    /// The caller must ensure all of the following invariants are satisfied:
217    ///
218    /// - `offsets` and `sizes` must be non-nullable integer arrays.
219    /// - `offsets` and `sizes` must have the same length.
220    /// - Size integer width must be smaller than or equal to offset type (to prevent overflow).
221    /// - For each `i`, `offsets[i] + sizes[i]` must not overflow and must be `<= elements.len()`
222    ///   (even if the corresponding view is defined as null by the validity array).
223    /// - If validity is an array, its length must equal `offsets.len()`.
224    pub unsafe fn new_unchecked() -> Self {
225        Self::new()
226    }
227
228    /// Validates the components that would be used to create a `ListViewArray`.
229    pub fn validate(
230        elements: &ArrayRef,
231        offsets: &ArrayRef,
232        sizes: &ArrayRef,
233        validity: &Validity,
234    ) -> VortexResult<()> {
235        // Check that offsets and sizes are integer arrays and non-nullable.
236        vortex_ensure!(
237            offsets.dtype().is_int() && !offsets.dtype().is_nullable(),
238            "offsets must be non-nullable integer array, got {}",
239            offsets.dtype()
240        );
241        vortex_ensure!(
242            sizes.dtype().is_int() && !sizes.dtype().is_nullable(),
243            "sizes must be non-nullable integer array, got {}",
244            sizes.dtype()
245        );
246
247        // Check that they have the same length.
248        vortex_ensure!(
249            offsets.len() == sizes.len(),
250            "offsets and sizes must have the same length, got {} and {}",
251            offsets.len(),
252            sizes.len()
253        );
254
255        // If a validity array is present, it must be the same length as the `ListViewArray`.
256        if let Some(validity_len) = validity.maybe_len() {
257            vortex_ensure!(
258                validity_len == offsets.len(),
259                "validity with size {validity_len} does not match array size {}",
260                offsets.len()
261            );
262        }
263
264        // Skip host-only validation when offsets/sizes are not host-resident.
265        if offsets.is_host() && sizes.is_host() {
266            #[allow(clippy::disallowed_methods)]
267            let mut ctx = legacy_session().create_execution_ctx();
268            let offsets_primitive = offsets.clone().execute::<PrimitiveArray>(&mut ctx)?;
269            let sizes_primitive = sizes.clone().execute::<PrimitiveArray>(&mut ctx)?;
270            // Offsets and sizes are non-negative; reinterpret to unsigned to dispatch over 4 widths
271            // each (4x4 instead of 8x8). This is a read-only validation, so result types are moot.
272            let offsets_primitive =
273                offsets_primitive.reinterpret_cast(offsets_primitive.ptype().to_unsigned());
274            let sizes_primitive =
275                sizes_primitive.reinterpret_cast(sizes_primitive.ptype().to_unsigned());
276
277            // Validate the `offsets` and `sizes` arrays.
278            match_each_unsigned_integer_ptype!(offsets_primitive.ptype(), |O| {
279                match_each_unsigned_integer_ptype!(sizes_primitive.ptype(), |S| {
280                    let offsets_slice = offsets_primitive.as_slice::<O>();
281                    let sizes_slice = sizes_primitive.as_slice::<S>();
282
283                    validate_offsets_and_sizes::<O, S>(
284                        offsets_slice,
285                        sizes_slice,
286                        elements.len() as u64,
287                    )?;
288                })
289            });
290        }
291
292        Ok(())
293    }
294
295    /// Sets whether this `ListViewArray` is zero-copyable to a [`ListArray`].
296    ///
297    /// This is an optimization flag that enables more efficient conversion to [`ListArray`] without
298    /// needing to copy or reorganize the data.
299    ///
300    /// [`ListArray`]: crate::arrays::ListArray
301    ///
302    /// # Safety
303    ///
304    /// When setting `is_zctl` to `true`, the caller must ensure that the `ListViewArray` is
305    /// actually zero-copyable to a [`ListArray`]. This means:
306    ///
307    /// - Offsets must be sorted (but not strictly sorted, zero-length lists are allowed).
308    /// - `offsets[i] + sizes[i] == offsets[i + 1]` for all `i`.
309    /// - No gaps in elements between first and last referenced elements.
310    /// - No overlapping list views (each element referenced at most once).
311    ///
312    /// Note that leading and trailing unreferenced elements **ARE** allowed.
313    pub unsafe fn with_zero_copy_to_list(mut self, is_zctl: bool) -> Self {
314        self.is_zero_copy_to_list = is_zctl;
315        self
316    }
317
318    /// Returns true if the `ListViewArray` is zero-copyable to a
319    /// [`ListArray`](crate::arrays::ListArray).
320    pub fn is_zero_copy_to_list(&self) -> bool {
321        self.is_zero_copy_to_list
322    }
323}
324
325impl Default for ListViewData {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331/// Walks parallel `(offset, size)` slices and sets each range `[offset, offset + size]` in `buf`.
332///
333/// **Preconditions**
334///
335/// `offsets` and `sizes` must be the same length (which is always the case in valid `ListViewArray`s).
336fn fill_referenced_mask<O: IntegerPType, S: IntegerPType>(
337    buf: &mut BitBufferMut,
338    offsets: &[O],
339    sizes: &[S],
340) {
341    let len = offsets.len();
342
343    assert_eq!(
344        len,
345        sizes.len(),
346        "offsets and sizes must be the same length"
347    );
348
349    for i in 0..len {
350        let start: usize = offsets[i].as_();
351        let size: usize = sizes[i].as_();
352        buf.fill_range(start, start + size, true);
353    }
354}
355
356pub trait ListViewArrayExt: ListViewArraySlotsExt {
357    fn nullability(&self) -> crate::dtype::Nullability {
358        match self.as_ref().dtype() {
359            DType::List(_, nullability) => *nullability,
360            _ => unreachable!("ListViewArrayExt requires a list dtype"),
361        }
362    }
363
364    fn listview_validity(&self) -> Validity {
365        child_to_validity(
366            self.as_ref().slots()[ListViewSlots::VALIDITY].as_ref(),
367            self.nullability(),
368        )
369    }
370
371    #[allow(clippy::disallowed_methods)]
372    fn offset_at(&self, index: usize) -> usize {
373        assert!(
374            index < self.as_ref().len(),
375            "Index {index} out of bounds 0..{}",
376            self.as_ref().len()
377        );
378        self.offsets()
379            .as_opt::<Primitive>()
380            .map(|p| match_each_integer_ptype!(p.ptype(), |P| { p.as_slice::<P>()[index].as_() }))
381            .unwrap_or_else(|| {
382                self.offsets()
383                    .execute_scalar(index, &mut legacy_session().create_execution_ctx())
384                    .vortex_expect("offsets must support execute_scalar")
385                    .as_primitive()
386                    .as_::<usize>()
387                    .vortex_expect("offset must fit in usize")
388            })
389    }
390
391    #[allow(clippy::disallowed_methods)]
392    fn size_at(&self, index: usize) -> usize {
393        assert!(
394            index < self.as_ref().len(),
395            "Index {} out of bounds 0..{}",
396            index,
397            self.as_ref().len()
398        );
399        self.sizes()
400            .as_opt::<Primitive>()
401            .map(|p| match_each_integer_ptype!(p.ptype(), |P| { p.as_slice::<P>()[index].as_() }))
402            .unwrap_or_else(|| {
403                self.sizes()
404                    .execute_scalar(index, &mut legacy_session().create_execution_ctx())
405                    .vortex_expect("sizes must support execute_scalar")
406                    .as_primitive()
407                    .as_::<usize>()
408                    .vortex_expect("size must fit in usize")
409            })
410    }
411
412    fn list_elements_at(&self, index: usize) -> VortexResult<ArrayRef> {
413        let offset = self.offset_at(index);
414        let size = self.size_at(index);
415        self.elements().slice(offset..offset + size)
416    }
417
418    /// Returns a [`Mask`] of length `elements.len()` where each bit is set iff that
419    /// position in `elements` is referenced by at least one view. Caller must ensure `elements`
420    /// is non-empty.
421    ///
422    /// Walks every `(offset, size)` pair, canonicalizes both `offsets` and `sizes`,
423    /// and allocates a `BitBuffer` of length `elements.len()`, so it is extremely costly.
424    ///
425    /// **Preconditions**
426    ///
427    /// `self.elements()` must be non-empty.
428    fn compute_referenced_elements_mask(&self, ctx: &mut ExecutionCtx) -> VortexResult<Mask> {
429        assert!(!self.elements().is_empty());
430        let len = self.elements().len();
431
432        let offsets_primitive = self.offsets().clone().execute::<PrimitiveArray>(ctx)?;
433        let sizes_primitive = self.sizes().clone().execute::<PrimitiveArray>(ctx)?;
434
435        let mut buf = BitBufferMut::new_unset(len);
436
437        // Offsets/sizes are non-negative; reinterpret to unsigned (4x4 instead of 8x8).
438        let offsets_primitive =
439            offsets_primitive.reinterpret_cast(offsets_primitive.ptype().to_unsigned());
440        let sizes_primitive =
441            sizes_primitive.reinterpret_cast(sizes_primitive.ptype().to_unsigned());
442        match_each_unsigned_integer_ptype!(offsets_primitive.ptype(), |O| {
443            match_each_unsigned_integer_ptype!(sizes_primitive.ptype(), |S| {
444                fill_referenced_mask::<O, S>(
445                    &mut buf,
446                    offsets_primitive.as_slice::<O>(),
447                    sizes_primitive.as_slice::<S>(),
448                );
449            })
450        });
451
452        Ok(Mask::from_buffer(buf.freeze()))
453    }
454
455    /// Exact fraction of `elements` referenced by some view, in `[0.0, 1.0]`. Extremely costly.
456    ///
457    /// Returns `Ok(1.0)` when `elements` is empty instead of dividing by 0.
458    fn compute_density(&self, ctx: &mut ExecutionCtx) -> VortexResult<f32> {
459        if self.elements().is_empty() {
460            return Ok(1.0);
461        }
462
463        if self.sizes().is_empty() {
464            return Ok(0.0);
465        }
466
467        let density = match self.compute_referenced_elements_mask(ctx)? {
468            Mask::AllTrue(_) => 1.0,
469            Mask::AllFalse(_) => 0.0,
470            Mask::Values(values) => values.true_count() as f32 / self.elements().len() as f32,
471        };
472
473        Ok(density)
474    }
475
476    /// Upper-bound estimate of [`compute_density`](Self::compute_density) via
477    /// `sum(sizes) / elements.len()`, clamped to `[0.0, 1.0]`.
478    ///
479    /// Exact for non-overlapping views, but overcounts when multiple views share the same elements.
480    ///
481    /// Returns `Ok(1.0)` when `elements` is empty instead of dividing by 0.
482    fn upper_bound_density(&self, ctx: &mut ExecutionCtx) -> VortexResult<f32> {
483        let n_elts = self.elements().len();
484        if n_elts == 0 {
485            return Ok(1.0);
486        }
487
488        let sizes = self.sizes();
489        if sizes.is_empty() {
490            return Ok(0.0);
491        }
492
493        // compute_stat short-circuits on a cached exact Sum and otherwise computes
494        let sizes_sum = sizes
495            .statistics()
496            .compute_stat(Stat::Sum, ctx)?
497            .vortex_expect("sizes array has integer ptype elements")
498            .as_primitive()
499            .as_::<u64>()
500            .vortex_expect("integer ptypes can be upcast to u64");
501
502        // if the same elements are referenced more than once the estimate may be
503        // greater than 1.0, so clamp
504        let estimate = (sizes_sum as f32 / n_elts as f32).min(1.0);
505
506        debug_assert!(estimate >= 0.0);
507
508        Ok(estimate)
509    }
510
511    /// Returns the half-open range `[start, end)` of `elements` indices referenced by any view:
512    /// the minimum offset and the maximum `offset + size`. Elements outside this range are
513    /// unreferenced leading or trailing slack that a
514    /// [`TrimElements`](super::ListViewRebuildMode::TrimElements) rebuild would reclaim.
515    ///
516    /// For **zero-copy-to-list** arrays this is `O(1)`: views are sorted and non-overlapping with
517    /// no interior gaps, so the bounds are exactly `[first_offset, last_offset + last_size)`.
518    /// Otherwise it computes min/max statistics over `offsets` and `offsets + sizes`.
519    ///
520    /// # Preconditions
521    ///
522    /// The array must contain at least one list (`len() > 0`).
523    fn referenced_element_bounds(&self, ctx: &mut ExecutionCtx) -> VortexResult<(usize, usize)> {
524        let n_lists = self.as_ref().len();
525        vortex_ensure!(
526            n_lists > 0,
527            "referenced_element_bounds requires a non-empty array"
528        );
529
530        if self.is_zero_copy_to_list() {
531            let start = self.offset_at(0);
532            let end = self.offset_at(n_lists - 1) + self.size_at(n_lists - 1);
533            return Ok((start, end));
534        }
535
536        let start = self
537            .offsets()
538            .statistics()
539            .compute_min::<usize>(ctx)
540            .vortex_expect("offsets must report a usize min statistic");
541
542        // Cast offsets and sizes to the widest integer type so that `offset + size` cannot overflow
543        // the narrower input width.
544        let wide_dtype = DType::from(if self.offsets().dtype().as_ptype().is_unsigned_int() {
545            PType::U64
546        } else {
547            PType::I64
548        });
549        let offsets = self.offsets().cast(wide_dtype.clone())?;
550        let sizes = self.sizes().cast(wide_dtype)?;
551        let end = min_max(
552            &offsets.binary(sizes, Operator::Add)?,
553            ctx,
554            NumericalAggregateOpts::default(),
555        )?
556        .vortex_expect("non-empty array must report a min/max")
557        .max
558        .as_primitive()
559        .as_::<usize>()
560        .vortex_expect("max `offset + size` must fit in a usize");
561
562        Ok((start, end))
563    }
564}
565impl<T: TypedArrayRef<ListView>> ListViewArrayExt for T {}
566
567impl Array<ListView> {
568    /// Creates a new `ListViewArray`.
569    pub fn new(elements: ArrayRef, offsets: ArrayRef, sizes: ArrayRef, validity: Validity) -> Self {
570        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
571        let len = offsets.len();
572        let slots = ListViewData::make_slots(&elements, &offsets, &sizes, &validity, len);
573        ListViewData::validate(&elements, &offsets, &sizes, &validity)
574            .vortex_expect("`ListViewArray` construction failed");
575        let data = ListViewData::new();
576        unsafe {
577            Array::from_parts_unchecked(
578                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
579            )
580        }
581    }
582
583    /// Constructs a new `ListViewArray`.
584    pub fn try_new(
585        elements: ArrayRef,
586        offsets: ArrayRef,
587        sizes: ArrayRef,
588        validity: Validity,
589    ) -> VortexResult<Self> {
590        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
591        let len = offsets.len();
592        let slots = ListViewData::make_slots(&elements, &offsets, &sizes, &validity, len);
593        ListViewData::validate(&elements, &offsets, &sizes, &validity)?;
594        let data = ListViewData::try_new()?;
595        Ok(unsafe {
596            Array::from_parts_unchecked(
597                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
598            )
599        })
600    }
601
602    /// Creates a new `ListViewArray` without validation.
603    ///
604    /// # Safety
605    ///
606    /// See [`ListViewData::new_unchecked`].
607    pub unsafe fn new_unchecked(
608        elements: ArrayRef,
609        offsets: ArrayRef,
610        sizes: ArrayRef,
611        validity: Validity,
612    ) -> Self {
613        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
614        let len = offsets.len();
615        let slots = ListViewData::make_slots(&elements, &offsets, &sizes, &validity, len);
616        let data = unsafe { ListViewData::new_unchecked() };
617        unsafe {
618            Array::from_parts_unchecked(
619                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
620            )
621        }
622    }
623
624    /// Mark whether this list view can be zero-copy converted to a list.
625    ///
626    /// # Safety
627    ///
628    /// See [`ListViewData::with_zero_copy_to_list`].
629    pub unsafe fn with_zero_copy_to_list(self, is_zctl: bool) -> Self {
630        if cfg!(debug_assertions) && is_zctl {
631            #[allow(clippy::disallowed_methods)]
632            let mut ctx = legacy_session().create_execution_ctx();
633            let offsets_primitive = self
634                .offsets()
635                .clone()
636                .execute::<PrimitiveArray>(&mut ctx)
637                .vortex_expect("offsets must canonicalize to primitive");
638            let sizes_primitive = self
639                .sizes()
640                .clone()
641                .execute::<PrimitiveArray>(&mut ctx)
642                .vortex_expect("sizes must canonicalize to primitive");
643            validate_zctl(self.elements(), offsets_primitive, sizes_primitive)
644                .vortex_expect("Failed to validate zero-copy to list flag");
645        }
646        let dtype = self.dtype().clone();
647        let len = self.len();
648        let slots: ArraySlots = self.slots().iter().cloned().collect();
649        let data = unsafe { self.into_data().with_zero_copy_to_list(is_zctl) };
650        unsafe {
651            Array::from_parts_unchecked(
652                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
653            )
654        }
655    }
656
657    pub fn into_data_parts(self) -> ListViewDataParts {
658        let elements = self.slots()[ListViewSlots::ELEMENTS]
659            .clone()
660            .vortex_expect("ListViewArray elements slot");
661        let offsets = self.slots()[ListViewSlots::OFFSETS]
662            .clone()
663            .vortex_expect("ListViewArray offsets slot");
664        let sizes = self.slots()[ListViewSlots::SIZES]
665            .clone()
666            .vortex_expect("ListViewArray sizes slot");
667        let validity = self.listview_validity();
668        ListViewDataParts {
669            elements_dtype: Arc::new(elements.dtype().clone()),
670            elements,
671            offsets,
672            sizes,
673            validity,
674        }
675    }
676}
677
678/// Helper function to validate `offsets` and `sizes` with specific types.
679fn validate_offsets_and_sizes<O, S>(
680    offsets_slice: &[O],
681    sizes_slice: &[S],
682    elements_len: u64,
683) -> VortexResult<()>
684where
685    O: IntegerPType,
686    S: IntegerPType,
687{
688    debug_assert_eq!(offsets_slice.len(), sizes_slice.len());
689
690    #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)]
691    for i in 0..offsets_slice.len() {
692        let offset = offsets_slice[i];
693        let size = sizes_slice[i];
694
695        vortex_ensure!(offset >= O::zero(), "cannot have negative offsets");
696        vortex_ensure!(size >= S::zero(), "cannot have negative size");
697
698        let offset_u64 = offset
699            .to_u64()
700            .ok_or_else(|| vortex_err!("offset[{i}] = {offset:?} cannot be converted to u64"))?;
701
702        let size_u64 = size
703            .to_u64()
704            .ok_or_else(|| vortex_err!("size[{i}] = {size:?} cannot be converted to u64"))?;
705
706        // Check for overflow when adding offset + size.
707        let end = offset_u64.checked_add(size_u64).ok_or_else(|| {
708            vortex_err!("offset[{i}] ({offset_u64}) + size[{i}] ({size_u64}) would overflow u64")
709        })?;
710
711        if offset_u64 == elements_len {
712            vortex_ensure!(
713                size_u64 == 0,
714                "views to the end of the elements array (length {elements_len}) must have size 0 \
715                    (had size {size_u64})"
716            );
717        }
718
719        vortex_ensure!(
720            end <= elements_len,
721            "offset[{i}] + size[{i}] = {offset_u64} + {size_u64} = {end} \
722            exceeds elements length {elements_len}",
723        );
724    }
725
726    Ok(())
727}
728
729/// Helper function to validate if the `ListViewArray` components are actually zero-copyable to
730/// [`ListArray`](crate::arrays::ListArray).
731#[allow(clippy::disallowed_methods)]
732fn validate_zctl(
733    elements: &ArrayRef,
734    offsets_primitive: PrimitiveArray,
735    sizes_primitive: PrimitiveArray,
736) -> VortexResult<()> {
737    // Offsets must be sorted (but not strictly sorted, zero-length lists are allowed), even
738    // if there are null views.
739    let mut ctx = legacy_session().create_execution_ctx();
740    if let Some(is_sorted) = offsets_primitive.statistics().compute_is_sorted(&mut ctx) {
741        vortex_ensure!(is_sorted, "offsets must be sorted");
742    } else {
743        vortex_bail!("offsets must report is_sorted statistic");
744    }
745
746    // Validate that offset[i] + size[i] <= offset[i+1] for all items
747    // This ensures views are non-overlapping and properly ordered for zero-copy-to-list
748    fn validate_monotonic_ends<O: IntegerPType, S: IntegerPType>(
749        offsets_slice: &[O],
750        sizes_slice: &[S],
751        len: usize,
752    ) -> VortexResult<()> {
753        let mut max_end = 0usize;
754
755        for i in 0..len {
756            let offset = offsets_slice[i].to_usize().unwrap_or(usize::MAX);
757            let size = sizes_slice[i].to_usize().unwrap_or(usize::MAX);
758
759            // Check that this view starts at or after the previous view ended
760            vortex_ensure!(
761                offset >= max_end,
762                "Zero-copy-to-list requires views to be non-overlapping and ordered: \
763                 view[{}] starts at {} but previous views extend to {}",
764                i,
765                offset,
766                max_end
767            );
768
769            // Update max_end for the next iteration
770            let end = offset.saturating_add(size);
771            max_end = max_end.max(end);
772        }
773
774        Ok(())
775    }
776
777    let offsets_dtype = offsets_primitive.dtype();
778    let sizes_dtype = sizes_primitive.dtype();
779    let len = offsets_primitive.len();
780
781    // Offsets/sizes are non-negative; reinterpret to unsigned (4x4 instead of 8x8).
782    let offsets_unsigned =
783        offsets_primitive.reinterpret_cast(offsets_dtype.as_ptype().to_unsigned());
784    let sizes_unsigned = sizes_primitive.reinterpret_cast(sizes_dtype.as_ptype().to_unsigned());
785
786    // Check that offset + size values are monotonic (no overlaps)
787    match_each_unsigned_integer_ptype!(offsets_unsigned.ptype(), |O| {
788        match_each_unsigned_integer_ptype!(sizes_unsigned.ptype(), |S| {
789            let offsets_slice = offsets_unsigned.as_slice::<O>();
790            let sizes_slice = sizes_unsigned.as_slice::<S>();
791
792            validate_monotonic_ends(offsets_slice, sizes_slice, len)?;
793        })
794    });
795
796    // TODO(connor)[ListView]: Making this allocation is expensive, but the more efficient
797    // implementation would be even more complicated than this. We could use a bit buffer denoting
798    // if positions in `elements` are used, and then additionally store a separate flag that tells
799    // us if a position is used more than once.
800    let mut element_references = vec![0u8; elements.len()];
801
802    fn count_references<O: IntegerPType, S: IntegerPType>(
803        element_references: &mut [u8],
804        offsets_primitive: PrimitiveArray,
805        sizes_primitive: PrimitiveArray,
806    ) {
807        let offsets_slice = offsets_primitive.as_slice::<O>();
808        let sizes_slice = sizes_primitive.as_slice::<S>();
809
810        // Note that we ignore nulls here, as the "null" view metadata must still maintain the same
811        // invariants as non-null views, even for a `bool` information.
812        for i in 0..offsets_slice.len() {
813            let offset: usize = offsets_slice[i].as_();
814            let size: usize = sizes_slice[i].as_();
815            for j in offset..offset + size {
816                element_references[j] = element_references[j].saturating_add(1);
817            }
818        }
819    }
820
821    match_each_unsigned_integer_ptype!(offsets_unsigned.ptype(), |O| {
822        match_each_unsigned_integer_ptype!(sizes_unsigned.ptype(), |S| {
823            count_references::<O, S>(&mut element_references, offsets_unsigned, sizes_unsigned);
824        })
825    });
826
827    // Allow leading and trailing unreferenced elements, but not gaps in the middle.
828    let leftmost_used = element_references
829        .iter()
830        .position(|&references| references != 0);
831    let rightmost_used = element_references
832        .iter()
833        .rposition(|&references| references != 0);
834
835    if let (Some(first_ref), Some(last_ref)) = (leftmost_used, rightmost_used) {
836        vortex_ensure!(
837            element_references[first_ref..=last_ref]
838                .iter()
839                .all(|&references| references != 0),
840            "found gap in elements array between first and last referenced elements"
841        );
842    }
843
844    vortex_ensure!(element_references.iter().all(|&references| references <= 1));
845
846    Ok(())
847}