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

vortex-data / vortex / 16605708121

29 Jul 2025 07:37PM UTC coverage: 83.004% (+0.3%) from 82.684%
16605708121

Pull #4055

github

web-flow
Merge d4d61ec22 into 6fb0f3e49
Pull Request #4055: [chore] cross-operation consistency tests

355 of 360 new or added lines in 22 files covered. (98.61%)

27 existing lines in 2 files now uncovered.

45688 of 55043 relevant lines covered (83.0%)

353098.6 hits per line

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

91.72
/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::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::BoolArray;
24
use crate::arrays::PrimitiveArray;
25
use crate::compute::mask;
26
use crate::compute::{cast, filter, take};
27
use crate::search_sorted::{SearchResult, SearchSorted, SearchSortedSide};
28
use crate::vtable::ValidityHelper;
29
use crate::{Array, ArrayRef, IntoArray, ToCanonical};
30

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

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

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

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

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

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

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

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

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

136
    pub fn array_len(&self) -> usize {
1,241,190✔
137
        self.array_len
1,241,190✔
138
    }
1,241,190✔
139

140
    pub fn num_patches(&self) -> usize {
9,007✔
141
        self.indices.len()
9,007✔
142
    }
9,007✔
143

144
    pub fn dtype(&self) -> &DType {
4,156✔
145
        self.values.dtype()
4,156✔
146
    }
4,156✔
147

148
    pub fn indices(&self) -> &ArrayRef {
39,207✔
149
        &self.indices
39,207✔
150
    }
39,207✔
151

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

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

160
    pub fn values(&self) -> &ArrayRef {
140,496✔
161
        &self.values
140,496✔
162
    }
140,496✔
163

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

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

172
    pub fn offset(&self) -> usize {
104,955✔
173
        self.offset
104,955✔
174
    }
104,955✔
175

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

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

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

212
    /// Get the patched value at a given index if it exists.
213
    pub fn get_patched(&self, index: usize) -> VortexResult<Option<Scalar>> {
1,939,810✔
214
        if let Some(patch_idx) = self.search_index(index)?.to_found() {
1,939,810✔
215
            self.values().scalar_at(patch_idx).map(Some)
94,784✔
216
        } else {
217
            Ok(None)
1,845,026✔
218
        }
219
    }
1,939,810✔
220

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

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

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

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

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

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

273
    /// Filter the patches by a mask, resulting in new patches for the filtered array.
274
    pub fn filter(&self, mask: &Mask) -> VortexResult<Option<Self>> {
4,322✔
275
        match mask.indices() {
4,322✔
276
            AllOr::All => Ok(Some(self.clone())),
1✔
277
            AllOr::None => Ok(None),
1✔
278
            AllOr::Some(mask_indices) => {
4,320✔
279
                let flat_indices = self.indices().to_primitive()?;
4,320✔
280
                match_each_unsigned_integer_ptype!(flat_indices.ptype(), |I| {
4,320✔
281
                    filter_patches_with_mask(
×
282
                        flat_indices.as_slice::<I>(),
×
283
                        self.offset(),
×
284
                        self.values(),
×
285
                        mask_indices,
×
286
                    )
287
                })
288
            }
289
        }
290
    }
4,322✔
291

292
    /// Mask the patches, setting patch values to null where the mask is true.
293
    /// Unlike filter, this preserves the patch indices.
