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 951d9396e1 fix(arrow-array): check logical nulls, not is_nullable, for 
a non-nullable list field (#11178)
951d9396e1 is described below

commit 951d9396e11b21169671ef52d9da558a7a4f0a66
Author: Amit Vijapur <[email protected]>
AuthorDate: Fri Sep 25 09:38:34 2026 +0100

    fix(arrow-array): check logical nulls, not is_nullable, for a non-nullable 
list field (#11178)
    
    # Which issue does this PR close?
    
    Closes #6538.
    
    # Rationale for this change
    
    `GenericListArray::try_new` and `GenericListViewArray::try_new` reject a
    non-nullable `field` whenever `values.is_nullable()` is true.
    `Array::is_nullable` may return `true` conservatively, and
    `DictionaryArray::is_nullable` does so when the keys carry a null
    buffer, even an all-valid one, or when the values hold a null that no
    key references. Such a dictionary has zero logical nulls and is rejected
    anyway.
    
    The `UnionArray` case in the issue no longer reproduces: #6540 made
    `UnionArray::is_nullable` ask its children. The dictionary case still
    does.
    
    # What changes are included in this PR?
    
    Both constructors test `values.logical_null_count() != 0` instead, which
    is what `FixedSizeListArray::try_new` already checks through
    `logical_nulls()`. The doc bullets are updated to match.
    
    # Are these changes tested?
    
    A test in each file builds a list over an `Int8DictionaryArray` whose
    keys have an all-valid null buffer under a non-nullable field, which
    failed before the change, and then one with a null key, which still
    fails with `cannot contain nulls`. `cargo test -p arrow-array` passes.
    
    # Are there any user-facing changes?
    
    A list or list-view array over a dictionary with no logical nulls can
    now be built under a non-nullable field. Arrays that hold a logical null
    are still rejected as before.
---
 arrow-array/src/array/list_array.rs      | 33 ++++++++++++++++++++++++++--
 arrow-array/src/array/list_view_array.rs | 37 +++++++++++++++++++++++++++++---
 2 files changed, 65 insertions(+), 5 deletions(-)

diff --git a/arrow-array/src/array/list_array.rs 
b/arrow-array/src/array/list_array.rs
index 430750f48a..d13675a007 100644
--- a/arrow-array/src/array/list_array.rs
+++ b/arrow-array/src/array/list_array.rs
@@ -204,7 +204,7 @@ impl<OffsetSize: OffsetSizeTrait> 
GenericListArray<OffsetSize> {
     ///
     /// * `offsets.len() - 1 != nulls.len()`
     /// * `offsets.last() > values.len()`
-    /// * `!field.is_nullable() && values.is_nullable()`
+    /// * `!field.is_nullable() && values.logical_null_count() != 0`
     /// * `field.data_type() != values.data_type()`
     pub fn try_new(
         field: FieldRef,
@@ -232,7 +232,7 @@ impl<OffsetSize: OffsetSizeTrait> 
GenericListArray<OffsetSize> {
                 n.len(),
             )));
         }
-        if !field.is_nullable() && values.is_nullable() {
+        if !field.is_nullable() && values.logical_null_count() != 0 {
             return Err(ArrowError::InvalidArgumentError(format!(
                 "Non-nullable field of {}ListArray {:?} cannot contain nulls",
                 OffsetSize::PREFIX,
@@ -1353,6 +1353,35 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_try_new_non_nullable_field_dictionary_values() {
+        let keys = Int8Array::new(vec![0i8, 1].into(), 
Some(NullBuffer::new_valid(2)));
+        let values = StringArray::from(vec!["x", "y"]);
+        let dict = Int8DictionaryArray::try_new(keys, 
Arc::new(values)).unwrap();
+        let field = Arc::new(Field::new("element", dict.data_type().clone(), 
false));
+        ListArray::try_new(
+            field,
+            OffsetBuffer::new(vec![0, 2].into()),
+            Arc::new(dict),
+            None,
+        )
+        .unwrap();
+
+        let keys = Int8Array::from(vec![Some(0i8), None]);
+        let values = StringArray::from(vec!["x", "y"]);
+        let dict = Int8DictionaryArray::try_new(keys, 
Arc::new(values)).unwrap();
+        let field = Arc::new(Field::new("element", dict.data_type().clone(), 
false));
+        let err = ListArray::try_new(
+            field,
+            OffsetBuffer::new(vec![0, 2].into()),
+            Arc::new(dict),
+            None,
+        )
+        .unwrap_err();
+
+        assert!(err.to_string().contains("cannot contain nulls"));
+    }
+
     #[test]
     fn test_from_fixed_size_list() {
         let mut builder = FixedSizeListBuilder::new(Int32Builder::new(), 3);
diff --git a/arrow-array/src/array/list_view_array.rs 
b/arrow-array/src/array/list_view_array.rs
index 1983e1a841..62b8b3ed1b 100644
--- a/arrow-array/src/array/list_view_array.rs
+++ b/arrow-array/src/array/list_view_array.rs
@@ -134,7 +134,7 @@ impl<OffsetSize: OffsetSizeTrait> 
GenericListViewArray<OffsetSize> {
     /// * `offsets.len() != sizes.len()`
     /// * `offsets.len() != nulls.len()`
     /// * `offsets[i] > values.len()`
-    /// * `!field.is_nullable() && values.is_nullable()`
+    /// * `!field.is_nullable() && values.logical_null_count() != 0`
     /// * `field.data_type() != values.data_type()`
     /// * `0 <= offsets[i] <= length of the child array`
     /// * `0 <= offsets[i] + size[i] <= length of the child array`
@@ -181,7 +181,7 @@ impl<OffsetSize: OffsetSizeTrait> 
GenericListViewArray<OffsetSize> {
             }
         }
 
-        if !field.is_nullable() && values.is_nullable() {
+        if !field.is_nullable() && values.logical_null_count() != 0 {
             return Err(ArrowError::InvalidArgumentError(format!(
                 "Non-nullable field of {}ListViewArray {:?} cannot contain 
nulls",
                 OffsetSize::PREFIX,
@@ -710,7 +710,7 @@ mod tests {
     use crate::builder::{FixedSizeListBuilder, Int32Builder};
     use crate::cast::AsArray;
     use crate::types::Int32Type;
-    use crate::{Int32Array, Int64Array};
+    use crate::{Int8Array, Int8DictionaryArray, Int32Array, Int64Array, 
StringArray};
 
     use super::*;
 
@@ -1140,6 +1140,37 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_try_new_non_nullable_field_dictionary_values() {
+        let keys = Int8Array::new(vec![0i8, 1].into(), 
Some(NullBuffer::new_valid(2)));
+        let values = StringArray::from(vec!["x", "y"]);
+        let dict = Int8DictionaryArray::try_new(keys, 
Arc::new(values)).unwrap();
+        let field = Arc::new(Field::new("element", dict.data_type().clone(), 
false));
+        ListViewArray::try_new(
+            field,
+            ScalarBuffer::from(vec![0]),
+            ScalarBuffer::from(vec![2]),
+            Arc::new(dict),
+            None,
+        )
+        .unwrap();
+
+        let keys = Int8Array::from(vec![Some(0i8), None]);
+        let values = StringArray::from(vec!["x", "y"]);
+        let dict = Int8DictionaryArray::try_new(keys, 
Arc::new(values)).unwrap();
+        let field = Arc::new(Field::new("element", dict.data_type().clone(), 
false));
+        let err = ListViewArray::try_new(
+            field,
+            ScalarBuffer::from(vec![0]),
+            ScalarBuffer::from(vec![2]),
+            Arc::new(dict),
+            None,
+        )
+        .unwrap_err();
+
+        assert!(err.to_string().contains("cannot contain nulls"));
+    }
+
     #[test]
     fn test_from_fixed_size_list() {
         let mut builder = FixedSizeListBuilder::new(Int32Builder::new(), 3);

Reply via email to