• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 17041372319

18 Aug 2025 01:01PM UTC coverage: 88.047% (+0.1%) from 87.913%
17041372319

Pull #4177

github

web-flow
Merge 74cbbea45 into abfc8534c
Pull Request #4177: feat: ArrayOperations infallible

1187 of 1273 new or added lines in 146 files covered. (93.24%)

18 existing lines in 12 files now uncovered.

56492 of 64161 relevant lines covered (88.05%)

622803.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.34
/vortex-array/src/patches.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
use std::cmp::Ordering;
5
use std::fmt::Debug;
6
use std::hash::Hash;
7

8
use arrow_buffer::BooleanBuffer;
9
use itertools::Itertools as _;
10
use num_traits::{NumCast, ToPrimitive};
11
use serde::{Deserialize, Serialize};
12
use vortex_buffer::BufferMut;
13
use vortex_dtype::Nullability::NonNullable;
14
use vortex_dtype::{
15
    DType, NativePType, PType, match_each_integer_ptype, match_each_unsigned_integer_ptype,
16
};
17
use vortex_error::{
18
    VortexError, VortexExpect, VortexResult, VortexUnwrap, vortex_bail, vortex_err,
19
};
20
use vortex_mask::{AllOr, Mask};
21
use vortex_scalar::{PValue, Scalar};
22
use vortex_utils::aliases::hash_map::HashMap;
23

24
use crate::arrays::PrimitiveArray;
25
use crate::compute::{cast, filter, take};
26
use crate::search_sorted::{SearchResult, SearchSorted, SearchSortedSide};
27
use crate::vtable::ValidityHelper;
28
use crate::{Array, ArrayRef, IntoArray, ToCanonical};
29

30
#[derive(Copy, Clone, Serialize, Deserialize, prost::Message)]
31
pub struct PatchesMetadata {
32
    #[prost(uint64, tag = "1")]
33
    len: u64,
34
    #[prost(uint64, tag = "2")]
35
    offset: u64,
36
    #[prost(enumeration = "PType", tag = "3")]
37
    indices_ptype: i32,
38
}
39

40
impl PatchesMetadata {
41
    pub fn new(len: usize, offset: usize, indices_ptype: PType) -> Self {
78✔
42
        Self {
78✔
43
            len: len as u64,
78✔
44
            offset: offset as u64,
78✔
45
            indices_ptype: indices_ptype as i32,
78✔
46
        }
78✔
47
    }
78✔
48

49
    #[inline]
50
    pub fn len(&self) -> usize {
1,954✔
51
        usize::try_from(self.len).vortex_expect("len is a valid usize")
1,954✔
52
    }
1,954✔
53

54
    #[inline]
55
    pub fn is_empty(&self) -> bool {
×
56
        self.len == 0
×
57
    }
×
58

59
    #[inline]
60
    pub fn offset(&self) -> usize {
977✔
61
        usize::try_from(self.offset).vortex_expect("offset is a valid usize")
977✔
62
    }
977✔
63

64
    #[inline]
65
    pub fn indices_dtype(&self) -> DType {
977✔
66
        assert!(
977✔
67
            self.indices_ptype().is_unsigned_int(),
977✔
68
            "Patch indices must be unsigned integers"
×
69
        );
70
        DType::Primitive(self.indices_ptype(), NonNullable)
977✔
71
    }
977✔
72
}
73

74
/// A helper for working with patched arrays.
75
#[derive(Debug, Clone)]
76
pub struct Patches {
77
    array_len: usize,
78
    offset: usize,
79
    indices: ArrayRef,
80
    values: ArrayRef,
81
}
82