294
    pub fn mask(&self, filter_mask: &Mask) -> VortexResult<Self> {
157✔
295
        // Get the patch indices as a primitive array
296
        let patch_indices = self.indices().to_primitive()?;
157✔
297

298
        // Create a mask for the patch values based on which indices are masked
299
        let mut patch_mask = Vec::with_capacity(patch_indices.len());
157✔
300

301
        // Check each patch index to see if it should be masked
302
        for i in 0..patch_indices.len() {
927✔
303
            let idx = patch_indices.scalar_at(i)?;
927✔
304
            let idx_usize = usize::try_from(&idx)?;
927✔
305

306
            // Subtract offset to get the actual array index
307
            let actual_idx = idx_usize - self.offset;
927✔
308

309
            // Check if this index is masked in the original mask
310
            let is_masked = match filter_mask.boolean_buffer() {
927✔
311
                AllOr::All => true,
3✔
312
                AllOr::None => false,
3✔
313
                AllOr::Some(buffer) => buffer.value(actual_idx),
921✔
314
            };
315
            patch_mask.push(is_masked);
927✔
316
        }
317

318
        // Create a mask for the patch values
319
        let patch_values_mask = Mask::try_from(&BoolArray::from_iter(patch_mask))?;
157✔
320

321
        // Apply the mask to patch values
322
        let masked_values = mask(self.values(), &patch_values_mask)?;
157✔
323

324
        Ok(Self::new_unchecked(
157✔
325
            self.array_len,
157✔
326
            self.offset,
157✔
327
            self.indices.clone(),
157✔
328
            masked_values,
157✔
329
        ))
157✔
330
    }
157✔
331

332
    /// Slice the patches by a range of the patched array.
333
    pub fn slice(&self, start: usize, stop: usize) -> VortexResult<Option<Self>> {
1,988✔
334
        let patch_start = self.search_index(start)?.to_index();
1,988✔
335
        let patch_stop = self.search_index(stop)?.to_index();
1,988✔
336

337
        if patch_start == patch_stop {
1,988✔
338
            return Ok(None);
547✔
339
        }
1,441✔
340

341
        // Slice out the values and indices
342
        let values = self.values().slice(patch_start, patch_stop)?;
1,441✔
343
        let indices = self.indices().slice(patch_start, patch_stop)?;
1,441✔
344

345
        Ok(Some(Self::new(
1,441✔
346
            stop - start,
1,441✔
347
            start + self.offset(),
1,441✔
348
            indices,
1,441✔
349
            values,
1,441✔
350
        )))
1,441✔
351
    }
1,988✔
352

353
    // https://docs.google.com/spreadsheets/d/1D9vBZ1QJ6mwcIvV5wIL0hjGgVchcEnAyhvitqWu2ugU
354
    const PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN: f64 = 5.0;
355

356
    fn is_map_faster_than_search(&self, take_indices: &PrimitiveArray) -> bool {
5,967✔
357
        (self.num_patches() as f64 / take_indices.len() as f64)
5,967✔
358
            < Self::PREFER_MAP_WHEN_PATCHES_OVER_INDICES_LESS_THAN
5,967✔
359
    }
5,967✔
360

361
    /// Take the indicies from the patches
362
    ///
363
    /// Any nulls in take_indices are added to the resulting patches.
364
    pub fn take_with_nulls(&self, take_indices: &dyn Array) -> VortexResult<Option<Self>> {
1,824✔
365
        if take_indices.is_empty() {
1,824✔
UNCOV
366
            return Ok(None);
×
367
        }
1,824✔
368

369
        let take_indices = take_indices.to_primitive()?;
1,824✔
370
        if self.is_map_faster_than_search(&take_indices) {
1,824✔
371
            self.take_map(take_indices, true)
1,786✔
372
        } else {
373
            self.take_search(take_indices, true)
38✔
374
        }
375
    }
1,824✔
376

377
    /// Take the indices from the patches.
378
    ///
379
    /// Any nulls in take_indices are ignored.
380
    pub fn take(&self, take_indices: &dyn Array) -> VortexResult<Option<Self>> {
4,143✔
381
        if take_indices.is_empty() {
4,143✔
UNCOV
382
            return Ok(None);
×
383
        }
4,143✔
384

385
        let take_indices = take_indices.to_primitive()?;
4,143✔
386
        if self.is_map_faster_than_search(&take_indices) {
4,143✔
387
            self.take_map(take_indices, false)
3,839✔
388
        } else {
389
            self.take_search(take_indices, false)
304✔
390
        }
391
    }
4,143✔
392

