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 b89e3020d9 fix(arrow-select): preserve nullability for REE and Union 
take (#10994)
b89e3020d9 is described below

commit b89e3020d9ad6c5fc5c9e9acafc83fb21866f536
Author: yongster <[email protected]>
AuthorDate: Thu Sep 24 19:23:52 2026 +0800

    fix(arrow-select): preserve nullability for REE and Union take (#10994)
    
    `take` can introduce output nulls when its indices contain nulls.
    
    Most Arrow arrays represent these with a top-level validity bitmap.
    However,
    RunEndEncoded and Union arrays derive logical nullability from their
    child
    arrays:
    
    - A RunEndEncoded array derives nullability from its `values` field.
    - A Dense Union represents a null through a nullable selected child.
    - A Sparse Union stores every child at every output position.
    
    Previously, `take` could write nulls into children whose corresponding
    field
    metadata was marked as non-nullable. This made the physical output
    inconsistent
    with its declared schema.
    
    - Closes #10992.
    
    - Return a compute error when null take indices would introduce nulls
    into a
      RunEndEncoded array with a non-nullable `values` field.
    - For Dense Union arrays, select a nullable child to represent null take
      indices.
    - Return a compute error for Sparse Union arrays with non-nullable
    fields when
    take indices contain nulls, since every child would otherwise receive an
      introduced null.
    - Add regression tests for each behavior.
    
    This complements #10909 and ensures that arrays marked as non-nullable
    do not
    produce output containing newly introduced nulls.
    
    ---------
    
    Co-authored-by: yang3.xie <[email protected]>
---
 arrow-select/src/take.rs | 438 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 410 insertions(+), 28 deletions(-)

diff --git a/arrow-select/src/take.rs b/arrow-select/src/take.rs
index f52774825c..8c9c4ba3da 100644
--- a/arrow-select/src/take.rs
+++ b/arrow-select/src/take.rs
@@ -339,24 +339,27 @@ fn take_impl<IndexType: ArrowPrimitiveType, const 
CHECKED: bool>(
             }
         }
         DataType::Union(fields, UnionMode::Sparse) => {
-            let mut children = Vec::with_capacity(fields.len());
             let values = values.as_any().downcast_ref::<UnionArray>().unwrap();
-            let type_ids = take_union_type_ids(fields, values.type_ids(), 
indices)?;
-            for (type_id, _field) in fields.iter() {
-                let values = values.child(type_id);
-                let values = take_impl::<_, CHECKED>(values, indices)?;
-                children.push(values);
-            }
+            let (type_ids, null_type_id) =
+                take_union_type_ids(fields, values.type_ids(), indices)?;
+            let children = fields
+                .iter()
+                .map(|(type_id, _)| {
+                    take_sparse_union_child::<_, CHECKED>(
+                        values.child(type_id),
+                        null_type_id == Some(type_id),
+                        indices,
+                    )
+                })
+                .collect::<Result<Vec<_>, _>>()?;
             let array = UnionArray::try_new(fields.clone(), type_ids, None, 
children)?;
             Ok(Arc::new(array))
         }
         DataType::Union(fields, UnionMode::Dense) => {
             let values = values.as_any().downcast_ref::<UnionArray>().unwrap();
 
-            let type_ids = PrimitiveArray::<Int8Type>::try_new(
-                take_union_type_ids(fields, values.type_ids(), indices)?,
-                None,
-            )?;
+            let (type_ids, _) = take_union_type_ids(fields, values.type_ids(), 
indices)?;
+            let type_ids = PrimitiveArray::<Int8Type>::try_new(type_ids, 
None)?;
             // Keep index nulls so `take` of each child writes a null instead 
of
             // reading child offset 0 (the default `take_native` fills in).
             let offsets = <PrimitiveArray<Int32Type>>::try_new(
@@ -399,29 +402,26 @@ fn take_impl<IndexType: ArrowPrimitiveType, const 
CHECKED: bool>(
     }
 }
 
-/// Takes union type ids, substituting a valid type id for null take indices.
+/// Takes union type ids, substituting a child that can represent a null for
+/// null take indices.
 ///
-/// Union arrays do not have a top-level null bitmap. A null is represented by 
selecting an
-/// arbitrary valid child type id with a null value in that child. In 
particular, a null index
-/// cannot fall back to type id `0`, as unions are not required to have such a 
child.
+/// Union arrays do not have a top-level null bitmap. A null is represented by
+/// selecting a child that can store a null value. In particular, a null index
+/// cannot fall back to type id `0`, as unions are not required to have such a
+/// child.
+///
+/// Returns the taken type ids and, when `indices` contains nulls, the type id
+/// used to represent those nulls.
 fn take_union_type_ids<IndexType: ArrowPrimitiveType>(
     fields: &UnionFields,
     type_ids: &ScalarBuffer<i8>,
     indices: &PrimitiveArray<IndexType>,
-) -> Result<ScalarBuffer<i8>, ArrowError> {
+) -> Result<(ScalarBuffer<i8>, Option<i8>), ArrowError> {
     if indices.null_count() == 0 {
-        return Ok(take_native(type_ids, indices));
+        return Ok((take_native(type_ids, indices), None));
     }
 
-    let null_type_id = fields
-        .iter()
-        .next()
-        .map(|(type_id, _)| type_id)
-        .ok_or_else(|| {
-            ArrowError::ComputeError(
-                "Cannot take from a union with zero fields when indices 
contains nulls".into(),
-            )
-        })?;
+    let null_type_id = union_null_type_id(fields)?;
     let taken_type_ids = take_native(type_ids, indices);
     let type_ids = indices
         .iter()
@@ -434,7 +434,78 @@ fn take_union_type_ids<IndexType: ArrowPrimitiveType>(
             }
         })
         .collect::<ScalarBuffer<_>>();
-    Ok(type_ids)
+    Ok((type_ids, Some(null_type_id)))
+}
+
+/// Type id of a union child that can represent a newly introduced null.
+fn union_null_type_id(fields: &UnionFields) -> Result<i8, ArrowError> {
+    fields
+        .iter()
+        .find_map(|(type_id, field)| 
field_can_represent_take_null(field).then_some(type_id))
+        .ok_or_else(|| {
+            ArrowError::ComputeError(
+                "Cannot take null indices from a union with no field that can 
represent nulls"
+                    .into(),
+            )
+        })
+}
+
+/// Whether `field` can physically store a newly introduced null.
+///
+/// Union and RunEndEncoded have no top-level validity bitmap, so a null must
+/// be represented by a descendant that is marked nullable. A field marked
+/// nullable is not sufficient if its nested type cannot store a null.
+fn field_can_represent_take_null(field: &FieldRef) -> bool {
+    if !field.is_nullable() {
+        return false;
+    }
+    match field.data_type() {
+        DataType::RunEndEncoded(_, values) => 
field_can_represent_take_null(values),
+        DataType::Union(fields, _) => fields
+            .iter()
+            .any(|(_, child)| field_can_represent_take_null(child)),
+        _ => true,
+    }
+}
+
+/// Takes a sparse union child for `indices`.
+///
+/// Null take indices select one child that can represent a null
+/// ([`take_union_type_ids`]). Values in the other children at those positions
+/// are unspecified, so they are taken with dummy indices instead of 
introducing
+/// nulls that would contradict field metadata.
+fn take_sparse_union_child<IndexType: ArrowPrimitiveType, const CHECKED: bool>(
+    values: &dyn Array,
+    represent_nulls: bool,
+    indices: &PrimitiveArray<IndexType>,
+) -> Result<ArrayRef, ArrowError> {
+    if represent_nulls || indices.null_count() == 0 {
+        return take_impl::<_, CHECKED>(values, indices);
+    }
+
+    if values.is_empty() {
+        // Dummy index 0 is OOB on an empty child. Sparse children have the 
same
+        // length as the union, so a non-null index is also OOB and already
+        // panics in `take_native` via [`take_union_type_ids`].
+        return Ok(make_array(
+            new_null_array(values.data_type(), indices.len())
+                .to_data()
+                .into_builder()
+                .nulls(None)
+                .build()?,
+        ));
+    }
+
+    take_impl::<_, CHECKED>(values, &indices_without_nulls(indices))
+}
+
+/// Replaces null take indices with `0` and drops the null bitmap.
+fn indices_without_nulls<IndexType: ArrowPrimitiveType>(
+    indices: &PrimitiveArray<IndexType>,
+) -> PrimitiveArray<IndexType> {
+    let dummy = IndexType::Native::ZERO;
+    let normalized = indices.iter().map(|idx| idx.unwrap_or(dummy));
+    PrimitiveArray::from_iter_values(normalized)
 }
 
 /// Options that define how `take` should behave