83
impl Patches {
84
    pub fn new(array_len: usize, offset: usize, indices: ArrayRef, values: ArrayRef) -> Self {
42,572✔
85
        assert_eq!(
42,572✔
86
            indices.len(),
42,572✔
87
            values.len(),
42,572✔
88
            "Patch indices and values must have the same length"
×
89
        );
90
        assert!(
42,571✔
91
            indices.dtype().is_unsigned_int() && !indices.dtype().is_nullable(),
42,572✔
92
            "Patch indices must be non-nullable unsigned integers, got {:?}",
1✔
93
            indices.dtype()
1✔
94
        );
95
        assert!(
42,571✔
96
            indices.len() <= array_len,
42,571✔
97
            "Patch indices must be shorter than the array length"
×
98
        );
99
        assert!(!indices.is_empty(), "Patch indices must not be empty");
42,571✔
100
        let max = usize::try_from(&indices.scalar_at(indices.len() - 1))
42,571✔
101
            .vortex_expect("indices must be a number");
42,571✔
102
        assert!(
42,571✔
103
            max - offset < array_len,
42,571✔
104
            "Patch indices {max:?}, offset {offset} are longer than the array length {array_len}"
×
105
        );
106
        Self::new_unchecked(array_len, offset, indices, values)
42,571✔
107
    }
42,571✔
108

109
    /// Construct new patches without validating any of the arguments
110
    ///
111
    /// # Safety
112
    ///
113
    /// Users have to assert that
114
    /// * Indices and values have the same length
115
    /// * Indices is an unsigned integer type
116
    /// * Indices must be sorted
117
    /// * Last value in indices is smaller than array_len
118
    pub fn new_unchecked(
50,418✔
119
        array_len: usize,
50,418✔
120
        offset: usize,
50,418✔
121
        indices: ArrayRef,
50,418✔
122
        values: ArrayRef,
50,418✔
123
    ) -> Self {
50,418✔
124
        Self {
50,418✔
125
            array_len,
50,418✔
126
            offset,
50,418✔
127
            indices,
50,418✔
128
            values,
50,418✔
129
        }
50,418✔
130
    }
50,418✔
131

132
    pub fn array_len(&self) -> usize {
2,185,488✔
133
        self.array_len
2,185,488✔
134
    }
2,185,488✔
135

136
    pub fn num_patches(&self) -> usize {
14,548✔
137
        self.indices.len()
14,548✔
138
    }
14,548✔
139

140
    pub fn dtype(&self) -> &DType {
6,630✔
141
        self.values.dtype()
6,630✔
142
    }
6,630✔
143

144
    pub fn indices(&self) -> &ArrayRef {
45,209✔
145
        &self.indices
45,209✔
146
    }
45,209✔
147

148
    pub fn into_indices(self) -> ArrayRef {
×
149
        self.indices
×
150
    }
×
151

152
    pub fn indices_mut(&mut self) -> &mut ArrayRef {
×
153
        &mut self.indices
×
154
    }
×
155

156
    pub fn values(&self) -> &ArrayRef {
274,161✔
157
        &self.values
274,161✔
158
    }
274,161✔
159

160
    pub fn into_values(self) -> ArrayRef {
702✔
161
        self.values
702✔
162
    }
702✔
163

164
    pub fn values_mut(&mut self) -> &mut ArrayRef {
×
165
        &mut self.values
×
166
    }
×
167

168
    pub fn offset(&self) -> usize {
131,603✔
169
        self.offset
131,603✔
170
    }
131,603✔
171

172
    pub fn indices_ptype(&self) -> PType {
×
173
        PType::try_from(self.indices.dtype()).vortex_expect("primitive indices")
×
174
    }
×
175

176
    pub fn to_metadata(&self, len: usize, dtype: &DType) -> VortexResult<PatchesMetadata> {
274✔
177
        if self.indices.len() > len {
274✔
178
            vortex_bail!(
×
179
                "Patch indices {} are longer than the array length {}",
×
180
                self.indices.len(),
×
181
                len
182
            );
183
        }
274✔
184
        if self.values.dtype() != dtype {
274✔
185
            vortex_bail!(
×
186
                "Patch values dtype {} does not match array dtype {}",
×
187
                self.values.dtype(),
×
188
                dtype
189
            );
190
        }
274✔
191
        Ok(PatchesMetadata {
274✔
192
            len: self.indices.len() as u64,
274✔
193
            offset: self.offset as u64,
274✔
194
            indices_ptype: PType::try_from(self.indices.dtype()).vortex_expect("primitive indices")
274✔
195
                as i32,
274✔
196
        })
274✔
197
    }
274✔
198

199
    pub fn cast_values(self, values_dtype: &DType) -> VortexResult<Self> {
7,683✔
200
        Ok(Self::new_unchecked(
7,683✔
201
            self.array_len,
7,683✔
202
            self.offset,
7,683✔
203
            self.indices,
7,683✔
204
            cast(&self.values, values_dtype)?,
7,683✔
205
        ))
206
    }
7,683✔
207

208
    /// Get the patched value at a given index if it exists.
209
    pub fn get_patched(&self, index: usize) -> Option<Scalar> {
2,780,658✔
210
        self.search_index(index)
2,780,658✔
211
            .to_found()
2,780,658✔
212
            .map(|patch_idx| self.values().scalar_at(patch_idx))
2,780,658✔
213
    }
2,780,658✔
214

215
    /// Return the insertion point of `index` in the [Self::indices].
216
    pub fn search_index(&self, index: usize) -> SearchResult {
2,790,191✔
217
        self.indices.as_primitive_typed().search_sorted(
2,790,191✔
218
            &PValue::U64((index + self.offset) as u64),
2,790,191✔
219
            SearchSortedSide::Left,
2,790,191✔
220
        )
221
    }
2,790,191✔
222

223
    /// Return the search_sorted result for the given target re-mapped into the original indices.
224
    pub fn search_sorted<T: Into<Scalar>>(
11✔
225
        &self,
11✔
226
        target: T,
11✔
227
        side: SearchSortedSide,
11✔
228
    ) -> VortexResult<SearchResult> {
11✔
229
        let target = target.into();
11✔
230

231
        let sr = if self.values().dtype().is_primitive() {
11✔
232
            self.values()
11✔
233
                .as_primitive_typed()
11✔
234
                .search_sorted(&target.as_primitive().pvalue(), side)
11✔
235
        } else {
236
            self.values().search_sorted(&target, side)
×
237
        };
238

239
        let index_idx = sr.to_offsets_index(self.indices().len(), side);
11✔
240
        let index = usize::try_from(&self.indices().scalar_at(index_idx))? - self.offset;
11✔
241
        Ok(match sr {
11✔
242
            // If we reached the end of patched values when searching then the result is one after the last patch index
243
            SearchResult::Found(i) => SearchResult::Found(
3✔
244
                if i == self.indices().len() || side == SearchSortedSide::Right {
3✔
245
                    index + 1
2✔
246
                } else {
247
                    index
1✔
248
                },
249
            ),
250
            // If the result is NotFound we should return index that's one after the nearest not found index for the corresponding value
251
            SearchResult::NotFound(i) => {
8✔
252
                SearchResult::NotFound(if i == 0 { index } else { index + 1 })
8✔
253
            }
254
        })
255
    }
11✔
256

257
    /// Returns the minimum patch index
258
    pub fn min_index(&self) -> usize {
5,774✔
259
        let first = self
5,774✔
260
            .indices
5,774✔
261
            .scalar_at(0)
5,774✔
262
            .as_primitive()
5,774✔
263
            .as_::<usize>()
5,774✔
264
            .vortex_expect("non-null");
5,774✔
265
        first - self.offset
5,774✔
266
    }
5,774✔
267

268
    /// Returns the maximum patch index
269
    pub fn max_index(&self) -> usize {
5,774✔
270
        let last = self
5,774✔
271
            .indices
5,774✔
272
            .scalar_at(self.indices.len() - 1)
5,774✔
273
            .as_primitive()
5,774✔
274
            .as_::<usize>()
5,774✔
275
            .vortex_expect("non-null");
5,774✔
276
        last - self.offset
5,774✔
277
    }
5,774✔
278

279
    /// Filter the patches by a mask, resulting in new patches for the filtered array.
280
    pub fn filter(&self, mask: &Mask) -> VortexResult<Option<Self>> {
4,594✔
281
        if mask.len() != self.array_len {
4,594✔
282
            vortex_bail!(
×
283
                "Filter mask length {} does not match array length {}",
×
284
                mask.len(),
×
285
                self.array_len
286
            );
287
        }
4,594✔
288

289
        match mask.indices() {
4,594✔
290
            AllOr::All => Ok(Some(self.clone())),
1✔
291
            AllOr::None => Ok(None),
1✔
292
            AllOr::Some(mask_indices) => {
4,592✔
293
                let flat_indices = self.indices().to_primitive()?;
4,592✔
294
                match_each_unsigned_integer_ptype!(flat_indices.ptype(), |I| {
4,592✔
295
                    filter_patches_with_mask(
×
296
                        flat_indices.as_slice::<I>(),
×
297
                        self.offset(),
×
298
                        self.values(),
×
299
                        mask_indices,
×
300
                    )
301
                })
302
            }
303
        }
304
    }
4,594✔
305

306
    /// Mask the patches, REMOVING the patches where the mask is true.
307
    /// Unlike filter, this preserves the patch indices.
308
    /// Unlike mask on a single array, this does not set masked values to null.
309
    pub fn mask(&self, mask: &Mask) -> VortexResult<Option<Self>> {
169✔
310
        if mask.len() != self.array_len {
169✔
311
            vortex_bail!(
1✔
312
                "Filter mask length {} does not match array length {}",
1✔
313
                mask.len(),
1✔
314
                self.array_len
315
            );
316
        }
168✔
317

318
        let filter_mask = match mask.boolean_buffer() {
168✔
319
            AllOr::All => return Ok(None),
1✔
320
            AllOr::None => return Ok(Some(self.clone())),
1✔
321
            AllOr::Some(masked) => {
166✔
322
                let patch_indices = self.indices().to_primitive()?;
166✔
323
                match_each_unsigned_integer_ptype!(patch_indices.ptype(), |P| {
166✔
324
                    let patch_indices = patch_indices.as_slice::<P>();
×
325
                    Mask::from_buffer(BooleanBuffer::collect_bool(patch_indices.len(), |i| {
884✔
326
                        #[allow(clippy::cast_possible_truncation)]
327
                        let idx = (patch_indices[i] as usize) - self.offset;
884✔
328
                        !masked.value(idx)
884✔
329
                    }))
884✔
330
                })
331
            }
332
        };
333

334
        if filter_mask.all_false() {
166✔
335
            return Ok(None);
2✔
336
        }
164✔
337

338
        Ok(Some(Self::new_unchecked(
164✔
339
            self.array_len,
164✔
340
            self.offset,
164✔
341
            filter(&self.indices, &filter_mask)?,
164✔
342
            filter(&self.values, &filter_mask)?,
164✔
343
        )))
344
    }
169✔
345

346
    /// Slice the patches by a range of the patched array.
347
    pub fn slice(&self, start: usize, stop: usize) -> Option<Self> {
4,763✔
348
        let patch_start = self.search_index(start).to_index();
4,763✔
349
        let patch_stop = self.search_index(stop).to_index();
4,763✔
350

351
        if patch_start == patch_stop {
4,763✔
352
            return None;
1,291✔
353
        }
3,472✔
354

355
        // Slice out the values and indices
356
        let values = self.values().slice(patch_start, patch_stop);
3,472✔
357
        let indices = self.indices().slice(patch_start, patch_stop);
3,472✔
358

359
        Some(Self::new(
3,472✔
360
            stop - start,
3,472✔
361
            start + self.offset(),
3,472✔
362
            indices,
3,472✔
363
            values,
3,472✔
364
        ))
3,472✔
365
    }
4,763✔
366

367
    // https://docs.google.com/spreadsheets/d/1D9vBZ1QJ6mwcIvV5wIL0hjGgVchcEnAyhvitqWu2ugU
368
    const PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN: f64 = 5.0;
369

370
    fn is_map_faster_than_search(&self, take_indices: &PrimitiveArray) -> bool {
6,124✔
371
        (self.num_patches() as f64 / take_indices.len() as f64)
6,124✔
372
            < Self::PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN
6,124✔
373
    }
6,124✔
374

375
    /// Take the indices from the patches
376
    ///
377
    /// Any nulls in take_indices are added to the resulting patches.
378
    pub fn take_with_nulls(&self, take_indices: &dyn Array) -> VortexResult<Option<Self>> {
1,872✔
379
        if take_indices.is_empty() {
1,872✔
380
            return Ok(None);
×
381
        }
1,872✔
382

383
        let take_indices = take_indices.to_primitive()?;
1,872✔
384
        if self.is_map_faster_than_search(&take_indices) {
1,872✔
385
            self.take_map(take_indices, true)
1,833✔
386
        } else {
387
            self.take_search(take_indices, true)
39✔
388
        }
389
    }
1,872✔
390

391
    /// Take the indices from the patches.
392
    ///
393
    /// Any nulls in take_indices are ignored.
394
    pub fn take(&self, take_indices: &dyn Array) -> VortexResult<Option<Self>> {
4,252✔
395
        if take_indices.is_empty() {
4,252✔
396
            return Ok(None);
×
397
        }
4,252✔
398

399
        let take_indices = take_indices.to_primitive()?;
4,252✔
400
        if self.is_map_faster_than_search(&take_indices) {
4,252✔
401
            self.take_map(take_indices, false)
3,940✔
402
        } else {
403
            self.take_search(take_indices, false)
312✔
404
        }
405
    }
4,252✔
406

407
    pub fn take_search(
351✔
408
        &self,
351✔
409
        take_indices: PrimitiveArray,
351✔
410
        include_nulls: bool,
351✔
411
    ) -> VortexResult<Option<Self>> {
351✔
412
        let indices = self.indices.to_primitive()?;
351✔
413
        let new_length = take_indices.len();
351✔
414

415
        let Some((new_indices, values_indices)) =
273✔
416
            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
351✔
417
                match_each_integer_ptype!(take_indices.ptype(), |TakeIndices| {
×
418
                    take_search::<_, TakeIndices>(
×
419
                        indices.as_slice::<Indices>(),
×
420
                        take_indices,
×
421
                        self.offset(),
×
422
                        include_nulls,
×
423
                    )?
×
424
                })
425
            })
