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 0aece99d08 fix(arrow-select): treat null take indices as null for 
dense union and REE (#10909)
0aece99d08 is described below

commit 0aece99d0834c6d3646aa4d8583bb8dbf446e15d
Author: yongster <[email protected]>
AuthorDate: Mon Aug 31 17:17:47 2026 +0800

    fix(arrow-select): treat null take indices as null for dense union and REE 
(#10909)
    
    # Which issue does this PR close?
    
    - Closes #10908.
    
    # Rationale for this change
    
    `take` leaves a null in the output when the index at that slot is null.
    That is already true for primitive arrays, dictionaries, lists, and
    sparse unions.
    
    Two arms did not:
    
    1. **Dense `UnionArray`** — `take_native` fills a null index with offset
    `0`, and `PrimitiveArray::try_new(..., None)` dropped the index null
    bitmap. The fake offset was then used to take from the child, so the
    first child value was copied into that slot.
    2. **`RunEndEncoded`** — `take_run` called
    `get_physical_indices(logical_indices.values())`. A null integer slot
    still has a backing value (typically `0`), so the lookup used logical
    index 0 and produced the first run's value.
    
    Examples on current `main`:
    
    - Dense union `[1, 2, 3]` with indices `[0, null, 2]` became `[1, 1, 3]`
    - REE `[10, 10, 99, …]` with the same indices became `[10, 10, 99]`
    - Sparse union and primitive `take` already return `[1, null, 3]` /
    `[10, null, 99]`
    
    # What changes are included in this PR?
    
    - Dense union keeps the index null bitmap on the taken offsets, so
    `take` of each child writes a null instead of reading offset 0.
    - `take_run` maps only valid indices to physical runs. A null logical
    index becomes a null run; consecutive nulls are merged.
    - Add regression tests for both arms, including a dense vs sparse
    comparison.
    
    # Are these changes tested?
    
    Yes. I ran:
    
    ```text
    cargo test -p arrow-select --lib -- take
    cargo clippy -p arrow-select --all-targets --all-features -- -D warnings
    cargo fmt -- arrow-select/src/take.rs
    ```
    
    ---------
    
    Co-authored-by: yang3.xie <[email protected]>
---
 arrow-select/src/take.rs | 264 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 250 insertions(+), 14 deletions(-)

diff --git a/arrow-select/src/take.rs b/arrow-select/src/take.rs
index 2bc5c3a31a..d429bbd268 100644
--- a/arrow-select/src/take.rs
+++ b/arrow-select/src/take.rs
@@ -26,12 +26,12 @@ use arrow_array::cast::AsArray;
 use arrow_array::types::*;
 use arrow_array::*;
 use arrow_buffer::{
-    ArrowNativeType, BooleanBuffer, Buffer, MutableBuffer, NullBuffer, 
OffsetBuffer, RunEndBuffer,
-    ScalarBuffer, bit_util,
+    ArrowNativeType, BooleanBuffer, Buffer, MutableBuffer, NullBuffer, 
NullBufferBuilder,
+    OffsetBuffer, RunEndBuffer, ScalarBuffer, bit_util,
 };
 use arrow_cmp::make_comparator;
 use arrow_data::{ArrayData, transform::MutableArrayData};
-use arrow_schema::{ArrowError, DataType, FieldRef, SortOptions, UnionMode};
+use arrow_schema::{ArrowError, DataType, FieldRef, SortOptions, UnionFields, 
UnionMode};
 
 use num_traits::{CheckedAdd, Zero};
 
@@ -214,6 +214,13 @@ fn take_impl<IndexType: ArrowPrimitiveType, const CHECKED: 
bool>(
     indices: &PrimitiveArray<IndexType>,
 ) -> Result<ArrayRef, ArrowError> {
     if indices.is_empty() {
+        if let DataType::Union(fields, _) = values.data_type()
+            && fields.is_empty()
+        {
+            // `new_empty_array` cannot construct a union with no fields, but 
an existing empty
+            // union can be sliced without materializing a child array.
+            return Ok(values.slice(0, 0));
+        }
         return Ok(new_empty_array(values.data_type()));
     }
     downcast_primitive_array! {
@@ -334,7 +341,7 @@ 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_native(values.type_ids(), indices);
+            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)?;
@@ -346,8 +353,16 @@ fn take_impl<IndexType: ArrowPrimitiveType, const CHECKED: 
bool>(
         DataType::Union(fields, UnionMode::Dense) => {
             let values = values.as_any().downcast_ref::<UnionArray>().unwrap();
 
-            let type_ids = 
<PrimitiveArray<Int8Type>>::try_new(take_native(values.type_ids(), indices), 
None)?;
-            let offsets = 
<PrimitiveArray<Int32Type>>::try_new(take_native(values.offsets().unwrap(), 
indices), None)?;
+            let type_ids = PrimitiveArray::<Int8Type>::try_new(
+                take_union_type_ids(fields, values.type_ids(), indices)?,
+                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(
+                take_native(values.offsets().unwrap(), indices),
+                indices.nulls().cloned(),
+            )?;
 
             let children = fields.iter()
                 .map(|(field_type_id, _)| {
@@ -384,6 +399,44 @@ fn take_impl<IndexType: ArrowPrimitiveType, const CHECKED: 
bool>(
     }
 }
 
+/// Takes union type ids, substituting a valid type id 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.
+fn take_union_type_ids<IndexType: ArrowPrimitiveType>(
+    fields: &UnionFields,
+    type_ids: &ScalarBuffer<i8>,
+    indices: &PrimitiveArray<IndexType>,
+) -> Result<ScalarBuffer<i8>, ArrowError> {
+    if indices.null_count() == 0 {
+        return Ok(take_native(type_ids, indices));
+    }
+
+    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 taken_type_ids = take_native(type_ids, indices);
+    let type_ids = indices
+        .iter()
+        .zip(&taken_type_ids)
+        .map(|(index, &type_id)| {
+            if index.is_some() {
+                type_id
+            } else {
+                null_type_id
+            }
+        })
+        .collect::<ScalarBuffer<_>>();
+    Ok(type_ids)
+}
+
 /// Options that define how `take` should behave
 #[derive(Clone, Debug, Default)]
 pub struct TakeOptions {
@@ -1078,18 +1131,20 @@ fn take_dict<T: ArrowDictionaryKeyType, I: 
ArrowPrimitiveType, const CHECKED: bo
 /// For e.g. an input `RunArray{ run_ends = [2,4,6,8], values=[1,2,1,2] }` and 
`logical_indices=[2,3,6,7]`
 /// would be converted to `physical_indices=[1,1,3,3]` which will be used to 
build
 /// output `RunArray{ run_ends=[2,4], values=[2,2] }`.
+///
+/// A null logical index becomes a null run. Consecutive nulls are merged.
 fn take_run<T: RunEndIndexType, I: ArrowPrimitiveType>(
     run_array: &RunArray<T>,
     logical_indices: &PrimitiveArray<I>,
 ) -> Result<RunArray<T>, ArrowError> {
-    // get physical indices for the input logical indices
-    let physical_indices = 
run_array.get_physical_indices(logical_indices.values())?;
+    let physical_indices = physical_indices_for_take(run_array, 
logical_indices)?;
 
     // Run encode the physical indices into new_run_ends
     // Keep track of the physical indices to take in take_value_indices
     // `unwrap` is used in this function because the unwrapped values are 
bounded by the corresponding `::Native`.
     let mut new_run_ends = Vec::with_capacity(1);
     let mut take_value_indices = Vec::with_capacity(1);
+    let mut take_value_is_valid = NullBufferBuilder::new(1);
 
     let values_cmp = make_comparator(
         run_array.values().as_ref(),
@@ -1100,14 +1155,20 @@ fn take_run<T: RunEndIndexType, I: ArrowPrimitiveType>(
     for ix in 1..physical_indices.len() {
         let prev_idx = physical_indices[ix - 1];
         let cur_idx = physical_indices[ix];
-        let is_new_run = cur_idx != prev_idx && values_cmp(cur_idx, 
prev_idx).is_ne();
-        if is_new_run {
-            take_value_indices.push(I::Native::from_usize(prev_idx).unwrap());
+        if is_new_run_take(run_array.values().as_ref(), prev_idx, cur_idx, 
&values_cmp) {
+            // Safe unwrap since physical indices came from a valid run array.
+            let index = 
I::Native::from_usize(prev_idx.unwrap_or_default()).unwrap();
+            take_value_indices.push(index);
+            take_value_is_valid
+                .append(prev_idx.is_some_and(|idx| 
run_array.values().is_valid(idx)));
             new_run_ends.push(T::Native::from_usize(ix).unwrap());
         }
     }
-    take_value_indices
-        .push(I::Native::from_usize(physical_indices[physical_indices.len() - 
1]).unwrap());
+    let last = physical_indices[physical_indices.len() - 1];
+    // Safe unwrap since physical indices came from a valid run array.
+    let index = I::Native::from_usize(last.unwrap_or_default()).unwrap();
+    take_value_indices.push(index);
+    take_value_is_valid.append(last.is_some_and(|idx| 
run_array.values().is_valid(idx)));
     new_run_ends.push(T::Native::from_usize(physical_indices.len()).unwrap());
 
     // SAFETY: run-ends are strictly increasing with last value == logical 
length.
@@ -1115,7 +1176,9 @@ fn take_run<T: RunEndIndexType, I: ArrowPrimitiveType>(
         RunEndBuffer::new_unchecked(ScalarBuffer::from(new_run_ends), 0, 
physical_indices.len())
     };
 
-    let take_value_indices = 
PrimitiveArray::<I>::new(ScalarBuffer::from(take_value_indices), None);
+    let nulls = take_value_is_valid.finish();
+    let take_value_indices =
+        PrimitiveArray::<I>::new(ScalarBuffer::from(take_value_indices), 
nulls);
 
     let new_values = take(run_array.values(), &take_value_indices, None)?;
 
@@ -1127,6 +1190,57 @@ fn take_run<T: RunEndIndexType, I: ArrowPrimitiveType>(
     )
 }
 
+/// Physical run index for each logical take slot.
+///
+/// `None` means the logical index is null. Only valid indices are passed to
+/// [`RunArray::get_physical_indices`]; a null slot's backing integer is 
ignored
+/// and may be out of range.
+fn physical_indices_for_take<T: RunEndIndexType, I: ArrowPrimitiveType>(
+    run_array: &RunArray<T>,
+    logical_indices: &PrimitiveArray<I>,
+) -> Result<Vec<Option<usize>>, ArrowError> {
+    if logical_indices.null_count() == 0 {
+        return Ok(run_array
+            .get_physical_indices(logical_indices.values())?
+            .into_iter()
+            .map(Some)
+            .collect());
+    }
+
+    let valid_logical: Vec<_> = logical_indices.iter().flatten().collect();
+
+    let valid_physical = if valid_logical.is_empty() {
+        Vec::new()
+    } else {
+        run_array.get_physical_indices(&valid_logical)?
+    };
+
+    let mut valid_physical = valid_physical.into_iter();
+    Ok(logical_indices
+        .iter()
+        .map(|index| index.map(|_| valid_physical.next().unwrap()))
+        .collect())
+}
+
+fn is_new_run_take(
+    values: &dyn Array,
+    prev_idx: Option<usize>,
+    cur_idx: Option<usize>,
+    values_cmp: &arrow_cmp::DynComparator,
+) -> bool {
+    let prev_valid = prev_idx.is_some_and(|idx| values.is_valid(idx));
+    let cur_valid = cur_idx.is_some_and(|idx| values.is_valid(idx));
+    match (prev_valid, cur_valid) {
+        (false, false) => false,
+        (true, true) => {
+            let prev = prev_idx.unwrap();
+            let cur = cur_idx.unwrap();
+            prev != cur && values_cmp(cur, prev).is_ne()
+        }
+        _ => true,
+    }
+}
+
 /// Takes/filters a fixed size list array's inner data using the offsets of 
the list array.
 fn take_value_indices_from_fixed_size_list<IndexType>(
     list: &FixedSizeListArray,
@@ -2787,6 +2901,22 @@ mod tests {
         assert_eq!(take_out_values.values(), &[2, 1]);
     }
 
+    #[test]
+    fn test_take_runs_null_indices() {
+        // A null index must not become logical index 0, and null indices must 
merge with
+        // consecutive runs whose values are already null.
+        let mut builder = PrimitiveRunBuilder::<Int32Type, Int32Type>::new();
+        builder.extend([Some(10), Some(10), None, None, Some(99)]);
+        let run_array = builder.finish();
+
+        let indices = Int32Array::from(vec![Some(0), None, Some(2), Some(3), 
Some(4)]);
+        let taken = take(&run_array, &indices, None).unwrap();
+        let run = taken.as_run::<Int32Type>();
+        let logical: Vec<Option<i32>> = 
run.downcast::<Int32Array>().unwrap().into_iter().collect();
+        assert_eq!(logical, vec![Some(10), None, None, None, Some(99)]);
+        assert_eq!(run.run_ends().values(), &[1_i32, 4, 5]);
+    }
+
     #[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];
@@ -2983,6 +3113,112 @@ mod tests {
         );
     }
 
+    fn union_i32_logical(array: &UnionArray) -> Vec<Option<i32>> {
+        (0..array.len())
+            .map(|i| {
+                let child = 
array.child(array.type_id(i)).as_primitive::<Int32Type>();
+                let offset = array.value_offset(i);
+                if child.is_null(offset) {
+                    None
+                } else {
+                    Some(child.value(offset))
+                }
+            })
+            .collect()
+    }
+
+    #[test]
+    fn test_take_union_dense_null_indices() {
+        // Dense [1, 2, 3]; a null index must not become child offset 0. Use a 
non-zero
+        // type id to verify null indices don't use the invalid default type 
id 0.
+        let fields =
+            UnionFields::try_new(vec![5], vec![Field::new("i", 
DataType::Int32, true)]).unwrap();
+        let dense = UnionArray::try_new(
+            fields.clone(),
+            ScalarBuffer::from(vec![5_i8, 5, 5]),
+            Some(ScalarBuffer::from(vec![0_i32, 1, 2])),
+            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
+        )
+        .unwrap();
+        let sparse = UnionArray::try_new(
+            fields,
+            ScalarBuffer::from(vec![5_i8, 5, 5]),
+            None,
+            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
+        )
+        .unwrap();
+
+        // Use an out-of-bounds backing value for the null index to exercise 
`take_native`'s
+        // default type id path. A union need not have a child with type id 0.
+        let indices = UInt32Array::new(
+            ScalarBuffer::from(vec![0_u32, 99, 2]),
+            Some(NullBuffer::from(vec![true, false, true])),
+        );
+        let dense_taken = take(&dense, &indices, None).unwrap();
+        let sparse_taken = take(&sparse, &indices, None).unwrap();
+        let dense_logical = 
union_i32_logical(dense_taken.as_any().downcast_ref().unwrap());
+        let sparse_logical = 
union_i32_logical(sparse_taken.as_any().downcast_ref().unwrap());
+
+        assert_eq!(dense_logical, vec![Some(1), None, Some(3)]);
+        assert_eq!(dense_logical, sparse_logical);
+    }
+
+    #[test]
+    fn test_take_empty_union_without_null_indices() {
+        let fields = UnionFields::try_new(vec![], 
Vec::<Field>::new()).unwrap();
+        let indices = UInt32Array::from(Vec::<u32>::new());
+
+        let sparse = UnionArray::try_new(
+            fields.clone(),
+            ScalarBuffer::<i8>::from(vec![]),
+            None,
+            vec![],
+        )
+        .unwrap();
+        let dense = UnionArray::try_new(
+            fields,
+            ScalarBuffer::<i8>::from(vec![]),
+            Some(ScalarBuffer::<i32>::from(vec![])),
+            vec![],
+        )
+        .unwrap();
+
+        for values in [&sparse, &dense] {
+            let taken = take(values, &indices, None).unwrap();
+            assert_eq!(taken.len(), 0);
+            assert_eq!(taken.data_type(), values.data_type());
+        }
+    }
+
+    #[test]
+    fn test_take_empty_union_with_null_indices() {
+        let fields = UnionFields::try_new(vec![], 
Vec::<Field>::new()).unwrap();
+        let indices = UInt32Array::from(vec![None]);
+
+        let sparse = UnionArray::try_new(
+            fields.clone(),
+            ScalarBuffer::<i8>::from(vec![]),
+            None,
+            vec![],
+        )
+        .unwrap();
+        let dense = UnionArray::try_new(
+            fields,
+            ScalarBuffer::<i8>::from(vec![]),
+            Some(ScalarBuffer::<i32>::from(vec![])),
+            vec![],
+        )
+        .unwrap();
+
+        for values in [&sparse, &dense] {
+            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"
+            );
+        }
+    }
+
     #[test]
     fn test_take_union_dense_using_builder() {
         let mut builder = UnionBuilder::new_dense();

Reply via email to