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

vortex-data / vortex / 16629653619

30 Jul 2025 05:36PM UTC coverage: 83.032% (+0.3%) from 82.767%
16629653619

push

github

web-flow
[chore] cross-operation consistency tests (#4055)

⏺ Summary of Consistency Test Improvements

### Overview

We've made comprehensive improvements to Vortex array compute
conformance tests, fixing critical bugs and adding extensive test
coverage across 20+ encoding types.

  Key Changes

  1. Fixed ALP Array Double Mask Bug
- Issue: mask(mask(array, mask1), mask2) ≠ mask(array, mask1 | mask2)
for ALP arrays with patches
- Root cause: Patches were using filter (adjusting indices) instead of
mask (preserving indices)
- Solution: Implemented Patches::mask() method that preserves indices
while masking values
  2. Added Consistency Tests for All Major Encodings
    - Added test_array_consistency() test suites to 20+ encodings:
- Core arrays: Bool, Primitive, Constant, Null, Struct, List, Extension
      - Variable-length: VarBin, VarBinView
      - Compressed: ALP, ALP-RD, BitPacked, FoR, FSST, Dict, RunEnd
- Specialized: Sparse, Chunked, Sequence, DecimalByteParts,
DateTimeParts
    - Each encoding now has 5-15 test cases covering different scenarios
  3. Implemented Missing Compute Kernels
- DecimalArray: Added CastKernel for nullable/non-nullable conversions
- ExtensionArray: Added MaskKernel to avoid Arrow conversion failures
    - DateTimePartsArray: Added MaskKernel
    - DecimalByteParts: Added TakeKernel and MaskKernel
  4. Fixed Consistency Test Framework
    - Made test_array_consistency() public for use across crates
    - Fixed mask combination logic: changed from AND to OR operation
- Fixed sparse array tests to use nullable arrays when using null fill
values
    - Removed string test from RunEnd (doesn't support strings)
  5. Architectural Improvements
- Moved patch masking logic from ALP-specific code to reusable
Patches::mask()
    - Added comprehensive tests for Patches struct operations (17 tests)
- Improved cast implementations to use validity.cast_nullability() for
cleaner code

  Impact

  - All consistency tests now pass across the en... (continued)

490 of 547 new or added lines in 23 files covered. (89.58%)

8 existing lines in 1 file now uncovered.

45902 of 55282 relevant lines covered (83.03%)

352110.25 hits per line

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

83.67
/vortex-array/src/compute/conformance/consistency.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
//! # Array Consistency Tests
5
//!
6
//! This module contains tests that verify consistency between related compute operations
7
//! on Vortex arrays. These tests ensure that different ways of achieving the same result
8
//! produce identical outputs.
9
//!
10
//! ## Test Categories
11
//!
12
//! - **Filter/Take Consistency**: Verifies that filtering with a mask produces the same
13
//!   result as taking with the indices where the mask is true.
14
//! - **Mask Composition**: Ensures that applying multiple masks sequentially produces
15
//!   the same result as applying a combined mask.
16
//! - **Identity Operations**: Tests that operations with identity inputs (all-true masks,
17
//!   sequential indices) preserve the original array.
18
//! - **Null Handling**: Verifies consistent behavior when operations introduce or
19
//!   interact with null values.
20
//! - **Edge Cases**: Tests empty arrays, single elements, and boundary conditions.
21

22
use vortex_error::VortexUnwrap;
23
use vortex_mask::Mask;
24

25
use crate::arrays::{BoolArray, PrimitiveArray};
26
use crate::compute::{filter, mask, take};
27
use crate::{Array, IntoArray};
28

29
/// Tests that filter and take operations produce consistent results.
30
///
31
/// # Invariant
32
/// `filter(array, mask)` should equal `take(array, indices_where_mask_is_true)`
33
///
34
/// # Test Details
35
/// - Creates a mask that keeps elements where index % 3 != 1
36
/// - Applies filter with this mask
37
/// - Creates indices array containing positions where mask is true
38
/// - Applies take with these indices
39
/// - Verifies both results are identical
40
pub fn test_filter_take_consistency(array: &dyn Array) {
3,651✔
41
    let len = array.len();
3,651✔
42
    if len == 0 {
3,651✔
43
        return;
1✔
44
    }
3,650✔
45

46
    // Create a test mask (keep elements where index % 3 != 1)
47
    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 3 != 1).collect();
1,373,272✔
48
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern.clone())).vortex_unwrap();
3,650✔
49

