Skip to main content

vortex_array/arrays/listview/
rebuild.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use num_traits::FromPrimitive;
5use vortex_buffer::BufferMut;
6use vortex_error::VortexExpect;
7use vortex_error::VortexResult;
8use vortex_error::vortex_bail;
9use vortex_error::vortex_err;
10use vortex_mask::Mask;
11
12use crate::ExecutionCtx;
13use crate::IntoArray;
14use crate::arrays::ConstantArray;
15use crate::arrays::ListViewArray;
16use crate::arrays::PiecewiseSequenceArray;
17use crate::arrays::PrimitiveArray;
18use crate::arrays::listview::ListViewArrayExt;
19use crate::arrays::listview::ListViewArraySlotsExt;
20use crate::arrays::primitive::PrimitiveArrayExt;
21use crate::builtins::ArrayBuiltins;
22use crate::dtype::IntegerPType;
23use crate::dtype::Nullability;
24use crate::dtype::PType;
25use crate::match_each_integer_ptype;
26use crate::match_each_unsigned_integer_ptype;
27use crate::scalar::Scalar;
28use crate::scalar_fn::fns::operators::Operator;
29use crate::validity::Validity;
30
31/// The widened offset type used for a rebuilt, zero-copy-to-list array.
32///
33/// Offsets are widened to at least 32 bits (Arrow only permits 32/64-bit list offsets) while
34/// preserving signedness, since a signed result keeps the array zero-copyable to Arrow's
35/// `ListArray`.
36fn rebuilt_offset_ptype(offsets_ptype: PType) -> PType {
37    match offsets_ptype {
38        PType::U8 | PType::U16 | PType::U32 => PType::U32,
39        PType::U64 => PType::U64,
40        PType::I8 | PType::I16 | PType::I32 => PType::I32,
41        PType::I64 => PType::I64,
42        _ => unreachable!("invalid offsets PType"),
43    }
44}
45
46/// Density threshold to decide whether to rebuild a sparse `ListViewArray`.
47///
48/// A `ListViewArray` can accumulate unreferenced bytes in its `elements` buffer after
49/// metadata-only operations like `take` and `filter`. When density (referenced fraction of `elements`)
50/// falls below this threshold, the benefits of a rebuild may outweigh its cost.
51///
52/// This is a somewhat arbitrary rule-of-thumb and may be suboptimal depending on different use cases and
53/// list element dtypes.
54pub const DEFAULT_REBUILD_DENSITY_THRESHOLD: f32 = 0.1;
55
56/// Waste threshold to decide whether to trim a zero-copy-to-list `ListViewArray`.
57///
58/// A zero-copy-to-list array has no overlaps and no interior gaps, so its only unreferenced bytes
59/// are leading and trailing elements. Trimming those is much cheaper than a full rebuild (a lazy
60/// `elements` slice plus an `O(num_lists)` offset adjustment, with no element data copy), so we use
61/// a more aggressive threshold than [`DEFAULT_REBUILD_DENSITY_THRESHOLD`].
62///
63/// When the unreferenced (leading + trailing) fraction of `elements` exceeds this threshold, we trim.
64pub const DEFAULT_TRIM_ELEMENTS_THRESHOLD: f32 = 0.05;
65
66/// Modes for rebuilding a [`ListViewArray`].
67pub enum ListViewRebuildMode {
68    /// Removes all unused data and flattens out all list data, such that the array is zero-copyable
69    /// to a [`ListArray`].
70    ///
71    /// This mode will deduplicate all overlapping list views, such that the [`ListViewArray`] looks
72    /// like a [`ListArray`] but with an additional `sizes` array.
73    ///
74    /// [`ListArray`]: crate::arrays::ListArray
75    MakeZeroCopyToList,
76
77    /// Removes any leading or trailing elements that are unused / not referenced by any views in
78    /// the [`ListViewArray`].
79    ///
80    /// If the referenced `[start, end)` bounds are already known, prefer calling
81    /// [`trim_elements`](ListViewArray::trim_elements) directly to avoid recomputing them.
82    TrimElements,
83
84    /// Equivalent to `MakeZeroCopyToList` plus `TrimElements`.
85    ///
86    /// This is useful when concatenating multiple [`ListViewArray`]s together to create a new
87    /// [`ListViewArray`] that is also zero-copy to a [`ListArray`].
88    ///
89    /// [`ListArray`]: crate::arrays::ListArray
90    MakeExact,
91
92    // TODO(connor)[ListView]: Implement some version of this.
93    /// Finds the shortest packing / overlapping of list elements.
94    ///
95    /// This problem is known to be NP-hard, so maybe when someone proves that P=NP we can implement
96    /// this algorithm (but in all seriousness there are many approximate algorithms that could
97    /// work well here).
98    OverlapCompression,
99}
100
101impl ListViewArray {
102    /// Rebuilds the [`ListViewArray`] according to the specified mode.
103    pub fn rebuild(
104        &self,
105        mode: ListViewRebuildMode,
106        ctx: &mut ExecutionCtx,
107    ) -> VortexResult<ListViewArray> {
108        if self.is_empty() {
109            // SAFETY: An empty array is trivially zero-copyable to a `ListArray`.
110            return Ok(unsafe { self.clone().with_zero_copy_to_list(true) });
111        }
112
113        match mode {
114            ListViewRebuildMode::MakeZeroCopyToList => self.rebuild_zero_copy_to_list(ctx),
115            ListViewRebuildMode::TrimElements => self.rebuild_trim_elements(ctx),
116            ListViewRebuildMode::MakeExact => self.rebuild_make_exact(ctx),
117            ListViewRebuildMode::OverlapCompression => {
118                vortex_bail!("ListViewRebuildMode::OverlapCompression is not implemented")
119            }
120        }
121    }
122
123    /// Rebuilds a [`ListViewArray`], removing all data overlaps and creating a flattened layout.
124    ///
125    /// This is useful when the `elements` child array of the [`ListViewArray`] might have
126    /// overlapping, duplicate, and garbage data, and we want to have fully sequential data like
127    /// a [`ListArray`].
128    ///
129    /// [`ListArray`]: crate::arrays::ListArray
130    fn rebuild_zero_copy_to_list(&self, ctx: &mut ExecutionCtx) -> VortexResult<ListViewArray> {
131        if self.is_zero_copy_to_list() {
132            // Note that since everything in `ListViewArray` is `Arc`ed, this is quite cheap.
133            return Ok(self.clone());
134        }
135
136        let offsets_ptype = self.offsets().dtype().as_ptype();
137        let sizes_ptype = self.sizes().dtype().as_ptype();
138
139        // One of the main purposes behind adding this "zero-copyable to `ListArray`" optimization
140        // is that we want to pass data to systems that expect Arrow data.
141        // The arrow specification only allows for `i32` and `i64` offset and sizes types, so in
142        // order to also make `ListView` zero-copyable to **Arrow**'s `ListArray` (not just Vortex's
143        // `ListArray`), we rebuild the offsets as 32-bit or 64-bit integer types.
144        // TODO(connor)[ListView]: This is true for `sizes` as well, we should do this conversion
145        // for sizes as well.
146        match_each_unsigned_integer_ptype!(sizes_ptype.to_unsigned(), |S| {
147            match offsets_ptype.to_unsigned() {
148                PType::U8 => self.naive_rebuild::<u8, u32, S>(ctx),
149                PType::U16 => self.naive_rebuild::<u16, u32, S>(ctx),
150                PType::U32 => self.naive_rebuild::<u32, u32, S>(ctx),
151                PType::U64 => self.naive_rebuild::<u64, u64, S>(ctx),
152                _ => unreachable!("invalid offsets PType"),
153            }
154        })
155    }
156
157    /// Picks between [`rebuild_with_take`](Self::rebuild_with_take) and
158    /// [`rebuild_with_piecewise`](Self::rebuild_with_piecewise) based on average list size.
159    fn naive_rebuild<O: IntegerPType, NewOffset: IntegerPType, S: IntegerPType>(
160        &self,
161        ctx: &mut ExecutionCtx,
162    ) -> VortexResult<ListViewArray> {
163        let sizes_canonical = self.sizes().clone().execute::<PrimitiveArray>(ctx)?;
164        let sizes_canonical =
165            sizes_canonical.reinterpret_cast(sizes_canonical.ptype().to_unsigned());
166        let total: u64 = sizes_canonical
167            .as_slice::<S>()
168            .iter()
169            .map(|s| (*s).as_() as u64)
170            .sum();
171        if Self::should_use_take(total, self.len()) {
172            self.rebuild_with_take::<O, NewOffset, S>(ctx)
173        } else {
174            self.rebuild_with_piecewise::<NewOffset, S>(ctx)
175        }
176    }
177
178    /// Returns `true` when we are confident that `rebuild_with_take` will
179    /// outperform `rebuild_with_piecewise`.
180    ///
181    /// Take is dramatically faster for small lists (often 10-100×) because it
182    /// avoids per-list builder overhead. LBL is the safer default for larger
183    /// lists since its sequential memcpy scales well. We only choose take when
184    /// the average list size is small enough that take clearly dominates.
185    fn should_use_take(total_output_elements: u64, num_lists: usize) -> bool {
186        if num_lists == 0 {
187            return true;
188        }
189        let avg = total_output_elements / num_lists as u64;
190        avg < 128
191    }
192
193    /// Rebuilds elements using a single bulk `take`: collect all element indices into a flat
194    /// `BufferMut<u64>`, perform a single `take`.
195    fn rebuild_with_take<O: IntegerPType, NewOffset: IntegerPType, S: IntegerPType>(
196        &self,
197        ctx: &mut ExecutionCtx,
198    ) -> VortexResult<ListViewArray> {
199        let new_offset_ptype = rebuilt_offset_ptype(self.offsets().dtype().as_ptype());
200        let size_ptype = self.sizes().dtype().as_ptype();
201
202        let offsets_canonical = self.offsets().clone().execute::<PrimitiveArray>(ctx)?;
203        let offsets_canonical =
204            offsets_canonical.reinterpret_cast(offsets_canonical.ptype().to_unsigned());
205        let offsets_slice = offsets_canonical.as_slice::<O>();
206        let sizes_canonical = self.sizes().clone().execute::<PrimitiveArray>(ctx)?;
207        let sizes_canonical =
208            sizes_canonical.reinterpret_cast(sizes_canonical.ptype().to_unsigned());
209        let sizes_slice = sizes_canonical.as_slice::<S>();
210
211        let len = offsets_slice.len();
212
213        let mut new_offsets = BufferMut::<NewOffset>::with_capacity(len);
214        let mut new_sizes = BufferMut::<S>::with_capacity(len);
215        let mut take_indices = BufferMut::<u64>::with_capacity(self.elements().len());
216
217        // Resolve validity to a mask once instead of probing it per row: `execute_is_valid`
218        // executes a scalar on every call for array-backed validity, which is O(len) work repeated
219        // `len` times.
220        let validity = self.validity()?.execute_mask(len, ctx)?;
221
222        let mut n_elements = NewOffset::zero();
223        for index in 0..len {
224            if !validity.value(index) {
225                new_offsets.push(n_elements);
226                new_sizes.push(S::zero());
227                continue;
228            }
229
230            let offset = offsets_slice[index];
231            let size = sizes_slice[index];
232            let start = offset.as_();
233            let stop = start + size.as_();
234
235            new_offsets.push(n_elements);
236            new_sizes.push(size);
237            take_indices.extend(start as u64..stop as u64);
238            n_elements += num_traits::cast(size).vortex_expect("Cast failed");
239        }
240
241        let elements = self.elements().take(take_indices.into_array())?;
242        // Built unsigned; reinterpret back to the signed-preserving result types.
243        let offsets = PrimitiveArray::new(new_offsets.freeze(), Validity::NonNullable)
244            .reinterpret_cast(new_offset_ptype)
245            .into_array();
246        let sizes = PrimitiveArray::new(new_sizes.freeze(), Validity::NonNullable)
247            .reinterpret_cast(size_ptype)
248            .into_array();
249
250        // SAFETY: offsets are sequential and non-overlapping, all (offset, size) pairs reference
251        // valid elements, and the validity array is preserved from the original.
252        Ok(unsafe {
253            ListViewArray::new_unchecked(elements, offsets, sizes, self.validity()?)
254                .with_zero_copy_to_list(true)
255        })
256    }
257
258    /// Rebuilds elements using one contiguous-run take over the element child.
259    fn rebuild_with_piecewise<NewOffset: IntegerPType, S: IntegerPType>(
260        &self,
261        ctx: &mut ExecutionCtx,
262    ) -> VortexResult<ListViewArray> {
263        let new_offset_ptype = rebuilt_offset_ptype(self.offsets().dtype().as_ptype());
264        let size_ptype = self.sizes().dtype().as_ptype();
265
266        let offsets_canonical = self.offsets().clone().execute::<PrimitiveArray>(ctx)?;
267        let offsets_canonical =
268            offsets_canonical.reinterpret_cast(offsets_canonical.ptype().to_unsigned());
269        let sizes_canonical = self.sizes().clone().execute::<PrimitiveArray>(ctx)?;
270        let sizes_canonical =
271            sizes_canonical.reinterpret_cast(sizes_canonical.ptype().to_unsigned());
272
273        let len = offsets_canonical.len();
274        let validity = self.validity()?;
275        let validity_mask = validity.execute_mask(len, ctx)?;
276
277        let ranges = match_each_unsigned_integer_ptype!(offsets_canonical.ptype(), |O| {
278            rebuild_ranges::<NewOffset, O, S>(
279                offsets_canonical.as_slice::<O>(),
280                sizes_canonical.as_slice::<S>(),
281                &validity_mask,
282            )
283        })?;
284
285        let RebuildRanges {
286            new_offsets,
287            new_sizes,
288            starts,
289            lengths,
290            elements_len,
291        } = ranges;
292        let constant_length = lengths
293            .first()
294            .copied()
295            .filter(|first| lengths.iter().all(|length| *length == *first));
296        let lengths = match constant_length {
297            Some(length) => ConstantArray::new(length, starts.len()).into_array(),
298            None => lengths.into_array(),
299        };
300
301        // SAFETY: range starts and lengths are derived from valid ListView metadata; elements_len
302        // is the sum of all generated range lengths. Multiplier 1 preserves contiguous ranges.
303        let multipliers = ConstantArray::new(1u64, starts.len()).into_array();
304        let element_indices = unsafe {
305            PiecewiseSequenceArray::new_unchecked(
306                starts.into_array(),
307                lengths,
308                multipliers,
309                elements_len,
310            )
311        };
312        let elements = self.elements().take(element_indices.into_array())?;
313
314        // Built unsigned; reinterpret back to the signed-preserving result types.
315        let offsets = PrimitiveArray::new(new_offsets.freeze(), Validity::NonNullable)
316            .reinterpret_cast(new_offset_ptype)
317            .into_array();
318        let sizes = PrimitiveArray::new(new_sizes.freeze(), Validity::NonNullable)
319            .reinterpret_cast(size_ptype)
320            .into_array();
321
322        // SAFETY: offsets are sequential and non-overlapping, all (offset, size) pairs reference
323        // valid elements, and validity is preserved from the original array.
324        Ok(unsafe {
325            ListViewArray::new_unchecked(elements, offsets, sizes, validity)
326                .with_zero_copy_to_list(true)
327        })
328    }
329
330    /// Rebuilds a [`ListViewArray`] by trimming any unused / unreferenced leading and trailing
331    /// elements, which is defined as a contiguous run of values in the `elements` array that are
332    /// not referenced by any views in the corresponding [`ListViewArray`].
333    fn rebuild_trim_elements(&self, ctx: &mut ExecutionCtx) -> VortexResult<ListViewArray> {
334        let (start, end) = self.referenced_element_bounds(ctx)?;
335
336        // SAFETY: we calculated valid start and end bounds
337        unsafe { self.trim_elements(start, end) }
338    }
339
340    /// Unsafely trims `elements` to the referenced half-open range `[start, end)`, adjusting every offset
341    /// down by `start`. The result preserves the original `is_zero_copy_to_list` flag.
342    ///
343    /// # SAFETY
344    ///
345    /// `start` must be the minimum value of `offsets`, and end should be the maximum value of `offsets[i] + size[i]`
346    /// over all indices `i` of offsets. Otherwise, `offsets` and `sizes` may hold references to elements that no longer exist
347    /// and the array will be corrupted.
348    pub unsafe fn trim_elements(&self, start: usize, end: usize) -> VortexResult<ListViewArray> {
349        let adjusted_offsets = match_each_integer_ptype!(self.offsets().dtype().as_ptype(), |O| {
350            let offset = <O as FromPrimitive>::from_usize(start)
351                .vortex_expect("unable to convert the min offset `start` into a `usize`");
352            let scalar = Scalar::primitive(offset, Nullability::NonNullable);
353
354            self.offsets()
355                .clone()
356                .binary(
357                    ConstantArray::new(scalar, self.offsets().len()).into_array(),
358                    Operator::Sub,
359                )
360                .vortex_expect("was somehow unable to adjust offsets down by their minimum")
361        });
362
363        let sliced_elements = self.elements().slice(start..end)?;
364
365        // SAFETY: The only thing we changed was the elements (which we verify through mins and
366        // maxes that all adjusted offsets + sizes are within the correct bounds), so the parameters
367        // are valid. And if the original array was zero-copyable to list, trimming elements doesn't
368        // change that property.
369        Ok(unsafe {
370            ListViewArray::new_unchecked(
371                sliced_elements,
372                adjusted_offsets,
373                self.sizes().clone(),
374                self.validity()?,
375            )
376            .with_zero_copy_to_list(self.is_zero_copy_to_list())
377        })
378    }
379
380    fn rebuild_make_exact(&self, ctx: &mut ExecutionCtx) -> VortexResult<ListViewArray> {
381        if self.is_zero_copy_to_list() {
382            self.rebuild_trim_elements(ctx)
383        } else {
384            // When we completely rebuild the `ListViewArray`, we get the benefit that we also trim
385            // any leading and trailing garbage data.
386            self.rebuild_zero_copy_to_list(ctx)
387        }
388    }
389}
390
391struct RebuildRanges<NewOffset, S> {
392    new_offsets: BufferMut<NewOffset>,
393    new_sizes: BufferMut<S>,
394    starts: BufferMut<u64>,
395    lengths: BufferMut<u64>,
396    elements_len: usize,
397}
398
399fn rebuild_ranges<NewOffset, O, S>(
400    offsets: &[O],
401    sizes: &[S],
402    validity_mask: &Mask,
403) -> VortexResult<RebuildRanges<NewOffset, S>>
404where
405    NewOffset: IntegerPType,
406    O: IntegerPType,
407    S: IntegerPType,
408{
409    let len = offsets.len();
410    let mut new_offsets = BufferMut::<NewOffset>::with_capacity(len);
411    let mut new_sizes = BufferMut::<S>::with_capacity(len);
412    let mut starts = BufferMut::<u64>::with_capacity(len);
413    let mut lengths = BufferMut::<u64>::with_capacity(len);
414    let mut n_elements = NewOffset::zero();
415    let mut elements_len = 0usize;
416
417    for (index, is_valid) in validity_mask.iter().enumerate() {
418        if !is_valid {
419            new_offsets.push(n_elements);
420            new_sizes.push(S::zero());
421            starts.push(0);
422            lengths.push(0);
423            continue;
424        }
425
426        let size = sizes[index];
427        let start: usize = offsets[index].as_();
428        let length: usize = size.as_();
429        start.checked_add(length).ok_or_else(|| {
430            vortex_err!(
431                "ListView rebuild element range overflow for start {start} and length {length}"
432            )
433        })?;
434        elements_len = elements_len
435            .checked_add(length)
436            .ok_or_else(|| vortex_err!("ListView rebuild elements length overflow"))?;
437
438        new_offsets.push(n_elements);
439        new_sizes.push(size);
440        starts.push(start as u64);
441        lengths.push(length as u64);
442        n_elements += num_traits::cast(size).vortex_expect("Cast failed");
443    }
444
445    Ok(RebuildRanges {
446        new_offsets,
447        new_sizes,
448        starts,
449        lengths,
450        elements_len,
451    })
452}
453
454#[cfg(test)]
455mod tests {
456    use vortex_buffer::BitBuffer;
457    use vortex_error::VortexResult;
458
459    use super::super::tests::common::SESSION;
460    use super::ListViewRebuildMode;
461    use crate::IntoArray;
462    use crate::VortexSessionExecute;
463    use crate::arrays::ListViewArray;
464    use crate::arrays::PrimitiveArray;
465    use crate::arrays::listview::ListViewArrayExt;
466    use crate::arrays::listview::ListViewArraySlotsExt;
467    use crate::assert_arrays_eq;
468    use crate::dtype::Nullability;
469    use crate::validity::Validity;
470
471    #[test]
472    fn test_rebuild_flatten_removes_overlaps() -> VortexResult<()> {
473        // Create a list view with overlapping lists: [A, B, C]
474        // List 0: offset=0, size=3 -> [A, B, C]
475        // List 1: offset=1, size=2 -> [B, C] (overlaps with List 0)
476        let elements = PrimitiveArray::from_iter(vec![1i32, 2, 3]).into_array();
477        let offsets = PrimitiveArray::from_iter(vec![0u32, 1]).into_array();
478        let sizes = PrimitiveArray::from_iter(vec![3u32, 2]).into_array();
479
480        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
481
482        let mut ctx = SESSION.create_execution_ctx();
483        let flattened = listview.rebuild(ListViewRebuildMode::MakeZeroCopyToList, &mut ctx)?;
484
485        // After flatten: elements should be [A, B, C, B, C] = [1, 2, 3, 2, 3]
486        // Lists should be sequential with no overlaps
487        assert_eq!(flattened.elements().len(), 5);
488
489        // Offsets should be sequential
490        assert_eq!(flattened.offset_at(0), 0);
491        assert_eq!(flattened.size_at(0), 3);
492        assert_eq!(flattened.offset_at(1), 3);
493        assert_eq!(flattened.size_at(1), 2);
494
495        // Verify the data is correct
496        assert_arrays_eq!(
497            flattened.list_elements_at(0)?,
498            PrimitiveArray::from_iter([1i32, 2, 3]),
499            &mut ctx
500        );
501
502        assert_arrays_eq!(
503            flattened.list_elements_at(1)?,
504            PrimitiveArray::from_iter([2i32, 3]),
505            &mut ctx
506        );
507        Ok(())
508    }
509
510    #[test]
511    fn test_rebuild_flatten_with_nullable() -> VortexResult<()> {
512        use crate::arrays::BoolArray;
513
514        // Create a nullable list view with a null list
515        let elements = PrimitiveArray::from_iter(vec![1i32, 2, 3]).into_array();
516        let offsets = PrimitiveArray::from_iter(vec![0u32, 1, 2]).into_array();
517        let sizes = PrimitiveArray::from_iter(vec![2u32, 1, 1]).into_array();
518        let validity = Validity::Array(
519            BoolArray::new(
520                BitBuffer::from(vec![true, false, true]),
521                Validity::NonNullable,
522            )
523            .into_array(),
524        );
525
526        let listview = ListViewArray::new(elements, offsets, sizes, validity);
527
528        let mut ctx = SESSION.create_execution_ctx();
529        let flattened = listview.rebuild(ListViewRebuildMode::MakeZeroCopyToList, &mut ctx)?;
530
531        // Verify nullability is preserved
532        assert_eq!(flattened.dtype().nullability(), Nullability::Nullable);
533        assert!(flattened.validity()?.execute_is_valid(0, &mut ctx)?);
534        assert!(!flattened.validity()?.execute_is_valid(1, &mut ctx)?);
535        assert!(flattened.validity()?.execute_is_valid(2, &mut ctx)?);
536
537        // Verify valid lists contain correct data
538        assert_arrays_eq!(
539            flattened.list_elements_at(0)?,
540            PrimitiveArray::from_iter([1i32, 2]),
541            &mut ctx
542        );
543
544        assert_arrays_eq!(
545            flattened.list_elements_at(2)?,
546            PrimitiveArray::from_iter([3i32]),
547            &mut ctx
548        );
549        Ok(())
550    }
551
552    #[test]
553    fn test_rebuild_flatten_null_row_uses_valid_empty_range() -> VortexResult<()> {
554        let elements = PrimitiveArray::from_iter(vec![1i32, 2, 3, 4]).into_array();
555        let offsets = PrimitiveArray::from_iter(vec![0u32, 1, 2]).into_array();
556        let sizes = PrimitiveArray::from_iter(vec![1u32, 2, 2]).into_array();
557        let validity = Validity::from_iter([true, false, true]);
558
559        // SAFETY: all source ranges are valid, including the null row's non-empty range.
560        let listview = unsafe { ListViewArray::new_unchecked(elements, offsets, sizes, validity) };
561
562        let mut ctx = SESSION.create_execution_ctx();
563        let flattened = listview.rebuild(ListViewRebuildMode::MakeZeroCopyToList, &mut ctx)?;
564
565        assert_eq!(flattened.offset_at(0), 0);
566        assert_eq!(flattened.size_at(0), 1);
567        assert_eq!(flattened.offset_at(1), 1);
568        assert_eq!(flattened.size_at(1), 0);
569        assert_eq!(flattened.offset_at(2), 1);
570        assert_eq!(flattened.size_at(2), 2);
571        assert!(flattened.validity()?.execute_is_valid(0, &mut ctx)?);
572        assert!(!flattened.validity()?.execute_is_valid(1, &mut ctx)?);
573        assert!(flattened.validity()?.execute_is_valid(2, &mut ctx)?);
574
575        assert_arrays_eq!(
576            flattened.list_elements_at(0)?,
577            PrimitiveArray::from_iter([1i32]),
578            &mut ctx
579        );
580        assert_arrays_eq!(
581            flattened.list_elements_at(2)?,
582            PrimitiveArray::from_iter([3i32, 4]),
583            &mut ctx
584        );
585        Ok(())
586    }
587
588    #[test]
589    fn test_rebuild_trim_elements_basic() -> VortexResult<()> {
590        // Test trimming both leading and trailing unused elements while preserving gaps in the
591        // middle.
592        // Elements: [_, _, A, B, _, C, D, _, _]
593        //            0  1  2  3  4  5  6  7  8
594        // List 0: offset=2, size=2 -> [A, B]
595        // List 1: offset=5, size=2 -> [C, D]
596        // Should trim to: [A, B, _, C, D] with adjusted offsets.
597        let elements =
598            PrimitiveArray::from_iter(vec![99i32, 98, 1, 2, 97, 3, 4, 96, 95]).into_array();
599        let offsets = PrimitiveArray::from_iter(vec![2u32, 5]).into_array();
600        let sizes = PrimitiveArray::from_iter(vec![2u32, 2]).into_array();
601
602        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
603
604        let mut ctx = SESSION.create_execution_ctx();
605        let trimmed = listview.rebuild(ListViewRebuildMode::TrimElements, &mut ctx)?;
606
607        // After trimming: elements should be [A, B, _, C, D] = [1, 2, 97, 3, 4].
608        assert_eq!(trimmed.elements().len(), 5);
609
610        // Offsets should be adjusted: old offset 2 -> new offset 0, old offset 5 -> new offset 3.
611        assert_eq!(trimmed.offset_at(0), 0);
612        assert_eq!(trimmed.size_at(0), 2);
613        assert_eq!(trimmed.offset_at(1), 3);
614        assert_eq!(trimmed.size_at(1), 2);
615
616        // Verify the data is correct.
617        assert_arrays_eq!(
618            trimmed.list_elements_at(0)?,
619            PrimitiveArray::from_iter([1i32, 2]),
620            &mut ctx
621        );
622
623        assert_arrays_eq!(
624            trimmed.list_elements_at(1)?,
625            PrimitiveArray::from_iter([3i32, 4]),
626            &mut ctx
627        );
628
629        // Note that element at index 2 (97) is preserved as a gap.
630        let all_elements = trimmed
631            .elements()
632            .clone()
633            .execute::<PrimitiveArray>(&mut ctx)?;
634        assert_eq!(all_elements.execute_scalar(2, &mut ctx)?, 97i32.into());
635        Ok(())
636    }
637
638    #[test]
639    fn test_rebuild_flatten_large_lists_with_piecewise_indices() -> VortexResult<()> {
640        let elements = PrimitiveArray::from_iter(0i32..300).into_array();
641        let offsets = PrimitiveArray::from_iter(vec![10u32, 0]).into_array();
642        let sizes = PrimitiveArray::from_iter(vec![150u32, 130]).into_array();
643        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
644
645        let mut ctx = SESSION.create_execution_ctx();
646        let flattened = listview.rebuild(ListViewRebuildMode::MakeZeroCopyToList, &mut ctx)?;
647
648        assert_eq!(flattened.elements().len(), 280);
649        assert_eq!(flattened.offset_at(0), 0);
650        assert_eq!(flattened.size_at(0), 150);
651        assert_eq!(flattened.offset_at(1), 150);
652        assert_eq!(flattened.size_at(1), 130);
653
654        assert_arrays_eq!(
655            flattened.list_elements_at(0)?,
656            PrimitiveArray::from_iter(10i32..160),
657            &mut ctx
658        );
659        assert_arrays_eq!(
660            flattened.list_elements_at(1)?,
661            PrimitiveArray::from_iter(0i32..130),
662            &mut ctx
663        );
664        Ok(())
665    }
666
667    #[test]
668    fn test_rebuild_with_trailing_nulls_regression() -> VortexResult<()> {
669        // Regression test for issue #5412
670        // Tests that zero-copy-to-list arrays with trailing NULLs correctly calculate
671        // offsets for NULL items to maintain no-overlap invariant
672
673        // Create a ListViewArray with trailing NULLs
674        let elements = PrimitiveArray::from_iter(vec![1i32, 2, 3, 4]).into_array();
675        let offsets = PrimitiveArray::from_iter(vec![0u32, 2, 0, 0]).into_array();
676        let sizes = PrimitiveArray::from_iter(vec![2u32, 2, 0, 0]).into_array();
677        let validity = Validity::from_iter(vec![true, true, false, false]);
678
679        let listview = ListViewArray::new(elements, offsets, sizes, validity);
680
681        // First rebuild to make it zero-copy-to-list
682        let mut ctx = SESSION.create_execution_ctx();
683        let rebuilt = listview.rebuild(ListViewRebuildMode::MakeZeroCopyToList, &mut ctx)?;
684        assert!(rebuilt.is_zero_copy_to_list());
685
686        // Verify NULL items have correct offsets (should not reuse previous offsets)
687        // After rebuild: offsets should be [0, 2, 4, 4] for zero-copy-to-list
688        assert_eq!(rebuilt.offset_at(0), 0);
689        assert_eq!(rebuilt.offset_at(1), 2);
690        assert_eq!(rebuilt.offset_at(2), 4); // NULL should be at position 4
691        assert_eq!(rebuilt.offset_at(3), 4); // Second NULL also at position 4
692
693        // All sizes should be correct
694        assert_eq!(rebuilt.size_at(0), 2);
695        assert_eq!(rebuilt.size_at(1), 2);
696        assert_eq!(rebuilt.size_at(2), 0); // NULL has size 0
697        assert_eq!(rebuilt.size_at(3), 0); // NULL has size 0
698
699        // Now rebuild with MakeExact (which calls naive_rebuild then trim_elements)
700        // This should not panic (issue #5412)
701        let exact = rebuilt.rebuild(ListViewRebuildMode::MakeExact, &mut ctx)?;
702
703        // Verify the result is still valid
704        assert!(exact.is_valid(0, &mut ctx)?);
705        assert!(exact.is_valid(1, &mut ctx)?);
706        assert!(!exact.is_valid(2, &mut ctx)?);
707        assert!(!exact.is_valid(3, &mut ctx)?);
708
709        // Verify data is preserved
710        assert_arrays_eq!(
711            exact.list_elements_at(0)?,
712            PrimitiveArray::from_iter([1i32, 2]),
713            &mut ctx
714        );
715
716        assert_arrays_eq!(
717            exact.list_elements_at(1)?,
718            PrimitiveArray::from_iter([3i32, 4]),
719            &mut ctx
720        );
721        Ok(())
722    }
723
724    /// Regression test for <https://github.com/vortex-data/vortex/issues/6773>.
725    /// u32 offsets exceed u16::MAX, so u16 sizes are widened to u32 for the add.
726    #[test]
727    fn test_rebuild_trim_elements_offsets_wider_than_sizes() -> VortexResult<()> {
728        let mut elems = vec![0i32; 70_005];
729        elems[70_000] = 10;
730        elems[70_001] = 20;
731        elems[70_002] = 30;
732        elems[70_003] = 40;
733        let elements = PrimitiveArray::from_iter(elems).into_array();
734        let offsets = PrimitiveArray::from_iter(vec![70_000u32, 70_002]).into_array();
735        let sizes = PrimitiveArray::from_iter(vec![2u16, 2]).into_array();
736
737        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
738        let mut ctx = SESSION.create_execution_ctx();
739        let trimmed = listview.rebuild(ListViewRebuildMode::TrimElements, &mut ctx)?;
740        assert_arrays_eq!(
741            trimmed.list_elements_at(1)?,
742            PrimitiveArray::from_iter([30i32, 40]),
743            &mut ctx
744        );
745        Ok(())
746    }
747
748    /// Regression test for <https://github.com/vortex-data/vortex/issues/6773>.
749    /// u32 sizes exceed u16::MAX, so u16 offsets are widened to u32 for the add.
750    #[test]
751    fn test_rebuild_trim_elements_sizes_wider_than_offsets() -> VortexResult<()> {
752        let mut elems = vec![0i32; 70_001];
753        elems[3] = 30;
754        elems[4] = 40;
755        let elements = PrimitiveArray::from_iter(elems).into_array();
756        let offsets = PrimitiveArray::from_iter(vec![1u16, 3]).into_array();
757        let sizes = PrimitiveArray::from_iter(vec![70_000u32, 2]).into_array();
758
759        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
760        let mut ctx = SESSION.create_execution_ctx();
761        let trimmed = listview.rebuild(ListViewRebuildMode::TrimElements, &mut ctx)?;
762        assert_arrays_eq!(
763            trimmed.list_elements_at(1)?,
764            PrimitiveArray::from_iter([30i32, 40]),
765            &mut ctx
766        );
767        Ok(())
768    }
769
770    /// Rebuild with signed offsets/sizes: exercises the unsigned-reinterpret read path and asserts
771    /// the result offset/size dtypes preserve signedness (widened to >=32-bit for offsets).
772    #[test]
773    fn test_rebuild_preserves_signed_offset_and_size_types() -> VortexResult<()> {
774        use crate::arrays::listview::ListViewArraySlotsExt;
775        use crate::dtype::PType;
776
777        // Overlapping lists force an actual rebuild rather than the zero-copy fast path.
778        let elements = PrimitiveArray::from_iter(vec![1i32, 2, 3]).into_array();
779        let offsets = PrimitiveArray::from_iter(vec![0i32, 1]).into_array();
780        let sizes = PrimitiveArray::from_iter(vec![3i16, 2]).into_array();
781
782        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
783        let mut ctx = SESSION.create_execution_ctx();
784        let rebuilt = listview.rebuild(ListViewRebuildMode::MakeZeroCopyToList, &mut ctx)?;
785
786        // Values: [1,2,3] and [2,3].
787        assert_arrays_eq!(
788            rebuilt.list_elements_at(0)?,
789            PrimitiveArray::from_iter([1i32, 2, 3]),
790            &mut ctx
791        );
792        assert_arrays_eq!(
793            rebuilt.list_elements_at(1)?,
794            PrimitiveArray::from_iter([2i32, 3]),
795            &mut ctx
796        );
797
798        // Signed input -> signed result (offsets widened to i32, sizes kept i16).
799        assert_eq!(rebuilt.offsets().dtype().as_ptype(), PType::I32);
800        assert_eq!(rebuilt.sizes().dtype().as_ptype(), PType::I16);
801        Ok(())
802    }
803
804    // ── should_use_take heuristic tests ────────────────────────────────────
805
806    #[test]
807    fn heuristic_zero_lists_uses_take() {
808        assert!(ListViewArray::should_use_take(0, 0));
809    }
810
811    #[test]
812    fn heuristic_small_lists_use_take() {
813        // avg = 127 → take
814        assert!(ListViewArray::should_use_take(127_000, 1_000));
815        // avg = 128 → LBL
816        assert!(!ListViewArray::should_use_take(128_000, 1_000));
817    }
818
819    /// Regression test for <https://github.com/vortex-data/vortex/issues/6973>.
820    /// Both offsets and sizes are u8, and offset + size exceeds u8::MAX.
821    #[test]
822    fn test_rebuild_trim_elements_sum_overflows_type() -> VortexResult<()> {
823        let elements = PrimitiveArray::from_iter(vec![0i32; 261]).into_array();
824        let offsets = PrimitiveArray::from_iter(vec![215u8, 0]).into_array();
825        let sizes = PrimitiveArray::from_iter(vec![46u8, 10]).into_array();
826
827        let listview = ListViewArray::new(elements, offsets, sizes, Validity::NonNullable);
828        let mut ctx = SESSION.create_execution_ctx();
829        let trimmed = listview.rebuild(ListViewRebuildMode::TrimElements, &mut ctx)?;
830
831        // min(offsets) = 0, so nothing to trim; output should equal input.
832        assert_arrays_eq!(trimmed, listview, &mut ctx);
833        Ok(())
834    }
835}