This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new b54d812d6c arrow-data: remove unnecessary & incorrect null buffer
check in `ArrayData::validate` (#10709)
b54d812d6c is described below
commit b54d812d6c666f4a3a38bf6ae9fc2e5420cab480
Author: Ali Asghar <[email protected]>
AuthorDate: Sun Sep 6 17:38:14 2026 -0700
arrow-data: remove unnecessary & incorrect null buffer check in
`ArrayData::validate` (#10709)
# Which issue does this PR close?
- Closes #7379.
# Rationale for this change
`ArrayData::validate` sizes the null bitmap length check with
`len_plus_offset`, which folds in `ArrayData::offset`:
```rust
let actual_len = nulls.validity().len();
let needed_len = bit_util::ceil(len_plus_offset, 8);
```
That offset does not apply to the null buffer. `ArrayData::nulls` says
so directly: *"Note: `ArrayData::offset` does NOT apply to the returned
`NullBuffer`"*. The `NullBuffer` carries its own offset.
So a null buffer at offset 0 backing an array sliced to offset 50 is
rejected, using the reporter's test:
```
InvalidArgumentError("null_bit_buffer size too small. got 7 needed 13")
```
Decoupled offsets are a supported state rather than something the
validator was guarding against. `arrow-data/src/ffi.rs` `align_nulls`
exists precisely because `data.offset() != nulls.offset()` is legal: it
fast-paths when they match and re-aligns the bits otherwise.
As the issue notes, only the false-rejection direction is reachable
today, since there is currently no way to build an invalid
`BooleanBuffer`. This is a correctness fix to the check, not a soundness
fix.
# What changes are included in this PR?
One line in `ArrayData::validate`, sizing the check from the null
buffer's own offset and length.
The near-identical check in `ArrayData::try_new` is deliberately left
alone. It takes a raw `null_bit_buffer: Option<Buffer>` for which the
data offset genuinely does apply, and it is pinned by
`arrow/tests/array_validation.rs` `test_bitmap_too_small`. I confirmed
that test still passes.
# Are these changes tested?
Yes. Added `null_buffer_offset_is_independent_of_data_offset` in
`arrow-data/src/data.rs`, covering the reporter's scenario: 100 values
sliced to the last 50, then the same 50 nulls supplied both sliced
(offset 50) and unsliced (offset 0). Both must validate.
I checked it is not vacuous: reverting only the one-line fix while
keeping the test makes it fail with the exact error from the issue, `got
7 needed 13`.
Ran locally on `5ce0ebe`:
* `cargo test -p arrow-data`: 44 passed, plus 13 doctests
* `cargo test -p arrow-data -p arrow-array -p arrow-buffer -p
arrow-select -p arrow-cast`: all green
* `cargo test -p arrow --test array_validation`: 58 passed, including
`test_bitmap_too_small`
* `cargo fmt -p arrow-data -- --check` and `cargo clippy -p arrow-data
--all-targets -- -D warnings`: clean
One pre-existing failure unrelated to this change:
`util::test_util::tests::test_happy` needs the `testing/data` submodule,
which my clone did not initialize. It fails the same way on a pristine
checkout.
# Are there any user-facing changes?
`ArrayData::validate` no longer rejects a valid null buffer whose offset
differs from the array's. No API change. No behavior change for null
buffers whose offset already matched the data offset.
# AI disclosure
Per `CONTRIBUTING.md` "AI Generated Submissions". I used an AI assistant
to help draft the fix, the test and this description. I reproduced the
failure with the reporter's verbatim test before changing anything,
confirmed the `try_new` site is a separate case that must not change and
that its pinning test still passes, and verified the new test fails with
the fix reverted. I ran every command listed above myself.
---------
Signed-off-by: Ali <[email protected]>
---
arrow-data/src/data.rs | 37 +++++++++++++++++++++++++++++--------
1 file changed, 29 insertions(+), 8 deletions(-)
diff --git a/arrow-data/src/data.rs b/arrow-data/src/data.rs
index 4b9acca77a..4c76842111 100644
--- a/arrow-data/src/data.rs
+++ b/arrow-data/src/data.rs
@@ -1027,14 +1027,6 @@ impl ArrayData {
)));
}
- let actual_len = nulls.validity().len();
- let needed_len = bit_util::ceil(len_plus_offset, 8);
- if actual_len < needed_len {
- return Err(ArrowError::InvalidArgumentError(format!(
- "null_bit_buffer size too small. got {actual_len} needed
{needed_len}",
- )));
- }
-
if nulls.len() != self.len {
return Err(ArrowError::InvalidArgumentError(format!(
"null buffer incorrect size. got {} expected {}",
@@ -3575,6 +3567,35 @@ mod tests {
ArrayData::new_null(&dt, 1).validate_full().unwrap();
}
+ #[test]
+ fn null_buffer_offset_is_independent_of_data_offset() {
+ // 100 values sliced down to the last 50, so the data has offset 50.
+ let int_data = ArrayData::builder(DataType::UInt32)
+ .offset(50)
+ .len(50)
+ .add_buffer(Buffer::from_vec(vec![0_u32; 100]))
+ .build()
+ .unwrap();
+ int_data.validate().unwrap();
+
+ // A null buffer that happens to share the data's offset.
+ let nulls = NullBuffer::new(BooleanBuffer::from(vec![false;
100]).slice(0, 50));
+ let with_sliced_nulls = int_data
+ .clone()
+ .into_builder()
+ .nulls(Some(nulls))
+ .build()
+ .unwrap();
+ with_sliced_nulls.validate().unwrap();
+
+ // The same 50 nulls at offset 0. ArrayData::offset does not apply to
the
+ // null buffer, so this is just as valid and must not be rejected.
+ let nulls = NullBuffer::new(BooleanBuffer::from(vec![false; 50]));
+ let with_unsliced_nulls =
int_data.into_builder().nulls(Some(nulls)).build().unwrap();
+ with_unsliced_nulls.validate().unwrap();
+ assert_eq!(with_unsliced_nulls.null_count(), 50);
+ }
+
fn test_both_builder_and_array_data(
data_type: DataType,
len: usize,