50
    // Filter the array
51
    let filtered = filter(array, &mask).vortex_unwrap();
3,650✔
52

53
    // Create indices where mask is true
54
    let indices: Vec<u64> = mask_pattern
3,650✔
55
        .iter()
3,650✔
56
        .enumerate()
3,650✔
57
        .filter_map(|(i, &v)| v.then_some(i as u64))
1,373,272✔
58
        .collect();
3,650✔
59
    let indices_array = PrimitiveArray::from_iter(indices).into_array();
3,650✔
60

61
    // Take using those indices
62
    let taken = take(array, &indices_array).vortex_unwrap();
3,650✔
63

64
    // Results should be identical
65
    assert_eq!(
3,650✔
66
        filtered.len(),
3,650✔
67
        taken.len(),
3,650✔
NEW
68
        "Filter and take should produce arrays of the same length. \
×
NEW
69
         Filtered length: {}, Taken length: {}",
×
NEW
70
        filtered.len(),
×
NEW
71
        taken.len()
×
72
    );
73

74
    for i in 0..filtered.len() {
915,065✔
75
        let filtered_val = filtered.scalar_at(i).vortex_unwrap();
915,065✔
76
        let taken_val = taken.scalar_at(i).vortex_unwrap();
915,065✔
77
        assert_eq!(
915,065✔
78
            filtered_val, taken_val,
NEW
79
            "Filter and take produced different values at index {i}. \
×
NEW
80
             Filtered value: {filtered_val:?}, Taken value: {taken_val:?}"
×
81
        );
82
    }
83
}
3,651✔
84

85
/// Tests that double masking is consistent with combined mask.
86
///
87
/// # Invariant
88
/// `mask(mask(array, mask1), mask2)` should equal `mask(array, mask1 | mask2)`
89
///
90
/// # Test Details
91
/// - Creates two masks: mask1 (every 3rd element) and mask2 (every 2nd element)
92
/// - Applies masks sequentially: first mask1, then mask2 on the result
93
/// - Creates a combined mask using OR operation (element is masked if either mask is true)
94
/// - Applies the combined mask directly to the original array
95
/// - Verifies both approaches produce identical results
96
///
97
/// # Why This Matters
98
/// This test ensures that mask operations compose correctly, which is critical for
99
/// complex query operations that may apply multiple filters.
100
pub fn test_double_mask_consistency(array: &dyn Array) {
3,651✔
101
    let len = array.len();
3,651✔
102
    if len == 0 {
3,651✔
103
        return;
1✔
104
    }
3,650✔
105

106
    // Create two different mask patterns
107
    let mask1_pattern: Vec<bool> = (0..len).map(|i| i % 3 == 0).collect();
1,373,272✔
108
    let mask2_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
1,373,272✔
109

110
    let mask1 = Mask::try_from(&BoolArray::from_iter(mask1_pattern.clone())).vortex_unwrap();
3,650✔
111
    let mask2 = Mask::try_from(&BoolArray::from_iter(mask2_pattern.clone())).vortex_unwrap();
3,650✔
112

113
    // Apply masks sequentially
114
    let first_masked = mask(array, &mask1).vortex_unwrap();
3,650✔
115
    let double_masked = mask(&first_masked, &mask2).vortex_unwrap();
3,650✔
116

117
    // Create combined mask (OR operation - element is masked if EITHER mask is true)
118
    let combined_pattern: Vec<bool> = mask1_pattern
3,650✔
119
        .iter()
3,650✔
120
        .zip(mask2_pattern.iter())
3,650✔
121
        .map(|(&a, &b)| a || b)
1,373,272✔
122
        .collect();
3,650✔
123
    let combined_mask = Mask::try_from(&BoolArray::from_iter(combined_pattern)).vortex_unwrap();
3,650✔
124

125
    // Apply combined mask directly
126
    let directly_masked = mask(array, &combined_mask).vortex_unwrap();
3,650✔
127

128
    // Results should be identical
129
    assert_eq!(
3,650✔
130
        double_masked.len(),
3,650✔
131
        directly_masked.len(),
3,650✔
NEW
132
        "Sequential masking and combined masking should produce arrays of the same length. \
×
NEW
133
         Sequential length: {}, Combined length: {}",
×
NEW
134
        double_masked.len(),
×
NEW
135
        directly_masked.len()
×
136
    );
137

138
    for i in 0..double_masked.len() {
1,373,272✔
139
        let double_val = double_masked.scalar_at(i).vortex_unwrap();
1,373,272✔
140
        let direct_val = directly_masked.scalar_at(i).vortex_unwrap();
1,373,272✔
141
        assert_eq!(
1,373,272✔
142
            double_val, direct_val,
NEW
143
            "Sequential masking and combined masking produced different values at index {i}. \
×
NEW
144
             Sequential masking value: {double_val:?}, Combined masking value: {direct_val:?}\n\
×
NEW
145
             This likely indicates an issue with how masks are composed in the array implementation."
×
146
        );
147
    }
148
}
3,651✔
149

