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