Skip to content

Commit a7f3ba8

Browse files
authored
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.
1 parent 50f5562 commit a7f3ba8

File tree

2 files changed

+41
-2
lines changed

2 files changed

+41
-2
lines changed

arrow-cast/src/cast/decimal.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,11 @@ where
614614
Ok(Arc::new(value_builder.finish()))
615615
}
616616

617-
// Cast the decimal array to floating-point array
617+
/// Cast a decimal array to a floating point array.
618+
///
619+
/// Conversion is lossy and follows standard floating point semantics. Values
620+
/// that exceed the representable range become `INFINITY` or `-INFINITY` without
621+
/// returning an error.
618622
pub(crate) fn cast_decimal_to_float<D: DecimalType, T: ArrowPrimitiveType, F>(
619623
array: &dyn Array,
620624
op: F,

arrow-cast/src/cast/mod.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,8 @@ fn timestamp_to_date32<T: ArrowTimestampType>(
603603
/// * Temporal to/from backing Primitive: zero-copy with data type change
604604
/// * `Float32/Float64` to `Decimal(precision, scale)` rounds to the `scale` decimals
605605
/// (i.e. casting `6.4999` to `Decimal(10, 1)` becomes `6.5`).
606+
/// * `Decimal` to `Float32/Float64` is lossy and values outside the representable
607+
/// range become `INFINITY` or `-INFINITY` without error.
606608
///
607609
/// Unsupported Casts (check with `can_cast_types` before calling):
608610
/// * To or from `StructArray`
@@ -891,7 +893,7 @@ pub fn cast_with_options(
891893
scale,
892894
from_type,
893895
to_type,
894-
|x: i256| x.to_f64().unwrap(),
896+
|x: i256| decimal256_to_f64(x),
895897
cast_options,
896898
)
897899
}
@@ -1993,6 +1995,17 @@ where
19931995
}
19941996
}
19951997

1998+
/// Convert a [`i256`] to `f64` saturating to infinity on overflow.
1999+
fn decimal256_to_f64(v: i256) -> f64 {
2000+
v.to_f64().unwrap_or_else(|| {
2001+
if v.is_negative() {
2002+
f64::NEG_INFINITY
2003+
} else {
2004+
f64::INFINITY
2005+
}
2006+
})
2007+
}
2008+
19962009
fn cast_to_decimal<D, M>(
19972010
array: &dyn Array,
19982011
base: M,
@@ -8660,6 +8673,28 @@ mod tests {
86608673
"did not find expected error '{expected_error}' in actual error '{err}'"
86618674
);
86628675
}
8676+
#[test]
8677+
fn test_cast_decimal256_to_f64_overflow() {
8678+
// Test positive overflow (positive infinity)
8679+
let array = vec![Some(i256::MAX)];
8680+
let array = create_decimal256_array(array, 76, 2).unwrap();
8681+
let array = Arc::new(array) as ArrayRef;
8682+
8683+
let result = cast(&array, &DataType::Float64).unwrap();
8684+
let result = result.as_primitive::<Float64Type>();
8685+
assert!(result.value(0).is_infinite());
8686+
assert!(result.value(0) > 0.0); // Positive infinity
8687+
8688+
// Test negative overflow (negative infinity)
8689+
let array = vec![Some(i256::MIN)];
8690+
let array = create_decimal256_array(array, 76, 2).unwrap();
8691+
let array = Arc::new(array) as ArrayRef;
8692+
8693+
let result = cast(&array, &DataType::Float64).unwrap();
8694+
let result = result.as_primitive::<Float64Type>();
8695+
assert!(result.value(0).is_infinite());
8696+
assert!(result.value(0) < 0.0); // Negative infinity
8697+
}
86638698

86648699
#[test]
86658700
fn test_cast_decimal128_to_decimal128_negative_scale() {

0 commit comments

Comments
 (0)