150
/// Tests that filtering with an all-true mask preserves the array.
151
///
152
/// # Invariant
153
/// `filter(array, all_true_mask)` should equal `array`
154
///
155
/// # Test Details
156
/// - Creates a mask with all elements set to true
157
/// - Applies filter with this mask
158
/// - Verifies the result is identical to the original array
159
///
160
/// # Why This Matters
161
/// This is an identity operation that should be optimized in implementations
162
/// to avoid unnecessary copying.
163
pub fn test_filter_identity(array: &dyn Array) {
3,651✔
164
    let len = array.len();
3,651✔
165
    if len == 0 {
3,651✔
166
        return;
1✔
167
    }
3,650✔
168

169
    let all_true_mask = Mask::new_true(len);
3,650✔
170
    let filtered = filter(array, &all_true_mask).vortex_unwrap();
3,650✔
171

172
    // Filtered array should be identical to original
173
    assert_eq!(
3,650✔
174
        filtered.len(),
3,650✔
175
        array.len(),
3,650✔
NEW
176
        "Filtering with all-true mask should preserve array length. \
×
NEW
177
         Original length: {}, Filtered length: {}",
×
NEW
178
        array.len(),
×
NEW
179
        filtered.len()
×
180
    );
181

182
    for i in 0..len {
1,373,272✔
183
        let original_val = array.scalar_at(i).vortex_unwrap();
1,373,272✔
184
        let filtered_val = filtered.scalar_at(i).vortex_unwrap();
1,373,272✔
185
        assert_eq!(
1,373,272✔
186
            filtered_val, original_val,
NEW
187
            "Filtering with all-true mask should preserve all values. \
×
NEW
188
             Value at index {i} changed from {original_val:?} to {filtered_val:?}"
×
189
        );
190
    }
191
}
3,651✔
192

193
/// Tests that masking with an all-false mask preserves values while making them nullable.
194
///
195
/// # Invariant
196
/// `mask(array, all_false_mask)` should have same values as `array` but with nullable type
197
///
198
/// # Test Details
199
/// - Creates a mask with all elements set to false (no elements are nullified)
200
/// - Applies mask operation
201
/// - Verifies all values are preserved but the array type becomes nullable
202
///
203
/// # Why This Matters
204
/// Masking always produces a nullable array, even when no values are actually masked.
205
/// This test ensures the type system handles this correctly.
206
pub fn test_mask_identity(array: &dyn Array) {
3,651✔
207
    let len = array.len();
3,651✔
208
    if len == 0 {
3,651✔
209
        return;
1✔
210
    }
3,650✔
211

212
    let all_false_mask = Mask::new_false(len);
3,650✔
213
    let masked = mask(array, &all_false_mask).vortex_unwrap();
3,650✔
214

215
    // Masked array should have same values (just nullable)
216
    assert_eq!(
3,650✔
217
        masked.len(),
3,650✔
218
        array.len(),
3,650✔
NEW
219
        "Masking with all-false mask should preserve array length. \
×
NEW
220
         Original length: {}, Masked length: {}",
×
NEW
221
        array.len(),
×
NEW
222
        masked.len()
×
223
    );
224

225
    assert!(
3,650✔
226
        masked.dtype().is_nullable(),
3,650✔
NEW
227
        "Mask operation should always produce a nullable array, but dtype is {:?}",
×
NEW
228
        masked.dtype()
×
229
    );
230

231
    for i in 0..len {
1,373,272✔
232
        let original_val = array.scalar_at(i).vortex_unwrap();
1,373,272✔
233
        let masked_val = masked.scalar_at(i).vortex_unwrap();
1,373,272✔
234
        let expected_val = original_val.clone().into_nullable();
1,373,272✔
235
        assert_eq!(
1,373,272✔
236
            masked_val, expected_val,
NEW
237
            "Masking with all-false mask should preserve values (as nullable). \
×
NEW
238
             Value at index {i}: original = {original_val:?}, masked = {masked_val:?}, expected = {expected_val:?}"
×
239
        );
240
    }
241
}
3,651✔
242

