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