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