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")]);
