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);