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