Skip to main content

vortex_array/
patches.rs

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