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() {

Reply via email to