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;
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!(
482        total_len == output_len,
483        "PiecewiseSequenceArray expanded length {total_len} does not match declared length {output_len}"
484    );
485
486    // Chunks visited in order: the per-chunk gathers already concatenate into the result.
487    if monotonic {
488        let mut gathered = Vec::new();
489        for (chunk_idx, plan) in plans.into_iter().enumerate() {
490            if plan.starts.is_empty() {
491                continue;
492            }
493            gathered.push(gather_chunk(array.chunk(chunk_idx), plan)?);
494        }
495        let result = if gathered.len() == 1 {
496            gathered.swap_remove(0)
497        } else {
498            // SAFETY: every gathered chunk is a take of a chunk with dtype `array.dtype()`, and
499            // the non-nullable piecewise indices leave the dtype unchanged.
500            unsafe { ChunkedArray::new_unchecked(gathered, array.dtype().clone()) }.into_array()
501        };
502        return Ok(Some(result));
503    }
504
505    // Out-of-order sub-pieces: flatten the per-chunk gathers, then take the sub-piece runs from
506    // the flattened result in output order.
507    let mut bases = vec![0usize; nchunks];
508    let mut running = 0usize;
509    let mut flattener = ChunkFlattener::new(array.dtype(), output_len, ctx.allocator());
510    for (chunk_idx, plan) in plans.into_iter().enumerate() {
511        if plan.starts.is_empty() {
512            continue;
513        }
514        bases[chunk_idx] = running;
515        running += plan.total;
516        flattener.push(gather_chunk(array.chunk(chunk_idx), plan)?, ctx)?;
517    }
518    let flat = flattener.finish(array.dtype(), ctx)?;
519
520    let mut reorder_starts = Vec::with_capacity(sub_pieces.len());
521    let mut reorder_lengths = Vec::with_capacity(sub_pieces.len());
522    for &(chunk_idx, offset, run) in &sub_pieces {
523        reorder_starts.push((bases[chunk_idx] + offset) as u64);
524        reorder_lengths.push(run as u64);
525    }
526    flat.take(contiguous_runs(reorder_starts, reorder_lengths, output_len))
527        .map(Some)
528}
529
530/// Gathers a chunk's planned runs: a single run is a zero-copy slice, multiple runs become a
531/// chunk-local piecewise take.
532fn gather_chunk(chunk: &ArrayRef, plan: ChunkGather) -> VortexResult<ArrayRef> {
533    if let [start] = plan.starts.as_slice() {
534        let start = usize::try_from(*start)?;
535        return chunk.slice(start..start + plan.total);
536    }
537    chunk.take(contiguous_runs(plan.starts, plan.lengths, plan.total))
538}
539
540/// Builds a `PiecewiseSequenceArray` of contiguous (unit multiplier) runs whose lengths sum to
541/// `total`.
542fn contiguous_runs(starts: Vec<u64>, lengths: Vec<u64>, total: usize) -> ArrayRef {
543    let count = starts.len();
544    debug_assert_eq!(count, lengths.len());
545    let starts = PrimitiveArray::new(Buffer::from(starts), Validity::NonNullable).into_array();
546    let lengths = match lengths.first() {
547        Some(&first) if lengths.iter().all(|&length| length == first) => {
548            ConstantArray::new(first, count).into_array()
549        }
550        _ => PrimitiveArray::new(Buffer::from(lengths), Validity::NonNullable).into_array(),
551    };
552    let multipliers = ConstantArray::new(1u64, count).into_array();
553    // SAFETY: starts, lengths, and multipliers are non-nullable u64 arrays of equal length, and
554    // `total` is the sum of the lengths.
555    unsafe { PiecewiseSequenceArray::new_unchecked(starts, lengths, multipliers, total) }
556        .into_array()
557}
558
559#[cfg(test)]
560mod tests {
561    use vortex_buffer::Buffer;
562    use vortex_buffer::bitbuffer;
563    use vortex_buffer::buffer;
564    use vortex_error::VortexResult;
565
566    use crate::ArrayRef;
567    use crate::Canonical;
568    use crate::IntoArray;
569    use crate::VortexSessionExecute;
570    use crate::array_session;
571    use crate::arrays::BoolArray;
572    use crate::arrays::ChunkedArray;
573    use crate::arrays::ConstantArray;
574    use crate::arrays::FixedSizeListArray;
575    use crate::arrays::PiecewiseSequenceArray;
576    use crate::arrays::PrimitiveArray;
577    use crate::arrays::StructArray;
578    use crate::arrays::chunked::ChunkedArrayExt;
579    use crate::assert_arrays_eq;
580    use crate::compute::conformance::take::test_take_conformance;
581    use crate::dtype::DType;
582    use crate::dtype::FieldNames;
583    use crate::dtype::Nullability;
584    use crate::dtype::PType;
585    use crate::scalar::Scalar;
586    use crate::validity::Validity;
587
588    fn chunked_i32() -> VortexResult<ChunkedArray> {
589        ChunkedArray::try_new(
590            vec![
591                buffer![0i32, 1, 2, 3, 4].into_array(),
592                buffer![5i32, 6, 7, 8, 9].into_array(),
593                buffer![10i32, 11, 12, 13, 14].into_array(),
594            ],
595            DType::Primitive(PType::I32, Nullability::NonNullable),
596        )
597    }
598
599    fn contiguous_pieces(starts: &[u64], lengths: &[u64]) -> VortexResult<ArrayRef> {
600        let len = usize::try_from(lengths.iter().sum::<u64>())?;
601        Ok(PiecewiseSequenceArray::try_new(
602            starts.iter().copied().collect::<Buffer<u64>>().into_array(),
603            lengths
604                .iter()
605                .copied()
606                .collect::<Buffer<u64>>()
607                .into_array(),
608            ConstantArray::new(1u64, starts.len()).into_array(),
609            len,
610        )?
611        .into_array())
612    }
613
614    #[test]
615    fn test_take_piecewise_monotonic_spanning_chunks() -> VortexResult<()> {
616        let mut ctx = array_session().create_execution_ctx();
617        let arr = chunked_i32()?;
618
619        // The second piece crosses the first chunk boundary, the third spans the last two chunks.
620        let indices = contiguous_pieces(&[1, 4, 9], &[3, 4, 6])?;
621        let result = arr.take(indices)?;
622
623        assert_arrays_eq!(
624            result,
625            PrimitiveArray::from_iter([1i32, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14]),
626            &mut ctx
627        );
628        Ok(())
629    }
630
631    #[test]
632    fn test_take_piecewise_interleaved() -> VortexResult<()> {
633        let mut ctx = array_session().create_execution_ctx();
634        let arr = chunked_i32()?;
635
636        // Pieces visit chunks out of order, forcing the reorder take.
637        let indices = contiguous_pieces(&[12, 2, 7, 0], &[3, 2, 3, 1])?;
638        let result = arr.take(indices)?;
639
640        assert_arrays_eq!(
641            result,
642            PrimitiveArray::from_iter([12i32, 13, 14, 2, 3, 7, 8, 9, 0]),
643            &mut ctx
644        );
645        Ok(())
646    }
647
648    #[test]
649    fn test_take_piecewise_whole_array() -> VortexResult<()> {
650        let mut ctx = array_session().create_execution_ctx();
651        let arr = chunked_i32()?;
652
653        let indices = contiguous_pieces(&[0], &[15])?;
654        let result = arr.take(indices)?;
655
656        assert_arrays_eq!(result, PrimitiveArray::from_iter(0i32..15), &mut ctx);
657        Ok(())
658    }
659
660    #[test]
661    fn test_take_piecewise_across_empty_chunk() -> VortexResult<()> {
662        let mut ctx = array_session().create_execution_ctx();
663        let arr = ChunkedArray::try_new(
664            vec![
665                buffer![0i32, 1, 2, 3, 4].into_array(),
666                PrimitiveArray::empty::<i32>(Nullability::NonNullable).into_array(),
667                buffer![5i32, 6, 7, 8, 9].into_array(),
668            ],
669            DType::Primitive(PType::I32, Nullability::NonNullable),
670        )?;
671
672        let indices = contiguous_pieces(&[3], &[4])?;
673        let result = arr.take(indices)?;
674
675        assert_arrays_eq!(result, PrimitiveArray::from_iter([3i32, 4, 5, 6]), &mut ctx);
676        Ok(())
677    }
678
679    #[test]
680    fn test_take_piecewise_adjacent_pieces_merge() -> VortexResult<()> {
681        let mut ctx = array_session().create_execution_ctx();
682        let arr = chunked_i32()?;
683
684        // The first two pieces are contiguous within one chunk, so they merge into a single run
685        // that gathers as a zero-copy slice.
686        let indices = contiguous_pieces(&[0, 2, 7], &[2, 2, 2])?;
687        let result = arr.take(indices)?;
688
689        assert_arrays_eq!(
690            result,
691            PrimitiveArray::from_iter([0i32, 1, 2, 3, 7, 8]),
692            &mut ctx
693        );
694        Ok(())
695    }
696
697    #[test]
698    fn test_take_piecewise_same_chunk_out_of_order() -> VortexResult<()> {
699        let mut ctx = array_session().create_execution_ctx();
700        let arr = chunked_i32()?;
701
702        // Non-contiguous pieces in the same chunk must stay separate runs in piece order.
703        let indices = contiguous_pieces(&[2, 0], &[2, 2])?;
704        let result = arr.take(indices)?;
705
706        assert_arrays_eq!(result, PrimitiveArray::from_iter([2i32, 3, 0, 1]), &mut ctx);
707        Ok(())
708    }
709
710    #[test]
711    fn test_take_piecewise_zero_length_pieces() -> VortexResult<()> {
712        let mut ctx = array_session().create_execution_ctx();
713        let arr = chunked_i32()?;
714
715        let indices = contiguous_pieces(&[5, 0, 2], &[0, 4, 0])?;
716        let result = arr.take(indices)?;
717
718        assert_arrays_eq!(result, PrimitiveArray::from_iter([0i32, 1, 2, 3]), &mut ctx);
719        Ok(())
720    }
721
722    #[test]
723    fn test_take_piecewise_out_of_bounds() -> VortexResult<()> {
724        let mut ctx = array_session().create_execution_ctx();
725        let arr = chunked_i32()?;
726
727        let indices = contiguous_pieces(&[12], &[5])?;
728        let result = arr
729            .take(indices)
730            .and_then(|taken| taken.execute::<Canonical>(&mut ctx));
731
732        assert!(result.is_err());
733        Ok(())
734    }
735
736    #[test]
737    fn test_take_piecewise_non_unit_multiplier() -> VortexResult<()> {
738        let mut ctx = array_session().create_execution_ctx();
739        let arr = chunked_i32()?;
740
741        // Multiplier 2 falls back to the generic path but must stay correct.
742        let indices = PiecewiseSequenceArray::try_new(
743            buffer![0u64, 1].into_array(),
744            buffer![5u64, 3].into_array(),
745            buffer![2u64, 4].into_array(),
746            8,
747        )?
748        .into_array();
749        let result = arr.take(indices)?;
750
751        assert_arrays_eq!(
752            result,
753            PrimitiveArray::from_iter([0i32, 2, 4, 6, 8, 1, 5, 9]),
754            &mut ctx
755        );
756        Ok(())
757    }
758
759    #[test]
760    fn test_take_piecewise_nullable_values() -> VortexResult<()> {
761        let mut ctx = array_session().create_execution_ctx();
762        let arr = ChunkedArray::try_new(
763            vec![
764                PrimitiveArray::from_option_iter([Some(0i32), None, Some(2)]).into_array(),
765                PrimitiveArray::from_option_iter([None, Some(4i32), Some(5)]).into_array(),
766            ],
767            DType::Primitive(PType::I32, Nullability::Nullable),
768        )?;
769
770        let indices = contiguous_pieces(&[4, 1], &[2, 3])?;
771        let result = arr.take(indices)?;
772
773        assert_arrays_eq!(
774            result,
775            PrimitiveArray::from_option_iter([Some(4i32), Some(5), None, Some(2), None]),
776            &mut ctx
777        );
778        Ok(())
779    }
780
781    #[test]
782    fn test_take_fsl_over_chunked_elements() -> VortexResult<()> {
783        let mut ctx = array_session().create_execution_ctx();
784        // Chunk boundaries at 8 and 13 do not line up with the list size of 3.
785        let elements = ChunkedArray::try_new(
786            vec![
787                PrimitiveArray::from_iter(0i32..8).into_array(),
788                PrimitiveArray::from_iter(8i32..13).into_array(),
789                PrimitiveArray::from_iter(13i32..18).into_array(),
790            ],
791            DType::Primitive(PType::I32, Nullability::NonNullable),
792        )?
793        .into_array();
794        let fsl = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 6)?;
795
796        let indices = buffer![4u64, 0, 2, 5, 1, 4].into_array();
797        let result = fsl.take(indices.clone())?;
798        let expected = FixedSizeListArray::try_new(
799            PrimitiveArray::from_iter(0i32..18).into_array(),
800            3,
801            Validity::NonNullable,
802            6,
803        )?
804        .take(indices)?;
805
806        assert_arrays_eq!(result, expected, &mut ctx);
807        Ok(())
808    }
809
810    #[test]
811    fn test_take_fsl_over_chunked_elements_conformance() -> VortexResult<()> {
812        let elements = ChunkedArray::try_new(
813            vec![
814                PrimitiveArray::from_iter(0i32..8).into_array(),
815                PrimitiveArray::from_iter(8i32..13).into_array(),
816                PrimitiveArray::from_iter(13i32..18).into_array(),
817            ],
818            DType::Primitive(PType::I32, Nullability::NonNullable),
819        )?
820        .into_array();
821        let fsl = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 6)?;
822        test_take_conformance(
823            &fsl.into_array(),
824            &mut array_session().create_execution_ctx(),
825        );
826        Ok(())
827    }
828
829    #[test]
830    fn test_take_chunked_fsl() -> VortexResult<()> {
831        let mut ctx = array_session().create_execution_ctx();
832        let c0 = FixedSizeListArray::try_new(
833            PrimitiveArray::from_iter(0i32..6).into_array(),
834            2,
835            Validity::NonNullable,
836            3,
837        )?;
838        let c1 = FixedSizeListArray::try_new(
839            PrimitiveArray::from_iter(6i32..10).into_array(),
840            2,
841            Validity::NonNullable,
842            2,
843        )?;
844        let dtype = c0.dtype().clone();
845        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
846
847        let indices = buffer![4u64, 0, 3, 3, 1].into_array();
848        let result = arr.take(indices.clone())?;
849        let expected = FixedSizeListArray::try_new(
850            PrimitiveArray::from_iter(0i32..10).into_array(),
851            2,
852            Validity::NonNullable,
853            5,
854        )?
855        .take(indices)?;
856
857        assert_arrays_eq!(result, expected, &mut ctx);
858        Ok(())
859    }
860
861    #[test]
862    fn test_take_chunked_fsl_nullable() -> VortexResult<()> {
863        let mut ctx = array_session().create_execution_ctx();
864        let c0 = FixedSizeListArray::try_new(
865            PrimitiveArray::from_iter(0i32..6).into_array(),
866            2,
867            Validity::Array(bitbuffer![1 0 1].into_array()),
868            3,
869        )?;
870        let c1 = FixedSizeListArray::try_new(
871            PrimitiveArray::from_iter(6i32..10).into_array(),
872            2,
873            Validity::AllValid,
874            2,
875        )?;
876        let dtype = c0.dtype().clone();
877        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
878
879        let indices =
880            PrimitiveArray::from_option_iter([Some(4u64), None, Some(0), Some(1)]).into_array();
881        let result = arr.take(indices.clone())?;
882        let expected = FixedSizeListArray::try_new(
883            PrimitiveArray::from_iter(0i32..10).into_array(),
884            2,
885            Validity::Array(bitbuffer![1 0 1 1 1].into_array()),
886            5,
887        )?
888        .take(indices)?;
889
890        assert_arrays_eq!(result, expected, &mut ctx);
891        Ok(())
892    }
893
894    #[test]
895    fn test_take_chunked_fsl_non_fsl_chunk() -> VortexResult<()> {
896        let mut ctx = array_session().create_execution_ctx();
897        let c0 = FixedSizeListArray::try_new(
898            PrimitiveArray::from_iter(0i32..6).into_array(),
899            2,
900            Validity::NonNullable,
901            3,
902        )?;
903        let c1 = FixedSizeListArray::try_new(
904            PrimitiveArray::from_iter(6i32..10).into_array(),
905            2,
906            Validity::NonNullable,
907            2,
908        )?;
909        let dtype = c0.dtype().clone();
910        // Wrap one chunk in a nested chunked array so it is not FSL-encoded; the dtype-based
911        // swizzle must still handle it.
912        let nested = ChunkedArray::try_new(vec![c1.into_array()], dtype.clone())?;
913        let arr = ChunkedArray::try_new(vec![c0.into_array(), nested.into_array()], dtype)?;
914
915        let indices = buffer![4u64, 0, 3, 1].into_array();
916        let result = arr.take(indices.clone())?;
917        let expected = FixedSizeListArray::try_new(
918            PrimitiveArray::from_iter(0i32..10).into_array(),
919            2,
920            Validity::NonNullable,
921            5,
922        )?
923        .take(indices)?;
924
925        assert_arrays_eq!(result, expected, &mut ctx);
926        Ok(())
927    }
928
929    #[test]
930    fn test_take_chunked_fsl_constant_chunk() -> VortexResult<()> {
931        let mut ctx = array_session().create_execution_ctx();
932        let c0 = FixedSizeListArray::try_new(
933            PrimitiveArray::from_iter(0i32..6).into_array(),
934            2,
935            Validity::AllValid,
936            3,
937        )?;
938        let dtype = c0.dtype().clone();
939        // A constant chunk is only logically FSL; the swizzle canonicalizes it in place.
940        let c1 = ConstantArray::new(Scalar::null(dtype.clone()), 2).into_array();
941        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1], dtype)?;
942
943        let indices = buffer![4u64, 0, 3, 1].into_array();
944        let result = arr.take(indices.clone())?;
945        let expected = FixedSizeListArray::try_new(
946            PrimitiveArray::from_iter(0i32..10).into_array(),
947            2,
948            Validity::Array(bitbuffer![1 1 1 0 0].into_array()),
949            5,
950        )?
951        .take(indices)?;
952
953        assert_arrays_eq!(result, expected, &mut ctx);
954        Ok(())
955    }
956
957    #[test]
958    fn test_take_piecewise_chunked_fsl() -> VortexResult<()> {
959        let mut ctx = array_session().create_execution_ctx();
960        let c0 = FixedSizeListArray::try_new(
961            PrimitiveArray::from_iter(0i32..6).into_array(),
962            2,
963            Validity::NonNullable,
964            3,
965        )?;
966        let c1 = FixedSizeListArray::try_new(
967            PrimitiveArray::from_iter(6i32..10).into_array(),
968            2,
969            Validity::NonNullable,
970            2,
971        )?;
972        let dtype = c0.dtype().clone();
973        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
974        let reference = FixedSizeListArray::try_new(
975            PrimitiveArray::from_iter(0i32..10).into_array(),
976            2,
977            Validity::NonNullable,
978            5,
979        )?;
980
981        // A monotonic run crossing the chunk boundary.
982        let indices = contiguous_pieces(&[1], &[3])?;
983        assert_arrays_eq!(
984            arr.take(indices.clone())?,
985            reference.take(indices)?,
986            &mut ctx
987        );
988
989        // Out-of-order pieces forcing the reorder take.
990        let indices = contiguous_pieces(&[3, 0], &[2, 2])?;
991        assert_arrays_eq!(
992            arr.take(indices.clone())?,
993            reference.take(indices)?,
994            &mut ctx
995        );
996        Ok(())
997    }
998
999    #[test]
1000    fn test_take_chunked_struct_nested_flatten() -> VortexResult<()> {
1001        let mut ctx = array_session().create_execution_ctx();
1002        let s0 = StructArray::try_new(
1003            ["a"].into(),
1004            vec![buffer![0i32, 1, 2].into_array()],
1005            3,
1006            Validity::NonNullable,
1007        )?;
1008        let s1 = StructArray::try_new(
1009            ["a"].into(),
1010            vec![buffer![3i32, 4].into_array()],
1011            2,
1012            Validity::NonNullable,
1013        )?;
1014        let dtype = s0.dtype().clone();
1015        let arr = ChunkedArray::try_new(vec![s0.into_array(), s1.into_array()], dtype)?;
1016
1017        let result = arr.take(buffer![4u64, 0, 2, 4].into_array())?;
1018        let expected = StructArray::try_new(
1019            ["a"].into(),
1020            vec![buffer![4i32, 0, 2, 4].into_array()],
1021            4,
1022            Validity::NonNullable,
1023        )?;
1024
1025        assert_arrays_eq!(result, expected, &mut ctx);
1026        Ok(())
1027    }
1028
1029    #[test]
1030    fn test_take_chunked_fsl_conformance() -> VortexResult<()> {
1031        let c0 = FixedSizeListArray::try_new(
1032            PrimitiveArray::from_iter(0i32..6).into_array(),
1033            2,
1034            Validity::NonNullable,
1035            3,
1036        )?;
1037        let c1 = FixedSizeListArray::try_new(
1038            PrimitiveArray::from_iter(6i32..10).into_array(),
1039            2,
1040            Validity::NonNullable,
1041            2,
1042        )?;
1043        let dtype = c0.dtype().clone();
1044        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
1045        test_take_conformance(
1046            &arr.into_array(),
1047            &mut array_session().create_execution_ctx(),
1048        );
1049        Ok(())
1050    }
1051
1052    #[test]
1053    fn test_take() {
1054        let mut ctx = array_session().create_execution_ctx();
1055        let a = buffer![1i32, 2, 3].into_array();
1056        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1057            .unwrap();
1058        assert_eq!(arr.nchunks(), 3);
1059        assert_eq!(arr.len(), 9);
1060        let indices = buffer![0u64, 0, 6, 4].into_array();
1061
1062        let result = arr.take(indices).unwrap();
1063        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 1, 1, 2]), &mut ctx);
1064    }
1065
1066    #[test]
1067    fn test_take_nullable_values() {
1068        let mut ctx = array_session().create_execution_ctx();
1069        let a = PrimitiveArray::new(buffer![1i32, 2, 3], Validity::AllValid).into_array();
1070        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1071            .unwrap();
1072        assert_eq!(arr.nchunks(), 3);
1073        assert_eq!(arr.len(), 9);
1074        let indices = PrimitiveArray::new(buffer![0u64, 0, 6, 4], Validity::NonNullable);
1075
1076        let result = arr.take(indices.into_array()).unwrap();
1077        assert_arrays_eq!(
1078            result,
1079            PrimitiveArray::from_option_iter([1i32, 1, 1, 2].map(Some)),
1080            &mut ctx
1081        );
1082    }
1083
1084    #[test]
1085    fn test_take_nullable_indices() {
1086        let mut ctx = array_session().create_execution_ctx();
1087        let a = buffer![1i32, 2, 3].into_array();
1088        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1089            .unwrap();
1090        assert_eq!(arr.nchunks(), 3);
1091        assert_eq!(arr.len(), 9);
1092        let indices = PrimitiveArray::new(
1093            buffer![0u64, 0, 6, 4],
1094            Validity::Array(bitbuffer![1 0 0 1].into_array()),
1095        );
1096
1097        let result = arr.take(indices.into_array()).unwrap();
1098        assert_arrays_eq!(
1099            result,
1100            PrimitiveArray::from_option_iter([Some(1i32), None, None, Some(2)]),
1101            &mut ctx
1102        );
1103    }
1104
1105    #[test]
1106    fn test_take_nullable_struct() {
1107        let mut ctx = array_session().create_execution_ctx();
1108        let struct_array =
1109            StructArray::try_new(FieldNames::default(), vec![], 100, Validity::NonNullable)
1110                .unwrap();
1111
1112        let arr = ChunkedArray::from_iter(vec![
1113            struct_array.clone().into_array(),
1114            struct_array.into_array(),
1115        ]);
1116
1117        let result = arr
1118            .take(PrimitiveArray::from_option_iter(vec![Some(0), None, Some(101)]).into_array())
1119            .unwrap();
1120
1121        let expect = StructArray::try_new(
1122            FieldNames::default(),
1123            vec![],
1124            3,
1125            Validity::Array(BoolArray::from_iter(vec![true, false, true]).into_array()),
1126        )
1127        .unwrap();
1128        assert_arrays_eq!(result, expect, &mut ctx);
1129    }
1130
1131    #[test]
1132    fn test_empty_take() {
1133        let mut ctx = array_session().create_execution_ctx();
1134        let a = buffer![1i32, 2, 3].into_array();
1135        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1136            .unwrap();
1137        assert_eq!(arr.nchunks(), 3);
1138        assert_eq!(arr.len(), 9);
1139
1140        let indices = PrimitiveArray::empty::<u64>(Nullability::NonNullable);
1141        let result = arr.take(indices.into_array()).unwrap();
1142
1143        assert!(result.is_empty());
1144        assert_eq!(result.dtype(), arr.dtype());
1145        assert_arrays_eq!(
1146            result,
1147            PrimitiveArray::empty::<i32>(Nullability::NonNullable),
1148            &mut ctx
1149        );
1150    }
1151
1152    #[test]
1153    fn test_take_shuffled_indices() -> VortexResult<()> {
1154        let mut ctx = array_session().create_execution_ctx();
1155        let c0 = buffer![0i32, 1, 2].into_array();
1156        let c1 = buffer![3i32, 4, 5].into_array();
1157        let c2 = buffer![6i32, 7, 8].into_array();
1158        let arr = ChunkedArray::try_new(
1159            vec![c0, c1, c2],
1160            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1161                .dtype()
1162                .clone(),
1163        )?;
1164
1165        // Fully shuffled indices that cross every chunk boundary.
1166        let indices = buffer![8u64, 0, 5, 3, 2, 7, 1, 6, 4].into_array();
1167        let result = arr.take(indices)?;
1168
1169        assert_arrays_eq!(
1170            result,
1171            PrimitiveArray::from_iter([8i32, 0, 5, 3, 2, 7, 1, 6, 4]),
1172            &mut ctx
1173        );
1174        Ok(())
1175    }
1176
1177    #[test]
1178    fn test_take_shuffled_duplicates_with_empty_chunks() -> VortexResult<()> {
1179        let mut ctx = array_session().create_execution_ctx();
1180        let empty = PrimitiveArray::empty::<i32>(Nullability::NonNullable).into_array();
1181        let arr = ChunkedArray::try_new(
1182            vec![
1183                empty.clone(),
1184                buffer![0i32, 1].into_array(),
1185                empty.clone(),
1186                buffer![2i32, 3].into_array(),
1187                empty,
1188            ],
1189            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1190                .dtype()
1191                .clone(),
1192        )?;
1193
1194        let result = arr.take(buffer![3u64, 0, 2, 0, 3, 1, 2].into_array())?;
1195
1196        assert_arrays_eq!(
1197            result,
1198            PrimitiveArray::from_iter([3i32, 0, 2, 0, 3, 1, 2]),
1199            &mut ctx
1200        );
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn test_take_small_shuffled_fixed_size_lists() -> VortexResult<()> {
1206        let mut ctx = array_session().create_execution_ctx();
1207        let first = FixedSizeListArray::new(
1208            buffer![0i32, 1, 2, 3].into_array(),
1209            2,
1210            Validity::NonNullable,
1211            2,
1212        )
1213        .into_array();
1214        let second = FixedSizeListArray::new(
1215            buffer![4i32, 5, 6, 7].into_array(),
1216            2,
1217            Validity::NonNullable,
1218            2,
1219        )
1220        .into_array();
1221        let dtype = first.dtype().clone();
1222        let array = ChunkedArray::try_new(vec![first, second], dtype)?;
1223
1224        let result = array.take(buffer![3u64, 0, 2, 1, 3].into_array())?;
1225        let expected = FixedSizeListArray::new(
1226            buffer![6i32, 7, 0, 1, 4, 5, 2, 3, 6, 7].into_array(),
1227            2,
1228            Validity::NonNullable,
1229            5,
1230        );
1231
1232        assert_arrays_eq!(result, expected, &mut ctx);
1233        Ok(())
1234    }
1235
1236    #[test]
1237    fn test_take_shuffled_large() -> VortexResult<()> {
1238        let mut ctx = array_session().create_execution_ctx();
1239        let nchunks: i32 = 100;
1240        let chunk_len: i32 = 1_000;
1241        let total = nchunks * chunk_len;
1242
1243        let chunks: Vec<_> = (0..nchunks)
1244            .map(|c| {
1245                let start = c * chunk_len;
1246                PrimitiveArray::from_iter(start..start + chunk_len).into_array()
1247            })
1248            .collect();
1249        let dtype = chunks[0].dtype().clone();
1250        let arr = ChunkedArray::try_new(chunks, dtype)?;
1251
1252        // Fisher-Yates shuffle with a fixed seed for determinism.
1253        let mut indices: Vec<u64> = (0..u64::try_from(total)?).collect();
1254        let mut seed: u64 = 0xdeadbeef;
1255        for i in (1..indices.len()).rev() {
1256            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1257            let j = (seed >> 33) as usize % (i + 1);
1258            indices.swap(i, j);
1259        }
1260
1261        let indices_arr = PrimitiveArray::new(Buffer::from(indices.clone()), Validity::NonNullable);
1262        let result = arr.take(indices_arr.into_array())?;
1263
1264        // Verify every element.
1265        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
1266        let result_vals = result.as_slice::<i32>();
1267        for (pos, &idx) in indices.iter().enumerate() {
1268            assert_eq!(
1269                result_vals[pos],
1270                i32::try_from(idx)?,
1271                "mismatch at position {pos}"
1272            );
1273        }
1274        Ok(())
1275    }
1276
1277    #[test]
1278    fn test_take_null_indices() -> VortexResult<()> {
1279        let mut ctx = array_session().create_execution_ctx();
1280        let c0 = buffer![10i32, 20, 30].into_array();
1281        let c1 = buffer![40i32, 50, 60].into_array();
1282        let arr = ChunkedArray::try_new(
1283            vec![c0, c1],
1284            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1285                .dtype()
1286                .clone(),
1287        )?;
1288
1289        // Indices with nulls scattered across chunk boundaries.
1290        let indices =
1291            PrimitiveArray::from_option_iter([Some(5u64), None, Some(0), Some(3), None, Some(2)]);
1292        let result = arr.take(indices.into_array())?;
1293
1294        assert_arrays_eq!(
1295            result,
1296            PrimitiveArray::from_option_iter([
1297                Some(60i32),
1298                None,
1299                Some(10),
1300                Some(40),
1301                None,
1302                Some(30)
1303            ]),
1304            &mut ctx
1305        );
1306        Ok(())
1307    }
1308
1309    #[test]
1310    fn test_take_sorted_indices() -> VortexResult<()> {
1311        let mut ctx = array_session().create_execution_ctx();
1312        let arr = chunked_i32()?;
1313
1314        // Strictly increasing indices spanning chunks hit the sorted fast path.
1315        let indices = buffer![1u64, 4, 5, 9, 10, 14].into_array();
1316        let result = arr.take(indices)?;
1317
1318        assert_arrays_eq!(
1319            result,
1320            PrimitiveArray::from_iter([1i32, 4, 5, 9, 10, 14]),
1321            &mut ctx
1322        );
1323        Ok(())
1324    }
1325
1326    #[test]
1327    fn test_take_out_of_bounds() -> VortexResult<()> {
1328        let mut ctx = array_session().create_execution_ctx();
1329        let arr = chunked_i32()?;
1330
1331        // Sorted indices hit the fast path, unsorted the generic path; both must fail.
1332        for indices in [buffer![0u64, 100], buffer![100u64, 0]] {
1333            let result = arr
1334                .take(indices.into_array())
1335                .and_then(|taken| taken.execute::<Canonical>(&mut ctx));
1336            assert!(result.is_err(), "expected out-of-bounds error");
1337        }
1338        Ok(())
1339    }
1340
1341    #[test]
1342    fn test_take_all_null_indices() -> VortexResult<()> {
1343        let mut ctx = array_session().create_execution_ctx();
1344        let arr = chunked_i32()?;
1345
1346        let indices = PrimitiveArray::from_option_iter([None::<u64>, None]).into_array();
1347        let result = arr.take(indices)?;
1348
1349        assert_arrays_eq!(
1350            result,
1351            PrimitiveArray::from_option_iter([None::<i32>, None]),
1352            &mut ctx
1353        );
1354        Ok(())
1355    }
1356
1357    #[test]
1358    fn test_take_chunked_conformance() {
1359        let a = buffer![1i32, 2, 3].into_array();
1360        let b = buffer![4i32, 5].into_array();
1361        let arr = ChunkedArray::try_new(
1362            vec![a, b],
1363            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1364                .dtype()
1365                .clone(),
1366        )
1367        .unwrap();
1368        test_take_conformance(
1369            &arr.into_array(),
1370            &mut array_session().create_execution_ctx(),
1371        );
1372
1373        // Test with nullable chunked array
1374        let a = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]);
1375        let b = PrimitiveArray::from_option_iter([Some(4i32), Some(5)]);
1376        let dtype = a.dtype().clone();
1377        let arr = ChunkedArray::try_new(vec![a.into_array(), b.into_array()], dtype).unwrap();
1378        test_take_conformance(
1379            &arr.into_array(),
1380            &mut array_session().create_execution_ctx(),
1381        );
1382
1383        // Test with multiple identical chunks
1384        let chunk = buffer![10i32, 20, 30, 40, 50].into_array();
1385        let arr = ChunkedArray::try_new(
1386            vec![chunk.clone(), chunk.clone(), chunk.clone()],
1387            chunk.dtype().clone(),
1388        )
1389        .unwrap();
1390        test_take_conformance(
1391            &arr.into_array(),
1392            &mut array_session().create_execution_ctx(),
1393        );
1394    }
1395}