243
/// Tests that slice and filter with contiguous mask produce same results.
244
///
245
/// # Invariant
246
/// `filter(array, contiguous_true_mask)` should equal `slice(array, start, end)`
247
///
248
/// # Test Details
249
/// - Creates a mask that is true only for indices 1, 2, and 3
250
/// - Filters the array with this mask
251
/// - Slices the array from index 1 to 4
252
/// - Verifies both operations produce identical results
253
///
254
/// # Why This Matters
255
/// When a filter mask represents a contiguous range, it should be equivalent to
256
/// a slice operation. Some implementations may optimize this case.
257
pub fn test_slice_filter_consistency(array: &dyn Array) {
3,651✔
258
    let len = array.len();
3,651✔
259
    if len < 4 {
3,651✔
260
        return; // Need at least 4 elements for meaningful test
587✔
261
    }
3,064✔
262

263
    // Create a contiguous mask (true from index 1 to 3)
264
    let mut mask_pattern = vec![false; len];
3,064✔
265
    mask_pattern[1..4.min(len)].fill(true);
3,064✔
266

267
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
3,064✔
268
    let filtered = filter(array, &mask).vortex_unwrap();
3,064✔
269

270
    // Slice should produce the same result
271
    let sliced = array.slice(1, 4.min(len)).vortex_unwrap();
3,064✔
272

273
    assert_eq!(
3,064✔
274
        filtered.len(),
3,064✔
275
        sliced.len(),
3,064✔
NEW
276
        "Filter with contiguous mask and slice should produce same length. \
×
NEW
277
         Filtered length: {}, Sliced length: {}",
×
NEW
278
        filtered.len(),
×
NEW
279
        sliced.len()
×
280
    );
281

282
    for i in 0..filtered.len() {
9,192✔
283
        let filtered_val = filtered.scalar_at(i).vortex_unwrap();
9,192✔
284
        let sliced_val = sliced.scalar_at(i).vortex_unwrap();
9,192✔
285
        assert_eq!(
9,192✔
286
            filtered_val, sliced_val,
NEW
287
            "Filter with contiguous mask and slice produced different values at index {i}. \
×
NEW
288
             Filtered value: {filtered_val:?}, Sliced value: {sliced_val:?}"
×
289
        );
290
    }
291
}
3,651✔
292

293
/// Tests that take with sequential indices equals slice.
294
///
295
/// # Invariant
296
/// `take(array, [1, 2, 3, ...])` should equal `slice(array, 1, n)`
297
///
298
/// # Test Details
299
/// - Creates indices array with sequential values [1, 2, 3]
300
/// - Takes elements at these indices
301
/// - Slices array from index 1 to 4
302
/// - Verifies both operations produce identical results
303
///
304
/// # Why This Matters
305
/// Sequential takes are a common pattern that can be optimized to slice operations.
306
pub fn test_take_slice_consistency(array: &dyn Array) {
3,651✔
307
    let len = array.len();
3,651✔
308
    if len < 3 {
3,651✔
309
        return; // Need at least 3 elements
433✔
310
    }
3,218✔
311

312
    // Take indices [1, 2, 3]
313
    let end = 4.min(len);
3,218✔
314
    let indices = PrimitiveArray::from_iter((1..end).map(|i| i as u64)).into_array();
9,500✔
315
    let taken = take(array, &indices).vortex_unwrap();
3,218✔
316

317
    // Slice from 1 to end
318
    let sliced = array.slice(1, end).vortex_unwrap();
3,218✔
319

320
    assert_eq!(
3,218✔
321
        taken.len(),
3,218✔
322
        sliced.len(),
3,218✔
NEW
323
        "Take with sequential indices and slice should produce same length. \
×
NEW
324
         Taken length: {}, Sliced length: {}",
×
NEW
325
        taken.len(),
×
NEW
326
        sliced.len()
×
327
    );
328

329
    for i in 0..taken.len() {
9,500✔
330
        let taken_val = taken.scalar_at(i).vortex_unwrap();
9,500✔
331
        let sliced_val = sliced.scalar_at(i).vortex_unwrap();
9,500✔
332
        assert_eq!(
9,500✔
333
            taken_val, sliced_val,
NEW
334
            "Take with sequential indices and slice produced different values at index {i}. \
×
NEW
335
             Taken value: {taken_val:?}, Sliced value: {sliced_val:?}"
×
336
        );
337
    }
338
}
3,651✔
339

