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