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 40f69b47e2 Support casting negative scale decimals to numeric (#9207)
40f69b47e2 is described below

commit 40f69b47e299a97432321d281cc6d4efc7a4ed8a
Author: Chiicake <[email protected]>
AuthorDate: Tue Jan 27 05:52:17 2026 +0800

    Support casting negative scale decimals to numeric (#9207)
    
    # Which issue does this PR close?
    
    - Closes #9201 .
    
    # Rationale for this change
    
    Casting decimals with negative scale to integer types currently errors
    because the scale factor is always applied as a division. Negative
    scales represent powers of ten that should scale the integer value up,
    so the cast should multiply instead.
    
    # What changes are included in this PR?
    
    - Apply the scale factor using multiplication when scale < 0 in
    cast_decimal_to_integer.
    
    # Are these changes tested?
    
    Yes, the test given by issue is passed.
    ```
    // arrow-cast/src/cast/mod.rs
        #[test]
        fn test_cast_decimal_to_numeric_negative_scale() {
            let value_array: Vec<Option<i256>> = vec![
                Some(i256::from_i128(125)),
                Some(i256::from_i128(225)),
                Some(i256::from_i128(325)),
                None,
                Some(i256::from_i128(525)),
            ];
            let array = create_decimal256_array(value_array, 38, -1).unwrap();
    
            generate_cast_test_case!(
                &array,
                Int64Array,
                &DataType::Int64,
                vec![Some(1_250), Some(2_250), Some(3_250), None, Some(5_250)]
            );
        }
    ```
    
    # Are there any user-facing changes?
    
    No.
---
 arrow-cast/src/cast/decimal.rs |  98 ++++++++++++++++++++---------
 arrow-cast/src/cast/mod.rs     | 139 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 209 insertions(+), 28 deletions(-)

diff --git a/arrow-cast/src/cast/decimal.rs b/arrow-cast/src/cast/decimal.rs
index 71338a6921..f8fe06a573 100644
--- a/arrow-cast/src/cast/decimal.rs
+++ b/arrow-cast/src/cast/decimal.rs
@@ -816,7 +816,7 @@ where
 {
     let array = array.as_primitive::<D>();
 
-    let div: D::Native = base.pow_checked(scale as u32).map_err(|_| {
+    let div: D::Native = base.pow_checked(scale.unsigned_abs() as 
u32).map_err(|_| {
         ArrowError::CastError(format!(
             "Cannot cast to {:?}. The scale {} causes overflow.",
             D::PREFIX,
@@ -826,36 +826,78 @@ where
 
     let mut value_builder = PrimitiveBuilder::<T>::with_capacity(array.len());
 
-    if cast_options.safe {
-        for i in 0..array.len() {
-            if array.is_null(i) {
-                value_builder.append_null();
-            } else {
-                let v = array
-                    .value(i)
-                    .div_checked(div)
-                    .ok()
-                    .and_then(<T::Native as NumCast>::from::<D::Native>);
-
-                value_builder.append_option(v);
+    if scale < 0 {
+        match cast_options.safe {
+            true => {
+                for i in 0..array.len() {
+                    if array.is_null(i) {
+                        value_builder.append_null();
+                    } else {
+                        let v = array
+                            .value(i)
+                            .mul_checked(div)
+                            .ok()
+                            .and_then(<T::Native as 
NumCast>::from::<D::Native>);
+                        value_builder.append_option(v);
+                    }
+                }
+            }
+            false => {
+                for i in 0..array.len() {
+                    if array.is_null(i) {
+                        value_builder.append_null();
+                    } else {
+                        let v = array.value(i).mul_checked(div)?;
+
+                        let value =
+                            <T::Native as 
NumCast>::from::<D::Native>(v).ok_or_else(|| {
+                                ArrowError::CastError(format!(
+                                    "value of {:?} is out of range {}",
+                                    v,
+                                    T::DATA_TYPE
+                                ))
+                            })?;
+
+                        value_builder.append_value(value);
+                    }
+                }
             }
         }
     } else {
-        for i in 0..array.len() {
-            if array.is_null(i) {
-                value_builder.append_null();
-            } else {
-                let v = array.value(i).div_checked(div)?;
-
-                let value = <T::Native as 
NumCast>::from::<D::Native>(v).ok_or_else(|| {
-                    ArrowError::CastError(format!(
-                        "value of {:?} is out of range {}",
-                        v,
-                        T::DATA_TYPE
-                    ))
-                })?;
-
-                value_builder.append_value(value);
+        match cast_options.safe {
+            true => {
+                for i in 0..array.len() {
+                    if array.is_null(i) {
+                        value_builder.append_null();
+                    } else {
+                        let v = array
+                            .value(i)
+                            .div_checked(div)
+                            .ok()
+                            .and_then(<T::Native as 
NumCast>::from::<D::Native>);
+                        value_builder.append_option(v);
+                    }
+                }
+            }
+            false => {
+                for i in 0..array.len() {
+                    if array.is_null(i) {
+                        value_builder.append_null();
+                    } else {
+                        let v = array.value(i).div_checked(div)?;
+
+                        let value =
+                            <T::Native as 
NumCast>::from::<D::Native>(v).ok_or_else(|| {
+                                ArrowError::CastError(format!(
+                                    "value of {:?} is out of range {}",
+                                    v,
+                                    T::DATA_TYPE
+                                ))
+                            })?;
+
+                        value_builder.append_value(value);
+                    }
+                }
             }
         }
     }
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index fb77993a30..da2d6a54ae 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -3886,6 +3886,145 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_cast_decimal_to_numeric_negative_scale() {
+        let value_array: Vec<Option<i256>> = vec![
+            Some(i256::from_i128(125)),
+            Some(i256::from_i128(225)),
+            Some(i256::from_i128(325)),
+            None,
+            Some(i256::from_i128(525)),
+        ];
+        let array = create_decimal256_array(value_array, 38, -1).unwrap();
+
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![Some(1_250), Some(2_250), Some(3_250), None, Some(5_250)]
+        );
+
+        let value_array: Vec<Option<i32>> = vec![Some(125), Some(225), 
Some(325), None, Some(525)];
+        let array = create_decimal32_array(value_array, 8, -2).unwrap();
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![Some(12_500), Some(22_500), Some(32_500), None, Some(52_500)]
+        );
+
+        let value_array: Vec<Option<i32>> = vec![Some(2), Some(1), None];
+        let array = create_decimal32_array(value_array, 9, -9).unwrap();
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![Some(2_000_000_000), Some(1_000_000_000), None]
+        );
+
+        let value_array: Vec<Option<i64>> = vec![Some(125), Some(225), 
Some(325), None, Some(525)];
+        let array = create_decimal64_array(value_array, 18, -3).unwrap();
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![
+                Some(125_000),
+                Some(225_000),
+                Some(325_000),
+                None,
+                Some(525_000)
+            ]
+        );
+
+        let value_array: Vec<Option<i64>> = vec![Some(12), Some(34), None];
+        let array = create_decimal64_array(value_array, 18, -10).unwrap();
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![Some(120_000_000_000), Some(340_000_000_000), None]
+        );
+
+        let value_array: Vec<Option<i128>> = vec![Some(125), Some(225), 
Some(325), None, Some(525)];
+        let array = create_decimal128_array(value_array, 38, -4).unwrap();
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![
+                Some(1_250_000),
+                Some(2_250_000),
+                Some(3_250_000),
+                None,
+                Some(5_250_000)
+            ]
+        );
+
+        let value_array: Vec<Option<i128>> = vec![Some(9), Some(1), None];
+        let array = create_decimal128_array(value_array, 38, -18).unwrap();
+        generate_cast_test_case!(
+            &array,
+            Int64Array,
+            &DataType::Int64,
+            vec![
+                Some(9_000_000_000_000_000_000),
+                Some(1_000_000_000_000_000_000),
+                None
+            ]
+        );
+
+        let array = create_decimal32_array(vec![Some(999_999_999)], 9, 
-1).unwrap();
+        let casted_array = cast_with_options(
+            &array,
+            &DataType::Int64,
+            &CastOptions {
+                safe: false,
+                format_options: FormatOptions::default(),
+            },
+        );
+        assert_eq!(
+            "Arithmetic overflow: Overflow happened on: 999999999 * 
10".to_string(),
+            casted_array.unwrap_err().to_string()
+        );
+
+        let casted_array = cast_with_options(
+            &array,
+            &DataType::Int64,
+            &CastOptions {
+                safe: true,
+                format_options: FormatOptions::default(),
+            },
+        );
+        assert!(casted_array.is_ok());
+        assert!(casted_array.unwrap().is_null(0));
+
+        let array = create_decimal64_array(vec![Some(13)], 18, -1).unwrap();
+        let casted_array = cast_with_options(
+            &array,
+            &DataType::Int8,
+            &CastOptions {
+                safe: false,
+                format_options: FormatOptions::default(),
+            },
+        );
+        assert_eq!(
+            "Cast error: value of 130 is out of range Int8".to_string(),
+            casted_array.unwrap_err().to_string()
+        );
+
+        let casted_array = cast_with_options(
+            &array,
+            &DataType::Int8,
+            &CastOptions {
+                safe: true,
+                format_options: FormatOptions::default(),
+            },
+        );
+        assert!(casted_array.is_ok());
+        assert!(casted_array.unwrap().is_null(0));
+    }
+
     #[test]
     fn test_cast_numeric_to_decimal128() {
         let decimal_type = DataType::Decimal128(38, 6);

Reply via email to