This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-11642-24c53c7b0803d32575e5bcb302bf78065ded31b6
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit c0970925a2bb29efe2f855305a76986959171685
Author: ump45nose <[email protected]>
AuthorDate: Sun Oct 18 02:37:20 2026 +0000

    fix(arrow-data): check view nulls at the right index in `ArrayData` 
equality (#11642)
    
    # Which issue does this PR close?
    
    - Closes #11583.
    
    # Rationale for this change
    
    When a list, list view, fixed-size list or struct compares a `Utf8View`
    / `BinaryView` child range that doesn't start at 0, `byte_view_equal`
    read the null bit of the wrong child slot. Arrays that only differ in
    the data under a null compared unequal, and a real difference in a valid
    slot could be skipped and compare equal.
    
    # What changes are included in this PR?
    
    `byte_view_equal` now checks `lhs.is_null(lhs_start + idx)` instead of
    `lhs.is_null(idx)`, since `idx` counts from the start of the compared
    range. The views it compares were already offset by `lhs_start`, so this
    only lines the null check up with them.
    
    # Are these changes tested?
    
    Yes. `test_list_of_view_child_nulls_past_start` in
    `arrow/tests/array_equal.rs` builds a two-row list over a `Utf8View` and
    a `BinaryView` child and compares the second row sliced off, covering
    both directions: two `[[null]]` values with different hidden data (now
    equal) and two valid rows with different values next to a null at child
    index 0 (now unequal). It fails without the fix.
    
    # Are there any significant user-facing changes?
    
    No.
    
    AI disclosure: I used an AI coding assistant to write the fix, the test
    and this description, and I reviewed all of it.
    
    Co-authored-by: ump45nose <[email protected]>
---
 arrow-data/src/equal/byte_view.rs |  3 ++-
 arrow/tests/array_equal.rs        | 49 ++++++++++++++++++++++++++++++++++++---
 2 files changed, 48 insertions(+), 4 deletions(-)

diff --git a/arrow-data/src/equal/byte_view.rs 
b/arrow-data/src/equal/byte_view.rs
index 7c6001e9a2..248328bc21 100644
--- a/arrow-data/src/equal/byte_view.rs
+++ b/arrow-data/src/equal/byte_view.rs
@@ -32,7 +32,8 @@ pub(super) fn byte_view_equal(
     for (idx, (l, r)) in lhs_views.iter().zip(rhs_views).enumerate() {
         // Only checking one null mask here because by the time the control 
flow reaches
         // this point, the equality of the two masks would have already been 
verified.
-        if lhs.is_null(idx) {
+        // `idx` counts from `lhs_start`, so offset it to index the null mask.
+        if lhs.is_null(lhs_start + idx) {
             continue;
         }
 
diff --git a/arrow/tests/array_equal.rs b/arrow/tests/array_equal.rs
index 9ccd8dda3e..baaa54ffe0 100644
--- a/arrow/tests/array_equal.rs
+++ b/arrow/tests/array_equal.rs
@@ -27,10 +27,10 @@ use arrow_array::builder::{
 };
 use arrow_array::cast::AsArray;
 use arrow_array::{
-    DictionaryArray, FixedSizeListArray, Float32Array, GenericListViewArray, 
PrimitiveArray,
-    RunArray, StringViewArray,
+    BinaryViewArray, DictionaryArray, FixedSizeListArray, Float32Array, 
GenericListViewArray,
+    PrimitiveArray, RunArray, StringViewArray,
 };
-use arrow_buffer::{Buffer, ScalarBuffer, ToByteSlice};
+use arrow_buffer::{Buffer, NullBuffer, OffsetBuffer, ScalarBuffer, 
ToByteSlice};
 use arrow_data::{ArrayData, ArrayDataBuilder};
 use arrow_schema::{DataType, Field, Fields, UnionFields};
 use arrow_select::take::take;
@@ -358,6 +358,49 @@ fn test_string_view_equal() {
     test_equal(&a1_s, &a2_s, true);
 }
 
+/// Builds `[[child[0]], [child[1]]]` with the given child validity and slices 
it to its second
+/// row, so the equality check compares a child range that starts at 1 instead 
of 0.
+fn sliced_list_of_view(child: ArrayData, validity: [bool; 2]) -> ListArray {
+    let child = child
+        .into_builder()
+        .nulls(Some(NullBuffer::from(validity.to_vec())))
+        .build()
+        .unwrap();
+    let field = Arc::new(Field::new("item", child.data_type().clone(), true));
+    let list = ListArray::new(
+        field,
+        OffsetBuffer::from_lengths([1, 1]),
+        make_array(child),
+        None,
+    );
+    list.slice(1, 1)
+}
+
+// https://github.com/apache/arrow-rs/issues/11583
+#[test]
+fn test_list_of_view_child_nulls_past_start() {
+    let long_a = "a long string that is not inlined in the view";
+    let long_b = "another long string that is not inlined either";
+
+    let children: [fn(&str) -> ArrayData; 2] = [
+        |second| StringViewArray::from(vec!["x", second]).into_data(),
+        |second| BinaryViewArray::from(vec![b"x".as_slice(), 
second.as_bytes()]).into_data(),
+    ];
+
+    for child in children {
+        // Both are [[null]] and differ only in the data under the null slot
+        let a = sliced_list_of_view(child(long_a), [true, false]);
+        let b = sliced_list_of_view(child(long_b), [true, false]);
+        test_equal(&a, &b, true);
+
+        // Both rows are valid and hold different values; the null at child 
index 0 is
+        // outside the compared range and must not hide the difference
+        let a = sliced_list_of_view(child(long_a), [false, true]);
+        let b = sliced_list_of_view(child(long_b), [false, true]);
+        test_equal(&a, &b, false);
+    }
+}
+
 #[test]
 fn test_string_offset() {
     let a = StringArray::from(vec![Some("a"), None, Some("b")]);

Reply via email to