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

vortex-data / vortex / 16992684502

15 Aug 2025 02:56PM UTC coverage: 87.875% (+0.2%) from 87.72%
16992684502

Pull #2456

github

web-flow
Merge 2d540e578 into 4a23f65b3
Pull Request #2456: feat: basic BoolBuffer / BoolBufferMut

1275 of 1428 new or added lines in 110 files covered. (89.29%)

334 existing lines in 31 files now uncovered.

57169 of 65057 relevant lines covered (87.88%)

658056.52 hits per line

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

92.03
/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 itertools::Itertools as _;
9
use num_traits::{NumCast, ToPrimitive};
10
use serde::{Deserialize, Serialize};
11
use vortex_buffer::{BitBuffer, BufferMut};
12
use vortex_dtype::Nullability::NonNullable;
13
use vortex_dtype::{
14
    DType, NativePType, PType, match_each_integer_ptype, match_each_unsigned_integer_ptype,
15
};
16
use vortex_error::{
17
    VortexError, VortexExpect, VortexResult, VortexUnwrap, vortex_bail, vortex_err,
18
};
19
use vortex_mask::{AllOr, Mask};
20
use vortex_scalar::{PValue, Scalar};
21
use vortex_utils::aliases::hash_map::HashMap;
22

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

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

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

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

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

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

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

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

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

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

135
    pub fn array_len(&self) -> usize {
2,170,590✔
136
        self.array_len
2,170,590✔
137
    }
2,170,590✔
138

139
    pub fn num_patches(&self) -> usize {
14,548✔
140
        self.indices.len()
14,548✔
141
    }
14,548✔
142

143
    pub fn dtype(&self) -> &DType {
8,841✔
144
        self.values.dtype()
8,841✔
145
    }
8,841✔
146

147
    pub fn indices(&self) -> &ArrayRef {
62,493✔
148
        &self.indices
62,493✔
149
    }
62,493✔
150

151
    pub fn into_indices(self) -> ArrayRef {
×
152
        self.indices
×
153
    }
×
154

155
    pub fn indices_mut(&mut self) -> &mut ArrayRef {
×
156
        &mut self.indices
×
157
    }
×
158

159
    pub fn values(&self) -> &ArrayRef {
271,822✔
160
        &self.values
271,822✔
161
    }
271,822✔
162

163
    pub fn into_values(self) -> ArrayRef {
702✔
164
        self.values
702✔
165
    }
702✔
166

167
    pub fn values_mut(&mut self) -> &mut ArrayRef {
×
168
        &mut self.values
×
169
    }
×
170

171
    pub fn offset(&self) -> usize {
131,565✔
172
        self.offset
131,565✔
173
    }
131,565✔
174

175
    pub fn indices_ptype(&self) -> PType {
×
176
        PType::try_from(self.indices.dtype()).vortex_expect("primitive indices")
×
177
    }
×
178

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

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

211
    /// Get the patched value at a given index if it exists.
212
    pub fn get_patched(&self, index: usize) -> VortexResult<Option<Scalar>> {
2,767,242✔
213
        if let Some(patch_idx) = self.search_index(index)?.to_found() {
2,767,242✔
214
            self.values().scalar_at(patch_idx).map(Some)
166,977✔
215
        } else {
216
            Ok(None)
2,600,265✔
217
        }
218
    }
2,767,242✔
219

220
    /// Return the insertion point of `index` in the [Self::indices].
221
    pub fn search_index(&self, index: usize) -> VortexResult<SearchResult> {
2,776,697✔
222
        Ok(self.indices.as_primitive_typed().search_sorted(
2,776,697✔
223
            &PValue::U64((index + self.offset) as u64),
2,776,697✔
224
            SearchSortedSide::Left,
2,776,697✔
225
        ))
2,776,697✔
226
    }
2,776,697✔
227

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

236
        let sr = if self.values().dtype().is_primitive() {
11✔
237
            self.values()
11✔
238
                .as_primitive_typed()
11✔
239
                .search_sorted(&target.as_primitive().pvalue(), side)
11✔
240
        } else {
241
            self.values().search_sorted(&target, side)
×
242
        };
243

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