426
        else {
427
            return Ok(None);
78✔
428
        };
429

430
        Ok(Some(Self::new(
273✔
431
            new_length,
273✔
432
            0,
433
            new_indices,
273✔
434
            take(self.values(), &values_indices)?,
273✔
435
        )))
436
    }
351✔
437

438
    pub fn take_map(
5,773✔
439
        &self,
5,773✔
440
        take_indices: PrimitiveArray,
5,773✔
441
        include_nulls: bool,
5,773✔
442
    ) -> VortexResult<Option<Self>> {
5,773✔
443
        let indices = self.indices.to_primitive()?;
5,773✔
444
        let new_length = take_indices.len();
5,773✔
445

446
        let Some((new_sparse_indices, value_indices)) =
4,096✔
447
            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
5,773✔
448
                match_each_integer_ptype!(take_indices.ptype(), |TakeIndices| {
78✔
449
                    take_map::<_, TakeIndices>(
×
450
                        indices.as_slice::<Indices>(),
×
451
                        take_indices,
×
452
                        self.offset(),
×
NEW
453
                        self.min_index(),
×
NEW
454
                        self.max_index(),
×
455
                        include_nulls,
×
456
                    )?
×
457
                })
458
            })
459
        else {
460
            return Ok(None);
1,677✔
461
        };
462

463
        Ok(Some(Patches::new(
4,096✔
464
            new_length,
4,096✔
465
            0,
466
            new_sparse_indices,
4,096✔
467
            take(self.values(), &value_indices)?,
4,096✔
468
        )))
469
    }
5,773✔
470

471
    pub fn map_values<F>(self, f: F) -> VortexResult<Self>
370✔
472
    where
370✔
473
        F: FnOnce(ArrayRef) -> VortexResult<ArrayRef>,
370✔
474
    {
475
        let values = f(self.values)?;
370✔
476
        if self.indices.len() != values.len() {
342✔
477
            vortex_bail!(
×
478
                "map_values must preserve length: expected {} received {}",
×
479
                self.indices.len(),
×
480
                values.len()
×
481
            )
482
        }
342✔
483
        Ok(Self::new(self.array_len, self.offset, self.indices, values))
342✔
484
    }
370✔
485
}
486

487
fn take_search<I: NativePType + NumCast + PartialOrd, T: NativePType + NumCast>(
351✔
488
    indices: &[I],
351✔
489
    take_indices: PrimitiveArray,
351✔
490
    indices_offset: usize,
351✔
491
    include_nulls: bool,
351✔
492
) -> VortexResult<Option<(ArrayRef, ArrayRef)>>
351✔
493
where
351✔
494
    usize: TryFrom<T>,
351✔
495
    VortexError: From<<usize as TryFrom<T>>::Error>,