393
    pub fn take_search(
342✔
394
        &self,
342✔
395
        take_indices: PrimitiveArray,
342✔
396
        include_nulls: bool,
342✔
397
    ) -> VortexResult<Option<Self>> {
342✔
398
        let indices = self.indices.to_primitive()?;
342✔
399
        let new_length = take_indices.len();
342✔
400

401
        let Some((new_indices, values_indices)) =
266✔
402
            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
342✔
UNCOV
403
                match_each_integer_ptype!(take_indices.ptype(), |TakeIndices| {
×
UNCOV
404
                    take_search::<_, TakeIndices>(
×
UNCOV
405
                        indices.as_slice::<Indices>(),
×
UNCOV
406
                        take_indices,
×
UNCOV
407
                        self.offset(),
×
UNCOV
408
                        include_nulls,
×
UNCOV
409
                    )?
×
410
                })
411
            })
412
        else {
413
            return Ok(None);
76✔
414
        };
415

416
        Ok(Some(Self::new(
266✔
417
            new_length,
266✔
418
            0,
419
            new_indices,
266✔
420
            take(self.values(), &values_indices)?,
266✔
421
        )))
422
    }
342✔
423

424
    pub fn take_map(
5,625✔
425
        &self,
5,625✔
426
        take_indices: PrimitiveArray,
5,625✔
427
        include_nulls: bool,
5,625✔
428
    ) -> VortexResult<Option<Self>> {
5,625✔
429
        let indices = self.indices.to_primitive()?;
5,625✔
430
        let new_length = take_indices.len();
5,625✔
431

432
        let Some((new_sparse_indices, value_indices)) =
3,991✔
433
            match_each_unsigned_integer_ptype!(indices.ptype(), |Indices| {
5,625✔
434
                match_each_integer_ptype!(take_indices.ptype(), |TakeIndices| {
76✔
UNCOV
435
                    take_map::<_, TakeIndices>(
×
UNCOV
436
                        indices.as_slice::<Indices>(),
×
UNCOV
437
                        take_indices,
×
UNCOV
438
                        self.offset(),
×
UNCOV
439
                        self.min_index()?,
×
UNCOV
440
                        self.max_index()?,
×
UNCOV
441
                        include_nulls,
×
UNCOV
442
                    )?
×
443
                })
444
            })
445
        else {
446
            return Ok(None);
1,634✔
447
        };
448

449
        Ok(Some(Patches::new(
3,991✔
450
            new_length,
3,991✔
451
            0,
452
            new_sparse_indices,
3,991✔
453
            take(self.values(), &value_indices)?,
3,991✔
454
        )))
455
    }
5,625✔
456

457
    pub fn map_values<F>(self, f: F) -> VortexResult<Self>
16✔
458
    where
16✔
459
        F: FnOnce(ArrayRef) -> VortexResult<ArrayRef>,
16✔
460
    {
461
        let values = f(self.values)?;
16✔
462
        if self.indices.len() != values.len() {
16✔
UNCOV
463
            vortex_bail!(
×
UNCOV
464
                "map_values must preserve length: expected {} received {}",
×
UNCOV
465
                self.indices.len(),
×
UNCOV
466
                values.len()
×
467
            )
468
        }
16✔
469
        Ok(Self::new(self.array_len, self.offset, self.indices, values))
16✔
470
    }
16✔
471
}
472

473
fn take_search<I: NativePType + NumCast + PartialOrd, T: NativePType + NumCast>(
342✔
474
    indices: &[I],
342✔
475
    take_indices: PrimitiveArray,
342✔
476
    indices_offset: usize,
342✔
477
    include_nulls: bool,
342✔
478
) -> VortexResult<Option<(ArrayRef, ArrayRef)>>
342✔
479
where
342✔
480
    usize: TryFrom<T>,
342✔
481
    VortexError: From<<usize as TryFrom<T>>::Error>,
342✔
482
{
483
    let take_indices_validity = take_indices.validity();
342✔
484
    let indices_offset = I::from(indices_offset).vortex_expect("indices_offset out of range");
342✔
485

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

507
    if new_indices.is_empty() {
342✔
508
        return Ok(None);
76✔
509
    }
266✔
510

511
    let new_indices = new_indices.into_array();
266✔
512
    let values_validity = take_indices_validity.take(&new_indices)?;
266✔
513
    Ok(Some((
266✔
514
        new_indices,
266✔
515
        PrimitiveArray::new(values_indices, values_validity).into_array(),
266✔
516
    )))
266✔
517
}
342✔
518

