This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] 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 9d81159089 fix(arrow-arith): keep decimal division precision valid
(#11420)
9d81159089 is described below
commit 9d811590898b00993654275febd58e1e3b09776a
Author: Geonho <[email protected]>
AuthorDate: Wed Oct 7 08:54:42 2026 +0000
fix(arrow-arith): keep decimal division precision valid (#11420)
# Which issue does this PR close?
Follow-up to #10996.
# Rationale for this change
Decimal division can reject representable results when the divisor has a
negative scale. For example, `0.1 / 10000` with input types
`Decimal32(1, 1)` and `Decimal32(1, -4)` should produce `0.00001` at
scale 5. Instead, the inferred precision is 1, and the result fails with
`scale 5 is greater than precision 1`.
# What changes are included in this PR?
Calculate the inferred precision in a wider signed integer, then bound
it by the minimum valid precision and the decimal type's maximum. The
lower bound is 1 or the output scale, whichever is greater. Scale
selection and coefficient arithmetic stay unchanged.
# Are these changes tested?
Two regressions cover all four decimal widths, separating the
positive-scale constraint from the minimum precision of 1. They compare
both coefficients and metadata, including signed values, zero, nulls and
adjacent cases that already work.
# Are there any significant user-facing changes?
When the inferred precision was negative, the unsigned conversion could
give a zero quotient the type's maximum precision. These results now use
the minimum valid precision; their coefficients and scale are unchanged.
AI assistance was used for the implementation, regression tests and this
description.
---
arrow-arith/src/numeric.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 70 insertions(+), 1 deletion(-)
diff --git a/arrow-arith/src/numeric.rs b/arrow-arith/src/numeric.rs
index 170d013d6d..d1d0c6435c 100644
--- a/arrow-arith/src/numeric.rs
+++ b/arrow-arith/src/numeric.rs
@@ -1116,7 +1116,10 @@ fn decimal_op<T: DecimalType>(
let mul_pow = result_scale - s1 + s2;
// p1 - s1 + s2 + result_scale
- let result_precision = (mul_pow.saturating_add(*p1 as i8) as
u8).min(T::MAX_PRECISION);
+ // Precision must be positive and at least the output scale.
+ let result_precision = (mul_pow as i16 + *p1 as i16)
+ .clamp(result_scale.max(1) as i16, T::MAX_PRECISION as i16)
+ as u8;
let (l_mul, r_mul) = match mul_pow.cmp(&0) {
Ordering::Greater => (
@@ -1650,6 +1653,72 @@ mod tests {
);
}
+ #[test]
+ fn test_decimal_div_precision_at_least_scale() {
+ fn check<T: DecimalType>()
+ where
+ T::Native: From<i32>,
+ {
+ let a: PrimitiveArray<T> = [Some(1), Some(-1), Some(0), None]
+ .into_iter()
+ .map(|v| v.map(T::Native::from))
+ .collect();
+ let a = a.with_precision_and_scale(1, 1).unwrap();
+ for (scale, coefficient) in [(0, 10000), (-1, 1000), (-4, 1), (-5,
0), (-6, 0)] {
+ let b = PrimitiveArray::<T>::from_iter_values(
+ [1, 1, 1, 0].into_iter().map(T::Native::from),
+ )
+ .with_precision_and_scale(1, scale)
+ .unwrap();
+ let expected: PrimitiveArray<T> =
+ [Some(coefficient), Some(-coefficient), Some(0), None]
+ .into_iter()
+ .map(|v| v.map(T::Native::from))
+ .collect();
+ let expected = expected.with_precision_and_scale(5,
5).unwrap();
+ assert_eq!(div(&a, &b).unwrap().as_ref(), &expected);
+ }
+ }
+ check::<Decimal32Type>();
+ check::<Decimal64Type>();
+ check::<Decimal128Type>();
+ check::<Decimal256Type>();
+ }
+
+ #[test]
+ fn test_decimal_div_precision_at_least_one() {
+ fn check<T: DecimalType>()
+ where
+ T::Native: From<i32>,
+ {
+ for scale in [-4, -5] {
+ let a: PrimitiveArray<T> = [Some(1), Some(-1), Some(0), None]
+ .into_iter()
+ .map(|v| v.map(T::Native::from))
+ .collect();
+ let a = a.with_precision_and_scale(1, scale).unwrap();
+ for (divisor_scale, coefficient) in [(-4, 1), (-5, 0), (-6,
0)] {
+ let b = PrimitiveArray::<T>::from_iter_values(
+ [1, 1, 1, 0].into_iter().map(T::Native::from),
+ )
+ .with_precision_and_scale(1, divisor_scale)
+ .unwrap();
+ let expected: PrimitiveArray<T> =
+ [Some(coefficient), Some(-coefficient), Some(0), None]
+ .into_iter()
+ .map(|v| v.map(T::Native::from))
+ .collect();
+ let expected = expected.with_precision_and_scale(1, scale
+ 4).unwrap();
+ assert_eq!(div(&a, &b).unwrap().as_ref(), &expected);
+ }
+ }
+ }
+ check::<Decimal32Type>();
+ check::<Decimal64Type>();
+ check::<Decimal128Type>();
+ check::<Decimal256Type>();
+ }
+
#[test]
fn test_decimal256_div_wide_intermediate() {
// Dividing two scale-37 values needs l * 10^41, which is 79 digits
and does not