351✔
496
{
497
    let take_indices_validity = take_indices.validity();
351✔
498
    let indices_offset = I::from(indices_offset).vortex_expect("indices_offset out of range");
351✔
499

500
    let (values_indices, new_indices): (BufferMut<u64>, BufferMut<u64>) = take_indices
351✔
501
        .as_slice::<T>()
351✔
502
        .iter()
351✔
503
        .enumerate()
351✔
504
        .filter_map(|(i, &v)| {
1,365✔
505
            I::from(v)
1,365✔
506
                .and_then(|v| {
1,365✔
507
                    // If we have to take nulls the take index doesn't matter, make it 0 for consistency
508
                    if include_nulls && take_indices_validity.is_null(i).vortex_unwrap() {
1,365✔
509
                        Some(0)
×
510
                    } else {
511
                        indices
1,365✔
512
                            .search_sorted(&(v + indices_offset), SearchSortedSide::Left)
1,365✔
513
                            .to_found()
1,365✔
514
                            .map(|patch_idx| patch_idx as u64)
1,365✔
515
                    }
516
                })
1,365✔
517
                .map(|patch_idx| (patch_idx, i as u64))
1,365✔
518
        })
1,365✔
519
        .unzip();
351✔
520

521
    if new_indices.is_empty() {
351✔
522
        return Ok(None);
78✔
523
    }
273✔
524

525
    let new_indices = new_indices.into_array();
273✔
526
    let values_validity = take_indices_validity.take(&new_indices)?;
273✔
527
    Ok(Some((
273✔
528
        new_indices,
273✔
529
        PrimitiveArray::new(values_indices, values_validity).into_array(),
273✔
530
    )))
273✔
531
}
351✔
532

533
fn take_map<I: NativePType + Hash + Eq + TryFrom<usize>, T: NativePType>(
5,773✔
534
    indices: &[I],
5,773✔
535
    take_indices: PrimitiveArray,
5,773✔
536
    indices_offset: usize,
5,773✔
537
    min_index: usize,
5,773✔
538
    max_index: usize,
5,773✔
539
    include_nulls: bool,
5,773✔
540
) -> VortexResult<Option<(ArrayRef, ArrayRef)>>
5,773✔
541
where
5,773✔
542
    usize: TryFrom<T>,
5,773✔
543
    VortexError: From<<I as TryFrom<usize>>::Error>,
5,773✔
544
{
545
    let take_indices_validity = take_indices.validity();
5,773✔
546
    let take_indices = take_indices.as_slice::<T>();
5,773✔
547
    let offset_i = I::try_from(indices_offset)?;
5,773✔
548

549
    let sparse_index_to_value_index: HashMap<I, usize> = indices
5,773✔
550
        .iter()
5,773✔
551
        .copied()
5,773✔
552
        .map(|idx| idx - offset_i)
17,085✔
553
        .enumerate()
5,773✔
554
        .map(|(value_index, sparse_index)| (sparse_index, value_index))
17,085✔
555
        .collect();
5,773✔
556

557
    let (new_sparse_indices, value_indices): (BufferMut<u64>, BufferMut<u64>) = take_indices
5,773✔
558
        .iter()
5,773✔
559
        .copied()
5,773✔
560
        .map(usize::try_from)
5,773✔
561
        .process_results(|iter| {
5,773✔
562
            iter.enumerate()
5,773✔
563
                .filter_map(|(idx_in_take, ti)| {
241,412✔
564
                    // If we have to take nulls the take index doesn't matter, make it 0 for consistency
565
                    if include_nulls && take_indices_validity.is_null(idx_in_take).vortex_unwrap() {
241,412✔
566
                        Some((idx_in_take as u64, 0))
351✔
567
                    } else if ti < min_index || ti > max_index {
241,061✔
568
                        None
53,470✔
569
                    } else {
570
                        sparse_index_to_value_index
187,591✔
571
                            .get(
187,591✔
572
                                &I::try_from(ti)
187,591✔
573
                                    .vortex_expect("take index is between min and max index"),
187,591✔
574
                            )
575
                            .map(|value_index| (idx_in_take as u64, *value_index as u64))
187,591✔
576
                    }
577
                })
241,412✔
578
                .unzip()
5,773✔
579
        })
5,773✔
580
        .map_err(|_| vortex_err!("Failed to convert index to usize"))?;
5,773✔
581

582
    if new_sparse_indices.is_empty() {
5,773✔
583
        return Ok(None);
1,677✔
584
    }
4,096✔
585

586
    let new_sparse_indices = new_sparse_indices.into_array();
4,096✔
587
    let values_validity = take_indices_validity.take(&new_sparse_indices)?;
4,096✔
588
    Ok(Some((
4,096✔
589
        new_sparse_indices,
4,096✔
590
        PrimitiveArray::new(value_indices, values_validity).into_array(),
4,096✔
591
    )))
4,096✔
592
}
5,773✔
593

594
/// Filter patches with the provided mask (in flattened space).
595
///
596
/// The filter mask may contain indices that are non-patched. The return value of this function
597
/// is a new set of `Patches` with the indices relative to the provided `mask` rank, and the
598
/// patch values.
599
fn filter_patches_with_mask<T: ToPrimitive + Copy + Ord>(
4,592✔
600
    patch_indices: &[T],
4,592✔
601
    offset: usize,
4,592✔
602
    patch_values: &dyn Array,
4,592✔
603
    mask_indices: &[usize],
4,592✔
604
) -> VortexResult<Option<Patches>> {
4,592✔
605
    let true_count = mask_indices.len();
4,592✔
606
    let mut new_patch_indices = BufferMut::<u64>::with_capacity(true_count);
4,592✔
607
    let mut new_mask_indices = Vec::with_capacity(true_count);
4,592✔
608

609
    // Attempt to move the window by `STRIDE` elements on each iteration. This assumes that
610
    // the patches are relatively sparse compared to the overall mask, and so many indices in the
611
    // mask will end up being skipped.
612
    const STRIDE: usize = 4;
613

614
    let mut mask_idx = 0usize;
4,592✔
615
    let mut true_idx = 0usize;
4,592✔
616

617
    while mask_idx < patch_indices.len() && true_idx < true_count {
668,873✔
618
        // NOTE: we are searching for overlaps between sorted, unaligned indices in `patch_indices`
619
        //  and `mask_indices`. We assume that Patches are sparse relative to the global space of
620
        //  the mask (which covers both patch and non-patch values of the parent array), and so to
621
        //  quickly jump through regions with no overlap, we attempt to move our pointers by STRIDE
622
        //  elements on each iteration. If we cannot rule out overlap due to min/max values, we
623
        //  fallback to performing a two-way iterator merge.
624
        if (mask_idx + STRIDE) < patch_indices.len() && (true_idx + STRIDE) < mask_indices.len() {
664,281✔
625
            // Load a vector of each into our registers.
626
            let left_min = patch_indices[mask_idx].to_usize().vortex_expect("left_min") - offset;
520,773✔
627
            let left_max = patch_indices[mask_idx + STRIDE]
520,773✔
628
                .to_usize()
520,773✔
629
                .vortex_expect("left_max")
520,773✔
630
                - offset;
520,773✔
631
            let right_min = mask_indices[true_idx];
520,773✔
632
            let right_max = mask_indices[true_idx + STRIDE];
520,773✔
633

634
            if left_min > right_max {
520,773✔
635
                // Advance right side
636
                true_idx += STRIDE;
47,307✔
637
                continue;
47,307✔
638
            } else if right_min > left_max {
473,466✔
639
                mask_idx += STRIDE;
12,762✔
640
                continue;
12,762✔
641
            } else {
460,704✔
642
                // Fallthrough to direct comparison path.
460,704✔
643
            }
460,704✔
644
        }
143,508✔
645

646
        // Two-way sorted iterator merge:
647

648
        let left = patch_indices[mask_idx].to_usize().vortex_expect("left") - offset;
604,212✔
649
        let right = mask_indices[true_idx];
604,212✔
650

651
        match left.cmp(&right) {
604,212✔
652
            Ordering::Less => {
211,112✔
653
                mask_idx += 1;
211,112✔
654
            }
211,112✔
655
            Ordering::Greater => {
383,046✔
656
                true_idx += 1;
383,046✔
657
            }
383,046✔
658
            Ordering::Equal => {
10,054✔
659
                // Save the mask index as well as the positional index.
10,054✔
660
                new_mask_indices.push(mask_idx);
10,054✔
661
                new_patch_indices.push(true_idx as u64);
10,054✔
662

10,054✔
663
                mask_idx += 1;
10,054✔
664
                true_idx += 1;
10,054✔
665
            }
10,054✔
666
        }
667
    }
668

669
    if new_mask_indices.is_empty() {
4,592✔
670
        return Ok(None);
1,200✔
671
    }
3,392✔
672

673
    let new_patch_indices = new_patch_indices.into_array();
3,392✔
674
    let new_patch_values = filter(
3,392✔
675
        patch_values,
3,392✔
676
        &Mask::from_indices(patch_values.len(), new_mask_indices),
3,392✔
677
    )?;
×
678

679
    Ok(Some(Patches::new(
3,392✔
680
        true_count,
3,392✔
681
        0,
3,392✔
682
        new_patch_indices,
3,392✔
683
        new_patch_values,
3,392✔
684
    )))
3,392✔
685
}
4,592✔
686