262
    /// Returns the minimum patch index
263
    pub fn min_index(&self) -> VortexResult<usize> {
5,774✔
264
        Ok(usize::try_from(&self.indices().scalar_at(0)?)? - self.offset)
5,774✔
265
    }
5,774✔
266

267
    /// Returns the maximum patch index
268
    pub fn max_index(&self) -> VortexResult<usize> {
5,774✔
269
        Ok(usize::try_from(&self.indices().scalar_at(self.indices().len() - 1)?)? - self.offset)
5,774✔
270
    }
5,774✔
271

272
    /// Filter the patches by a mask, resulting in new patches for the filtered array.
273
    pub fn filter(&self, mask: &Mask) -> VortexResult<Option<Self>> {
4,594✔
274
        if mask.len() != self.array_len {
4,594✔
275
            vortex_bail!(
×
276
                "Filter mask length {} does not match array length {}",
×
277
                mask.len(),
×
278
                self.array_len
279
            );
280
        }
4,594✔
281

282
        match mask.indices() {
4,594✔
283
            AllOr::All => Ok(Some(self.clone())),
1✔
284
            AllOr::None => Ok(None),
1✔
285
            AllOr::Some(mask_indices) => {
4,592✔
286
                let flat_indices = self.indices().to_primitive()?;
4,592✔
287
                match_each_unsigned_integer_ptype!(flat_indices.ptype(), |I| {
4,592✔
288
                    filter_patches_with_mask(
×
289
                        flat_indices.as_slice::<I>(),
×
290
                        self.offset(),
×
291
                        self.values(),
×
292
                        mask_indices,
×
293
                    )
294
                })
295
            }
296
        }
297
    }
4,594✔
298

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

311
        let filter_mask = match mask.bit_buffer() {
168✔
312
            AllOr::All => return Ok(None),
1✔
313
            AllOr::None => return Ok(Some(self.clone())),
1✔
314
            AllOr::Some(masked) => {
166✔
315
                let patch_indices = self.indices().to_primitive()?;
166✔
316
                match_each_unsigned_integer_ptype!(patch_indices.ptype(), |P| {
166✔
317
                    let patch_indices = patch_indices.as_slice::<P>();
×
318
                    Mask::from_buffer(BitBuffer::collect_bool(patch_indices.len(), |i| {
884✔
319
                        #[allow(clippy::cast_possible_truncation)]
320
                        let idx = (patch_indices[i] as usize) - self.offset;
884✔
321
                        !masked.value(idx)
884✔
322
                    }))
884✔
323
                })
324
            }
325
        };
326

327
        if filter_mask.all_false() {
166✔
328
            return Ok(None);
2✔
329
        }
164✔
330

331
        Ok(Some(Self::new_unchecked(
164✔
332
            self.array_len,
164✔
333
            self.offset,
164✔
334
            filter(&self.indices, &filter_mask)?,
164✔
335
            filter(&self.values, &filter_mask)?,
164✔
336
        )))
337
    }
169✔
338

339
    /// Slice the patches by a range of the patched array.
340
    pub fn slice(&self, start: usize, stop: usize) -> VortexResult<Option<Self>> {
4,724✔
341
        let patch_start = self.search_index(start)?.to_index();
4,724✔
342
        let patch_stop = self.search_index(stop)?.to_index();
4,724✔
343

344
        if patch_start == patch_stop {
4,724✔
345
            return Ok(None);
1,291✔
346
        }
3,433✔
347

348
        // Slice out the values and indices
349
        let values = self.values().slice(patch_start, patch_stop)?;
3,433✔
350
        let indices = self.indices().slice(patch_start, patch_stop)?;
3,433✔
351

352
        Ok(Some(Self::new(
3,433✔
353
            stop - start,
3,433✔
354
            start + self.offset(),
3,433✔
355
            indices,
3,433✔
356
            values,
3,433✔
357
        )))
3,433✔
358
    }
4,724✔
359

360
    // https://docs.google.com/spreadsheets/d/1D9vBZ1QJ6mwcIvV5wIL0hjGgVchcEnAyhvitqWu2ugU
361
    const PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN: f64 = 5.0;
362

