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