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 2f2e705734 feat: add constructor to help efficiently upgrade key for 
GenericBytesDictionaryBuilder (#7611)
2f2e705734 is described below

commit 2f2e705734194aa1256379ff6dd55e58b235323e
Author: albertlockett <[email protected]>
AuthorDate: Fri Jun 13 12:20:49 2025 -0400

    feat: add constructor to help efficiently upgrade key for 
GenericBytesDictionaryBuilder (#7611)
    
    # Which issue does this PR close?
    
    - Closes https://github.com/apache/arrow-rs/issues/7610
    
    # Rationale for this change
    
    
    I'm adding this because I would like to have a more efficient method for
    upgrading the key type of a dictionary builder in the case where my
    dictionary keys have overflowed.
    
    # What changes are included in this PR?
    
    
    This adds a method called `try_new_from_builder` to
    `GenericByteDictionaryBuilder` that can be used to construct a new
    builder from the passed argument with the same values and internal
    state, but a keys array builder of a different type (the motivation
    being that the new key type could hold more values).
    
    # Are there any user-facing changes?
    
    ---------
    
    Co-authored-by: Andrew Lamb <[email protected]>
---
 .../builder/generic_bytes_dictionary_builder.rs    | 163 ++++++++++++++++++++-
 1 file changed, 161 insertions(+), 2 deletions(-)

diff --git a/arrow-array/src/builder/generic_bytes_dictionary_builder.rs 
b/arrow-array/src/builder/generic_bytes_dictionary_builder.rs
index ead151d5ce..3713a41123 100644
--- a/arrow-array/src/builder/generic_bytes_dictionary_builder.rs
+++ b/arrow-array/src/builder/generic_bytes_dictionary_builder.rs
@@ -17,10 +17,13 @@
 
 use crate::builder::{ArrayBuilder, GenericByteBuilder, PrimitiveBuilder};
 use crate::types::{ArrowDictionaryKeyType, ByteArrayType, GenericBinaryType, 
GenericStringType};
-use crate::{Array, ArrayRef, DictionaryArray, GenericByteArray, 
TypedDictionaryArray};
+use crate::{
+    Array, ArrayRef, DictionaryArray, GenericByteArray, PrimitiveArray, 
TypedDictionaryArray,
+};
 use arrow_buffer::ArrowNativeType;
 use arrow_schema::{ArrowError, DataType};
 use hashbrown::HashTable;
+use num::NumCast;
 use std::any::Any;
 use std::sync::Arc;
 
@@ -152,6 +155,71 @@ where
             values_builder,
         })
     }
+
+    /// Creates a new `GenericByteDictionaryBuilder` from the existing builder 
with the same
+    /// keys and values, but with a new data type for the keys.
+    ///
+    /// # Example
+    /// ```
+    /// #
+    /// # use arrow_array::builder::StringDictionaryBuilder;
+    /// # use arrow_array::types::{UInt8Type, UInt16Type};
+    /// # use arrow_array::UInt16Array;
+    /// # use arrow_schema::ArrowError;
+    ///
+    /// let mut u8_keyed_builder = StringDictionaryBuilder::<UInt8Type>::new();
+    ///
+    /// // appending too many values causes the dictionary to overflow
+    /// for i in 0..256 {
+    ///     u8_keyed_builder.append_value(format!("{}", i));
+    /// }
+    /// let result = u8_keyed_builder.append("256");
+    /// assert!(matches!(result, 
Err(ArrowError::DictionaryKeyOverflowError{})));
+    ///
+    /// // we need to upgrade to a larger key type
+    /// let mut u16_keyed_builder = 
StringDictionaryBuilder::<UInt16Type>::try_new_from_builder(u8_keyed_builder).unwrap();
+    /// let dictionary_array = u16_keyed_builder.finish();
+    /// let keys = dictionary_array.keys();
+    ///
+    /// assert_eq!(keys, &UInt16Array::from_iter(0..256));
+    /// ```
+    pub fn try_new_from_builder<K2>(
+        mut source: GenericByteDictionaryBuilder<K2, T>,
+    ) -> Result<Self, ArrowError>
+    where
+        K::Native: NumCast,
+        K2: ArrowDictionaryKeyType,
+        K2::Native: NumCast,
+    {
+        let state = source.state;
+        let dedup = source.dedup;
+        let values_builder = source.values_builder;
+
+        let source_keys = source.keys_builder.finish();
+        let new_keys: PrimitiveArray<K> = source_keys.try_unary(|value| {
+            num::cast::cast::<K2::Native, K::Native>(value).ok_or_else(|| {
+                ArrowError::CastError(format!(
+                    "Can't cast dictionary keys from source type {:?} to type 
{:?}",
+                    K2::DATA_TYPE,
+                    K::DATA_TYPE
+                ))
+            })
+        })?;
+
+        // drop source key here because currently source_keys and new_keys are 
holding reference to
+        // the same underlying null_buffer. Below we want to call 
new_keys.into_builder() it must
+        // be the only reference holder.
+        drop(source_keys);
+
+        Ok(Self {
+            state,
+            dedup,
+            keys_builder: new_keys
+                .into_builder()
+                .expect("underlying buffer has no references"),
+            values_builder,
+        })
+    }
 }
 
 impl<K, T> ArrayBuilder for GenericByteDictionaryBuilder<K, T>