340
/// Tests that filter preserves relative ordering
341
pub fn test_filter_preserves_order(array: &dyn Array) {
3,651✔
342
    let len = array.len();
3,651✔
343
    if len < 4 {
3,651✔
344
        return;
587✔
345
    }
3,064✔
346

347
    // Create a mask that selects elements at indices 0, 2, 3
348
    let mask_pattern: Vec<bool> = (0..len).map(|i| i == 0 || i == 2 || i == 3).collect();
1,372,337✔
349
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
3,064✔
350

351
    let filtered = filter(array, &mask).vortex_unwrap();
3,064✔
352

353
    // Verify the filtered array contains the right elements in order
354
    assert_eq!(filtered.len(), 3.min(len));
3,064✔
355
    if len >= 4 {
3,064✔
356
        assert_eq!(
3,064✔
357
            filtered.scalar_at(0).vortex_unwrap(),
3,064✔
358
            array.scalar_at(0).vortex_unwrap()
3,064✔
359
        );
360
        assert_eq!(
3,064✔
361
            filtered.scalar_at(1).vortex_unwrap(),
3,064✔
362
            array.scalar_at(2).vortex_unwrap()
3,064✔
363
        );
364
        assert_eq!(
3,064✔
365
            filtered.scalar_at(2).vortex_unwrap(),
3,064✔
366
            array.scalar_at(3).vortex_unwrap()
3,064✔
367
        );
368
    }
×
369
}
3,651✔
370

371
/// Tests that take with repeated indices works correctly
372
pub fn test_take_repeated_indices(array: &dyn Array) {
3,651✔
373
    let len = array.len();
3,651✔
374
    if len == 0 {
3,651✔
375
        return;
1✔
376
    }
3,650✔
377

378
    // Take the first element three times
379
    let indices = PrimitiveArray::from_iter([0u64, 0, 0]).into_array();
3,650✔
380
    let taken = take(array, &indices).vortex_unwrap();
3,650✔
381

382
    assert_eq!(taken.len(), 3);
3,650✔
383
    for i in 0..3 {
14,600✔
384
        assert_eq!(
10,950✔
385
            taken.scalar_at(i).vortex_unwrap(),
10,950✔
386
            array.scalar_at(0).vortex_unwrap()
10,950✔
387
        );
388
    }
389
}
3,651✔
390

391
/// Tests mask and filter interaction with nulls
392
pub fn test_mask_filter_null_consistency(array: &dyn Array) {
3,651✔
393
    let len = array.len();
3,651✔
394
    if len < 3 {
3,651✔
395
        return;
433✔
396
    }
3,218✔
397

398
    // First mask some elements
399
    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
1,372,799✔
400
    let mask_array = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
3,218✔
401
    let masked = mask(array, &mask_array).vortex_unwrap();
3,218✔
402

403
    // Then filter to remove the nulls
404
    let filter_pattern: Vec<bool> = (0..len).map(|i| i % 2 != 0).collect();
1,372,799✔
405
    let filter_mask = Mask::try_from(&BoolArray::from_iter(filter_pattern)).vortex_unwrap();
3,218✔
406
    let filtered = filter(&masked, &filter_mask).vortex_unwrap();
3,218✔
407

408
    // This should be equivalent to directly filtering the original array
409
    let direct_filtered = filter(array, &filter_mask).vortex_unwrap();
3,218✔
410

411
    assert_eq!(filtered.len(), direct_filtered.len());
3,218✔
412
    for i in 0..filtered.len() {
685,334✔
413
        assert_eq!(
685,334✔
414
            filtered.scalar_at(i).vortex_unwrap(),
685,334✔
415
            direct_filtered.scalar_at(i).vortex_unwrap()
685,334✔
416
        );
417
    }
418
}
3,651✔
419