363
    fn is_map_faster_than_search(&self, take_indices: &PrimitiveArray) -> bool {
6,124✔
364
        (self.num_patches() as f64 / take_indices.len() as f64)
6,124✔
365
            < Self::PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN
6,124✔
366
    }
6,124✔
367

368
    /// Take the indices from the patches
369
    ///
370
    /// Any nulls in take_indices are added to the resulting patches.
371
    pub fn take_with_nulls(&self, take_indices: &dyn Array) -> VortexResult<Option<Self>> {
1,872✔
372
        if take_indices.is_empty() {
1,872✔
373
            return Ok(None);
×
374
        }
1,872✔
375

376
        let take_indices = take_indices.to_primitive()?;
1,872✔
377
        if self.is_map_faster_than_search(&take_indices) {
1,872✔
378
            self.take_map(take_indices, true)
1,833✔
379
        } else {
380
            self.take_search(take_indices, true)
39✔
381
        }
382
    }
1,872✔
383

384
    /// Take the indices from the patches.
385
    ///
386
    /// Any nulls in take_indices are ignored.
387
    pub fn take(&self, take_indices: &dyn Array) -> VortexResult<Option<Self>> {
4,252✔
388
        if take_indices.is_empty() {
4,252✔
389
            return Ok(None);
×
390
        }
4,252✔
391

392
        let take_indices = take_indices.to_primitive()?;
4,252✔
393
        if self.is_map_faster_than_search(&take_indices) {
4,252✔
394
            self.take_map(take_indices, false)
3,940✔
395
        } else {
396
            self.take_search(take_indices, false)
312✔
397
        }
398
    }
4,252✔
399

400
    pub fn take_search(
351✔
401
        &self,
351✔
402
        take_indices: PrimitiveArray,
351✔
403
        include_nulls: bool,
351✔
404
    ) -> VortexResult<Option<Self>> {
351✔
405
        let indices = self.indices.to_primitive()?;
351✔
406
        let new_length = take_indices.len();
351✔
407

408
        let Some((new_indices, values_indices)) =
273✔
409
            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
351✔
410
                match_each_integer_ptype!(take_indices.ptype(), |TakeIndices| {
×
411
                    take_search::<_, TakeIndices>(
×
412
                        indices.as_slice::<Indices>(),
×
413
                        take_indices,
×
414
                        self.offset(),
×
415
                        include_nulls,
×
416
                    )?
×
417
                })
418
            })
419
        else {
420
            return Ok(None);
78✔
421
        };
422

423
        Ok(Some(Self::new(
273✔
424
            new_length,
273✔
425
            0,
426
            new_indices,
273✔
427
            take(self.values(), &values_indices)?,
273✔
428
        )))
429
    }
351✔
430

431
    pub fn take_map(
5,773✔
432
        &self,
5,773✔
433
        take_indices: PrimitiveArray,
5,773✔
434
        include_nulls: bool,
5,773✔
435
    ) -> VortexResult<Option<Self>> {
5,773✔
436
        let indices = self.indices.to_primitive()?;
5,773✔
437
        let new_length = take_indices.len();
5,773✔
438

439
        let Some((new_sparse_indices, value_indices)) =
4,096✔
440
            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
5,773✔
441
                match_each_integer_ptype!(take_indices.ptype(), |TakeIndices| {
78✔
442
                    take_map::<_, TakeIndices>(
×
443
                        indices.as_slice::<Indices>(),
×
444
                        take_indices,
×
445
                        self.offset(),
×
446
                        self.min_index()?,
×
447
                        self.max_index()?,
×
448
                        include_nulls,
×
449
                    )?
×
450
                })
451
            })
452
        else {
453
            return Ok(None);
1,677✔
454
        };
455

456
        Ok(Some(Patches::new(
4,096✔
457
            new_length,
4,096✔
458
            0,
459
            new_sparse_indices,
4,096✔
460
            take(self.values(), &value_indices)?,
4,096✔
461
        )))
462
    }
5,773✔
463

464
    pub fn map_values<F>(self, f: F) -> VortexResult<Self>
360✔
465
    where
360✔
466
        F: FnOnce(ArrayRef) -> VortexResult<ArrayRef>,