519
fn take_map<I: NativePType + Hash + Eq + TryFrom<usize>, T: NativePType>(
5,625✔
520
    indices: &[I],
5,625✔
521
    take_indices: PrimitiveArray,
5,625✔
522
    indices_offset: usize,
5,625✔
523
    min_index: usize,
5,625✔
524
    max_index: usize,
5,625✔
525
    include_nulls: bool,
5,625✔
526
) -> VortexResult<Option<(ArrayRef, ArrayRef)>>
5,625✔
527
where
5,625✔
528
    usize: TryFrom<T>,
5,625✔
529
    VortexError: From<<I as TryFrom<usize>>::Error>,
5,625✔
530
{
531
    let take_indices_validity = take_indices.validity();
5,625✔
532
    let take_indices = take_indices.as_slice::<T>();
5,625✔
533
    let offset_i = I::try_from(indices_offset)?;
5,625✔
534

535
    let sparse_index_to_value_index: HashMap<I, usize> = indices
5,625✔
536
        .iter()
5,625✔
537
        .copied()
5,625✔
538
        .map(|idx| idx - offset_i)
16,647✔
539
        .enumerate()
5,625✔
540
        .map(|(value_index, sparse_index)| (sparse_index, value_index))
16,647✔
541
        .collect();
5,625✔
542

543
    let (new_sparse_indices, value_indices): (BufferMut<u64>, BufferMut<u64>) = take_indices
5,625✔
544
        .iter()
5,625✔
545
        .copied()
5,625✔
546
        .map(usize::try_from)
5,625✔
547
        .process_results(|iter| {
5,625✔
548
            iter.enumerate()
5,625✔
549
                .filter_map(|(idx_in_take, ti)| {
235,222✔
550
                    // If we have to take nulls the take index doesn't matter, make it 0 for consistency
551
                    if include_nulls && take_indices_validity.is_null(idx_in_take).vortex_unwrap() {
235,222✔
552
                        Some((idx_in_take as u64, 0))
342✔
553
                    } else if ti < min_index || ti > max_index {
234,880✔
554
                        None
52,099✔
555
                    } else {
556
                        sparse_index_to_value_index
182,781✔
557
                            .get(
182,781✔
558
                                &I::try_from(ti)
182,781✔
559
                                    .vortex_expect("take index is between min and max index"),
182,781✔
560
                            )
561
                            .map(|value_index| (idx_in_take as u64, *value_index as u64))
182,781✔
562
                    }
563
                })
235,222✔
564
                .unzip()
5,625✔
565
        })
5,625✔
566
        .map_err(|_| vortex_err!("Failed to convert index to usize"))?;
5,625✔
567

568
    if new_sparse_indices.is_empty() {
5,625✔
569
        return Ok(None);
1,634✔
570
    }
3,991✔
571

572
    let new_sparse_indices = new_sparse_indices.into_array();
3,991✔
573
    let values_validity = take_indices_validity.take(&new_sparse_indices)?;
3,991✔
574
    Ok(Some((
3,991✔
575
        new_sparse_indices,
3,991✔
576
        PrimitiveArray::new(value_indices, values_validity).into_array(),
3,991✔
577
    )))
3,991✔
578
}
5,625✔
579

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

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

600
    let mut mask_idx = 0usize;
4,320✔
601
    let mut true_idx = 0usize;
4,320✔
602

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

620
            if left_min > right_max {
326,540✔
621
                // Advance right side
622
                true_idx += STRIDE;
37,106✔
623
                continue;
37,106✔
624
            } else if right_min > left_max {
289,434✔
625
                mask_idx += STRIDE;
7,836✔
626
                continue;
7,836✔
627
            } else {
281,598✔
628
                // Fallthrough to direct comparison path.
281,598✔
629
            }
281,598✔
630
        }
132,150✔
631

632
        // Two-way sorted iterator merge:
633

634
        let left = patch_indices[mask_idx].to_usize().vortex_expect("left") - offset;
413,748✔
635
        let right = mask_indices[true_idx];
413,748✔
636