420
/// Tests that empty operations are consistent
421
pub fn test_empty_operations_consistency(array: &dyn Array) {
3,651✔
422
    let len = array.len();
3,651✔
423

424
    // Empty filter
425
    let empty_filter = filter(array, &Mask::new_false(len)).vortex_unwrap();
3,651✔
426
    assert_eq!(empty_filter.len(), 0);
3,651✔
427
    assert_eq!(empty_filter.dtype(), array.dtype());
3,651✔
428

429
    // Empty take
430
    let empty_indices =
3,651✔
431
        PrimitiveArray::empty::<u64>(vortex_dtype::Nullability::NonNullable).into_array();
3,651✔
432
    let empty_take = take(array, &empty_indices).vortex_unwrap();
3,651✔
433
    assert_eq!(empty_take.len(), 0);
3,651✔
434
    assert_eq!(empty_take.dtype(), array.dtype());
3,651✔
435

436
    // Empty slice (if array is non-empty)
437
    if len > 0 {
3,651✔
438
        let empty_slice = array.slice(0, 0).vortex_unwrap();
3,650✔
439
        assert_eq!(empty_slice.len(), 0);
3,650✔
440
        assert_eq!(empty_slice.dtype(), array.dtype());
3,650✔
441
    }
1✔
442
}
3,651✔
443

444
/// Tests that take preserves array properties
445
pub fn test_take_preserves_properties(array: &dyn Array) {
3,651✔
446
    let len = array.len();
3,651✔
447
    if len == 0 {
3,651✔
448
        return;
1✔
449
    }
3,650✔
450

451
    // Take all elements in original order
452
    let indices = PrimitiveArray::from_iter((0..len).map(|i| i as u64)).into_array();
1,373,272✔
453
    let taken = take(array, &indices).vortex_unwrap();
3,650✔
454

455
    // Should be identical to original
456
    assert_eq!(taken.len(), array.len());
3,650✔
457
    assert_eq!(taken.dtype(), array.dtype());
3,650✔
458
    for i in 0..len {
1,373,272✔
459
        assert_eq!(
1,373,272✔
460
            taken.scalar_at(i).vortex_unwrap(),
1,373,272✔
461
            array.scalar_at(i).vortex_unwrap()
1,373,272✔
462
        );
463
    }
464
}
3,651✔
465

466
/// Tests consistency with nullable indices.
467
///
468
/// # Invariant
469
/// `take(array, [Some(0), None, Some(2)])` should produce `[array[0], null, array[2]]`
470
///
471
/// # Test Details
472
/// - Creates an indices array with null at position 1: `[Some(0), None, Some(2)]`
473
/// - Takes elements using these indices
474
/// - Verifies that:
475
///   - Position 0 contains the value from array index 0
476
///   - Position 1 contains null
477
///   - Position 2 contains the value from array index 2
478
///   - The result array has nullable type
479
///
480
/// # Why This Matters
481
/// Nullable indices are a powerful feature that allows introducing nulls during
482
/// a take operation, which is useful for outer joins and similar operations.
483
pub fn test_nullable_indices_consistency(array: &dyn Array) {
3,651✔
484
    let len = array.len();
3,651✔
485
    if len < 3 {
3,651✔
486
        return; // Need at least 3 elements to test indices 0 and 2
433✔
487
    }
3,218✔
488

489
    // Create nullable indices where some indices are null
490
    let indices = PrimitiveArray::from_option_iter([Some(0u64), None, Some(2u64)]).into_array();
3,218✔
491

492
    let taken = take(array, &indices).vortex_unwrap();
3,218✔
493

494
    // Result should have nulls where indices were null
495
    assert_eq!(
3,218✔
496
        taken.len(),
3,218✔
497
        3,
NEW
498
        "Take with nullable indices should produce array of length 3, got {}",
×
NEW
499
        taken.len()
×
500
    );
501

502
    assert!(
3,218✔
503
        taken.dtype().is_nullable(),
3,218✔
NEW
504
        "Take with nullable indices should produce nullable array, but dtype is {:?}",
×
NEW
505
        taken.dtype()
×
506
    );
507

508
    // Check first element (from index 0)
509
    let expected_0 = array.scalar_at(0).vortex_unwrap().into_nullable();
3,218✔
510
    let actual_0 = taken.scalar_at(0).vortex_unwrap();
3,218✔
511
    assert_eq!(
3,218✔
512
        actual_0, expected_0,
NEW
513
        "Take with nullable indices: element at position 0 should be from array index 0. \
×
NEW
514
         Expected: {expected_0:?}, Actual: {actual_0:?}"
×
515
    );
516

517
    // Check second element (should be null)
518
    let actual_1 = taken.scalar_at(1).vortex_unwrap();
3,218✔
519
    assert!(
3,218✔
520
        actual_1.is_null(),
3,218✔
NEW
521
        "Take with nullable indices: element at position 1 should be null, but got {actual_1:?}"
×
522
    );
523

524
    // Check third element (from index 2)
525
    let expected_2 = array.scalar_at(2).vortex_unwrap().into_nullable();
3,218✔
526
    let actual_2 = taken.scalar_at(2).vortex_unwrap();
3,218✔
527
    assert_eq!(
3,218✔
528
        actual_2, expected_2,
NEW
529
        "Take with nullable indices: element at position 2 should be from array index 2. \
×
NEW
530
         Expected: {expected_2:?}, Actual: {actual_2:?}"
×
531
    );
532
}
3,651✔
533

