Skip to main content

vortex_runend/compute/
take.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use num_traits::AsPrimitive;
5use num_traits::NumCast;
6use vortex_array::ArrayRef;
7use vortex_array::ArrayView;
8use vortex_array::ExecutionCtx;
9use vortex_array::IntoArray;
10use vortex_array::arrays::ConstantArray;
11use vortex_array::arrays::PrimitiveArray;
12use vortex_array::arrays::dict::TakeExecute;
13use vortex_array::dtype::UnsignedPType;
14use vortex_array::match_each_integer_ptype;
15use vortex_array::match_each_unsigned_integer_ptype;
16use vortex_array::scalar::Scalar;
17use vortex_array::validity::Validity;
18use vortex_buffer::Buffer;
19use vortex_buffer::BufferMut;
20use vortex_error::VortexResult;
21use vortex_error::vortex_bail;
22use vortex_mask::AllOr;
23use vortex_mask::Mask;
24
25use crate::RunEnd;
26use crate::array::RunEndArrayExt;
27use crate::iter::trimmed_ends_iter;
28
29const SORTED_LINEAR_RUNS_PER_INDEX_THRESHOLD: usize = 16;
30const UNSORTED_LINEAR_RUNS_PER_INDEX_THRESHOLD: usize = 4;
31/// Sorting the indices and merging only beats per-index binary search once the run ends are too
32/// large to stay cache-resident; below this run count binary search wins.
33const UNSORTED_LINEAR_MIN_RUNS: usize = 1 << 19;
34/// Use a dense logical-position-to-run-index table when the array length is at most this many
35/// times the number of valid indices: building the table is O(array_len) and each index then
36/// resolves with a single unconditional gather.
37const TABLE_LEN_PER_INDEX_THRESHOLD: usize = 8;
38
39impl TakeExecute for RunEnd {
40    fn take(
41        array: ArrayView<'_, Self>,
42        indices: &ArrayRef,
43        ctx: &mut ExecutionCtx,
44    ) -> VortexResult<Option<ArrayRef>> {
45        let primitive_indices = indices.clone().execute::<PrimitiveArray>(ctx)?;
46        let indices_validity = primitive_indices.validity()?;
47        let indices_mask = indices_validity.execute_mask(primitive_indices.len(), ctx)?;
48
49        let taken = match_each_integer_ptype!(primitive_indices.ptype(), |P| {
50            take_indices(
51                array,
52                primitive_indices.as_slice::<P>(),
53                &indices_validity,
54                &indices_mask,
55                true,
56                ctx,
57            )?
58        });
59
60        Ok(Some(taken))
61    }
62}
63
64/// Perform a take operation on a RunEndArray without bounds-checking the indices.
65///
66/// The caller must guarantee that all valid indices are in bounds for the array.
67pub fn take_indices_unchecked<T: AsPrimitive<usize>>(
68    array: ArrayView<'_, RunEnd>,
69    indices: &[T],
70    validity: &Validity,
71    ctx: &mut ExecutionCtx,
72) -> VortexResult<ArrayRef> {
73    let validity_mask = validity.execute_mask(indices.len(), ctx)?;
74    take_indices(array, indices, validity, &validity_mask, false, ctx)
75}
76
77fn take_indices<T: AsPrimitive<usize>>(
78    array: ArrayView<'_, RunEnd>,
79    indices: &[T],
80    validity: &Validity,
81    validity_mask: &Mask,
82    check_bounds: bool,
83    ctx: &mut ExecutionCtx,
84) -> VortexResult<ArrayRef> {
85    if validity_mask.all_false() {
86        return Ok(
87            ConstantArray::new(Scalar::null(array.dtype().as_nullable()), indices.len())
88                .into_array(),
89        );
90    }
91
92    let stats = valid_indices_stats(indices, validity_mask, array.len(), check_bounds)?;
93    let ends = array.ends().clone().execute::<PrimitiveArray>(ctx)?;
94
95    let physical_indices = match_each_unsigned_integer_ptype!(ends.ptype(), |I| {
96        let ends = ends.as_slice::<I>();
97        // Run indices fit in u32 for any realistic array; the narrower physical indices halve
98        // the memory traffic of the downstream take on the values.
99        if ends.len() <= u32::MAX as usize {
100            PrimitiveArray::new(
101                physical_indices_with_stats::<_, _, u32>(
102                    ends,
103                    array.offset(),
104                    array.len(),
105                    indices,
106                    validity_mask,
107                    stats,
108                ),
109                validity.clone(),
110            )
111        } else {
112            PrimitiveArray::new(
113                physical_indices_with_stats::<_, _, u64>(
114                    ends,
115                    array.offset(),
116                    array.len(),
117                    indices,
118                    validity_mask,
119                    stats,
120                ),
121                validity.clone(),
122            )
123        }
124    });
125
126    array.values().take(physical_indices.into_array())
127}
128
129#[derive(Clone, Copy)]
130struct ValidIndicesStats {
131    count: usize,
132    sorted: bool,
133}
134
135fn physical_indices_with_stats<I, T, O>(
136    ends: &[I],
137    offset: usize,
138    array_len: usize,
139    indices: &[T],
140    validity_mask: &Mask,
141    stats: ValidIndicesStats,
142) -> Buffer<O>
143where
144    I: UnsignedPType,
145    T: AsPrimitive<usize>,
146    O: UnsignedPType,
147    usize: AsPrimitive<O>,
148{
149    if stats.count == 0 {
150        return Buffer::zeroed(indices.len());
151    }
152
153    if stats.sorted
154        && prefer_linear_scan(
155            ends.len(),
156            stats.count,
157            SORTED_LINEAR_RUNS_PER_INDEX_THRESHOLD,
158        )
159    {
160        return physical_indices_linear_sorted(ends, offset, indices, validity_mask);
161    }
162
163    // A dense take resolves fastest through the position table regardless of index ordering.
164    // Sorted indices reach here only when there are too many runs for the sorted linear scan,
165    // for example a narrow slice of a heavily run-encoded array where runs far exceed array_len.
166    if array_len <= stats.count.saturating_mul(TABLE_LEN_PER_INDEX_THRESHOLD) {
167        return physical_indices_table(ends, offset, array_len, indices, validity_mask);
168    }
169
170    if ends.len() >= UNSORTED_LINEAR_MIN_RUNS
171        && prefer_linear_scan(
172            ends.len(),
173            stats.count,
174            UNSORTED_LINEAR_RUNS_PER_INDEX_THRESHOLD,
175        )
176    {
177        return physical_indices_linear_unsorted(ends, offset, indices, validity_mask, stats.count);
178    }
179
180    physical_indices_binary(ends, offset, indices, validity_mask)
181}
182
183/// Count the valid indices and determine whether they are sorted, bounds-checking each valid
184/// index against `array_len` when `check_bounds` is set.
185fn valid_indices_stats<T: AsPrimitive<usize>>(
186    indices: &[T],
187    validity_mask: &Mask,
188    array_len: usize,
189    check_bounds: bool,
190) -> VortexResult<ValidIndicesStats> {
191    debug_assert_eq!(indices.len(), validity_mask.len());
192
193    let count = validity_mask.true_count();
194    if count == 0 {
195        return Ok(ValidIndicesStats {
196            count,
197            sorted: true,
198        });
199    }
200
201    let sorted = match validity_mask.bit_buffer() {
202        AllOr::All => valid_indices_sorted_all(indices, array_len, check_bounds)?,
203        AllOr::None => true,
204        AllOr::Some(validity) => {
205            valid_indices_sorted_masked(indices, validity.iter(), array_len, check_bounds)?
206        }
207    };
208
209    Ok(ValidIndicesStats { count, sorted })
210}
211
212fn valid_indices_sorted_all<T: AsPrimitive<usize>>(
213    indices: &[T],
214    array_len: usize,
215    check_bounds: bool,
216) -> VortexResult<bool> {
217    // Seed the comparison with the first index; an empty or single-element slice is trivially
218    // sorted, so the loop below starts from the second element.
219    let Some((first, rest)) = indices.split_first() else {
220        return Ok(true);
221    };
222
223    let mut previous_idx = first.as_();
224    if check_bounds {
225        check_index(previous_idx, array_len)?;
226    }
227
228    let mut sorted = true;
229    for idx in rest {
230        let idx = idx.as_();
231        if check_bounds {
232            check_index(idx, array_len)?;
233        }
234        if previous_idx > idx {
235            sorted = false;
236            if !check_bounds {
237                break;
238            }
239        }
240        previous_idx = idx;
241    }
242
243    Ok(sorted)
244}
245
246fn valid_indices_sorted_masked<T: AsPrimitive<usize>>(
247    indices: &[T],
248    is_valid: impl Iterator<Item = bool>,
249    array_len: usize,
250    check_bounds: bool,
251) -> VortexResult<bool> {
252    // Invalid positions are skipped without a bounds check, matching the take path that never
253    // dereferences them.
254    let mut valid = is_valid
255        .zip(indices.iter())
256        .filter(|(is_valid, _)| *is_valid)
257        .map(|(_, idx)| idx.as_());
258
259    // Seed the comparison with the first valid index; zero or one valid index is trivially
260    // sorted, so the loop below starts from the second valid index.
261    let Some(mut previous_idx) = valid.next() else {
262        return Ok(true);
263    };
264    if check_bounds {
265        check_index(previous_idx, array_len)?;
266    }
267
268    let mut sorted = true;
269    for idx in valid {
270        if check_bounds {
271            check_index(idx, array_len)?;
272        }
273        if previous_idx > idx {
274            sorted = false;
275            if !check_bounds {
276                break;
277            }
278        }
279        previous_idx = idx;
280    }
281
282    Ok(sorted)
283}
284
285fn prefer_linear_scan(
286    ends_len: usize,
287    valid_count: usize,
288    runs_per_index_threshold: usize,
289) -> bool {
290    ends_len <= valid_count.saturating_mul(runs_per_index_threshold)
291}
292
293fn check_index(index: usize, array_len: usize) -> VortexResult<()> {
294    if index >= array_len {
295        vortex_bail!(OutOfBounds: index, 0, array_len);
296    }
297    Ok(())
298}
299
300fn physical_indices_linear_sorted<I, T, O>(
301    ends: &[I],
302    offset: usize,
303    indices: &[T],
304    validity_mask: &Mask,
305) -> Buffer<O>
306where
307    I: UnsignedPType,
308    T: AsPrimitive<usize>,
309    O: UnsignedPType,
310    usize: AsPrimitive<O>,
311{
312    let mut run_idx = 0;
313
314    match validity_mask.bit_buffer() {
315        AllOr::All => Buffer::from_trusted_len_iter(indices.iter().map(|idx| {
316            advance_run(ends, &mut run_idx, idx.as_() + offset);
317            run_idx.as_()
318        })),
319        AllOr::None => unreachable!("AllInvalid indices have been handled earlier"),
320        AllOr::Some(validity) => {
321            // Invalid positions keep physical index zero, which is always in-bounds for the
322            // values and masked out by the result validity.
323            let mut physical_indices = BufferMut::zeroed(indices.len());
324            for (idx_pos, (is_valid, idx)) in validity.iter().zip(indices.iter()).enumerate() {
325                if !is_valid {
326                    continue;
327                }
328
329                advance_run(ends, &mut run_idx, idx.as_() + offset);
330                physical_indices[idx_pos] = run_idx.as_();
331            }
332            physical_indices.freeze()
333        }
334    }
335}
336
337/// Resolve indices through a dense logical-position-to-run-index table.
338///
339/// Building the table costs O(array_len), but every index then resolves with an unconditional
340/// gather, which beats per-index binary search and sort-then-merge for dense takes. Invalid
341/// indices may hold arbitrary values (even out of bounds), so they are redirected to position
342/// zero instead of branching; the result validity masks whatever they resolve to.
343fn physical_indices_table<I, T, O>(
344    ends: &[I],
345    offset: usize,
346    array_len: usize,
347    indices: &[T],
348    validity_mask: &Mask,
349) -> Buffer<O>
350where
351    I: UnsignedPType,
352    T: AsPrimitive<usize>,
353    O: UnsignedPType,
354    usize: AsPrimitive<O>,
355{
356    let table = run_index_table::<I, O>(ends, offset, array_len);
357    let table = table.as_slice();
358
359    match validity_mask.bit_buffer() {
360        AllOr::All => Buffer::from_trusted_len_iter(indices.iter().map(|idx| table[idx.as_()])),
361        AllOr::None => unreachable!("AllInvalid indices have been handled earlier"),
362        AllOr::Some(validity) => Buffer::from_trusted_len_iter(
363            validity
364                .iter()
365                .zip(indices.iter())
366                .map(|(is_valid, idx)| table[if is_valid { idx.as_() } else { 0 }]),
367        ),
368    }
369}
370
371/// Materialize the run index of every logical position in `[0, len)`.
372fn run_index_table<I, O>(ends: &[I], offset: usize, len: usize) -> Buffer<O>
373where
374    I: UnsignedPType,
375    O: UnsignedPType,
376    usize: AsPrimitive<O>,
377{
378    let mut table = BufferMut::with_capacity(len);
379    let mut run_start = 0;
380    for (run_idx, run_end) in trimmed_ends_iter(ends, offset, len).enumerate() {
381        table.push_n(run_idx.as_(), run_end - run_start);
382        run_start = run_end;
383    }
384    table.freeze()
385}
386
387fn physical_indices_linear_unsorted<I, T, O>(
388    ends: &[I],
389    offset: usize,
390    indices: &[T],
391    validity_mask: &Mask,
392    valid_count: usize,
393) -> Buffer<O>
394where
395    I: UnsignedPType,
396    T: AsPrimitive<usize>,
397    O: UnsignedPType,
398    usize: AsPrimitive<O>,
399{
400    let mut pairs = Vec::with_capacity(valid_count);
401    match validity_mask.bit_buffer() {
402        AllOr::All => {
403            pairs.extend(
404                indices
405                    .iter()
406                    .enumerate()
407                    .map(|(idx_pos, idx)| (idx.as_(), idx_pos)),
408            );
409        }
410        AllOr::None => unreachable!("AllInvalid indices have been handled earlier"),
411        AllOr::Some(validity) => {
412            for (idx_pos, (is_valid, idx)) in validity.iter().zip(indices.iter()).enumerate() {
413                if is_valid {
414                    pairs.push((idx.as_(), idx_pos));
415                }
416            }
417        }
418    }
419    pairs.sort_unstable();
420
421    let mut physical_indices = BufferMut::zeroed(indices.len());
422    let mut run_idx = 0;
423
424    for (idx, idx_pos) in pairs {
425        advance_run(ends, &mut run_idx, idx + offset);
426        physical_indices[idx_pos] = run_idx.as_();
427    }
428
429    physical_indices.freeze()
430}
431
432fn physical_indices_binary<I, T, O>(
433    ends: &[I],
434    offset: usize,
435    indices: &[T],
436    validity_mask: &Mask,
437) -> Buffer<O>
438where
439    I: UnsignedPType,
440    T: AsPrimitive<usize>,
441    O: UnsignedPType,
442    usize: AsPrimitive<O>,
443{
444    match validity_mask.bit_buffer() {
445        AllOr::All => Buffer::from_trusted_len_iter(
446            indices
447                .iter()
448                .map(|idx| physical_index_binary(ends, idx.as_() + offset).as_()),
449        ),
450        AllOr::None => Buffer::zeroed(indices.len()),
451        AllOr::Some(validity) => {
452            let mut physical_indices = BufferMut::zeroed(indices.len());
453            for (idx_pos, (is_valid, idx)) in validity.iter().zip(indices.iter()).enumerate() {
454                if !is_valid {
455                    continue;
456                }
457
458                physical_indices[idx_pos] = physical_index_binary(ends, idx.as_() + offset).as_();
459            }
460            physical_indices.freeze()
461        }
462    }
463}
464
465fn physical_index_binary<I: UnsignedPType>(ends: &[I], logical_idx: usize) -> usize {
466    let index = match <I as NumCast>::from(logical_idx) {
467        Some(logical_idx) => ends.partition_point(|end| *end <= logical_idx),
468        None => ends.len(),
469    };
470    index.min(ends.len() - 1)
471}
472
473fn advance_run<I: UnsignedPType>(ends: &[I], run_idx: &mut usize, logical_idx: usize) {
474    // A logical index that overflows the run-end type sits past every run, so it lands in the
475    // final run; otherwise advance while the current run ends at or before it.
476    let Some(logical_idx) = I::from(logical_idx) else {
477        *run_idx = ends.len().saturating_sub(1);
478        return;
479    };
480    while *run_idx + 1 < ends.len() && ends[*run_idx] <= logical_idx {
481        *run_idx += 1;
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use std::sync::LazyLock;
488
489    use rstest::rstest;
490    use vortex_array::ArrayRef;
491    use vortex_array::Canonical;
492    use vortex_array::IntoArray;
493    use vortex_array::VortexSessionExecute;
494    use vortex_array::arrays::BoolArray;
495    use vortex_array::arrays::PrimitiveArray;
496    use vortex_array::assert_arrays_eq;
497    use vortex_array::compute::conformance::take::test_take_conformance;
498    use vortex_array::validity::Validity;
499    use vortex_buffer::buffer;
500    use vortex_mask::Mask;
501    use vortex_session::VortexSession;
502
503    use super::physical_indices_binary;
504    use super::physical_indices_linear_sorted;
505    use super::physical_indices_linear_unsorted;
506    use super::physical_indices_table;
507    use crate::RunEnd;
508    use crate::RunEndArray;
509
510    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
511        let session = vortex_array::array_session();
512        crate::initialize(&session);
513        session
514    });
515
516    fn ree_array() -> RunEndArray {
517        RunEnd::encode(
518            buffer![1, 1, 1, 4, 4, 4, 2, 2, 5, 5, 5, 5].into_array(),
519            &mut SESSION.create_execution_ctx(),
520        )
521        .unwrap()
522    }
523
524    #[test]
525    fn ree_take() {
526        let taken = ree_array().take(buffer![9, 8, 1, 3].into_array()).unwrap();
527        let expected = PrimitiveArray::from_iter(vec![5i32, 5, 1, 4]).into_array();
528        assert_arrays_eq!(taken, expected, &mut SESSION.create_execution_ctx());
529    }
530
531    #[test]
532    fn ree_take_end() {
533        let taken = ree_array().take(buffer![11].into_array()).unwrap();
534        let expected = PrimitiveArray::from_iter(vec![5i32]).into_array();
535        assert_arrays_eq!(taken, expected, &mut SESSION.create_execution_ctx());
536    }
537
538    #[test]
539    fn ree_take_sorted_boundaries() {
540        let taken = ree_array()
541            .take(buffer![0, 2, 3, 6, 8, 11].into_array())
542            .unwrap();
543        let expected = PrimitiveArray::from_iter(vec![1i32, 1, 4, 2, 5, 5]).into_array();
544        assert_arrays_eq!(taken, expected, &mut SESSION.create_execution_ctx());
545    }
546
547    #[test]
548    #[should_panic]
549    fn ree_take_out_of_bounds() {
550        let _array = ree_array()
551            .take(buffer![12].into_array())
552            .unwrap()
553            .execute::<Canonical>(&mut SESSION.create_execution_ctx())
554            .unwrap();
555    }
556
557    #[test]
558    fn sliced_take() {
559        let sliced = ree_array().slice(4..9).unwrap();
560        let taken = sliced.take(buffer![1, 3, 4].into_array()).unwrap();
561
562        let expected = PrimitiveArray::from_iter(vec![4i32, 2, 5]).into_array();
563        assert_arrays_eq!(taken, expected, &mut SESSION.create_execution_ctx());
564    }
565
566    #[test]
567    fn sliced_take_unsorted_dense() {
568        let sliced = ree_array().slice(4..9).unwrap();
569        let taken = sliced.take(buffer![4, 0, 2, 1].into_array()).unwrap();
570
571        let expected = PrimitiveArray::from_iter(vec![5i32, 4, 2, 4]).into_array();
572        assert_arrays_eq!(taken, expected, &mut SESSION.create_execution_ctx());
573    }
574
575    #[test]
576    fn ree_take_nullable() {
577        let taken = ree_array()
578            .take(PrimitiveArray::from_option_iter([Some(1), None]).into_array())
579            .unwrap();
580
581        let expected = PrimitiveArray::from_option_iter([Some(1i32), None]);
582        assert_arrays_eq!(
583            taken,
584            expected.into_array(),
585            &mut SESSION.create_execution_ctx()
586        );
587    }
588
589    #[test]
590    fn ree_take_all_null_indices() {
591        let taken = ree_array()
592            .take(PrimitiveArray::from_option_iter([None::<u64>, None]).into_array())
593            .unwrap();
594
595        let expected = PrimitiveArray::from_option_iter([None::<i32>, None]);
596        assert_arrays_eq!(
597            taken,
598            expected.into_array(),
599            &mut SESSION.create_execution_ctx()
600        );
601    }
602
603    #[test]
604    fn ree_take_null_index_skips_out_of_bounds_value() {
605        let indices = PrimitiveArray::new(
606            buffer![1u64, 12],
607            Validity::Array(BoolArray::from_iter([true, false]).into_array()),
608        );
609        let taken = ree_array().take(indices.into_array()).unwrap();
610
611        let expected = PrimitiveArray::from_option_iter([Some(1i32), None]);
612        assert_arrays_eq!(
613            taken,
614            expected.into_array(),
615            &mut SESSION.create_execution_ctx()
616        );
617    }
618
619    #[test]
620    fn ree_take_unsorted_null_index_skips_out_of_bounds_value() {
621        let indices = PrimitiveArray::new(
622            buffer![3u64, 12, 1],
623            Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
624        );
625        let taken = ree_array().take(indices.into_array()).unwrap();
626
627        let expected = PrimitiveArray::from_option_iter([Some(4i32), None, Some(1)]);
628        assert_arrays_eq!(
629            taken,
630            expected.into_array(),
631            &mut SESSION.create_execution_ctx()
632        );
633    }
634
635    #[test]
636    fn ree_take_dense_null_index_skips_out_of_bounds_value() {
637        let indices = PrimitiveArray::new(
638            buffer![0u64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
639            Validity::Array(
640                BoolArray::from_iter([
641                    true, true, true, true, true, true, true, true, true, true, true, false,
642                ])
643                .into_array(),
644            ),
645        );
646        let taken = ree_array().take(indices.into_array()).unwrap();
647
648        let expected = PrimitiveArray::from_option_iter([
649            Some(1i32),
650            Some(1),
651            Some(1),
652            Some(4),
653            Some(4),
654            Some(4),
655            Some(2),
656            Some(2),
657            Some(5),
658            Some(5),
659            Some(5),
660            None,
661        ]);
662        assert_arrays_eq!(
663            taken,
664            expected.into_array(),
665            &mut SESSION.create_execution_ctx()
666        );
667    }
668
669    #[rstest]
670    #[case(vec![3u32, 6, 8, 12], 0, 12, vec![0u64, 11, 3, 3, 7, 2, 9], Mask::new_true(7))]
671    #[case(vec![3u32, 6, 8, 12], 0, 12, vec![5u64, 100, 2, 11, 0], Mask::from_indices(5, [0, 2, 3, 4]))]
672    #[case(vec![6u32, 8, 12], 4, 5, vec![4u64, 0, 2, 1, 3], Mask::new_true(5))]
673    fn unsorted_strategies_agree(
674        #[case] ends: Vec<u32>,
675        #[case] offset: usize,
676        #[case] len: usize,
677        #[case] indices: Vec<u64>,
678        #[case] mask: Mask,
679    ) {
680        let binary = physical_indices_binary::<u32, u64, u64>(&ends, offset, &indices, &mask);
681        let table = physical_indices_table::<u32, u64, u64>(&ends, offset, len, &indices, &mask);
682        let sort_merge = physical_indices_linear_unsorted::<u32, u64, u64>(
683            &ends,
684            offset,
685            &indices,
686            &mask,
687            mask.true_count(),
688        );
689
690        assert_eq!(binary.as_slice(), table.as_slice());
691        assert_eq!(binary.as_slice(), sort_merge.as_slice());
692    }
693
694    #[rstest]
695    #[case(vec![3u32, 6, 8, 12], 0, 12, vec![0u64, 2, 3, 6, 8, 11], Mask::new_true(6))]
696    #[case(vec![3u32, 6, 8, 12], 0, 12, vec![1u64, 100, 5, 9], Mask::from_indices(4, [0, 2, 3]))]
697    #[case(vec![6u32, 8, 12], 4, 5, vec![0u64, 1, 3, 4], Mask::new_true(4))]
698    fn sorted_strategies_agree(
699        #[case] ends: Vec<u32>,
700        #[case] offset: usize,
701        #[case] len: usize,
702        #[case] indices: Vec<u64>,
703        #[case] mask: Mask,
704    ) {
705        let binary = physical_indices_binary::<u32, u64, u64>(&ends, offset, &indices, &mask);
706        let table = physical_indices_table::<u32, u64, u64>(&ends, offset, len, &indices, &mask);
707        let sorted =
708            physical_indices_linear_sorted::<u32, u64, u64>(&ends, offset, &indices, &mask);
709
710        assert_eq!(binary.as_slice(), table.as_slice());
711        assert_eq!(binary.as_slice(), sorted.as_slice());
712    }
713
714    #[rstest]
715    #[case(ree_array())]
716    #[case(RunEnd::encode(
717        buffer![1u8, 1, 2, 2, 2, 3, 3, 3, 3, 4].into_array(),
718        &mut SESSION.create_execution_ctx(),
719    ).unwrap())]
720    #[case(RunEnd::encode(
721        PrimitiveArray::from_option_iter([
722            Some(10),
723            Some(10),
724            None,
725            None,
726            Some(20),
727            Some(20),
728            Some(20),
729        ])
730        .into_array(),
731        &mut SESSION.create_execution_ctx(),
732    ).unwrap())]
733    #[case(RunEnd::encode(buffer![42i32, 42, 42, 42, 42].into_array(),
734        &mut SESSION.create_execution_ctx())
735        .unwrap())]
736    #[case(RunEnd::encode(
737        buffer![1i32, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_array(),
738        &mut SESSION.create_execution_ctx(),
739    ).unwrap())]
740    #[case({
741        let mut values = Vec::new();
742        for i in 0..20 {
743            for _ in 0..=i {
744                values.push(i);
745            }
746        }
747        RunEnd::encode(
748            PrimitiveArray::from_iter(values).into_array(),
749            &mut SESSION.create_execution_ctx(),
750        )
751        .unwrap()
752    })]
753    fn test_take_runend_conformance(#[case] array: RunEndArray) {
754        test_take_conformance(&array.into_array(), &mut SESSION.create_execution_ctx());
755    }
756
757    #[rstest]
758    #[case(ree_array().slice(3..6).unwrap())]
759    #[case({
760        let array = RunEnd::encode(
761            buffer![1i32, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3].into_array(),
762            &mut SESSION.create_execution_ctx(),
763        )
764        .unwrap();
765        array.slice(2..8).unwrap()
766    })]
767    fn test_take_sliced_runend_conformance(#[case] sliced: ArrayRef) {
768        test_take_conformance(&sliced, &mut SESSION.create_execution_ctx());
769    }
770}