Skip to main content

vortex_array/
patches.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::hash::Hash;
6use std::ops::Range;
7
8use num_traits::NumCast;
9use vortex_buffer::BitBuffer;
10use vortex_buffer::BitBufferMut;
11use vortex_buffer::BufferAllocatorRef;
12use vortex_buffer::BufferMut;
13use vortex_error::VortexError;
14use vortex_error::VortexExpect as _;
15use vortex_error::VortexResult;
16use vortex_error::vortex_bail;
17use vortex_error::vortex_ensure;
18use vortex_error::vortex_ensure_eq;
19use vortex_error::vortex_err;
20use vortex_mask::AllOr;
21use vortex_mask::Mask;
22use vortex_mask::MaskValues;
23use vortex_utils::aliases::hash_map::HashMap;
24
25use crate::ArrayRef;
26use crate::ArraySlots;
27use crate::ExecutionCtx;
28use crate::IntoArray;
29use crate::arrays::Primitive;
30use crate::arrays::PrimitiveArray;
31use crate::arrays::primitive::PrimitiveArrayExt;
32use crate::builtins::ArrayBuiltins;
33use crate::dtype::DType;
34use crate::dtype::IntegerPType;
35use crate::dtype::NativePType;
36use crate::dtype::Nullability;
37use crate::dtype::Nullability::NonNullable;
38use crate::dtype::PType;
39use crate::dtype::UnsignedPType;
40use crate::match_each_unsigned_integer_ptype;
41use crate::scalar::Scalar;
42use crate::search_sorted::SearchResult;
43use crate::search_sorted::SearchSorted;
44use crate::search_sorted::SearchSortedPrimitiveArray;
45use crate::search_sorted::SearchSortedSide;
46use crate::validity::Validity;
47
48/// One patch index offset is stored for each chunk.
49/// This allows for constant time patch index lookups.
50pub const PATCH_CHUNK_SIZE: usize = 1024;
51
52#[derive(Copy, Clone, prost::Message)]
53pub struct PatchesMetadata {
54    #[prost(uint64, tag = "1")]
55    len: u64,
56    #[prost(uint64, tag = "2")]
57    offset: u64,
58    #[prost(enumeration = "PType", tag = "3")]
59    indices_ptype: i32,
60    #[prost(uint64, optional, tag = "4")]
61    chunk_offsets_len: Option<u64>,
62    #[prost(enumeration = "PType", optional, tag = "5")]
63    chunk_offsets_ptype: Option<i32>,
64    #[prost(uint64, optional, tag = "6")]
65    offset_within_chunk: Option<u64>,
66}
67
68impl PatchesMetadata {
69    #[inline]
70    pub fn new(
71        len: usize,
72        offset: usize,
73        indices_ptype: PType,
74        chunk_offsets_len: Option<usize>,
75        chunk_offsets_ptype: Option<PType>,
76        offset_within_chunk: Option<usize>,
77    ) -> Self {
78        Self {
79            len: len as u64,
80            offset: offset as u64,
81            indices_ptype: indices_ptype as i32,
82            chunk_offsets_len: chunk_offsets_len.map(|len| len as u64),
83            chunk_offsets_ptype: chunk_offsets_ptype.map(|pt| pt as i32),
84            offset_within_chunk: offset_within_chunk.map(|len| len as u64),
85        }
86    }
87
88    #[inline]
89    pub fn len(&self) -> VortexResult<usize> {
90        usize::try_from(self.len).map_err(|_| vortex_err!("len does not fit in usize"))
91    }
92
93    #[inline]
94    pub fn is_empty(&self) -> bool {
95        self.len == 0
96    }
97
98    #[inline]
99    pub fn offset(&self) -> VortexResult<usize> {
100        usize::try_from(self.offset).map_err(|_| vortex_err!("offset does not fit in usize"))
101    }
102
103    #[inline]
104    pub fn chunk_offsets_dtype(&self) -> VortexResult<Option<DType>> {
105        self.chunk_offsets_ptype
106            .map(|t| {
107                PType::try_from(t)
108                    .map_err(|e| vortex_err!("invalid i32 value {t} for PType: {}", e))
109                    .map(|ptype| DType::Primitive(ptype, NonNullable))
110            })
111            .transpose()
112    }
113
114    #[inline]
115    pub fn indices_dtype(&self) -> VortexResult<DType> {
116        let ptype = PType::try_from(self.indices_ptype).map_err(|e| {
117            vortex_err!("invalid i32 value {} for PType: {}", self.indices_ptype, e)
118        })?;
119        vortex_ensure!(
120            ptype.is_unsigned_int(),
121            "Patch indices must be unsigned integers"
122        );
123        Ok(DType::Primitive(ptype, NonNullable))
124    }
125}
126
127/// Metadata stored in an array's data struct for reconstructing [`Patches`] from slots.
128///
129/// The actual patch arrays (indices, values, chunk_offsets) live in the array's
130/// slots. This struct stores only the scalar metadata needed to reassemble them.
131#[derive(Clone, Debug, Hash, PartialEq, Eq)]
132pub struct PatchesData {
133    offset: usize,
134    offset_within_chunk: Option<usize>,
135}
136
137/// Slot indices for the three patch components within a slot array.
138#[derive(Copy, Clone, Debug)]
139pub struct PatchSlotIndices {
140    pub indices: usize,
141    pub values: usize,
142    pub chunk_offsets: usize,
143}
144
145impl PatchesData {
146    /// Extract patch metadata from an existing [`Patches`].
147    pub fn from_patches(patches: &Patches) -> Self {
148        Self {
149            offset: patches.offset(),
150            offset_within_chunk: patches.offset_within_chunk(),
151        }
152    }
153
154    /// Reconstruct patches from the given slot positions.
155    ///
156    /// Returns `None` if `patches_data` is `None`.
157    /// Panics if `patches_data` is `Some` but the indices or values slots are missing.
158    pub fn patches_from_slots(
159        patches_data: Option<&Self>,
160        len: usize,
161        slots: &[Option<ArrayRef>],
162        slot_idx: PatchSlotIndices,
163    ) -> Option<Patches> {
164        let data = patches_data?;
165        let indices = slots[slot_idx.indices]
166            .as_ref()
167            .vortex_expect("patches_data is set but patch_indices slot is missing");
168        let values = slots[slot_idx.values]
169            .as_ref()
170            .vortex_expect("patches_data is set but patch_values slot is missing");
171        Some(unsafe {
172            Patches::new_unchecked(
173                len,
174                data.offset,
175                indices.clone(),
176                values.clone(),
177                slots[slot_idx.chunk_offsets].clone(),
178                data.offset_within_chunk,
179            )
180        })
181    }
182
183    /// Push 3 patch slots (indices, values, chunk_offsets) onto a slot vector.
184    ///
185    /// If `patches` is `None`, pushes three `None` entries.
186    pub fn push_slots(slots: &mut ArraySlots, patches: Option<&Patches>) {
187        match patches {
188            Some(p) => {
189                slots.push(Some(p.indices().clone()));
190                slots.push(Some(p.values().clone()));
191                slots.push(p.chunk_offsets().clone());
192            }
193            None => {
194                slots.push(None);
195                slots.push(None);
196                slots.push(None);
197            }
198        }
199    }
200
201    /// Returns the patch offset.
202    #[inline]
203    pub fn offset(&self) -> usize {
204        self.offset
205    }
206
207    /// Returns the offset within the first chunk, if chunk offsets are present.
208    #[inline]
209    pub fn offset_within_chunk(&self) -> Option<usize> {
210        self.offset_within_chunk
211    }
212}
213
214/// A helper for working with patched arrays.
215#[derive(Debug, Clone)]
216pub struct Patches {
217    array_len: usize,
218    offset: usize,
219    indices: ArrayRef,
220    values: ArrayRef,
221    /// Stores the patch index offset for each chunk.
222    ///
223    /// This allows us to lookup the patches for a given chunk in constant time via
224    /// `patch_indices[chunk_offsets[i]..chunk_offsets[i+1]]`.
225    ///
226    /// This is optional for compatibility reasons.
227    chunk_offsets: Option<ArrayRef>,
228    /// Chunk offsets are only sliced off in case the slice is fully
229    /// outside of the chunk range.
230    ///
231    /// Though the range for indices and values is sliced in terms of
232    /// individual elements, not chunks. To account for that we do a
233    /// saturating sub when adjusting the indices based on the chunk offset.
234    ///
235    /// `offset_within_chunk` is necessary in order to keep track of how many
236    /// elements were sliced off within the chunk.
237    offset_within_chunk: Option<usize>,
238}
239
240impl Patches {
241    pub fn new(
242        array_len: usize,
243        offset: usize,
244        indices: ArrayRef,
245        values: ArrayRef,
246        chunk_offsets: Option<ArrayRef>,
247        ctx: &mut ExecutionCtx,
248    ) -> VortexResult<Self> {
249        vortex_ensure_eq!(indices.len(), values.len());
250        vortex_ensure!(
251            indices.dtype().is_unsigned_int() && !indices.dtype().is_nullable(),
252            "Patch indices must be non-nullable unsigned integers, got {:?}",
253            indices.dtype()
254        );
255
256        vortex_ensure!(
257            indices.len() <= array_len,
258            "Patch indices must be shorter than the array length"
259        );
260        vortex_ensure!(!indices.is_empty(), "Patch indices must not be empty");
261
262        // Perform validation of components when they are host-resident.
263        // This is not possible to do eagerly when the data is on GPU memory.
264        if indices.is_host() && values.is_host() {
265            let max = usize::try_from(&indices.execute_scalar(indices.len() - 1, ctx)?)
266                .map_err(|_| vortex_err!("indices must be a number"))?;
267            vortex_ensure!(
268                max - offset < array_len,
269                "Patch indices {max:?}, offset {offset} are longer than the array length {array_len}"
270            );
271
272            #[cfg(debug_assertions)]
273            {
274                use crate::aggregate_fn::fns::is_sorted::is_sorted;
275                assert!(
276                    is_sorted(&indices, ctx).unwrap_or(false),
277                    "Patch indices must be sorted"
278                );
279            }
280        }
281
282        Ok(Self {
283            array_len,
284            offset,
285            indices,
286            values,
287            chunk_offsets: chunk_offsets.clone(),
288            // Initialize with `Some(0)` only if `chunk_offsets` are set.
289            offset_within_chunk: chunk_offsets.map(|_| 0),
290        })
291    }
292
293    /// Construct new patches without validating any of the arguments
294    ///
295    /// # Safety
296    ///
297    /// Users have to assert that
298    /// * Indices and values have the same length
299    /// * Indices is an unsigned integer type
300    /// * Indices must be sorted
301    /// * Last value in indices is smaller than array_len
302    pub unsafe fn new_unchecked(
303        array_len: usize,
304        offset: usize,
305        indices: ArrayRef,
306        values: ArrayRef,
307        chunk_offsets: Option<ArrayRef>,
308        offset_within_chunk: Option<usize>,
309    ) -> Self {
310        Self {
311            array_len,
312            offset,
313            indices,
314            values,
315            chunk_offsets,
316            offset_within_chunk,
317        }
318    }
319
320    #[inline]
321    pub fn array_len(&self) -> usize {
322        self.array_len
323    }
324
325    #[inline]
326    pub fn num_patches(&self) -> usize {
327        self.indices.len()
328    }
329
330    #[inline]
331    pub fn dtype(&self) -> &DType {
332        self.values.dtype()
333    }
334
335    #[inline]
336    pub fn indices(&self) -> &ArrayRef {
337        &self.indices
338    }
339
340    #[inline]
341    pub fn into_indices(self) -> ArrayRef {
342        self.indices
343    }
344
345    #[inline]
346    pub fn indices_mut(&mut self) -> &mut ArrayRef {
347        &mut self.indices
348    }
349
350    #[inline]
351    pub fn values(&self) -> &ArrayRef {
352        &self.values
353    }
354
355    #[inline]
356    pub fn into_values(self) -> ArrayRef {
357        self.values
358    }
359
360    #[inline]
361    pub fn values_mut(&mut self) -> &mut ArrayRef {
362        &mut self.values
363    }
364
365    #[inline]
366    // Absolute offset: 0 if the array is unsliced.
367    pub fn offset(&self) -> usize {
368        self.offset
369    }
370
371    #[inline]
372    pub fn chunk_offsets(&self) -> &Option<ArrayRef> {
373        &self.chunk_offsets
374    }
375
376    #[inline]
377    pub fn chunk_offset_at(&self, idx: usize, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
378        let Some(chunk_offsets) = &self.chunk_offsets else {
379            vortex_bail!("chunk_offsets must be set to retrieve offset at index")
380        };
381
382        chunk_offsets
383            .execute_scalar(idx, ctx)?
384            .as_primitive()
385            .as_::<usize>()
386            .ok_or_else(|| vortex_err!("chunk offset does not fit in usize"))
387    }
388
389    /// Returns the number of patches sliced off from the current first chunk.
390    ///
391    /// When patches are sliced, the chunk offsets array is also sliced to only include
392    /// chunks that overlap with the slice range. However, the slice boundary may fall
393    /// in the middle of a chunk's patch range. This offset indicates how many patches
394    /// at the start of the first chunk should be skipped.
395    ///
396    /// Returns `None` if chunk offsets are not set.
397    #[inline]
398    pub fn offset_within_chunk(&self) -> Option<usize> {
399        self.offset_within_chunk
400    }
401
402    #[inline]
403    pub fn indices_ptype(&self) -> VortexResult<PType> {
404        PType::try_from(self.indices.dtype())
405            .map_err(|_| vortex_err!("indices dtype is not primitive"))
406    }
407
408    pub fn to_metadata(&self, len: usize, dtype: &DType) -> VortexResult<PatchesMetadata> {
409        if self.indices.len() > len {
410            vortex_bail!(
411                "Patch indices {} are longer than the array length {}",
412                self.indices.len(),
413                len
414            );
415        }
416        if self.values.dtype() != dtype {
417            vortex_bail!(
418                "Patch values dtype {} does not match array dtype {}",
419                self.values.dtype(),
420                dtype
421            );
422        }
423        let chunk_offsets_len = self.chunk_offsets.as_ref().map(|co| co.len());
424        let chunk_offsets_ptype = self.chunk_offsets.as_ref().map(|co| co.dtype().as_ptype());
425
426        Ok(PatchesMetadata::new(
427            self.indices.len(),
428            self.offset,
429            self.indices.dtype().as_ptype(),
430            chunk_offsets_len,
431            chunk_offsets_ptype,
432            self.offset_within_chunk,
433        ))
434    }
435
436    /// Get the patched value at a given index if it exists.
437    pub fn get_patched(
438        &self,
439        index: usize,
440        ctx: &mut ExecutionCtx,
441    ) -> VortexResult<Option<Scalar>> {
442        self.search_index(index, ctx)?
443            .to_found()
444            .map(|patch_idx| self.values().execute_scalar(patch_idx, ctx))
445            .transpose()
446    }
447
448    /// Searches for `index` in the indices array.
449    ///
450    /// Chooses between chunked search when [`Self::chunk_offsets`] is
451    /// available, and binary search otherwise. The `index` parameter is
452    /// adjusted by [`Self::offset`] for both.
453    ///
454    /// # Arguments
455    /// * `index` - The index to search for
456    ///
457    /// # Returns
458    /// * [`SearchResult::Found(patch_idx)`] - If a patch exists at this index, returns the
459    ///   position in the patches array
460    /// * [`SearchResult::NotFound(insertion_point)`] - If no patch exists, returns where
461    ///   a patch at this index would be inserted to maintain sorted order
462    ///
463    /// [`SearchResult::Found(patch_idx)`]: SearchResult::Found
464    /// [`SearchResult::NotFound(insertion_point)`]: SearchResult::NotFound
465    pub fn search_index(&self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<SearchResult> {
466        if self.chunk_offsets.is_some() {
467            return self.search_index_chunked(index, ctx);
468        }
469
470        search_index_binary_search(&self.indices, index + self.offset, ctx)
471    }
472
473    /// Constant time searches for `index` in the indices array.
474    ///
475    /// First determines which chunk the target index falls into, then performs
476    /// a binary search within that chunk's range.
477    ///
478    /// Returns a [`SearchResult`] indicating either the exact patch index if found,
479    /// or the insertion point if not found.
480    ///
481    /// Returns an error if `chunk_offsets` or `offset_within_chunk` are not set.
482    fn search_index_chunked(
483        &self,
484        index: usize,
485        ctx: &mut ExecutionCtx,
486    ) -> VortexResult<SearchResult> {
487        let Some(chunk_offsets) = &self.chunk_offsets else {
488            vortex_bail!("chunk_offsets is required to be set")
489        };
490
491        let Some(offset_within_chunk) = self.offset_within_chunk else {
492            vortex_bail!("offset_within_chunk is required to be set")
493        };
494
495        if index >= self.array_len() {
496            return Ok(SearchResult::NotFound(self.indices().len()));
497        }
498
499        let chunk_idx = (index + self.offset % PATCH_CHUNK_SIZE) / PATCH_CHUNK_SIZE;
500
501        // The three reads below are of the same array, so they share one probe rather than
502        // building one per read as `Self::chunk_offset_at` does.
503        let mut probe = chunk_offsets.repeated_probe();
504        let mut chunk_offset_at = |idx: usize| -> VortexResult<usize> {
505            probe
506                .execute_scalar(idx, ctx)?
507                .as_primitive()
508                .as_::<usize>()
509                .ok_or_else(|| vortex_err!("chunk offset does not fit in usize"))
510        };
511
512        // Patch index offsets are absolute and need to be offset by the first chunk of the current slice.
513        let base_offset = chunk_offset_at(0)?;
514
515        let patches_start_idx = (chunk_offset_at(chunk_idx)? - base_offset)
516            // Chunk offsets are only sliced off in case the slice is fully
517            // outside of the chunk range.
518            //
519            // Though the range for indices and values is sliced in terms of
520            // individual elements, not chunks. To account for that we do a
521            // saturating sub when adjusting the indices based on the chunk offset.
522            .saturating_sub(offset_within_chunk);
523
524        let patches_end_idx = if chunk_idx < chunk_offsets.len() - 1 {
525            (chunk_offset_at(chunk_idx + 1)? - base_offset)
526                .saturating_sub(offset_within_chunk)
527                .min(self.indices.len())
528        } else {
529            self.indices.len()
530        };
531
532        let chunk_indices = self.indices.slice(patches_start_idx..patches_end_idx)?;
533        let result = search_index_binary_search(&chunk_indices, index + self.offset, ctx)?;
534
535        Ok(match result {
536            SearchResult::Found(idx) => SearchResult::Found(patches_start_idx + idx),
537            SearchResult::NotFound(idx) => SearchResult::NotFound(patches_start_idx + idx),
538        })
539    }
540
541    /// Batch version of `search_index`.
542    ///
543    /// In contrast to `search_index`, this function requires `indices` as
544    /// well as `chunk_offsets` to be passed as slices. This is to avoid
545    /// redundant canonicalization and `scalar_at` lookups across calls.
546    fn search_index_chunked_batch<T, O>(
547        &self,
548        indices: &[T],
549        chunk_offsets: &[O],
550        index: T,
551    ) -> VortexResult<SearchResult>
552    where
553        T: UnsignedPType,
554        O: UnsignedPType,
555        usize: TryFrom<T>,
556        usize: TryFrom<O>,
557    {
558        let Some(offset_within_chunk) = self.offset_within_chunk else {
559            vortex_bail!("offset_within_chunk is required to be set")
560        };
561
562        let chunk_idx = {
563            let Ok(index) = usize::try_from(index) else {
564                // If the needle cannot be converted to usize, it's larger than all values in this array.
565                return Ok(SearchResult::NotFound(indices.len()));
566            };
567
568            if index >= self.array_len() {
569                return Ok(SearchResult::NotFound(self.indices().len()));
570            }
571
572            (index + self.offset % PATCH_CHUNK_SIZE) / PATCH_CHUNK_SIZE
573        };
574
575        // Patch index offsets are absolute and need to be offset by the first chunk of the current slice.
576        let chunk_offset = usize::try_from(chunk_offsets[chunk_idx] - chunk_offsets[0])
577            .map_err(|_| vortex_err!("chunk_offset failed to convert to usize"))?;
578
579        let patches_start_idx = chunk_offset
580            // Chunk offsets are only sliced off in case the slice is fully
581            // outside of the chunk range.
582            //
583            // Though the range for indices and values is sliced in terms of
584            // individual elements, not chunks. To account for that we do a
585            // saturating sub when adjusting the indices based on the chunk offset.
586            .saturating_sub(offset_within_chunk);
587
588        let patches_end_idx = if chunk_idx < chunk_offsets.len() - 1 {
589            usize::try_from(chunk_offsets[chunk_idx + 1] - chunk_offsets[0])
590                .map_err(|_| vortex_err!("patches_end_idx failed to convert to usize"))?
591                .saturating_sub(offset_within_chunk)
592                .min(indices.len())
593        } else {
594            self.indices.len()
595        };
596
597        let Some(offset) = T::from(self.offset) else {
598            // If the offset cannot be converted to T, it's larger than all values in this array.
599            return Ok(SearchResult::NotFound(indices.len()));
600        };
601
602        let chunk_indices = &indices[patches_start_idx..patches_end_idx];
603        let result = chunk_indices.search_sorted(&(index + offset), SearchSortedSide::Left)?;
604
605        Ok(match result {
606            SearchResult::Found(idx) => SearchResult::Found(patches_start_idx + idx),
607            SearchResult::NotFound(idx) => SearchResult::NotFound(patches_start_idx + idx),
608        })
609    }
610
611    /// Returns the minimum patch index
612    pub fn min_index(&self, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
613        let first = self
614            .indices
615            .execute_scalar(0, ctx)?
616            .as_primitive()
617            .as_::<usize>()
618            .ok_or_else(|| vortex_err!("index does not fit in usize"))?;
619        Ok(first - self.offset)
620    }
621
622    /// Returns the maximum patch index
623    pub fn max_index(&self, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
624        let last = self
625            .indices
626            .execute_scalar(self.indices.len() - 1, ctx)?
627            .as_primitive()
628            .as_::<usize>()
629            .ok_or_else(|| vortex_err!("index does not fit in usize"))?;
630        Ok(last - self.offset)
631    }
632
633    /// Filter the patches by a mask, resulting in new patches for the filtered array.
634    pub fn filter(&self, mask: &Mask, ctx: &mut ExecutionCtx) -> VortexResult<Option<Self>> {
635        if mask.len() != self.array_len {
636            vortex_bail!(
637                "Filter mask length {} does not match array length {}",
638                mask.len(),
639                self.array_len
640            );
641        }
642
643        match mask {
644            Mask::AllTrue(_) => Ok(Some(self.clone())),
645            Mask::AllFalse(_) => Ok(None),
646            Mask::Values(mask_values) => {
647                let flat_indices = self.indices().clone().execute::<PrimitiveArray>(ctx)?;
648                match_each_unsigned_integer_ptype!(flat_indices.ptype(), |I| {
649                    filter_patches_with_mask(
650                        flat_indices.as_slice::<I>(),
651                        self.offset(),
652                        self.values(),
653                        mask_values,
654                        ctx.allocator(),
655                    )
656                })
657            }
658        }
659    }
660
661    /// Mask the patches, REMOVING the patches where the mask is true.
662    /// Unlike filter, this preserves the patch indices.
663    /// Unlike mask on a single array, this does not set masked values to null.
664    ///
665    /// Masking an array always yields a nullable result (see [`crate::scalar_fn::fns::mask`]),
666    /// so the surviving patch values are widened to nullable to match the masked parent. Callers
667    /// therefore receive patches with the correct dtype and do not need to re-cast them.
668    // TODO(joe): make this lazy and remove the ctx.
669    pub fn mask(&self, mask: &Mask, ctx: &mut ExecutionCtx) -> VortexResult<Option<Self>> {
670        if mask.len() != self.array_len {
671            vortex_bail!(
672                "Filter mask length {} does not match array length {}",
673                mask.len(),
674                self.array_len
675            );
676        }
677
678        let filter_mask = match mask.bit_buffer() {
679            AllOr::All => return Ok(None),
680            AllOr::None => return self.clone().into_nullable_values().map(Some),
681            AllOr::Some(masked) => {
682                let patch_indices = self.indices().clone().execute::<PrimitiveArray>(ctx)?;
683                match_each_unsigned_integer_ptype!(patch_indices.ptype(), |P| {
684                    let patch_indices = patch_indices.as_slice::<P>();
685                    Mask::from_buffer(BitBuffer::collect_bool_in(
686                        patch_indices.len(),
687                        |i| {
688                            #[allow(clippy::cast_possible_truncation)]
689                            let idx = (patch_indices[i] as usize) - self.offset;
690                            !masked.value(idx)
691                        },
692                        ctx.allocator().clone(),
693                    ))
694                })
695            }
696        };
697
698        if filter_mask.all_false() {
699            return Ok(None);
700        }
701
702        // SAFETY: filtering indices/values with same mask maintains their 1:1 relationship
703        let filtered_indices = self.indices.filter(filter_mask.clone())?;
704        let filtered_values = self.values.filter(filter_mask)?;
705
706        Self {
707            array_len: self.array_len,
708            offset: self.offset,
709            indices: filtered_indices,
710            values: filtered_values,
711            // TODO(0ax1): Chunk offsets are invalid after a filter is applied.
712            chunk_offsets: None,
713            offset_within_chunk: self.offset_within_chunk,
714        }
715        .into_nullable_values()
716        .map(Some)
717    }
718
719    /// Widen the patch values to their nullable dtype, leaving indices and offsets untouched.
720    ///
721    /// The values stay logically unchanged (all currently-valid entries remain valid); only the
722    /// dtype's nullability flag is set. Used by [`Self::mask`], whose result must be nullable.
723    fn into_nullable_values(self) -> VortexResult<Self> {
724        if self.values.dtype().is_nullable() {
725            return Ok(self);
726        }
727        let nullable = self.values.dtype().as_nullable();
728        self.map_values(|values| values.cast(nullable))
729    }
730
731    /// Slice the patches by a range of the patched array.
732    pub fn slice(&self, range: Range<usize>, ctx: &mut ExecutionCtx) -> VortexResult<Option<Self>> {
733        let slice_start_idx = self.search_index(range.start, ctx)?.to_index();
734        let slice_end_idx = self.search_index(range.end, ctx)?.to_index();
735
736        if slice_start_idx == slice_end_idx {
737            return Ok(None);
738        }
739
740        let values = self.values().slice(slice_start_idx..slice_end_idx)?;
741        let indices = self.indices().slice(slice_start_idx..slice_end_idx)?;
742
743        let new_chunk_offsets = self
744            .chunk_offsets
745            .as_ref()
746            .map(|chunk_offsets| -> VortexResult<ArrayRef> {
747                let chunk_relative_offset = self.offset % PATCH_CHUNK_SIZE;
748                let chunk_start_idx = (chunk_relative_offset + range.start) / PATCH_CHUNK_SIZE;
749                let chunk_end_idx = (chunk_relative_offset + range.end).div_ceil(PATCH_CHUNK_SIZE);
750                chunk_offsets.slice(chunk_start_idx..chunk_end_idx)
751            })
752            .transpose()?;
753
754        let offset_within_chunk = new_chunk_offsets
755            .as_ref()
756            .map(|new_chunk_offsets| -> VortexResult<usize> {
757                let new_chunk_base = new_chunk_offsets
758                    .execute_scalar(0, ctx)?
759                    .as_primitive()
760                    .as_::<usize>()
761                    .ok_or_else(|| vortex_err!("chunk offset does not fit in usize"))?;
762                let parent_chunk_base = self.chunk_offset_at(0, ctx)?;
763                let parent_within = self.offset_within_chunk.unwrap_or(0);
764                Ok(parent_chunk_base + parent_within + slice_start_idx - new_chunk_base)
765            })
766            .transpose()?;
767
768        Ok(Some(Self {
769            array_len: range.len(),
770            offset: range.start + self.offset(),
771            indices,
772            values,
773            chunk_offsets: new_chunk_offsets,
774            offset_within_chunk,
775        }))
776    }
777
778    // https://docs.google.com/spreadsheets/d/1D9vBZ1QJ6mwcIvV5wIL0hjGgVchcEnAyhvitqWu2ugU
779    const PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN: f64 = 5.0;
780
781    fn is_map_faster_than_search(&self, take_indices: &PrimitiveArray) -> bool {
782        (self.num_patches() as f64 / take_indices.len() as f64)
783            < Self::PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN
784    }
785
786    /// Take the indices from the patches
787    ///
788    /// Any nulls in take_indices are added to the resulting patches.
789    pub fn take_with_nulls(
790        &self,
791        take_indices: &ArrayRef,
792        ctx: &mut ExecutionCtx,
793    ) -> VortexResult<Option<Self>> {
794        if take_indices.is_empty() {
795            return Ok(None);
796        }
797
798        let take_indices = take_indices.clone().execute::<PrimitiveArray>(ctx)?;
799        if self.is_map_faster_than_search(&take_indices) {
800            self.take_map(take_indices, true, ctx)
801        } else {
802            self.take_search(take_indices, true, ctx)
803        }
804    }
805
806    /// Take the indices from the patches.
807    ///
808    /// Any nulls in take_indices are ignored.
809    pub fn take(
810        &self,
811        take_indices: &ArrayRef,
812        ctx: &mut ExecutionCtx,
813    ) -> VortexResult<Option<Self>> {
814        if take_indices.is_empty() {
815            return Ok(None);
816        }
817
818        let take_indices = take_indices.clone().execute::<PrimitiveArray>(ctx)?;
819        if self.is_map_faster_than_search(&take_indices) {
820            self.take_map(take_indices, false, ctx)
821        } else {
822            self.take_search(take_indices, false, ctx)
823        }
824    }
825
826    #[expect(
827        clippy::cognitive_complexity,
828        reason = "complexity is from nested match_each_* macros"
829    )]
830    pub fn take_search(
831        &self,
832        take_indices: PrimitiveArray,
833        include_nulls: bool,
834        ctx: &mut ExecutionCtx,
835    ) -> VortexResult<Option<Self>> {
836        let take_indices_validity = take_indices.validity()?;
837        // Take indices are non-negative; reinterpret to unsigned so the `TakeT` dimension
838        // dispatches over 4 widths instead of 8.
839        let take_indices_unsigned =
840            take_indices.reinterpret_cast(take_indices.ptype().to_unsigned());
841        let patch_indices = self.indices.clone().execute::<PrimitiveArray>(ctx)?;
842        let chunk_offsets = self
843            .chunk_offsets()
844            .as_ref()
845            .map(|co| co.clone().execute::<PrimitiveArray>(ctx))
846            .transpose()?;
847
848        let (values_indices, new_indices): (BufferMut<u64>, BufferMut<u64>) =
849            match_each_unsigned_integer_ptype!(patch_indices.ptype(), |PatchT| {
850                let patch_indices_slice = patch_indices.as_slice::<PatchT>();
851                match_each_unsigned_integer_ptype!(take_indices_unsigned.ptype(), |TakeT| {
852                    let take_slice = take_indices_unsigned.as_slice::<TakeT>();
853
854                    if let Some(chunk_offsets) = chunk_offsets {
855                        match_each_unsigned_integer_ptype!(chunk_offsets.ptype(), |OffsetT| {
856                            let chunk_offsets = chunk_offsets.as_slice::<OffsetT>();
857                            take_indices_with_search_fn(
858                                patch_indices_slice,
859                                take_slice,
860                                take_indices
861                                    .as_ref()
862                                    .validity()?
863                                    .execute_mask(take_indices.as_ref().len(), ctx)?,
864                                include_nulls,
865                                ctx.allocator(),
866                                |take_idx| {
867                                    self.search_index_chunked_batch(
868                                        patch_indices_slice,
869                                        chunk_offsets,
870                                        take_idx,
871                                    )
872                                },
873                            )?
874                        })
875                    } else {
876                        take_indices_with_search_fn(
877                            patch_indices_slice,
878                            take_slice,
879                            take_indices
880                                .as_ref()
881                                .validity()?
882                                .execute_mask(take_indices.as_ref().len(), ctx)?,
883                            include_nulls,
884                            ctx.allocator(),
885                            |take_idx| {
886                                let Some(offset) = <PatchT as NumCast>::from(self.offset) else {
887                                    // If the offset cannot be converted to T, it's larger than all values in this array.
888                                    return Ok(SearchResult::NotFound(patch_indices_slice.len()));
889                                };
890
891                                patch_indices_slice
892                                    .search_sorted(&(take_idx + offset), SearchSortedSide::Left)
893                            },
894                        )?
895                    }
896                })
897            });
898
899        if new_indices.is_empty() {
900            return Ok(None);
901        }
902
903        let new_indices = new_indices.into_array();
904        let new_array_len = take_indices.len();
905        let values_validity = take_indices_validity.take(&new_indices)?;
906
907        Ok(Some(Self {
908            array_len: new_array_len,
909            offset: 0,
910            indices: new_indices,
911            values: self
912                .values()
913                .take(PrimitiveArray::new(values_indices, values_validity).into_array())?,
914            chunk_offsets: None,
915            offset_within_chunk: Some(0), // Reset when creating new Patches.
916        }))
917    }
918
919    pub fn take_map(
920        &self,
921        take_indices: PrimitiveArray,
922        include_nulls: bool,
923        ctx: &mut ExecutionCtx,
924    ) -> VortexResult<Option<Self>> {
925        let indices = self.indices.clone().execute::<PrimitiveArray>(ctx)?;
926        let new_length = take_indices.len();
927        // Take indices are non-negative; reinterpret to unsigned (4 widths instead of 8).
928        let take_indices_unsigned =
929            take_indices.reinterpret_cast(take_indices.ptype().to_unsigned());
930
931        let min_index = self.min_index(ctx)?;
932        let max_index = self.max_index(ctx)?;
933
934        let Some((new_sparse_indices, value_indices)) =
935            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
936                match_each_unsigned_integer_ptype!(take_indices_unsigned.ptype(), |TakeIndices| {
937                    let take_validity = take_indices
938                        .validity()?
939                        .execute_mask(take_indices.len(), ctx)?;
940                    let take_nullability = take_indices.validity()?.nullability();
941                    let take_slice = take_indices_unsigned.as_slice::<TakeIndices>();
942                    take_map::<_, TakeIndices>(
943                        indices.as_slice::<Indices>(),
944                        take_slice,
945                        take_validity,
946                        take_nullability,
947                        self.offset(),
948                        min_index,
949                        max_index,
950                        include_nulls,
951                        ctx.allocator(),
952                    )?
953                })
954            })
955        else {
956            return Ok(None);
957        };
958
959        let taken_values = self.values().take(value_indices)?;
960
961        Ok(Some(Patches {
962            array_len: new_length,
963            offset: 0,
964            indices: new_sparse_indices,
965            values: taken_values,
966            // TODO(0ax1): Chunk offsets are invalid after take is applied.
967            chunk_offsets: None,
968            offset_within_chunk: self.offset_within_chunk,
969        }))
970    }
971
972    pub fn map_values<F>(self, f: F) -> VortexResult<Self>
973    where
974        F: FnOnce(ArrayRef) -> VortexResult<ArrayRef>,
975    {
976        let values = f(self.values)?;
977        if self.indices.len() != values.len() {
978            vortex_bail!(
979                "map_values must preserve length: expected {} received {}",
980                self.indices.len(),
981                values.len()
982            )
983        }
984
985        Ok(Self {
986            array_len: self.array_len,
987            offset: self.offset,
988            indices: self.indices,
989            values,
990            chunk_offsets: self.chunk_offsets,
991            offset_within_chunk: self.offset_within_chunk,
992        })
993    }
994}
995
996/// Binary searches for `needle` in the indices array.
997///
998/// # Returns
999/// [`SearchResult::Found`] with the position if needle exists, or [`SearchResult::NotFound`]
1000/// with the insertion point if not found.
1001fn search_index_binary_search(
1002    indices: &ArrayRef,
1003    needle: usize,
1004    ctx: &mut ExecutionCtx,
1005) -> VortexResult<SearchResult> {
1006    if let Some(primitive) = indices.as_opt::<Primitive>() {
1007        match_each_unsigned_integer_ptype!(primitive.ptype(), |T| {
1008            let Ok(needle) = T::try_from(needle) else {
1009                // If the needle is not of type T, then it cannot possibly be in this array.
1010                //
1011                // The needle is a non-negative integer (a usize); therefore, it must be larger
1012                // than all values in this array.
1013                return Ok(SearchResult::NotFound(primitive.len()));
1014            };
1015            return primitive
1016                .as_slice::<T>()
1017                .search_sorted(&needle, SearchSortedSide::Left);
1018        });
1019    }
1020
1021    search_index_binary_search_scalar(indices, needle, ctx)
1022}
1023
1024fn search_index_binary_search_scalar(
1025    indices: &ArrayRef,
1026    needle: usize,
1027    ctx: &mut ExecutionCtx,
1028) -> VortexResult<SearchResult> {
1029    match_each_unsigned_integer_ptype!(indices.dtype().as_ptype(), |T| {
1030        SearchSortedPrimitiveArray::<T>::new(indices, ctx)
1031            .search_sorted(&needle, SearchSortedSide::Left)
1032    })
1033}
1034
1035#[expect(clippy::too_many_arguments)] // private function, can clean up one day
1036fn take_map<I: NativePType + Hash + Eq + TryFrom<usize>, T: NativePType>(
1037    indices: &[I],
1038    take_indices: &[T],
1039    take_validity: Mask,
1040    take_nullability: Nullability,
1041    indices_offset: usize,
1042    min_index: usize,
1043    max_index: usize,
1044    include_nulls: bool,
1045    allocator: &BufferAllocatorRef,
1046) -> VortexResult<Option<(ArrayRef, ArrayRef)>>
1047where
1048    usize: TryFrom<T>,
1049    VortexError: From<<I as TryFrom<usize>>::Error>,
1050{
1051    let offset_i = I::try_from(indices_offset)?;
1052
1053    let sparse_index_to_value_index: HashMap<I, usize> = indices
1054        .iter()
1055        .copied()
1056        .map(|idx| idx - offset_i)
1057        .enumerate()
1058        .map(|(value_index, sparse_index)| (sparse_index, value_index))
1059        .collect();
1060
1061    let mut new_sparse_indices =
1062        BufferMut::<u64>::with_capacity_in(take_indices.len(), allocator.clone());
1063    let mut value_indices =
1064        BufferMut::<u64>::with_capacity_in(take_indices.len(), allocator.clone());
1065
1066    for (idx_in_take, &take_idx) in take_indices.iter().enumerate() {
1067        let ti = usize::try_from(take_idx)
1068            .map_err(|_| vortex_err!("Failed to convert index to usize"))?;
1069
1070        // If we have to take nulls the take index doesn't matter, make it 0 for consistency
1071        let is_null = match take_validity.bit_buffer() {
1072            AllOr::All => false,
1073            AllOr::None => true,
1074            AllOr::Some(buf) => !buf.value(idx_in_take),
1075        };
1076        if is_null {
1077            if include_nulls {
1078                new_sparse_indices.push(idx_in_take as u64);
1079                value_indices.push(0);
1080            }
1081        } else if ti >= min_index && ti <= max_index {
1082            let ti_as_i = I::try_from(ti)
1083                .map_err(|_| vortex_err!("take index does not fit in index type"))?;
1084            if let Some(&value_index) = sparse_index_to_value_index.get(&ti_as_i) {
1085                new_sparse_indices.push(idx_in_take as u64);
1086                value_indices.push(value_index as u64);
1087            }
1088        }
1089    }
1090
1091    if new_sparse_indices.is_empty() {
1092        return Ok(None);
1093    }
1094
1095    let new_sparse_indices = new_sparse_indices.into_array();
1096    let values_validity =
1097        Validity::from_mask(take_validity, take_nullability).take(&new_sparse_indices)?;
1098    Ok(Some((
1099        new_sparse_indices,
1100        PrimitiveArray::new(value_indices, values_validity).into_array(),
1101    )))
1102}
1103
1104/// Filter patches with the provided mask (in flattened space).
1105///
1106/// The filter mask may contain indices that are non-patched. The return value of this function
1107/// is a new set of `Patches` with the indices relative to the provided `mask` rank, and the
1108/// patch values.
1109///
1110/// We search the larger side for each element of the smaller side, without building the mask
1111/// indices:
1112///
1113/// * More patches than mask indices: gallop through the patches.
1114/// * Cached mask indices: gallop through the mask indices.
1115/// * Otherwise: rank each patch in the mask bitmap.
1116fn filter_patches_with_mask<T: IntegerPType>(
1117    patch_indices: &[T],
1118    offset: usize,
1119    patch_values: &ArrayRef,
1120    mask: &MaskValues,
1121    allocator: &BufferAllocatorRef,
1122) -> VortexResult<Option<Patches>> {
1123    let true_count = mask.true_count();
1124    let patch_count = patch_indices.len();
1125    let expected_matches = true_count.min(patch_count);
1126
1127    // The mask rank of each kept patch, and a bitmap over the original patches marking them.
1128    let mut new_patch_indices =
1129        BufferMut::<u64>::with_capacity_in(expected_matches, allocator.clone());
1130    let mut kept_patches = BitBufferMut::from_buffer(
1131        BufferMut::zeroed_in(patch_count.div_ceil(8), allocator.clone()),
1132        0,
1133        patch_count,
1134    );
1135
1136    if patch_count > true_count {
1137        gallop_patches_for_mask_indices(
1138            patch_indices,
1139            offset,
1140            mask,
1141            &mut new_patch_indices,
1142            &mut kept_patches,
1143        )?;
1144    } else {
1145        match mask.cached_indices() {
1146            Some(mask_indices) => gallop_mask_indices_for_patches(
1147                patch_indices,
1148                offset,
1149                mask_indices,
1150                &mut new_patch_indices,
1151                &mut kept_patches,
1152            )?,
1153            None => rank_patches_in_mask_bitmap(
1154                patch_indices,
1155                offset,
1156                mask.bit_buffer(),
1157                &mut new_patch_indices,
1158                &mut kept_patches,
1159            )?,
1160        }
1161    }
1162
1163    let new_patch_values = match new_patch_indices.len() {
1164        0 => return Ok(None),
1165        kept if kept == patch_count => patch_values.clone(),
1166        _ => patch_values.filter(Mask::from_buffer(kept_patches.freeze()))?,
1167    };
1168    let new_patch_indices = new_patch_indices.into_array();
1169
1170    // SAFETY: there is one index and one value per kept patch. The indices are strictly
1171    // increasing non-nullable `u64` mask ranks below `true_count`.
1172    Ok(Some(unsafe {
1173        Patches::new_unchecked(
1174            true_count,
1175            0,
1176            new_patch_indices,
1177            new_patch_values,
1178            // TODO(0ax1): Chunk offsets are invalid after a filter is applied.
1179            None,
1180            None,
1181        )
1182    }))
1183}
1184
1185/// Returns the first position at or after `start` whose value is not less than `needle`.
1186///
1187/// The cost is logarithmic in the distance from `start` to the result.
1188fn gallop_lower_bound<T: Ord>(values: &[T], start: usize, needle: &T) -> usize {
1189    let mut low = start;
1190    let mut high = start;
1191    let mut step = 1;
1192
1193    while high < values.len() && values[high] < *needle {
1194        low = high + 1;
1195        high += step;
1196        step *= 2;
1197    }
1198
1199    let high = high.min(values.len());
1200    low + values[low..high].partition_point(|value| value < needle)
1201}
1202
1203/// Finds the patch of each mask index, for masks that are sparser than the patches.
1204///
1205/// Mask indices after the last patch cannot match, so we skip them.
1206fn gallop_patches_for_mask_indices<T: IntegerPType>(
1207    patch_indices: &[T],
1208    offset: usize,
1209    mask: &MaskValues,
1210    new_patch_indices: &mut BufferMut<u64>,
1211    kept_patches: &mut BitBufferMut,
1212) -> VortexResult<()> {
1213    let Some(&last_patch_index) = patch_indices.last() else {
1214        return Ok(());
1215    };
1216    let last_mask_index = patch_index_to_usize(last_patch_index, offset)?;
1217
1218    let mut patch_position = 0;
1219    let mut mask_position = 0;
1220
1221    let mut visit = |mask_index: usize| {
1222        // At most the last patch index, so it fits in `T` and the search stays in bounds.
1223        let needle = <T as NumCast>::from(mask_index + offset)
1224            .vortex_expect("mask index is at most the last patch index");
1225
1226        patch_position = gallop_lower_bound(patch_indices, patch_position, &needle);
1227        if patch_indices[patch_position] == needle {
1228            new_patch_indices.push(mask_position as u64);
1229            kept_patches.set(patch_position);
1230            patch_position += 1;
1231        }
1232
1233        mask_position += 1;
1234    };
1235
1236    match mask.cached_indices() {
1237        Some(mask_indices) => {
1238            let end = mask_indices.partition_point(|&mask_index| mask_index <= last_mask_index);
1239            mask_indices[..end]
1240                .iter()
1241                .for_each(|&mask_index| visit(mask_index));
1242        }
1243        None => mask
1244            .bit_buffer()
1245            .slice(..=last_mask_index)
1246            .for_each_set_index(visit),
1247    }
1248
1249    Ok(())
1250}
1251
1252/// Finds the mask position of each patch, for patches that are not sparser than the mask.
1253fn gallop_mask_indices_for_patches<T: IntegerPType>(
1254    patch_indices: &[T],
1255    offset: usize,
1256    mask_indices: &[usize],
1257    new_patch_indices: &mut BufferMut<u64>,
1258    kept_patches: &mut BitBufferMut,
1259) -> VortexResult<()> {
1260    let mut mask_position = 0;
1261
1262    for (patch_position, patch_index) in patch_indices.iter().enumerate() {
1263        let index = patch_index_to_usize(*patch_index, offset)?;
1264
1265        mask_position = gallop_lower_bound(mask_indices, mask_position, &index);
1266        if mask_position == mask_indices.len() {
1267            break;
1268        }
1269
1270        if mask_indices[mask_position] == index {
1271            new_patch_indices.push(mask_position as u64);
1272            kept_patches.set(patch_position);
1273            mask_position += 1;
1274        }
1275    }
1276
1277    Ok(())
1278}
1279
1280/// Finds the mask position of each patch from the mask bitmap, without building the mask indices.
1281///
1282/// The mask position is the number of set bits before the patch, counted between adjacent patches.
1283fn rank_patches_in_mask_bitmap<T: IntegerPType>(
1284    patch_indices: &[T],
1285    offset: usize,
1286    mask: &BitBuffer,
1287    new_patch_indices: &mut BufferMut<u64>,
1288    kept_patches: &mut BitBufferMut,
1289) -> VortexResult<()> {
1290    let mut rank = 0;
1291    let mut previous_index = 0;
1292
1293    for (patch_position, patch_index) in patch_indices.iter().enumerate() {
1294        let index = patch_index_to_usize(*patch_index, offset)?;
1295
1296        rank += mask.count_range(previous_index, index);
1297        previous_index = index;
1298
1299        if mask.value(index) {
1300            new_patch_indices.push(rank as u64);
1301            kept_patches.set(patch_position);
1302        }
1303    }
1304
1305    Ok(())
1306}
1307
1308#[inline]
1309fn patch_index_to_usize<T: IntegerPType>(patch_index: T, offset: usize) -> VortexResult<usize> {
1310    Ok(patch_index
1311        .to_usize()
1312        .ok_or_else(|| vortex_err!("patch index does not fit in usize"))?
1313        - offset)
1314}
1315
1316fn take_indices_with_search_fn<
1317    I: UnsignedPType,
1318    T: IntegerPType,
1319    F: Fn(I) -> VortexResult<SearchResult>,
1320>(
1321    indices: &[I],
1322    take_indices: &[T],
1323    take_validity: Mask,
1324    include_nulls: bool,
1325    allocator: &BufferAllocatorRef,
1326    search_fn: F,
1327) -> VortexResult<(BufferMut<u64>, BufferMut<u64>)> {
1328    let mut values_indices = BufferMut::with_capacity_in(take_indices.len(), allocator.clone());
1329    let mut new_indices = BufferMut::with_capacity_in(take_indices.len(), allocator.clone());
1330
1331    for (new_patch_idx, &take_idx) in take_indices.iter().enumerate() {
1332        if !take_validity.value(new_patch_idx) {
1333            if include_nulls {
1334                // For nulls, patch index doesn't matter - use 0 for consistency
1335                values_indices.push(0u64);
1336                new_indices.push(new_patch_idx as u64);
1337            }
1338            continue;
1339        } else {
1340            let search_result = match I::from(take_idx) {
1341                Some(idx) => search_fn(idx)?,
1342                None => SearchResult::NotFound(indices.len()),
1343            };
1344
1345            if let Some(patch_idx) = search_result.to_found() {
1346                values_indices.push(patch_idx as u64);
1347                new_indices.push(new_patch_idx as u64);
1348            }
1349        }
1350    }
1351
1352    Ok((values_indices, new_indices))
1353}
1354
1355#[cfg(test)]
1356mod test {
1357    use rstest::rstest;
1358    use vortex_buffer::BitBuffer;
1359    use vortex_buffer::Buffer;
1360    use vortex_buffer::BufferMut;
1361    use vortex_buffer::buffer;
1362    use vortex_error::VortexExpect;
1363    use vortex_error::VortexResult;
1364    use vortex_mask::Mask;
1365
1366    use crate::IntoArray;
1367    use crate::VortexSessionExecute;
1368    use crate::array_session;
1369    use crate::assert_arrays_eq;
1370    use crate::patches::Patches;
1371    use crate::patches::PrimitiveArray;
1372    use crate::search_sorted::SearchResult;
1373    use crate::validity::Validity;
1374
1375    #[test]
1376    fn test_filter() {
1377        let mut ctx = array_session().create_execution_ctx();
1378        let patches = Patches::new(
1379            100,
1380            0,
1381            buffer![10u32, 11, 20].into_array(),
1382            buffer![100, 110, 200].into_array(),
1383            None,
1384            &mut ctx,
1385        )
1386        .unwrap();
1387
1388        let filtered = patches
1389            .filter(&Mask::from_indices(100, vec![10, 20, 30]), &mut ctx)
1390            .unwrap()
1391            .unwrap();
1392
1393        assert_arrays_eq!(
1394            filtered.indices(),
1395            PrimitiveArray::from_iter([0u64, 1]),
1396            &mut ctx
1397        );
1398        assert_arrays_eq!(
1399            filtered.values(),
1400            PrimitiveArray::from_iter([100i32, 200]),
1401            &mut ctx
1402        );
1403    }
1404
1405    #[test]
1406    fn take_with_nulls() {
1407        let mut ctx = array_session().create_execution_ctx();
1408        let patches = Patches::new(
1409            20,
1410            0,
1411            buffer![2u64, 9, 15].into_array(),
1412            PrimitiveArray::new(buffer![33_i32, 44, 55], Validity::AllValid).into_array(),
1413            None,
1414            &mut ctx,
1415        )
1416        .unwrap();
1417
1418        let taken = patches
1419            .take(
1420                &PrimitiveArray::new(buffer![9, 0], Validity::from_iter(vec![true, false]))
1421                    .into_array(),
1422                &mut ctx,
1423            )
1424            .unwrap()
1425            .unwrap();
1426        let values = taken.values();
1427        let primitive_indices = taken.indices();
1428        assert_eq!(taken.array_len(), 2);
1429        assert_arrays_eq!(
1430            values,
1431            PrimitiveArray::from_option_iter([Some(44i32)]),
1432            &mut ctx
1433        );
1434        assert_arrays_eq!(
1435            primitive_indices,
1436            PrimitiveArray::from_iter([0u64]),
1437            &mut ctx
1438        );
1439        assert_eq!(
1440            values
1441                .validity()
1442                .unwrap()
1443                .execute_mask(values.len(), &mut ctx)
1444                .unwrap(),
1445            Mask::from_iter(vec![true])
1446        );
1447    }
1448
1449    #[test]
1450    fn take_search_with_nulls_chunked() {
1451        let mut ctx = array_session().create_execution_ctx();
1452        let patches = Patches::new(
1453            20,
1454            0,
1455            buffer![2u64, 9, 15].into_array(),
1456            buffer![33_i32, 44, 55].into_array(),
1457            Some(buffer![0u64].into_array()),
1458            &mut ctx,
1459        )
1460        .unwrap();
1461
1462        let taken = patches
1463            .take_search(
1464                PrimitiveArray::new(buffer![9, 0], Validity::from_iter([true, false])),
1465                true,
1466                &mut ctx,
1467            )
1468            .unwrap()
1469            .unwrap();
1470
1471        let values = taken.values();
1472        assert_eq!(taken.array_len(), 2);
1473        assert_arrays_eq!(
1474            values,
1475            PrimitiveArray::from_option_iter([Some(44i32), None]),
1476            &mut ctx
1477        );
1478        assert_arrays_eq!(
1479            taken.indices(),
1480            PrimitiveArray::from_iter([0u64, 1]),
1481            &mut ctx
1482        );
1483
1484        assert_eq!(
1485            values
1486                .validity()
1487                .unwrap()
1488                .execute_mask(values.len(), &mut ctx)
1489                .unwrap(),
1490            Mask::from_iter([true, false])
1491        );
1492    }
1493
1494    #[test]
1495    fn take_search_chunked_multiple_chunks() {
1496        let mut ctx = array_session().create_execution_ctx();
1497        let patches = Patches::new(
1498            2048,
1499            0,
1500            buffer![100u64, 500, 1200, 1800].into_array(),
1501            buffer![10_i32, 20, 30, 40].into_array(),
1502            Some(buffer![0u64, 2].into_array()),
1503            &mut ctx,
1504        )
1505        .unwrap();
1506
1507        let taken = patches
1508            .take_search(
1509                PrimitiveArray::new(buffer![500, 1200, 999], Validity::AllValid),
1510                true,
1511                &mut ctx,
1512            )
1513            .unwrap()
1514            .unwrap();
1515
1516        assert_eq!(taken.array_len(), 3);
1517        assert_arrays_eq!(
1518            taken.values(),
1519            PrimitiveArray::from_option_iter([Some(20i32), Some(30)]),
1520            &mut ctx
1521        );
1522        assert_arrays_eq!(
1523            taken.indices(),
1524            PrimitiveArray::from_iter([0u64, 1]),
1525            &mut ctx
1526        );
1527    }
1528
1529    #[test]
1530    fn take_search_chunked_indices_with_no_patches() {
1531        let mut ctx = array_session().create_execution_ctx();
1532        let patches = Patches::new(
1533            20,
1534            0,
1535            buffer![2u64, 9, 15].into_array(),
1536            buffer![33_i32, 44, 55].into_array(),
1537            Some(buffer![0u64].into_array()),
1538            &mut ctx,
1539        )
1540        .unwrap();
1541
1542        let taken = patches
1543            .take_search(
1544                PrimitiveArray::new(buffer![3, 4, 5], Validity::AllValid),
1545                true,
1546                &mut ctx,
1547            )
1548            .unwrap();
1549
1550        assert!(taken.is_none());
1551    }
1552
1553    #[test]
1554    fn take_search_chunked_interleaved() {
1555        let mut ctx = array_session().create_execution_ctx();
1556        let patches = Patches::new(
1557            30,
1558            0,
1559            buffer![5u64, 10, 20, 25].into_array(),
1560            buffer![100_i32, 200, 300, 400].into_array(),
1561            Some(buffer![0u64].into_array()),
1562            &mut ctx,
1563        )
1564        .unwrap();
1565
1566        let taken = patches
1567            .take_search(
1568                PrimitiveArray::new(buffer![10, 15, 20, 99], Validity::AllValid),
1569                true,
1570                &mut ctx,
1571            )
1572            .unwrap()
1573            .unwrap();
1574
1575        assert_eq!(taken.array_len(), 4);
1576        assert_arrays_eq!(
1577            taken.values(),
1578            PrimitiveArray::from_option_iter([Some(200i32), Some(300)]),
1579            &mut ctx
1580        );
1581        assert_arrays_eq!(
1582            taken.indices(),
1583            PrimitiveArray::from_iter([0u64, 2]),
1584            &mut ctx
1585        );
1586    }
1587
1588    #[test]
1589    fn test_take_search_multiple_chunk_offsets() {
1590        let mut ctx = array_session().create_execution_ctx();
1591        let patches = Patches::new(
1592            1500,
1593            0,
1594            BufferMut::from_iter(0..1500u64).into_array(),
1595            BufferMut::from_iter(0..1500i32).into_array(),
1596            Some(buffer![0u64, 1024u64].into_array()),
1597            &mut ctx,
1598        )
1599        .unwrap();
1600
1601        let taken = patches
1602            .take_search(
1603                PrimitiveArray::new(BufferMut::from_iter(0..1500u64), Validity::AllValid),
1604                false,
1605                &mut ctx,
1606            )
1607            .unwrap()
1608            .unwrap();
1609
1610        assert_eq!(taken.array_len(), 1500);
1611        assert_arrays_eq!(
1612            taken.indices(),
1613            PrimitiveArray::from_iter(0..1500u64),
1614            &mut ctx
1615        );
1616        assert_arrays_eq!(
1617            taken.values(),
1618            PrimitiveArray::from_option_iter((0..1500i32).map(Some)),
1619            &mut ctx
1620        );
1621    }
1622
1623    #[test]
1624    fn doubly_sliced() {
1625        let mut ctx = array_session().create_execution_ctx();
1626        let values = buffer![15_u32, 135, 13531, 42].into_array();
1627        let indices = buffer![10_u64, 11, 50, 100].into_array();
1628
1629        let patches = Patches::new(101, 0, indices, values, None, &mut ctx).unwrap();
1630
1631        let sliced = patches.slice(15..100, &mut ctx).unwrap().unwrap();
1632        assert_eq!(sliced.array_len(), 100 - 15);
1633        assert_arrays_eq!(
1634            sliced.values(),
1635            PrimitiveArray::from_iter([13531u32]),
1636            &mut ctx
1637        );
1638
1639        assert_eq!(sliced.offset(), 15);
1640        assert_arrays_eq!(
1641            sliced.indices(),
1642            PrimitiveArray::from_iter([50u64]),
1643            &mut ctx
1644        );
1645        let doubly_sliced = sliced.slice(35..36, &mut ctx).unwrap().unwrap();
1646        assert_eq!(doubly_sliced.array_len(), 1);
1647        assert_eq!(doubly_sliced.offset(), 50);
1648        assert_eq!(
1649            doubly_sliced.search_index(0, &mut ctx).unwrap(),
1650            SearchResult::Found(0)
1651        );
1652        assert_arrays_eq!(
1653            doubly_sliced.values(),
1654            PrimitiveArray::from_iter([13531u32]),
1655            &mut ctx
1656        );
1657    }
1658
1659    #[test]
1660    fn test_mask_all_true() {
1661        let mut ctx = array_session().create_execution_ctx();
1662        let patches = Patches::new(
1663            10,
1664            0,
1665            buffer![2u64, 5, 8].into_array(),
1666            buffer![100i32, 200, 300].into_array(),
1667            None,
1668            &mut ctx,
1669        )
1670        .unwrap();
1671
1672        let mask = Mask::new_true(10);
1673        let masked = patches.mask(&mask, &mut ctx).unwrap();
1674        assert!(masked.is_none());
1675    }
1676
1677    #[test]
1678    fn test_mask_all_false() {
1679        let mut ctx = array_session().create_execution_ctx();
1680        let patches = Patches::new(
1681            10,
1682            0,
1683            buffer![2u64, 5, 8].into_array(),
1684            buffer![100i32, 200, 300].into_array(),
1685            None,
1686            &mut ctx,
1687        )
1688        .unwrap();
1689
1690        let mask = Mask::new_false(10);
1691        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
1692
1693        // Masking widens the surviving (still valid) values to nullable.
1694        assert!(masked.values().dtype().is_nullable());
1695        assert_arrays_eq!(
1696            masked.values(),
1697            PrimitiveArray::from_option_iter([Some(100i32), Some(200), Some(300)]),
1698            &mut ctx
1699        );
1700        assert!(masked.values().is_valid(0, &mut ctx).unwrap());
1701        assert!(masked.values().is_valid(1, &mut ctx).unwrap());
1702        assert!(masked.values().is_valid(2, &mut ctx).unwrap());
1703
1704        // Indices should remain unchanged
1705        assert_arrays_eq!(
1706            masked.indices(),
1707            PrimitiveArray::from_iter([2u64, 5, 8]),
1708            &mut ctx
1709        );
1710    }
1711
1712    #[test]
1713    fn test_mask_partial() {
1714        let mut ctx = array_session().create_execution_ctx();
1715        let patches = Patches::new(
1716            10,
1717            0,
1718            buffer![2u64, 5, 8].into_array(),
1719            buffer![100i32, 200, 300].into_array(),
1720            None,
1721            &mut ctx,
1722        )
1723        .unwrap();
1724
1725        // Mask that removes patches at indices 2 and 8 (but not 5)
1726        let mask = Mask::from_iter([
1727            false, false, true, false, false, false, false, false, true, false,
1728        ]);
1729        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
1730
1731        // Only the patch at index 5 should remain
1732        assert_eq!(masked.values().len(), 1);
1733        assert_arrays_eq!(
1734            masked.values(),
1735            PrimitiveArray::from_option_iter([Some(200i32)]),
1736            &mut ctx
1737        );
1738
1739        // Only index 5 should remain
1740        assert_arrays_eq!(
1741            masked.indices(),
1742            PrimitiveArray::from_iter([5u64]),
1743            &mut ctx
1744        );
1745    }
1746
1747    #[test]
1748    fn test_mask_with_offset() {
1749        let mut ctx = array_session().create_execution_ctx();
1750        let patches = Patches::new(
1751            10,
1752            5,                                  // offset
1753            buffer![7u64, 10, 13].into_array(), // actual indices are 2, 5, 8
1754            buffer![100i32, 200, 300].into_array(),
1755            None,
1756            &mut ctx,
1757        )
1758        .unwrap();
1759
1760        // Mask that sets actual index 2 to null
1761        let mask = Mask::from_iter([
1762            false, false, true, false, false, false, false, false, false, false,
1763        ]);
1764
1765        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
1766        assert_eq!(masked.array_len(), 10);
1767        assert_eq!(masked.offset(), 5);
1768        assert_arrays_eq!(
1769            masked.indices(),
1770            PrimitiveArray::from_iter([10u64, 13]),
1771            &mut ctx
1772        );
1773        assert_arrays_eq!(
1774            masked.values(),
1775            PrimitiveArray::from_option_iter([Some(200i32), Some(300)]),
1776            &mut ctx
1777        );
1778    }
1779
1780    #[test]
1781    fn test_mask_nullable_values() {
1782        let mut ctx = array_session().create_execution_ctx();
1783        let patches = Patches::new(
1784            10,
1785            0,
1786            buffer![2u64, 5, 8].into_array(),
1787            PrimitiveArray::from_option_iter([Some(100i32), None, Some(300)]).into_array(),
1788            None,
1789            &mut ctx,
1790        )
1791        .unwrap();
1792
1793        // Test masking removes patch at index 2
1794        let mask = Mask::from_iter([
1795            false, false, true, false, false, false, false, false, false, false,
1796        ]);
1797        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
1798
1799        // Patches at indices 5 and 8 should remain
1800        assert_arrays_eq!(
1801            masked.indices(),
1802            PrimitiveArray::from_iter([5u64, 8]),
1803            &mut ctx
1804        );
1805
1806        // Values should be the null and 300
1807        let masked_values = masked
1808            .values()
1809            .clone()
1810            .execute::<PrimitiveArray>(&mut ctx)
1811            .unwrap();
1812        assert_eq!(masked_values.len(), 2);
1813        assert!(!masked_values.is_valid(0, &mut ctx).unwrap()); // the null value at index 5
1814        assert!(masked_values.is_valid(1, &mut ctx).unwrap()); // the 300 value at index 8
1815        assert_eq!(
1816            i32::try_from(&masked_values.execute_scalar(1, &mut ctx).unwrap()).unwrap(),
1817            300i32
1818        );
1819    }
1820
1821    /// Covers each filter algorithm.
1822    #[rstest]
1823    #[case::sparse_mask((0..1000).step_by(2).collect(), (0..1000).step_by(97).collect())]
1824    #[case::sparse_patches((0..1000).step_by(97).collect(), (0..1000).step_by(2).collect())]
1825    #[case::similar_sizes((0..1000).step_by(3).collect(), (0..1000).step_by(2).collect())]
1826    #[case::no_overlap((0..500).collect(), (500..1000).step_by(50).collect())]
1827    #[case::no_overlap_sparse_patches((500..1000).step_by(100).collect(), (0..500).collect())]
1828    #[case::edges(vec![0, 1, 998, 999], vec![0, 999])]
1829    #[case::all_patches_kept((0..1000).step_by(100).collect(), (0..1000).step_by(2).collect())]
1830    fn test_filter_matches_naive(
1831        #[case] patch_indices: Vec<usize>,
1832        #[case] mask_indices: Vec<usize>,
1833        #[values(0, 5)] offset: usize,
1834        #[values(true, false)] cache_mask_indices: bool,
1835    ) -> VortexResult<()> {
1836        const LEN: usize = 1000;
1837        let mut ctx = array_session().create_execution_ctx();
1838
1839        let patches = Patches::new(
1840            LEN,
1841            offset,
1842            patch_indices
1843                .iter()
1844                .map(|&index| u32::try_from(index + offset).vortex_expect("index fits in u32"))
1845                .collect::<Buffer<u32>>()
1846                .into_array(),
1847            (0i32..)
1848                .take(patch_indices.len())
1849                .collect::<Buffer<i32>>()
1850                .into_array(),
1851            None,
1852            &mut ctx,
1853        )?;
1854
1855        let mask = if cache_mask_indices {
1856            Mask::from_indices(LEN, mask_indices.iter().copied())
1857        } else {
1858            Mask::from_buffer(BitBuffer::from_iter(
1859                (0..LEN).map(|index| mask_indices.binary_search(&index).is_ok()),
1860            ))
1861        };
1862
1863        let (expected_indices, expected_values): (Vec<u64>, Vec<i32>) = mask_indices
1864            .iter()
1865            .enumerate()
1866            .filter_map(|(mask_position, index)| {
1867                patch_indices
1868                    .binary_search(index)
1869                    .ok()
1870                    .map(|patch_position| {
1871                        (
1872                            mask_position as u64,
1873                            i32::try_from(patch_position).vortex_expect("position fits in i32"),
1874                        )
1875                    })
1876            })
1877            .unzip();
1878
1879        let filtered = patches.filter(&mask, &mut ctx)?;
1880
1881        let Some(filtered) = filtered else {
1882            assert!(expected_indices.is_empty());
1883            return Ok(());
1884        };
1885
1886        assert_eq!(filtered.array_len(), mask_indices.len());
1887        assert_arrays_eq!(
1888            filtered.indices(),
1889            PrimitiveArray::from_iter(expected_indices),
1890            &mut ctx
1891        );
1892        assert_arrays_eq!(
1893            filtered.values(),
1894            PrimitiveArray::from_iter(expected_values),
1895            &mut ctx
1896        );
1897
1898        Ok(())
1899    }
1900
1901    /// Mask indices past the maximum of the patch index type cannot match a patch.
1902    #[test]
1903    fn test_filter_sparse_mask_past_index_type_max() -> VortexResult<()> {
1904        let mut ctx = array_session().create_execution_ctx();
1905        let patches = Patches::new(
1906            1000,
1907            0,
1908            (0..=u8::MAX).collect::<Buffer<u8>>().into_array(),
1909            (0..256i32).collect::<Buffer<i32>>().into_array(),
1910            None,
1911            &mut ctx,
1912        )?;
1913
1914        let mask = Mask::from_indices(1000, [3, 255, 256, 900]);
1915        let filtered = patches
1916            .filter(&mask, &mut ctx)?
1917            .expect("patches at 3 and 255 are kept");
1918
1919        assert_arrays_eq!(
1920            filtered.indices(),
1921            PrimitiveArray::from_iter([0u64, 1]),
1922            &mut ctx
1923        );
1924        assert_arrays_eq!(
1925            filtered.values(),
1926            PrimitiveArray::from_iter([3i32, 255]),
1927            &mut ctx
1928        );
1929
1930        Ok(())
1931    }
1932
1933    #[test]
1934    fn test_filter_keep_all() {
1935        let mut ctx = array_session().create_execution_ctx();
1936        let patches = Patches::new(
1937            10,
1938            0,
1939            buffer![2u64, 5, 8].into_array(),
1940            buffer![100i32, 200, 300].into_array(),
1941            None,
1942            &mut ctx,
1943        )
1944        .unwrap();
1945
1946        // Keep all indices (mask with indices 0-9)
1947        let mask = Mask::from_indices(10, 0..10);
1948        let filtered = patches.filter(&mask, &mut ctx).unwrap().unwrap();
1949
1950        assert_arrays_eq!(
1951            filtered.indices(),
1952            PrimitiveArray::from_iter([2u64, 5, 8]),
1953            &mut ctx
1954        );
1955        assert_arrays_eq!(
1956            filtered.values(),
1957            PrimitiveArray::from_iter([100i32, 200, 300]),
1958            &mut ctx
1959        );
1960    }
1961
1962    #[test]
1963    fn test_filter_none() {
1964        let mut ctx = array_session().create_execution_ctx();
1965        let patches = Patches::new(
1966            10,
1967            0,
1968            buffer![2u64, 5, 8].into_array(),
1969            buffer![100i32, 200, 300].into_array(),
1970            None,
1971            &mut ctx,
1972        )
1973        .unwrap();
1974
1975        // Filter out all (empty mask means keep nothing)
1976        let mask = Mask::from_indices(10, vec![]);
1977        let filtered = patches.filter(&mask, &mut ctx).unwrap();
1978        assert!(filtered.is_none());
1979    }
1980
1981    #[test]
1982    fn test_filter_with_indices() {
1983        let mut ctx = array_session().create_execution_ctx();
1984        let patches = Patches::new(
1985            10,
1986            0,
1987            buffer![2u64, 5, 8].into_array(),
1988            buffer![100i32, 200, 300].into_array(),
1989            None,
1990            &mut ctx,
1991        )
1992        .unwrap();
1993
1994        // Keep indices 2, 5, 9 (so patches at 2 and 5 remain)
1995        let mask = Mask::from_indices(10, vec![2, 5, 9]);
1996        let filtered = patches.filter(&mask, &mut ctx).unwrap().unwrap();
1997
1998        assert_arrays_eq!(
1999            filtered.indices(),
2000            PrimitiveArray::from_iter([0u64, 1]),
2001            &mut ctx
2002        ); // Adjusted indices
2003        assert_arrays_eq!(
2004            filtered.values(),
2005            PrimitiveArray::from_iter([100i32, 200]),
2006            &mut ctx
2007        );
2008    }
2009
2010    #[test]
2011    fn test_slice_full_range() {
2012        let mut ctx = array_session().create_execution_ctx();
2013        let patches = Patches::new(
2014            10,
2015            0,
2016            buffer![2u64, 5, 8].into_array(),
2017            buffer![100i32, 200, 300].into_array(),
2018            None,
2019            &mut ctx,
2020        )
2021        .unwrap();
2022
2023        let sliced = patches.slice(0..10, &mut ctx).unwrap().unwrap();
2024
2025        assert_arrays_eq!(
2026            sliced.indices(),
2027            PrimitiveArray::from_iter([2u64, 5, 8]),
2028            &mut ctx
2029        );
2030        assert_arrays_eq!(
2031            sliced.values(),
2032            PrimitiveArray::from_iter([100i32, 200, 300]),
2033            &mut ctx
2034        );
2035    }
2036
2037    #[test]
2038    fn test_slice_partial() {
2039        let mut ctx = array_session().create_execution_ctx();
2040        let patches = Patches::new(
2041            10,
2042            0,
2043            buffer![2u64, 5, 8].into_array(),
2044            buffer![100i32, 200, 300].into_array(),
2045            None,
2046            &mut ctx,
2047        )
2048        .unwrap();
2049
2050        // Slice from 3 to 8 (includes patch at 5)
2051        let sliced = patches.slice(3..8, &mut ctx).unwrap().unwrap();
2052
2053        assert_arrays_eq!(
2054            sliced.indices(),
2055            PrimitiveArray::from_iter([5u64]),
2056            &mut ctx
2057        ); // Index stays the same
2058        assert_arrays_eq!(
2059            sliced.values(),
2060            PrimitiveArray::from_iter([200i32]),
2061            &mut ctx
2062        );
2063        assert_eq!(sliced.array_len(), 5); // 8 - 3 = 5
2064        assert_eq!(sliced.offset(), 3); // New offset
2065    }
2066
2067    #[test]
2068    fn test_slice_no_patches() {
2069        let mut ctx = array_session().create_execution_ctx();
2070        let patches = Patches::new(
2071            10,
2072            0,
2073            buffer![2u64, 5, 8].into_array(),
2074            buffer![100i32, 200, 300].into_array(),
2075            None,
2076            &mut ctx,
2077        )
2078        .unwrap();
2079
2080        // Slice from 6 to 7 (no patches in this range)
2081        let sliced = patches.slice(6..7, &mut ctx).unwrap();
2082        assert!(sliced.is_none());
2083    }
2084
2085    #[test]
2086    fn test_slice_with_offset() {
2087        let mut ctx = array_session().create_execution_ctx();
2088        let patches = Patches::new(
2089            10,
2090            5,                                  // offset
2091            buffer![7u64, 10, 13].into_array(), // actual indices are 2, 5, 8
2092            buffer![100i32, 200, 300].into_array(),
2093            None,
2094            &mut ctx,
2095        )
2096        .unwrap();
2097
2098        // Slice from 3 to 8 (includes patch at actual index 5)
2099        let sliced = patches.slice(3..8, &mut ctx).unwrap().unwrap();
2100
2101        assert_arrays_eq!(
2102            sliced.indices(),
2103            PrimitiveArray::from_iter([10u64]),
2104            &mut ctx
2105        ); // Index stays the same (offset + 5 = 10)
2106        assert_arrays_eq!(
2107            sliced.values(),
2108            PrimitiveArray::from_iter([200i32]),
2109            &mut ctx
2110        );
2111        assert_eq!(sliced.offset(), 8); // New offset = 5 + 3
2112    }
2113
2114    #[test]
2115    fn test_indices_range() {
2116        let mut ctx = array_session().create_execution_ctx();
2117        let patches = Patches::new(
2118            10,
2119            0,
2120            buffer![2u64, 5, 8].into_array(),
2121            buffer![100i32, 200, 300].into_array(),
2122            None,
2123            &mut ctx,
2124        )
2125        .unwrap();
2126
2127        assert_eq!(patches.min_index(&mut ctx).unwrap(), 2);
2128        assert_eq!(patches.max_index(&mut ctx).unwrap(), 8);
2129    }
2130
2131    #[test]
2132    fn test_search_index() {
2133        let mut ctx = array_session().create_execution_ctx();
2134        let patches = Patches::new(
2135            10,
2136            0,
2137            buffer![2u64, 5, 8].into_array(),
2138            buffer![100i32, 200, 300].into_array(),
2139            None,
2140            &mut ctx,
2141        )
2142        .unwrap();
2143
2144        // Search for exact indices
2145        assert_eq!(
2146            patches.search_index(2, &mut ctx).unwrap(),
2147            SearchResult::Found(0)
2148        );
2149        assert_eq!(
2150            patches.search_index(5, &mut ctx).unwrap(),
2151            SearchResult::Found(1)
2152        );
2153        assert_eq!(
2154            patches.search_index(8, &mut ctx).unwrap(),
2155            SearchResult::Found(2)
2156        );
2157
2158        // Search for non-patch indices
2159        assert_eq!(
2160            patches.search_index(0, &mut ctx).unwrap(),
2161            SearchResult::NotFound(0)
2162        );
2163        assert_eq!(
2164            patches.search_index(3, &mut ctx).unwrap(),
2165            SearchResult::NotFound(1)
2166        );
2167        assert_eq!(
2168            patches.search_index(6, &mut ctx).unwrap(),
2169            SearchResult::NotFound(2)
2170        );
2171        assert_eq!(
2172            patches.search_index(9, &mut ctx).unwrap(),
2173            SearchResult::NotFound(3)
2174        );
2175    }
2176
2177    #[test]
2178    fn test_mask_boundary_patches() {
2179        let mut ctx = array_session().create_execution_ctx();
2180        // Test masking patches at array boundaries
2181        let patches = Patches::new(
2182            10,
2183            0,
2184            buffer![0u64, 9].into_array(),
2185            buffer![100i32, 200].into_array(),
2186            None,
2187            &mut ctx,
2188        )
2189        .unwrap();
2190
2191        let mask = Mask::from_iter([
2192            true, false, false, false, false, false, false, false, false, false,
2193        ]);
2194        let masked = patches.mask(&mask, &mut ctx).unwrap();
2195        assert!(masked.is_some());
2196        let masked = masked.unwrap();
2197        assert_arrays_eq!(
2198            masked.indices(),
2199            PrimitiveArray::from_iter([9u64]),
2200            &mut ctx
2201        );
2202        assert_arrays_eq!(
2203            masked.values(),
2204            PrimitiveArray::from_option_iter([Some(200i32)]),
2205            &mut ctx
2206        );
2207    }
2208
2209    #[test]
2210    fn test_mask_all_patches_removed() {
2211        let mut ctx = array_session().create_execution_ctx();
2212        // Test when all patches are masked out
2213        let patches = Patches::new(
2214            10,
2215            0,
2216            buffer![2u64, 5, 8].into_array(),
2217            buffer![100i32, 200, 300].into_array(),
2218            None,
2219            &mut ctx,
2220        )
2221        .unwrap();
2222
2223        // Mask that removes all patches
2224        let mask = Mask::from_iter([
2225            false, false, true, false, false, true, false, false, true, false,
2226        ]);
2227        let masked = patches.mask(&mask, &mut ctx).unwrap();
2228        assert!(masked.is_none());
2229    }
2230
2231    #[test]
2232    fn test_mask_no_patches_removed() {
2233        let mut ctx = array_session().create_execution_ctx();
2234        // Test when no patches are masked
2235        let patches = Patches::new(
2236            10,
2237            0,
2238            buffer![2u64, 5, 8].into_array(),
2239            buffer![100i32, 200, 300].into_array(),
2240            None,
2241            &mut ctx,
2242        )
2243        .unwrap();
2244
2245        // Mask that doesn't affect any patches
2246        let mask = Mask::from_iter([
2247            true, false, false, true, false, false, true, false, false, true,
2248        ]);
2249        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
2250
2251        assert_arrays_eq!(
2252            masked.indices(),
2253            PrimitiveArray::from_iter([2u64, 5, 8]),
2254            &mut ctx
2255        );
2256        assert_arrays_eq!(
2257            masked.values(),
2258            PrimitiveArray::from_option_iter([Some(100i32), Some(200), Some(300)]),
2259            &mut ctx
2260        );
2261    }
2262
2263    #[test]
2264    fn test_mask_single_patch() {
2265        let mut ctx = array_session().create_execution_ctx();
2266        // Test with a single patch
2267        let patches = Patches::new(
2268            5,
2269            0,
2270            buffer![2u64].into_array(),
2271            buffer![42i32].into_array(),
2272            None,
2273            &mut ctx,
2274        )
2275        .unwrap();
2276
2277        // Mask that removes the single patch
2278        let mask = Mask::from_iter([false, false, true, false, false]);
2279        let masked = patches.mask(&mask, &mut ctx).unwrap();
2280        assert!(masked.is_none());
2281
2282        // Mask that keeps the single patch
2283        let mask = Mask::from_iter([true, false, false, true, false]);
2284        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
2285        assert_arrays_eq!(
2286            masked.indices(),
2287            PrimitiveArray::from_iter([2u64]),
2288            &mut ctx
2289        );
2290        assert_arrays_eq!(
2291            masked.values(),
2292            PrimitiveArray::from_option_iter([Some(42i32)]),
2293            &mut ctx
2294        );
2295    }
2296
2297    #[test]
2298    fn test_mask_contiguous_patches() {
2299        let mut ctx = array_session().create_execution_ctx();
2300        // Test with contiguous patches
2301        let patches = Patches::new(
2302            10,
2303            0,
2304            buffer![3u64, 4, 5, 6].into_array(),
2305            buffer![100i32, 200, 300, 400].into_array(),
2306            None,
2307            &mut ctx,
2308        )
2309        .unwrap();
2310
2311        // Mask that removes middle patches
2312        let mask = Mask::from_iter([
2313            false, false, false, false, true, true, false, false, false, false,
2314        ]);
2315        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
2316
2317        assert_arrays_eq!(
2318            masked.indices(),
2319            PrimitiveArray::from_iter([3u64, 6]),
2320            &mut ctx
2321        );
2322        assert_arrays_eq!(
2323            masked.values(),
2324            PrimitiveArray::from_option_iter([Some(100i32), Some(400)]),
2325            &mut ctx
2326        );
2327    }
2328
2329    #[test]
2330    fn test_mask_with_large_offset() {
2331        let mut ctx = array_session().create_execution_ctx();
2332        // Test with a large offset that shifts all indices
2333        let patches = Patches::new(
2334            20,
2335            15,
2336            buffer![16u64, 17, 19].into_array(), // actual indices are 1, 2, 4
2337            buffer![100i32, 200, 300].into_array(),
2338            None,
2339            &mut ctx,
2340        )
2341        .unwrap();
2342
2343        // Mask that removes the patch at actual index 2
2344        let mask = Mask::from_iter([
2345            false, false, true, false, false, false, false, false, false, false, false, false,
2346            false, false, false, false, false, false, false, false,
2347        ]);
2348        let masked = patches.mask(&mask, &mut ctx).unwrap().unwrap();
2349
2350        assert_arrays_eq!(
2351            masked.indices(),
2352            PrimitiveArray::from_iter([16u64, 19]),
2353            &mut ctx
2354        );
2355        assert_arrays_eq!(
2356            masked.values(),
2357            PrimitiveArray::from_option_iter([Some(100i32), Some(300)]),
2358            &mut ctx
2359        );
2360    }
2361
2362    #[test]
2363    #[should_panic(expected = "Filter mask length 5 does not match array length 10")]
2364    fn test_mask_wrong_length() {
2365        let mut ctx = array_session().create_execution_ctx();
2366        let patches = Patches::new(
2367            10,
2368            0,
2369            buffer![2u64, 5, 8].into_array(),
2370            buffer![100i32, 200, 300].into_array(),
2371            None,
2372            &mut ctx,
2373        )
2374        .unwrap();
2375
2376        // Mask with wrong length
2377        let mask = Mask::from_iter([false, false, true, false, false]);
2378        patches.mask(&mask, &mut ctx).unwrap();
2379    }
2380
2381    #[test]
2382    fn test_chunk_offsets_search() {
2383        let mut ctx = array_session().create_execution_ctx();
2384        let indices = buffer![100u64, 200, 3000, 3100].into_array();
2385        let values = buffer![10i32, 20, 30, 40].into_array();
2386        let chunk_offsets = buffer![0u64, 2, 2, 3].into_array();
2387        let patches =
2388            Patches::new(4096, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2389
2390        assert!(patches.chunk_offsets.is_some());
2391
2392        // chunk 0: patches at 100, 200
2393        assert_eq!(
2394            patches.search_index(100, &mut ctx).unwrap(),
2395            SearchResult::Found(0)
2396        );
2397        assert_eq!(
2398            patches.search_index(200, &mut ctx).unwrap(),
2399            SearchResult::Found(1)
2400        );
2401
2402        // Chunk 1 has no patches; 2000 is before the first patch in chunk 2.
2403        assert_eq!(
2404            patches.search_index(1500, &mut ctx).unwrap(),
2405            SearchResult::NotFound(2)
2406        );
2407        assert_eq!(
2408            patches.search_index(2000, &mut ctx).unwrap(),
2409            SearchResult::NotFound(2)
2410        );
2411
2412        // The patches at 3000 and 3100 straddle chunks 2 and 3.
2413        assert_eq!(
2414            patches.search_index(3000, &mut ctx).unwrap(),
2415            SearchResult::Found(2)
2416        );
2417        assert_eq!(
2418            patches.search_index(3100, &mut ctx).unwrap(),
2419            SearchResult::Found(3)
2420        );
2421
2422        assert_eq!(
2423            patches.search_index(1024, &mut ctx).unwrap(),
2424            SearchResult::NotFound(2)
2425        );
2426    }
2427
2428    #[test]
2429    fn test_chunk_offsets_with_slice() {
2430        let mut ctx = array_session().create_execution_ctx();
2431        let indices = buffer![100u64, 500, 1200, 1300, 1500, 1800, 2100, 2500].into_array();
2432        let values = buffer![10i32, 20, 30, 35, 40, 45, 50, 60].into_array();
2433        let chunk_offsets = buffer![0u64, 2, 6].into_array();
2434        let patches =
2435            Patches::new(3000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2436
2437        let sliced = patches.slice(1000..2200, &mut ctx).unwrap().unwrap();
2438
2439        assert!(sliced.chunk_offsets.is_some());
2440        assert_eq!(sliced.offset(), 1000);
2441
2442        assert_eq!(
2443            sliced.search_index(200, &mut ctx).unwrap(),
2444            SearchResult::Found(0)
2445        );
2446        assert_eq!(
2447            sliced.search_index(500, &mut ctx).unwrap(),
2448            SearchResult::Found(2)
2449        );
2450        assert_eq!(
2451            sliced.search_index(1100, &mut ctx).unwrap(),
2452            SearchResult::Found(4)
2453        );
2454
2455        assert_eq!(
2456            sliced.search_index(250, &mut ctx).unwrap(),
2457            SearchResult::NotFound(1)
2458        );
2459        assert_arrays_eq!(
2460            sliced.values(),
2461            PrimitiveArray::from_iter([30i32, 35, 40, 45, 50]),
2462            &mut ctx
2463        );
2464    }
2465
2466    #[test]
2467    fn test_chunk_offsets_with_slice_after_first_chunk() {
2468        let mut ctx = array_session().create_execution_ctx();
2469        let indices = buffer![100u64, 500, 1200, 1300, 1500, 1800, 2100, 2500].into_array();
2470        let values = buffer![10i32, 20, 30, 35, 40, 45, 50, 60].into_array();
2471        let chunk_offsets = buffer![0u64, 2, 6].into_array();
2472        let patches =
2473            Patches::new(3000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2474
2475        let sliced = patches.slice(1300..2200, &mut ctx).unwrap().unwrap();
2476
2477        assert!(sliced.chunk_offsets.is_some());
2478        assert_eq!(sliced.offset(), 1300);
2479
2480        assert_eq!(
2481            sliced.search_index(0, &mut ctx).unwrap(),
2482            SearchResult::Found(0)
2483        );
2484        assert_eq!(
2485            sliced.search_index(200, &mut ctx).unwrap(),
2486            SearchResult::Found(1)
2487        );
2488        assert_eq!(
2489            sliced.search_index(500, &mut ctx).unwrap(),
2490            SearchResult::Found(2)
2491        );
2492        assert_eq!(
2493            sliced.search_index(250, &mut ctx).unwrap(),
2494            SearchResult::NotFound(2)
2495        );
2496        assert_eq!(
2497            sliced.search_index(900, &mut ctx).unwrap(),
2498            SearchResult::NotFound(4)
2499        );
2500        assert_arrays_eq!(
2501            sliced.values(),
2502            PrimitiveArray::from_iter([35i32, 40, 45, 50]),
2503            &mut ctx
2504        );
2505    }
2506
2507    #[test]
2508    fn test_chunk_offsets_slice_empty_result() {
2509        let mut ctx = array_session().create_execution_ctx();
2510        let indices = buffer![100u64, 200, 3000, 3100].into_array();
2511        let values = buffer![10i32, 20, 30, 40].into_array();
2512        let chunk_offsets = buffer![0u64, 2, 2, 3].into_array();
2513        let patches =
2514            Patches::new(4000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2515
2516        let sliced = patches.slice(1000..2000, &mut ctx).unwrap();
2517        assert!(sliced.is_none());
2518    }
2519
2520    #[test]
2521    fn test_chunk_offsets_slice_single_patch() {
2522        let mut ctx = array_session().create_execution_ctx();
2523        let indices = buffer![100u64, 1200, 1300, 2500].into_array();
2524        let values = buffer![10i32, 20, 30, 40].into_array();
2525        let chunk_offsets = buffer![0u64, 1, 3].into_array();
2526        let patches =
2527            Patches::new(3000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2528
2529        let sliced = patches.slice(1100..1250, &mut ctx).unwrap().unwrap();
2530
2531        assert_eq!(sliced.num_patches(), 1);
2532        assert_eq!(sliced.offset(), 1100);
2533        assert_eq!(
2534            sliced.search_index(100, &mut ctx).unwrap(),
2535            SearchResult::Found(0)
2536        ); // 1200 - 1100 = 100
2537        assert_eq!(
2538            sliced.search_index(50, &mut ctx).unwrap(),
2539            SearchResult::NotFound(0)
2540        );
2541        assert_eq!(
2542            sliced.search_index(150, &mut ctx).unwrap(),
2543            SearchResult::NotFound(1)
2544        );
2545        assert_arrays_eq!(
2546            sliced.values(),
2547            PrimitiveArray::from_iter([20i32]),
2548            &mut ctx
2549        );
2550    }
2551
2552    #[test]
2553    fn test_chunk_offsets_slice_across_chunks() {
2554        let mut ctx = array_session().create_execution_ctx();
2555        let indices = buffer![100u64, 200, 1100, 1200, 2100, 2200].into_array();
2556        let values = buffer![10i32, 20, 30, 40, 50, 60].into_array();
2557        let chunk_offsets = buffer![0u64, 2, 4].into_array();
2558        let patches =
2559            Patches::new(3000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2560
2561        let sliced = patches.slice(150..2150, &mut ctx).unwrap().unwrap();
2562
2563        assert_eq!(sliced.num_patches(), 4);
2564        assert_eq!(sliced.offset(), 150);
2565
2566        assert_eq!(
2567            sliced.search_index(50, &mut ctx).unwrap(),
2568            SearchResult::Found(0)
2569        ); // 200 - 150 = 50
2570        assert_eq!(
2571            sliced.search_index(950, &mut ctx).unwrap(),
2572            SearchResult::Found(1)
2573        ); // 1100 - 150 = 950
2574        assert_eq!(
2575            sliced.search_index(1050, &mut ctx).unwrap(),
2576            SearchResult::Found(2)
2577        ); // 1200 - 150 = 1050
2578        assert_eq!(
2579            sliced.search_index(1950, &mut ctx).unwrap(),
2580            SearchResult::Found(3)
2581        ); // 2100 - 150 = 1950
2582        assert_arrays_eq!(
2583            sliced.values(),
2584            PrimitiveArray::from_iter([20i32, 30, 40, 50]),
2585            &mut ctx
2586        );
2587    }
2588
2589    #[test]
2590    fn test_chunk_offsets_boundary_searches() {
2591        let mut ctx = array_session().create_execution_ctx();
2592        let indices = buffer![1023u64, 1024, 1025, 2047, 2048].into_array();
2593        let values = buffer![10i32, 20, 30, 40, 50].into_array();
2594        let chunk_offsets = buffer![0u64, 1, 4].into_array();
2595        let patches =
2596            Patches::new(3000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2597
2598        assert_eq!(
2599            patches.search_index(1023, &mut ctx).unwrap(),
2600            SearchResult::Found(0)
2601        );
2602        assert_eq!(
2603            patches.search_index(1024, &mut ctx).unwrap(),
2604            SearchResult::Found(1)
2605        );
2606        assert_eq!(
2607            patches.search_index(1025, &mut ctx).unwrap(),
2608            SearchResult::Found(2)
2609        );
2610        assert_eq!(
2611            patches.search_index(2047, &mut ctx).unwrap(),
2612            SearchResult::Found(3)
2613        );
2614        assert_eq!(
2615            patches.search_index(2048, &mut ctx).unwrap(),
2616            SearchResult::Found(4)
2617        );
2618
2619        assert_eq!(
2620            patches.search_index(1022, &mut ctx).unwrap(),
2621            SearchResult::NotFound(0)
2622        );
2623        assert_eq!(
2624            patches.search_index(2046, &mut ctx).unwrap(),
2625            SearchResult::NotFound(3)
2626        );
2627    }
2628
2629    #[test]
2630    fn test_chunk_offsets_slice_edge_cases() {
2631        let mut ctx = array_session().create_execution_ctx();
2632        let indices = buffer![0u64, 1, 1023, 1024, 2047, 2048].into_array();
2633        let values = buffer![10i32, 20, 30, 40, 50, 60].into_array();
2634        let chunk_offsets = buffer![0u64, 3, 5].into_array();
2635        let patches =
2636            Patches::new(3000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2637
2638        // Slice at the very beginning
2639        let sliced = patches.slice(0..10, &mut ctx).unwrap().unwrap();
2640        assert_eq!(sliced.num_patches(), 2);
2641        assert_eq!(
2642            sliced.search_index(0, &mut ctx).unwrap(),
2643            SearchResult::Found(0)
2644        );
2645        assert_eq!(
2646            sliced.search_index(1, &mut ctx).unwrap(),
2647            SearchResult::Found(1)
2648        );
2649
2650        assert_arrays_eq!(
2651            sliced.values(),
2652            PrimitiveArray::from_iter([10i32, 20]),
2653            &mut ctx
2654        );
2655
2656        // Slice at the very end
2657        let sliced = patches.slice(2040..3000, &mut ctx).unwrap().unwrap();
2658        assert_eq!(sliced.num_patches(), 2); // patches at 2047 and 2048
2659        assert_eq!(
2660            sliced.search_index(7, &mut ctx).unwrap(),
2661            SearchResult::Found(0)
2662        ); // 2047 - 2040
2663        assert_eq!(
2664            sliced.search_index(8, &mut ctx).unwrap(),
2665            SearchResult::Found(1)
2666        ); // 2048 - 2040
2667        assert_arrays_eq!(
2668            sliced.values(),
2669            PrimitiveArray::from_iter([50i32, 60]),
2670            &mut ctx
2671        );
2672    }
2673
2674    #[test]
2675    fn test_chunk_offsets_slice_nested() {
2676        let mut ctx = array_session().create_execution_ctx();
2677        let indices = buffer![100u64, 200, 300, 400, 500, 600].into_array();
2678        let values = buffer![10i32, 20, 30, 40, 50, 60].into_array();
2679        let chunk_offsets = buffer![0u64].into_array();
2680        let patches =
2681            Patches::new(1000, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2682
2683        let sliced1 = patches.slice(150..550, &mut ctx).unwrap().unwrap();
2684        assert_eq!(sliced1.num_patches(), 4); // 200, 300, 400, 500
2685
2686        let sliced2 = sliced1.slice(100..250, &mut ctx).unwrap().unwrap();
2687        assert_eq!(sliced2.num_patches(), 1); // 300
2688        assert_eq!(sliced2.offset(), 250);
2689
2690        assert_eq!(
2691            sliced2.search_index(50, &mut ctx).unwrap(),
2692            SearchResult::Found(0)
2693        ); // 300 - 250
2694        assert_eq!(
2695            sliced2.search_index(150, &mut ctx).unwrap(),
2696            SearchResult::NotFound(1)
2697        );
2698        assert_arrays_eq!(
2699            sliced1.values(),
2700            PrimitiveArray::from_iter([20i32, 30, 40, 50]),
2701            &mut ctx
2702        );
2703        assert_arrays_eq!(
2704            sliced2.values(),
2705            PrimitiveArray::from_iter([30i32]),
2706            &mut ctx
2707        );
2708    }
2709
2710    #[test]
2711    fn test_nested_slice_with_dropped_first_chunk() {
2712        let mut ctx = array_session().create_execution_ctx();
2713        // PATCH_CHUNK_SIZE = 1024, so the two patches land in different chunks.
2714        let indices = buffer![0u64, 1024].into_array();
2715        let values = buffer![1i32, 2].into_array();
2716        let chunk_offsets = buffer![0u64, 1].into_array();
2717        let patches =
2718            Patches::new(2048, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2719
2720        // Drop chunk 0, then re-slice the result.
2721        let dropped_first = patches.slice(1024..2048, &mut ctx).unwrap().unwrap();
2722        let resliced = dropped_first.slice(0..1024, &mut ctx).unwrap().unwrap();
2723        assert_eq!(resliced.num_patches(), 1);
2724        assert_eq!(resliced.array_len(), 1024);
2725        assert_eq!(resliced.offset(), 1024);
2726        assert_eq!(
2727            resliced.search_index(0, &mut ctx).unwrap(),
2728            SearchResult::Found(0)
2729        );
2730        assert_arrays_eq!(
2731            resliced.values(),
2732            PrimitiveArray::from_iter([2i32]),
2733            &mut ctx
2734        );
2735    }
2736
2737    #[test]
2738    fn test_index_larger_than_length() {
2739        let mut ctx = array_session().create_execution_ctx();
2740        let chunk_offsets = buffer![0u64].into_array();
2741        let indices = buffer![1023u64].into_array();
2742        let values = buffer![42i32].into_array();
2743        let patches =
2744            Patches::new(1024, 0, indices, values, Some(chunk_offsets), &mut ctx).unwrap();
2745        assert_eq!(
2746            patches.search_index(2048, &mut ctx).unwrap(),
2747            SearchResult::NotFound(1)
2748        );
2749    }
2750}