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-11345-705e6405b56cc01efb1c267203d240d0d85804ab in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit 862b1e065b602400784268df9ded5d0a18710eed Author: Neil Conway <[email protected]> AuthorDate: Sat Oct 3 02:58:49 2026 +0000 fix(arrow-data): fix gaps in ListView equality (#11345) # Which issue does this PR close? - N/A # Rationale for this change `list_view_equal` had two bugs: * When both arrays had null rows, it compared each non-null row using the size of the LHS only. For example, `[[1, 2], null]` was considered equal to `[[1, 2, 3], null]`. * Rows that differed only in their null masks were not handled correctly: they were either incorrectly considered equal, or the comparison resulted in a panic. # What changes are included in this PR? * Fix ListView equality bugs * Add unit tests # Are these changes tested? Yes; existing tests pass, new tests added. # Are there any significant user-facing changes? No. # AI usage Developed with Claude Code (Opus 5.5). --- arrow-data/src/equal/list_view.rs | 23 +++++++++++++++-------- arrow-data/src/equal/mod.rs | 3 +++ arrow/tests/array_equal.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/arrow-data/src/equal/list_view.rs b/arrow-data/src/equal/list_view.rs index f431e84589..285fbc8a9a 100644 --- a/arrow-data/src/equal/list_view.rs +++ b/arrow-data/src/equal/list_view.rs @@ -17,7 +17,7 @@ use crate::ArrayData; use crate::data::count_nulls; -use crate::equal::equal_values; +use crate::equal::equal_range; use arrow_buffer::ArrowNativeType; use num_integer::Integer; @@ -66,8 +66,7 @@ pub(super) fn list_view_equal<T: ArrowNativeType + Integer>( let rhs_offset = rhs_offset.to_usize().unwrap(); let size = size.to_usize().unwrap(); - // Check if offsets are valid for the given range - if !equal_values(lhs_data, rhs_data, lhs_offset, rhs_offset, size) { + if !equal_range(lhs_data, rhs_data, lhs_offset, rhs_offset, size) { return false; } } @@ -77,10 +76,10 @@ pub(super) fn list_view_equal<T: ArrowNativeType + Integer>( let rhs_nulls = rhs.nulls().unwrap().slice(rhs_start, len); // Check values for equality, with null checking - for (index, ((&lhs_offset, &rhs_offset), &size)) in lhs_range_offsets + for (index, ((&lhs_offset, &rhs_offset), (&lhs_size, &rhs_size))) in lhs_range_offsets .iter() .zip(rhs_range_offsets) - .zip(lhs_range_sizes) + .zip(lhs_range_sizes.iter().zip(rhs_range_sizes)) .enumerate() { let lhs_is_null = lhs_nulls.is_null(index); @@ -90,12 +89,20 @@ pub(super) fn list_view_equal<T: ArrowNativeType + Integer>( return false; } + // Null rows are equal whatever their sizes and values + if lhs_is_null { + continue; + } + + if lhs_size != rhs_size { + return false; + } + let lhs_offset = lhs_offset.to_usize().unwrap(); let rhs_offset = rhs_offset.to_usize().unwrap(); - let size = size.to_usize().unwrap(); + let size = lhs_size.to_usize().unwrap(); - // Check if values match in the range - if !lhs_is_null && !equal_values(lhs_data, rhs_data, lhs_offset, rhs_offset, size) { + if !equal_range(lhs_data, rhs_data, lhs_offset, rhs_offset, size) { return false; } } diff --git a/arrow-data/src/equal/mod.rs b/arrow-data/src/equal/mod.rs index b241b8cf66..011f591d10 100644 --- a/arrow-data/src/equal/mod.rs +++ b/arrow-data/src/equal/mod.rs @@ -58,6 +58,9 @@ use variable_size::variable_sized_equal; /// Compares the values of two [ArrayData] starting at `lhs_start` and `rhs_start` respectively /// for `len` slots. +/// +/// Assumes the null masks of the two ranges are already known to be equal. To compare +/// ranges of child data, use [`equal_range`], which also compares the null masks. #[inline] fn equal_values( lhs: &ArrayData, diff --git a/arrow/tests/array_equal.rs b/arrow/tests/array_equal.rs index c108cc9670..cc45526a02 100644 --- a/arrow/tests/array_equal.rs +++ b/arrow/tests/array_equal.rs @@ -822,6 +822,22 @@ fn test_test_list_view_array<T: OffsetSizeTrait>() { test_equal(&a, &b, true); } +fn test_list_view_sizes_and_child_nulls<T: OffsetSizeTrait>() { + // Same first elements but different sizes, with a null row + let a = create_list_view_array::<T, _, _>([Some(vec![Some(1), Some(2)]), None]); + let b = create_list_view_array::<T, _, _>([Some(vec![Some(1), Some(2), Some(3)]), None]); + test_equal(&a, &b, false); + + // Child values that differ only in their null masks, without and with a null row + let a = create_list_view_array::<T, _, _>([Some(vec![Some(1), Some(0)])]); + let b = create_list_view_array::<T, _, _>([Some(vec![Some(1), None])]); + test_equal(&a, &b, false); + + let a = create_list_view_array::<T, _, _>([None, Some(vec![Some(1), Some(0)])]); + let b = create_list_view_array::<T, _, _>([None, Some(vec![Some(1), None])]); + test_equal(&a, &b, false); +} + // Special test for List<ListView<i32>>. // This tests the equal_ranges kernel fn test_sliced_list_of_list_view<T: OffsetSizeTrait>() { @@ -871,6 +887,16 @@ fn test_large_list_view_array() { test_test_list_view_array::<i64>(); } +#[test] +fn test_list_view_array_sizes_and_child_nulls() { + test_list_view_sizes_and_child_nulls::<i32>(); +} + +#[test] +fn test_large_list_view_array_sizes_and_child_nulls() { + test_list_view_sizes_and_child_nulls::<i64>(); +} + #[test] fn test_nested_list_view_array() { test_sliced_list_of_list_view::<i32>();
