Skip to main content

vortex_array/arrays/listview/
conversion.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexExpect;
5use vortex_error::VortexResult;
6
7use crate::ArrayRef;
8use crate::Canonical;
9use crate::ExecutionCtx;
10use crate::IntoArray;
11use crate::ToCanonical;
12use crate::arrays::ExtensionArray;
13use crate::arrays::FixedSizeListArray;
14use crate::arrays::ListArray;
15use crate::arrays::ListViewArray;
16use crate::arrays::PrimitiveArray;
17use crate::arrays::StructArray;
18use crate::arrays::extension::ExtensionArrayExt;
19use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
20use crate::arrays::list::ListArrayExt;
21use crate::arrays::listview::ListViewArrayExt;
22use crate::arrays::listview::ListViewRebuildMode;
23use crate::arrays::struct_::StructArrayExt;
24use crate::builders::PrimitiveBuilder;
25use crate::dtype::IntegerPType;
26use crate::dtype::Nullability;
27use crate::match_each_integer_ptype;
28
29/// Creates a `ListViewArray` from a `ListArray` by computing `sizes` from `offsets`.
30///
31/// The output `ListViewArray` will be zero-copyable back to a `ListArray`, and additionally it
32/// will not have any leading or trailing garbage data.
33pub fn list_view_from_list(list: ListArray, ctx: &mut ExecutionCtx) -> VortexResult<ListViewArray> {
34    // If the list is empty, create an empty `ListViewArray` with the same offset `DType` as the
35    // input.
36    if list.is_empty() {
37        return Ok(Canonical::empty(list.dtype()).into_listview());
38    }
39
40    // We reset the offsets here because mostly for convenience, and also because callers of this
41    // function might not expect the output `ListViewArray` to have a bunch of leading and trailing
42    // garbage data when they turn it back into a `ListArray`.
43    let list = list.reset_offsets(false).vortex_expect("This can't fail");
44
45    let list_offsets = list.offsets().clone();
46
47    // Create `sizes` array by computing differences between consecutive offsets.
48    // We use the same `DType` for the sizes as the `offsets` array to ensure compatibility.
49    let sizes = match_each_integer_ptype!(list_offsets.dtype().as_ptype(), |O| {
50        build_sizes_from_offsets::<O>(&list, ctx)?
51    });
52
53    // We need to slice the `offsets` to remove the last element (`ListArray` has `n + 1` offsets).
54    debug_assert_eq!(list_offsets.len(), list.len() + 1);
55    let adjusted_offsets = list_offsets.slice(0..list.len())?;
56
57    // SAFETY: Since everything came from an existing valid `ListArray`, and the `sizes` were
58    // derived from valid and in-order `offsets`, we know these fields are valid.
59    // We also just came directly from a `ListArray`, so we know this is zero-copyable.
60    Ok(unsafe {
61        ListViewArray::new_unchecked(
62            list.elements().clone(),
63            adjusted_offsets,
64            sizes,
65            list.validity()?,
66        )
67        .with_zero_copy_to_list(true)
68    })
69}
70
71/// Builds a sizes array from a `ListArray` by computing differences between consecutive offsets.
72fn build_sizes_from_offsets<O: IntegerPType>(
73    list: &ListArray,
74    ctx: &mut ExecutionCtx,
75) -> VortexResult<ArrayRef> {
76    let len = list.len();
77    let mut sizes_builder = PrimitiveBuilder::<O>::with_capacity(Nullability::NonNullable, len);
78
79    // Create `UninitRange` for direct memory access.
80    let mut sizes_range = sizes_builder.uninit_range(len);
81
82    let offsets = list.offsets().clone().execute::<PrimitiveArray>(ctx)?;
83    let offsets_slice = offsets.as_slice::<O>();
84    debug_assert_eq!(len + 1, offsets_slice.len());
85    debug_assert!(offsets_slice.is_sorted());
86
87    // Compute sizes as the difference between consecutive offsets.
88    for i in 0..len {
89        let size = offsets_slice[i + 1] - offsets_slice[i];
90        sizes_range.set_value(i, size);
91    }
92
93    // SAFETY: We have initialized all values in the range.
94    unsafe {
95        sizes_range.finish();
96    }
97
98    Ok(sizes_builder.finish_into_primitive().into_array())
99}
100
101// TODO(connor)[ListView]: Note that it is not exactly zero-copy because we have to add a single
102// offset at the end, but it is fast enough.
103/// Creates a `ListArray` from a `ListViewArray`. The resulting `ListArray` will not have any
104/// leading or trailing garbage data.
105///
106/// If `ListViewArray::is_zero_copy_to_list` is `true`, then this operation is fast
107///
108/// Otherwise, this function fall back to the (very) expensive path and will rebuild the
109/// `ListArray` from scratch.
110pub fn list_from_list_view(list_view: ListViewArray) -> VortexResult<ListArray> {
111    // Rebuild as zero-copyable to list array and also trim all leading and trailing elements.
112    let zctl_array = list_view.rebuild(ListViewRebuildMode::MakeExact)?;
113    debug_assert!(zctl_array.is_zero_copy_to_list());
114
115    let list_offsets = match_each_integer_ptype!(zctl_array.offsets().dtype().as_ptype(), |O| {
116        // SAFETY: We just made the array zero-copyable to `ListArray`, so the safety contract is
117        // upheld.
118        unsafe { build_list_offsets_from_list_view::<O>(&zctl_array) }
119    });
120
121    // SAFETY: Because the shape of the `ListViewArray` is zero-copyable to a `ListArray`, we
122    // can simply reuse all of the data (besides the offsets). We also trim all of the elements to
123    // make it easier for the caller to use the `ListArray`.
124    Ok(unsafe {
125        ListArray::new_unchecked(
126            zctl_array.elements().clone(),
127            list_offsets,
128            zctl_array.validity()?,
129        )
130    })
131}
132
133// TODO(connor)[ListView]: We can optimize this by always keeping extra memory in `ListViewArray`
134// offsets for an `n+1`th offset.
135/// Builds a `ListArray` offsets array from a `ListViewArray` by constructing `n+1` offsets.
136/// The last offset is computed as `last_offset + last_size`.
137///
138/// # Safety
139///
140/// The `ListViewArray` must have offsets that are sorted, and every size must be equal to the gap
141/// between `offset[i]` and `offset[i + 1]`.
142unsafe fn build_list_offsets_from_list_view<O: IntegerPType>(
143    list_view: &ListViewArray,
144) -> ArrayRef {
145    let len = list_view.len();
146    let mut offsets_builder =
147        PrimitiveBuilder::<O>::with_capacity(Nullability::NonNullable, len + 1);
148
149    // Create uninit range for direct memory access.
150    let mut offsets_range = offsets_builder.uninit_range(len + 1);
151
152    let offsets = list_view.offsets().to_primitive();
153    let offsets_slice = offsets.as_slice::<O>();
154    debug_assert!(offsets_slice.is_sorted());
155
156    // Copy the existing n offsets.
157    offsets_range.copy_from_slice(0, offsets_slice);
158
159    // Append the final offset (last offset + last size).
160    let final_offset = if len != 0 {
161        let last_offset = offsets_slice[len - 1];
162
163        let last_size = list_view.size_at(len - 1);
164        let last_size =
165            O::from_usize(last_size).vortex_expect("size somehow did not fit into offsets");
166
167        last_offset + last_size
168    } else {
169        O::zero()
170    };
171
172    offsets_range.set_value(len, final_offset);
173
174    // SAFETY: We have initialized all values in the range.
175    unsafe {
176        offsets_range.finish();
177    }
178
179    offsets_builder.finish_into_primitive().into_array()
180}
181
182/// Recursively converts all `ListViewArray`s to `ListArray`s in a nested array structure.
183///
184/// The conversion happens bottom-up, processing children before parents.
185pub fn recursive_list_from_list_view(array: ArrayRef) -> VortexResult<ArrayRef> {
186    if !array.dtype().is_nested() {
187        return Ok(array);
188    }
189
190    let canonical = array.to_canonical()?;
191
192    Ok(match canonical {
193        Canonical::List(listview) => {
194            let converted_elements = recursive_list_from_list_view(listview.elements().clone())?;
195            debug_assert_eq!(converted_elements.len(), listview.elements().len());
196
197            // Avoid cloning if elements didn't change.
198            let listview_with_converted_elements =
199                if !ArrayRef::ptr_eq(&converted_elements, listview.elements()) {
200                    // SAFETY: We are effectively just replacing the child elements array, which
201                    // must have the same length, so all invariants are maintained.
202                    unsafe {
203                        ListViewArray::new_unchecked(
204                            converted_elements,
205                            listview.offsets().clone(),
206                            listview.sizes().clone(),
207                            listview.validity()?,
208                        )
209                        .with_zero_copy_to_list(listview.is_zero_copy_to_list())
210                    }
211                } else {
212                    listview
213                };
214
215            // Make the conversion to `ListArray`.
216            let list_array = list_from_list_view(listview_with_converted_elements)?;
217            list_array.into_array()
218        }
219        Canonical::FixedSizeList(fixed_size_list) => {
220            let converted_elements =
221                recursive_list_from_list_view(fixed_size_list.elements().clone())?;
222
223            // Avoid cloning if elements didn't change.
224            if !ArrayRef::ptr_eq(&converted_elements, fixed_size_list.elements()) {
225                FixedSizeListArray::try_new(
226                    converted_elements,
227                    fixed_size_list.list_size(),
228                    fixed_size_list.validity()?,
229                    fixed_size_list.len(),
230                )
231                .vortex_expect(
232                    "FixedSizeListArray reconstruction should not fail with valid components",
233                )
234                .into_array()
235            } else {
236                fixed_size_list.into_array()
237            }
238        }
239        Canonical::Struct(struct_array) => {
240            let fields = struct_array.unmasked_fields();
241            let mut converted_fields = Vec::with_capacity(fields.len());
242            let mut any_changed = false;
243
244            for field in fields.iter() {
245                let converted_field = recursive_list_from_list_view(field.clone())?;
246                // Avoid cloning if elements didn't change.
247                any_changed |= !ArrayRef::ptr_eq(&converted_field, field);
248                converted_fields.push(converted_field);
249            }
250
251            if any_changed {
252                StructArray::try_new(
253                    struct_array.names().clone(),
254                    converted_fields,
255                    struct_array.len(),
256                    struct_array.validity()?,
257                )
258                .vortex_expect("StructArray reconstruction should not fail with valid components")
259                .into_array()
260            } else {
261                struct_array.into_array()
262            }
263        }
264        Canonical::Extension(ext_array) => {
265            let converted_storage =
266                recursive_list_from_list_view(ext_array.storage_array().clone())?;
267
268            // Avoid cloning if elements didn't change.
269            if !ArrayRef::ptr_eq(&converted_storage, ext_array.storage_array()) {
270                ExtensionArray::new(ext_array.ext_dtype().clone(), converted_storage).into_array()
271            } else {
272                ext_array.into_array()
273            }
274        }
275        _ => unreachable!(),
276    })
277}
278
279#[cfg(test)]
280mod tests {
281
282    use vortex_buffer::buffer;
283    use vortex_error::VortexExpect;
284    use vortex_error::VortexResult;
285
286    use super::super::tests::common::create_basic_listview;
287    use super::super::tests::common::create_empty_lists_listview;
288    use super::super::tests::common::create_nullable_listview;
289    use super::super::tests::common::create_overlapping_listview;
290    use super::recursive_list_from_list_view;
291    use crate::ArrayEq;
292    use crate::ArrayRef;
293    use crate::IntoArray;
294    use crate::LEGACY_SESSION;
295    use crate::Precision;
296    use crate::VortexSessionExecute;
297    use crate::arrays::BoolArray;
298    use crate::arrays::FixedSizeListArray;
299    use crate::arrays::ListArray;
300    use crate::arrays::ListViewArray;
301    use crate::arrays::PrimitiveArray;
302    use crate::arrays::StructArray;
303    use crate::arrays::VarBinViewArray;
304    use crate::arrays::list::ListArrayExt;
305    use crate::arrays::listview::ListViewArrayExt;
306    use crate::arrays::listview::list_from_list_view;
307    use crate::arrays::listview::list_view_from_list;
308    use crate::assert_arrays_eq;
309    use crate::dtype::FieldNames;
310    use crate::validity::Validity;
311
312    #[test]
313    fn test_list_to_listview_basic() -> VortexResult<()> {
314        // Create a basic ListArray: [[0,1,2], [3,4], [5,6], [7,8,9]].
315        let elements = buffer![0i32, 1, 2, 3, 4, 5, 6, 7, 8, 9].into_array();
316        let offsets = buffer![0u32, 3, 5, 7, 10].into_array();
317        let list_array = ListArray::try_new(elements.clone(), offsets, Validity::NonNullable)?;
318
319        let mut ctx = LEGACY_SESSION.create_execution_ctx();
320        let list_view = list_view_from_list(list_array.clone(), &mut ctx)?;
321
322        // Verify structure.
323        assert_eq!(list_view.len(), 4);
324        assert_arrays_eq!(elements, list_view.elements().clone());
325
326        // Verify offsets (should be same but without last element).
327        let expected_offsets = buffer![0u32, 3, 5, 7].into_array();
328        assert_arrays_eq!(expected_offsets, list_view.offsets().clone());
329
330        // Verify sizes.
331        let expected_sizes = buffer![3u32, 2, 2, 3].into_array();
332        assert_arrays_eq!(expected_sizes, list_view.sizes().clone());
333
334        // Verify data integrity.
335        assert_arrays_eq!(list_array, list_view);
336        Ok(())
337    }
338
339    #[test]
340    fn test_listview_to_list_zero_copy() -> VortexResult<()> {
341        let list_view = create_basic_listview();
342        let list_array = list_from_list_view(list_view.clone())?;
343
344        // Should have same elements.
345        assert_arrays_eq!(list_view.elements().clone(), list_array.elements().clone());
346
347        // ListArray offsets should have n+1 elements for n lists (add the final offset).
348        // Check that the first n offsets match.
349        let list_array_offsets_without_last = list_array.offsets().slice(0..list_view.len())?;
350        assert_arrays_eq!(list_view.offsets().clone(), list_array_offsets_without_last);
351
352        // Verify data integrity.
353        assert_arrays_eq!(list_view, list_array);
354        Ok(())
355    }
356
357    #[test]
358    fn test_empty_array_conversions() -> VortexResult<()> {
359        // Empty ListArray to ListViewArray.
360        let empty_elements = PrimitiveArray::from_iter::<[i32; 0]>([]).into_array();
361        let empty_offsets = buffer![0u32].into_array();
362        let empty_list = ListArray::try_new(empty_elements, empty_offsets, Validity::NonNullable)?;
363
364        // This conversion will create an empty ListViewArray.
365        // Note: list_view_from_list handles the empty case specially.
366        let mut ctx = LEGACY_SESSION.create_execution_ctx();
367        let empty_list_view = list_view_from_list(empty_list.clone(), &mut ctx)?;
368        assert_eq!(empty_list_view.len(), 0);
369
370        // Convert back.
371        let converted_back = list_from_list_view(empty_list_view)?;
372        assert_eq!(converted_back.len(), 0);
373        // For empty arrays, we can't use assert_arrays_eq directly since the offsets might differ.
374        // Just check that it's empty.
375        assert_eq!(empty_list.len(), converted_back.len());
376        Ok(())
377    }
378
379    #[test]
380    fn test_nullable_conversions() -> VortexResult<()> {
381        // Create nullable ListArray: [[10,20], null, [50]].
382        let elements = buffer![10i32, 20, 30, 40, 50].into_array();
383        let offsets = buffer![0u32, 2, 4, 5].into_array();
384        let validity = Validity::Array(BoolArray::from_iter(vec![true, false, true]).into_array());
385        let nullable_list = ListArray::try_new(elements, offsets, validity.clone())?;
386
387        let mut ctx = LEGACY_SESSION.create_execution_ctx();
388        let nullable_list_view = list_view_from_list(nullable_list.clone(), &mut ctx)?;
389
390        // Verify validity is preserved.
391        assert!(
392            nullable_list_view
393                .validity()
394                .vortex_expect("listview validity should be derivable")
395                .array_eq(&validity, Precision::Ptr)
396        );
397        assert_eq!(nullable_list_view.len(), 3);
398
399        // Round-trip conversion.
400        let converted_back = list_from_list_view(nullable_list_view)?;
401        assert_arrays_eq!(nullable_list, converted_back);
402        Ok(())
403    }
404
405    #[test]
406    fn test_non_zero_copy_listview_to_list() -> VortexResult<()> {
407        // Create ListViewArray with overlapping lists (not zero-copyable).
408        let list_view = create_overlapping_listview();
409        let list_array = list_from_list_view(list_view.clone())?;
410
411        // The resulting ListArray should have monotonic offsets.
412        for i in 0..list_array.len() {
413            let start = list_array.offset_at(i)?;
414            let end = list_array.offset_at(i + 1)?;
415            assert!(end >= start, "Offsets should be monotonic after conversion");
416        }
417
418        // The data should still be correct even though it required a rebuild.
419        assert_arrays_eq!(list_view, list_array);
420        Ok(())
421    }
422
423    #[test]
424    fn test_empty_sublists() -> VortexResult<()> {
425        let empty_lists_view = create_empty_lists_listview();
426
427        // Convert to ListArray.
428        let list_array = list_from_list_view(empty_lists_view.clone())?;
429        assert_eq!(list_array.len(), 4);
430
431        // All sublists should be empty.
432        for i in 0..list_array.len() {
433            assert_eq!(list_array.list_elements_at(i)?.len(), 0);
434        }
435
436        // Round-trip.
437        let mut ctx = LEGACY_SESSION.create_execution_ctx();
438        let converted_back = list_view_from_list(list_array, &mut ctx)?;
439        assert_arrays_eq!(empty_lists_view, converted_back);
440        Ok(())
441    }
442
443    #[test]
444    fn test_different_offset_types() -> VortexResult<()> {
445        // Test with i32 offsets.
446        let elements = buffer![1i32, 2, 3, 4, 5].into_array();
447        let i32_offsets = buffer![0i32, 2, 5].into_array();
448        let list_i32 =
449            ListArray::try_new(elements.clone(), i32_offsets.clone(), Validity::NonNullable)?;
450
451        let mut ctx = LEGACY_SESSION.create_execution_ctx();
452        let list_view_i32 = list_view_from_list(list_i32.clone(), &mut ctx)?;
453        assert_eq!(list_view_i32.offsets().dtype(), i32_offsets.dtype());
454        assert_eq!(list_view_i32.sizes().dtype(), i32_offsets.dtype());
455
456        // Test with i64 offsets.
457        let i64_offsets = buffer![0i64, 2, 5].into_array();
458        let list_i64 = ListArray::try_new(elements, i64_offsets.clone(), Validity::NonNullable)?;
459
460        let list_view_i64 = list_view_from_list(list_i64.clone(), &mut ctx)?;
461        assert_eq!(list_view_i64.offsets().dtype(), i64_offsets.dtype());
462        assert_eq!(list_view_i64.sizes().dtype(), i64_offsets.dtype());
463
464        // Verify data integrity.
465        assert_arrays_eq!(list_i32, list_view_i32);
466        assert_arrays_eq!(list_i64, list_view_i64);
467        Ok(())
468    }
469
470    #[test]
471    fn test_round_trip_conversions() -> VortexResult<()> {
472        let mut ctx = LEGACY_SESSION.create_execution_ctx();
473
474        // Test 1: Basic round-trip.
475        let original = create_basic_listview();
476        let to_list = list_from_list_view(original.clone())?;
477        let back_to_view = list_view_from_list(to_list, &mut ctx)?;
478        assert_arrays_eq!(original, back_to_view);
479
480        // Test 2: Nullable round-trip.
481        let nullable = create_nullable_listview();
482        let nullable_to_list = list_from_list_view(nullable.clone())?;
483        let nullable_back = list_view_from_list(nullable_to_list, &mut ctx)?;
484        assert_arrays_eq!(nullable, nullable_back);
485
486        // Test 3: Non-zero-copyable round-trip.
487        let overlapping = create_overlapping_listview();
488
489        let overlapping_to_list = list_from_list_view(overlapping.clone())?;
490        let overlapping_back = list_view_from_list(overlapping_to_list, &mut ctx)?;
491        assert_arrays_eq!(overlapping, overlapping_back);
492        Ok(())
493    }
494
495    #[test]
496    fn test_single_element_lists() -> VortexResult<()> {
497        // Create lists with single elements: [[100], [200], [300]].
498        let elements = buffer![100i32, 200, 300].into_array();
499        let offsets = buffer![0u32, 1, 2, 3].into_array();
500        let single_elem_list = ListArray::try_new(elements, offsets, Validity::NonNullable)?;
501
502        let mut ctx = LEGACY_SESSION.create_execution_ctx();
503        let list_view = list_view_from_list(single_elem_list.clone(), &mut ctx)?;
504        assert_eq!(list_view.len(), 3);
505
506        // Verify sizes are all 1.
507        let expected_sizes = buffer![1u32, 1, 1].into_array();
508        assert_arrays_eq!(expected_sizes, list_view.sizes().clone());
509
510        // Round-trip.
511        let converted_back = list_from_list_view(list_view)?;
512        assert_arrays_eq!(single_elem_list, converted_back);
513        Ok(())
514    }
515
516    #[test]
517    fn test_mixed_empty_and_non_empty_lists() -> VortexResult<()> {
518        // Create: [[1,2], [], [3], [], [4,5,6]].
519        let elements = buffer![1i32, 2, 3, 4, 5, 6].into_array();
520        let offsets = buffer![0u32, 2, 2, 3, 3, 6].into_array();
521        let mixed_list = ListArray::try_new(elements, offsets, Validity::NonNullable)?;
522
523        let mut ctx = LEGACY_SESSION.create_execution_ctx();
524        let list_view = list_view_from_list(mixed_list.clone(), &mut ctx)?;
525        assert_eq!(list_view.len(), 5);
526
527        // Verify sizes.
528        let expected_sizes = buffer![2u32, 0, 1, 0, 3].into_array();
529        assert_arrays_eq!(expected_sizes, list_view.sizes().clone());
530
531        // Round-trip.
532        let converted_back = list_from_list_view(list_view)?;
533        assert_arrays_eq!(mixed_list, converted_back);
534        Ok(())
535    }
536
537    #[test]
538    fn test_recursive_simple_listview() -> VortexResult<()> {
539        let list_view = create_basic_listview();
540        let result = recursive_list_from_list_view(list_view.clone().into_array())?;
541
542        assert_eq!(result.len(), list_view.len());
543        assert_arrays_eq!(list_view.into_array(), result);
544        Ok(())
545    }
546
547    #[test]
548    fn test_recursive_nested_listview() -> VortexResult<()> {
549        let inner_elements = buffer![1i32, 2, 3].into_array();
550        let inner_offsets = buffer![0u32, 2].into_array();
551        let inner_sizes = buffer![2u32, 1].into_array();
552        let inner_listview = unsafe {
553            ListViewArray::new_unchecked(
554                inner_elements,
555                inner_offsets,
556                inner_sizes,
557                Validity::NonNullable,
558            )
559            .with_zero_copy_to_list(true)
560        };
561
562        let outer_offsets = buffer![0u32, 1].into_array();
563        let outer_sizes = buffer![1u32, 1].into_array();
564        let outer_listview = unsafe {
565            ListViewArray::new_unchecked(
566                inner_listview.into_array(),
567                outer_offsets,
568                outer_sizes,
569                Validity::NonNullable,
570            )
571            .with_zero_copy_to_list(true)
572        };
573
574        let result = recursive_list_from_list_view(outer_listview.clone().into_array())?;
575
576        assert_eq!(result.len(), 2);
577        assert_arrays_eq!(outer_listview.into_array(), result);
578        Ok(())
579    }
580
581    #[test]
582    fn test_recursive_struct_with_listview_fields() -> VortexResult<()> {
583        let listview_field = create_basic_listview().into_array();
584        let primitive_field = buffer![10i32, 20, 30, 40].into_array();
585
586        let struct_array = StructArray::try_new(
587            FieldNames::from(["lists", "values"]),
588            vec![listview_field, primitive_field],
589            4,
590            Validity::NonNullable,
591        )?;
592
593        let result = recursive_list_from_list_view(struct_array.clone().into_array())?;
594
595        assert_eq!(result.len(), 4);
596        assert_arrays_eq!(struct_array.into_array(), result);
597        Ok(())
598    }
599
600    #[test]
601    fn test_recursive_fixed_size_list_with_listview_elements() -> VortexResult<()> {
602        let lv1_elements = buffer![1i32, 2].into_array();
603        let lv1_offsets = buffer![0u32].into_array();
604        let lv1_sizes = buffer![2u32].into_array();
605        let lv1 = unsafe {
606            ListViewArray::new_unchecked(
607                lv1_elements,
608                lv1_offsets,
609                lv1_sizes,
610                Validity::NonNullable,
611            )
612            .with_zero_copy_to_list(true)
613        };
614
615        let lv2_elements = buffer![3i32, 4].into_array();
616        let lv2_offsets = buffer![0u32].into_array();
617        let lv2_sizes = buffer![2u32].into_array();
618        let lv2 = unsafe {
619            ListViewArray::new_unchecked(
620                lv2_elements,
621                lv2_offsets,
622                lv2_sizes,
623                Validity::NonNullable,
624            )
625            .with_zero_copy_to_list(true)
626        };
627
628        let dtype = lv1.dtype().clone();
629        let chunked_listviews =
630            crate::arrays::ChunkedArray::try_new(vec![lv1.into_array(), lv2.into_array()], dtype)?;
631
632        let fixed_list =
633            FixedSizeListArray::new(chunked_listviews.into_array(), 1, Validity::NonNullable, 2);
634
635        let result = recursive_list_from_list_view(fixed_list.clone().into_array())?;
636
637        assert_eq!(result.len(), 2);
638        assert_arrays_eq!(fixed_list.into_array(), result);
639        Ok(())
640    }
641
642    #[test]
643    fn test_recursive_deep_nesting() -> VortexResult<()> {
644        let innermost_elements = buffer![1i32, 2, 3].into_array();
645        let innermost_offsets = buffer![0u32, 2].into_array();
646        let innermost_sizes = buffer![2u32, 1].into_array();
647        let innermost_listview = unsafe {
648            ListViewArray::new_unchecked(
649                innermost_elements,
650                innermost_offsets,
651                innermost_sizes,
652                Validity::NonNullable,
653            )
654            .with_zero_copy_to_list(true)
655        };
656
657        let struct_array = StructArray::try_new(
658            FieldNames::from(["inner_lists"]),
659            vec![innermost_listview.into_array()],
660            2,
661            Validity::NonNullable,
662        )?;
663
664        let outer_offsets = buffer![0u32, 1].into_array();
665        let outer_sizes = buffer![1u32, 1].into_array();
666        let outer_listview = unsafe {
667            ListViewArray::new_unchecked(
668                struct_array.into_array(),
669                outer_offsets,
670                outer_sizes,
671                Validity::NonNullable,
672            )
673            .with_zero_copy_to_list(true)
674        };
675
676        let result = recursive_list_from_list_view(outer_listview.clone().into_array())?;
677
678        assert_eq!(result.len(), 2);
679        assert_arrays_eq!(outer_listview.into_array(), result);
680        Ok(())
681    }
682
683    #[test]
684    fn test_recursive_primitive_unchanged() -> VortexResult<()> {
685        let prim = buffer![1i32, 2, 3].into_array();
686        let prim_clone = prim.clone();
687        let result = recursive_list_from_list_view(prim)?;
688
689        assert!(ArrayRef::ptr_eq(&result, &prim_clone));
690        Ok(())
691    }
692
693    #[test]
694    fn test_recursive_mixed_listview_and_list() -> VortexResult<()> {
695        let listview = create_basic_listview();
696        let list = list_from_list_view(listview.clone())?;
697
698        let struct_array = StructArray::try_new(
699            FieldNames::from(["listview_field", "list_field"]),
700            vec![listview.into_array(), list.into_array()],
701            4,
702            Validity::NonNullable,
703        )?;
704
705        let result = recursive_list_from_list_view(struct_array.clone().into_array())?;
706
707        assert_eq!(result.len(), 4);
708        assert_arrays_eq!(struct_array.into_array(), result);
709        Ok(())
710    }
711
712    /// Regression test for <https://github.com/vortex-data/vortex/issues/6882>.
713    ///
714    /// An empty `ListViewArray` constructed via `try_new` has `is_zero_copy_to_list: false`.
715    /// `list_from_list_view` should still succeed because empty arrays are trivially
716    /// zero-copyable.
717    #[test]
718    fn test_empty_listview_to_list_without_zctl_flag() -> VortexResult<()> {
719        let elements = VarBinViewArray::from_iter_str(Vec::<&str>::new()).into_array();
720        let offsets = PrimitiveArray::from_iter(Vec::<i16>::new()).into_array();
721        let sizes = PrimitiveArray::from_iter(Vec::<i16>::new()).into_array();
722        let list_view = ListViewArray::try_new(elements, offsets, sizes, Validity::AllValid)?;
723
724        // `try_new` sets `is_zero_copy_to_list: false`.
725        assert!(!list_view.is_zero_copy_to_list());
726
727        let list_array = list_from_list_view(list_view)?;
728        assert_eq!(list_array.len(), 0);
729        Ok(())
730    }
731}