Skip to main content

vortex_array/arrays/chunked/compute/
take.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use itertools::Itertools;
5use num_traits::AsPrimitive;
6use vortex_buffer::Buffer;
7use vortex_buffer::BufferAllocatorRef;
8use vortex_buffer::BufferMut;
9use vortex_error::VortexResult;
10use vortex_error::vortex_bail;
11use vortex_error::vortex_ensure_eq;
12use vortex_error::vortex_err;
13use vortex_mask::Mask;
14
15use crate::ArrayRef;
16use crate::Canonical;
17use crate::Columnar;
18use crate::IntoArray;
19use crate::array::ArrayView;
20use crate::arrays::Chunked;
21use crate::arrays::ChunkedArray;
22use crate::arrays::ConstantArray;
23use crate::arrays::FixedSizeListArray;
24use crate::arrays::PiecewiseSequence;
25use crate::arrays::PiecewiseSequenceArray;
26use crate::arrays::PrimitiveArray;
27use crate::arrays::chunked::ChunkedArrayExt;
28use crate::arrays::dict::TakeExecute;
29use crate::arrays::piecewise_sequence::constant_unsigned_usize;
30use crate::arrays::piecewise_sequence::maybe_contiguous_slices;
31use crate::arrays::primitive::PrimitiveArrayExt;
32use crate::builders::ArrayBuilder;
33use crate::builders::builder_with_capacity_in;
34use crate::builtins::ArrayBuiltins;
35use crate::dtype::DType;
36use crate::dtype::PType;
37use crate::executor::ExecutionCtx;
38use crate::match_each_unsigned_integer_ptype;
39use crate::validity::Validity;
40
41/// Flattens per-chunk take/filter results into a single array. Flat dtypes append directly into
42/// a canonical builder; nested dtypes collect the chunks and canonicalize them as a chunked
43/// array, which reuses the chunks' children zero-copy.
44enum ChunkFlattener {
45    Builder(Box<dyn ArrayBuilder>),
46    Chunks(Vec<ArrayRef>),
47}
48
49impl ChunkFlattener {
50    fn new(dtype: &DType, capacity: usize, allocator: &BufferAllocatorRef) -> Self {
51        if dtype.is_nested() {
52            Self::Chunks(Vec::new())
53        } else {
54            Self::Builder(builder_with_capacity_in(dtype, capacity, allocator))
55        }
56    }
57
58    fn push(&mut self, chunk: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()> {
59        match self {
60            Self::Builder(builder) => chunk.append_to_builder(builder.as_mut(), ctx),
61            Self::Chunks(chunks) => {
62                chunks.push(chunk);
63                Ok(())
64            }
65        }
66    }
67
68    fn finish(self, dtype: &DType, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
69        match self {
70            Self::Builder(mut builder) => Ok(builder.finish()),
71            // SAFETY: every chunk is a filter or take of a chunk of a chunked array with `dtype`,
72            // which leaves the dtype unchanged.
73            Self::Chunks(chunks) => unsafe { ChunkedArray::new_unchecked(chunks, dtype.clone()) }
74                .into_array()
75                .execute::<Canonical>(ctx)
76                .map(IntoArray::into_array),
77        }
78    }
79}
80
81fn take_chunked_via_sort(
82    array: ArrayView<'_, Chunked>,
83    indices: &PrimitiveArray,
84    indices_mask: Mask,
85    ctx: &mut ExecutionCtx,
86) -> VortexResult<ArrayRef> {
87    let indices_values = indices.as_slice::<u64>();
88    let n = indices_values.len();
89    let mut pairs: Vec<(u64, usize)> = indices_values
90        .iter()
91        .enumerate()
92        .filter(|&(position, _)| indices_mask.value(position))
93        .map(|(position, &index)| (index, position))
94        .collect();
95    pairs.sort_unstable();
96
97    if let Some(&(index, _)) = pairs.last() {
98        let index = usize::try_from(index)?;
99        if index >= array.len() {
100            vortex_bail!(OutOfBounds: index, 0, array.len());
101        }
102    }
103
104    let chunk_offsets = array.chunk_offset_values();
105    let nchunks = array.nchunks();
106    let mut flattener = ChunkFlattener::new(array.dtype(), pairs.len(), ctx.allocator());
107    let mut final_take = BufferMut::<u64>::zeroed(n);
108    let mut cursor = 0usize;
109    let mut dedup_idx = 0u64;
110
111    for chunk_idx in 0..nchunks {
112        let chunk_start = chunk_offsets[chunk_idx];
113        let chunk_end = chunk_offsets[chunk_idx + 1];
114        let chunk_len = chunk_end - chunk_start;
115        let chunk_end_u64 = u64::try_from(chunk_end)?;
116        let range_end = cursor + pairs[cursor..].partition_point(|&(v, _)| v < chunk_end_u64);
117        let chunk_pairs = &pairs[cursor..range_end];
118
119        if !chunk_pairs.is_empty() {
120            let mut local_indices = Vec::new();
121            for (i, &(value, original_position)) in chunk_pairs.iter().enumerate() {
122                if cursor + i > 0 && value != pairs[cursor + i - 1].0 {
123                    dedup_idx += 1;
124                }
125                let local_index = usize::try_from(value)? - chunk_start;
126                if local_indices.last() != Some(&local_index) {
127                    local_indices.push(local_index);
128                }
129                final_take[original_position] = dedup_idx;
130            }
131
132            flattener.push(
133                array
134                    .chunk(chunk_idx)
135                    .filter(Mask::from_indices(chunk_len, local_indices))?,
136                ctx,
137            )?;
138        }
139
140        cursor = range_end;
141    }
142
143    let flat = flattener.finish(array.dtype(), ctx)?;
144    let take_validity = Validity::from_mask(indices_mask, indices.dtype().nullability());
145    flat.take(PrimitiveArray::new(final_take.freeze(), take_validity).into_array())
146}
147
148fn valid_indices_are_monotonic(indices: &[u64], indices_mask: &Mask) -> bool {
149    let mut previous = None;
150    for (position, &index) in indices.iter().enumerate() {
151        if !indices_mask.value(position) {
152            continue;
153        }
154        if previous.is_some_and(|previous| index < previous) {
155            return false;
156        }
157        previous = Some(index);
158    }
159    true
160}
161
162// TODO(joe): we want to return a chunked array ideally.
163fn take_chunked(
164    array: ArrayView<'_, Chunked>,
165    indices: &ArrayRef,
166    ctx: &mut ExecutionCtx,
167) -> VortexResult<ArrayRef> {
168    let indices = indices
169        .cast(DType::Primitive(PType::U64, indices.dtype().nullability()))?
170        .execute::<PrimitiveArray>(ctx)?;
171
172    let indices_mask = indices
173        .as_ref()
174        .validity()?
175        .execute_mask(indices.as_ref().len(), ctx)?;
176    let indices_values = indices.as_slice::<u64>();
177    let n = indices_values.len();
178    let chunk_offsets = array.chunk_offset_values();
179    let nchunks = array.nchunks();
180
181    // Strictly increasing non-nullable indices select every row at most once and in order, so the
182    // per-chunk gathers concatenate straight into the result with no dedup or reorder take.
183    if !indices.as_ref().dtype().is_nullable() && indices_values.is_sorted_by(|a, b| a < b) {
184        return take_chunked_sorted(array, indices_values);
185    }
186
187    // For a small unsorted fixed-size-list take over a small number of chunks, sorting has lower
188    // fixed overhead and lets the nested elements filter in source order. Larger, monotonic, and
189    // non-nested takes use bucketing.
190    if matches!(array.dtype(), DType::FixedSizeList(..))
191        && n <= 64
192        && nchunks <= 16
193        && !valid_indices_are_monotonic(indices_values, &indices_mask)
194    {
195        return take_chunked_via_sort(array, &indices, indices_mask, ctx);
196    }
197
198    // Route each valid index into its source chunk. Within each bucket, preserve the request order
199    // so the taken chunks can be assembled and then restored to the original cross-chunk order.
200    let mut buckets = vec![Vec::<(u64, usize)>::new(); nchunks];
201    let mut monotonic = true;
202    let mut last_index = None;
203    let mut sorted_chunk_idx = 0;
204
205    for (original_position, &index) in indices_values.iter().enumerate() {
206        if !indices_mask.value(original_position) {
207            continue;
208        }
209
210        let index = usize::try_from(index)?;
211        if index >= array.len() {
212            vortex_bail!(OutOfBounds: index, 0, array.len());
213        }
214
215        let still_sorted = last_index.is_none_or(|last_index| index >= last_index);
216        let chunk_idx = if monotonic && still_sorted {
217            while chunk_offsets[sorted_chunk_idx + 1] <= index {
218                sorted_chunk_idx += 1;
219            }
220            sorted_chunk_idx
221        } else {
222            monotonic = false;
223            chunk_offsets.partition_point(|&offset| offset <= index) - 1
224        };
225        last_index = Some(index);
226
227        let local_index = u64::try_from(index - chunk_offsets[chunk_idx])?;
228        buckets[chunk_idx].push((local_index, original_position));
229    }
230
231    let mut flattener =
232        ChunkFlattener::new(array.dtype(), indices_mask.true_count(), ctx.allocator());
233    let mut final_take =
234        (!monotonic || indices.dtype().is_nullable()).then(|| BufferMut::<u64>::zeroed(n));
235    let mut grouped_position = 0u64;
236
237    for (chunk_idx, bucket) in buckets.into_iter().enumerate() {
238        if bucket.is_empty() {
239            continue;
240        }
241
242        let mut local_indices = BufferMut::<u64>::with_capacity(bucket.len());
243        for (local_index, original_position) in bucket {
244            local_indices.push(local_index);
245            if let Some(final_take) = &mut final_take {
246                final_take[original_position] = grouped_position;
247            }
248            grouped_position += 1;
249        }
250
251        let local_indices =
252            PrimitiveArray::new(local_indices.freeze(), Validity::NonNullable).into_array();
253        flattener.push(array.chunk(chunk_idx).take(local_indices)?, ctx)?;
254    }
255
256    // TODO(joe): can we relax this.
257    let flat = flattener.finish(array.dtype(), ctx)?;
258
259    // Non-nullable monotonic indices are already in the same order as the assembled chunks, so no
260    // final reorder is needed.
261    let Some(final_take) = final_take else {
262        return Ok(flat);
263    };
264
265    // A single take restores original order and expands duplicates. Carrying the original index
266    // validity makes null indices produce null outputs.
267    let take_validity = Validity::from_mask(indices_mask, indices.dtype().nullability());
268    flat.take(PrimitiveArray::new(final_take.freeze(), take_validity).into_array())
269}
270
271/// Take for strictly increasing, non-nullable indices: every row is selected at most once and in
272/// order, so the per-chunk gathers concatenate directly into the result with no bucketing, dedup,
273/// or reorder take.
274fn take_chunked_sorted(
275    array: ArrayView<'_, Chunked>,
276    indices_values: &[u64],
277) -> VortexResult<ArrayRef> {
278    let chunk_offsets = array.chunk_offset_values();
279    let mut chunks = Vec::new();
280    let mut cursor = 0usize;
281
282    // Skip straight to the chunk holding the first index instead of walking every leading chunk;
283    // `<=` steps past empty chunks sharing the same offset.
284    let first_chunk = match indices_values.first() {
285        Some(&first) => {
286            let first = usize::try_from(first)?;
287            chunk_offsets
288                .partition_point(|&offset| offset <= first)
289                .saturating_sub(1)
290        }
291        None => 0,
292    };
293
294    for chunk_idx in first_chunk..array.nchunks() {
295        if cursor == indices_values.len() {
296            break;
297        }
298        let chunk_start = chunk_offsets[chunk_idx];
299        let chunk_end = chunk_offsets[chunk_idx + 1];
300        let chunk_end_u64 = u64::try_from(chunk_end)?;
301
302        let range_end = cursor + indices_values[cursor..].partition_point(|&v| v < chunk_end_u64);
303        if range_end > cursor {
304            let chunk_start_u64 = u64::try_from(chunk_start)?;
305            let mut local_indices = BufferMut::<u64>::with_capacity(range_end - cursor);
306            for &val in &indices_values[cursor..range_end] {
307                local_indices.push(val - chunk_start_u64);
308            }
309            let local_indices =
310                PrimitiveArray::new(local_indices.freeze(), Validity::NonNullable).into_array();
311            chunks.push(array.chunk(chunk_idx).take(local_indices)?);
312        }
313        cursor = range_end;
314    }
315    if cursor != indices_values.len() {
316        vortex_bail!(
317            OutOfBounds: usize::try_from(indices_values[cursor])?, 0, array.as_ref().len()
318        );
319    }
320
321    if chunks.len() == 1 {
322        return Ok(chunks.swap_remove(0));
323    }
324    // SAFETY: every chunk is a take of a chunk of `array` with non-nullable indices, which
325    // leaves the dtype unchanged.
326    Ok(unsafe { ChunkedArray::new_unchecked(chunks, array.dtype().clone()) }.into_array())
327}
328
329impl TakeExecute for Chunked {
330    fn take(
331        array: ArrayView<'_, Chunked>,
332        indices: &ArrayRef,
333        ctx: &mut ExecutionCtx,
334    ) -> VortexResult<Option<ArrayRef>> {
335        if array.nchunks() == 1 {
336            return array.chunk(0).take(indices.clone()).map(Some);
337        }
338
339        if let Some(taken) = take_chunked_fsl(array, indices, ctx)? {
340            return Ok(Some(taken));
341        }
342
343        if let Some(piecewise_indices) = indices.as_opt::<PiecewiseSequence>()
344            && let Some(taken) = take_piecewise_chunked(array, piecewise_indices, ctx)?
345        {
346            return Ok(Some(taken));
347        }
348
349        take_chunked(array, indices, ctx).map(Some)
350    }
351}
352
353/// Rewrites take over a chunked fixed-size list array as take over a single
354/// [`FixedSizeListArray`]. The swizzle is structural: FSL-encoded chunks execute to themselves,
355/// so their elements chain zero-copy, and the FSL take then gathers per-chunk element runs
356/// instead of taking each chunk.
357fn take_chunked_fsl(
358    array: ArrayView<'_, Chunked>,
359    indices: &ArrayRef,
360    ctx: &mut ExecutionCtx,
361) -> VortexResult<Option<ArrayRef>> {
362    if !matches!(array.dtype(), DType::FixedSizeList(..)) {
363        return Ok(None);
364    }
365
366    let fsl = array.as_ref().clone().execute::<FixedSizeListArray>(ctx)?;
367    fsl.into_array().take(indices.clone()).map(Some)
368}
369
370/// A per-chunk gather plan: chunk-local sub-piece runs to take from one chunk, in output order.
371#[derive(Default)]
372struct ChunkGather {
373    starts: Vec<u64>,
374    lengths: Vec<u64>,
375    total: usize,
376}
377
378/// Take for [`PiecewiseSequence`] indices with unit multipliers.
379///
380/// Each piece is a contiguous index run, so instead of expanding one index per element, pieces
381/// are split at chunk boundaries and gathered from each chunk with a chunk-local
382/// `PiecewiseSequenceArray`. Sub-pieces that visit chunks in non-decreasing order concatenate
383/// directly into the result; otherwise the gathered chunks are flattened once and a second
384/// piecewise take restores output order.
385fn take_piecewise_chunked(
386    array: ArrayView<'_, Chunked>,
387    indices: ArrayView<'_, PiecewiseSequence>,
388    ctx: &mut ExecutionCtx,
389) -> VortexResult<Option<ArrayRef>> {
390    let Some((starts, lengths)) = maybe_contiguous_slices(indices, ctx)? else {
391        return Ok(None);
392    };
393
394    let output_len = indices.as_ref().len();
395    let starts: Vec<usize> = match_each_unsigned_integer_ptype!(starts.ptype(), |S| {
396        starts
397            .as_slice::<S>()
398            .iter()
399            .map(|&start| start.as_())
400            .collect()
401    });
402    let lengths: Vec<usize> = match lengths {
403        Columnar::Constant(lengths) => vec![constant_unsigned_usize(&lengths); starts.len()],
404        Columnar::Canonical(lengths) => {
405            let lengths = lengths.into_primitive();
406            match_each_unsigned_integer_ptype!(lengths.ptype(), |L| {
407                lengths
408                    .as_slice::<L>()
409                    .iter()
410                    .map(|&length| length.as_())
411                    .collect()
412            })
413        }
414    };
415
416    let array_len = array.as_ref().len();
417    let chunk_offsets = array.chunk_offset_values();
418    let nchunks = array.nchunks();
419
420    let mut plans: Vec<ChunkGather> = Vec::new();
421    plans.resize_with(nchunks, ChunkGather::default);
422    // Sub-pieces in output order: (chunk index, offset within that chunk's gathered output, run
423    // length).
424    let mut sub_pieces: Vec<(usize, usize, usize)> = Vec::with_capacity(starts.len());
425    let mut monotonic = true;
426    let mut prev_chunk = 0usize;
427    let mut total_len = 0usize;
428
429    for (&start, &length) in starts.iter().zip_eq(&lengths) {
430        if length == 0 {
431            continue;
432        }
433        let end = start
434            .checked_add(length)
435            .ok_or_else(|| vortex_err!("PiecewiseSequenceArray range overflows usize"))?;
436        if end > array_len {
437            vortex_bail!(OutOfBounds: end - 1, 0, array_len);
438        }
439        total_len = total_len
440            .checked_add(length)
441            .ok_or_else(|| vortex_err!("PiecewiseSequenceArray output length overflows usize"))?;
442
443        // Locate the chunk containing `start`; `<=` skips empty chunks sharing the same offset.
444        let mut chunk_idx = chunk_offsets.partition_point(|&offset| offset <= start) - 1;
445        let mut cursor = start;
446        let mut remaining = length;
447        while remaining > 0 {
448            while chunk_offsets[chunk_idx + 1] <= cursor {
449                chunk_idx += 1;
450            }
451            let run = remaining.min(chunk_offsets[chunk_idx + 1] - cursor);
452            monotonic &= chunk_idx >= prev_chunk;
453            prev_chunk = chunk_idx;
454
455            let plan = &mut plans[chunk_idx];
456            // Consecutive sub-pieces in the same chunk stay adjacent in its gathered output, so
457            // they merge into one reorder run.
458            match sub_pieces.last_mut() {
459                Some((last_chunk, _, last_run)) if *last_chunk == chunk_idx => *last_run += run,
460                _ => sub_pieces.push((chunk_idx, plan.total, run)),
461            }
462            // A run that continues exactly where the chunk's previous run ended extends it, so
463            // contiguous spans gather as one run.
464            let local_start = (cursor - chunk_offsets[chunk_idx]) as u64;
465            match (plan.starts.last(), plan.lengths.last_mut()) {
466                (Some(&prev_start), Some(prev_len)) if prev_start + *prev_len == local_start => {
467                    *prev_len += run as u64;
468                }
469                _ => {
470                    plan.starts.push(local_start);
471                    plan.lengths.push(run as u64);
472                }
473            }
474            plan.total += run;
475
476            cursor += run;
477            remaining -= run;
478        }
479    }
480
481    vortex_ensure_eq!(
482        total_len,
483        output_len,
484        "PiecewiseSequenceArray expanded length does not match declared length",
485    );
486
487    // Chunks visited in order: the per-chunk gathers already concatenate into the result.
488    if monotonic {
489        let mut gathered = Vec::new();
490        for (chunk_idx, plan) in plans.into_iter().enumerate() {
491            if plan.starts.is_empty() {
492                continue;
493            }
494            gathered.push(gather_chunk(array.chunk(chunk_idx), plan)?);
495        }
496        let result = if gathered.len() == 1 {
497            gathered.swap_remove(0)
498        } else {
499            // SAFETY: every gathered chunk is a take of a chunk with dtype `array.dtype()`, and
500            // the non-nullable piecewise indices leave the dtype unchanged.
501            unsafe { ChunkedArray::new_unchecked(gathered, array.dtype().clone()) }.into_array()
502        };
503        return Ok(Some(result));
504    }
505
506    // Out-of-order sub-pieces: flatten the per-chunk gathers, then take the sub-piece runs from
507    // the flattened result in output order.
508    let mut bases = vec![0usize; nchunks];
509    let mut running = 0usize;
510    let mut flattener = ChunkFlattener::new(array.dtype(), output_len, ctx.allocator());
511    for (chunk_idx, plan) in plans.into_iter().enumerate() {
512        if plan.starts.is_empty() {
513            continue;
514        }
515        bases[chunk_idx] = running;
516        running += plan.total;
517        flattener.push(gather_chunk(array.chunk(chunk_idx), plan)?, ctx)?;
518    }
519    let flat = flattener.finish(array.dtype(), ctx)?;
520
521    let mut reorder_starts = Vec::with_capacity(sub_pieces.len());
522    let mut reorder_lengths = Vec::with_capacity(sub_pieces.len());
523    for &(chunk_idx, offset, run) in &sub_pieces {
524        reorder_starts.push((bases[chunk_idx] + offset) as u64);
525        reorder_lengths.push(run as u64);
526    }
527    flat.take(contiguous_runs(reorder_starts, reorder_lengths, output_len))
528        .map(Some)
529}
530
531/// Gathers a chunk's planned runs: a single run is a zero-copy slice, multiple runs become a
532/// chunk-local piecewise take.
533fn gather_chunk(chunk: &ArrayRef, plan: ChunkGather) -> VortexResult<ArrayRef> {
534    if let [start] = plan.starts.as_slice() {
535        let start = usize::try_from(*start)?;
536        return chunk.slice(start..start + plan.total);
537    }
538    chunk.take(contiguous_runs(plan.starts, plan.lengths, plan.total))
539}
540
541/// Builds a `PiecewiseSequenceArray` of contiguous (unit multiplier) runs whose lengths sum to
542/// `total`.
543fn contiguous_runs(starts: Vec<u64>, lengths: Vec<u64>, total: usize) -> ArrayRef {
544    let count = starts.len();
545    debug_assert_eq!(count, lengths.len());
546    let starts = PrimitiveArray::new(Buffer::from(starts), Validity::NonNullable).into_array();
547    let lengths = match lengths.first() {
548        Some(&first) if lengths.iter().all(|&length| length == first) => {
549            ConstantArray::new(first, count).into_array()
550        }
551        _ => PrimitiveArray::new(Buffer::from(lengths), Validity::NonNullable).into_array(),
552    };
553    let multipliers = ConstantArray::new(1u64, count).into_array();
554    // SAFETY: starts, lengths, and multipliers are non-nullable u64 arrays of equal length, and
555    // `total` is the sum of the lengths.
556    unsafe { PiecewiseSequenceArray::new_unchecked(starts, lengths, multipliers, total) }
557        .into_array()
558}
559
560#[cfg(test)]
561mod tests {
562    use vortex_buffer::Buffer;
563    use vortex_buffer::bitbuffer;
564    use vortex_buffer::buffer;
565    use vortex_error::VortexResult;
566
567    use crate::ArrayRef;
568    use crate::Canonical;
569    use crate::IntoArray;
570    use crate::VortexSessionExecute;
571    use crate::array_session;
572    use crate::arrays::BoolArray;
573    use crate::arrays::ChunkedArray;
574    use crate::arrays::ConstantArray;
575    use crate::arrays::FixedSizeListArray;
576    use crate::arrays::PiecewiseSequenceArray;
577    use crate::arrays::PrimitiveArray;
578    use crate::arrays::StructArray;
579    use crate::arrays::chunked::ChunkedArrayExt;
580    use crate::assert_arrays_eq;
581    use crate::compute::conformance::take::test_take_conformance;
582    use crate::dtype::DType;
583    use crate::dtype::FieldNames;
584    use crate::dtype::Nullability;
585    use crate::dtype::PType;
586    use crate::scalar::Scalar;
587    use crate::validity::Validity;
588
589    fn chunked_i32() -> VortexResult<ChunkedArray> {
590        ChunkedArray::try_new(
591            vec![
592                buffer![0i32, 1, 2, 3, 4].into_array(),
593                buffer![5i32, 6, 7, 8, 9].into_array(),
594                buffer![10i32, 11, 12, 13, 14].into_array(),
595            ],
596            DType::Primitive(PType::I32, Nullability::NonNullable),
597        )
598    }
599
600    fn contiguous_pieces(starts: &[u64], lengths: &[u64]) -> VortexResult<ArrayRef> {
601        let len = usize::try_from(lengths.iter().sum::<u64>())?;
602        Ok(PiecewiseSequenceArray::try_new(
603            starts.iter().copied().collect::<Buffer<u64>>().into_array(),
604            lengths
605                .iter()
606                .copied()
607                .collect::<Buffer<u64>>()
608                .into_array(),
609            ConstantArray::new(1u64, starts.len()).into_array(),
610            len,
611        )?
612        .into_array())
613    }
614
615    #[test]
616    fn test_take_piecewise_monotonic_spanning_chunks() -> VortexResult<()> {
617        let mut ctx = array_session().create_execution_ctx();
618        let arr = chunked_i32()?;
619
620        // The second piece crosses the first chunk boundary, the third spans the last two chunks.
621        let indices = contiguous_pieces(&[1, 4, 9], &[3, 4, 6])?;
622        let result = arr.take(indices)?;
623
624        assert_arrays_eq!(
625            result,
626            PrimitiveArray::from_iter([1i32, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14]),
627            &mut ctx
628        );
629        Ok(())
630    }
631
632    #[test]
633    fn test_take_piecewise_interleaved() -> VortexResult<()> {
634        let mut ctx = array_session().create_execution_ctx();
635        let arr = chunked_i32()?;
636
637        // Pieces visit chunks out of order, forcing the reorder take.
638        let indices = contiguous_pieces(&[12, 2, 7, 0], &[3, 2, 3, 1])?;
639        let result = arr.take(indices)?;
640
641        assert_arrays_eq!(
642            result,
643            PrimitiveArray::from_iter([12i32, 13, 14, 2, 3, 7, 8, 9, 0]),
644            &mut ctx
645        );
646        Ok(())
647    }
648
649    #[test]
650    fn test_take_piecewise_whole_array() -> VortexResult<()> {
651        let mut ctx = array_session().create_execution_ctx();
652        let arr = chunked_i32()?;
653
654        let indices = contiguous_pieces(&[0], &[15])?;
655        let result = arr.take(indices)?;
656
657        assert_arrays_eq!(result, PrimitiveArray::from_iter(0i32..15), &mut ctx);
658        Ok(())
659    }
660
661    #[test]
662    fn test_take_piecewise_across_empty_chunk() -> VortexResult<()> {
663        let mut ctx = array_session().create_execution_ctx();
664        let arr = ChunkedArray::try_new(
665            vec![
666                buffer![0i32, 1, 2, 3, 4].into_array(),
667                PrimitiveArray::empty::<i32>(Nullability::NonNullable).into_array(),
668                buffer![5i32, 6, 7, 8, 9].into_array(),
669            ],
670            DType::Primitive(PType::I32, Nullability::NonNullable),
671        )?;
672
673        let indices = contiguous_pieces(&[3], &[4])?;
674        let result = arr.take(indices)?;
675
676        assert_arrays_eq!(result, PrimitiveArray::from_iter([3i32, 4, 5, 6]), &mut ctx);
677        Ok(())
678    }
679
680    #[test]
681    fn test_take_piecewise_adjacent_pieces_merge() -> VortexResult<()> {
682        let mut ctx = array_session().create_execution_ctx();
683        let arr = chunked_i32()?;
684
685        // The first two pieces are contiguous within one chunk, so they merge into a single run
686        // that gathers as a zero-copy slice.
687        let indices = contiguous_pieces(&[0, 2, 7], &[2, 2, 2])?;
688        let result = arr.take(indices)?;
689
690        assert_arrays_eq!(
691            result,
692            PrimitiveArray::from_iter([0i32, 1, 2, 3, 7, 8]),
693            &mut ctx
694        );
695        Ok(())
696    }
697
698    #[test]
699    fn test_take_piecewise_same_chunk_out_of_order() -> VortexResult<()> {
700        let mut ctx = array_session().create_execution_ctx();
701        let arr = chunked_i32()?;
702
703        // Non-contiguous pieces in the same chunk must stay separate runs in piece order.
704        let indices = contiguous_pieces(&[2, 0], &[2, 2])?;
705        let result = arr.take(indices)?;
706
707        assert_arrays_eq!(result, PrimitiveArray::from_iter([2i32, 3, 0, 1]), &mut ctx);
708        Ok(())
709    }
710
711    #[test]
712    fn test_take_piecewise_zero_length_pieces() -> VortexResult<()> {
713        let mut ctx = array_session().create_execution_ctx();
714        let arr = chunked_i32()?;
715
716        let indices = contiguous_pieces(&[5, 0, 2], &[0, 4, 0])?;
717        let result = arr.take(indices)?;
718
719        assert_arrays_eq!(result, PrimitiveArray::from_iter([0i32, 1, 2, 3]), &mut ctx);
720        Ok(())
721    }
722
723    #[test]
724    fn test_take_piecewise_out_of_bounds() -> VortexResult<()> {
725        let mut ctx = array_session().create_execution_ctx();
726        let arr = chunked_i32()?;
727
728        let indices = contiguous_pieces(&[12], &[5])?;
729        let result = arr
730            .take(indices)
731            .and_then(|taken| taken.execute::<Canonical>(&mut ctx));
732
733        assert!(result.is_err());
734        Ok(())
735    }
736
737    #[test]
738    fn test_take_piecewise_non_unit_multiplier() -> VortexResult<()> {
739        let mut ctx = array_session().create_execution_ctx();
740        let arr = chunked_i32()?;
741
742        // Multiplier 2 falls back to the generic path but must stay correct.
743        let indices = PiecewiseSequenceArray::try_new(
744            buffer![0u64, 1].into_array(),
745            buffer![5u64, 3].into_array(),
746            buffer![2u64, 4].into_array(),
747            8,
748        )?
749        .into_array();
750        let result = arr.take(indices)?;
751
752        assert_arrays_eq!(
753            result,
754            PrimitiveArray::from_iter([0i32, 2, 4, 6, 8, 1, 5, 9]),
755            &mut ctx
756        );
757        Ok(())
758    }
759
760    #[test]
761    fn test_take_piecewise_nullable_values() -> VortexResult<()> {
762        let mut ctx = array_session().create_execution_ctx();
763        let arr = ChunkedArray::try_new(
764            vec![
765                PrimitiveArray::from_option_iter([Some(0i32), None, Some(2)]).into_array(),
766                PrimitiveArray::from_option_iter([None, Some(4i32), Some(5)]).into_array(),
767            ],
768            DType::Primitive(PType::I32, Nullability::Nullable),
769        )?;
770
771        let indices = contiguous_pieces(&[4, 1], &[2, 3])?;
772        let result = arr.take(indices)?;
773
774        assert_arrays_eq!(
775            result,
776            PrimitiveArray::from_option_iter([Some(4i32), Some(5), None, Some(2), None]),
777            &mut ctx
778        );
779        Ok(())
780    }
781
782    #[test]
783    fn test_take_fsl_over_chunked_elements() -> VortexResult<()> {
784        let mut ctx = array_session().create_execution_ctx();
785        // Chunk boundaries at 8 and 13 do not line up with the list size of 3.
786        let elements = ChunkedArray::try_new(
787            vec![
788                PrimitiveArray::from_iter(0i32..8).into_array(),
789                PrimitiveArray::from_iter(8i32..13).into_array(),
790                PrimitiveArray::from_iter(13i32..18).into_array(),
791            ],
792            DType::Primitive(PType::I32, Nullability::NonNullable),
793        )?
794        .into_array();
795        let fsl = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 6)?;
796
797        let indices = buffer![4u64, 0, 2, 5, 1, 4].into_array();
798        let result = fsl.take(indices.clone())?;
799        let expected = FixedSizeListArray::try_new(
800            PrimitiveArray::from_iter(0i32..18).into_array(),
801            3,
802            Validity::NonNullable,
803            6,
804        )?
805        .take(indices)?;
806
807        assert_arrays_eq!(result, expected, &mut ctx);
808        Ok(())
809    }
810
811    #[test]
812    fn test_take_fsl_over_chunked_elements_conformance() -> VortexResult<()> {
813        let elements = ChunkedArray::try_new(
814            vec![
815                PrimitiveArray::from_iter(0i32..8).into_array(),
816                PrimitiveArray::from_iter(8i32..13).into_array(),
817                PrimitiveArray::from_iter(13i32..18).into_array(),
818            ],
819            DType::Primitive(PType::I32, Nullability::NonNullable),
820        )?
821        .into_array();
822        let fsl = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 6)?;
823        test_take_conformance(
824            &fsl.into_array(),
825            &mut array_session().create_execution_ctx(),
826        );
827        Ok(())
828    }
829
830    #[test]
831    fn test_take_chunked_fsl() -> VortexResult<()> {
832        let mut ctx = array_session().create_execution_ctx();
833        let c0 = FixedSizeListArray::try_new(
834            PrimitiveArray::from_iter(0i32..6).into_array(),
835            2,
836            Validity::NonNullable,
837            3,
838        )?;
839        let c1 = FixedSizeListArray::try_new(
840            PrimitiveArray::from_iter(6i32..10).into_array(),
841            2,
842            Validity::NonNullable,
843            2,
844        )?;
845        let dtype = c0.dtype().clone();
846        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
847
848        let indices = buffer![4u64, 0, 3, 3, 1].into_array();
849        let result = arr.take(indices.clone())?;
850        let expected = FixedSizeListArray::try_new(
851            PrimitiveArray::from_iter(0i32..10).into_array(),
852            2,
853            Validity::NonNullable,
854            5,
855        )?
856        .take(indices)?;
857
858        assert_arrays_eq!(result, expected, &mut ctx);
859        Ok(())
860    }
861
862    #[test]
863    fn test_take_chunked_fsl_nullable() -> VortexResult<()> {
864        let mut ctx = array_session().create_execution_ctx();
865        let c0 = FixedSizeListArray::try_new(
866            PrimitiveArray::from_iter(0i32..6).into_array(),
867            2,
868            Validity::Array(bitbuffer![1 0 1].into_array()),
869            3,
870        )?;
871        let c1 = FixedSizeListArray::try_new(
872            PrimitiveArray::from_iter(6i32..10).into_array(),
873            2,
874            Validity::AllValid,
875            2,
876        )?;
877        let dtype = c0.dtype().clone();
878        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
879
880        let indices =
881            PrimitiveArray::from_option_iter([Some(4u64), None, Some(0), Some(1)]).into_array();
882        let result = arr.take(indices.clone())?;
883        let expected = FixedSizeListArray::try_new(
884            PrimitiveArray::from_iter(0i32..10).into_array(),
885            2,
886            Validity::Array(bitbuffer![1 0 1 1 1].into_array()),
887            5,
888        )?
889        .take(indices)?;
890
891        assert_arrays_eq!(result, expected, &mut ctx);
892        Ok(())
893    }
894
895    #[test]
896    fn test_take_chunked_fsl_non_fsl_chunk() -> VortexResult<()> {
897        let mut ctx = array_session().create_execution_ctx();
898        let c0 = FixedSizeListArray::try_new(
899            PrimitiveArray::from_iter(0i32..6).into_array(),
900            2,
901            Validity::NonNullable,
902            3,
903        )?;
904        let c1 = FixedSizeListArray::try_new(
905            PrimitiveArray::from_iter(6i32..10).into_array(),
906            2,
907            Validity::NonNullable,
908            2,
909        )?;
910        let dtype = c0.dtype().clone();
911        // Wrap one chunk in a nested chunked array so it is not FSL-encoded; the dtype-based
912        // swizzle must still handle it.
913        let nested = ChunkedArray::try_new(vec![c1.into_array()], dtype.clone())?;
914        let arr = ChunkedArray::try_new(vec![c0.into_array(), nested.into_array()], dtype)?;
915
916        let indices = buffer![4u64, 0, 3, 1].into_array();
917        let result = arr.take(indices.clone())?;
918        let expected = FixedSizeListArray::try_new(
919            PrimitiveArray::from_iter(0i32..10).into_array(),
920            2,
921            Validity::NonNullable,
922            5,
923        )?
924        .take(indices)?;
925
926        assert_arrays_eq!(result, expected, &mut ctx);
927        Ok(())
928    }
929
930    #[test]
931    fn test_take_chunked_fsl_constant_chunk() -> VortexResult<()> {
932        let mut ctx = array_session().create_execution_ctx();
933        let c0 = FixedSizeListArray::try_new(
934            PrimitiveArray::from_iter(0i32..6).into_array(),
935            2,
936            Validity::AllValid,
937            3,
938        )?;
939        let dtype = c0.dtype().clone();
940        // A constant chunk is only logically FSL; the swizzle canonicalizes it in place.
941        let c1 = ConstantArray::new(Scalar::null(dtype.clone()), 2).into_array();
942        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1], dtype)?;
943
944        let indices = buffer![4u64, 0, 3, 1].into_array();
945        let result = arr.take(indices.clone())?;
946        let expected = FixedSizeListArray::try_new(
947            PrimitiveArray::from_iter(0i32..10).into_array(),
948            2,
949            Validity::Array(bitbuffer![1 1 1 0 0].into_array()),
950            5,
951        )?
952        .take(indices)?;
953
954        assert_arrays_eq!(result, expected, &mut ctx);
955        Ok(())
956    }
957
958    #[test]
959    fn test_take_piecewise_chunked_fsl() -> VortexResult<()> {
960        let mut ctx = array_session().create_execution_ctx();
961        let c0 = FixedSizeListArray::try_new(
962            PrimitiveArray::from_iter(0i32..6).into_array(),
963            2,
964            Validity::NonNullable,
965            3,
966        )?;
967        let c1 = FixedSizeListArray::try_new(
968            PrimitiveArray::from_iter(6i32..10).into_array(),
969            2,
970            Validity::NonNullable,
971            2,
972        )?;
973        let dtype = c0.dtype().clone();
974        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
975        let reference = FixedSizeListArray::try_new(
976            PrimitiveArray::from_iter(0i32..10).into_array(),
977            2,
978            Validity::NonNullable,
979            5,
980        )?;
981
982        // A monotonic run crossing the chunk boundary.
983        let indices = contiguous_pieces(&[1], &[3])?;
984        assert_arrays_eq!(
985            arr.take(indices.clone())?,
986            reference.take(indices)?,
987            &mut ctx
988        );
989
990        // Out-of-order pieces forcing the reorder take.
991        let indices = contiguous_pieces(&[3, 0], &[2, 2])?;
992        assert_arrays_eq!(
993            arr.take(indices.clone())?,
994            reference.take(indices)?,
995            &mut ctx
996        );
997        Ok(())
998    }
999
1000    #[test]
1001    fn test_take_chunked_struct_nested_flatten() -> VortexResult<()> {
1002        let mut ctx = array_session().create_execution_ctx();
1003        let s0 = StructArray::try_new(
1004            ["a"].into(),
1005            vec![buffer![0i32, 1, 2].into_array()],
1006            3,
1007            Validity::NonNullable,
1008        )?;
1009        let s1 = StructArray::try_new(
1010            ["a"].into(),
1011            vec![buffer![3i32, 4].into_array()],
1012            2,
1013            Validity::NonNullable,
1014        )?;
1015        let dtype = s0.dtype().clone();
1016        let arr = ChunkedArray::try_new(vec![s0.into_array(), s1.into_array()], dtype)?;
1017
1018        let result = arr.take(buffer![4u64, 0, 2, 4].into_array())?;
1019        let expected = StructArray::try_new(
1020            ["a"].into(),
1021            vec![buffer![4i32, 0, 2, 4].into_array()],
1022            4,
1023            Validity::NonNullable,
1024        )?;
1025
1026        assert_arrays_eq!(result, expected, &mut ctx);
1027        Ok(())
1028    }
1029
1030    #[test]
1031    fn test_take_chunked_fsl_conformance() -> VortexResult<()> {
1032        let c0 = FixedSizeListArray::try_new(
1033            PrimitiveArray::from_iter(0i32..6).into_array(),
1034            2,
1035            Validity::NonNullable,
1036            3,
1037        )?;
1038        let c1 = FixedSizeListArray::try_new(
1039            PrimitiveArray::from_iter(6i32..10).into_array(),
1040            2,
1041            Validity::NonNullable,
1042            2,
1043        )?;
1044        let dtype = c0.dtype().clone();
1045        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
1046        test_take_conformance(
1047            &arr.into_array(),
1048            &mut array_session().create_execution_ctx(),
1049        );
1050        Ok(())
1051    }
1052
1053    #[test]
1054    fn test_take() {
1055        let mut ctx = array_session().create_execution_ctx();
1056        let a = buffer![1i32, 2, 3].into_array();
1057        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1058            .unwrap();
1059        assert_eq!(arr.nchunks(), 3);
1060        assert_eq!(arr.len(), 9);
1061        let indices = buffer![0u64, 0, 6, 4].into_array();
1062
1063        let result = arr.take(indices).unwrap();
1064        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 1, 1, 2]), &mut ctx);
1065    }
1066
1067    #[test]
1068    fn test_take_nullable_values() {
1069        let mut ctx = array_session().create_execution_ctx();
1070        let a = PrimitiveArray::new(buffer![1i32, 2, 3], Validity::AllValid).into_array();
1071        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1072            .unwrap();
1073        assert_eq!(arr.nchunks(), 3);
1074        assert_eq!(arr.len(), 9);
1075        let indices = PrimitiveArray::new(buffer![0u64, 0, 6, 4], Validity::NonNullable);
1076
1077        let result = arr.take(indices.into_array()).unwrap();
1078        assert_arrays_eq!(
1079            result,
1080            PrimitiveArray::from_option_iter([1i32, 1, 1, 2].map(Some)),
1081            &mut ctx
1082        );
1083    }
1084
1085    #[test]
1086    fn test_take_nullable_indices() {
1087        let mut ctx = array_session().create_execution_ctx();
1088        let a = buffer![1i32, 2, 3].into_array();
1089        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1090            .unwrap();
1091        assert_eq!(arr.nchunks(), 3);
1092        assert_eq!(arr.len(), 9);
1093        let indices = PrimitiveArray::new(
1094            buffer![0u64, 0, 6, 4],
1095            Validity::Array(bitbuffer![1 0 0 1].into_array()),
1096        );
1097
1098        let result = arr.take(indices.into_array()).unwrap();
1099        assert_arrays_eq!(
1100            result,
1101            PrimitiveArray::from_option_iter([Some(1i32), None, None, Some(2)]),
1102            &mut ctx
1103        );
1104    }
1105
1106    #[test]
1107    fn test_take_nullable_struct() {
1108        let mut ctx = array_session().create_execution_ctx();
1109        let struct_array =
1110            StructArray::try_new(FieldNames::default(), vec![], 100, Validity::NonNullable)
1111                .unwrap();
1112
1113        let arr = ChunkedArray::from_iter(vec![
1114            struct_array.clone().into_array(),
1115            struct_array.into_array(),
1116        ]);
1117
1118        let result = arr
1119            .take(PrimitiveArray::from_option_iter(vec![Some(0), None, Some(101)]).into_array())
1120            .unwrap();
1121
1122        let expect = StructArray::try_new(
1123            FieldNames::default(),
1124            vec![],
1125            3,
1126            Validity::Array(BoolArray::from_iter(vec![true, false, true]).into_array()),
1127        )
1128        .unwrap();
1129        assert_arrays_eq!(result, expect, &mut ctx);
1130    }
1131
1132    #[test]
1133    fn test_empty_take() {
1134        let mut ctx = array_session().create_execution_ctx();
1135        let a = buffer![1i32, 2, 3].into_array();
1136        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1137            .unwrap();
1138        assert_eq!(arr.nchunks(), 3);
1139        assert_eq!(arr.len(), 9);
1140
1141        let indices = PrimitiveArray::empty::<u64>(Nullability::NonNullable);
1142        let result = arr.take(indices.into_array()).unwrap();
1143
1144        assert!(result.is_empty());
1145        assert_eq!(result.dtype(), arr.dtype());
1146        assert_arrays_eq!(
1147            result,
1148            PrimitiveArray::empty::<i32>(Nullability::NonNullable),
1149            &mut ctx
1150        );
1151    }
1152
1153    #[test]
1154    fn test_take_shuffled_indices() -> VortexResult<()> {
1155        let mut ctx = array_session().create_execution_ctx();
1156        let c0 = buffer![0i32, 1, 2].into_array();
1157        let c1 = buffer![3i32, 4, 5].into_array();
1158        let c2 = buffer![6i32, 7, 8].into_array();
1159        let arr = ChunkedArray::try_new(
1160            vec![c0, c1, c2],
1161            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1162                .dtype()
1163                .clone(),
1164        )?;
1165
1166        // Fully shuffled indices that cross every chunk boundary.
1167        let indices = buffer![8u64, 0, 5, 3, 2, 7, 1, 6, 4].into_array();
1168        let result = arr.take(indices)?;
1169
1170        assert_arrays_eq!(
1171            result,
1172            PrimitiveArray::from_iter([8i32, 0, 5, 3, 2, 7, 1, 6, 4]),
1173            &mut ctx
1174        );
1175        Ok(())
1176    }
1177
1178    #[test]
1179    fn test_take_shuffled_duplicates_with_empty_chunks() -> VortexResult<()> {
1180        let mut ctx = array_session().create_execution_ctx();
1181        let empty = PrimitiveArray::empty::<i32>(Nullability::NonNullable).into_array();
1182        let arr = ChunkedArray::try_new(
1183            vec![
1184                empty.clone(),
1185                buffer![0i32, 1].into_array(),
1186                empty.clone(),
1187                buffer![2i32, 3].into_array(),
1188                empty,
1189            ],
1190            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1191                .dtype()
1192                .clone(),
1193        )?;
1194
1195        let result = arr.take(buffer![3u64, 0, 2, 0, 3, 1, 2].into_array())?;
1196
1197        assert_arrays_eq!(
1198            result,
1199            PrimitiveArray::from_iter([3i32, 0, 2, 0, 3, 1, 2]),
1200            &mut ctx
1201        );
1202        Ok(())
1203    }
1204
1205    #[test]
1206    fn test_take_small_shuffled_fixed_size_lists() -> VortexResult<()> {
1207        let mut ctx = array_session().create_execution_ctx();
1208        let first = FixedSizeListArray::new(
1209            buffer![0i32, 1, 2, 3].into_array(),
1210            2,
1211            Validity::NonNullable,
1212            2,
1213        )
1214        .into_array();
1215        let second = FixedSizeListArray::new(
1216            buffer![4i32, 5, 6, 7].into_array(),
1217            2,
1218            Validity::NonNullable,
1219            2,
1220        )
1221        .into_array();
1222        let dtype = first.dtype().clone();
1223        let array = ChunkedArray::try_new(vec![first, second], dtype)?;
1224
1225        let result = array.take(buffer![3u64, 0, 2, 1, 3].into_array())?;
1226        let expected = FixedSizeListArray::new(
1227            buffer![6i32, 7, 0, 1, 4, 5, 2, 3, 6, 7].into_array(),
1228            2,
1229            Validity::NonNullable,
1230            5,
1231        );
1232
1233        assert_arrays_eq!(result, expected, &mut ctx);
1234        Ok(())
1235    }
1236
1237    #[test]
1238    fn test_take_shuffled_large() -> VortexResult<()> {
1239        let mut ctx = array_session().create_execution_ctx();
1240        let nchunks: i32 = 100;
1241        let chunk_len: i32 = 1_000;
1242        let total = nchunks * chunk_len;
1243
1244        let chunks: Vec<_> = (0..nchunks)
1245            .map(|c| {
1246                let start = c * chunk_len;
1247                PrimitiveArray::from_iter(start..start + chunk_len).into_array()
1248            })
1249            .collect();
1250        let dtype = chunks[0].dtype().clone();
1251        let arr = ChunkedArray::try_new(chunks, dtype)?;
1252
1253        // Fisher-Yates shuffle with a fixed seed for determinism.
1254        let mut indices: Vec<u64> = (0..u64::try_from(total)?).collect();
1255        let mut seed: u64 = 0xdeadbeef;
1256        for i in (1..indices.len()).rev() {
1257            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1258            let j = (seed >> 33) as usize % (i + 1);
1259            indices.swap(i, j);
1260        }
1261
1262        let indices_arr = PrimitiveArray::new(Buffer::from(indices.clone()), Validity::NonNullable);
1263        let result = arr.take(indices_arr.into_array())?;
1264
1265        // Verify every element.
1266        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
1267        let result_vals = result.as_slice::<i32>();
1268        for (pos, &idx) in indices.iter().enumerate() {
1269            assert_eq!(
1270                result_vals[pos],
1271                i32::try_from(idx)?,
1272                "mismatch at position {pos}"
1273            );
1274        }
1275        Ok(())
1276    }
1277
1278    #[test]
1279    fn test_take_null_indices() -> VortexResult<()> {
1280        let mut ctx = array_session().create_execution_ctx();
1281        let c0 = buffer![10i32, 20, 30].into_array();
1282        let c1 = buffer![40i32, 50, 60].into_array();
1283        let arr = ChunkedArray::try_new(
1284            vec![c0, c1],
1285            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1286                .dtype()
1287                .clone(),
1288        )?;
1289
1290        // Indices with nulls scattered across chunk boundaries.
1291        let indices =
1292            PrimitiveArray::from_option_iter([Some(5u64), None, Some(0), Some(3), None, Some(2)]);
1293        let result = arr.take(indices.into_array())?;
1294
1295        assert_arrays_eq!(
1296            result,
1297            PrimitiveArray::from_option_iter([
1298                Some(60i32),
1299                None,
1300                Some(10),
1301                Some(40),
1302                None,
1303                Some(30)
1304            ]),
1305            &mut ctx
1306        );
1307        Ok(())
1308    }
1309
1310    #[test]
1311    fn test_take_sorted_indices() -> VortexResult<()> {
1312        let mut ctx = array_session().create_execution_ctx();
1313        let arr = chunked_i32()?;
1314
1315        // Strictly increasing indices spanning chunks hit the sorted fast path.
1316        let indices = buffer![1u64, 4, 5, 9, 10, 14].into_array();
1317        let result = arr.take(indices)?;
1318
1319        assert_arrays_eq!(
1320            result,
1321            PrimitiveArray::from_iter([1i32, 4, 5, 9, 10, 14]),
1322            &mut ctx
1323        );
1324        Ok(())
1325    }
1326
1327    #[test]
1328    fn test_take_out_of_bounds() -> VortexResult<()> {
1329        let mut ctx = array_session().create_execution_ctx();
1330        let arr = chunked_i32()?;
1331
1332        // Sorted indices hit the fast path, unsorted the generic path; both must fail.
1333        for indices in [buffer![0u64, 100], buffer![100u64, 0]] {
1334            let result = arr
1335                .take(indices.into_array())
1336                .and_then(|taken| taken.execute::<Canonical>(&mut ctx));
1337            assert!(result.is_err(), "expected out-of-bounds error");
1338        }
1339        Ok(())
1340    }
1341
1342    #[test]
1343    fn test_take_all_null_indices() -> VortexResult<()> {
1344        let mut ctx = array_session().create_execution_ctx();
1345        let arr = chunked_i32()?;
1346
1347        let indices = PrimitiveArray::from_option_iter([None::<u64>, None]).into_array();
1348        let result = arr.take(indices)?;
1349
1350        assert_arrays_eq!(
1351            result,
1352            PrimitiveArray::from_option_iter([None::<i32>, None]),
1353            &mut ctx
1354        );
1355        Ok(())
1356    }
1357
1358    #[test]
1359    fn test_take_chunked_conformance() {
1360        let a = buffer![1i32, 2, 3].into_array();
1361        let b = buffer![4i32, 5].into_array();
1362        let arr = ChunkedArray::try_new(
1363            vec![a, b],
1364            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1365                .dtype()
1366                .clone(),
1367        )
1368        .unwrap();
1369        test_take_conformance(
1370            &arr.into_array(),
1371            &mut array_session().create_execution_ctx(),
1372        );
1373
1374        // Test with nullable chunked array
1375        let a = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]);
1376        let b = PrimitiveArray::from_option_iter([Some(4i32), Some(5)]);
1377        let dtype = a.dtype().clone();
1378        let arr = ChunkedArray::try_new(vec![a.into_array(), b.into_array()], dtype).unwrap();
1379        test_take_conformance(
1380            &arr.into_array(),
1381            &mut array_session().create_execution_ctx(),
1382        );
1383
1384        // Test with multiple identical chunks
1385        let chunk = buffer![10i32, 20, 30, 40, 50].into_array();
1386        let arr = ChunkedArray::try_new(
1387            vec![chunk.clone(), chunk.clone(), chunk.clone()],
1388            chunk.dtype().clone(),
1389        )
1390        .unwrap();
1391        test_take_conformance(
1392            &arr.into_array(),
1393            &mut array_session().create_execution_ctx(),
1394        );
1395    }
1396}