Skip to main content

vortex_array/compute/conformance/
consistency.rs

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