637
        match left.cmp(&right) {
413,748✔
638
            Ordering::Less => {
127,228✔
639
                mask_idx += 1;
127,228✔
640
            }
127,228✔
641
            Ordering::Greater => {
277,658✔
642
                true_idx += 1;
277,658✔
643
            }
277,658✔
644
            Ordering::Equal => {
8,862✔
645
                // Save the mask index as well as the positional index.
8,862✔
646
                new_mask_indices.push(mask_idx);
8,862✔
647
                new_patch_indices.push(true_idx as u64);
8,862✔
648

8,862✔
649
                mask_idx += 1;
8,862✔
650
                true_idx += 1;
8,862✔
651
            }
8,862✔
652
        }
653
    }
654

655
    if new_mask_indices.is_empty() {
4,320✔
656
        return Ok(None);
1,058✔
657
    }
3,262✔
658

659
    let new_patch_indices = new_patch_indices.into_array();
3,262✔
660
    let new_patch_values = filter(
3,262✔
661
        patch_values,
3,262✔
662
        &Mask::from_indices(patch_values.len(), new_mask_indices),
3,262✔
UNCOV
663
    )?;
×
664

665
    Ok(Some(Patches::new(
3,262✔
666
        true_count,
3,262✔
667
        0,
3,262✔
668
        new_patch_indices,
3,262✔
669
        new_patch_values,
3,262✔
670
    )))
3,262✔
671
}
4,320✔
672

