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