360✔
467
    {
468
        let values = f(self.values)?;
360✔
469
        if self.indices.len() != values.len() {
332✔
470
            vortex_bail!(
×
471
                "map_values must preserve length: expected {} received {}",
×
472
                self.indices.len(),
×
473
                values.len()
×
474
            )
475
        }
332✔
476
        Ok(Self::new(self.array_len, self.offset, self.indices, values))
332✔
477
    }
360✔
478
}
479

480
fn take_search<I: NativePType + NumCast + PartialOrd, T: NativePType + NumCast>(
351✔
481
    indices: &[I],
351✔
482
    take_indices: PrimitiveArray,
351✔
483
    indices_offset: usize,
351✔
484
    include_nulls: bool,
351✔
485
) -> VortexResult<Option<(ArrayRef, ArrayRef)>>
351✔
486
where
351✔
487
    usize: TryFrom<T>,
351✔
488
    VortexError: From<<usize as TryFrom<T>>::Error>,
351✔
489
{
490
    let take_indices_validity = take_indices.validity();
351✔
491
    let indices_offset = I::from(indices_offset).vortex_expect("indices_offset out of range");
351✔
492

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

514
    if new_indices.is_empty() {
351✔
515
        return Ok(None);
78✔
516
    }
273✔
517

518
    let new_indices = new_indices.into_array();
273✔
519
    let values_validity = take_indices_validity.take(&new_indices)?;
273✔
520
    Ok(Some((
273✔
521
        new_indices,
273✔
522
        PrimitiveArray::new(values_indices, values_validity).into_array(),
273✔
523
    )))
273✔
524
}
351✔
525

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

542
    let sparse_index_to_value_index: HashMap<I, usize> = indices
5,773✔
543
        .iter()
5,773✔
544
        .copied()
5,773✔
545
        .map(|idx| idx - offset_i)
17,085✔
546
        .enumerate()
5,773✔
547
        .map(|(value_index, sparse_index)| (sparse_index, value_index))
17,085✔
548
        .collect();
5,773✔
549

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

575
    if new_sparse_indices.is_empty() {
5,773✔
576
        return Ok(None);
1,677✔
577
    }
4,096✔
578

579
    let new_sparse_indices = new_sparse_indices.into_array();
4,096✔
580
    let values_validity = take_indices_validity.take(&new_sparse_indices)?;
4,096✔
581
    Ok(Some((
4,096✔
582
        new_sparse_indices,
4,096✔
583
        PrimitiveArray::new(value_indices, values_validity).into_array(),
4,096✔
584
    )))
4,096✔
585
}
5,773✔
586

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

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

607
    let mut mask_idx = 0usize;
4,592✔
608
    let mut true_idx = 0usize;
4,592✔
609

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

627
            if left_min > right_max {
520,773✔
628
                // Advance right side
629
                true_idx += STRIDE;
47,307✔
630
                continue;
47,307✔
631
            } else if right_min > left_max {
473,466✔
632
                mask_idx += STRIDE;
12,762✔
633
                continue;
12,762✔
634
            } else {
460,704✔
635
                // Fallthrough to direct comparison path.
460,704✔
636
            }
460,704✔
637
        }
143,508✔
638

639
        // Two-way sorted iterator merge:
640

641
        let left = patch_indices[mask_idx].to_usize().vortex_expect("left") - offset;
604,212✔
642
        let right = mask_indices[true_idx];
604,212✔
643

644
        match left.cmp(&right) {
604,212✔
645
            Ordering::Less => {
211,112✔
646
                mask_idx += 1;
211,112✔
647
            }
211,112✔
648
            Ordering::Greater => {
383,046✔
649
                true_idx += 1;
383,046✔
650
            }
383,046✔
651
            Ordering::Equal => {
10,054✔
652
                // Save the mask index as well as the positional index.
10,054✔
653
                new_mask_indices.push(mask_idx);
10,054✔
654
                new_patch_indices.push(true_idx as u64);
10,054✔
655

10,054✔
656
                mask_idx += 1;
10,054✔
657
                true_idx += 1;
10,054✔
658
            }
10,054✔
659
        }
660
    }
661

