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