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 b372d1f14c fix(arrow-data): align struct null validation with parent
offset (#10970)
b372d1f14c is described below
commit b372d1f14cfe8974d8b71849e83bfa85f2a0494c
Author: hylin <[email protected]>
AuthorDate: Sat Sep 5 08:33:06 2026 +0800
fix(arrow-data): align struct null validation with parent offset (#10970)
# Which issue does this PR close?
- Closes #10951.
# Rationale for this change
For a struct with a non-zero parent offset, visible parent row `i` maps
to child row `offset + i`. Nullability validation compared the visible
parent null buffer against the unsliced child null buffer, so it could
reject masked child nulls and accept visible child nulls.
# What changes are included in this PR?
- Slice each struct child to the parent-visible window before validating
non-nullable fields.
- Add a regression test covering both the correctly masked and
incorrectly unmasked cases.
# Are these changes tested?
Yes. TDD evidence on `main` at `c134baf8f`:
- Before the production change, the focused regression failed because
the correctly masked child null was rejected.
- After the change, the focused regression passes.
- `cargo test -p arrow-data --lib`: 56 passed.
- `cargo fmt --all -- --check`: passed.
- `cargo clippy -p arrow-data --all-targets -- -D warnings`: passed.
- `git diff --check`: passed.
# Are there any user-facing changes?
Yes. Validation now correctly accepts child nulls masked by a sliced
struct parent and rejects child nulls visible through a non-null parent
row. There are no API changes.
# AI assistance
I used AI assistance to investigate the validation path, implement the
focused test and fix, and prepare this description. I reviewed the
complete diff and verified the behavior with the commands above.
---
arrow-data/src/data.rs | 45 +++++++++++++++++++++++++++++++++++++++------
1 file changed, 39 insertions(+), 6 deletions(-)
diff --git a/arrow-data/src/data.rs b/arrow-data/src/data.rs
index 8078c7cfb5..4b9acca77a 100644
--- a/arrow-data/src/data.rs
+++ b/arrow-data/src/data.rs
@@ -1525,7 +1525,8 @@ impl ArrayData {
match &self.data_type {
DataType::List(f) | DataType::LargeList(f) | DataType::Map(f, _)
=> {
if !f.is_nullable() {
- self.validate_non_nullable(None, &self.child_data[0])?
+ let child = &self.child_data[0];
+ self.validate_non_nullable(None, child, child.nulls())?
}
}
DataType::FixedSizeList(field, len) => {
@@ -1535,16 +1536,19 @@ impl ArrayData {
Some(nulls) => {
let element_len = *len as usize;
let expanded = nulls.expand(element_len);
- self.validate_non_nullable(Some(&expanded),
child)?;
+ self.validate_non_nullable(Some(&expanded), child,
child.nulls())?;
}
- None => self.validate_non_nullable(None, child)?,
+ None => self.validate_non_nullable(None, child,
child.nulls())?,
}
}
}
DataType::Struct(fields) => {
for (field, child) in fields.iter().zip(&self.child_data) {
if !field.is_nullable() {
- self.validate_non_nullable(self.nulls(), child)?
+ let child_nulls = child
+ .nulls()
+ .map(|nulls| nulls.slice(self.offset, self.len));
+ self.validate_non_nullable(self.nulls(), child,
child_nulls.as_ref())?
}
}
}
@@ -1559,9 +1563,10 @@ impl ArrayData {
&self,
mask: Option<&NullBuffer>,
child: &ArrayData,
+ child_nulls: Option<&NullBuffer>,
) -> Result<(), ArrowError> {
let Some(mask) = mask else {
- return match child.null_count() {
+ return match
child_nulls.map(NullBuffer::null_count).unwrap_or_default() {
0 => Ok(()),
_ => Err(ArrowError::InvalidArgumentError(format!(
"non-nullable child of type {} contains nulls not present
in parent {}",
@@ -1570,7 +1575,7 @@ impl ArrayData {
};
};
- match child.nulls() {
+ match child_nulls {
Some(nulls) if !mask.contains(nulls) =>
Err(ArrowError::InvalidArgumentError(format!(
"non-nullable child of type {} contains nulls not present in
parent",
child.data_type
@@ -2538,6 +2543,34 @@ mod tests {
));
}
+ #[test]
+ fn test_struct_non_nullable_child_nulls_account_for_parent_offset() {
+ let build = |parent_nulls| {
+ let child = ArrayData::builder(DataType::Int32)
+ .len(5)
+ .add_buffer(Buffer::from_slice_ref([0, 1, 2, 3, 4]))
+ .nulls(Some(NullBuffer::new(BooleanBuffer::from(vec![
+ true, true, false, true, true,
+ ]))))
+ .build()
+ .unwrap();
+
+ ArrayData::builder(DataType::Struct(Fields::from(vec![Field::new(
+ "x",
+ DataType::Int32,
+ false,
+ )])))
+ .len(4)
+ .offset(1)
+ .nulls(Some(NullBuffer::new(BooleanBuffer::from(parent_nulls))))
+ .add_child_data(child)
+ .build()
+ };
+
+ assert!(build(vec![true, false, true, true]).is_ok());
+ assert!(build(vec![true, true, false, true]).is_err());
+ }
+
#[test]
fn test_struct_equal_accounts_for_parent_offset() {
let data_type =