662
    if new_mask_indices.is_empty() {
4,592✔
663
        return Ok(None);
1,200✔
664
    }
3,392✔
665

666
    let new_patch_indices = new_patch_indices.into_array();
3,392✔
667
    let new_patch_values = filter(
3,392✔
668
        patch_values,
3,392✔
669
        &Mask::from_indices(patch_values.len(), new_mask_indices),
3,392✔
670
    )?;
×
671

672
    Ok(Some(Patches::new(
3,392✔
673
        true_count,
3,392✔
674
        0,
3,392✔
675
        new_patch_indices,
3,392✔
676
        new_patch_values,
3,392✔
677
    )))
3,392✔
678
}
4,592✔
679

680
#[cfg(test)]
681
mod test {
682
    use rstest::{fixture, rstest};
683
    use vortex_buffer::buffer;
684
    use vortex_mask::Mask;
685

686
    use crate::arrays::PrimitiveArray;
687
    use crate::patches::Patches;
688
    use crate::search_sorted::{SearchResult, SearchSortedSide};
689
    use crate::validity::Validity;
690
    use crate::{IntoArray, ToCanonical};
691

692
    #[test]
693
    fn test_filter() {
1✔
694
        let patches = Patches::new(
1✔
695
            100,
696
            0,
697
            buffer![10u32, 11, 20].into_array(),
1✔
698
            buffer![100, 110, 200].into_array(),
1✔
699
        );
700

701
        let filtered = patches
1✔
702
            .filter(&Mask::from_indices(100, vec![10, 20, 30]))
1✔
703
            .unwrap()
1✔
704
            .unwrap();
1✔
705

706
        let indices = filtered.indices().to_primitive().unwrap();
1✔
707
        let values = filtered.values().to_primitive().unwrap();
1✔
708
        assert_eq!(indices.as_slice::<u64>(), &[0, 1]);
1✔
709
        assert_eq!(values.as_slice::<i32>(), &[100, 200]);
1✔
710
    }
1✔
711

712
    #[fixture]
713
    fn patches() -> Patches {
714
        Patches::new(
715
            20,
716
            0,
717
            buffer![2u64, 9, 15].into_array(),
718
            PrimitiveArray::new(buffer![33_i32, 44, 55], Validity::AllValid).into_array(),
719
        )
720
    }
721

722
    #[rstest]
723
    fn search_larger_than(patches: Patches) {
724
        let res = patches.search_sorted(66, SearchSortedSide::Left).unwrap();
725
        assert_eq!(res, SearchResult::NotFound(16));
726
    }
727

728
    #[rstest]
729
    fn search_less_than(patches: Patches) {
730
        let res = patches.search_sorted(22, SearchSortedSide::Left).unwrap();
731
        assert_eq!(res, SearchResult::NotFound(2));
732
    }
733

734
    #[rstest]
735
    fn search_found(patches: Patches) {
736
        let res = patches.search_sorted(44, SearchSortedSide::Left).unwrap();
737
        assert_eq!(res, SearchResult::Found(9));
738
    }
739

740
    #[rstest]
741
    fn search_not_found_right(patches: Patches) {
742
        let res = patches.search_sorted(56, SearchSortedSide::Right).unwrap();
743
        assert_eq!(res, SearchResult::NotFound(16));
744
    }
745

746
    #[rstest]
747
    fn search_sliced(patches: Patches) {
748
        let sliced = patches.slice(7, 20).unwrap().unwrap();
749
        assert_eq!(
750
            sliced.search_sorted(22, SearchSortedSide::Left).unwrap(),
751
            SearchResult::NotFound(2)
752
        );
753
    }
754

755
    #[test]
756
    fn search_right() {
1✔
757
        let patches = Patches::new(
1✔
758
            6,
759
            0,
760
            buffer![0u8, 1, 4, 5].into_array(),
1✔
761
            buffer![-128i8, -98, 8, 50].into_array(),
1✔
762
        );
763

764
        assert_eq!(
1✔
765
            patches.search_sorted(-98, SearchSortedSide::Right).unwrap(),
1✔
766
            SearchResult::Found(2)
767
        );
768
        assert_eq!(
1✔
769
            patches.search_sorted(50, SearchSortedSide::Right).unwrap(),
1✔
770
            SearchResult::Found(6),
771
        );
772
        assert_eq!(
1✔
773
            patches.search_sorted(7, SearchSortedSide::Right).unwrap(),
1✔
774
            SearchResult::NotFound(2),
775
        );
776
        assert_eq!(
1✔
777
            patches.search_sorted(51, SearchSortedSide::Right).unwrap(),
1✔
778
            SearchResult::NotFound(6)
779
        );
780
    }
1✔
781

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

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

818
    #[test]
819
    fn test_slice() {
1✔
820
        let values = buffer![15_u32, 135, 13531, 42].into_array();
1✔
821
        let indices = buffer![10_u64, 11, 50, 100].into_array();
1✔
822

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

825
        let sliced = patches.slice(15, 100).unwrap().unwrap();
1✔
826
        assert_eq!(sliced.array_len(), 100 - 15);
1✔
827
        let primitive = sliced.values().to_primitive().unwrap();
1✔
828

829
        assert_eq!(primitive.as_slice::<u32>(), &[13531]);
1✔
830
    }
1✔
831

832
    #[test]
833
    fn doubly_sliced() {
1✔
834
        let values = buffer![15_u32, 135, 13531, 42].into_array();
1✔
835
        let indices = buffer![10_u64, 11, 50, 100].into_array();
1✔
836

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

839
        let sliced = patches.slice(15, 100).unwrap().unwrap();
1✔
840
        assert_eq!(sliced.array_len(), 100 - 15);
1✔
841
        let primitive = sliced.values().to_primitive().unwrap();
1✔
842

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

845
        let doubly_sliced = sliced.slice(35, 36).unwrap().unwrap();
1✔
846
        let primitive_doubly_sliced = doubly_sliced.values().to_primitive().unwrap();
1✔
847

848
        assert_eq!(primitive_doubly_sliced.as_slice::<u32>(), &[13531]);
1✔
849
    }
1✔
850

851
    #[test]
852
    fn test_mask_all_true() {
1✔
853
        let patches = Patches::new(
1✔
854
            10,
855
            0,
856
            buffer![2u64, 5, 8].into_array(),
1✔
857
            buffer![100i32, 200, 300].into_array(),
1✔
858
        );
859

860
        let mask = Mask::new_true(10);
1✔
861
        let masked = patches.mask(&mask).unwrap();
1✔
862
        assert!(masked.is_none());
1✔
863
    }
1✔
864

865
    #[test]
866
    fn test_mask_all_false() {
1✔
867
        let patches = Patches::new(
1✔
868
            10,
869
            0,
870
            buffer![2u64, 5, 8].into_array(),
1✔
871
            buffer![100i32, 200, 300].into_array(),
1✔
872
        );
873

874
        let mask = Mask::new_false(10);
1✔
875
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
876

877
        // No patch values should be masked
878
        let masked_values = masked.values().to_primitive().unwrap();
1✔
879
        assert_eq!(masked_values.as_slice::<i32>(), &[100, 200, 300]);
1✔
880
        assert!(masked_values.is_valid(0).unwrap());
1✔
881
        assert!(masked_values.is_valid(1).unwrap());
1✔
882
        assert!(masked_values.is_valid(2).unwrap());
1✔
883

884
        // Indices should remain unchanged
885
        let indices = masked.indices().to_primitive().unwrap();
1✔
886
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
887
    }
1✔
888

889
    #[test]
890
    fn test_mask_partial() {
1✔
891
        let patches = Patches::new(
1✔
892
            10,
893
            0,
894
            buffer![2u64, 5, 8].into_array(),
1✔
895
            buffer![100i32, 200, 300].into_array(),
1✔
896
        );
897

898
        // Mask that removes patches at indices 2 and 8 (but not 5)
899
        let mask = Mask::from_iter([
1✔
900
            false, false, true, false, false, false, false, false, true, false,
1✔
901
        ]);
1✔
902
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
903

904
        // Only the patch at index 5 should remain
905
        let masked_values = masked.values().to_primitive().unwrap();
1✔
906
        assert_eq!(masked_values.len(), 1);
1✔
907
        assert_eq!(masked_values.as_slice::<i32>(), &[200]);
1✔
908

909
        // Only index 5 should remain
910
        let indices = masked.indices().to_primitive().unwrap();
1✔
911
        assert_eq!(indices.as_slice::<u64>(), &[5]);
1✔
912
    }
1✔
913

914
    #[test]
915
    fn test_mask_with_offset() {
1✔
916
        let patches = Patches::new(
1✔
917
            10,
918
            5,                                  // offset
919
            buffer![7u64, 10, 13].into_array(), // actual indices are 2, 5, 8
1✔
920
            buffer![100i32, 200, 300].into_array(),
1✔
921
        );
922

923
        // Mask that sets actual index 2 to null
924
        let mask = Mask::from_iter([
1✔
925
            false, false, true, false, false, false, false, false, false, false,
1✔
926
        ]);
1✔
927

928
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
929
        assert_eq!(masked.array_len(), 10);
1✔
930
        assert_eq!(masked.offset(), 5);
1✔
931
        let indices = masked.indices().to_primitive().unwrap();
1✔
932
        assert_eq!(indices.as_slice::<u64>(), &[10, 13]);
1✔
933
        let masked_values = masked.values().to_primitive().unwrap();
1✔
934
        assert_eq!(masked_values.as_slice::<i32>(), &[200, 300]);
1✔
935
    }
1✔
936

937
    #[test]
938
    fn test_mask_nullable_values() {
1✔
939
        let patches = Patches::new(
1✔
940
            10,
941
            0,
942
            buffer![2u64, 5, 8].into_array(),
1✔
943
            PrimitiveArray::from_option_iter([Some(100i32), None, Some(300)]).into_array(),
1✔
944
        );
945

946
        // Test masking removes patch at index 2
947
        let mask = Mask::from_iter([
1✔
948
            false, false, true, false, false, false, false, false, false, false,
1✔
949
        ]);
1✔
950
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
951

952
        // Patches at indices 5 and 8 should remain
953
        let indices = masked.indices().to_primitive().unwrap();
1✔
954
        assert_eq!(indices.as_slice::<u64>(), &[5, 8]);
1✔
955

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

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

976
        // Keep all indices (mask with indices 0-9)
977
        let mask = Mask::from_indices(10, (0..10).collect());
1✔
978
        let filtered = patches.filter(&mask).unwrap().unwrap();
1✔
979

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

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

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

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

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

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

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

1029
        let sliced = patches.slice(0, 10).unwrap().unwrap();
1✔
1030

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

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

1046
        // Slice from 3 to 8 (includes patch at 5)
1047
        let sliced = patches.slice(3, 8).unwrap().unwrap();
1✔
1048

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

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

1066
        // Slice from 6 to 7 (no patches in this range)
1067
        let sliced = patches.slice(6, 7).unwrap();
1✔
1068
        assert!(sliced.is_none());
1✔
1069
    }
1✔
1070

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

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

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

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

1099
        let values = patches.values().to_primitive().unwrap();
1✔
1100
        assert_eq!(
1✔
1101
            i32::try_from(&values.scalar_at(0).unwrap()).unwrap(),
1✔
1102
            100i32
1103
        );
1104
        assert_eq!(
1✔
1105
            i32::try_from(&values.scalar_at(1).unwrap()).unwrap(),
1✔
1106
            200i32
1107
        );
1108
        assert_eq!(
1✔
1109
            i32::try_from(&values.scalar_at(2).unwrap()).unwrap(),
1✔
1110
            300i32
1111
        );
1112
    }