@@ -503,7 +571,7 @@ mod tests {
 
     use crate::array::Int8Array;
     use crate::cast::AsArray;
-    use crate::types::{Int16Type, Int32Type, Int8Type, Utf8Type};
+    use crate::types::{Int16Type, Int32Type, Int8Type, UInt16Type, UInt8Type, 
Utf8Type};
     use crate::{ArrowPrimitiveType, BinaryArray, StringArray};
 
     fn test_bytes_dictionary_builder<T>(values: Vec<&T::Native>)
@@ -614,6 +682,97 @@ mod tests {
         ]);
     }
 
+    fn _test_try_new_from_builder_generic_for_key_types<K1, K2, T>(values: 
Vec<&T::Native>)
+    where
+        K1: ArrowDictionaryKeyType,
+        K1::Native: NumCast,
+        K2: ArrowDictionaryKeyType,
+        K2::Native: NumCast + From<u8>,
+        T: ByteArrayType,
+        <T as ByteArrayType>::Native: PartialEq + AsRef<<T as 
ByteArrayType>::Native>,
+    {
+        let mut source = GenericByteDictionaryBuilder::<K1, T>::new();
+        source.append(values[0]).unwrap();
+        source.append(values[1]).unwrap();
+        source.append_null();
+        source.append(values[2]).unwrap();
+
+        let mut result =
+            GenericByteDictionaryBuilder::<K2, 
T>::try_new_from_builder(source).unwrap();
+        let array = result.finish();
+
+        let mut expected_keys_builder = PrimitiveBuilder::<K2>::new();
+        expected_keys_builder
+            .append_value(<<K2 as ArrowPrimitiveType>::Native as 
From<u8>>::from(0u8));
+        expected_keys_builder
+            .append_value(<<K2 as ArrowPrimitiveType>::Native as 
From<u8>>::from(1u8));
+        expected_keys_builder.append_null();
+        expected_keys_builder
+            .append_value(<<K2 as ArrowPrimitiveType>::Native as 
From<u8>>::from(2u8));
+        let expected_keys = expected_keys_builder.finish();
+        assert_eq!(array.keys(), &expected_keys);
+
+        let av = array.values();
+        let ava: &GenericByteArray<T> = 
av.as_any().downcast_ref::<GenericByteArray<T>>().unwrap();
+        assert_eq!(ava.value(0), values[0]);
+        assert_eq!(ava.value(1), values[1]);
+        assert_eq!(ava.value(2), values[2]);
+    }
+
+    fn test_try_new_from_builder<T>(values: Vec<&T::Native>)
+    where
+        T: ByteArrayType,
+        <T as ByteArrayType>::Native: PartialEq + AsRef<<T as 
ByteArrayType>::Native>,
+    {
+        // test cast to bigger size unsigned
+        _test_try_new_from_builder_generic_for_key_types::<UInt8Type, 
UInt16Type, T>(
+            values.clone(),
+        );
+        // test cast going to smaller size unsigned
+        _test_try_new_from_builder_generic_for_key_types::<UInt16Type, 
UInt8Type, T>(
+            values.clone(),
+        );
+        // test cast going to bigger size signed
+        _test_try_new_from_builder_generic_for_key_types::<Int8Type, 
Int16Type, T>(values.clone());
+        // test cast going to smaller size signed
+        _test_try_new_from_builder_generic_for_key_types::<Int32Type, 
Int16Type, T>(values.clone());
+        // test going from signed to signed for different size changes
+        _test_try_new_from_builder_generic_for_key_types::<UInt8Type, 
Int16Type, T>(values.clone());
+        _test_try_new_from_builder_generic_for_key_types::<Int8Type, 
UInt8Type, T>(values.clone());
+        _test_try_new_from_builder_generic_for_key_types::<Int8Type, 
UInt16Type, T>(values.clone());
+        _test_try_new_from_builder_generic_for_key_types::<Int32Type, 
Int16Type, T>(values.clone());
+    }
+
+    #[test]
+    fn test_string_dictionary_builder_try_new_from_builder() {
+        test_try_new_from_builder::<GenericStringType<i32>>(vec!["abc", "def", 
"ghi"]);
+    }
+
+    #[test]
+    fn test_binary_dictionary_builder_try_new_from_builder() {
+        test_try_new_from_builder::<GenericBinaryType<i32>>(vec![b"abc", 
b"def", b"ghi"]);
+    }
+
+    #[test]
+    fn test_try_new_from_builder_cast_fails() {
+        let mut source_builder = StringDictionaryBuilder::<UInt16Type>::new();
+        for i in 0..257 {
+            source_builder.append_value(format!("val{}", i));
+        }
+
+        // there should be too many values that we can't downcast to the 
underlying type
+        // we have keys that wouldn't fit into UInt8Type
+        let result = 
StringDictionaryBuilder::<UInt8Type>::try_new_from_builder(source_builder);
+        assert!(result.is_err());
+        if let Err(e) = result {
+            assert!(matches!(e, ArrowError::CastError(_)));
+            assert_eq!(
+                e.to_string(),
+                "Cast error: Can't cast dictionary keys from source type 
UInt16 to type UInt8"
+            );
+        }
+    }
+
     fn test_bytes_dictionary_builder_with_existing_dictionary<T>(
         dictionary: GenericByteArray<T>,
         values: Vec<&T::Native>,

Reply via email to