673
#[cfg(test)]
674
mod test {
675
    use rstest::{fixture, rstest};
676
    use vortex_buffer::buffer;
677
    use vortex_mask::Mask;
678

679
    use crate::arrays::PrimitiveArray;
680
    use crate::patches::Patches;
681
    use crate::search_sorted::{SearchResult, SearchSortedSide};
682
    use crate::validity::Validity;
683
    use crate::{IntoArray, ToCanonical};
684

685
    #[test]
686
    fn test_filter() {
1✔
687
        let patches = Patches::new(
1✔
688
            100,
689
            0,
690
            buffer![10u32, 11, 20].into_array(),
1✔
691
            buffer![100, 110, 200].into_array(),
1✔
692
        );
693

694
        let filtered = patches
1✔
695
            .filter(&Mask::from_indices(100, vec![10, 20, 30]))
1✔
696
            .unwrap()
1✔
697
            .unwrap();
1✔
698

699
        let indices = filtered.indices().to_primitive().unwrap();
1✔
700
        let values = filtered.values().to_primitive().unwrap();
1✔
701
        assert_eq!(indices.as_slice::<u64>(), &[0, 1]);
1✔
702
        assert_eq!(values.as_slice::<i32>(), &[100, 200]);
1✔
703
    }
1✔
704

705
    #[fixture]
706
    fn patches() -> Patches {
707
        Patches::new(
708
            20,
709
            0,
710
            buffer![2u64, 9, 15].into_array(),
711
            PrimitiveArray::new(buffer![33_i32, 44, 55], Validity::AllValid).into_array(),
712
        )
713
    }
714

715
    #[rstest]
716
    fn search_larger_than(patches: Patches) {
717
        let res = patches.search_sorted(66, SearchSortedSide::Left).unwrap();
718
        assert_eq!(res, SearchResult::NotFound(16));
719
    }
720

721
    #[rstest]
722
    fn search_less_than(patches: Patches) {
723
        let res = patches.search_sorted(22, SearchSortedSide::Left).unwrap();
724
        assert_eq!(res, SearchResult::NotFound(2));
725
    }
726

727
    #[rstest]
728
    fn search_found(patches: Patches) {
729
        let res = patches.search_sorted(44, SearchSortedSide::Left).unwrap();
730
        assert_eq!(res, SearchResult::Found(9));
731
    }
732

733
    #[rstest]
734
    fn search_not_found_right(patches: Patches) {
735
        let res = patches.search_sorted(56, SearchSortedSide::Right).unwrap();
736
        assert_eq!(res, SearchResult::NotFound(16));
737
    }
738

739
    #[rstest]
740
    fn search_sliced(patches: Patches) {
741
        let sliced = patches.slice(7, 20).unwrap().unwrap();
742
        assert_eq!(
743
            sliced.search_sorted(22, SearchSortedSide::Left).unwrap(),
744
            SearchResult::NotFound(2)
745
        );
746
    }
747

748
    #[test]
749
    fn search_right() {
1✔
750
        let patches = Patches::new(
1✔
751
            6,
752
            0,
753
            buffer![0u8, 1, 4, 5].into_array(),
1✔
754
            buffer![-128i8, -98, 8, 50].into_array(),
1✔
755
        );
756

757
        assert_eq!(
1✔
758
            patches.search_sorted(-98, SearchSortedSide::Right).unwrap(),
1✔
759
            SearchResult::Found(2)
760
        );
761
        assert_eq!(
1✔
762
            patches.search_sorted(50, SearchSortedSide::Right).unwrap(),
1✔
763
            SearchResult::Found(6),
764
        );
765
        assert_eq!(
1✔
766
            patches.search_sorted(7, SearchSortedSide::Right).unwrap(),
1✔
767
            SearchResult::NotFound(2),
768
        );
769
        assert_eq!(
1✔
770
            patches.search_sorted(51, SearchSortedSide::Right).unwrap(),
1✔
771
            SearchResult::NotFound(6)
772
        );
773
    }
1✔
774

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

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

811
    #[test]
812
    fn test_slice() {
1✔
813
        let values = buffer![15_u32, 135, 13531, 42].into_array();
1✔
814
        let indices = buffer![10_u64, 11, 50, 100].into_array();
1✔
815

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

818
        let sliced = patches.slice(15, 100).unwrap().unwrap();
1✔
819
        assert_eq!(sliced.array_len(), 100 - 15);
1✔
820
        let primitive = sliced.values().to_primitive().unwrap();
1✔
821

822
        assert_eq!(primitive.as_slice::<u32>(), &[13531]);
1✔
823
    }
1✔
824

825
    #[test]
826
    fn doubly_sliced() {
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().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

838
        let doubly_sliced = sliced.slice(35, 36).unwrap().unwrap();
1✔
839
        let primitive_doubly_sliced = doubly_sliced.values().to_primitive().unwrap();
1✔
840

841
        assert_eq!(primitive_doubly_sliced.as_slice::<u32>(), &[13531]);
1✔
842
    }
1✔
843

844
    #[test]
845
    fn test_mask_all_true() {
1✔
846
        let patches = Patches::new(
1✔
847
            10,
848
            0,
849
            buffer![2u64, 5, 8].into_array(),
1✔
850
            buffer![100i32, 200, 300].into_array(),
1✔
851
        );
852

853
        let mask = Mask::new_true(10);
1✔
854
        let masked = patches.mask(&mask).unwrap();
1✔
855

856
        // All patch values should be masked (set to null)
857
        let masked_values = masked.values().to_primitive().unwrap();
1✔
858
        assert_eq!(masked_values.len(), 3);
1✔
859
        assert!(!masked_values.is_valid(0).unwrap());
1✔
860
        assert!(!masked_values.is_valid(1).unwrap());
1✔
861
        assert!(!masked_values.is_valid(2).unwrap());
1✔
862

863
        // Indices should remain unchanged
864
        let indices = masked.indices().to_primitive().unwrap();
1✔
865
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
866
    }
1✔
867

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

877
        let mask = Mask::new_false(10);
1✔
878
        let masked = patches.mask(&mask).unwrap();
1✔
879

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

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

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

901
        // Mask that sets indices 2 and 8 to null (but not 5)
902
        let mask = Mask::from_iter([
1✔
903
            false, false, true, false, false, false, false, false, true, false,
1✔
904
        ]);
1✔
905
        let masked = patches.mask(&mask).unwrap();
1✔
906

907
        // First and third patch values should be masked
908
        let masked_values = masked.values().to_primitive().unwrap();
1✔
909
        assert_eq!(masked_values.len(), 3);
1✔
910
        assert!(!masked_values.is_valid(0).unwrap()); // index 2 is masked
1✔
911
        assert!(masked_values.is_valid(1).unwrap()); // index 5 is not masked
1✔
912
        assert!(!masked_values.is_valid(2).unwrap()); // index 8 is masked
1✔
913

914
        // When valid, values should be unchanged
915
        assert_eq!(
1✔
916
            i32::try_from(&masked_values.scalar_at(1).unwrap()).unwrap(),
1✔
917
            200i32
918
        );
919

920
        // Indices should remain unchanged
921
        let indices = masked.indices().to_primitive().unwrap();
1✔
922
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
923
    }