1✔
1113

1114
    #[test]
1115
    fn test_indices_range() {
1✔
1116
        let patches = Patches::new(
1✔
1117
            10,
1118
            0,
1119
            buffer![2u64, 5, 8].into_array(),
1✔
1120
            buffer![100i32, 200, 300].into_array(),
1✔
1121
        );
1122

1123
        assert_eq!(patches.min_index().unwrap(), 2);
1✔
1124
        assert_eq!(patches.max_index().unwrap(), 8);
1✔
1125
    }
1✔
1126

1127
    #[test]
1128
    fn test_search_index() {
1✔
1129
        let patches = Patches::new(
1✔
1130
            10,
1131
            0,
1132
            buffer![2u64, 5, 8].into_array(),
1✔
1133
            buffer![100i32, 200, 300].into_array(),
1✔
1134
        );
1135

1136
        // Search for exact indices
1137
        assert_eq!(patches.search_index(2).unwrap(), SearchResult::Found(0));
1✔
1138
        assert_eq!(patches.search_index(5).unwrap(), SearchResult::Found(1));
1✔
1139
        assert_eq!(patches.search_index(8).unwrap(), SearchResult::Found(2));
1✔
1140

1141
        // Search for non-patch indices
1142
        assert_eq!(patches.search_index(0).unwrap(), SearchResult::NotFound(0));
1✔
1143
        assert_eq!(patches.search_index(3).unwrap(), SearchResult::NotFound(1));
1✔
1144
        assert_eq!(patches.search_index(6).unwrap(), SearchResult::NotFound(2));
1✔
1145
        assert_eq!(patches.search_index(9).unwrap(), SearchResult::NotFound(3));
1✔
1146
    }
