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 a7f3ba8f3a Fix panic on lossy decimal to float casting: round to
saturation for overflows (#7887)
a7f3ba8f3a is described below
commit a7f3ba8f3a748243af1575bce8d50dfc6a81ab73
Author: kosiew <[email protected]>
AuthorDate: Wed Jul 23 19:29:38 2025 +0800
Fix panic on lossy decimal to float casting: round to saturation for
overflows (#7887)
# Which issue does this PR close?
Closes #7886.
# Rationale for this change
Casting large `Decimal256` values to `Float64` can exceed the
representable range of floating point numbers. Previously, this could
result in a panic due to unwrapping a failed conversion.
This PR introduces a safe conversion that saturates overflowing values
to `INFINITY` or `-INFINITY`, following standard floating point
semantics. This ensures stable, predictable behavior without runtime
crashes.
# What changes are included in this PR?
- Introduced a helper function `decimal256_to_f64` that converts `i256`
to `f64`, returning `INFINITY` or `-INFINITY` when the value is out of
range.
- Updated the casting logic for `Decimal256` → `Float64` to use the new
safe conversion.
- Improved inline and module-level documentation to reflect that this
conversion is lossy and saturating.
- Added a unit test `test_cast_decimal256_to_f64_overflow` to validate
overflow behavior.
# Are there any user-facing changes?
Yes.
- **Behavior Change:** When casting `Decimal256` values that exceed the
`f64` range, users now receive `INFINITY` or `-INFINITY` instead of a
panic.
- **Improved Docs:** Updated documentation clarifies the lossy and
saturating behavior of decimal-to-float casting.
- **Not a Breaking Change:** There are no API changes, but users relying
on panics for overflow detection may observe different behavior.
---
arrow-cast/src/cast/decimal.rs | 6 +++++-
arrow-cast/src/cast/mod.rs | 37 ++++++++++++++++++++++++++++++++++++-
2 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/arrow-cast/src/cast/decimal.rs b/arrow-cast/src/cast/decimal.rs
index 57dfc51d74..597f384fa4 100644
--- a/arrow-cast/src/cast/decimal.rs
+++ b/arrow-cast/src/cast/decimal.rs
@@ -614,7 +614,11 @@ where
Ok(Arc::new(value_builder.finish()))
}
-// Cast the decimal array to floating-point array
+/// Cast a decimal array to a floating point array.
+///
+/// Conversion is lossy and follows standard floating point semantics. Values
+/// that exceed the representable range become `INFINITY` or `-INFINITY`
without
+/// returning an error.
pub(crate) fn cast_decimal_to_float<D: DecimalType, T: ArrowPrimitiveType, F>(
array: &dyn Array,
op: F,
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index d8cc514100..dbe4401c78 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -603,6 +603,8 @@ fn timestamp_to_date32<T: ArrowTimestampType>(
/// * Temporal to/from backing Primitive: zero-copy with data type change
/// * `Float32/Float64` to `Decimal(precision, scale)` rounds to the `scale`
decimals
/// (i.e. casting `6.4999` to `Decimal(10, 1)` becomes `6.5`).
+/// * `Decimal` to `Float32/Float64` is lossy and values outside the
representable
+/// range become `INFINITY` or `-INFINITY` without error.
///
/// Unsupported Casts (check with `can_cast_types` before calling):
/// * To or from `StructArray`
@@ -891,7 +893,7 @@ pub fn cast_with_options(
scale,
from_type,
to_type,
- |x: i256| x.to_f64().unwrap(),
+ |x: i256| decimal256_to_f64(x),
cast_options,
)
}
@@ -1993,6 +1995,17 @@ where
}
}
+/// Convert a [`i256`] to `f64` saturating to infinity on overflow.
+fn decimal256_to_f64(v: i256) -> f64 {
+ v.to_f64().unwrap_or_else(|| {
+ if v.is_negative() {
+ f64::NEG_INFINITY
+ } else {
+ f64::INFINITY
+ }
+ })
+}
+
fn cast_to_decimal<D, M>(
array: &dyn Array,
base: M,
@@ -8660,6 +8673,28 @@ mod tests {
"did not find expected error '{expected_error}' in actual error
'{err}'"
);
}
+ #[test]
+ fn test_cast_decimal256_to_f64_overflow() {
+ // Test positive overflow (positive infinity)
+ let array = vec![Some(i256::MAX)];
+ let array = create_decimal256_array(array, 76, 2).unwrap();
+ let array = Arc::new(array) as ArrayRef;
+
+ let result = cast(&array, &DataType::Float64).unwrap();
+ let result = result.as_primitive::<Float64Type>();
+ assert!(result.value(0).is_infinite());
+ assert!(result.value(0) > 0.0); // Positive infinity
+
+ // Test negative overflow (negative infinity)
+ let array = vec![Some(i256::MIN)];
+ let array = create_decimal256_array(array, 76, 2).unwrap();
+ let array = Arc::new(array) as ArrayRef;
+
+ let result = cast(&array, &DataType::Float64).unwrap();
+ let result = result.as_primitive::<Float64Type>();
+ assert!(result.value(0).is_infinite());
+ assert!(result.value(0) < 0.0); // Negative infinity
+ }
#[test]
fn test_cast_decimal128_to_decimal128_negative_scale() {