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