687
#[cfg(test)]
688
mod test {
689
    use rstest::{fixture, rstest};
690
    use vortex_buffer::buffer;
691
    use vortex_mask::Mask;
692

693
    use crate::arrays::PrimitiveArray;
694
    use crate::patches::Patches;
695
    use crate::search_sorted::{SearchResult, SearchSortedSide};
696
    use crate::validity::Validity;
697
    use crate::{IntoArray, ToCanonical};
698

699
    #[test]
700
    fn test_filter() {
1✔
701
        let patches = Patches::new(
1✔
702
            100,
703
            0,
704
            buffer![10u32, 11, 20].into_array(),
1✔
705
            buffer![100, 110, 200].into_array(),
1✔
706
        );
707

708
        let filtered = patches
1✔
709
            .filter(&Mask::from_indices(100, vec![10, 20, 30]))
1✔
710
            .unwrap()
1✔
711
            .unwrap();
1✔
712

713
        let indices = filtered.indices().to_primitive().unwrap();
1✔
714
        let values = filtered.values().to_primitive().unwrap();
1✔
715
        assert_eq!(indices.as_slice::<u64>(), &[0, 1]);
1✔
716
        assert_eq!(values.as_slice::<i32>(), &[100, 200]);
1✔
717
    }
1✔
718

719
    #[fixture]
720
    fn patches() -> Patches {
721
        Patches::new(
722
            20,
723
            0,
724
            buffer![2u64, 9, 15].into_array(),
725
            PrimitiveArray::new(buffer![33_i32, 44, 55], Validity::AllValid).into_array(),
726
        )
727
    }
728

729
    #[rstest]
730
    fn search_larger_than(patches: Patches) {
731
        let res = patches.search_sorted(66, SearchSortedSide::Left).unwrap();
732
        assert_eq!(res, SearchResult::NotFound(16));
733
    }
734

735
    #[rstest]
736
    fn search_less_than(patches: Patches) {
737
        let res = patches.search_sorted(22, SearchSortedSide::Left).unwrap();
738
        assert_eq!(res, SearchResult::NotFound(2));
739
    }
740

741
    #[rstest]
742
    fn search_found(patches: Patches) {
743
        let res = patches.search_sorted(44, SearchSortedSide::Left).unwrap();
744
        assert_eq!(res, SearchResult::Found(9));
745
    }
746

747
    #[rstest]
748
    fn search_not_found_right(patches: Patches) {
749
        let res = patches.search_sorted(56, SearchSortedSide::Right).unwrap();
750
        assert_eq!(res, SearchResult::NotFound(16));
751
    }
752

753
    #[rstest]
754
    fn search_sliced(patches: Patches) {
755
        let sliced = patches.slice(7, 20).unwrap();
756
        assert_eq!(
757
            sliced.search_sorted(22, SearchSortedSide::Left).unwrap(),
758
            SearchResult::NotFound(2)
759
        );
760
    }
761

762
    #[test]
763
    fn search_right() {
1✔
764
        let patches = Patches::new(
1✔
765
            6,
766
            0,
767
            buffer![0u8, 1, 4, 5].into_array(),
1✔
768
            buffer![-128i8, -98, 8, 50].into_array(),
1✔
769
        );
770

771
        assert_eq!(
1✔
772
            patches.search_sorted(-98, SearchSortedSide::Right).unwrap(),
1✔
773
            SearchResult::Found(2)
774
        );
775
        assert_eq!(
1✔
776
            patches.search_sorted(50, SearchSortedSide::Right).unwrap(),
1✔
777
            SearchResult::Found(6),
778
        );
779
        assert_eq!(
1✔
780
            patches.search_sorted(7, SearchSortedSide::Right).unwrap(),
1✔
781
            SearchResult::NotFound(2),
782
        );
783
        assert_eq!(
1✔
784
            patches.search_sorted(51, SearchSortedSide::Right).unwrap(),
1✔
785
            SearchResult::NotFound(6)
786
        );
787
    }
1✔
788

789
    #[test]
790
    fn search_left() {
1✔
791
        let patches = Patches::new(
1✔
792
            20,
793
            0,
794
            buffer![0u64, 1, 17, 18, 19].into_array(),
1✔
795
            buffer![11i32, 22, 33, 44, 55].into_array(),
1✔
796
        );
797
        assert_eq!(
1✔
798
            patches.search_sorted(30, SearchSortedSide::Left).unwrap(),
1✔
799
            SearchResult::NotFound(2)
800
        );
801
        assert_eq!(
1✔
802
            patches.search_sorted(54, SearchSortedSide::Left).unwrap(),
1✔
803
            SearchResult::NotFound(19)
804
        );
805
    }
1✔
806

807
    #[rstest]
808
    fn take_with_nulls(patches: Patches) {
809
        let taken = patches
810
            .take(
811
                &PrimitiveArray::new(buffer![9, 0], Validity::from_iter(vec![true, false]))
812
                    .into_array(),
813
            )
814
            .unwrap()
815
            .unwrap();
816
        let primitive_values = taken.values().to_primitive().unwrap();
817
        assert_eq!(taken.array_len(), 2);
818
        assert_eq!(primitive_values.as_slice::<i32>(), [44]);
819
        assert_eq!(
820
            primitive_values.validity_mask().unwrap(),
821
            Mask::from_iter(vec![true])
822
        );
823
    }
824

825
    #[test]
826
    fn test_slice() {
1✔
827
        let values = buffer![15_u32, 135, 13531, 42].into_array();
1✔
828
        let indices = buffer![10_u64, 11, 50, 100].into_array();
1✔
829

830
        let patches = Patches::new(101, 0, indices, values);
1✔
831

832
        let sliced = patches.slice(15, 100).unwrap();
1✔
833
        assert_eq!(sliced.array_len(), 100 - 15);
1✔
834
        let primitive = sliced.values().to_primitive().unwrap();
1✔
835

836
        assert_eq!(primitive.as_slice::<u32>(), &[13531]);
1✔
837
    }
1✔
838

839
    #[test]
840
    fn doubly_sliced() {
1✔
841
        let values = buffer![15_u32, 135, 13531, 42].into_array();
1✔
842
        let indices = buffer![10_u64, 11, 50, 100].into_array();
1✔
843

844
        let patches = Patches::new(101, 0, indices, values);
1✔
845

846
        let sliced = patches.slice(15, 100).unwrap();
1✔
847
        assert_eq!(sliced.array_len(), 100 - 15);
1✔
848
        let primitive = sliced.values().to_primitive().unwrap();
1✔
849

850
        assert_eq!(primitive.as_slice::<u32>(), &[13531]);
1✔
851

852
        let doubly_sliced = sliced.slice(35, 36).unwrap();
1✔
853
        let primitive_doubly_sliced = doubly_sliced.values().to_primitive().unwrap();
1✔
854

855
        assert_eq!(primitive_doubly_sliced.as_slice::<u32>(), &[13531]);
1✔
856
    }
1✔
857

858
    #[test]
859
    fn test_mask_all_true() {
1✔
860
        let patches = Patches::new(
1✔
861
            10,
862
            0,
863
            buffer![2u64, 5, 8].into_array(),
1✔
864
            buffer![100i32, 200, 300].into_array(),
1✔
865
        );
866

867
        let mask = Mask::new_true(10);
1✔
868
        let masked = patches.mask(&mask).unwrap();
1✔
869
        assert!(masked.is_none());
1✔
870
    }
1✔
871

872
    #[test]
873
    fn test_mask_all_false() {
1✔
874
        let patches = Patches::new(
1✔
875
            10,
876
            0,
877
            buffer![2u64, 5, 8].into_array(),
1✔
878
            buffer![100i32, 200, 300].into_array(),
1✔
879
        );
880

881
        let mask = Mask::new_false(10);
1✔
882
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
883

884
        // No patch values should be masked
885
        let masked_values = masked.values().to_primitive().unwrap();
1✔
886
        assert_eq!(masked_values.as_slice::<i32>(), &[100, 200, 300]);
1✔
887
        assert!(masked_values.is_valid(0).unwrap());
1✔
888
        assert!(masked_values.is_valid(1).unwrap());
1✔
889
        assert!(masked_values.is_valid(2).unwrap());
1✔
890

891
        // Indices should remain unchanged
892
        let indices = masked.indices().to_primitive().unwrap();
1✔
893
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
894
    }
1✔
895

896
    #[test]
897
    fn test_mask_partial() {
1✔
898
        let patches = Patches::new(
1✔
899
            10,
900
            0,
901
            buffer![2u64, 5, 8].into_array(),
1✔
902
            buffer![100i32, 200, 300].into_array(),
1✔
903
        );
904

905
        // Mask that removes patches at indices 2 and 8 (but not 5)
906
        let mask = Mask::from_iter([
1✔
907
            false, false, true, false, false, false, false, false, true, false,
1✔
908
        ]);
1✔
909
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
910

911
        // Only the patch at index 5 should remain
912
        let masked_values = masked.values().to_primitive().unwrap();
1✔
913
        assert_eq!(masked_values.len(), 1);
1✔
914
        assert_eq!(masked_values.as_slice::<i32>(), &[200]);
1✔
915

916
        // Only index 5 should remain
917
        let indices = masked.indices().to_primitive().unwrap();
1✔
918
        assert_eq!(indices.as_slice::<u64>(), &[5]);
1✔
919
    }
1✔
920

921
    #[test]
922
    fn test_mask_with_offset() {
1✔
923
        let patches = Patches::new(
1✔
924
            10,
925
            5,                                  // offset
926
            buffer![7u64, 10, 13].into_array(), // actual indices are 2, 5, 8
1✔
927
            buffer![100i32, 200, 300].into_array(),
1✔
928
        );
929

930
        // Mask that sets actual index 2 to null
931
        let mask = Mask::from_iter([
1✔
932
            false, false, true, false, false, false, false, false, false, false,
1✔
933
        ]);
1✔
934

935
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
936
        assert_eq!(masked.array_len(), 10);
1✔
937
        assert_eq!(masked.offset(), 5);
1✔
938
        let indices = masked.indices().to_primitive().unwrap();
1✔
939
        assert_eq!(indices.as_slice::<u64>(), &[10, 13]);
1✔
940
        let masked_values = masked.values().to_primitive().unwrap();
1✔
941
        assert_eq!(masked_values.as_slice::<i32>(), &[200, 300]);
1✔
942
    }
1✔
943

944
    #[test]
945
    fn test_mask_nullable_values() {
1✔
946
        let patches = Patches::new(
1✔
947
            10,
948
            0,
949
            buffer![2u64, 5, 8].into_array(),
1✔
950
            PrimitiveArray::from_option_iter([Some(100i32), None, Some(300)]).into_array(),
1✔
951
        );
952

953
        // Test masking removes patch at index 2
954
        let mask = Mask::from_iter([
1✔
955
            false, false, true, false, false, false, false, false, false, false,
1✔
956
        ]);
1✔
957
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
958

959
        // Patches at indices 5 and 8 should remain
960
        let indices = masked.indices().to_primitive().unwrap();
1✔
961
        assert_eq!(indices.as_slice::<u64>(), &[5, 8]);
1✔
962

963
        // Values should be the null and 300
964
        let masked_values = masked.values().to_primitive().unwrap();
1✔
965
        assert_eq!(masked_values.len(), 2);
1✔
966
        assert!(!masked_values.is_valid(0).unwrap()); // the null value at index 5
1✔
967
        assert!(masked_values.is_valid(1).unwrap()); // the 300 value at index 8
1✔
968
        assert_eq!(i32::try_from(&masked_values.scalar_at(1)).unwrap(), 300i32);
1✔
969
    }
1✔
970

971
    #[test]
972
    fn test_filter_keep_all() {
1✔
973
        let patches = Patches::new(
1✔
974
            10,
975
            0,
976
            buffer![2u64, 5, 8].into_array(),
1✔
977
            buffer![100i32, 200, 300].into_array(),
1✔
978
        );
979

980
        // Keep all indices (mask with indices 0-9)
981
        let mask = Mask::from_indices(10, (0..10).collect());
1✔
982
        let filtered = patches.filter(&mask).unwrap().unwrap();
1✔
983

984
        let indices = filtered.indices().to_primitive().unwrap();
1✔
985
        let values = filtered.values().to_primitive().unwrap();
1✔
986
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
987
        assert_eq!(values.as_slice::<i32>(), &[100, 200, 300]);
1✔
988
    }
1✔
989

990
    #[test]
991
    fn test_filter_none() {
1✔
992
        let patches = Patches::new(
1✔
993
            10,
994
            0,
995
            buffer![2u64, 5, 8].into_array(),
1✔
996
            buffer![100i32, 200, 300].into_array(),
1✔
997
        );
998

999
        // Filter out all (empty mask means keep nothing)
1000
        let mask = Mask::from_indices(10, vec![]);
1✔
1001
        let filtered = patches.filter(&mask).unwrap();
1✔
1002
        assert!(filtered.is_none());
1✔
1003
    }
1✔
1004

1005
    #[test]
1006
    fn test_filter_with_indices() {
1✔
1007
        let patches = Patches::new(
1✔
1008
            10,
1009
            0,
1010
            buffer![2u64, 5, 8].into_array(),
1✔
1011
            buffer![100i32, 200, 300].into_array(),
1✔
1012
        );
1013

1014
        // Keep indices 2, 5, 9 (so patches at 2 and 5 remain)
1015
        let mask = Mask::from_indices(10, vec![2, 5, 9]);
1✔
1016
        let filtered = patches.filter(&mask).unwrap().unwrap();
1✔
1017

1018
        let indices = filtered.indices().to_primitive().unwrap();
1✔
1019
        let values = filtered.values().to_primitive().unwrap();
1✔
1020
        assert_eq!(indices.as_slice::<u64>(), &[0, 1]); // Adjusted indices
1✔
1021
        assert_eq!(values.as_slice::<i32>(), &[100, 200]);
1✔
1022
    }
1✔
1023

1024
    #[test]
1025
    fn test_slice_full_range() {
1✔
1026
        let patches = Patches::new(
1✔
1027
            10,
1028
            0,
1029
            buffer![2u64, 5, 8].into_array(),
1✔
1030
            buffer![100i32, 200, 300].into_array(),
1✔
1031
        );
1032

1033
        let sliced = patches.slice(0, 10).unwrap();
1✔
1034

1035
        let indices = sliced.indices().to_primitive().unwrap();
1✔
1036
        let values = sliced.values().to_primitive().unwrap();
1✔
1037
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
1038
        assert_eq!(values.as_slice::<i32>(), &[100, 200, 300]);
1✔
1039
    }
1✔
1040

1041
    #[test]
1042
    fn test_slice_partial() {
1✔
1043
        let patches = Patches::new(
1✔
1044
            10,
1045
            0,
1046
            buffer![2u64, 5, 8].into_array(),
1✔
1047
            buffer![100i32, 200, 300].into_array(),
1✔
1048
        );
1049

1050
        // Slice from 3 to 8 (includes patch at 5)
1051
        let sliced = patches.slice(3, 8).unwrap();
1✔
1052

1053
        let indices = sliced.indices().to_primitive().unwrap();
1✔
1054
        let values = sliced.values().to_primitive().unwrap();
1✔
1055
        assert_eq!(indices.as_slice::<u64>(), &[5]); // Index stays the same
1✔
1056
        assert_eq!(values.as_slice::<i32>(), &[200]);
1✔
1057
        assert_eq!(sliced.array_len(), 5); // 8 - 3 = 5
1✔
1058
        assert_eq!(sliced.offset(), 3); // New offset
1✔
1059
    }
1✔
1060

1061
    #[test]
1062
    fn test_slice_no_patches() {
1✔
1063
        let patches = Patches::new(
1✔
1064
            10,
1065
            0,
1066
            buffer![2u64, 5, 8].into_array(),
1✔
1067
            buffer![100i32, 200, 300].into_array(),
1✔
1068
        );
1069

1070
        // Slice from 6 to 7 (no patches in this range)
1071
        let sliced = patches.slice(6, 7);
1✔
1072
        assert!(sliced.is_none());
1✔
1073
    }
1✔
1074

1075
    #[test]
1076
    fn test_slice_with_offset() {
1✔
1077
        let patches = Patches::new(
1✔
1078
            10,
1079
            5,                                  // offset
1080
            buffer![7u64, 10, 13].into_array(), // actual indices are 2, 5, 8
1✔
1081
            buffer![100i32, 200, 300].into_array(),
1✔
1082
        );
1083

1084
        // Slice from 3 to 8 (includes patch at actual index 5)
1085
        let sliced = patches.slice(3, 8).unwrap();
1✔
1086

1087
        let indices = sliced.indices().to_primitive().unwrap();
1✔
1088
        let values = sliced.values().to_primitive().unwrap();
1✔
1089
        assert_eq!(indices.as_slice::<u64>(), &[10]); // Index stays the same (offset + 5 = 10)
1✔
1090
        assert_eq!(values.as_slice::<i32>(), &[200]);
1✔
1091
        assert_eq!(sliced.offset(), 8); // New offset = 5 + 3
1✔
1092
    }
1✔
1093

1094
    #[test]
1095
    fn test_patch_values() {
1✔
1096
        let patches = Patches::new(
1✔
1097
            10,
1098
            0,
1099
            buffer![2u64, 5, 8].into_array(),
1✔
1100
            buffer![100i32, 200, 300].into_array(),
1✔
1101
        );
1102

1103
        let values = patches.values().to_primitive().unwrap();
1✔
1104
        assert_eq!(i32::try_from(&values.scalar_at(0)).unwrap(), 100i32);
1✔
1105
        assert_eq!(i32::try_from(&values.scalar_at(1)).unwrap(), 200i32);
1✔
1106
        assert_eq!(i32::try_from(&values.scalar_at(2)).unwrap(), 300i32);
1✔
1107
    }
1✔
1108

1109
    #[test]
1110
    fn test_indices_range() {
1✔
1111
        let patches = Patches::new(
1✔
1112
            10,
1113
            0,
1114
            buffer![2u64, 5, 8].into_array(),
1✔
1115
            buffer![100i32, 200, 300].into_array(),
1✔
1116
        );
1117

1118
        assert_eq!(patches.min_index(), 2);
1✔
1119
        assert_eq!(patches.max_index(), 8);
1✔
1120
    }
1✔
1121

1122
    #[test]
1123
    fn test_search_index() {
1✔
1124
        let patches = Patches::new(
1✔
1125
            10,
1126
            0,
1127
            buffer![2u64, 5, 8].into_array(),
1✔
1128
            buffer![100i32, 200, 300].into_array(),
1✔
1129
        );
1130

1131
        // Search for exact indices
1132
        assert_eq!(patches.search_index(2), SearchResult::Found(0));
1✔
1133
        assert_eq!(patches.search_index(5), SearchResult::Found(1));
1✔
1134
        assert_eq!(patches.search_index(8), SearchResult::Found(2));
1✔
1135

1136
        // Search for non-patch indices
1137
        assert_eq!(patches.search_index(0), SearchResult::NotFound(0));
1✔
1138
        assert_eq!(patches.search_index(3), SearchResult::NotFound(1));
1✔
1139
        assert_eq!(patches.search_index(6), SearchResult::NotFound(2));
1✔
1140
        assert_eq!(patches.search_index(9), SearchResult::NotFound(3));
1✔
1141
    }
1✔
1142

1143
    #[test]
1144
    fn test_mask_boundary_patches() {
1✔
1145
        // Test masking patches at array boundaries
1146
        let patches = Patches::new(
1✔
1147
            10,
1148
            0,
1149
            buffer![0u64, 9].into_array(),
1✔
1150
            buffer![100i32, 200].into_array(),
1✔
1151
        );
1152

1153
        let mask = Mask::from_iter([
1✔
1154
            true, false, false, false, false, false, false, false, false, false,
1✔
1155
        ]);
1✔
1156
        let masked = patches.mask(&mask).unwrap();
1✔
1157
        assert!(masked.is_some());
1✔
1158
        let masked = masked.unwrap();
1✔
1159
        let indices = masked.indices().to_primitive().unwrap();
1✔
1160
        assert_eq!(indices.as_slice::<u64>(), &[9]);
1✔
1161
        let values = masked.values().to_primitive().unwrap();
1✔
1162
        assert_eq!(values.as_slice::<i32>(), &[200]);
1✔
1163
    }
1✔
1164

1165
    #[test]
1166
    fn test_mask_all_patches_removed() {
1✔
1167
        // Test when all patches are masked out
1168
        let patches = Patches::new(
1✔
1169
            10,
1170
            0,
1171
            buffer![2u64, 5, 8].into_array(),
1✔
1172
            buffer![100i32, 200, 300].into_array(),
1✔
1173
        );
1174

1175
        // Mask that removes all patches
1176
        let mask = Mask::from_iter([
1✔
1177
            false, false, true, false, false, true, false, false, true, false,
1✔
1178
        ]);
1✔
1179
        let masked = patches.mask(&mask).unwrap();
1✔
1180
        assert!(masked.is_none());
1✔
1181
    }
1✔
1182

1183
    #[test]
1184
    fn test_mask_no_patches_removed() {
1✔
1185
        // Test when no patches are masked
1186
        let patches = Patches::new(
1✔
1187
            10,
1188
            0,
1189
            buffer![2u64, 5, 8].into_array(),
1✔
1190
            buffer![100i32, 200, 300].into_array(),
1✔
1191
        );
1192

1193
        // Mask that doesn't affect any patches
1194
        let mask = Mask::from_iter([
1✔
1195
            true, false, false, true, false, false, true, false, false, true,
1✔
1196
        ]);
1✔
1197
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1198

1199
        let indices = masked.indices().to_primitive().unwrap();
1✔
1200
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
1201
        let values = masked.values().to_primitive().unwrap();
1✔
1202
        assert_eq!(values.as_slice::<i32>(), &[100, 200, 300]);
1✔
1203
    }
1✔
1204

1205
    #[test]
1206
    fn test_mask_single_patch() {
1✔
1207
        // Test with a single patch
1208
        let patches = Patches::new(
1✔
1209
            5,
1210
            0,
1211
            buffer![2u64].into_array(),
1✔
1212
            buffer![42i32].into_array(),
1✔
1213
        );
1214

1215
        // Mask that removes the single patch
1216
        let mask = Mask::from_iter([false, false, true, false, false]);
1✔
1217
        let masked = patches.mask(&mask).unwrap();
1✔
1218
        assert!(masked.is_none());
1✔
1219

1220
        // Mask that keeps the single patch
1221
        let mask = Mask::from_iter([true, false, false, true, false]);
1✔
1222
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1223
        let indices = masked.indices().to_primitive().unwrap();
1✔
1224
        assert_eq!(indices.as_slice::<u64>(), &[2]);
1✔
1225
    }
1✔
1226

1227
    #[test]
1228
    fn test_mask_contiguous_patches() {
1✔
1229
        // Test with contiguous patches
1230
        let patches = Patches::new(
1✔
1231
            10,
1232
            0,
1233
            buffer![3u64, 4, 5, 6].into_array(),
1✔
1234
            buffer![100i32, 200, 300, 400].into_array(),
1✔
1235
        );
1236

1237
        // Mask that removes middle patches
1238
        let mask = Mask::from_iter([
1✔
1239
            false, false, false, false, true, true, false, false, false, false,
1✔
1240
        ]);
1✔
1241
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1242

1243
        let indices = masked.indices().to_primitive().unwrap();
1✔
1244
        assert_eq!(indices.as_slice::<u64>(), &[3, 6]);
1✔
1245
        let values = masked.values().to_primitive().unwrap();
1✔
1246
        assert_eq!(values.as_slice::<i32>(), &[100, 400]);
1✔
1247
    }
1✔
1248

1249
    #[test]
1250
    fn test_mask_with_large_offset() {
1✔
1251
        // Test with a large offset that shifts all indices
1252
        let patches = Patches::new(
1✔
1253
            20,
1254
            15,
1255
            buffer![16u64, 17, 19].into_array(), // actual indices are 1, 2, 4
1✔
1256
            buffer![100i32, 200, 300].into_array(),
1✔
1257
        );
1258

1259
        // Mask that removes the patch at actual index 2
1260
        let mask = Mask::from_iter([
1✔
1261
            false, false, true, false, false, false, false, false, false, false, false, false,
1✔
1262
            false, false, false, false, false, false, false, false,
1✔
1263
        ]);
1✔
1264
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1265

1266
        let indices = masked.indices().to_primitive().unwrap();
1✔
1267
        assert_eq!(indices.as_slice::<u64>(), &[16, 19]);
1✔
1268
        let values = masked.values().to_primitive().unwrap();
1✔
1269
        assert_eq!(values.as_slice::<i32>(), &[100, 300]);
1✔
1270
    }
1✔
1271

1272
    #[test]
1273
    #[should_panic(expected = "Filter mask length 5 does not match array length 10")]
1274
    fn test_mask_wrong_length() {
1✔
1275
        let patches = Patches::new(
1✔
1276
            10,
1277
            0,
1278
            buffer![2u64, 5, 8].into_array(),
1✔
1279
            buffer![100i32, 200, 300].into_array(),
1✔
1280
        );
1281

1282
        // Mask with wrong length
1283
        let mask = Mask::from_iter([false, false, true, false, false]);
1✔
1284
        let _ = patches.mask(&mask).unwrap();
1✔
1285
    }
1✔
1286
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc