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 2edc0a044f fix(arrow-data): allow full dictionary key range when 
concatenating (#10323)
2edc0a044f is described below

commit 2edc0a044f495606e65878dadffe8d1a64f43c01
Author: raphaelroshan <[email protected]>
AuthorDate: Thu Jul 16 01:21:21 2026 +0800

    fix(arrow-data): allow full dictionary key range when concatenating (#10323)
    
    # Which issue does this PR close?
    
    Closes #9366.
    
    # Rationale for this change
    
    Concatenating dictionary arrays whose merged dictionary length is
    exactly the key type's maximum value + 1 (e.g. 256 distinct values with
    `u8` keys) panics with `DictionaryKeyOverflowError`. The largest key
    index is `length - 1`, so 256 values use keys `0..=255`, which fit in
    `u8` — the overflow check was off by one.
    
    # What changes are included in this PR?
    
    `build_extend_dictionary` now checks that the key type can hold `max -
    1` (the largest index) rather than `max` (the length).
    
    # Are these changes tested?
    
    Yes — added `test_dict_overflow_9366`, which concatenates two
    `Dictionary<u8, _>` arrays totaling 256 values and asserts it succeeds
    (panics before this change).
    
    # Are there any user-facing changes?
    
    No API change; a previously-panicking concat now succeeds.
    
    Co-authored-by: raphaelroshan <[email protected]>
---
 arrow-data/src/transform/mod.rs |  5 ++++-
 arrow-select/src/concat.rs      | 50 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/arrow-data/src/transform/mod.rs b/arrow-data/src/transform/mod.rs
index 21570d8c02..7b43c090a9 100644
--- a/arrow-data/src/transform/mod.rs
+++ b/arrow-data/src/transform/mod.rs
@@ -195,7 +195,10 @@ impl std::fmt::Debug for MutableArrayData<'_> {
 fn build_extend_dictionary(array: &ArrayData, offset: usize, max: usize) -> 
Option<Extend<'_>> {
     macro_rules! validate_and_build {
         ($dt: ty) => {{
-            let _: $dt = max.try_into().ok()?;
+            // `max` is the merged dictionary length; the largest key index is
+            // `max - 1`, so the key type only needs to hold `max - 1` (e.g. 
256
+            // values use keys 0..=255, which fit in u8).
+            let _: $dt = max.saturating_sub(1).try_into().ok()?;
             let offset: $dt = offset.try_into().ok()?;
             Some(primitive::build_extend_with_offset(array, offset))
         }};
diff --git a/arrow-select/src/concat.rs b/arrow-select/src/concat.rs
index 839fceae34..f18624c5ad 100644
--- a/arrow-select/src/concat.rs
+++ b/arrow-select/src/concat.rs
@@ -644,6 +644,56 @@ mod tests {
     use arrow_schema::{Field, Schema};
     use std::fmt::Debug;
 
+    #[test]
+    fn test_dict_overflow_9366() {
+        use arrow_schema::DataType;
+
+        let schema = Arc::new(Schema::new(vec![Field::new(
+            "a",
+            DataType::Dictionary(
+                Box::new(DataType::UInt8),
+                Box::new(DataType::FixedSizeBinary(8)),
+            ),
+            false,
+        )]));
+        let make = |vals: std::ops::Range<u64>| {
+            let dict = FixedSizeBinaryArray::try_from_iter(vals.map(|i| 
i.to_le_bytes())).unwrap();
+            let keys = UInt8Array::from_iter_values(0..128);
+            let arr = DictionaryArray::try_new(keys, Arc::new(dict)).unwrap();
+            RecordBatch::try_new(schema.clone(), vec![Arc::new(arr)]).unwrap()
+        };
+        // 256 distinct values fit in u8 keys (0..=255): concat must succeed.
+        let out = concat_batches(&schema, &[make(0..128), 
make(128..256)]).unwrap();
+        assert_eq!(out.num_rows(), 256);
+        let dict = out.column(0).as_dictionary::<UInt8Type>();
+        assert_eq!(dict.values().len(), 256);
+    }
+
+    #[test]
+    fn test_dict_overflow_i8_9366() {
+        use arrow_schema::DataType;
+
+        // Same boundary for a signed key type: i8 holds 128 keys (0..=127).
+        let schema = Arc::new(Schema::new(vec![Field::new(
+            "a",
+            DataType::Dictionary(
+                Box::new(DataType::Int8),
+                Box::new(DataType::FixedSizeBinary(8)),
+            ),
+            false,
+        )]));
+        let make = |vals: std::ops::Range<u64>| {
+            let dict = FixedSizeBinaryArray::try_from_iter(vals.map(|i| 
i.to_le_bytes())).unwrap();
+            let keys = Int8Array::from_iter_values(0..64);
+            let arr = DictionaryArray::try_new(keys, Arc::new(dict)).unwrap();
+            RecordBatch::try_new(schema.clone(), vec![Arc::new(arr)]).unwrap()
+        };
+        let out = concat_batches(&schema, &[make(0..64), 
make(64..128)]).unwrap();
+        assert_eq!(out.num_rows(), 128);
+        let dict = out.column(0).as_dictionary::<Int8Type>();
+        assert_eq!(dict.values().len(), 128);
+    }
+
     #[test]
     fn test_concat_empty_vec() {
         let re = concat(&[]);

Reply via email to