1✔
1147

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

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

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

1180
        // Mask that removes all patches
1181
        let mask = Mask::from_iter([
1✔
1182
            false, false, true, false, false, true, false, false, true, false,
1✔
1183
        ]);
1✔
1184
        let masked = patches.mask(&mask).unwrap();
1✔
1185
        assert!(masked.is_none());
1✔
1186
    }
1✔
1187

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

1198
        // Mask that doesn't affect any patches
1199
        let mask = Mask::from_iter([
1✔
1200
            true, false, false, true, false, false, true, false, false, true,
1✔
1201
        ]);
1✔
1202
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1203

1204
        let indices = masked.indices().to_primitive().unwrap();
1✔
1205
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
1206
        let values = masked.values().to_primitive().unwrap();
1✔
1207
        assert_eq!(values.as_slice::<i32>(), &[100, 200, 300]);
1✔
1208
    }
1✔
1209

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

1220
        // Mask that removes the single patch
1221
        let mask = Mask::from_iter([false, false, true, false, false]);
1✔
1222
        let masked = patches.mask(&mask).unwrap();
1✔
1223
        assert!(masked.is_none());
1✔
1224

1225
        // Mask that keeps the single patch
1226
        let mask = Mask::from_iter([true, false, false, true, false]);
1✔
1227
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1228
        let indices = masked.indices().to_primitive().unwrap();
