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_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_error::vortex_bail;
12use vortex_error::vortex_ensure;
13use vortex_error::vortex_err;
14
15use crate::ArrayRef;
16use crate::LEGACY_SESSION;
17use crate::ToCanonical;
18use crate::VortexSessionExecute;
19use crate::array::Array;
20use crate::array::ArrayParts;
21use crate::array::TypedArrayRef;
22use crate::array::child_to_validity;
23use crate::array::validity_to_child;
24use crate::arrays::ListView;
25use crate::arrays::Primitive;
26use crate::arrays::PrimitiveArray;
27use crate::arrays::bool;
28use crate::dtype::DType;
29use crate::dtype::IntegerPType;
30use crate::match_each_integer_ptype;
31use crate::validity::Validity;
32
33/// The `elements` data array, where each list scalar is a _slice_ of the `elements` array, and
34/// each inner list element is a _scalar_ of the `elements` array.
35pub(super) const ELEMENTS_SLOT: usize = 0;
36/// The `offsets` array indicating the start position of each list in elements.
37///
38/// Since we also store `sizes`, this `offsets` field is allowed to be stored out-of-order
39/// (which is different from [`ListArray`](crate::arrays::ListArray)).
40pub(super) const OFFSETS_SLOT: usize = 1;
41/// The `sizes` array indicating the length of each list.
42///
43/// This field is intended to be paired with a corresponding offset to determine the list scalar
44/// we want to access.
45pub(super) const SIZES_SLOT: usize = 2;
46/// The validity bitmap indicating which list elements are non-null.
47pub(super) const VALIDITY_SLOT: usize = 3;
48pub(super) const NUM_SLOTS: usize = 4;
49pub(super) const SLOT_NAMES: [&str; NUM_SLOTS] = ["elements", "offsets", "sizes", "validity"];
50
51/// The canonical encoding for variable-length list arrays.
52///
53/// The `ListViewArray` encoding differs from [`ListArray`] in that it stores a child `sizes` array
54/// in addition to a child `offsets` array (which is the _only_ child in [`ListArray`]).
55///
56/// In the past, we used [`ListArray`] as the canonical encoding for [`DType::List`], but we have
57/// since migrated to `ListViewArray` for a few reasons:
58///
59/// - Enables better SIMD vectorization (no sequential dependency when reading `offsets`)
60/// - Allows out-of-order offsets for better compression (we can shuffle the buffers)
61/// - Supports different integer types for offsets vs sizes
62///
63/// It is worth mentioning that this encoding mirrors Apache Arrow's `ListView` array type, but does
64/// not exactly mirror the similar type found in DuckDB and Velox, which stores the pair of offset
65/// and size in a row-major fashion rather than column-major. More specifically, the row-major
66/// layout has a single child array with alternating offset and size next to each other.
67///
68/// We choose the column-major layout as it allows better compressability, as well as using
69/// different (logical) integer widths for our `offsets` and `sizes` buffers (note that the
70/// compressor will likely compress to a different bit-packed width, but this is speaking strictly
71/// about flexibility in the logcial type).
72///
73/// # Examples
74///
75/// ```
76/// # fn main() -> vortex_error::VortexResult<()> {
77/// # use vortex_array::arrays::{ListViewArray, PrimitiveArray};
78/// # use vortex_array::arrays::listview::ListViewArrayExt;
79/// # use vortex_array::validity::Validity;
80/// # use vortex_array::IntoArray;
81/// # use vortex_buffer::buffer;
82/// # use std::sync::Arc;
83/// #
84/// // Create a list view array representing [[3, 4], [1], [2, 3]].
85/// // Note: Unlike `ListArray`, offsets don't need to be monotonic.
86///
87/// let elements = buffer![1i32, 2, 3, 4, 5].into_array();
88/// let offsets = buffer![2u32, 0, 1].into_array();  // Out-of-order offsets
89/// let sizes = buffer![2u32, 1, 2].into_array();  // The sizes cause overlaps
90///
91/// let list_view = ListViewArray::new(
92///     elements.into_array(),
93///     offsets.into_array(),
94///     sizes.into_array(),
95///     Validity::NonNullable,
96/// );
97///
98/// assert_eq!(list_view.len(), 3);
99///
100/// // Access individual lists
101/// let first_list = list_view.list_elements_at(0)?;
102/// assert_eq!(first_list.len(), 2);
103/// // First list contains elements[2..4] = [3, 4]
104///
105/// let first_offset = list_view.offset_at(0);
106/// let first_size = list_view.size_at(0);
107/// assert_eq!(first_offset, 2);
108/// assert_eq!(first_size, 2);
109/// # Ok(())
110/// # }
111/// ```
112///
113/// [`ListArray`]: crate::arrays::ListArray
114#[derive(Clone, Debug)]
115pub struct ListViewData {
116    // TODO(connor)[ListView]: Add the n+1 memory allocation optimization.
117    /// A flag denoting if the array is zero-copyable* to a [`ListArray`](crate::arrays::ListArray).
118    ///
119    /// We use this information to help us more efficiently rebuild / compact our data.
120    ///
121    /// When this flag is true (indicating sorted offsets with no gaps and no overlaps and all
122    /// `offsets[i] + sizes[i]` are in order), conversions can bypass the very expensive rebuild
123    /// process which must rebuild the array from scratch.
124    is_zero_copy_to_list: bool,
125}
126
127impl Display for ListViewData {
128    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
129        write!(f, "is_zero_copy_to_list: {}", self.is_zero_copy_to_list)
130    }
131}
132
133pub struct ListViewDataParts {
134    pub elements_dtype: Arc<DType>,
135
136    /// See `ListViewArray::elements`
137    pub elements: ArrayRef,
138
139    /// See `ListViewArray::offsets`
140    pub offsets: ArrayRef,
141
142    /// See `ListViewArray::sizes`
143    pub sizes: ArrayRef,
144
145    /// See `ListViewArray::validity`
146    pub validity: Validity,
147}
148
149impl ListViewData {
150    pub(crate) fn make_slots(
151        elements: &ArrayRef,
152        offsets: &ArrayRef,
153        sizes: &ArrayRef,
154        validity: &Validity,
155        len: usize,
156    ) -> Vec<Option<ArrayRef>> {
157        vec![
158            Some(elements.clone()),
159            Some(offsets.clone()),
160            Some(sizes.clone()),
161            validity_to_child(validity, len),
162        ]
163    }
164
165    /// Creates a new `ListViewArray`.
166    ///
167    /// # Panics
168    ///
169    /// Panics if the provided components do not satisfy the invariants documented
170    /// in `ListViewArray::new_unchecked`.
171    pub fn new() -> Self {
172        Self {
173            is_zero_copy_to_list: false,
174        }
175    }
176
177    /// Constructs a new `ListViewArray`.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if the provided components do not satisfy the invariants documented
182    /// in `ListViewArray::new_unchecked`.
183    pub fn try_new() -> VortexResult<Self> {
184        Ok(Self::new())
185    }
186
187    /// Creates a new `ListViewArray` without validation.
188    ///
189    /// This unsafe function does not check the validity of the data. Prefer calling [`new()`] or
190    /// [`try_new()`] over this function, as they will check the validity of the data.
191    ///
192    /// [`ListArray`]: crate::arrays::ListArray
193    /// [`new()`]: Self::new
194    /// [`try_new()`]: Self::try_new
195    ///
196    /// # Safety
197    ///
198    /// The caller must ensure all of the following invariants are satisfied:
199    ///
200    /// - `offsets` and `sizes` must be non-nullable integer arrays.
201    /// - `offsets` and `sizes` must have the same length.
202    /// - Size integer width must be smaller than or equal to offset type (to prevent overflow).
203    /// - For each `i`, `offsets[i] + sizes[i]` must not overflow and must be `<= elements.len()`
204    ///   (even if the corresponding view is defined as null by the validity array).
205    /// - If validity is an array, its length must equal `offsets.len()`.
206    pub unsafe fn new_unchecked() -> Self {
207        Self::new()
208    }
209
210    /// Validates the components that would be used to create a `ListViewArray`.
211    pub fn validate(
212        elements: &ArrayRef,
213        offsets: &ArrayRef,
214        sizes: &ArrayRef,
215        validity: &Validity,
216    ) -> VortexResult<()> {
217        // Check that offsets and sizes are integer arrays and non-nullable.
218        vortex_ensure!(
219            offsets.dtype().is_int() && !offsets.dtype().is_nullable(),
220            "offsets must be non-nullable integer array, got {}",
221            offsets.dtype()
222        );
223        vortex_ensure!(
224            sizes.dtype().is_int() && !sizes.dtype().is_nullable(),
225            "sizes must be non-nullable integer array, got {}",
226            sizes.dtype()
227        );
228
229        // Check that they have the same length.
230        vortex_ensure!(
231            offsets.len() == sizes.len(),
232            "offsets and sizes must have the same length, got {} and {}",
233            offsets.len(),
234            sizes.len()
235        );
236
237        // Check that the size type can fit within the offset type to prevent overflows.
238        let size_ptype = sizes.dtype().as_ptype();
239        let offset_ptype = offsets.dtype().as_ptype();
240
241        // If a validity array is present, it must be the same length as the `ListViewArray`.
242        if let Some(validity_len) = validity.maybe_len() {
243            vortex_ensure!(
244                validity_len == offsets.len(),
245                "validity with size {validity_len} does not match array size {}",
246                offsets.len()
247            );
248        }
249
250        // Skip host-only validation when offsets/sizes are not host-resident.
251        if offsets.is_host() && sizes.is_host() {
252            let offsets_primitive = offsets.to_primitive();
253            let sizes_primitive = sizes.to_primitive();
254
255            // Validate the `offsets` and `sizes` arrays.
256            match_each_integer_ptype!(offset_ptype, |O| {
257                match_each_integer_ptype!(size_ptype, |S| {
258                    let offsets_slice = offsets_primitive.as_slice::<O>();
259                    let sizes_slice = sizes_primitive.as_slice::<S>();
260
261                    validate_offsets_and_sizes::<O, S>(
262                        offsets_slice,
263                        sizes_slice,
264                        elements.len() as u64,
265                    )?;
266                })
267            });
268        }
269
270        Ok(())
271    }
272
273    /// Sets whether this `ListViewArray` is zero-copyable to a [`ListArray`].
274    ///
275    /// This is an optimization flag that enables more efficient conversion to [`ListArray`] without
276    /// needing to copy or reorganize the data.
277    ///
278    /// [`ListArray`]: crate::arrays::ListArray
279    ///
280    /// # Safety
281    ///
282    /// When setting `is_zctl` to `true`, the caller must ensure that the `ListViewArray` is
283    /// actually zero-copyable to a [`ListArray`]. This means:
284    ///
285    /// - Offsets must be sorted (but not strictly sorted, zero-length lists are allowed).
286    /// - `offsets[i] + sizes[i] == offsets[i + 1]` for all `i`.
287    /// - No gaps in elements between first and last referenced elements.
288    /// - No overlapping list views (each element referenced at most once).
289    ///
290    /// Note that leading and trailing unreferenced elements **ARE** allowed.
291    pub unsafe fn with_zero_copy_to_list(mut self, is_zctl: bool) -> Self {
292        self.is_zero_copy_to_list = is_zctl;
293        self
294    }
295
296    /// Returns true if the `ListViewArray` is zero-copyable to a
297    /// [`ListArray`](crate::arrays::ListArray).
298    pub fn is_zero_copy_to_list(&self) -> bool {
299        self.is_zero_copy_to_list
300    }
301}
302
303impl Default for ListViewData {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309pub trait ListViewArrayExt: TypedArrayRef<ListView> {
310    fn nullability(&self) -> crate::dtype::Nullability {
311        match self.as_ref().dtype() {
312            DType::List(_, nullability) => *nullability,
313            _ => unreachable!("ListViewArrayExt requires a list dtype"),
314        }
315    }
316
317    fn elements(&self) -> &ArrayRef {
318        self.as_ref().slots()[ELEMENTS_SLOT]
319            .as_ref()
320            .vortex_expect("ListViewArray elements slot")
321    }
322
323    fn offsets(&self) -> &ArrayRef {
324        self.as_ref().slots()[OFFSETS_SLOT]
325            .as_ref()
326            .vortex_expect("ListViewArray offsets slot")
327    }
328
329    fn sizes(&self) -> &ArrayRef {
330        self.as_ref().slots()[SIZES_SLOT]
331            .as_ref()
332            .vortex_expect("ListViewArray sizes slot")
333    }
334
335    fn listview_validity(&self) -> Validity {
336        child_to_validity(&self.as_ref().slots()[VALIDITY_SLOT], self.nullability())
337    }
338
339    fn offset_at(&self, index: usize) -> usize {
340        assert!(
341            index < self.as_ref().len(),
342            "Index {index} out of bounds 0..{}",
343            self.as_ref().len()
344        );
345        self.offsets()
346            .as_opt::<Primitive>()
347            .map(|p| match_each_integer_ptype!(p.ptype(), |P| { p.as_slice::<P>()[index].as_() }))
348            .unwrap_or_else(|| {
349                self.offsets()
350                    .execute_scalar(index, &mut LEGACY_SESSION.create_execution_ctx())
351                    .vortex_expect("offsets must support execute_scalar")
352                    .as_primitive()
353                    .as_::<usize>()
354                    .vortex_expect("offset must fit in usize")
355            })
356    }
357
358    fn size_at(&self, index: usize) -> usize {
359        assert!(
360            index < self.as_ref().len(),
361            "Index {} out of bounds 0..{}",
362            index,
363            self.as_ref().len()
364        );
365        self.sizes()
366            .as_opt::<Primitive>()
367            .map(|p| match_each_integer_ptype!(p.ptype(), |P| { p.as_slice::<P>()[index].as_() }))
368            .unwrap_or_else(|| {
369                self.sizes()
370                    .execute_scalar(index, &mut LEGACY_SESSION.create_execution_ctx())
371                    .vortex_expect("sizes must support execute_scalar")
372                    .as_primitive()
373                    .as_::<usize>()
374                    .vortex_expect("size must fit in usize")
375            })
376    }
377
378    fn list_elements_at(&self, index: usize) -> VortexResult<ArrayRef> {
379        let offset = self.offset_at(index);
380        let size = self.size_at(index);
381        self.elements().slice(offset..offset + size)
382    }
383
384    fn verify_is_zero_copy_to_list(&self) -> bool {
385        validate_zctl(
386            self.elements(),
387            self.offsets().to_primitive(),
388            self.sizes().to_primitive(),
389        )
390        .is_ok()
391    }
392}
393impl<T: TypedArrayRef<ListView>> ListViewArrayExt for T {}
394
395impl Array<ListView> {
396    /// Creates a new `ListViewArray`.
397    pub fn new(elements: ArrayRef, offsets: ArrayRef, sizes: ArrayRef, validity: Validity) -> Self {
398        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
399        let len = offsets.len();
400        let slots = ListViewData::make_slots(&elements, &offsets, &sizes, &validity, len);
401        ListViewData::validate(&elements, &offsets, &sizes, &validity)
402            .vortex_expect("`ListViewArray` construction failed");
403        let data = ListViewData::new();
404        unsafe {
405            Array::from_parts_unchecked(
406                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
407            )
408        }
409    }
410
411    /// Constructs a new `ListViewArray`.
412    pub fn try_new(
413        elements: ArrayRef,
414        offsets: ArrayRef,
415        sizes: ArrayRef,
416        validity: Validity,
417    ) -> VortexResult<Self> {
418        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
419        let len = offsets.len();
420        let slots = ListViewData::make_slots(&elements, &offsets, &sizes, &validity, len);
421        ListViewData::validate(&elements, &offsets, &sizes, &validity)?;
422        let data = ListViewData::try_new()?;
423        Ok(unsafe {
424            Array::from_parts_unchecked(
425                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
426            )
427        })
428    }
429
430    /// Creates a new `ListViewArray` without validation.
431    ///
432    /// # Safety
433    ///
434    /// See [`ListViewData::new_unchecked`].
435    pub unsafe fn new_unchecked(
436        elements: ArrayRef,
437        offsets: ArrayRef,
438        sizes: ArrayRef,
439        validity: Validity,
440    ) -> Self {
441        let dtype = DType::List(Arc::new(elements.dtype().clone()), validity.nullability());
442        let len = offsets.len();
443        let slots = ListViewData::make_slots(&elements, &offsets, &sizes, &validity, len);
444        let data = unsafe { ListViewData::new_unchecked() };
445        unsafe {
446            Array::from_parts_unchecked(
447                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
448            )
449        }
450    }
451
452    /// Mark whether this list view can be zero-copy converted to a list.
453    ///
454    /// # Safety
455    ///
456    /// See [`ListViewData::with_zero_copy_to_list`].
457    pub unsafe fn with_zero_copy_to_list(self, is_zctl: bool) -> Self {
458        if cfg!(debug_assertions) && is_zctl {
459            validate_zctl(
460                self.elements(),
461                self.offsets().to_primitive(),
462                self.sizes().to_primitive(),
463            )
464            .vortex_expect("Failed to validate zero-copy to list flag");
465        }
466        let dtype = self.dtype().clone();
467        let len = self.len();
468        let slots = self.slots().to_vec();
469        let data = unsafe { self.into_data().with_zero_copy_to_list(is_zctl) };
470        unsafe {
471            Array::from_parts_unchecked(
472                ArrayParts::new(ListView, dtype, len, data).with_slots(slots),
473            )
474        }
475    }
476
477    pub fn into_data_parts(self) -> ListViewDataParts {
478        let elements = self.slots()[ELEMENTS_SLOT]
479            .clone()
480            .vortex_expect("ListViewArray elements slot");
481        let offsets = self.slots()[OFFSETS_SLOT]
482            .clone()
483            .vortex_expect("ListViewArray offsets slot");
484        let sizes = self.slots()[SIZES_SLOT]
485            .clone()
486            .vortex_expect("ListViewArray sizes slot");
487        let validity = self.listview_validity();
488        ListViewDataParts {
489            elements_dtype: Arc::new(elements.dtype().clone()),
490            elements,
491            offsets,
492            sizes,
493            validity,
494        }
495    }
496}
497
498/// Helper function to validate `offsets` and `sizes` with specific types.
499fn validate_offsets_and_sizes<O, S>(
500    offsets_slice: &[O],
501    sizes_slice: &[S],
502    elements_len: u64,
503) -> VortexResult<()>
504where
505    O: IntegerPType,
506    S: IntegerPType,
507{
508    debug_assert_eq!(offsets_slice.len(), sizes_slice.len());
509
510    #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)]
511    for i in 0..offsets_slice.len() {
512        let offset = offsets_slice[i];
513        let size = sizes_slice[i];
514
515        vortex_ensure!(offset >= O::zero(), "cannot have negative offsets");
516        vortex_ensure!(size >= S::zero(), "cannot have negative size");
517
518        let offset_u64 = offset
519            .to_u64()
520            .ok_or_else(|| vortex_err!("offset[{i}] = {offset:?} cannot be converted to u64"))?;
521
522        let size_u64 = size
523            .to_u64()
524            .ok_or_else(|| vortex_err!("size[{i}] = {size:?} cannot be converted to u64"))?;
525
526        // Check for overflow when adding offset + size.
527        let end = offset_u64.checked_add(size_u64).ok_or_else(|| {
528            vortex_err!("offset[{i}] ({offset_u64}) + size[{i}] ({size_u64}) would overflow u64")
529        })?;
530
531        if offset_u64 == elements_len {
532            vortex_ensure!(
533                size_u64 == 0,
534                "views to the end of the elements array (length {elements_len}) must have size 0 \
535                    (had size {size_u64})"
536            );
537        }
538
539        vortex_ensure!(
540            end <= elements_len,
541            "offset[{i}] + size[{i}] = {offset_u64} + {size_u64} = {end} \
542            exceeds elements length {elements_len}",
543        );
544    }
545
546    Ok(())
547}
548
549/// Helper function to validate if the `ListViewArray` components are actually zero-copyable to
550/// [`ListArray`](crate::arrays::ListArray).
551fn validate_zctl(
552    elements: &ArrayRef,
553    offsets_primitive: PrimitiveArray,
554    sizes_primitive: PrimitiveArray,
555) -> VortexResult<()> {
556    // Offsets must be sorted (but not strictly sorted, zero-length lists are allowed), even
557    // if there are null views.
558    let mut ctx = LEGACY_SESSION.create_execution_ctx();
559    if let Some(is_sorted) = offsets_primitive.statistics().compute_is_sorted(&mut ctx) {
560        vortex_ensure!(is_sorted, "offsets must be sorted");
561    } else {
562        vortex_bail!("offsets must report is_sorted statistic");
563    }
564
565    // Validate that offset[i] + size[i] <= offset[i+1] for all items
566    // This ensures views are non-overlapping and properly ordered for zero-copy-to-list
567    fn validate_monotonic_ends<O: IntegerPType, S: IntegerPType>(
568        offsets_slice: &[O],
569        sizes_slice: &[S],
570        len: usize,
571    ) -> VortexResult<()> {
572        let mut max_end = 0usize;
573
574        for i in 0..len {
575            let offset = offsets_slice[i].to_usize().unwrap_or(usize::MAX);
576            let size = sizes_slice[i].to_usize().unwrap_or(usize::MAX);
577
578            // Check that this view starts at or after the previous view ended
579            vortex_ensure!(
580                offset >= max_end,
581                "Zero-copy-to-list requires views to be non-overlapping and ordered: \
582                 view[{}] starts at {} but previous views extend to {}",
583                i,
584                offset,
585                max_end
586            );
587
588            // Update max_end for the next iteration
589            let end = offset.saturating_add(size);
590            max_end = max_end.max(end);
591        }
592
593        Ok(())
594    }
595
596    let offsets_dtype = offsets_primitive.dtype();
597    let sizes_dtype = sizes_primitive.dtype();
598    let len = offsets_primitive.len();
599
600    // Check that offset + size values are monotonic (no overlaps)
601    match_each_integer_ptype!(offsets_dtype.as_ptype(), |O| {
602        match_each_integer_ptype!(sizes_dtype.as_ptype(), |S| {
603            let offsets_slice = offsets_primitive.as_slice::<O>();
604            let sizes_slice = sizes_primitive.as_slice::<S>();
605
606            validate_monotonic_ends(offsets_slice, sizes_slice, len)?;
607        })
608    });
609
610    // TODO(connor)[ListView]: Making this allocation is expensive, but the more efficient
611    // implementation would be even more complicated than this. We could use a bit buffer denoting
612    // if positions in `elements` are used, and then additionally store a separate flag that tells
613    // us if a position is used more than once.
614    let mut element_references = vec![0u8; elements.len()];
615
616    fn count_references<O: IntegerPType, S: IntegerPType>(
617        element_references: &mut [u8],
618        offsets_primitive: PrimitiveArray,
619        sizes_primitive: PrimitiveArray,
620    ) {
621        let offsets_slice = offsets_primitive.as_slice::<O>();
622        let sizes_slice = sizes_primitive.as_slice::<S>();
623
624        // Note that we ignore nulls here, as the "null" view metadata must still maintain the same
625        // invariants as non-null views, even for a `bool` information.
626        for i in 0..offsets_slice.len() {
627            let offset: usize = offsets_slice[i].as_();
628            let size: usize = sizes_slice[i].as_();
629            for j in offset..offset + size {
630                element_references[j] = element_references[j].saturating_add(1);
631            }
632        }
633    }
634
635    match_each_integer_ptype!(offsets_primitive.ptype(), |O| {
636        match_each_integer_ptype!(sizes_primitive.ptype(), |S| {
637            count_references::<O, S>(&mut element_references, offsets_primitive, sizes_primitive);
638        })
639    });
640
641    // Allow leading and trailing unreferenced elements, but not gaps in the middle.
642    let leftmost_used = element_references
643        .iter()
644        .position(|&references| references != 0);
645    let rightmost_used = element_references
646        .iter()
647        .rposition(|&references| references != 0);
648
649    if let (Some(first_ref), Some(last_ref)) = (leftmost_used, rightmost_used) {
650        vortex_ensure!(
651            element_references[first_ref..=last_ref]
652                .iter()
653                .all(|&references| references != 0),
654            "found gap in elements array between first and last referenced elements"
655        );
656    }
657
658    vortex_ensure!(element_references.iter().all(|&references| references <= 1));
659
660    Ok(())
661}