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