1✔
1229
        assert_eq!(indices.as_slice::<u64>(), &[2]);
1✔
1230
    }
1✔
1231

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

1242
        // Mask that removes middle patches
1243
        let mask = Mask::from_iter([
1✔
1244
            false, false, false, false, true, true, false, false, false, false,
1✔
1245
        ]);
1✔
1246
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1247

1248
        let indices = masked.indices().to_primitive().unwrap();
1✔
1249
        assert_eq!(indices.as_slice::<u64>(), &[3, 6]);
1✔
1250
        let values = masked.values().to_primitive().unwrap();
1✔
1251
        assert_eq!(values.as_slice::<i32>(), &[100, 400]);
1✔
1252
    }
1✔
1253

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

1264
        // Mask that removes the patch at actual index 2
1265
        let mask = Mask::from_iter([
1✔
1266
            false, false, true, false, false, false, false, false, false, false, false, false,
1✔
1267
            false, false, false, false, false, false, false, false,
1✔
1268
        ]);
1✔
1269
        let masked = patches.mask(&mask).unwrap().unwrap();
1✔
1270

1271
        let indices = masked.indices().to_primitive().unwrap();
1✔
1272
        assert_eq!(indices.as_slice::<u64>(), &[16, 19]);
1✔
1273
        let values = masked.values().to_primitive().unwrap();
1✔
1274
        assert_eq!(values.as_slice::<i32>(), &[100, 300]);
1✔
1275
    }
1✔
1276

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

1287
        // Mask with wrong length
1288
        let mask = Mask::from_iter([false, false, true, false, false]);
1✔
1289
        let _ = patches.mask(&mask).unwrap();
1✔
1290
    }
1✔
1291
}
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