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)?;
799        let expected = FixedSizeListArray::try_new(
800            PrimitiveArray::from_iter([
801                12i32, 13, 14, 0, 1, 2, 6, 7, 8, 15, 16, 17, 3, 4, 5, 12, 13, 14,
802            ])
803            .into_array(),
804            3,
805            Validity::NonNullable,
806            6,
807        )?;
808
809        assert_arrays_eq!(result, expected, &mut ctx);
810        Ok(())
811    }
812
813    #[test]
814    fn test_take_fsl_over_chunked_elements_conformance() -> VortexResult<()> {
815        let elements = ChunkedArray::try_new(
816            vec![
817                PrimitiveArray::from_iter(0i32..8).into_array(),
818                PrimitiveArray::from_iter(8i32..13).into_array(),
819                PrimitiveArray::from_iter(13i32..18).into_array(),
820            ],
821            DType::Primitive(PType::I32, Nullability::NonNullable),
822        )?
823        .into_array();
824        let fsl = FixedSizeListArray::try_new(elements, 3, Validity::NonNullable, 6)?;
825        test_take_conformance(
826            &fsl.into_array(),
827            &mut array_session().create_execution_ctx(),
828        );
829        Ok(())
830    }
831
832    #[test]
833    fn test_take_chunked_fsl() -> VortexResult<()> {
834        let mut ctx = array_session().create_execution_ctx();
835        let c0 = FixedSizeListArray::try_new(
836            PrimitiveArray::from_iter(0i32..6).into_array(),
837            2,
838            Validity::NonNullable,
839            3,
840        )?;
841        let c1 = FixedSizeListArray::try_new(
842            PrimitiveArray::from_iter(6i32..10).into_array(),
843            2,
844            Validity::NonNullable,
845            2,
846        )?;
847        let dtype = c0.dtype().clone();
848        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
849
850        let indices = buffer![4u64, 0, 3, 3, 1].into_array();
851        let result = arr.take(indices)?;
852        let expected = FixedSizeListArray::try_new(
853            PrimitiveArray::from_iter([8i32, 9, 0, 1, 6, 7, 6, 7, 2, 3]).into_array(),
854            2,
855            Validity::NonNullable,
856            5,
857        )?;
858
859        assert_arrays_eq!(result, expected, &mut ctx);
860        Ok(())
861    }
862
863    #[test]
864    fn test_take_chunked_fsl_nullable() -> VortexResult<()> {
865        let mut ctx = array_session().create_execution_ctx();
866        let c0 = FixedSizeListArray::try_new(
867            PrimitiveArray::from_iter(0i32..6).into_array(),
868            2,
869            Validity::Array(bitbuffer![1 0 1].into_array()),
870            3,
871        )?;
872        let c1 = FixedSizeListArray::try_new(
873            PrimitiveArray::from_iter(6i32..10).into_array(),
874            2,
875            Validity::AllValid,
876            2,
877        )?;
878        let dtype = c0.dtype().clone();
879        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
880
881        let indices =
882            PrimitiveArray::from_option_iter([Some(4u64), None, Some(0), Some(1)]).into_array();
883        let result = arr.take(indices)?;
884        let expected = FixedSizeListArray::try_new(
885            PrimitiveArray::from_iter([8i32, 9, 0, 0, 0, 1, 0, 0]).into_array(),
886            2,
887            Validity::Array(bitbuffer![1 0 1 0].into_array()),
888            4,
889        )?;
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)?;
918        let expected = FixedSizeListArray::try_new(
919            PrimitiveArray::from_iter([8i32, 9, 0, 1, 6, 7, 2, 3]).into_array(),
920            2,
921            Validity::NonNullable,
922            4,
923        )?;
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)?;
945        let expected = FixedSizeListArray::try_new(
946            PrimitiveArray::from_iter([0i32, 0, 0, 1, 0, 0, 2, 3]).into_array(),
947            2,
948            Validity::Array(bitbuffer![0 1 0 1].into_array()),
949            4,
950        )?;
951
952        assert_arrays_eq!(result, expected, &mut ctx);
953        Ok(())
954    }
955
956    #[test]
957    fn test_take_piecewise_chunked_fsl() -> VortexResult<()> {
958        let mut ctx = array_session().create_execution_ctx();
959        let c0 = FixedSizeListArray::try_new(
960            PrimitiveArray::from_iter(0i32..6).into_array(),
961            2,
962            Validity::NonNullable,
963            3,
964        )?;
965        let c1 = FixedSizeListArray::try_new(
966            PrimitiveArray::from_iter(6i32..10).into_array(),
967            2,
968            Validity::NonNullable,
969            2,
970        )?;
971        let dtype = c0.dtype().clone();
972        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
973        let reference = FixedSizeListArray::try_new(
974            PrimitiveArray::from_iter(0i32..10).into_array(),
975            2,
976            Validity::NonNullable,
977            5,
978        )?;
979
980        // A monotonic run crossing the chunk boundary.
981        let indices = contiguous_pieces(&[1], &[3])?;
982        let result = arr.take(indices.clone())?;
983        assert_arrays_eq!(&result, reference.take(indices)?, &mut ctx);
984        let expected = FixedSizeListArray::try_new(
985            PrimitiveArray::from_iter([2i32, 3, 4, 5, 6, 7]).into_array(),
986            2,
987            Validity::NonNullable,
988            3,
989        )?;
990        assert_arrays_eq!(result, expected, &mut ctx);
991
992        // Out-of-order pieces forcing the reorder take.
993        let indices = contiguous_pieces(&[3, 0], &[2, 2])?;
994        let result = arr.take(indices.clone())?;
995        assert_arrays_eq!(&result, reference.take(indices)?, &mut ctx);
996        let expected = FixedSizeListArray::try_new(
997            PrimitiveArray::from_iter([6i32, 7, 8, 9, 0, 1, 2, 3]).into_array(),
998            2,
999            Validity::NonNullable,
1000            4,
1001        )?;
1002        assert_arrays_eq!(result, expected, &mut ctx);
1003        Ok(())
1004    }
1005
1006    #[test]
1007    fn test_take_chunked_struct_nested_flatten() -> VortexResult<()> {
1008        let mut ctx = array_session().create_execution_ctx();
1009        let s0 = StructArray::try_new(
1010            ["a"].into(),
1011            vec![buffer![0i32, 1, 2].into_array()],
1012            3,
1013            Validity::NonNullable,
1014        )?;
1015        let s1 = StructArray::try_new(
1016            ["a"].into(),
1017            vec![buffer![3i32, 4].into_array()],
1018            2,
1019            Validity::NonNullable,
1020        )?;
1021        let dtype = s0.dtype().clone();
1022        let arr = ChunkedArray::try_new(vec![s0.into_array(), s1.into_array()], dtype)?;
1023
1024        let result = arr.take(buffer![4u64, 0, 2, 4].into_array())?;
1025        let expected = StructArray::try_new(
1026            ["a"].into(),
1027            vec![buffer![4i32, 0, 2, 4].into_array()],
1028            4,
1029            Validity::NonNullable,
1030        )?;
1031
1032        assert_arrays_eq!(result, expected, &mut ctx);
1033        Ok(())
1034    }
1035
1036    #[test]
1037    fn test_take_chunked_fsl_conformance() -> VortexResult<()> {
1038        let c0 = FixedSizeListArray::try_new(
1039            PrimitiveArray::from_iter(0i32..6).into_array(),
1040            2,
1041            Validity::NonNullable,
1042            3,
1043        )?;
1044        let c1 = FixedSizeListArray::try_new(
1045            PrimitiveArray::from_iter(6i32..10).into_array(),
1046            2,
1047            Validity::NonNullable,
1048            2,
1049        )?;
1050        let dtype = c0.dtype().clone();
1051        let arr = ChunkedArray::try_new(vec![c0.into_array(), c1.into_array()], dtype)?;
1052        test_take_conformance(
1053            &arr.into_array(),
1054            &mut array_session().create_execution_ctx(),
1055        );
1056        Ok(())
1057    }
1058
1059    #[test]
1060    fn test_take() {
1061        let mut ctx = array_session().create_execution_ctx();
1062        let a = buffer![1i32, 2, 3].into_array();
1063        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1064            .unwrap();
1065        assert_eq!(arr.nchunks(), 3);
1066        assert_eq!(arr.len(), 9);
1067        let indices = buffer![0u64, 0, 6, 4].into_array();
1068
1069        let result = arr.take(indices).unwrap();
1070        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 1, 1, 2]), &mut ctx);
1071    }
1072
1073    #[test]
1074    fn test_take_nullable_values() {
1075        let mut ctx = array_session().create_execution_ctx();
1076        let a = PrimitiveArray::new(buffer![1i32, 2, 3], Validity::AllValid).into_array();
1077        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1078            .unwrap();
1079        assert_eq!(arr.nchunks(), 3);
1080        assert_eq!(arr.len(), 9);
1081        let indices = PrimitiveArray::new(buffer![0u64, 0, 6, 4], Validity::NonNullable);
1082
1083        let result = arr.take(indices.into_array()).unwrap();
1084        assert_arrays_eq!(
1085            result,
1086            PrimitiveArray::from_option_iter([1i32, 1, 1, 2].map(Some)),
1087            &mut ctx
1088        );
1089    }
1090
1091    #[test]
1092    fn test_take_nullable_indices() {
1093        let mut ctx = array_session().create_execution_ctx();
1094        let a = buffer![1i32, 2, 3].into_array();
1095        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1096            .unwrap();
1097        assert_eq!(arr.nchunks(), 3);
1098        assert_eq!(arr.len(), 9);
1099        let indices = PrimitiveArray::new(
1100            buffer![0u64, 0, 6, 4],
1101            Validity::Array(bitbuffer![1 0 0 1].into_array()),
1102        );
1103
1104        let result = arr.take(indices.into_array()).unwrap();
1105        assert_arrays_eq!(
1106            result,
1107            PrimitiveArray::from_option_iter([Some(1i32), None, None, Some(2)]),
1108            &mut ctx
1109        );
1110    }
1111
1112    #[test]
1113    fn test_take_nullable_struct() {
1114        let mut ctx = array_session().create_execution_ctx();
1115        let struct_array =
1116            StructArray::try_new(FieldNames::default(), vec![], 100, Validity::NonNullable)
1117                .unwrap();
1118
1119        let arr = ChunkedArray::from_iter(vec![
1120            struct_array.clone().into_array(),
1121            struct_array.into_array(),
1122        ]);
1123
1124        let result = arr
1125            .take(PrimitiveArray::from_option_iter(vec![Some(0), None, Some(101)]).into_array())
1126            .unwrap();
1127
1128        let expect = StructArray::try_new(
1129            FieldNames::default(),
1130            vec![],
1131            3,
1132            Validity::Array(BoolArray::from_iter(vec![true, false, true]).into_array()),
1133        )
1134        .unwrap();
1135        assert_arrays_eq!(result, expect, &mut ctx);
1136    }
1137
1138    #[test]
1139    fn test_empty_take() {
1140        let mut ctx = array_session().create_execution_ctx();
1141        let a = buffer![1i32, 2, 3].into_array();
1142        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
1143            .unwrap();
1144        assert_eq!(arr.nchunks(), 3);
1145        assert_eq!(arr.len(), 9);
1146
1147        let indices = PrimitiveArray::empty::<u64>(Nullability::NonNullable);
1148        let result = arr.take(indices.into_array()).unwrap();
1149
1150        assert!(result.is_empty());
1151        assert_eq!(result.dtype(), arr.dtype());
1152        assert_arrays_eq!(
1153            result,
1154            PrimitiveArray::empty::<i32>(Nullability::NonNullable),
1155            &mut ctx
1156        );
1157    }
1158
1159    #[test]
1160    fn test_take_shuffled_indices() -> VortexResult<()> {
1161        let mut ctx = array_session().create_execution_ctx();
1162        let c0 = buffer![0i32, 1, 2].into_array();
1163        let c1 = buffer![3i32, 4, 5].into_array();
1164        let c2 = buffer![6i32, 7, 8].into_array();
1165        let arr = ChunkedArray::try_new(
1166            vec![c0, c1, c2],
1167            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1168                .dtype()
1169                .clone(),
1170        )?;
1171
1172        // Fully shuffled indices that cross every chunk boundary.
1173        let indices = buffer![8u64, 0, 5, 3, 2, 7, 1, 6, 4].into_array();
1174        let result = arr.take(indices)?;
1175
1176        assert_arrays_eq!(
1177            result,
1178            PrimitiveArray::from_iter([8i32, 0, 5, 3, 2, 7, 1, 6, 4]),
1179            &mut ctx
1180        );
1181        Ok(())
1182    }
1183
1184    #[test]
1185    fn test_take_shuffled_duplicates_with_empty_chunks() -> VortexResult<()> {
1186        let mut ctx = array_session().create_execution_ctx();
1187        let empty = PrimitiveArray::empty::<i32>(Nullability::NonNullable).into_array();
1188        let arr = ChunkedArray::try_new(
1189            vec![
1190                empty.clone(),
1191                buffer![0i32, 1].into_array(),
1192                empty.clone(),
1193                buffer![2i32, 3].into_array(),
1194                empty,
1195            ],
1196            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1197                .dtype()
1198                .clone(),
1199        )?;
1200
1201        let result = arr.take(buffer![3u64, 0, 2, 0, 3, 1, 2].into_array())?;
1202
1203        assert_arrays_eq!(
1204            result,
1205            PrimitiveArray::from_iter([3i32, 0, 2, 0, 3, 1, 2]),
1206            &mut ctx
1207        );
1208        Ok(())
1209    }
1210
1211    #[test]
1212    fn test_take_small_shuffled_fixed_size_lists() -> VortexResult<()> {
1213        let mut ctx = array_session().create_execution_ctx();
1214        let first = FixedSizeListArray::new(
1215            buffer![0i32, 1, 2, 3].into_array(),
1216            2,
1217            Validity::NonNullable,
1218            2,
1219        )
1220        .into_array();
1221        let second = FixedSizeListArray::new(
1222            buffer![4i32, 5, 6, 7].into_array(),
1223            2,
1224            Validity::NonNullable,
1225            2,
1226        )
1227        .into_array();
1228        let dtype = first.dtype().clone();
1229        let array = ChunkedArray::try_new(vec![first, second], dtype)?;
1230
1231        let result = array.take(buffer![3u64, 0, 2, 1, 3].into_array())?;
1232        let expected = FixedSizeListArray::new(
1233            buffer![6i32, 7, 0, 1, 4, 5, 2, 3, 6, 7].into_array(),
1234            2,
1235            Validity::NonNullable,
1236            5,
1237        );
1238
1239        assert_arrays_eq!(result, expected, &mut ctx);
1240        Ok(())
1241    }
1242
1243    #[test]
1244    fn test_take_shuffled_large() -> VortexResult<()> {
1245        let mut ctx = array_session().create_execution_ctx();
1246        let nchunks: i32 = 100;
1247        let chunk_len: i32 = 1_000;
1248        let total = nchunks * chunk_len;
1249
1250        let chunks: Vec<_> = (0..nchunks)
1251            .map(|c| {
1252                let start = c * chunk_len;
1253                PrimitiveArray::from_iter(start..start + chunk_len).into_array()
1254            })
1255            .collect();
1256        let dtype = chunks[0].dtype().clone();
1257        let arr = ChunkedArray::try_new(chunks, dtype)?;
1258
1259        // Fisher-Yates shuffle with a fixed seed for determinism.
1260        let mut indices: Vec<u64> = (0..u64::try_from(total)?).collect();
1261        let mut seed: u64 = 0xdeadbeef;
1262        for i in (1..indices.len()).rev() {
1263            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1264            let j = (seed >> 33) as usize % (i + 1);
1265            indices.swap(i, j);
1266        }
1267
1268        let indices_arr = PrimitiveArray::new(Buffer::from(indices.clone()), Validity::NonNullable);
1269        let result = arr.take(indices_arr.into_array())?;
1270
1271        // Verify every element.
1272        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
1273        assert_eq!(
1274            result.dtype(),
1275            &DType::Primitive(PType::I32, Nullability::NonNullable)
1276        );
1277        assert_eq!(result.len(), indices.len());
1278        let result_vals = result.as_slice::<i32>();
1279        for (pos, &idx) in indices.iter().enumerate() {
1280            assert_eq!(
1281                result_vals[pos],
1282                i32::try_from(idx)?,
1283                "mismatch at position {pos}"
1284            );
1285        }
1286        Ok(())
1287    }
1288
1289    #[test]
1290    fn test_take_null_indices() -> VortexResult<()> {
1291        let mut ctx = array_session().create_execution_ctx();
1292        let c0 = buffer![10i32, 20, 30].into_array();
1293        let c1 = buffer![40i32, 50, 60].into_array();
1294        let arr = ChunkedArray::try_new(
1295            vec![c0, c1],
1296            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1297                .dtype()
1298                .clone(),
1299        )?;
1300
1301        // Indices with nulls scattered across chunk boundaries.
1302        let indices =
1303            PrimitiveArray::from_option_iter([Some(5u64), None, Some(0), Some(3), None, Some(2)]);
1304        let result = arr.take(indices.into_array())?;
1305
1306        assert_arrays_eq!(
1307            result,
1308            PrimitiveArray::from_option_iter([
1309                Some(60i32),
1310                None,
1311                Some(10),
1312                Some(40),
1313                None,
1314                Some(30)
1315            ]),
1316            &mut ctx
1317        );
1318        Ok(())
1319    }
1320
1321    #[test]
1322    fn test_take_sorted_indices() -> VortexResult<()> {
1323        let mut ctx = array_session().create_execution_ctx();
1324        let arr = chunked_i32()?;
1325
1326        // Strictly increasing indices spanning chunks hit the sorted fast path.
1327        let indices = buffer![1u64, 4, 5, 9, 10, 14].into_array();
1328        let result = arr.take(indices)?;
1329
1330        assert_arrays_eq!(
1331            result,
1332            PrimitiveArray::from_iter([1i32, 4, 5, 9, 10, 14]),
1333            &mut ctx
1334        );
1335        Ok(())
1336    }
1337
1338    #[test]
1339    fn test_take_out_of_bounds() -> VortexResult<()> {
1340        let mut ctx = array_session().create_execution_ctx();
1341        let arr = chunked_i32()?;
1342
1343        // Sorted indices hit the fast path, unsorted the generic path; both must fail.
1344        for indices in [buffer![0u64, 100], buffer![100u64, 0]] {
1345            let result = arr
1346                .take(indices.into_array())
1347                .and_then(|taken| taken.execute::<Canonical>(&mut ctx));
1348            assert!(result.is_err(), "expected out-of-bounds error");
1349        }
1350        Ok(())
1351    }
1352
1353    #[test]
1354    fn test_take_all_null_indices() -> VortexResult<()> {
1355        let mut ctx = array_session().create_execution_ctx();
1356        let arr = chunked_i32()?;
1357
1358        let indices = PrimitiveArray::from_option_iter([None::<u64>, None]).into_array();
1359        let result = arr.take(indices)?;
1360
1361        assert_arrays_eq!(
1362            result,
1363            PrimitiveArray::from_option_iter([None::<i32>, None]),
1364            &mut ctx
1365        );
1366        Ok(())
1367    }
1368
1369    #[test]
1370    fn test_take_chunked_conformance() {
1371        let a = buffer![1i32, 2, 3].into_array();
1372        let b = buffer![4i32, 5].into_array();
1373        let arr = ChunkedArray::try_new(
1374            vec![a, b],
1375            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
1376                .dtype()
1377                .clone(),
1378        )
1379        .unwrap();
1380        test_take_conformance(
1381            &arr.into_array(),
1382            &mut array_session().create_execution_ctx(),
1383        );
1384
1385        // Test with nullable chunked array
1386        let a = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]);
1387        let b = PrimitiveArray::from_option_iter([Some(4i32), Some(5)]);
1388        let dtype = a.dtype().clone();
1389        let arr = ChunkedArray::try_new(vec![a.into_array(), b.into_array()], dtype).unwrap();
1390        test_take_conformance(
1391            &arr.into_array(),
1392            &mut array_session().create_execution_ctx(),
1393        );
1394
1395        // Test with multiple identical chunks
1396        let chunk = buffer![10i32, 20, 30, 40, 50].into_array();
1397        let arr = ChunkedArray::try_new(
1398            vec![chunk.clone(), chunk.clone(), chunk.clone()],
1399            chunk.dtype().clone(),
1400        )
1401        .unwrap();
1402        test_take_conformance(
1403            &arr.into_array(),
1404            &mut array_session().create_execution_ctx(),
1405        );
1406    }
1407}