Skip to main content

vortex_array/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
22use std::sync::Arc;
23
24use vortex_buffer::BitBuffer;
25use vortex_error::VortexExpect;
26use vortex_error::vortex_panic;
27use vortex_mask::Mask;
28
29use crate::ArrayRef;
30use crate::Canonical;
31use crate::ExecutionCtx;
32use crate::IntoArray;
33use crate::RecursiveCanonical;
34use crate::aggregate_fn::NumericalAggregateOpts;
35use crate::aggregate_fn::fns::min_max::min_max;
36use crate::aggregate_fn::fns::nan_count::nan_count;
37use crate::aggregate_fn::fns::sum::sum;
38use crate::arrays::BoolArray;
39use crate::arrays::ConstantArray;
40use crate::arrays::PrimitiveArray;
41use crate::assert_arrays_eq;
42use crate::builtins::ArrayBuiltins;
43use crate::dtype::DType;
44use crate::dtype::Nullability;
45use crate::dtype::PType;
46use crate::scalar_fn::fns::operators::Operator;
47
48/// Tests that filter and take operations produce consistent results.
49///
50/// # Invariant
51/// `filter(array, mask)` should equal `take(array, indices_where_mask_is_true)`
52///
53/// # Test Details
54/// - Creates a mask that keeps elements where index % 3 != 1
55/// - Applies filter with this mask
56/// - Creates indices array containing positions where mask is true
57/// - Applies take with these indices
58/// - Verifies both results are identical
59fn test_filter_take_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
60    let len = array.len();
61    if len == 0 {
62        return;
63    }
64
65    // Create a test mask (keep elements where index % 3 != 1)
66    let mask_pattern: BitBuffer = (0..len).map(|i| i % 3 != 1).collect();
67    let mask = Mask::from_buffer(mask_pattern.clone());
68
69    // Filter the array
70    let filtered = array
71        .filter(mask)
72        .vortex_expect("filter should succeed in conformance test")
73        .execute::<RecursiveCanonical>(ctx)
74        .vortex_expect("batch execution should succeed in conformance test")
75        .0
76        .into_array();
77
78    // Create indices where mask is true
79    let indices: Vec<u64> = mask_pattern
80        .iter()
81        .enumerate()
82        .filter_map(|(i, v)| v.then_some(i as u64))
83        .collect();
84    let indices_array = PrimitiveArray::from_iter(indices).into_array();
85
86    // Take using those indices
87    let taken = array
88        .take(indices_array)
89        .vortex_expect("take should succeed in conformance test")
90        .execute::<RecursiveCanonical>(ctx)
91        .vortex_expect("batch execution should succeed in conformance test")
92        .0
93        .into_array();
94
95    // Results should be identical
96    assert_eq!(
97        filtered.len(),
98        taken.len(),
99        "Filter and take should produce arrays of the same length. \
100         Filtered length: {}, Taken length: {}",
101        filtered.len(),
102        taken.len()
103    );
104
105    for i in 0..filtered.len() {
106        let filtered_val = filtered
107            .execute_scalar(i, ctx)
108            .vortex_expect("scalar_at should succeed in conformance test");
109        let taken_val = taken
110            .execute_scalar(i, ctx)
111            .vortex_expect("scalar_at should succeed in conformance test");
112        assert_eq!(
113            filtered_val, taken_val,
114            "Filter and take produced different values at index {i}. \
115             Filtered value: {filtered_val:?}, Taken value: {taken_val:?}"
116        );
117    }
118}
119
120/// Tests that double masking is consistent with combined mask.
121///
122/// # Invariant
123/// `mask(mask(array, mask1), mask2)` should equal `mask(array, mask1 | mask2)`
124///
125/// # Test Details
126/// - Creates two masks: mask1 (every 3rd element) and mask2 (every 2nd element)
127/// - Applies masks sequentially: first mask1, then mask2 on the result
128/// - Creates a combined mask using OR operation (element is masked if either mask is true)
129/// - Applies the combined mask directly to the original array
130/// - Verifies both approaches produce identical results
131///
132/// # Why This Matters
133/// This test ensures that mask operations compose correctly, which is critical for
134/// complex query operations that may apply multiple filters.
135fn test_double_mask_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
136    let len = array.len();
137    if len == 0 {
138        return;
139    }
140
141    // Create two different mask patterns
142    let mask1: Mask = (0..len).map(|i| i % 3 == 0).collect();
143    let mask2: Mask = (0..len).map(|i| i % 2 == 0).collect();
144
145    // Apply masks sequentially
146    let first_masked = array
147        .clone()
148        .mask((!&mask1).into_array())
149        .vortex_expect("mask should succeed in conformance test");
150    let double_masked = first_masked
151        .mask((!&mask2).into_array())
152        .vortex_expect("mask should succeed in conformance test")
153        .execute::<RecursiveCanonical>(ctx)
154        .vortex_expect("batch execution should succeed in conformance test")
155        .0
156        .into_array();
157
158    // Create combined mask (OR operation - element is masked if EITHER mask is true)
159    let combined_pattern: BitBuffer = mask1
160        .to_bit_buffer()
161        .iter()
162        .zip(mask2.to_bit_buffer().iter())
163        .map(|(a, b)| a || b)
164        .collect();
165    let combined_mask = Mask::from_buffer(combined_pattern);
166
167    // Apply combined mask directly
168    let directly_masked = array
169        .clone()
170        .mask((!&combined_mask).into_array())
171        .vortex_expect("mask should succeed in conformance test")
172        .execute::<RecursiveCanonical>(ctx)
173        .vortex_expect("batch execution should succeed in conformance test")
174        .0
175        .into_array();
176
177    // Results should be identical
178    assert_eq!(
179        double_masked.len(),
180        directly_masked.len(),
181        "Sequential masking and combined masking should produce arrays of the same length. \
182         Sequential length: {}, Combined length: {}",
183        double_masked.len(),
184        directly_masked.len()
185    );
186
187    for i in 0..double_masked.len() {
188        let double_val = double_masked
189            .execute_scalar(i, ctx)
190            .vortex_expect("scalar_at should succeed in conformance test");
191        let direct_val = directly_masked
192            .execute_scalar(i, ctx)
193            .vortex_expect("scalar_at should succeed in conformance test");
194        assert_eq!(
195            double_val, direct_val,
196            "Sequential masking and combined masking produced different values at index {i}. \
197             Sequential masking value: {double_val:?}, Combined masking value: {direct_val:?}\n\
198             This likely indicates an issue with how masks are composed in the array implementation."
199        );
200    }
201}
202
203/// Tests that filtering with an all-true mask preserves the array.
204///
205/// # Invariant
206/// `filter(array, all_true_mask)` should equal `array`
207///
208/// # Test Details
209/// - Creates a mask with all elements set to true
210/// - Applies filter with this mask
211/// - Verifies the result is identical to the original array
212///
213/// # Why This Matters
214/// This is an identity operation that should be optimized in implementations
215/// to avoid unnecessary copying.
216fn test_filter_identity(array: &ArrayRef, ctx: &mut ExecutionCtx) {
217    let len = array.len();
218    if len == 0 {
219        return;
220    }
221
222    let all_true_mask = Mask::new_true(len);
223    let filtered = array
224        .filter(all_true_mask)
225        .vortex_expect("filter should succeed in conformance test")
226        .execute::<RecursiveCanonical>(ctx)
227        .vortex_expect("batch execution should succeed in conformance test")
228        .0
229        .into_array();
230
231    // Filtered array should be identical to original
232    assert_eq!(
233        filtered.len(),
234        array.len(),
235        "Filtering with all-true mask should preserve array length. \
236         Original length: {}, Filtered length: {}",
237        array.len(),
238        filtered.len()
239    );
240
241    for i in 0..len {
242        let original_val = array
243            .execute_scalar(i, ctx)
244            .vortex_expect("scalar_at should succeed in conformance test");
245        let filtered_val = filtered
246            .execute_scalar(i, ctx)
247            .vortex_expect("scalar_at should succeed in conformance test");
248        assert_eq!(
249            filtered_val, original_val,
250            "Filtering with all-true mask should preserve all values. \
251             Value at index {i} changed from {original_val:?} to {filtered_val:?}"
252        );
253    }
254}
255
256/// Tests that slice and filter with contiguous mask produce same results.
257///
258/// # Invariant
259/// `filter(array, contiguous_true_mask)` should equal `slice(array, start, end)`
260///
261/// # Test Details
262/// - Creates a mask that is true only for indices 1, 2, and 3
263/// - Filters the array with this mask
264/// - Slices the array from index 1 to 4
265/// - Verifies both operations produce identical results
266///
267/// # Why This Matters
268/// When a filter mask represents a contiguous range, it should be equivalent to
269/// a slice operation. Some implementations may optimize this case.
270fn test_slice_filter_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
271    let len = array.len();
272    if len < 4 {
273        return; // Need at least 4 elements for meaningful test
274    }
275
276    // Create a contiguous mask (true from index 1 to 3)
277    let mut mask_pattern = vec![false; len];
278    mask_pattern[1..4.min(len)].fill(true);
279
280    let mask = Mask::from_iter(mask_pattern);
281    let filtered = array
282        .filter(mask)
283        .vortex_expect("filter should succeed in conformance test")
284        .execute::<RecursiveCanonical>(ctx)
285        .vortex_expect("batch execution should succeed in conformance test")
286        .0
287        .into_array();
288
289    // Slice should produce the same result
290    let sliced = array
291        .slice(1..4.min(len))
292        .vortex_expect("slice should succeed in conformance test")
293        .execute::<RecursiveCanonical>(ctx)
294        .vortex_expect("batch execution should succeed in conformance test")
295        .0
296        .into_array();
297
298    assert_eq!(
299        filtered.len(),
300        sliced.len(),
301        "Filter with contiguous mask and slice should produce same length. \
302         Filtered length: {}, Sliced length: {}",
303        filtered.len(),
304        sliced.len()
305    );
306
307    for i in 0..filtered.len() {
308        let filtered_val = filtered
309            .execute_scalar(i, ctx)
310            .vortex_expect("scalar_at should succeed in conformance test");
311        let sliced_val = sliced
312            .execute_scalar(i, ctx)
313            .vortex_expect("scalar_at should succeed in conformance test");
314        assert_eq!(
315            filtered_val, sliced_val,
316            "Filter with contiguous mask and slice produced different values at index {i}. \
317             Filtered value: {filtered_val:?}, Sliced value: {sliced_val:?}"
318        );
319    }
320}
321
322/// Tests that take with sequential indices equals slice.
323///
324/// # Invariant
325/// `take(array, [1, 2, 3, ...])` should equal `slice(array, 1, n)`
326///
327/// # Test Details
328/// - Creates indices array with sequential values [1, 2, 3]
329/// - Takes elements at these indices
330/// - Slices array from index 1 to 4
331/// - Verifies both operations produce identical results
332///
333/// # Why This Matters
334/// Sequential takes are a common pattern that can be optimized to slice operations.
335fn test_take_slice_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
336    let len = array.len();
337    if len < 3 {
338        return; // Need at least 3 elements
339    }
340
341    // Take indices [1, 2, 3]
342    let end = 4.min(len);
343    let indices = PrimitiveArray::from_iter((1..end).map(|i| i as u64)).into_array();
344    let taken = array
345        .take(indices)
346        .vortex_expect("take should succeed in conformance test")
347        .execute::<RecursiveCanonical>(ctx)
348        .vortex_expect("batch execution should succeed in conformance test")
349        .0
350        .into_array();
351
352    // Slice from 1 to end
353    let sliced = array
354        .slice(1..end)
355        .vortex_expect("slice should succeed in conformance test")
356        .execute::<RecursiveCanonical>(ctx)
357        .vortex_expect("batch execution should succeed in conformance test")
358        .0
359        .into_array();
360
361    assert_eq!(
362        taken.len(),
363        sliced.len(),
364        "Take with sequential indices and slice should produce same length. \
365         Taken length: {}, Sliced length: {}",
366        taken.len(),
367        sliced.len()
368    );
369
370    for i in 0..taken.len() {
371        let taken_val = taken
372            .execute_scalar(i, ctx)
373            .vortex_expect("scalar_at should succeed in conformance test");
374        let sliced_val = sliced
375            .execute_scalar(i, ctx)
376            .vortex_expect("scalar_at should succeed in conformance test");
377        assert_eq!(
378            taken_val, sliced_val,
379            "Take with sequential indices and slice produced different values at index {i}. \
380             Taken value: {taken_val:?}, Sliced value: {sliced_val:?}"
381        );
382    }
383}
384
385/// Tests that filter preserves relative ordering
386fn test_filter_preserves_order(array: &ArrayRef, ctx: &mut ExecutionCtx) {
387    let len = array.len();
388    if len < 4 {
389        return;
390    }
391
392    // Create a mask that selects elements at indices 0, 2, 3
393    let mask_pattern: Vec<bool> = (0..len).map(|i| i == 0 || i == 2 || i == 3).collect();
394    let mask = Mask::from_iter(mask_pattern);
395
396    let filtered = array
397        .filter(mask)
398        .vortex_expect("filter should succeed in conformance test")
399        .execute::<RecursiveCanonical>(ctx)
400        .vortex_expect("batch execution should succeed in conformance test")
401        .0
402        .into_array();
403
404    // Verify the filtered array contains the right elements in order
405    assert_eq!(filtered.len(), 3.min(len));
406    if len >= 4 {
407        assert_eq!(
408            filtered
409                .execute_scalar(0, ctx)
410                .vortex_expect("scalar_at should succeed in conformance test"),
411            array
412                .execute_scalar(0, ctx)
413                .vortex_expect("scalar_at should succeed in conformance test")
414        );
415        assert_eq!(
416            filtered
417                .execute_scalar(1, ctx)
418                .vortex_expect("scalar_at should succeed in conformance test"),
419            array
420                .execute_scalar(2, ctx)
421                .vortex_expect("scalar_at should succeed in conformance test")
422        );
423        assert_eq!(
424            filtered
425                .execute_scalar(2, ctx)
426                .vortex_expect("scalar_at should succeed in conformance test"),
427            array
428                .execute_scalar(3, ctx)
429                .vortex_expect("scalar_at should succeed in conformance test")
430        );
431    }
432}
433
434/// Tests mask and filter interaction with nulls
435fn test_mask_filter_null_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
436    let len = array.len();
437    if len < 3 {
438        return;
439    }
440
441    // First mask some elements
442    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
443    let mask_array = Mask::from_iter(mask_pattern);
444    let masked = array
445        .clone()
446        .mask((!&mask_array).into_array())
447        .vortex_expect("mask should succeed in conformance test");
448
449    // Then filter to remove the nulls
450    let filter_pattern: Vec<bool> = (0..len).map(|i| i % 2 != 0).collect();
451    let filter_mask = Mask::from_iter(filter_pattern);
452    let filtered = masked
453        .filter(filter_mask.clone())
454        .vortex_expect("filter should succeed in conformance test")
455        .execute::<RecursiveCanonical>(ctx)
456        .vortex_expect("batch execution should succeed in conformance test")
457        .0
458        .into_array();
459
460    // This should be equivalent to directly filtering the original array
461    let direct_filtered = array
462        .filter(filter_mask)
463        .vortex_expect("filter should succeed in conformance test")
464        .execute::<RecursiveCanonical>(ctx)
465        .vortex_expect("batch execution should succeed in conformance test")
466        .0
467        .into_array();
468
469    assert_eq!(filtered.len(), direct_filtered.len());
470    for i in 0..filtered.len() {
471        assert_eq!(
472            filtered
473                .execute_scalar(i, ctx)
474                .vortex_expect("scalar_at should succeed in conformance test"),
475            direct_filtered
476                .execute_scalar(i, ctx)
477                .vortex_expect("scalar_at should succeed in conformance test")
478        );
479    }
480}
481
482/// Tests that empty operations are consistent
483fn test_empty_operations_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
484    let len = array.len();
485
486    // Empty filter
487    let empty_filter = array
488        .filter(Mask::new_false(len))
489        .vortex_expect("filter should succeed in conformance test")
490        .execute::<RecursiveCanonical>(ctx)
491        .vortex_expect("batch execution should succeed in conformance test")
492        .0
493        .into_array();
494    assert_eq!(empty_filter.len(), 0);
495    assert_eq!(empty_filter.dtype(), array.dtype());
496
497    // Empty take
498    let empty_indices = PrimitiveArray::empty::<u64>(Nullability::NonNullable).into_array();
499    let empty_take = array
500        .take(empty_indices)
501        .vortex_expect("take should succeed in conformance test")
502        .execute::<RecursiveCanonical>(ctx)
503        .vortex_expect("batch execution should succeed in conformance test")
504        .0
505        .into_array();
506    assert_eq!(empty_take.len(), 0);
507    assert_eq!(empty_take.dtype(), array.dtype());
508
509    // Empty slice (if array is non-empty)
510    if len > 0 {
511        let empty_slice = array
512            .slice(0..0)
513            .vortex_expect("slice should succeed in conformance test")
514            .execute::<RecursiveCanonical>(ctx)
515            .vortex_expect("batch execution should succeed in conformance test")
516            .0
517            .into_array();
518        assert_eq!(empty_slice.len(), 0);
519        assert_eq!(empty_slice.dtype(), array.dtype());
520    }
521}
522
523/// Tests consistency with nullable indices.
524///
525/// # Invariant
526/// `take(array, [Some(0), None, Some(2)])` should produce `[array[0], null, array[2]]`
527///
528/// # Test Details
529/// - Creates an indices array with null at position 1: `[Some(0), None, Some(2)]`
530/// - Takes elements using these indices
531/// - Verifies that:
532///   - Position 0 contains the value from array index 0
533///   - Position 1 contains null
534///   - Position 2 contains the value from array index 2
535///   - The result array has nullable type
536///
537/// # Why This Matters
538/// Nullable indices are a powerful feature that allows introducing nulls during
539/// a take operation, which is useful for outer joins and similar operations.
540fn test_nullable_indices_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
541    let len = array.len();
542    if len < 3 {
543        return; // Need at least 3 elements to test indices 0 and 2
544    }
545
546    // Create nullable indices where some indices are null
547    let indices = PrimitiveArray::from_option_iter([Some(0u64), None, Some(2u64)]).into_array();
548
549    let taken = array
550        .take(indices)
551        .vortex_expect("take should succeed in conformance test")
552        .execute::<RecursiveCanonical>(ctx)
553        .vortex_expect("batch execution should succeed in conformance test")
554        .0
555        .into_array();
556
557    // Result should have nulls where indices were null
558    assert_eq!(
559        taken.len(),
560        3,
561        "Take with nullable indices should produce array of length 3, got {}",
562        taken.len()
563    );
564
565    assert!(
566        taken.dtype().is_nullable(),
567        "Take with nullable indices should produce nullable array, but dtype is {:?}",
568        taken.dtype()
569    );
570
571    // Check first element (from index 0)
572    let expected_0 = array
573        .execute_scalar(0, ctx)
574        .vortex_expect("scalar_at should succeed in conformance test")
575        .into_nullable();
576    let actual_0 = taken
577        .execute_scalar(0, ctx)
578        .vortex_expect("scalar_at should succeed in conformance test");
579    assert_eq!(
580        actual_0, expected_0,
581        "Take with nullable indices: element at position 0 should be from array index 0. \
582         Expected: {expected_0:?}, Actual: {actual_0:?}"
583    );
584
585    // Check second element (should be null)
586    let actual_1 = taken
587        .execute_scalar(1, ctx)
588        .vortex_expect("scalar_at should succeed in conformance test");
589    assert!(
590        actual_1.is_null(),
591        "Take with nullable indices: element at position 1 should be null, but got {actual_1:?}"
592    );
593
594    // Check third element (from index 2)
595    let expected_2 = array
596        .execute_scalar(2, ctx)
597        .vortex_expect("scalar_at should succeed in conformance test")
598        .into_nullable();
599    let actual_2 = taken
600        .execute_scalar(2, ctx)
601        .vortex_expect("scalar_at should succeed in conformance test");
602    assert_eq!(
603        actual_2, expected_2,
604        "Take with nullable indices: element at position 2 should be from array index 2. \
605         Expected: {expected_2:?}, Actual: {actual_2:?}"
606    );
607}
608
609/// Tests large array consistency
610fn test_large_array_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
611    let len = array.len();
612    if len < 1000 {
613        return;
614    }
615
616    // Test with every 10th element
617    let indices: Vec<u64> = (0..len).step_by(10).map(|i| i as u64).collect();
618    let indices_array = PrimitiveArray::from_iter(indices).into_array();
619    let taken = array
620        .take(indices_array)
621        .vortex_expect("take should succeed in conformance test")
622        .execute::<RecursiveCanonical>(ctx)
623        .vortex_expect("batch execution should succeed in conformance test")
624        .0
625        .into_array();
626
627    // Create equivalent filter mask
628    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 10 == 0).collect();
629    let mask = Mask::from_iter(mask_pattern);
630    let filtered = array
631        .filter(mask)
632        .vortex_expect("filter should succeed in conformance test")
633        .execute::<RecursiveCanonical>(ctx)
634        .vortex_expect("batch execution should succeed in conformance test")
635        .0
636        .into_array();
637
638    // Results should match
639    assert_eq!(taken.len(), filtered.len());
640    for i in 0..taken.len() {
641        assert_eq!(
642            taken
643                .execute_scalar(i, ctx)
644                .vortex_expect("scalar_at should succeed in conformance test"),
645            filtered
646                .execute_scalar(i, ctx)
647                .vortex_expect("scalar_at should succeed in conformance test")
648        );
649    }
650}
651
652/// Tests that comparison operations follow inverse relationships.
653///
654/// # Invariants
655/// - `compare(array, value, Eq)` is the inverse of `compare(array, value, NotEq)`
656/// - `compare(array, value, Gt)` is the inverse of `compare(array, value, Lte)`
657/// - `compare(array, value, Lt)` is the inverse of `compare(array, value, Gte)`
658///
659/// # Test Details
660/// - Creates comparison results for each operator
661/// - Verifies that inverse operations produce opposite boolean values
662/// - Tests with multiple scalar values to ensure consistency
663///
664/// # Why This Matters
665/// Comparison operations must maintain logical consistency across encodings.
666/// This test catches bugs where an encoding might implement one comparison
667/// correctly but fail on its logical inverse.
668fn test_comparison_inverse_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
669    let len = array.len();
670    if len == 0 {
671        return;
672    }
673
674    // Skip non-comparable types.
675    match array.dtype() {
676        DType::Null
677        | DType::List(..)
678        | DType::Struct(..)
679        | DType::Union(..)
680        | DType::Extension(_) => return,
681        _ => {}
682    }
683
684    let test_scalar = array
685        .execute_scalar(len / 2, ctx)
686        .vortex_expect("scalar_at should succeed in conformance test");
687    let const_array = ConstantArray::new(test_scalar, len).into_array();
688
689    for (operator, counterpart) in [
690        (Operator::Eq, Operator::NotEq),
691        (Operator::Gt, Operator::Lte),
692        (Operator::Lt, Operator::Gte),
693    ] {
694        let left = array
695            .binary(const_array.clone(), operator)
696            .vortex_expect("comparison should succeed in conformance test");
697        let left = left
698            .not()
699            .vortex_expect("not should succeed in conformance test");
700        let right = array
701            .binary(const_array.clone(), counterpart)
702            .vortex_expect("comparison should succeed in conformance test");
703        assert_arrays_eq!(left, right, ctx);
704    }
705}
706
707/// Tests that comparison operations maintain proper symmetry relationships.
708///
709/// # Invariants
710/// - `compare(array, value, Gt)` should equal `compare_scalar_array(value, array, Lt)`
711/// - `compare(array, value, Lt)` should equal `compare_scalar_array(value, array, Gt)`
712/// - `compare(array, value, Eq)` should equal `compare_scalar_array(value, array, Eq)`
713///
714/// # Test Details
715/// - Compares array-scalar operations with their symmetric scalar-array versions
716/// - Verifies that ordering relationships are properly reversed
717/// - Tests equality which should be symmetric
718///
719/// # Why This Matters
720/// Ensures that comparison operations maintain mathematical ordering properties
721/// regardless of operand order.
722fn test_comparison_symmetry_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
723    let len = array.len();
724    if len == 0 {
725        return;
726    }
727
728    // Skip non-comparable types.
729    match array.dtype() {
730        DType::Null
731        | DType::List(..)
732        | DType::Struct(..)
733        | DType::Union(..)
734        | DType::Extension(_) => return,
735        _ => {}
736    }
737
738    let test_scalar = array
739        .execute_scalar(len / 2, ctx)
740        .vortex_expect("scalar_at should succeed in conformance test");
741    let const_array = ConstantArray::new(test_scalar, len).into_array();
742
743    for (operator, counterpart) in [(Operator::Gt, Operator::Lt), (Operator::Eq, Operator::Eq)] {
744        let left = array
745            .binary(const_array.clone(), operator)
746            .vortex_expect("comparison should succeed in conformance test");
747        let right = const_array
748            .binary(array.clone(), counterpart)
749            .vortex_expect("comparison should succeed in conformance test");
750        assert_arrays_eq!(left, right, ctx);
751    }
752}
753
754/// Tests that boolean operations follow De Morgan's laws.
755///
756/// # Invariants
757/// - `NOT(A AND B)` equals `(NOT A) OR (NOT B)`
758/// - `NOT(A OR B)` equals `(NOT A) AND (NOT B)`
759///
760/// # Test Details
761/// - If the array is boolean, uses it directly for testing boolean operations
762/// - Creates two boolean masks from patterns based on the array
763/// - Computes AND/OR operations and their inversions
764/// - Verifies De Morgan's laws hold for all elements
765///
766/// # Why This Matters
767/// Boolean operations must maintain logical consistency across encodings.
768/// This test catches bugs where encodings might optimize boolean operations
769/// incorrectly, breaking fundamental logical properties.
770fn test_boolean_demorgan_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
771    if !matches!(array.dtype(), DType::Bool(_)) {
772        return;
773    }
774
775    let bool_mask = BoolArray::from_iter((0..array.len()).map(|i| i % 3 == 0)).into_array();
776    for (operator, counterpart) in [(Operator::And, Operator::Or), (Operator::Or, Operator::And)] {
777        let left = array
778            .binary(bool_mask.clone(), operator)
779            .vortex_expect("boolean operation should succeed in conformance test")
780            .not()
781            .vortex_expect("not should succeed in conformance test");
782        let not_a = array
783            .not()
784            .vortex_expect("not should succeed in conformance test");
785        let not_b = bool_mask
786            .not()
787            .vortex_expect("not should succeed in conformance test");
788        let right = not_a
789            .binary(not_b, counterpart)
790            .vortex_expect("boolean operation should succeed in conformance test");
791        assert_arrays_eq!(left, right, ctx);
792    }
793}
794
795/// Tests that slice and aggregate operations produce consistent results.
796///
797/// # Invariants
798/// - Aggregating a sliced array should equal aggregating the corresponding
799///   elements from the canonical form
800/// - This applies to sum, count, min/max, and other aggregate functions
801///
802/// # Test Details
803/// - Slices the array and computes aggregates
804/// - Compares against aggregating the canonical form's slice
805/// - Tests multiple aggregate functions where applicable
806///
807/// # Why This Matters
808/// Aggregate operations on sliced arrays must produce correct results
809/// regardless of the underlying encoding's offset handling.
810fn test_slice_aggregate_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
811    let len = array.len();
812    if len < 5 {
813        return; // Need enough elements for meaningful slice
814    }
815
816    // Define slice bounds
817    let start = 1;
818    let end = (len - 1).min(start + 10); // Take up to 10 elements
819
820    // Get sliced array and canonical slice
821    let sliced = array
822        .slice(start..end)
823        .vortex_expect("slice should succeed in conformance test");
824    let canonical = array
825        .clone()
826        .execute::<Canonical>(ctx)
827        .vortex_expect("to_canonical failed");
828    let canonical_sliced = canonical
829        .into_array()
830        .slice(start..end)
831        .vortex_expect("slice should succeed in conformance test");
832
833    // Test null count through invalid_count
834    let sliced_invalid_count = sliced
835        .invalid_count(ctx)
836        .vortex_expect("invalid_count should succeed in conformance test");
837    let canonical_invalid_count = canonical_sliced
838        .invalid_count(ctx)
839        .vortex_expect("invalid_count should succeed in conformance test");
840    assert_eq!(
841        sliced_invalid_count, canonical_invalid_count,
842        "null_count on sliced array should match canonical. \
843             Sliced: {sliced_invalid_count}, Canonical: {canonical_invalid_count}",
844    );
845
846    // Test sum for numeric types
847    if !matches!(array.dtype(), DType::Primitive(..)) {
848        return;
849    }
850
851    let slice_sum = sum(&sliced, ctx).vortex_expect("slice sum should succeed");
852    let canonical_sum = sum(&canonical_sliced, ctx).vortex_expect("canonical sum should succeed");
853    assert_eq!(
854        slice_sum, canonical_sum,
855        "sum on sliced array should match canonical"
856    );
857
858    let slice_minmax = min_max(&sliced, ctx, NumericalAggregateOpts::default())
859        .vortex_expect("slice min_max should succeed");
860    let canonical_minmax = min_max(&canonical_sliced, ctx, NumericalAggregateOpts::default())
861        .vortex_expect("canonical min_max should succeed");
862    assert_eq!(
863        slice_minmax, canonical_minmax,
864        "min_max on sliced array should match canonical"
865    );
866
867    if array.dtype().is_float() {
868        let slice_nan_count =
869            nan_count(&sliced, ctx).vortex_expect("slice nan_count should succeed");
870        let canonical_nan_count =
871            nan_count(&canonical_sliced, ctx).vortex_expect("canonical nan_count should succeed");
872        assert_eq!(
873            slice_nan_count, canonical_nan_count,
874            "nan_count on sliced array should match canonical"
875        );
876    }
877}
878
879fn widened_primitive_dtype(dtype: &DType) -> Option<DType> {
880    let DType::Primitive(ptype, nullability) = dtype else {
881        return None;
882    };
883    let widened = match ptype {
884        PType::U8 => PType::U16,
885        PType::U16 => PType::U32,
886        PType::U32 => PType::U64,
887        PType::U64 => return None,
888        PType::I8 => PType::I16,
889        PType::I16 => PType::I32,
890        PType::I32 => PType::I64,
891        PType::I64 => return None,
892        PType::F16 => PType::F32,
893        PType::F32 => PType::F64,
894        PType::F64 => return None,
895    };
896    Some(DType::Primitive(widened, *nullability))
897}
898
899/// Cast targets exercised for each logical dtype.
900fn slice_cast_targets(dtype: &DType) -> Vec<DType> {
901    match dtype {
902        DType::Null => vec![],
903        DType::Bool(nullability) => vec![
904            DType::Primitive(PType::U8, *nullability),
905            DType::Primitive(PType::I32, *nullability),
906        ],
907        DType::Primitive(ptype, nullability) => {
908            let mut targets = vec![];
909            // Test nullability changes
910            let opposite_nullability = match nullability {
911                Nullability::NonNullable => Nullability::Nullable,
912                Nullability::Nullable => Nullability::NonNullable,
913            };
914            targets.push(DType::Primitive(*ptype, opposite_nullability));
915
916            // Test widening casts
917            match ptype {
918                PType::U8 => {
919                    targets.push(DType::Primitive(PType::U16, *nullability));
920                    targets.push(DType::Primitive(PType::I16, *nullability));
921                }
922                PType::U16 => {
923                    targets.push(DType::Primitive(PType::U32, *nullability));
924                    targets.push(DType::Primitive(PType::I32, *nullability));
925                }
926                PType::U32 => {
927                    targets.push(DType::Primitive(PType::U64, *nullability));
928                    targets.push(DType::Primitive(PType::I64, *nullability));
929                }
930                PType::U64 => {
931                    targets.push(DType::Primitive(PType::F64, *nullability));
932                }
933                PType::I8 => {
934                    targets.push(DType::Primitive(PType::I16, *nullability));
935                    targets.push(DType::Primitive(PType::F32, *nullability));
936                }
937                PType::I16 => {
938                    targets.push(DType::Primitive(PType::I32, *nullability));
939                    targets.push(DType::Primitive(PType::F32, *nullability));
940                }
941                PType::I32 => {
942                    targets.push(DType::Primitive(PType::I64, *nullability));
943                    targets.push(DType::Primitive(PType::F64, *nullability));
944                }
945                PType::I64 => {
946                    targets.push(DType::Primitive(PType::F64, *nullability));
947                }
948                PType::F16 => {
949                    targets.push(DType::Primitive(PType::F32, *nullability));
950                }
951                PType::F32 => {
952                    targets.push(DType::Primitive(PType::F64, *nullability));
953                    targets.push(DType::Primitive(PType::I32, *nullability));
954                }
955                PType::F64 => {
956                    targets.push(DType::Primitive(PType::I64, *nullability));
957                }
958            }
959            targets
960        }
961        DType::Decimal(decimal_type, nullability) => {
962            let opposite = match nullability {
963                Nullability::NonNullable => Nullability::Nullable,
964                Nullability::Nullable => Nullability::NonNullable,
965            };
966            vec![DType::Decimal(*decimal_type, opposite)]
967        }
968        DType::Utf8(nullability) => {
969            let opposite = match nullability {
970                Nullability::NonNullable => Nullability::Nullable,
971                Nullability::Nullable => Nullability::NonNullable,
972            };
973            vec![DType::Utf8(opposite), DType::Binary(*nullability)]
974        }
975        DType::Binary(nullability) => {
976            let opposite = match nullability {
977                Nullability::NonNullable => Nullability::Nullable,
978                Nullability::Nullable => Nullability::NonNullable,
979            };
980            vec![
981                DType::Binary(opposite),
982                DType::Utf8(*nullability), // May fail if not valid UTF-8
983            ]
984        }
985        DType::List(element_type, nullability) => {
986            let opposite = match nullability {
987                Nullability::NonNullable => Nullability::Nullable,
988                Nullability::Nullable => Nullability::NonNullable,
989            };
990            vec![DType::List(Arc::clone(element_type), opposite)]
991        }
992        DType::FixedSizeList(element_type, list_size, nullability) => {
993            let opposite = match nullability {
994                Nullability::NonNullable => Nullability::Nullable,
995                Nullability::Nullable => Nullability::NonNullable,
996            };
997            vec![DType::FixedSizeList(
998                Arc::clone(element_type),
999                *list_size,
1000                opposite,
1001            )]
1002        }
1003        DType::Map(map_dtype, nullability) => {
1004            let opposite = match nullability {
1005                Nullability::NonNullable => Nullability::Nullable,
1006                Nullability::Nullable => Nullability::NonNullable,
1007            };
1008            let key_dtype = map_dtype.key_dtype();
1009            let value_dtype = map_dtype.value_dtype();
1010            let mut targets = vec![DType::Map(map_dtype.clone(), opposite)];
1011
1012            if let Some(widened_key) = widened_primitive_dtype(&key_dtype)
1013                && let Ok(dtype) = DType::map(
1014                    widened_key,
1015                    value_dtype.clone(),
1016                    map_dtype.keys_sorted(),
1017                    *nullability,
1018                )
1019            {
1020                targets.push(dtype);
1021            }
1022
1023            if let Some(widened_value) = widened_primitive_dtype(&value_dtype)
1024                && let Ok(dtype) = DType::map(
1025                    key_dtype.clone(),
1026                    widened_value,
1027                    map_dtype.keys_sorted(),
1028                    *nullability,
1029                )
1030            {
1031                targets.push(dtype);
1032            }
1033
1034            if map_dtype.keys_sorted()
1035                && let Ok(dtype) = DType::map(key_dtype, value_dtype, false, *nullability)
1036            {
1037                targets.push(dtype);
1038            }
1039
1040            targets
1041        }
1042        DType::Struct(fields, nullability) => {
1043            let opposite = match nullability {
1044                Nullability::NonNullable => Nullability::Nullable,
1045                Nullability::Nullable => Nullability::NonNullable,
1046            };
1047            vec![DType::Struct(fields.clone(), opposite)]
1048        }
1049        DType::Union(..) => vortex_panic!("TODO(connor)[Union]: unimplemented"),
1050        DType::Variant(_) => vortex_panic!("Variant conformance casting is not implemented"),
1051        DType::Extension(_) => vec![], // Extension types typically only cast to themselves
1052    }
1053}
1054
1055/// Tests that cast operations preserve array properties when sliced.
1056///
1057/// # Invariant
1058/// `cast(slice(array, start, end), dtype)` should equal `slice(cast(array, dtype), start, end)`
1059///
1060/// # Test Details
1061/// - Slices the array from index 2 to 7 (or len-2 if smaller)
1062/// - Casts the sliced array to a different type
1063/// - Compares against the canonical form of the array (without slicing or casting the canonical form)
1064/// - Verifies both approaches produce identical results
1065///
1066/// # Why This Matters
1067/// This test specifically catches bugs where encodings (like RunEndArray) fail to preserve
1068/// offset information during cast operations. Such bugs can lead to incorrect data being
1069/// returned after casting a sliced array.
1070fn test_cast_slice_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
1071    let len = array.len();
1072    if len < 5 {
1073        return; // Need at least 5 elements for meaningful slice
1074    }
1075
1076    // Define slice bounds
1077    let start = 2;
1078    let end = 7.min(len - 2).max(start + 1); // Ensure we have at least 1 element
1079
1080    // Get canonical form of the original array
1081    let canonical = array
1082        .clone()
1083        .execute::<RecursiveCanonical>(ctx)
1084        .vortex_expect("to_canonical failed")
1085        .0
1086        .into_array();
1087
1088    let target_dtypes = slice_cast_targets(array.dtype());
1089
1090    for target_dtype in target_dtypes {
1091        let mut slice_then_cast: Option<ArrayRef> = None;
1092        // Whole-array casts can fail on rows outside the selected slice. Establish
1093        // support from each canonical input independently before running its encoding.
1094        for slice_first in [true, false] {
1095            let reference_input = if slice_first {
1096                canonical
1097                    .slice(start..end)
1098                    .vortex_expect("canonical slice should succeed")
1099            } else {
1100                canonical.clone()
1101            };
1102            let expected = reference_input.cast(target_dtype.clone()).and_then(|a| {
1103                a.execute::<RecursiveCanonical>(ctx)
1104                    .map(|c| c.0.into_array())
1105            });
1106            let input = if slice_first {
1107                array
1108                    .slice(start..end)
1109                    .vortex_expect("slice should succeed")
1110            } else {
1111                array.clone()
1112            };
1113            let actual = input.cast(target_dtype.clone()).and_then(|a| {
1114                a.execute::<RecursiveCanonical>(ctx)
1115                    .map(|c| c.0.into_array())
1116            });
1117            let expected = match expected {
1118                Ok(expected) => expected,
1119                Err(_) => {
1120                    assert!(
1121                        actual.is_err(),
1122                        "encoded cast to {target_dtype} succeeded where canonical cast failed (slice_first={slice_first})"
1123                    );
1124                    continue;
1125                }
1126            };
1127            let actual =
1128                actual.vortex_expect("encoded cast should succeed when canonical cast succeeds");
1129            let (actual, expected) = if slice_first {
1130                (actual, expected)
1131            } else {
1132                (
1133                    actual
1134                        .slice(start..end)
1135                        .vortex_expect("cast result slice should succeed")
1136                        .execute::<RecursiveCanonical>(ctx)
1137                        .vortex_expect("slice batch execution should succeed")
1138                        .0
1139                        .into_array(),
1140                    expected
1141                        .slice(start..end)
1142                        .vortex_expect("canonical cast result slice should succeed")
1143                        .execute::<RecursiveCanonical>(ctx)
1144                        .vortex_expect("canonical slice batch execution should succeed")
1145                        .0
1146                        .into_array(),
1147                )
1148            };
1149            assert_eq!(actual.dtype(), &target_dtype);
1150            assert_eq!(actual.len(), end - start);
1151            assert_eq!(expected.dtype(), &target_dtype);
1152            assert_eq!(expected.len(), end - start);
1153            // AllNonDistinct does not support unions, including nested union children.
1154            // Both inputs have already executed their batch and child operations.
1155            for i in 0..actual.len() {
1156                assert_eq!(
1157                    actual
1158                        .execute_scalar(i, ctx)
1159                        .vortex_expect("cast scalar_at should succeed"),
1160                    expected
1161                        .execute_scalar(i, ctx)
1162                        .vortex_expect("reference scalar_at should succeed"),
1163                    "cast value differs from canonical reference at index {i}"
1164                );
1165                if let Some(sliced_cast) = &slice_then_cast {
1166                    assert_eq!(
1167                        actual
1168                            .execute_scalar(i, ctx)
1169                            .vortex_expect("cast scalar_at should succeed"),
1170                        sliced_cast
1171                            .execute_scalar(i, ctx)
1172                            .vortex_expect("slice cast scalar_at should succeed"),
1173                        "slice/cast order differs at index {i}"
1174                    );
1175                }
1176            }
1177            if slice_first {
1178                slice_then_cast = Some(actual.clone());
1179            }
1180
1181            // Scalar casts do not implement every array conversion. When supported,
1182            // retain a complete independent per-value oracle rather than a partial prefix.
1183            let scalar_expected: Result<Vec<_>, _> = (start..end)
1184                .map(|i| {
1185                    canonical
1186                        .execute_scalar(i, ctx)
1187                        .vortex_expect("canonical scalar_at should succeed")
1188                        .cast(&target_dtype)
1189                })
1190                .collect();
1191            if let Ok(scalars) = scalar_expected {
1192                for (i, expected) in scalars.into_iter().enumerate() {
1193                    let actual = actual
1194                        .execute_scalar(i, ctx)
1195                        .vortex_expect("scalar_at should succeed");
1196                    assert_eq!(actual, expected, "cast value differs at index {i}");
1197                }
1198            }
1199        }
1200    }
1201}
1202
1203/// Tests that a repeated probe agrees with one-off scalar reads.
1204///
1205/// # Invariant
1206/// `array.repeated_probe().execute_scalar(i)` equals `array.execute_scalar(i)` for every `i`,
1207/// whatever order the rows are visited in.
1208///
1209/// # Test Details
1210/// - Reads every row one-off to build the expected values
1211/// - Reads the same rows through a single retained probe, backwards, then forwards, then
1212///   sparsely, so a probe that caches per-row state has to stay correct across jumps and
1213///   revisits
1214/// - Checks nullness through the probe as well as the value
1215///
1216/// # Why This Matters
1217/// Encodings serve one-off and repeated reads from the same `probe_scalar` body, and a parent
1218/// encoding passes its children's probes down. A retained cache that outlives the row it was
1219/// built for, or a slot read through the wrong index, shows up here as a wrong value.
1220fn test_repeated_probe_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
1221    // Long arrays are covered by their prefix; the three passes below already cost 3n reads.
1222    let len = array.len().min(1024);
1223    if len == 0 {
1224        return;
1225    }
1226
1227    let expected: Vec<_> = (0..len)
1228        .map(|i| {
1229            array
1230                .execute_scalar(i, ctx)
1231                .vortex_expect("scalar_at should succeed in conformance test")
1232        })
1233        .collect();
1234
1235    let mut probe = array.repeated_probe();
1236    let order = (0..len).rev().chain(0..len).chain((0..len).step_by(7));
1237    for i in order {
1238        let actual = probe
1239            .execute_scalar(i, ctx)
1240            .vortex_expect("repeated probe read should succeed in conformance test");
1241        assert_eq!(
1242            actual, expected[i],
1243            "Repeated probe and one-off read disagree at index {i}. \
1244             Probe value: {actual:?}, one-off value: {:?}",
1245            expected[i]
1246        );
1247        let valid = probe
1248            .execute_is_valid(i, ctx)
1249            .vortex_expect("repeated probe validity read should succeed in conformance test");
1250        assert_eq!(
1251            valid,
1252            !expected[i].is_null(),
1253            "Repeated probe validity disagrees with the value read at index {i}"
1254        );
1255    }
1256}
1257
1258/// Run all consistency tests on an array.
1259///
1260/// This function executes a comprehensive suite of consistency tests that verify
1261/// the correctness of compute operations on Vortex arrays.
1262///
1263/// # Test Suite Overview
1264///
1265/// ## Core Operation Consistency
1266/// - **Filter/Take**: Verifies `filter(array, mask)` equals `take(array, true_indices)`
1267/// - **Mask Composition**: Ensures sequential masks equal combined masks
1268/// - **Slice/Filter**: Checks contiguous filters equal slice operations
1269/// - **Take/Slice**: Validates sequential takes equal slice operations
1270/// - **Cast/Slice**: Ensures cast operations preserve sliced array properties
1271///
1272/// ## Boolean Operations
1273/// - **De Morgan's Laws**: Verifies boolean operations follow logical laws
1274///
1275/// ## Comparison Operations
1276/// - **Inverse Relationships**: Verifies logical inverses (Eq/NotEq, Gt/Lte, Lt/Gte)
1277/// - **Symmetry**: Ensures proper ordering relationships when operands are swapped
1278///
1279/// ## Aggregate Operations
1280/// - **Slice/Aggregate**: Verifies aggregates on sliced arrays match canonical
1281///
1282/// ## Identity Operations
1283/// - **Filter Identity**: All-true mask preserves the array
1284/// - **Mask Identity**: All-false mask preserves values (as nullable)
1285/// - **Take Identity**: Taking all indices preserves the array
1286///
1287/// ## Edge Cases
1288/// - **Empty Operations**: Empty filters, takes, and slices behave correctly
1289/// - **Single Element**: Operations work with single-element arrays
1290/// - **Repeated Indices**: Take with duplicate indices works correctly
1291///
1292/// ## Null Handling
1293/// - **Nullable Indices**: Null indices produce null values
1294/// - **Mask/Filter Interaction**: Masking then filtering behaves predictably
1295///
1296/// ## Large Arrays
1297/// - **Performance**: Operations scale correctly to large arrays (1000+ elements)
1298/// ```text
1299pub fn test_array_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
1300    // Core operation consistency
1301    test_filter_take_consistency(array, ctx);
1302    test_double_mask_consistency(array, ctx);
1303    test_slice_filter_consistency(array, ctx);
1304    test_take_slice_consistency(array, ctx);
1305    test_cast_slice_consistency(array, ctx);
1306
1307    // Boolean operations
1308    test_boolean_demorgan_consistency(array, ctx);
1309
1310    // Comparison operations
1311    test_comparison_inverse_consistency(array, ctx);
1312    test_comparison_symmetry_consistency(array, ctx);
1313
1314    // Aggregate operations
1315    test_slice_aggregate_consistency(array, ctx);
1316
1317    // Identity operations
1318    test_filter_identity(array, ctx);
1319    if !array.is_empty() {
1320        super::mask::test_empty_mask(array, ctx);
1321        super::take::test_take_all(array, ctx);
1322    }
1323
1324    // Row access
1325    test_repeated_probe_consistency(array, ctx);
1326
1327    // Ordering and correctness
1328    test_filter_preserves_order(array, ctx);
1329    super::take::test_take_repeated_indices(array, ctx);
1330
1331    // Null handling
1332    test_mask_filter_null_consistency(array, ctx);
1333    test_nullable_indices_consistency(array, ctx);
1334
1335    // Edge cases
1336    test_empty_operations_consistency(array, ctx);
1337    test_large_array_consistency(array, ctx);
1338
1339    // Row access
1340    test_probe_consistency(array, ctx);
1341}
1342
1343/// Tests that one-off and repeated probes agree with each other and with validity.
1344///
1345/// # Invariant
1346/// For every index `i`, on the array and a slice of it:
1347/// - `probe().execute_scalar(i)` equals `repeated_probe().execute_scalar(i)`
1348/// - `execute_is_valid(i)` equals `!execute_scalar(i).is_null()` on both probes
1349fn test_probe_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
1350    check_probe_consistency(array, ctx);
1351    if array.len() > 2 {
1352        let sliced = array
1353            .slice(1..array.len() - 1)
1354            .vortex_expect("slice should succeed in conformance test");
1355        check_probe_consistency(&sliced, ctx);
1356    }
1357}
1358
1359fn check_probe_consistency(array: &ArrayRef, ctx: &mut ExecutionCtx) {
1360    let len = array.len();
1361    let stride = if len <= 1024 { 1 } else { 7 };
1362    let indices: Vec<usize> = (0..len).step_by(stride).collect();
1363
1364    let mut once = array.probe();
1365    let mut repeated = array.repeated_probe();
1366    for &i in indices.iter().chain(indices.iter().rev()) {
1367        let expected = once
1368            .execute_scalar(i, ctx)
1369            .vortex_expect("one-off probe should succeed in conformance test");
1370        let valid = once
1371            .execute_is_valid(i, ctx)
1372            .vortex_expect("one-off validity should succeed in conformance test");
1373        assert_eq!(
1374            valid,
1375            !expected.is_null(),
1376            "One-off validity and scalar disagree at index {i}: valid {valid}, scalar {expected:?}"
1377        );
1378
1379        let retained_valid = repeated
1380            .execute_is_valid(i, ctx)
1381            .vortex_expect("retained validity should succeed in conformance test");
1382        assert_eq!(
1383            retained_valid, valid,
1384            "Retained and one-off validity disagree at index {i}"
1385        );
1386        let actual = repeated
1387            .execute_scalar(i, ctx)
1388            .vortex_expect("retained probe should succeed in conformance test");
1389        assert_eq!(
1390            actual, expected,
1391            "Retained and one-off probes disagree at index {i}"
1392        );
1393    }
1394}