This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11420-3c5bc5f4ce8f13f0974e5f01fdbed965d206172e in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
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
