This is an automated email from the ASF dual-hosted git repository.

alamb 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 fbe75a3bd9 arrow-cast: Add optimized path for unnesting a dict (#10248)
fbe75a3bd9 is described below

commit fbe75a3bd9df6d66658e2954e14163e5a93a4187
Author: Frederic Branczyk <[email protected]>
AuthorDate: Tue Jun 30 20:33:49 2026 +0200

    arrow-cast: Add optimized path for unnesting a dict (#10248)
    
    # Which issue does this PR close?
    
    - Closes https://github.com/apache/arrow-rs/issues/10247
    
    # What changes are included in this PR?
    
    Benchmark and optimization.
    
    # Are these changes tested?
    
    Benchmark:
    
    `cargo bench -p arrow --bench cast_kernels --features test_utils --
    "cast nested dict to dict"`
    
    Before (unpack inner values):
    ```
    cast nested dict to dict
                            time:   [12.148 µs 12.217 µs 12.283 µs]
    ```
    
    After (compose indices, reuse inner values):
    ```
    cast nested dict to dict
                            time:   [2.7624 µs 2.7679 µs 2.7754 µs]
                            change: [−77.294% −77.149% −77.001%] (p = 0.00 < 
0.05)
                            Performance has improved.
    ```
    
    ~4.4x faster
    
    # Are there any user-facing changes?
    
    No, just an optimization.
    
    @alamb @Jefffrey
---
 arrow-cast/src/cast/dictionary.rs | 18 ++++++++++++++++++
 arrow-cast/src/cast/mod.rs        | 30 ++++++++++++++++++++++++++++++
 arrow/benches/cast_kernels.rs     | 28 ++++++++++++++++++++++++++++
 3 files changed, 76 insertions(+)

diff --git a/arrow-cast/src/cast/dictionary.rs 
b/arrow-cast/src/cast/dictionary.rs
index 83aa691482..db3611f551 100644
--- a/arrow-cast/src/cast/dictionary.rs
+++ b/arrow-cast/src/cast/dictionary.rs
@@ -59,6 +59,24 @@ fn dictionary_to_dictionary_cast<K: ArrowDictionaryKeyType>(
 ) -> Result<ArrayRef, ArrowError> {
     use DataType::*;
 
+    // Fast path for a nested dictionary source (`Dictionary<K, Dictionary<K2, 
V>>`).
+    // Both layers index into the same inner values, so the two index layers 
can
+    // be composed into one rather than materializing the values: `take` 
gathers
+    // the inner keys through the outer keys and reuses the inner values buffer
+    // untouched, so no value data is rewritten. The flattened single-level
+    // dictionary is then cast to the requested index/value types.
+    if matches!(array.values().data_type(), Dictionary(_, _)) {
+        let flattened = take(array.values().as_ref(), array.keys(), None)?;
+        return cast_with_options(
+            &flattened,
+            &Dictionary(
+                Box::new(to_index_type.clone()),
+                Box::new(to_value_type.clone()),
+            ),
+            cast_options,
+        );
+    }
+
     let keys_array: ArrayRef = 
Arc::new(PrimitiveArray::<K>::from(array.keys().to_data()));
     let values_array = array.values();
     let cast_keys = cast_with_options(&keys_array, to_index_type, 
cast_options)?;
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index d903bd0f04..20a3068e95 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -8932,6 +8932,36 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_cast_nested_dictionary_to_dictionary_reuses_values() {
+        let inner = DictionaryArray::<Int32Type>::new(
+            Int32Array::from(vec![Some(0), None, Some(1)]),
+            Arc::new(StringArray::from(vec!["x", "y"])),
+        );
+        let nested = DictionaryArray::<Int32Type>::new(
+            Int32Array::from(vec![Some(0), Some(1), Some(2), None, Some(0)]),
+            Arc::new(inner),
+        );
+
+        let result = cast(&nested, &Dictionary(Box::new(Int32), 
Box::new(Utf8))).unwrap();
+        let result = result.as_dictionary::<Int32Type>();
+
+        assert_eq!(
+            result.keys(),
+            &Int32Array::from(vec![Some(0), None, Some(1), None, Some(0)])
+        );
+        assert_eq!(
+            result.values().as_string::<i32>(),
+            &StringArray::from(vec!["x", "y"])
+        );
+        let logical: Vec<Option<&str>> = result
+            .downcast_dict::<StringArray>()
+            .unwrap()
+            .into_iter()
+            .collect();
+        assert_eq!(logical, vec![Some("x"), None, Some("y"), None, Some("x")]);
+    }
+
     #[test]
     fn test_cast_primitive_dict() {
         // FROM a dictionary with of INT32 values
diff --git a/arrow/benches/cast_kernels.rs b/arrow/benches/cast_kernels.rs
index 82e9696f04..7e98168924 100644
--- a/arrow/benches/cast_kernels.rs
+++ b/arrow/benches/cast_kernels.rs
@@ -185,6 +185,25 @@ fn build_dict_array(size: usize) -> ArrayRef {
     Arc::new(DictionaryArray::new(keys, Arc::new(values)))
 }
 
+// Dictionary<UInt32, Dictionary<UInt32, Utf8>>: an inner dictionary of 4096
+// entries over 64 moderately long distinct values, wrapped by `size` outer 
keys
+// that index into it. Casting to Dictionary<UInt32, Utf8> flattens the two 
index
+// layers; the old path unpacks (copies) the inner values, the fast path reuses
+// them and only remaps indices.
+fn build_nested_dict_array(size: usize) -> ArrayRef {
+    let distinct: Vec<String> = (0..64)
+        .map(|i| format!("a moderately long symbol name number {i:04}"))
+        .collect();
+    let inner_values = StringArray::from_iter(distinct.iter().map(|s| 
Some(s.as_str())));
+    let inner_len = 4096u32;
+    let distinct_len = inner_values.len() as u32;
+    let inner_keys = UInt32Array::from_iter((0..inner_len).map(|v| v % 
distinct_len));
+    let inner = DictionaryArray::new(inner_keys, Arc::new(inner_values));
+
+    let outer_keys = UInt32Array::from_iter((0..size as u32).map(|v| v % 
inner_len));
+    Arc::new(DictionaryArray::new(outer_keys, Arc::new(inner)))
+}
+
 // cast array from specified primitive array type to desired data type
 fn cast_array(array: &ArrayRef, to_type: DataType) {
     hint::black_box(cast(hint::black_box(array), 
hint::black_box(&to_type)).unwrap());
@@ -212,6 +231,7 @@ fn add_benchmark(c: &mut Criterion) {
     let wide_string_array = cast(&string_array, &DataType::LargeUtf8).unwrap();
 
     let dict_array = build_dict_array(10_000);
+    let nested_dict_array = build_nested_dict_array(10_000);
     let string_view_array = cast(&dict_array, &DataType::Utf8View).unwrap();
     let binary_view_array = cast(&string_view_array, 
&DataType::BinaryView).unwrap();
 
@@ -330,6 +350,14 @@ fn add_benchmark(c: &mut Criterion) {
     c.bench_function("cast dict to string view", |b| {
         b.iter(|| cast_array(&dict_array, DataType::Utf8View))
     });
+    c.bench_function("cast nested dict to dict", |b| {
+        b.iter(|| {
+            cast_array(
+                &nested_dict_array,
+                DataType::Dictionary(Box::new(DataType::UInt32), 
Box::new(DataType::Utf8)),
+            )
+        })
+    });
     c.bench_function("cast string view to dict", |b| {
         b.iter(|| {
             cast_array(

Reply via email to