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