1✔
924

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

934
        // Mask that sets actual index 2 to null
935
        let mask = Mask::from_iter([
1✔
936
            false, false, true, false, false, false, false, false, false, false,
1✔
937
        ]);
1✔
938
        let masked = patches.mask(&mask).unwrap();
1✔
939

940
        // First patch value should be masked
941
        let masked_values = masked.values().to_primitive().unwrap();
1✔
942
        assert!(!masked_values.is_valid(0).unwrap()); // index 2 is masked
1✔
943
        assert!(masked_values.is_valid(1).unwrap()); // index 5 is not masked
1✔
944
        assert!(masked_values.is_valid(2).unwrap()); // index 8 is not masked
1✔
945
    }
1✔
946

947
    #[test]
948
    fn test_filter_keep_all() {
1✔
949
        let patches = Patches::new(
1✔
950
            10,
951
            0,
952
            buffer![2u64, 5, 8].into_array(),
1✔
953
            buffer![100i32, 200, 300].into_array(),
1✔
954
        );
955

956
        // Keep all indices (mask with indices 0-9)
957
        let mask = Mask::from_indices(10, (0..10).collect());
1✔
958
        let filtered = patches.filter(&mask).unwrap().unwrap();
1✔
959

960
        let indices = filtered.indices().to_primitive().unwrap();
1✔
961
        let values = filtered.values().to_primitive().unwrap();
1✔
962
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
963
        assert_eq!(values.as_slice::<i32>(), &[100, 200, 300]);
1✔
964
    }
1✔
965

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

975
        // Filter out all (empty mask means keep nothing)
976
        let mask = Mask::from_indices(10, vec![]);
1✔
977
        let filtered = patches.filter(&mask).unwrap();
1✔
978
        assert!(filtered.is_none());
1✔
979
    }
1✔
980

981
    #[test]
982
    fn test_filter_with_indices() {
1✔
983
        let patches = Patches::new(
1✔
984
            10,
985
            0,
986
            buffer![2u64, 5, 8].into_array(),
1✔
987
            buffer![100i32, 200, 300].into_array(),
1✔
988
        );
989

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

994
        let indices = filtered.indices().to_primitive().unwrap();
1✔
995
        let values = filtered.values().to_primitive().unwrap();
1✔
996
        assert_eq!(indices.as_slice::<u64>(), &[0, 1]); // Adjusted indices
1✔
997
        assert_eq!(values.as_slice::<i32>(), &[100, 200]);
1✔
998
    }
1✔
999

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

1009
        let sliced = patches.slice(0, 10).unwrap().unwrap();
1✔
1010

1011
        let indices = sliced.indices().to_primitive().unwrap();
1✔
1012
        let values = sliced.values().to_primitive().unwrap();
1✔
1013
        assert_eq!(indices.as_slice::<u64>(), &[2, 5, 8]);
1✔
1014
        assert_eq!(values.as_slice::<i32>(), &[100, 200, 300]);
1✔
1015
    }
1✔
1016

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

1026
        // Slice from 3 to 8 (includes patch at 5)
1027
        let sliced = patches.slice(3, 8).unwrap().unwrap();
1✔
1028

1029
        let indices = sliced.indices().to_primitive().unwrap();
1✔
1030
        let values = sliced.values().to_primitive().unwrap();
1✔
1031
        assert_eq!(indices.as_slice::<u64>(), &[5]); // Index stays the same
1✔
1032
        assert_eq!(values.as_slice::<i32>(), &[200]);
1✔
1033
        assert_eq!(sliced.array_len(), 5); // 8 - 3 = 5
1✔
1034
        assert_eq!(sliced.offset(), 3); // New offset