534
/// Tests large array consistency
535
pub fn test_large_array_consistency(array: &dyn Array) {
3,651✔
536
    let len = array.len();
3,651✔
537
    if len < 1000 {
3,651✔
538
        return;
2,959✔
539
    }
692✔
540

541
    // Test with every 10th element
542
    let indices: Vec<u64> = (0..len).step_by(10).map(|i| i as u64).collect();
135,540✔
543
    let indices_array = PrimitiveArray::from_iter(indices).into_array();
692✔
544
    let taken = take(array, &indices_array).vortex_unwrap();
692✔
545

546
    // Create equivalent filter mask
547
    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 10 == 0).collect();
1,354,944✔
548
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
692✔
549
    let filtered = filter(array, &mask).vortex_unwrap();
692✔
550

551
    // Results should match
552
    assert_eq!(taken.len(), filtered.len());
692✔
553
    for i in 0..taken.len() {
135,540✔
554
        assert_eq!(
135,540✔
555
            taken.scalar_at(i).vortex_unwrap(),
135,540✔
556
            filtered.scalar_at(i).vortex_unwrap()
135,540✔
557
        );
558
    }
559
}
3,651✔
560

561
/// Run all consistency tests on an array.
562
///
563
/// This function executes a comprehensive suite of consistency tests that verify
564
/// the correctness of compute operations on Vortex arrays.
565
///
566
/// # Test Suite Overview
567
///
568
/// ## Core Operation Consistency
569
/// - **Filter/Take**: Verifies `filter(array, mask)` equals `take(array, true_indices)`
570
/// - **Mask Composition**: Ensures sequential masks equal combined masks
571
/// - **Slice/Filter**: Checks contiguous filters equal slice operations
572
/// - **Take/Slice**: Validates sequential takes equal slice operations
573
///
574
/// ## Identity Operations
575
/// - **Filter Identity**: All-true mask preserves the array
576
/// - **Mask Identity**: All-false mask preserves values (as nullable)
577
/// - **Take Identity**: Taking all indices preserves the array
578
///
579
/// ## Edge Cases
580
/// - **Empty Operations**: Empty filters, takes, and slices behave correctly
581
/// - **Single Element**: Operations work with single-element arrays
582
/// - **Repeated Indices**: Take with duplicate indices works correctly
583
///
584
/// ## Null Handling
585
/// - **Nullable Indices**: Null indices produce null values
586
/// - **Mask/Filter Interaction**: Masking then filtering behaves predictably
587
///
588
/// ## Large Arrays
589
/// - **Performance**: Operations scale correctly to large arrays (1000+ elements)
590
/// ```
591
pub fn test_array_consistency(array: &dyn Array) {
3,651✔
592
    // Core operation consistency
593
    test_filter_take_consistency(array);
3,651✔
594
    test_double_mask_consistency(array);
3,651✔
595
    test_slice_filter_consistency(array);
3,651✔
596
    test_take_slice_consistency(array);
3,651✔
597

598
    // Identity operations
599
    test_filter_identity(array);
3,651✔
600
    test_mask_identity(array);
3,651✔
601
    test_take_preserves_properties(array);
3,651✔
602

603
    // Ordering and correctness
604
    test_filter_preserves_order(array);
3,651✔
605
    test_take_repeated_indices(array);
3,651✔
606

607
    // Null handling
608
    test_mask_filter_null_consistency(array);
3,651✔
609
    test_nullable_indices_consistency(array);
3,651✔
610

611
    // Edge cases
612
    test_empty_operations_consistency(array);
3,651✔
613
    test_large_array_consistency(array);
3,651✔
614
}
3,651✔
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