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 vortex_buffer::BitBuffer;
23use vortex_dtype::DType;
24use vortex_dtype::Nullability;
25use vortex_dtype::PType;
26use vortex_error::VortexExpect;
27use vortex_error::vortex_panic;
28use vortex_mask::Mask;
29
30use crate::Array;
31use crate::IntoArray;
32use crate::arrays::BoolArray;
33use crate::arrays::PrimitiveArray;
34use crate::compute::Operator;
35use crate::compute::and;
36use crate::compute::cast;
37use crate::compute::compare;
38use crate::compute::filter;
39use crate::compute::invert;
40use crate::compute::mask;
41use crate::compute::or;
42use crate::compute::take;
43
44/// Tests that filter and take operations produce consistent results.
45///
46/// # Invariant
47/// `filter(array, mask)` should equal `take(array, indices_where_mask_is_true)`
48///
49/// # Test Details
50/// - Creates a mask that keeps elements where index % 3 != 1
51/// - Applies filter with this mask
52/// - Creates indices array containing positions where mask is true
53/// - Applies take with these indices
54/// - Verifies both results are identical
55fn test_filter_take_consistency(array: &dyn Array) {
56    let len = array.len();
57    if len == 0 {
58        return;
59    }
60
61    // Create a test mask (keep elements where index % 3 != 1)
62    let mask_pattern: BitBuffer = (0..len).map(|i| i % 3 != 1).collect();
63    let mask = Mask::from_buffer(mask_pattern.clone());
64
65    // Filter the array
66    let filtered = filter(array, &mask).vortex_expect("filter should succeed in conformance test");
67
68    // Create indices where mask is true
69    let indices: Vec<u64> = mask_pattern
70        .iter()
71        .enumerate()
72        .filter_map(|(i, v)| v.then_some(i as u64))
73        .collect();
74    let indices_array = PrimitiveArray::from_iter(indices).into_array();
75
76    // Take using those indices
77    let taken =
78        take(array, &indices_array).vortex_expect("take should succeed in conformance test");
79
80    // Results should be identical
81    assert_eq!(
82        filtered.len(),
83        taken.len(),
84        "Filter and take should produce arrays of the same length. \
85         Filtered length: {}, Taken length: {}",
86        filtered.len(),
87        taken.len()
88    );
89
90    for i in 0..filtered.len() {
91        let filtered_val = filtered.scalar_at(i);
92        let taken_val = taken.scalar_at(i);
93        assert_eq!(
94            filtered_val, taken_val,
95            "Filter and take produced different values at index {i}. \
96             Filtered value: {filtered_val:?}, Taken value: {taken_val:?}"
97        );
98    }
99}
100
101/// Tests that double masking is consistent with combined mask.
102///
103/// # Invariant
104/// `mask(mask(array, mask1), mask2)` should equal `mask(array, mask1 | mask2)`
105///
106/// # Test Details
107/// - Creates two masks: mask1 (every 3rd element) and mask2 (every 2nd element)
108/// - Applies masks sequentially: first mask1, then mask2 on the result
109/// - Creates a combined mask using OR operation (element is masked if either mask is true)
110/// - Applies the combined mask directly to the original array
111/// - Verifies both approaches produce identical results
112///
113/// # Why This Matters
114/// This test ensures that mask operations compose correctly, which is critical for
115/// complex query operations that may apply multiple filters.
116fn test_double_mask_consistency(array: &dyn Array) {
117    let len = array.len();
118    if len == 0 {
119        return;
120    }
121
122    // Create two different mask patterns
123    let mask1: Mask = (0..len).map(|i| i % 3 == 0).collect();
124    let mask2: Mask = (0..len).map(|i| i % 2 == 0).collect();
125
126    // Apply masks sequentially
127    let first_masked = mask(array, &mask1).vortex_expect("mask should succeed in conformance test");
128    let double_masked =
129        mask(&first_masked, &mask2).vortex_expect("mask should succeed in conformance test");
130
131    // Create combined mask (OR operation - element is masked if EITHER mask is true)
132    let combined_pattern: BitBuffer = mask1
133        .to_bit_buffer()
134        .iter()
135        .zip(mask2.to_bit_buffer().iter())
136        .map(|(a, b)| a || b)
137        .collect();
138    let combined_mask = Mask::from_buffer(combined_pattern);
139
140    // Apply combined mask directly
141    let directly_masked =
142        mask(array, &combined_mask).vortex_expect("mask should succeed in conformance test");
143
144    // Results should be identical
145    assert_eq!(
146        double_masked.len(),
147        directly_masked.len(),
148        "Sequential masking and combined masking should produce arrays of the same length. \
149         Sequential length: {}, Combined length: {}",
150        double_masked.len(),
151        directly_masked.len()
152    );
153
154    for i in 0..double_masked.len() {
155        let double_val = double_masked.scalar_at(i);
156        let direct_val = directly_masked.scalar_at(i);
157        assert_eq!(
158            double_val, direct_val,
159            "Sequential masking and combined masking produced different values at index {i}. \
160             Sequential masking value: {double_val:?}, Combined masking value: {direct_val:?}\n\
161             This likely indicates an issue with how masks are composed in the array implementation."
162        );
163    }
164}
165
166/// Tests that filtering with an all-true mask preserves the array.
167///
168/// # Invariant
169/// `filter(array, all_true_mask)` should equal `array`
170///
171/// # Test Details
172/// - Creates a mask with all elements set to true
173/// - Applies filter with this mask
174/// - Verifies the result is identical to the original array
175///
176/// # Why This Matters
177/// This is an identity operation that should be optimized in implementations
178/// to avoid unnecessary copying.
179fn test_filter_identity(array: &dyn Array) {
180    let len = array.len();
181    if len == 0 {
182        return;
183    }
184
185    let all_true_mask = Mask::new_true(len);
186    let filtered =
187        filter(array, &all_true_mask).vortex_expect("filter should succeed in conformance test");
188
189    // Filtered array should be identical to original
190    assert_eq!(
191        filtered.len(),
192        array.len(),
193        "Filtering with all-true mask should preserve array length. \
194         Original length: {}, Filtered length: {}",
195        array.len(),
196        filtered.len()
197    );
198
199    for i in 0..len {
200        let original_val = array.scalar_at(i);
201        let filtered_val = filtered.scalar_at(i);
202        assert_eq!(
203            filtered_val, original_val,
204            "Filtering with all-true mask should preserve all values. \
205             Value at index {i} changed from {original_val:?} to {filtered_val:?}"
206        );
207    }
208}
209
210/// Tests that masking with an all-false mask preserves values while making them nullable.
211///
212/// # Invariant
213/// `mask(array, all_false_mask)` should have same values as `array` but with nullable type
214///
215/// # Test Details
216/// - Creates a mask with all elements set to false (no elements are nullified)
217/// - Applies mask operation
218/// - Verifies all values are preserved but the array type becomes nullable
219///
220/// # Why This Matters
221/// Masking always produces a nullable array, even when no values are actually masked.
222/// This test ensures the type system handles this correctly.
223fn test_mask_identity(array: &dyn Array) {
224    let len = array.len();
225    if len == 0 {
226        return;
227    }
228
229    let all_false_mask = Mask::new_false(len);
230    let masked =
231        mask(array, &all_false_mask).vortex_expect("mask should succeed in conformance test");
232
233    // Masked array should have same values (just nullable)
234    assert_eq!(
235        masked.len(),
236        array.len(),
237        "Masking with all-false mask should preserve array length. \
238         Original length: {}, Masked length: {}",
239        array.len(),
240        masked.len()
241    );
242
243    assert!(
244        masked.dtype().is_nullable(),
245        "Mask operation should always produce a nullable array, but dtype is {:?}",
246        masked.dtype()
247    );
248
249    for i in 0..len {
250        let original_val = array.scalar_at(i);
251        let masked_val = masked.scalar_at(i);
252        let expected_val = original_val.clone().into_nullable();
253        assert_eq!(
254            masked_val, expected_val,
255            "Masking with all-false mask should preserve values (as nullable). \
256             Value at index {i}: original = {original_val:?}, masked = {masked_val:?}, expected = {expected_val:?}"
257        );
258    }
259}
260
261/// Tests that slice and filter with contiguous mask produce same results.
262///
263/// # Invariant
264/// `filter(array, contiguous_true_mask)` should equal `slice(array, start, end)`
265///
266/// # Test Details
267/// - Creates a mask that is true only for indices 1, 2, and 3
268/// - Filters the array with this mask
269/// - Slices the array from index 1 to 4
270/// - Verifies both operations produce identical results
271///
272/// # Why This Matters
273/// When a filter mask represents a contiguous range, it should be equivalent to
274/// a slice operation. Some implementations may optimize this case.
275fn test_slice_filter_consistency(array: &dyn Array) {
276    let len = array.len();
277    if len < 4 {
278        return; // Need at least 4 elements for meaningful test
279    }
280
281    // Create a contiguous mask (true from index 1 to 3)
282    let mut mask_pattern = vec![false; len];
283    mask_pattern[1..4.min(len)].fill(true);
284
285    let mask = Mask::from_iter(mask_pattern);
286    let filtered = filter(array, &mask).vortex_expect("filter should succeed in conformance test");
287
288    // Slice should produce the same result
289    let sliced = array.slice(1..4.min(len));
290
291    assert_eq!(
292        filtered.len(),
293        sliced.len(),
294        "Filter with contiguous mask and slice should produce same length. \
295         Filtered length: {}, Sliced length: {}",
296        filtered.len(),
297        sliced.len()
298    );
299
300    for i in 0..filtered.len() {
301        let filtered_val = filtered.scalar_at(i);
302        let sliced_val = sliced.scalar_at(i);
303        assert_eq!(
304            filtered_val, sliced_val,
305            "Filter with contiguous mask and slice produced different values at index {i}. \
306             Filtered value: {filtered_val:?}, Sliced value: {sliced_val:?}"
307        );
308    }
309}
310
311/// Tests that take with sequential indices equals slice.
312///
313/// # Invariant
314/// `take(array, [1, 2, 3, ...])` should equal `slice(array, 1, n)`
315///
316/// # Test Details
317/// - Creates indices array with sequential values [1, 2, 3]
318/// - Takes elements at these indices
319/// - Slices array from index 1 to 4
320/// - Verifies both operations produce identical results
321///
322/// # Why This Matters
323/// Sequential takes are a common pattern that can be optimized to slice operations.
324fn test_take_slice_consistency(array: &dyn Array) {
325    let len = array.len();
326    if len < 3 {
327        return; // Need at least 3 elements
328    }
329
330    // Take indices [1, 2, 3]
331    let end = 4.min(len);
332    let indices = PrimitiveArray::from_iter((1..end).map(|i| i as u64)).into_array();
333    let taken = take(array, &indices).vortex_expect("take should succeed in conformance test");
334
335    // Slice from 1 to end
336    let sliced = array.slice(1..end);
337
338    assert_eq!(
339        taken.len(),
340        sliced.len(),
341        "Take with sequential indices and slice should produce same length. \
342         Taken length: {}, Sliced length: {}",
343        taken.len(),
344        sliced.len()
345    );
346
347    for i in 0..taken.len() {
348        let taken_val = taken.scalar_at(i);
349        let sliced_val = sliced.scalar_at(i);
350        assert_eq!(
351            taken_val, sliced_val,
352            "Take with sequential indices and slice produced different values at index {i}. \
353             Taken value: {taken_val:?}, Sliced value: {sliced_val:?}"
354        );
355    }
356}
357
358/// Tests that filter preserves relative ordering
359fn test_filter_preserves_order(array: &dyn Array) {
360    let len = array.len();
361    if len < 4 {
362        return;
363    }
364
365    // Create a mask that selects elements at indices 0, 2, 3
366    let mask_pattern: Vec<bool> = (0..len).map(|i| i == 0 || i == 2 || i == 3).collect();
367    let mask = Mask::from_iter(mask_pattern);
368
369    let filtered = filter(array, &mask).vortex_expect("filter should succeed in conformance test");
370
371    // Verify the filtered array contains the right elements in order
372    assert_eq!(filtered.len(), 3.min(len));
373    if len >= 4 {
374        assert_eq!(filtered.scalar_at(0), array.scalar_at(0));
375        assert_eq!(filtered.scalar_at(1), array.scalar_at(2),);
376        assert_eq!(filtered.scalar_at(2), array.scalar_at(3));
377    }
378}
379
380/// Tests that take with repeated indices works correctly
381fn test_take_repeated_indices(array: &dyn Array) {
382    let len = array.len();
383    if len == 0 {
384        return;
385    }
386
387    // Take the first element three times
388    let indices = PrimitiveArray::from_iter([0u64, 0, 0]).into_array();
389    let taken = take(array, &indices).vortex_expect("take should succeed in conformance test");
390
391    assert_eq!(taken.len(), 3);
392    for i in 0..3 {
393        assert_eq!(taken.scalar_at(i), array.scalar_at(0),);
394    }
395}
396
397/// Tests mask and filter interaction with nulls
398fn test_mask_filter_null_consistency(array: &dyn Array) {
399    let len = array.len();
400    if len < 3 {
401        return;
402    }
403
404    // First mask some elements
405    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
406    let mask_array = Mask::from_iter(mask_pattern);
407    let masked = mask(array, &mask_array).vortex_expect("mask should succeed in conformance test");
408
409    // Then filter to remove the nulls
410    let filter_pattern: Vec<bool> = (0..len).map(|i| i % 2 != 0).collect();
411    let filter_mask = Mask::from_iter(filter_pattern);
412    let filtered =
413        filter(&masked, &filter_mask).vortex_expect("filter should succeed in conformance test");
414
415    // This should be equivalent to directly filtering the original array
416    let direct_filtered =
417        filter(array, &filter_mask).vortex_expect("filter should succeed in conformance test");
418
419    assert_eq!(filtered.len(), direct_filtered.len());
420    for i in 0..filtered.len() {
421        assert_eq!(filtered.scalar_at(i), direct_filtered.scalar_at(i));
422    }
423}
424
425/// Tests that empty operations are consistent
426fn test_empty_operations_consistency(array: &dyn Array) {
427    let len = array.len();
428
429    // Empty filter
430    let empty_filter = filter(array, &Mask::new_false(len))
431        .vortex_expect("filter should succeed in conformance test");
432    assert_eq!(empty_filter.len(), 0);
433    assert_eq!(empty_filter.dtype(), array.dtype());
434
435    // Empty take
436    let empty_indices = PrimitiveArray::empty::<u64>(Nullability::NonNullable).into_array();
437    let empty_take =
438        take(array, &empty_indices).vortex_expect("take should succeed in conformance test");
439    assert_eq!(empty_take.len(), 0);
440    assert_eq!(empty_take.dtype(), array.dtype());
441
442    // Empty slice (if array is non-empty)
443    if len > 0 {
444        let empty_slice = array.slice(0..0);
445        assert_eq!(empty_slice.len(), 0);
446        assert_eq!(empty_slice.dtype(), array.dtype());
447    }
448}
449
450/// Tests that take preserves array properties
451fn test_take_preserves_properties(array: &dyn Array) {
452    let len = array.len();
453    if len == 0 {
454        return;
455    }
456
457    // Take all elements in original order
458    let indices = PrimitiveArray::from_iter((0..len).map(|i| i as u64)).into_array();
459    let taken = take(array, &indices).vortex_expect("take should succeed in conformance test");
460
461    // Should be identical to original
462    assert_eq!(taken.len(), array.len());
463    assert_eq!(taken.dtype(), array.dtype());
464    for i in 0..len {
465        assert_eq!(taken.scalar_at(i), array.scalar_at(i),);
466    }
467}
468
469/// Tests consistency with nullable indices.
470///
471/// # Invariant
472/// `take(array, [Some(0), None, Some(2)])` should produce `[array[0], null, array[2]]`
473///
474/// # Test Details
475/// - Creates an indices array with null at position 1: `[Some(0), None, Some(2)]`
476/// - Takes elements using these indices
477/// - Verifies that:
478///   - Position 0 contains the value from array index 0
479///   - Position 1 contains null
480///   - Position 2 contains the value from array index 2
481///   - The result array has nullable type
482///
483/// # Why This Matters
484/// Nullable indices are a powerful feature that allows introducing nulls during
485/// a take operation, which is useful for outer joins and similar operations.
486fn test_nullable_indices_consistency(array: &dyn Array) {
487    let len = array.len();
488    if len < 3 {
489        return; // Need at least 3 elements to test indices 0 and 2
490    }
491
492    // Create nullable indices where some indices are null
493    let indices = PrimitiveArray::from_option_iter([Some(0u64), None, Some(2u64)]).into_array();
494
495    let taken = take(array, &indices).vortex_expect("take should succeed in conformance test");
496
497    // Result should have nulls where indices were null
498    assert_eq!(
499        taken.len(),
500        3,
501        "Take with nullable indices should produce array of length 3, got {}",
502        taken.len()
503    );
504
505    assert!(
506        taken.dtype().is_nullable(),
507        "Take with nullable indices should produce nullable array, but dtype is {:?}",
508        taken.dtype()
509    );
510
511    // Check first element (from index 0)
512    let expected_0 = array.scalar_at(0).into_nullable();
513    let actual_0 = taken.scalar_at(0);
514    assert_eq!(
515        actual_0, expected_0,
516        "Take with nullable indices: element at position 0 should be from array index 0. \
517         Expected: {expected_0:?}, Actual: {actual_0:?}"
518    );
519
520    // Check second element (should be null)
521    let actual_1 = taken.scalar_at(1);
522    assert!(
523        actual_1.is_null(),
524        "Take with nullable indices: element at position 1 should be null, but got {actual_1:?}"
525    );
526
527    // Check third element (from index 2)
528    let expected_2 = array.scalar_at(2).into_nullable();
529    let actual_2 = taken.scalar_at(2);
530    assert_eq!(
531        actual_2, expected_2,
532        "Take with nullable indices: element at position 2 should be from array index 2. \
533         Expected: {expected_2:?}, Actual: {actual_2:?}"
534    );
535}
536
537/// Tests large array consistency
538fn test_large_array_consistency(array: &dyn Array) {
539    let len = array.len();
540    if len < 1000 {
541        return;
542    }
543
544    // Test with every 10th element
545    let indices: Vec<u64> = (0..len).step_by(10).map(|i| i as u64).collect();
546    let indices_array = PrimitiveArray::from_iter(indices).into_array();
547    let taken =
548        take(array, &indices_array).vortex_expect("take should succeed in conformance test");
549
550    // Create equivalent filter mask
551    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 10 == 0).collect();
552    let mask = Mask::from_iter(mask_pattern);
553    let filtered = filter(array, &mask).vortex_expect("filter should succeed in conformance test");
554
555    // Results should match
556    assert_eq!(taken.len(), filtered.len());
557    for i in 0..taken.len() {
558        assert_eq!(taken.scalar_at(i), filtered.scalar_at(i),);
559    }
560}
561
562/// Tests that comparison operations follow inverse relationships.
563///
564/// # Invariants
565/// - `compare(array, value, Eq)` is the inverse of `compare(array, value, NotEq)`
566/// - `compare(array, value, Gt)` is the inverse of `compare(array, value, Lte)`
567/// - `compare(array, value, Lt)` is the inverse of `compare(array, value, Gte)`
568///
569/// # Test Details
570/// - Creates comparison results for each operator
571/// - Verifies that inverse operations produce opposite boolean values
572/// - Tests with multiple scalar values to ensure consistency
573///
574/// # Why This Matters
575/// Comparison operations must maintain logical consistency across encodings.
576/// This test catches bugs where an encoding might implement one comparison
577/// correctly but fail on its logical inverse.
578fn test_comparison_inverse_consistency(array: &dyn Array) {
579    let len = array.len();
580    if len == 0 {
581        return;
582    }
583
584    // Skip non-comparable types.
585    match array.dtype() {
586        DType::Null | DType::Extension(_) | DType::Struct(..) | DType::List(..) => return,
587        _ => {}
588    }
589
590    // Get a test value from the middle of the array
591    let test_scalar = if len == 0 {
592        return;
593    } else {
594        array.scalar_at(len / 2)
595    };
596
597    // Test Eq vs NotEq
598    let const_array = crate::arrays::ConstantArray::new(test_scalar, len);
599    if let (Ok(eq_result), Ok(neq_result)) = (
600        compare(array, const_array.as_ref(), Operator::Eq),
601        compare(array, const_array.as_ref(), Operator::NotEq),
602    ) {
603        let inverted_eq =
604            invert(&eq_result).vortex_expect("invert should succeed in conformance test");
605
606        assert_eq!(
607            inverted_eq.len(),
608            neq_result.len(),
609            "Inverted Eq should have same length as NotEq"
610        );
611
612        for i in 0..inverted_eq.len() {
613            let inv_val = inverted_eq.scalar_at(i);
614            let neq_val = neq_result.scalar_at(i);
615            assert_eq!(
616                inv_val, neq_val,
617                "At index {i}: NOT(Eq) should equal NotEq. \
618                 NOT(Eq) = {inv_val:?}, NotEq = {neq_val:?}"
619            );
620        }
621    }
622
623    // Test Gt vs Lte
624    if let (Ok(gt_result), Ok(lte_result)) = (
625        compare(array, const_array.as_ref(), Operator::Gt),
626        compare(array, const_array.as_ref(), Operator::Lte),
627    ) {
628        let inverted_gt =
629            invert(&gt_result).vortex_expect("invert should succeed in conformance test");
630
631        for i in 0..inverted_gt.len() {
632            let inv_val = inverted_gt.scalar_at(i);
633            let lte_val = lte_result.scalar_at(i);
634            assert_eq!(
635                inv_val, lte_val,
636                "At index {i}: NOT(Gt) should equal Lte. \
637                 NOT(Gt) = {inv_val:?}, Lte = {lte_val:?}"
638            );
639        }
640    }
641
642    // Test Lt vs Gte
643    if let (Ok(lt_result), Ok(gte_result)) = (
644        compare(array, const_array.as_ref(), Operator::Lt),
645        compare(array, const_array.as_ref(), Operator::Gte),
646    ) {
647        let inverted_lt =
648            invert(&lt_result).vortex_expect("invert should succeed in conformance test");
649
650        for i in 0..inverted_lt.len() {
651            let inv_val = inverted_lt.scalar_at(i);
652            let gte_val = gte_result.scalar_at(i);
653            assert_eq!(
654                inv_val, gte_val,
655                "At index {i}: NOT(Lt) should equal Gte. \
656                 NOT(Lt) = {inv_val:?}, Gte = {gte_val:?}"
657            );
658        }
659    }
660}
661
662/// Tests that comparison operations maintain proper symmetry relationships.
663///
664/// # Invariants
665/// - `compare(array, value, Gt)` should equal `compare_scalar_array(value, array, Lt)`
666/// - `compare(array, value, Lt)` should equal `compare_scalar_array(value, array, Gt)`
667/// - `compare(array, value, Eq)` should equal `compare_scalar_array(value, array, Eq)`
668///
669/// # Test Details
670/// - Compares array-scalar operations with their symmetric scalar-array versions
671/// - Verifies that ordering relationships are properly reversed
672/// - Tests equality which should be symmetric
673///
674/// # Why This Matters
675/// Ensures that comparison operations maintain mathematical ordering properties
676/// regardless of operand order.
677fn test_comparison_symmetry_consistency(array: &dyn Array) {
678    let len = array.len();
679    if len == 0 {
680        return;
681    }
682
683    // Skip non-comparable types.
684    match array.dtype() {
685        DType::Null | DType::Extension(_) | DType::Struct(..) | DType::List(..) => return,
686        _ => {}
687    }
688
689    // Get test values
690    let test_scalar = if len == 2 {
691        return;
692    } else {
693        array.scalar_at(len / 2)
694    };
695
696    // Create a constant array with the test scalar for reverse comparison
697    let const_array = crate::arrays::ConstantArray::new(test_scalar, len);
698
699    // Test Gt vs Lt symmetry
700    if let (Ok(arr_gt_scalar), Ok(scalar_lt_arr)) = (
701        compare(array, const_array.as_ref(), Operator::Gt),
702        compare(const_array.as_ref(), array, Operator::Lt),
703    ) {
704        assert_eq!(
705            arr_gt_scalar.len(),
706            scalar_lt_arr.len(),
707            "Symmetric comparisons should have same length"
708        );
709
710        for i in 0..arr_gt_scalar.len() {
711            let arr_gt = arr_gt_scalar.scalar_at(i);
712            let scalar_lt = scalar_lt_arr.scalar_at(i);
713            assert_eq!(
714                arr_gt, scalar_lt,
715                "At index {i}: (array > scalar) should equal (scalar < array). \
716                 array > scalar = {arr_gt:?}, scalar < array = {scalar_lt:?}"
717            );
718        }
719    }
720
721    // Test Eq symmetry
722    if let (Ok(arr_eq_scalar), Ok(scalar_eq_arr)) = (
723        compare(array, const_array.as_ref(), Operator::Eq),
724        compare(const_array.as_ref(), array, Operator::Eq),
725    ) {
726        for i in 0..arr_eq_scalar.len() {
727            let arr_eq = arr_eq_scalar.scalar_at(i);
728            let scalar_eq = scalar_eq_arr.scalar_at(i);
729            assert_eq!(
730                arr_eq, scalar_eq,
731                "At index {i}: (array == scalar) should equal (scalar == array). \
732                 array == scalar = {arr_eq:?}, scalar == array = {scalar_eq:?}"
733            );
734        }
735    }
736}
737
738/// Tests that boolean operations follow De Morgan's laws.
739///
740/// # Invariants
741/// - `NOT(A AND B)` equals `(NOT A) OR (NOT B)`
742/// - `NOT(A OR B)` equals `(NOT A) AND (NOT B)`
743///
744/// # Test Details
745/// - If the array is boolean, uses it directly for testing boolean operations
746/// - Creates two boolean masks from patterns based on the array
747/// - Computes AND/OR operations and their inversions
748/// - Verifies De Morgan's laws hold for all elements
749///
750/// # Why This Matters
751/// Boolean operations must maintain logical consistency across encodings.
752/// This test catches bugs where encodings might optimize boolean operations
753/// incorrectly, breaking fundamental logical properties.
754fn test_boolean_demorgan_consistency(array: &dyn Array) {
755    if !matches!(array.dtype(), DType::Bool(_)) {
756        return;
757    }
758
759    let mask = {
760        let mask_pattern: Vec<bool> = (0..array.len()).map(|i| i % 3 == 0).collect();
761        BoolArray::from_iter(mask_pattern)
762    };
763    let mask = mask.as_ref();
764
765    // Test first De Morgan's law: NOT(A AND B) = (NOT A) OR (NOT B)
766    if let (Ok(a_and_b), Ok(not_a), Ok(not_b)) = (and(array, mask), invert(array), invert(mask)) {
767        let not_a_and_b =
768            invert(&a_and_b).vortex_expect("invert should succeed in conformance test");
769        let not_a_or_not_b =
770            or(&not_a, &not_b).vortex_expect("or should succeed in conformance test");
771
772        assert_eq!(
773            not_a_and_b.len(),
774            not_a_or_not_b.len(),
775            "De Morgan's law results should have same length"
776        );
777
778        for i in 0..not_a_and_b.len() {
779            let left = not_a_and_b.scalar_at(i);
780            let right = not_a_or_not_b.scalar_at(i);
781            assert_eq!(
782                left, right,
783                "De Morgan's first law failed at index {i}: \
784                 NOT(A AND B) = {left:?}, (NOT A) OR (NOT B) = {right:?}"
785            );
786        }
787    }
788
789    // Test second De Morgan's law: NOT(A OR B) = (NOT A) AND (NOT B)
790    if let (Ok(a_or_b), Ok(not_a), Ok(not_b)) = (or(array, mask), invert(array), invert(mask)) {
791        let not_a_or_b = invert(&a_or_b).vortex_expect("invert should succeed in conformance test");
792        let not_a_and_not_b =
793            and(&not_a, &not_b).vortex_expect("and should succeed in conformance test");
794
795        for i in 0..not_a_or_b.len() {
796            let left = not_a_or_b.scalar_at(i);
797            let right = not_a_and_not_b.scalar_at(i);
798            assert_eq!(
799                left, right,
800                "De Morgan's second law failed at index {i}: \
801                 NOT(A OR B) = {left:?}, (NOT A) AND (NOT B) = {right:?}"
802            );
803        }
804    }
805}
806
807/// Tests that slice and aggregate operations produce consistent results.
808///
809/// # Invariants
810/// - Aggregating a sliced array should equal aggregating the corresponding
811///   elements from the canonical form
812/// - This applies to sum, count, min/max, and other aggregate functions
813///
814/// # Test Details
815/// - Slices the array and computes aggregates
816/// - Compares against aggregating the canonical form's slice
817/// - Tests multiple aggregate functions where applicable
818///
819/// # Why This Matters
820/// Aggregate operations on sliced arrays must produce correct results
821/// regardless of the underlying encoding's offset handling.
822fn test_slice_aggregate_consistency(array: &dyn Array) {
823    use vortex_dtype::DType;
824
825    use crate::compute::min_max;
826    use crate::compute::nan_count;
827    use crate::compute::sum;
828
829    let len = array.len();
830    if len < 5 {
831        return; // Need enough elements for meaningful slice
832    }
833
834    // Define slice bounds
835    let start = 1;
836    let end = (len - 1).min(start + 10); // Take up to 10 elements
837
838    // Get sliced array and canonical slice
839    let sliced = array.slice(start..end);
840    let canonical = array.to_canonical();
841    let canonical_sliced = canonical.as_ref().slice(start..end);
842
843    // Test null count through invalid_count
844    assert_eq!(
845        sliced.invalid_count(),
846        canonical_sliced.invalid_count(),
847        "null_count on sliced array should match canonical. \
848             Sliced: {}, Canonical: {}",
849        sliced.invalid_count(),
850        canonical_sliced.invalid_count()
851    );
852
853    // Test sum for numeric types
854    if !matches!(array.dtype(), DType::Primitive(..)) {
855        return;
856    }
857
858    if let (Ok(slice_sum), Ok(canonical_sum)) = (sum(&sliced), sum(&canonical_sliced)) {
859        // Compare sum scalars
860        assert_eq!(
861            slice_sum, canonical_sum,
862            "sum on sliced array should match canonical. \
863                 Sliced: {slice_sum:?}, Canonical: {canonical_sum:?}"
864        );
865    }
866
867    // Test min_max
868    if let (Ok(slice_minmax), Ok(canonical_minmax)) = (min_max(&sliced), min_max(&canonical_sliced))
869    {
870        match (slice_minmax, canonical_minmax) {
871            (Some(s_result), Some(c_result)) => {
872                assert_eq!(
873                    s_result.min, c_result.min,
874                    "min on sliced array should match canonical. \
875                         Sliced: {:?}, Canonical: {:?}",
876                    s_result.min, c_result.min
877                );
878                assert_eq!(
879                    s_result.max, c_result.max,
880                    "max on sliced array should match canonical. \
881                         Sliced: {:?}, Canonical: {:?}",
882                    s_result.max, c_result.max
883                );
884            }
885            (None, None) => {} // Both empty, OK
886            _ => vortex_panic!("min_max results don't match"),
887        }
888    }
889
890    // Test nan_count for floating point types
891    if array.dtype().is_float()
892        && let (Ok(slice_nan_count), Ok(canonical_nan_count)) =
893            (nan_count(&sliced), nan_count(&canonical_sliced))
894    {
895        assert_eq!(
896            slice_nan_count, canonical_nan_count,
897            "nan_count on sliced array should match canonical. \
898                 Sliced: {slice_nan_count}, Canonical: {canonical_nan_count}"
899        );
900    }
901}
902
903/// Tests that cast operations preserve array properties when sliced.
904///
905/// # Invariant
906/// `cast(slice(array, start, end), dtype)` should equal `slice(cast(array, dtype), start, end)`
907///
908/// # Test Details
909/// - Slices the array from index 2 to 7 (or len-2 if smaller)
910/// - Casts the sliced array to a different type
911/// - Compares against the canonical form of the array (without slicing or casting the canonical form)
912/// - Verifies both approaches produce identical results
913///
914/// # Why This Matters
915/// This test specifically catches bugs where encodings (like RunEndArray) fail to preserve
916/// offset information during cast operations. Such bugs can lead to incorrect data being
917/// returned after casting a sliced array.
918fn test_cast_slice_consistency(array: &dyn Array) {
919    let len = array.len();
920    if len < 5 {
921        return; // Need at least 5 elements for meaningful slice
922    }
923
924    // Define slice bounds
925    let start = 2;
926    let end = 7.min(len - 2).max(start + 1); // Ensure we have at least 1 element
927
928    // Get canonical form of the original array
929    let canonical = array.to_canonical();
930
931    // Choose appropriate target dtype based on the array's type
932    let target_dtypes = match array.dtype() {
933        DType::Null => vec![],
934        DType::Bool(nullability) => vec![
935            DType::Primitive(PType::U8, *nullability),
936            DType::Primitive(PType::I32, *nullability),
937        ],
938        DType::Primitive(ptype, nullability) => {
939            let mut targets = vec![];
940            // Test nullability changes
941            let opposite_nullability = match nullability {
942                Nullability::NonNullable => Nullability::Nullable,
943                Nullability::Nullable => Nullability::NonNullable,
944            };
945            targets.push(DType::Primitive(*ptype, opposite_nullability));
946
947            // Test widening casts
948            match ptype {
949                PType::U8 => {
950                    targets.push(DType::Primitive(PType::U16, *nullability));
951                    targets.push(DType::Primitive(PType::I16, *nullability));
952                }
953                PType::U16 => {
954                    targets.push(DType::Primitive(PType::U32, *nullability));
955                    targets.push(DType::Primitive(PType::I32, *nullability));
956                }
957                PType::U32 => {
958                    targets.push(DType::Primitive(PType::U64, *nullability));
959                    targets.push(DType::Primitive(PType::I64, *nullability));
960                }
961                PType::U64 => {
962                    targets.push(DType::Primitive(PType::F64, *nullability));
963                }
964                PType::I8 => {
965                    targets.push(DType::Primitive(PType::I16, *nullability));
966                    targets.push(DType::Primitive(PType::F32, *nullability));
967                }
968                PType::I16 => {
969                    targets.push(DType::Primitive(PType::I32, *nullability));
970                    targets.push(DType::Primitive(PType::F32, *nullability));
971                }
972                PType::I32 => {
973                    targets.push(DType::Primitive(PType::I64, *nullability));
974                    targets.push(DType::Primitive(PType::F64, *nullability));
975                }
976                PType::I64 => {
977                    targets.push(DType::Primitive(PType::F64, *nullability));
978                }
979                PType::F16 => {
980                    targets.push(DType::Primitive(PType::F32, *nullability));
981                }
982                PType::F32 => {
983                    targets.push(DType::Primitive(PType::F64, *nullability));
984                    targets.push(DType::Primitive(PType::I32, *nullability));
985                }
986                PType::F64 => {
987                    targets.push(DType::Primitive(PType::I64, *nullability));
988                }
989            }
990            targets
991        }
992        DType::Utf8(nullability) => {
993            let opposite = match nullability {
994                Nullability::NonNullable => Nullability::Nullable,
995                Nullability::Nullable => Nullability::NonNullable,
996            };
997            vec![DType::Utf8(opposite), DType::Binary(*nullability)]
998        }
999        DType::Binary(nullability) => {
1000            let opposite = match nullability {
1001                Nullability::NonNullable => Nullability::Nullable,
1002                Nullability::Nullable => Nullability::NonNullable,
1003            };
1004            vec![
1005                DType::Binary(opposite),
1006                DType::Utf8(*nullability), // May fail if not valid UTF-8
1007            ]
1008        }
1009        DType::Decimal(decimal_type, nullability) => {
1010            let opposite = match nullability {
1011                Nullability::NonNullable => Nullability::Nullable,
1012                Nullability::Nullable => Nullability::NonNullable,
1013            };
1014            vec![DType::Decimal(*decimal_type, opposite)]
1015        }
1016        DType::Struct(fields, nullability) => {
1017            let opposite = match nullability {
1018                Nullability::NonNullable => Nullability::Nullable,
1019                Nullability::Nullable => Nullability::NonNullable,
1020            };
1021            vec![DType::Struct(fields.clone(), opposite)]
1022        }
1023        DType::List(element_type, nullability) => {
1024            let opposite = match nullability {
1025                Nullability::NonNullable => Nullability::Nullable,
1026                Nullability::Nullable => Nullability::NonNullable,
1027            };
1028            vec![DType::List(element_type.clone(), opposite)]
1029        }
1030        DType::FixedSizeList(element_type, list_size, nullability) => {
1031            let opposite = match nullability {
1032                Nullability::NonNullable => Nullability::Nullable,
1033                Nullability::Nullable => Nullability::NonNullable,
1034            };
1035            vec![DType::FixedSizeList(
1036                element_type.clone(),
1037                *list_size,
1038                opposite,
1039            )]
1040        }
1041        DType::Extension(_) => vec![], // Extension types typically only cast to themselves
1042    };
1043
1044    // Test each target dtype
1045    for target_dtype in target_dtypes {
1046        // Slice the array
1047        let sliced = array.slice(start..end);
1048
1049        // Try to cast the sliced array
1050        let slice_then_cast = match cast(&sliced, &target_dtype) {
1051            Ok(result) => result,
1052            Err(_) => continue, // Skip if cast fails
1053        };
1054
1055        // Verify against canonical form
1056        assert_eq!(
1057            slice_then_cast.len(),
1058            end - start,
1059            "Sliced and casted array should have length {}, but has {}",
1060            end - start,
1061            slice_then_cast.len()
1062        );
1063
1064        // Compare each value against the canonical form
1065        for i in 0..slice_then_cast.len() {
1066            let slice_cast_val = slice_then_cast.scalar_at(i);
1067
1068            // Get the corresponding value from the canonical array (adjusted for slice offset)
1069            let canonical_val = canonical.as_ref().scalar_at(start + i);
1070
1071            // Cast the canonical scalar to the target dtype
1072            let expected_val = match canonical_val.cast(&target_dtype) {
1073                Ok(val) => val,
1074                Err(_) => {
1075                    // If scalar cast fails, we can't compare - skip this target dtype
1076                    // This can happen for some type conversions that aren't supported at scalar level
1077                    break;
1078                }
1079            };
1080
1081            assert_eq!(
1082                slice_cast_val,
1083                expected_val,
1084                "Cast of sliced array produced incorrect value at index {i}. \
1085                 Got: {slice_cast_val:?}, Expected: {expected_val:?} \
1086                 (canonical value at index {}: {canonical_val:?})\n\
1087                 This likely indicates the array encoding doesn't preserve offset information during cast.",
1088                start + i
1089            );
1090        }
1091
1092        // Also test the other way: cast then slice
1093        let casted = match cast(array, &target_dtype) {
1094            Ok(result) => result,
1095            Err(_) => continue, // Skip if cast fails
1096        };
1097        let cast_then_slice = casted.slice(start..end);
1098
1099        // Verify the two approaches produce identical results
1100        assert_eq!(
1101            slice_then_cast.len(),
1102            cast_then_slice.len(),
1103            "Slice-then-cast and cast-then-slice should produce arrays of the same length"
1104        );
1105
1106        for i in 0..slice_then_cast.len() {
1107            let slice_cast_val = slice_then_cast.scalar_at(i);
1108            let cast_slice_val = cast_then_slice.scalar_at(i);
1109            assert_eq!(
1110                slice_cast_val, cast_slice_val,
1111                "Slice-then-cast and cast-then-slice produced different values at index {i}. \
1112                 Slice-then-cast: {slice_cast_val:?}, Cast-then-slice: {cast_slice_val:?}"
1113            );
1114        }
1115    }
1116}
1117
1118/// Run all consistency tests on an array.
1119///
1120/// This function executes a comprehensive suite of consistency tests that verify
1121/// the correctness of compute operations on Vortex arrays.
1122///
1123/// # Test Suite Overview
1124///
1125/// ## Core Operation Consistency
1126/// - **Filter/Take**: Verifies `filter(array, mask)` equals `take(array, true_indices)`
1127/// - **Mask Composition**: Ensures sequential masks equal combined masks
1128/// - **Slice/Filter**: Checks contiguous filters equal slice operations
1129/// - **Take/Slice**: Validates sequential takes equal slice operations
1130/// - **Cast/Slice**: Ensures cast operations preserve sliced array properties
1131///
1132/// ## Boolean Operations
1133/// - **De Morgan's Laws**: Verifies boolean operations follow logical laws
1134///
1135/// ## Comparison Operations
1136/// - **Inverse Relationships**: Verifies logical inverses (Eq/NotEq, Gt/Lte, Lt/Gte)
1137/// - **Symmetry**: Ensures proper ordering relationships when operands are swapped
1138///
1139/// ## Aggregate Operations
1140/// - **Slice/Aggregate**: Verifies aggregates on sliced arrays match canonical
1141///
1142/// ## Identity Operations
1143/// - **Filter Identity**: All-true mask preserves the array
1144/// - **Mask Identity**: All-false mask preserves values (as nullable)
1145/// - **Take Identity**: Taking all indices preserves the array
1146///
1147/// ## Edge Cases
1148/// - **Empty Operations**: Empty filters, takes, and slices behave correctly
1149/// - **Single Element**: Operations work with single-element arrays
1150/// - **Repeated Indices**: Take with duplicate indices works correctly
1151///
1152/// ## Null Handling
1153/// - **Nullable Indices**: Null indices produce null values
1154/// - **Mask/Filter Interaction**: Masking then filtering behaves predictably
1155///
1156/// ## Large Arrays
1157/// - **Performance**: Operations scale correctly to large arrays (1000+ elements)
1158/// ```text
1159pub fn test_array_consistency(array: &dyn Array) {
1160    // Core operation consistency
1161    test_filter_take_consistency(array);
1162    test_double_mask_consistency(array);
1163    test_slice_filter_consistency(array);
1164    test_take_slice_consistency(array);
1165    test_cast_slice_consistency(array);
1166
1167    // Boolean operations
1168    test_boolean_demorgan_consistency(array);
1169
1170    // Comparison operations
1171    test_comparison_inverse_consistency(array);
1172    test_comparison_symmetry_consistency(array);
1173
1174    // Aggregate operations
1175    test_slice_aggregate_consistency(array);
1176
1177    // Identity operations
1178    test_filter_identity(array);
1179    test_mask_identity(array);
1180    test_take_preserves_properties(array);
1181
1182    // Ordering and correctness
1183    test_filter_preserves_order(array);
1184    test_take_repeated_indices(array);
1185
1186    // Null handling
1187    test_mask_filter_null_consistency(array);
1188    test_nullable_indices_consistency(array);
1189
1190    // Edge cases
1191    test_empty_operations_consistency(array);
1192    test_large_array_consistency(array);
1193}