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 365c76066c fix(arrow-data): don't panic on dictionary key overflow in 
interleave/concat (#10675)
365c76066c is described below

commit 365c76066c1ca647090eaf61b4b4df536cca82e1
Author: Danila Gornushko <[email protected]>
AuthorDate: Thu Aug 20 04:31:40 2026 +0300

    fix(arrow-data): don't panic on dictionary key overflow in 
interleave/concat (#10675)
    
    # Which issue does this PR close?
    
    - Closes #10674.
    
    # Rationale for this change
    
    `interleave()` and `concat()` document a `Result` return type, but for
    `Dictionary<K, Utf8View>`/`Dictionary<K, BinaryView>` arrays (top-level
    or nested inside a
    `List`/`FixedSizeList`/`Struct`/`RunEndEncoded`/`Union`), a genuine
    dictionary key overflow currently panics instead of returning `Err`.
    This happens because the fallback path builds a `MutableArrayData`
    directly and `.expect()`s the dictionary-concat result, and
    `MutableArrayData::new`/`with_capacities` have no fallible variant. See
    #10674 for the full analysis and a minimal repro.
    
    # What changes are included in this PR?
    
    - Add `MutableArrayData::try_new` / `try_with_capacities`, fallible
    variants of `new` / `with_capacities` that return
    `Err(ArrowError::DictionaryKeyOverflowError)` instead of panicking on
    dictionary key overflow. `new()`/`with_capacities()` keep their existing
    panicking behavior (now implemented as
    `try_with_capacities(..).expect(..)`) for the many existing callers that
    rely on infallibility.
    - Recursive child construction inside `try_with_capacities` (for
    
`List`/`LargeList`/`Map`/`ListView`/`LargeListView`/`FixedSizeList`/`Struct`/`RunEndEncoded`/`Union`)
    also uses the fallible variants and propagates errors with `?`, so
    dictionaries nested inside container types are covered, not just
    top-level dictionary arrays.
    - Switch `arrow-select`'s `interleave_fallback` and `concat_fallback`
    (the paths reached for dictionary arrays whose values can't/shouldn't be
    merged) to the fallible constructors.
    
    # Are these changes tested?
    
    Yes:
    - `concat_string_view_dictionary_overflow_returns_err` /
    `test_interleave_string_view_dictionary_overflow_returns_err`: top-level
    `Dictionary<UInt8, Utf8View>` overflow returns `Err` instead of
    panicking.
    - `concat_nested_dictionary_overflow_returns_err` /
    `test_interleave_nested_dictionary_overflow_returns_err`: same overflow
    nested inside a `FixedSizeList`, exercising the recursive child
    construction.
    
    Full `arrow-data`/`arrow-select` test suites pass (410 tests), `cargo
    fmt --check` and `cargo clippy --all-targets -- -D warnings` are clean
    for both crates.
    
    # Are there any user-facing changes?
    
    No breaking changes. `MutableArrayData::new`/`with_capacities` keep
    their documented panicking behavior and signatures. Two new public
    fallible methods are added (`try_new`, `try_with_capacities`).
    `interleave()`/`concat()` keep their existing `Result` signature -- the
    only visible change is that a specific previously-panicking input
    (genuine dictionary key overflow on `Utf8View`/`BinaryView`
    dictionaries) now returns `Err(ArrowError::DictionaryKeyOverflowError)`
    instead.
    
    ---------
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-data/src/transform/mod.rs | 70 ++++++++++++++++++++++++++++++-----------
 arrow-select/src/concat.rs      | 41 +++++++++++++++++++++++-
 arrow-select/src/interleave.rs  | 45 +++++++++++++++++++++++++-
 3 files changed, 135 insertions(+), 21 deletions(-)

diff --git a/arrow-data/src/transform/mod.rs b/arrow-data/src/transform/mod.rs
index 7145463ac4..6571e0b658 100644
--- a/arrow-data/src/transform/mod.rs
+++ b/arrow-data/src/transform/mod.rs
@@ -410,6 +410,20 @@ impl<'a> MutableArrayData<'a> {
         Self::with_capacities(arrays, use_nulls, Capacities::Array(capacity))
     }
 
+    /// Fallible variant of [MutableArrayData::new].
+    ///
+    /// Unlike [MutableArrayData::new], this does not panic when merging 
dictionary
+    /// arrays whose combined values would overflow the dictionary key type. 
Instead,
+    /// it returns an error, letting callers (e.g. [`interleave`](crate) / 
`concat`)
+    /// surface it as a normal error.
+    pub fn try_new(
+        arrays: Vec<&'a ArrayData>,
+        use_nulls: bool,
+        capacity: usize,
+    ) -> Result<Self, ArrowError> {
+        Self::try_with_capacities(arrays, use_nulls, 
Capacities::Array(capacity))
+    }
+
     /// Similar to [MutableArrayData::new], but lets users define the
     /// preallocated capacities of the array with more granularity.
     ///
@@ -417,13 +431,31 @@ impl<'a> MutableArrayData<'a> {
     ///
     /// # Panics
     ///
-    /// This function panics if the given `capacities` don't match the data 
type
-    /// of `arrays`. Or when a [Capacities] variant is not yet supported.
+    /// * if the given `capacities` don't match the data type of `arrays`
+    /// * if a [Capacities] variant is not yet supported
+    /// * when merging dictionary arrays whose combined values overflow the
+    ///   dictionary key type — see [MutableArrayData::try_with_capacities] 
for a
+    ///   fallible variant
     pub fn with_capacities(
         arrays: Vec<&'a ArrayData>,
         use_nulls: bool,
         capacities: Capacities,
     ) -> Self {
+        Self::try_with_capacities(arrays, use_nulls, capacities)
+            .expect("MutableArrayData::new is infallible")
+    }
+
+    /// Fallible variant of [MutableArrayData::with_capacities].
+    ///
+    /// Returns an error instead of panicking when merging dictionary arrays 
whose
+    /// combined values would overflow the dictionary key type. Still panics 
for
+    /// other unsupported combinations (inconsistent input types, unsupported
+    /// `Capacities` variants) as documented on 
[MutableArrayData::with_capacities].
+    pub fn try_with_capacities(
+        arrays: Vec<&'a ArrayData>,
+        use_nulls: bool,
+        capacities: Capacities,
+    ) -> Result<Self, ArrowError> {
         let data_type = arrays[0].data_type();
 
         for a in arrays.iter().skip(1) {
@@ -522,9 +554,9 @@ impl<'a> MutableArrayData<'a> {
                         Capacities::Array(array_capacity)
                     };
 
-                vec![MutableArrayData::with_capacities(
+                vec![MutableArrayData::try_with_capacities(
                     children, use_nulls, capacities,
-                )]
+                )?]
             }
             // the dictionary type just appends keys and clones the values.
             DataType::Dictionary(_, _) => vec![],
@@ -538,13 +570,13 @@ impl<'a> MutableArrayData<'a> {
                                 .iter()
                                 .map(|array| &array.child_data()[i])
                                 .collect::<Vec<_>>();
-                            MutableArrayData::with_capacities(
+                            MutableArrayData::try_with_capacities(
                                 child_arrays,
                                 use_nulls,
                                 child_cap.clone(),
                             )
                         })
-                        .collect::<Vec<_>>()
+                        .collect::<Result<Vec<_>, _>>()?
                 }
                 Capacities::Struct(capacity, None) => {
                     array_capacity = capacity;
@@ -554,9 +586,9 @@ impl<'a> MutableArrayData<'a> {
                                 .iter()
                                 .map(|array| &array.child_data()[i])
                                 .collect::<Vec<_>>();
-                            MutableArrayData::new(child_arrays, use_nulls, 
capacity)
+                            MutableArrayData::try_new(child_arrays, use_nulls, 
capacity)
                         })
-                        .collect::<Vec<_>>()
+                        .collect::<Result<Vec<_>, _>>()?
                 }
                 _ => (0..fields.len())
                     .map(|i| {
@@ -564,9 +596,9 @@ impl<'a> MutableArrayData<'a> {
                             .iter()
                             .map(|array| &array.child_data()[i])
                             .collect::<Vec<_>>();
-                        MutableArrayData::new(child_arrays, use_nulls, 
array_capacity)
+                        MutableArrayData::try_new(child_arrays, use_nulls, 
array_capacity)
                     })
-                    .collect::<Vec<_>>(),
+                    .collect::<Result<Vec<_>, _>>()?,
             },
             DataType::RunEndEncoded(_, _) => {
                 let run_ends_child = arrays
@@ -578,8 +610,8 @@ impl<'a> MutableArrayData<'a> {
                     .map(|array| &array.child_data()[1])
                     .collect::<Vec<_>>();
                 vec![
-                    MutableArrayData::new(run_ends_child, false, 
array_capacity),
-                    MutableArrayData::new(value_child, use_nulls, 
array_capacity),
+                    MutableArrayData::try_new(run_ends_child, false, 
array_capacity)?,
+                    MutableArrayData::try_new(value_child, use_nulls, 
array_capacity)?,
                 ]
             }
             DataType::FixedSizeList(_, size) => {
@@ -596,9 +628,9 @@ impl<'a> MutableArrayData<'a> {
                     } else {
                         Capacities::Array(array_capacity * *size as usize)
                     };
-                vec![MutableArrayData::with_capacities(
+                vec![MutableArrayData::try_with_capacities(
                     children, use_nulls, capacities,
-                )]
+                )?]
             }
             DataType::Union(fields, _) => (0..fields.len())
                 .map(|i| {
@@ -606,9 +638,9 @@ impl<'a> MutableArrayData<'a> {
                         .iter()
                         .map(|array| &array.child_data()[i])
                         .collect::<Vec<_>>();
-                    MutableArrayData::new(child_arrays, use_nulls, 
array_capacity)
+                    MutableArrayData::try_new(child_arrays, use_nulls, 
array_capacity)
                 })
-                .collect::<Vec<_>>(),
+                .collect::<Result<Vec<_>, _>>()?,
         };
 
         // Get the dictionary if any, and if it is a concatenation of multiple
@@ -688,7 +720,7 @@ impl<'a> MutableArrayData<'a> {
                     })
                     .collect();
 
-                extend_values.expect("MutableArrayData::new is infallible")
+                extend_values?
             }
             DataType::BinaryView | DataType::Utf8View => {
                 let mut next_offset = 0u32;
@@ -716,7 +748,7 @@ impl<'a> MutableArrayData<'a> {
             buffer2,
             child_data,
         };
-        Self {
+        Ok(Self {
             arrays,
             data,
             dictionary,
@@ -724,7 +756,7 @@ impl<'a> MutableArrayData<'a> {
             extend_values,
             extend_null_bits,
             extend_nulls,
-        }
+        })
     }
 
     /// Extends the in progress array with a region of the input arrays, 
returning an error on
diff --git a/arrow-select/src/concat.rs b/arrow-select/src/concat.rs
index a8d9117415..e1384edbc5 100644
--- a/arrow-select/src/concat.rs
+++ b/arrow-select/src/concat.rs
@@ -579,7 +579,7 @@ pub fn concat(arrays: &[&dyn Array]) -> Result<ArrayRef, 
ArrowError> {
 fn concat_fallback(arrays: &[&dyn Array], capacity: Capacities) -> 
Result<ArrayRef, ArrowError> {
     let array_data: Vec<_> = arrays.iter().map(|a| 
a.to_data()).collect::<Vec<_>>();
     let array_data = array_data.iter().collect();
-    let mut mutable = MutableArrayData::with_capacities(array_data, false, 
capacity);
+    let mut mutable = MutableArrayData::try_with_capacities(array_data, false, 
capacity)?;
 
     for (i, a) in arrays.iter().enumerate() {
         mutable.try_extend(i, 0, a.len())?
@@ -1732,6 +1732,45 @@ mod tests {
         );
     }
 
+    #[test]
+    fn concat_string_view_dictionary_overflow_returns_err() {
+        // concatenating dictionaries which results in overflowing the key 
type should
+        // surface an error not a panic
+        let values_a: StringViewArray = (0..200).map(|i| 
Some(format!("a{i}"))).collect();
+        let keys_a = UInt8Array::from_iter_values(0..200);
+        let dict_a = DictionaryArray::<UInt8Type>::new(keys_a, 
Arc::new(values_a));
+
+        let values_b: StringViewArray = (0..200).map(|i| 
Some(format!("b{i}"))).collect();
+        let keys_b = UInt8Array::from_iter_values(0..200);
+        let dict_b = DictionaryArray::<UInt8Type>::new(keys_b, 
Arc::new(values_b));
+
+        let err = concat(&[&dict_a, &dict_b]).unwrap_err();
+        assert!(matches!(err, ArrowError::DictionaryKeyOverflowError));
+    }
+
+    #[test]
+    fn concat_nested_dictionary_overflow_returns_err() {
+        // same as above, but with the dictionary nested inside a FixedSizeList
+        let field = Arc::new(arrow_schema::Field::new(
+            "item",
+            DataType::Dictionary(Box::new(DataType::UInt8), 
Box::new(DataType::Utf8View)),
+            false,
+        ));
+
+        let values_a: StringViewArray = (0..200).map(|i| 
Some(format!("a{i}"))).collect();
+        let keys_a = UInt8Array::from_iter_values(0..200);
+        let dict_a = DictionaryArray::<UInt8Type>::new(keys_a, 
Arc::new(values_a));
+        let list_a = FixedSizeListArray::new(field.clone(), 1, 
Arc::new(dict_a), None);
+
+        let values_b: StringViewArray = (0..200).map(|i| 
Some(format!("b{i}"))).collect();
+        let keys_b = UInt8Array::from_iter_values(0..200);
+        let dict_b = DictionaryArray::<UInt8Type>::new(keys_b, 
Arc::new(values_b));
+        let list_b = FixedSizeListArray::new(field, 1, Arc::new(dict_b), None);
+
+        let err = concat(&[&list_a, &list_b]).unwrap_err();
+        assert!(matches!(err, ArrowError::DictionaryKeyOverflowError));
+    }
+
     #[test]
     #[cfg_attr(miri, ignore)] // Takes too long
     fn concat_many_dictionary_list_arrays() {
diff --git a/arrow-select/src/interleave.rs b/arrow-select/src/interleave.rs
index f56864ac65..6d5033b1d4 100644
--- a/arrow-select/src/interleave.rs
+++ b/arrow-select/src/interleave.rs
@@ -768,7 +768,7 @@ fn interleave_fallback(
 ) -> Result<ArrayRef, ArrowError> {
     let arrays: Vec<_> = values.iter().map(|x| x.to_data()).collect();
     let arrays: Vec<_> = arrays.iter().collect();
-    let mut array_data = MutableArrayData::new(arrays, false, indices.len());
+    let mut array_data = MutableArrayData::try_new(arrays, false, 
indices.len())?;
 
     let mut cur_array = indices[0].0;
     let mut start_row_idx = indices[0].1;
@@ -2033,6 +2033,49 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_interleave_string_view_dictionary_overflow_returns_err() {
+        // interleaving dictionaries which results in overflowing the key type 
should
+        // surface an error not a panic
+        let values_a: StringViewArray = (0..200).map(|i| 
Some(format!("a{i}"))).collect();
+        let keys_a = UInt8Array::from_iter_values(0..200);
+        let dict_a = DictionaryArray::<UInt8Type>::new(keys_a, 
Arc::new(values_a));
+
+        let values_b: StringViewArray = (0..200).map(|i| 
Some(format!("b{i}"))).collect();
+        let keys_b = UInt8Array::from_iter_values(0..200);
+        let dict_b = DictionaryArray::<UInt8Type>::new(keys_b, 
Arc::new(values_b));
+
+        let indices: Vec<_> = (0..200).flat_map(|i| [(0, i), (1, 
i)]).collect();
+
+        let err = interleave(&[&dict_a, &dict_b], &indices).unwrap_err();
+        assert!(matches!(err, ArrowError::DictionaryKeyOverflowError));
+    }
+
+    #[test]
+    fn test_interleave_nested_dictionary_overflow_returns_err() {
+        // same as above, but with the dictionary nested inside a FixedSizeList
+        let field = Arc::new(arrow_schema::Field::new(
+            "item",
+            DataType::Dictionary(Box::new(DataType::UInt8), 
Box::new(DataType::Utf8View)),
+            false,
+        ));
+
+        let values_a: StringViewArray = (0..200).map(|i| 
Some(format!("a{i}"))).collect();
+        let keys_a = UInt8Array::from_iter_values(0..200);
+        let dict_a = DictionaryArray::<UInt8Type>::new(keys_a, 
Arc::new(values_a));
+        let list_a = FixedSizeListArray::new(field.clone(), 1, 
Arc::new(dict_a), None);
+
+        let values_b: StringViewArray = (0..200).map(|i| 
Some(format!("b{i}"))).collect();
+        let keys_b = UInt8Array::from_iter_values(0..200);
+        let dict_b = DictionaryArray::<UInt8Type>::new(keys_b, 
Arc::new(values_b));
+        let list_b = FixedSizeListArray::new(field, 1, Arc::new(dict_b), None);
+
+        let indices: Vec<_> = (0..200).flat_map(|i| [(0, i), (1, 
i)]).collect();
+
+        let err = interleave(&[&list_a, &list_b], &indices).unwrap_err();
+        assert!(matches!(err, ArrowError::DictionaryKeyOverflowError));
+    }
+
     #[test]
     #[cfg_attr(miri, ignore)] // Takes too long
     fn test_interleave_bytes_offset_overflow() {

Reply via email to