@@ -1346,6 +1417,13 @@ fn take_run<T: RunEndIndexType, I: ArrowPrimitiveType>(
     run_array: &RunArray<T>,
     logical_indices: &PrimitiveArray<I>,
 ) -> Result<RunArray<T>, ArrowError> {
+    if logical_indices.null_count() > 0 && 
!run_array.values_field().is_nullable() {
+        return Err(ArrowError::ComputeError(
+            "Cannot take null indices from a RunEndEncoded array with a 
non-nullable values field"
+                .into(),
+        ));
+    }
+
     let physical_indices = physical_indices_for_take(run_array, 
logical_indices)?;
 
     // Run encode the physical indices into new_run_ends
@@ -3226,6 +3304,27 @@ mod tests {
         assert_eq!(run.run_ends().values(), &[1_i32, 4, 5]);
     }
 
+    #[test]
+    fn test_take_runs_null_indices_non_nullable_values() {
+        let run_array = unsafe {
+            RunArray::<Int32Type>::new_unchecked(
+                DataType::RunEndEncoded(
+                    Arc::new(Field::new("run_ends", DataType::Int32, false)),
+                    Arc::new(Field::new("values", DataType::Int32, false)),
+                ),
+                RunEndBuffer::new(vec![1_i32, 2].into(), 0, 2),
+                Arc::new(Int32Array::from(vec![10, 20])),
+            )
+        };
+        let indices = Int32Array::from(vec![Some(0), None]);
+
+        let error = take(&run_array, &indices, None).unwrap_err();
+        assert_eq!(
+            error.to_string(),
+            "Compute error: Cannot take null indices from a RunEndEncoded 
array with a non-nullable values field"
+        );
+    }
+
     #[test]
     fn test_take_runs_sliced() {
         let logical_array: Vec<i32> = vec![1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 
6];
@@ -3472,6 +3571,134 @@ mod tests {
         assert_eq!(dense_logical, sparse_logical);
     }
 
+    #[test]
+    fn test_take_dense_union_null_indices_uses_nullable_field() {
+        let fields = UnionFields::try_new(
+            vec![0, 1],
+            vec![
+                Field::new("non_nullable", DataType::Int32, false),
+                Field::new("nullable", DataType::Int32, true),
+            ],
+        )
+        .unwrap();
+        let union = UnionArray::try_new(
+            fields,
+            ScalarBuffer::from(vec![0_i8]),
+            Some(ScalarBuffer::from(vec![0_i32])),
+            vec![
+                Arc::new(Int32Array::from(vec![10])),
+                Arc::new(Int32Array::from(vec![20])),
+            ],
+        )
+        .unwrap();
+
+        let taken = take(&union, &UInt32Array::from(vec![None]), 
None).unwrap();
+        let taken = taken.as_union();
+        assert_eq!(taken.type_id(0), 1);
+        assert!(taken.child(1).is_null(0));
+    }
+
+    #[test]
+    fn test_take_sparse_union_null_indices_uses_nullable_field() {
+        let fields = UnionFields::try_new(
+            vec![0, 1],
+            vec![
+                Field::new("non_nullable", DataType::Int32, false),
+                Field::new("nullable", DataType::Int32, true),
+            ],
+        )
+        .unwrap();
+        let union = UnionArray::try_new(
+            fields,
+            ScalarBuffer::from(vec![0_i8]),
+            None,
+            vec![
+                Arc::new(Int32Array::from(vec![10])),
+                Arc::new(Int32Array::from(vec![20])),
+            ],
+        )
+        .unwrap();
+
+        let taken = take(&union, &UInt32Array::from(vec![Some(0), None]), 
None).unwrap();
+        let taken = taken.as_union();
+        assert_eq!(taken.type_ids(), &ScalarBuffer::from(vec![0_i8, 1]));
+        assert_eq!(taken.logical_null_count(), 1);
+        assert_eq!(
+            taken
+                .child(0)
+                .as_primitive::<Int32Type>()
+                .iter()
+                .collect::<Vec<_>>(),
+            vec![Some(10), Some(10)]
+        );
+        assert_eq!(
+            taken
+                .child(1)
+                .as_primitive::<Int32Type>()
+                .iter()
+                .collect::<Vec<_>>(),
+            vec![Some(20), None]
+        );
+    }
+
+    #[test]
+    fn test_take_sparse_union_null_indices_no_nullable_fields() {
+        let fields = UnionFields::try_new(
+            vec![0, 1],
+            vec![
+                Field::new("a", DataType::Int32, false),
+                Field::new("b", DataType::Int32, false),
+            ],
+        )
+        .unwrap();
+        let union = UnionArray::try_new(
+            fields,
+            ScalarBuffer::from(vec![0_i8]),
+            None,
+            vec![
+                Arc::new(Int32Array::from(vec![10])),
+                Arc::new(Int32Array::from(vec![20])),
+            ],
+        )
+        .unwrap();
+
+        let error = take(&union, &UInt32Array::from(vec![None]), 
None).unwrap_err();
+        assert_eq!(
+            error.to_string(),
+            "Compute error: Cannot take null indices from a union with no 
field that can represent nulls"
+        );
+    }
+
+    #[test]
+    fn test_take_empty_sparse_union_null_indices_mixed_nullability() {
+        let fields = UnionFields::try_new(
+            vec![0, 1],
+            vec![
+                Field::new("non_nullable", DataType::Int32, false),
+                Field::new("nullable", DataType::Int32, true),
+            ],
+        )
+        .unwrap();
+        let union = UnionArray::try_new(
+            fields,
+            ScalarBuffer::<i8>::from(vec![]),
+            None,
+            vec![
+                Arc::new(Int32Array::from(Vec::<i32>::new())),
+                Arc::new(Int32Array::from(Vec::<i32>::new())),
+            ],
+        )
+        .unwrap();
+
+        let taken = take(&union, &UInt32Array::from(vec![None]), 
None).unwrap();
+        let taken = taken.as_union();
+        assert_eq!(taken.len(), 1);
+        assert_eq!(taken.type_id(0), 1);
+        assert_eq!(taken.logical_null_count(), 1);
+        assert!(!taken.child(0).is_null(0));
+        assert!(taken.child(1).is_null(0));
+    }
+
     #[test]
     fn test_take_empty_union_without_null_indices() {
         let fields = UnionFields::try_new(vec![], 
Vec::<Field>::new()).unwrap();
@@ -3523,11 +3750,166 @@ mod tests {
             let error = take(values, &indices, None).unwrap_err();
             assert_eq!(
                 error.to_string(),
-                "Compute error: Cannot take from a union with zero fields when 
indices contains nulls"
+                "Compute error: Cannot take null indices from a union with no 
field that can represent nulls"
+            );
+        }
+    }
+
+    fn inner_union_no_nullable_children(mode: UnionMode) -> (UnionFields, 
UnionArray) {
+        let fields = UnionFields::try_new(
+            vec![0, 1],
+            vec![
+                Field::new("a", DataType::Int32, false),
+                Field::new("b", DataType::Int32, false),
+            ],
+        )
+        .unwrap();
+        let children: Vec<ArrayRef> = vec![
+            Arc::new(Int32Array::from(vec![10])),
+            Arc::new(Int32Array::from(vec![20])),
+        ];
+        let array = match mode {
+            UnionMode::Sparse => UnionArray::try_new(
+                fields.clone(),
+                ScalarBuffer::from(vec![0_i8]),
+                None,
+                children,
+            )
+            .unwrap(),
+            UnionMode::Dense => UnionArray::try_new(
+                fields.clone(),
+                ScalarBuffer::from(vec![0_i8]),
+                Some(ScalarBuffer::from(vec![0_i32])),
+                children,
+            )
+            .unwrap(),
+        };
+        (fields, array)
+    }
+
+    #[test]
+    fn 
test_take_union_null_indices_skips_nested_union_without_nullable_children() {
+        // The inner union field is marked nullable, but it has no nullable
+        // children, so it cannot represent a take-null. The sibling Int32 
field
+        // can, and must be selected instead of erroring.
+        for mode in [UnionMode::Sparse, UnionMode::Dense] {
+            let (inner_fields, inner) = inner_union_no_nullable_children(mode);
+            let outer_fields = UnionFields::try_new(
+                vec![0, 1],
+                vec![
+                    Field::new("inner", DataType::Union(inner_fields, mode), 
true),
+                    Field::new("i", DataType::Int32, true),
+                ],
+            )
+            .unwrap();
+            let children: Vec<ArrayRef> =
+                vec![Arc::new(inner), Arc::new(Int32Array::from(vec![30]))];
+            let outer = match mode {
+                UnionMode::Sparse => UnionArray::try_new(
+                    outer_fields,
+                    ScalarBuffer::from(vec![1_i8]),
+                    None,
+                    children,
+                )
+                .unwrap(),
+                UnionMode::Dense => UnionArray::try_new(
+                    outer_fields,
+                    ScalarBuffer::from(vec![1_i8]),
+                    Some(ScalarBuffer::from(vec![0_i32])),
+                    children,
+                )
+                .unwrap(),
+            };
+
+            let taken = take(&outer, &UInt32Array::from(vec![None]), 
None).unwrap();
+            let taken = taken.as_union();
+            assert_eq!(taken.type_id(0), 1, "{mode:?}");
+            assert!(taken.child(1).is_null(0), "{mode:?}");
+            assert_eq!(taken.logical_null_count(), 1, "{mode:?}");
+        }
+    }
+
+    #[test]
+    fn test_take_union_null_indices_nested_union_cannot_represent_null() {
+        // A nullable-marked nested union is not a valid null child when none 
of
+        // its own children can store a null.
+        for mode in [UnionMode::Sparse, UnionMode::Dense] {
+            let (inner_fields, inner) = inner_union_no_nullable_children(mode);
+            let outer_fields = UnionFields::try_new(
+                vec![0],
+                vec![Field::new(
+                    "inner",
+                    DataType::Union(inner_fields, mode),
+                    true,
+                )],
+            )
+            .unwrap();
+            let children: Vec<ArrayRef> = vec![Arc::new(inner)];
+            let outer = match mode {
+                UnionMode::Sparse => UnionArray::try_new(
+                    outer_fields,
+                    ScalarBuffer::from(vec![0_i8]),
+                    None,
+                    children,
+                )
+                .unwrap(),
+                UnionMode::Dense => UnionArray::try_new(
+                    outer_fields,
+                    ScalarBuffer::from(vec![0_i8]),
+                    Some(ScalarBuffer::from(vec![0_i32])),
+                    children,
+                )
+                .unwrap(),
+            };
+
+            let error = take(&outer, &UInt32Array::from(vec![None]), 
None).unwrap_err();
+            assert_eq!(
+                error.to_string(),
+                "Compute error: Cannot take null indices from a union with no 
field that can represent nulls"
             );
         }
     }
 
+    #[test]
+    fn test_take_union_null_indices_skips_ree_with_non_nullable_values() {
+        // A nullable-marked REE child cannot represent a take-null when its
+        // values field is non-nullable. The sibling Int32 field must be used.
+        let run_array = unsafe {
+            RunArray::<Int32Type>::new_unchecked(
+                DataType::RunEndEncoded(
+                    Arc::new(Field::new("run_ends", DataType::Int32, false)),
+                    Arc::new(Field::new("values", DataType::Int32, false)),
+                ),
+                RunEndBuffer::new(vec![1_i32].into(), 0, 1),
+                Arc::new(Int32Array::from(vec![10])),
+            )
+        };
+        let fields = UnionFields::try_new(
+            vec![0, 1],
+            vec![
+                Field::new("ree", run_array.data_type().clone(), true),
+                Field::new("i", DataType::Int32, true),
+            ],
+        )
+        .unwrap();
+        let union = UnionArray::try_new(
+            fields,
+            ScalarBuffer::from(vec![1_i8]),
+            None,
+            vec![
+                Arc::new(run_array) as ArrayRef,
+                Arc::new(Int32Array::from(vec![30])),
+            ],
+        )
+        .unwrap();
+
+        let taken = take(&union, &UInt32Array::from(vec![None]), 
None).unwrap();
+        let taken = taken.as_union();
+        assert_eq!(taken.type_id(0), 1);
+        assert!(taken.child(1).is_null(0));
+        assert_eq!(taken.logical_null_count(), 1);
+    }
+
     #[test]
     fn test_take_union_dense_using_builder() {
         let mut builder = UnionBuilder::new_dense();

Reply via email to