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