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