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