1✔
1035
    }
1✔
1036

1037
    #[test]
1038
    fn test_slice_no_patches() {
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 6 to 7 (no patches in this range)
1047
        let sliced = patches.slice(6, 7).unwrap();
1✔
1048
        assert!(sliced.is_none());
1✔
1049
    }
1✔
1050

1051
    #[test]
1052
    fn test_slice_with_offset() {
1✔
1053
        let patches = Patches::new(
1✔
1054
            10,
1055
            5,                                  // offset
1056
            buffer![7u64, 10, 13].into_array(), // actual indices are 2, 5, 8
1✔
1057
            buffer![100i32, 200, 300].into_array(),
1✔
1058
        );
1059

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

1063
        let indices = sliced.indices().to_primitive().unwrap();
1✔
1064
        let values = sliced.values().to_primitive().unwrap();
1✔
1065
        assert_eq!(indices.as_slice::<u64>(), &[10]); // Index stays the same (offset + 5 = 10)
1✔
1066
        assert_eq!(values.as_slice::<i32>(), &[200]);
1✔
1067
        assert_eq!(sliced.offset(), 8); // New offset = 5 + 3
1✔
1068
    }
1✔
1069

1070
    #[test]
1071
    fn test_patch_values() {
1✔
1072
        let patches = Patches::new(
1✔
1073
            10,
1074
            0,
1075
            buffer![2u64, 5, 8].into_array(),
1✔
1076
            buffer![100i32, 200, 300].into_array(),
1✔
1077
        );
1078

1079
        let values = patches.values().to_primitive().unwrap();
1✔
1080
        assert_eq!(
1✔
1081
            i32::try_from(&values.scalar_at(0).unwrap()).unwrap(),
1✔
1082
            100i32
1083
        );
1084
        assert_eq!(
1✔
1085
            i32::try_from(&values.scalar_at(1).unwrap()).unwrap(),
1✔
1086
            200i32
1087
        );
1088
        assert_eq!(
1✔
1089
            i32::try_from(&values.scalar_at(2).unwrap()).unwrap(),
1✔
1090
            300i32
1091
        );
1092
    }
1✔
1093

1094
    #[test]
1095
    fn test_indices_range() {
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
        assert_eq!(patches.min_index().unwrap(), 2);
1✔
1104
        assert_eq!(patches.max_index().unwrap(), 8);
1✔
1105
    }
1✔
1106

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

1116
        // Search for exact indices
1117
        assert_eq!(patches.search_index(2).unwrap(), SearchResult::Found(0));
1✔
1118
        assert_eq!(patches.search_index(5).unwrap(), SearchResult::Found(1));
1✔
1119
        assert_eq!(patches.search_index(8).unwrap(), SearchResult::Found(2));
1✔
1120

1121
        // Search for non-patch indices
1122
        assert_eq!(patches.search_index(0).unwrap(), SearchResult::NotFound(0));
1✔
1123
        assert_eq!(patches.search_index(3).unwrap(), SearchResult::NotFound(1));
1✔
1124
        assert_eq!(patches.search_index(6).unwrap(), SearchResult::NotFound(2));
1✔
1125
        assert_eq!(patches.search_index(9).unwrap(), SearchResult::NotFound(3));
1✔
1126
    }
1✔
1127

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

1137
        // Test masking nullable values
1138
        let mask = Mask::from_iter([
1✔
1139
            false, false, true, false, false, false, false, false, false, false,
1✔
1140
        ]);
1✔
1141
        let masked = patches.mask(&mask).unwrap();
1✔
1142

1143
        let masked_values = masked.values().to_primitive().unwrap();
1✔
1144
        assert!(!masked_values.is_valid(0).unwrap()); // index 2 is masked
1✔
1145
        assert!(!masked_values.is_valid(1).unwrap()); // was already null
1✔
1146
        assert!(masked_values.is_valid(2).unwrap()); // index 8 is not masked
1✔
1147
        assert_eq!(
1✔
1148
            i32::try_from(&masked_values.scalar_at(2).unwrap()).unwrap(),
1✔
1149
            300i32
1150
        );
1151
    }
1✔
1152
}
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