Skip to main content

vortex_array/arrays/list/compute/
take.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::IntoArray;
9use crate::array::ArrayView;
10use crate::arrays::List;
11use crate::arrays::ListArray;
12use crate::arrays::Primitive;
13use crate::arrays::PrimitiveArray;
14use crate::arrays::dict::TakeExecute;
15use crate::arrays::list::ListArrayExt;
16use crate::arrays::primitive::PrimitiveArrayExt;
17use crate::builders::ArrayBuilder;
18use crate::builders::PrimitiveBuilder;
19use crate::dtype::IntegerPType;
20use crate::dtype::Nullability;
21use crate::executor::ExecutionCtx;
22use crate::match_each_unsigned_integer_ptype;
23use crate::match_smallest_offset_type;
24
25// TODO(connor)[ListView]: Re-revert to the version where we simply convert to a `ListView` and call
26// the `ListView::take` compute function once `ListView` is more stable.
27
28impl TakeExecute for List {
29    /// Take implementation for [`ListArray`].
30    ///
31    /// Unlike `ListView`, `ListArray` must rebuild the elements array to maintain its invariant
32    /// that lists are stored contiguously and in-order (`offset[i+1] >= offset[i]`). Taking
33    /// non-contiguous indices would violate this requirement.
34    #[expect(clippy::cognitive_complexity)]
35    fn take(
36        array: ArrayView<'_, List>,
37        indices: &ArrayRef,
38        ctx: &mut ExecutionCtx,
39    ) -> VortexResult<Option<ArrayRef>> {
40        let indices = indices.clone().execute::<PrimitiveArray>(ctx)?;
41        let indices = indices.reinterpret_cast(indices.ptype().to_unsigned());
42        let offsets = array.offsets().clone().execute::<PrimitiveArray>(ctx)?;
43        let offsets = offsets.reinterpret_cast(offsets.ptype().to_unsigned());
44        // This is an over-approximation of the total number of elements in the resulting array.
45        let total_approx = array.elements().len().saturating_mul(indices.len());
46
47        match_each_unsigned_integer_ptype!(offsets.ptype(), |O| {
48            match_each_unsigned_integer_ptype!(indices.ptype(), |I| {
49                match_smallest_offset_type!(total_approx, |OutputOffsetType| {
50                    _take::<I, O, OutputOffsetType>(
51                        array,
52                        offsets.as_view(),
53                        indices.as_view(),
54                        ctx,
55                    )
56                    .map(Some)
57                })
58            })
59        })
60    }
61}
62
63fn _take<I: IntegerPType, O: IntegerPType, OutputOffsetType: IntegerPType>(
64    array: ArrayView<'_, List>,
65    offsets_array: ArrayView<'_, Primitive>,
66    indices_array: ArrayView<'_, Primitive>,
67    ctx: &mut ExecutionCtx,
68) -> VortexResult<ArrayRef> {
69    let data_validity = array
70        .list_validity()
71        .execute_mask(array.as_ref().len(), ctx)?;
72    let indices_validity = indices_array
73        .validity()
74        .vortex_expect("Failed to compute validity mask")
75        .execute_mask(indices_array.as_ref().len(), ctx)?;
76
77    if !indices_validity.all_true() || !data_validity.all_true() {
78        return _take_nullable::<I, O, OutputOffsetType>(array, offsets_array, indices_array, ctx);
79    }
80
81    let offsets: &[O] = offsets_array.as_slice();
82    let indices: &[I] = indices_array.as_slice();
83
84    let mut new_offsets = PrimitiveBuilder::<OutputOffsetType>::with_capacity(
85        Nullability::NonNullable,
86        indices.len(),
87    );
88    let mut elements_to_take =
89        PrimitiveBuilder::with_capacity(Nullability::NonNullable, 2 * indices.len());
90
91    let mut current_offset = OutputOffsetType::zero();
92    new_offsets.append_zero();
93
94    for &data_idx in indices {
95        let data_idx: usize = data_idx.as_();
96
97        let start = offsets[data_idx];
98        let stop = offsets[data_idx + 1];
99
100        // Annoyingly, we can't turn (start..end) into a range, so we're doing that manually.
101        //
102        // We could convert start and end to usize, but that would impose a potentially
103        // harder constraint - now we don't care if they fit into usize as long as their
104        // difference does.
105        let additional: usize = (stop - start).as_();
106
107        // TODO(0ax1): optimize this
108        elements_to_take.reserve_exact(additional);
109        for i in 0..additional {
110            elements_to_take.append_value(start + O::from_usize(i).vortex_expect("i < additional"));
111        }
112        current_offset +=
113            OutputOffsetType::from_usize((stop - start).as_()).vortex_expect("offset conversion");
114        new_offsets.append_value(current_offset);
115    }
116
117    let elements_to_take = elements_to_take.finish();
118    let new_offsets = new_offsets.finish();
119
120    let new_elements = array.elements().take(elements_to_take)?;
121
122    Ok(ListArray::try_new(
123        new_elements,
124        new_offsets,
125        array.validity()?.take(indices_array.array())?,
126    )?
127    .into_array())
128}
129
130// Kept out-of-line: as a single-callsite generic helper it would otherwise be inlined into every
131// monomorphization of `_take`, duplicating the entire nullable path across all specializations.
132#[inline(never)]
133fn _take_nullable<I: IntegerPType, O: IntegerPType, OutputOffsetType: IntegerPType>(
134    array: ArrayView<'_, List>,
135    offsets_array: ArrayView<'_, Primitive>,
136    indices_array: ArrayView<'_, Primitive>,
137    ctx: &mut ExecutionCtx,
138) -> VortexResult<ArrayRef> {
139    let offsets: &[O] = offsets_array.as_slice();
140    let indices: &[I] = indices_array.as_slice();
141    let data_validity = array
142        .list_validity()
143        .execute_mask(array.as_ref().len(), ctx)?;
144    let indices_validity = indices_array
145        .validity()
146        .vortex_expect("Failed to compute validity mask")
147        .execute_mask(indices_array.as_ref().len(), ctx)?;
148
149    let mut new_offsets = PrimitiveBuilder::<OutputOffsetType>::with_capacity(
150        Nullability::NonNullable,
151        indices.len(),
152    );
153
154    // This will be the indices we push down to the child array to call `take` with.
155    //
156    // There are 2 things to note here:
157    // - We do not know how many elements we need to take from our child since lists are variable
158    //   size: thus we arbitrarily choose a capacity of `2 * # of indices`.
159    // - The type of the primitive builder needs to fit the largest offset of the (parent)
160    //   `ListArray`, so we make this `PrimitiveBuilder` generic over `O` (instead of `I`).
161    let mut elements_to_take =
162        PrimitiveBuilder::<O>::with_capacity(Nullability::NonNullable, 2 * indices.len());
163
164    let mut current_offset = OutputOffsetType::zero();
165    new_offsets.append_zero();
166
167    for (data_idx, index_valid) in indices.iter().zip(indices_validity.iter()) {
168        if !index_valid {
169            new_offsets.append_value(current_offset);
170            continue;
171        }
172
173        let data_idx: usize = data_idx.as_();
174
175        if !data_validity.value(data_idx) {
176            new_offsets.append_value(current_offset);
177            continue;
178        }
179
180        let start = offsets[data_idx];
181        let stop = offsets[data_idx + 1];
182
183        // See the note in `_take` on the reasoning.
184        let additional: usize = (stop - start).as_();
185
186        elements_to_take.reserve_exact(additional);
187        for i in 0..additional {
188            elements_to_take.append_value(start + O::from_usize(i).vortex_expect("i < additional"));
189        }
190        current_offset +=
191            OutputOffsetType::from_usize((stop - start).as_()).vortex_expect("offset conversion");
192        new_offsets.append_value(current_offset);
193    }
194
195    let elements_to_take = elements_to_take.finish();
196    let new_offsets = new_offsets.finish();
197    let new_elements = array.elements().take(elements_to_take)?;
198
199    Ok(ListArray::try_new(
200        new_elements,
201        new_offsets,
202        array.validity()?.take(indices_array.array())?,
203    )?
204    .into_array())
205}
206
207#[cfg(test)]
208mod test {
209    use std::sync::Arc;
210
211    use rstest::rstest;
212    use vortex_buffer::buffer;
213
214    use crate::IntoArray as _;
215    use crate::VortexSessionExecute;
216    use crate::array_session;
217    use crate::arrays::BoolArray;
218    use crate::arrays::ListArray;
219    use crate::arrays::ListViewArray;
220    use crate::arrays::PrimitiveArray;
221    use crate::compute::conformance::take::test_take_conformance;
222    use crate::dtype::DType;
223    use crate::dtype::Nullability;
224    use crate::dtype::PType::I32;
225    use crate::scalar::Scalar;
226    use crate::validity::Validity;
227
228    #[test]
229    fn nullable_take() {
230        let mut ctx = array_session().create_execution_ctx();
231        let list = ListArray::try_new(
232            buffer![0i32, 5, 3, 4].into_array(),
233            buffer![0, 2, 3, 4, 4].into_array(),
234            Validity::Array(BoolArray::from_iter(vec![true, true, false, true]).into_array()),
235        )
236        .unwrap()
237        .into_array();
238
239        let idx =
240            PrimitiveArray::from_option_iter(vec![Some(0), None, Some(1), Some(3)]).into_array();
241
242        let result = list.take(idx).unwrap();
243
244        assert_eq!(
245            result.dtype(),
246            &DType::List(
247                Arc::new(DType::Primitive(I32, Nullability::NonNullable)),
248                Nullability::Nullable
249            )
250        );
251
252        let result = result.execute::<ListViewArray>(&mut ctx).unwrap();
253
254        assert_eq!(result.len(), 4);
255
256        let element_dtype: Arc<DType> = Arc::new(I32.into());
257
258        assert!(
259            result
260                .is_valid(0, &mut array_session().create_execution_ctx())
261                .unwrap()
262        );
263        assert_eq!(
264            result
265                .execute_scalar(0, &mut array_session().create_execution_ctx())
266                .unwrap(),
267            Scalar::list(
268                Arc::clone(&element_dtype),
269                vec![0i32.into(), 5.into()],
270                Nullability::Nullable
271            )
272        );
273
274        assert!(
275            result
276                .is_invalid(1, &mut array_session().create_execution_ctx())
277                .unwrap()
278        );
279
280        assert!(
281            result
282                .is_valid(2, &mut array_session().create_execution_ctx())
283                .unwrap()
284        );
285        assert_eq!(
286            result
287                .execute_scalar(2, &mut array_session().create_execution_ctx())
288                .unwrap(),
289            Scalar::list(
290                Arc::clone(&element_dtype),
291                vec![3i32.into()],
292                Nullability::Nullable
293            )
294        );
295
296        assert!(
297            result
298                .is_valid(3, &mut array_session().create_execution_ctx())
299                .unwrap()
300        );
301        assert_eq!(
302            result
303                .execute_scalar(3, &mut array_session().create_execution_ctx())
304                .unwrap(),
305            Scalar::list(element_dtype, vec![], Nullability::Nullable)
306        );
307    }
308
309    #[test]
310    fn change_validity() {
311        let list = ListArray::try_new(
312            buffer![0i32, 5, 3, 4].into_array(),
313            buffer![0, 2, 3].into_array(),
314            Validity::NonNullable,
315        )
316        .unwrap()
317        .into_array();
318
319        let idx = PrimitiveArray::from_option_iter(vec![Some(0), Some(1), None]).into_array();
320        // since idx is nullable, the final list will also be nullable
321
322        let result = list.take(idx).unwrap();
323        assert_eq!(
324            result.dtype(),
325            &DType::List(
326                Arc::new(DType::Primitive(I32, Nullability::NonNullable)),
327                Nullability::Nullable
328            )
329        );
330    }
331
332    #[test]
333    fn non_nullable_take() {
334        let mut ctx = array_session().create_execution_ctx();
335        let list = ListArray::try_new(
336            buffer![0i32, 5, 3, 4].into_array(),
337            buffer![0, 2, 3, 3, 4].into_array(),
338            Validity::NonNullable,
339        )
340        .unwrap()
341        .into_array();
342
343        let idx = buffer![1, 0, 2].into_array();
344
345        let result = list.take(idx).unwrap();
346
347        assert_eq!(
348            result.dtype(),
349            &DType::List(
350                Arc::new(DType::Primitive(I32, Nullability::NonNullable)),
351                Nullability::NonNullable
352            )
353        );
354
355        let result = result.execute::<ListViewArray>(&mut ctx).unwrap();
356
357        assert_eq!(result.len(), 3);
358
359        let element_dtype: Arc<DType> = Arc::new(I32.into());
360
361        assert!(
362            result
363                .is_valid(0, &mut array_session().create_execution_ctx())
364                .unwrap()
365        );
366        assert_eq!(
367            result
368                .execute_scalar(0, &mut array_session().create_execution_ctx())
369                .unwrap(),
370            Scalar::list(
371                Arc::clone(&element_dtype),
372                vec![3i32.into()],
373                Nullability::NonNullable
374            )
375        );
376
377        assert!(
378            result
379                .is_valid(1, &mut array_session().create_execution_ctx())
380                .unwrap()
381        );
382        assert_eq!(
383            result
384                .execute_scalar(1, &mut array_session().create_execution_ctx())
385                .unwrap(),
386            Scalar::list(
387                Arc::clone(&element_dtype),
388                vec![0i32.into(), 5.into()],
389                Nullability::NonNullable
390            )
391        );
392
393        assert!(
394            result
395                .is_valid(2, &mut array_session().create_execution_ctx())
396                .unwrap()
397        );
398        assert_eq!(
399            result
400                .execute_scalar(2, &mut array_session().create_execution_ctx())
401                .unwrap(),
402            Scalar::list(element_dtype, vec![], Nullability::NonNullable)
403        );
404    }
405
406    #[test]
407    fn test_take_empty_array() {
408        let list = ListArray::try_new(
409            buffer![0i32, 5, 3, 4].into_array(),
410            buffer![0].into_array(),
411            Validity::NonNullable,
412        )
413        .unwrap()
414        .into_array();
415
416        let idx = PrimitiveArray::empty::<i32>(Nullability::Nullable).into_array();
417
418        let result = list.take(idx).unwrap();
419        assert_eq!(
420            result.dtype(),
421            &DType::List(
422                Arc::new(DType::Primitive(I32, Nullability::NonNullable)),
423                Nullability::Nullable
424            )
425        );
426        assert_eq!(result.len(), 0,);
427    }
428
429    #[rstest]
430    #[case(ListArray::try_new(
431        buffer![0i32, 1, 2, 3, 4, 5].into_array(),
432        buffer![0, 2, 3, 5, 5, 6].into_array(),
433        Validity::NonNullable,
434    ).unwrap())]
435    #[case(ListArray::try_new(
436        buffer![10i32, 20, 30, 40, 50].into_array(),
437        buffer![0, 2, 3, 4, 5].into_array(),
438        Validity::Array(BoolArray::from_iter(vec![true, false, true, true]).into_array()),
439    ).unwrap())]
440    #[case(ListArray::try_new(
441        buffer![1i32, 2, 3].into_array(),
442        buffer![0, 0, 2, 2, 3].into_array(), // First and third are empty
443        Validity::NonNullable,
444    ).unwrap())]
445    #[case(ListArray::try_new(
446        buffer![42i32, 43].into_array(),
447        buffer![0, 2].into_array(),
448        Validity::NonNullable,
449    ).unwrap())]
450    #[case({
451        let elements = buffer![0i32..200].into_array();
452        let mut offsets = vec![0u64];
453        for i in 1..=50 {
454            offsets.push(offsets[i - 1] + (i as u64 % 5)); // Variable length lists
455        }
456        ListArray::try_new(
457            elements,
458            PrimitiveArray::from_iter(offsets).into_array(),
459            Validity::NonNullable,
460        ).unwrap()
461    })]
462    #[case(ListArray::try_new(
463        PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), Some(4), None]).into_array(),
464        buffer![0, 2, 3, 5].into_array(),
465        Validity::NonNullable,
466    ).unwrap())]
467    fn test_take_list_conformance(#[case] list: ListArray) {
468        test_take_conformance(
469            &list.into_array(),
470            &mut array_session().create_execution_ctx(),
471        );
472    }
473
474    #[test]
475    fn test_u64_offset_accumulation_non_nullable() {
476        let mut ctx = array_session().create_execution_ctx();
477        let elements = buffer![0i32; 200].into_array();
478        let offsets = buffer![0u8, 200].into_array();
479        let list = ListArray::try_new(elements, offsets, Validity::NonNullable)
480            .unwrap()
481            .into_array();
482
483        // Take the same large list twice - would overflow u8 but works with u64.
484        let idx = buffer![0u8, 0].into_array();
485        let result = list.take(idx).unwrap();
486
487        assert_eq!(result.len(), 2);
488
489        let result_view = result.execute::<ListViewArray>(&mut ctx).unwrap();
490        assert_eq!(result_view.len(), 2);
491        assert!(
492            result_view
493                .is_valid(0, &mut array_session().create_execution_ctx())
494                .unwrap()
495        );
496        assert!(
497            result_view
498                .is_valid(1, &mut array_session().create_execution_ctx())
499                .unwrap()
500        );
501    }
502
503    #[test]
504    fn test_u64_offset_accumulation_nullable() {
505        let mut ctx = array_session().create_execution_ctx();
506        let elements = buffer![0i32; 150].into_array();
507        let offsets = buffer![0u8, 150, 150].into_array();
508        let validity = BoolArray::from_iter(vec![true, false]).into_array();
509        let list = ListArray::try_new(elements, offsets, Validity::Array(validity))
510            .unwrap()
511            .into_array();
512
513        // Take the same large list twice - would overflow u8 but works with u64.
514        let idx = PrimitiveArray::from_option_iter(vec![Some(0u8), None, Some(0u8)]).into_array();
515        let result = list.take(idx).unwrap();
516
517        assert_eq!(result.len(), 3);
518
519        let result_view = result.execute::<ListViewArray>(&mut ctx).unwrap();
520        assert_eq!(result_view.len(), 3);
521        assert!(
522            result_view
523                .is_valid(0, &mut array_session().create_execution_ctx())
524                .unwrap()
525        );
526        assert!(
527            result_view
528                .is_invalid(1, &mut array_session().create_execution_ctx())
529                .unwrap()
530        );
531        assert!(
532            result_view
533                .is_valid(2, &mut array_session().create_execution_ctx())
534                .unwrap()
535        );
536    }
537
538    /// Regression test for validity length mismatch bug.
539    ///
540    /// When source array has `Validity::Array(...)` and indices are non-nullable,
541    /// the result validity must have length equal to indices.len(), not source.len().
542    #[test]
543    fn test_take_validity_length_mismatch_regression() {
544        // Source array with explicit validity array (length 2).
545        let list = ListArray::try_new(
546            buffer![1i32, 2, 3, 4].into_array(),
547            buffer![0, 2, 4].into_array(),
548            Validity::Array(BoolArray::from_iter(vec![true, true]).into_array()),
549        )
550        .unwrap()
551        .into_array();
552
553        // Take more indices than source length (4 vs 2) with non-nullable indices.
554        let idx = buffer![0u32, 1, 0, 1].into_array();
555
556        // This should not panic - result should have length 4.
557        let result = list.take(idx).unwrap();
558        assert_eq!(result.len(), 4);
559    }
560}