This is an automated email from the ASF dual-hosted git repository.

Jefffrey 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 a381897ff0 arith: fallback to dividing decimals digitwise when regular 
scale into div overflows (#10911)
a381897ff0 is described below

commit a381897ff0104bf126adb94ae81e413e40f207db
Author: Bharadwaj Pendyala <[email protected]>
AuthorDate: Wed Sep 2 01:02:24 2026 -0500

    arith: fallback to dividing decimals digitwise when regular scale into div 
overflows (#10911)
    
    # Which issue does this PR close?
    
    - Closes #7216.
    
    # Rationale for this change
    
    `Op::Div` scales the left operand before dividing: `result_scale = s1 +
    4`, then `mul_pow = result_scale - s1 + s2`. For large operand scales,
    `mul_pow` includes all of `s2`, so `l * 10^mul_pow` overflows even when
    the quotient fits.
    
    On `main` at 4962d38, the issue example is `Decimal256(38, 37) /
    Decimal256(38, 37)` with `l = 60096743305738933273387748827369321010`,
    `r = 60096763826458053191384497987259478584`, and `mul_pow = 41`. `l *
    10^41 = 6.0e78`, above `i256::MAX = 5.8e76`, while the quotient is
    `9.9999965853869970143724273117679321341339e40` at scale 41. Today this
    reports `Arithmetic overflow: Overflow happened on:
    60096743305738933273387748827369321010 *
    100000000000000000000000000000000000000000`, despite 35 digits of result
    headroom.
    
    # What changes are included in this PR?
    
    `scaled_div` computes `l * 10^mul_pow / r` digit by digit after
    `mul_checked` overflows; the fast path is unchanged. It carries the
    remainder forward without calculating `remainder * 10`, adding it ten
    times and subtracting the divisor when needed. This also handles
    divisors above `T::Native::MAX / 10`, where that multiply would
    overflow. The calculation uses magnitudes and restores the sign,
    preserving truncation toward zero for all four sign combinations.
    
    # Are these changes tested?
    
    Four tests in `arrow-arith/src/numeric.rs` cover the issue operands,
    each negated in turn, and a zero divisor; a `Decimal256(76, 37)` divisor
    of `6e75` above `i256::MAX / 10`; the same wide-intermediate case for
    `Decimal128`; and `scaled_div` against `i128` `l * 10^mul_pow / r`
    across both signs, a zero numerator, and operands near `i128::MAX / 10`.
    
    I also checked 1000 random `Decimal256(76, 70)` pairs against Python
    integer division: 661 now return exact values with no sign or truncation
    disagreements. The remaining 339 have quotients that do not fit `i256`;
    that harness is not in the diff. `cargo test -p arrow-arith` gives 239
    passed / 0 failed, `cargo test -p arrow` is clean, and `cargo fmt --all
    --check` and `cargo clippy -p arrow-arith --all-targets -- -D warnings`
    are quiet.
    
    # Are there any user-facing changes?
    
    No API change. A zero divisor whose numerator would have overflowed now
    reports `Divide by zero error` instead of the multiplication overflow,
    because the zero check no longer sits behind scaling. Unrepresentable
    divisions still error, but the message names the multiplication the
    digit loop failed on rather than `l * 10^mul_pow`.
    
    `T::Native::MIN` remains unsupported as either operand because taking
    its magnitude overflows. No array with a valid precision can hold it:
    `MIN` has more digits than `MAX_PRECISION` allows for every decimal
    type.
    
    ---------
    
    Co-authored-by: RIchard Baah <[email protected]>
---
 arrow-arith/src/numeric.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 166 insertions(+), 1 deletion(-)

diff --git a/arrow-arith/src/numeric.rs b/arrow-arith/src/numeric.rs
index be6ae21ff5..f3a87a74d3 100644
--- a/arrow-arith/src/numeric.rs
+++ b/arrow-arith/src/numeric.rs
@@ -967,6 +967,58 @@ fn date_op<T: DateOp>(
     }
 }
 
+/// Divides `l * 10^mul_pow` by `r` a digit at a time, without forming the 
scaled numerator.
+/// Used when scaling `l` would overflow `T::Native`, which it does well 
before the quotient
+/// does.
+///
+/// Runs on magnitudes and restores the sign last, so it truncates toward zero.
+fn scaled_div<T: DecimalType>(
+    l: T::Native,
+    r: T::Native,
+    mul_pow: i8,
+) -> Result<T::Native, ArrowError> {
+    let zero = T::Native::ZERO;
+    let negative = l.is_lt(zero) != r.is_lt(zero);
+    let dividend = abs_checked::<T>(l)?;
+    let divisor = abs_checked::<T>(r)?;
+
+    let mut quotient = dividend.div_checked(divisor)?;
+    let mut remainder = dividend.mod_checked(divisor)?;
+    for _ in 0..mul_pow {
+        // `remainder * 10` overflows for divisors near `T::Native::MAX`, so 
add the remainder
+        // ten times and take the divisor off whenever the running sum reaches 
it.
+        let mut carried = zero;
+        let mut digit = zero;
+        for _ in 0..10 {
+            let headroom = divisor.sub_wrapping(remainder);
+            if carried.is_lt(headroom) {
+                carried = carried.add_wrapping(remainder);
+            } else {
+                carried = carried.sub_wrapping(headroom);
+                digit = digit.add_wrapping(T::Native::ONE);
+            }
+        }
+        quotient = quotient
+            .mul_checked(T::Native::usize_as(10))?
+            .add_checked(digit)?;
+        remainder = carried;
+    }
+
+    if negative {
+        quotient.neg_checked()
+    } else {
+        Ok(quotient)
+    }
+}
+
+fn abs_checked<T: DecimalType>(value: T::Native) -> Result<T::Native, 
ArrowError> {
+    if value.is_lt(T::Native::ZERO) {
+        value.neg_checked()
+    } else {
+        Ok(value)
+    }
+}
+
 /// Perform arithmetic operation on decimal arrays
 fn decimal_op<T: DecimalType>(
     op: Op,
@@ -1076,7 +1128,10 @@ fn decimal_op<T: DecimalType>(
                 l_s,
                 r,
                 r_s,
-                l.mul_checked(l_mul)?.div_checked(r.mul_checked(r_mul)?)
+                match l.mul_checked(l_mul) {
+                    Ok(scaled) => scaled.div_checked(r.mul_checked(r_mul)?),
+                    Err(_) => scaled_div::<T>(l, r, mul_pow),
+                }
             )
             .with_precision_and_scale(result_precision, result_scale)?
         }
@@ -1479,6 +1534,116 @@ mod tests {
         assert_eq!(err, "Divide by zero error");
     }
 
+    #[test]
+    fn test_decimal256_div_wide_intermediate() {
+        // Dividing two scale-37 values needs l * 10^41, which is 79 digits 
and does not
+        // fit in an i256, even though the 41-digit quotient does.
+        let a = Decimal256Array::from(vec![i256::from_i128(
+            60096743305738933273387748827369321010i128,
+        )])
+        .with_precision_and_scale(38, 37)
+        .unwrap();
+        let b = Decimal256Array::from(vec![i256::from_i128(
+            60096763826458053191384497987259478584i128,
+        )])
+        .with_precision_and_scale(38, 37)
+        .unwrap();
+
+        let result = div(&a, &b).unwrap();
+        assert_eq!(result.data_type(), &DataType::Decimal256(76, 41));
+        assert_eq!(
+            result.as_primitive::<Decimal256Type>().value(0),
+            
i256::from_string("99999965853869970143724273117679321341339").unwrap()
+        );
+
+        // Truncation stays toward zero on either side of the fallback.
+        let neg_a = neg(&a).unwrap();
+        let result = div(neg_a.as_primitive::<Decimal256Type>(), &b).unwrap();
+        assert_eq!(
+            result.as_primitive::<Decimal256Type>().value(0),
+            
i256::from_string("-99999965853869970143724273117679321341339").unwrap()
+        );
+
+        let neg_b = neg(&b).unwrap();
+        let result = div(&a, neg_b.as_primitive::<Decimal256Type>()).unwrap();
+        assert_eq!(
+            result.as_primitive::<Decimal256Type>().value(0),
+            
i256::from_string("-99999965853869970143724273117679321341339").unwrap()
+        );
+
+        let zero = Decimal256Array::from(vec![i256::ZERO])
+            .with_precision_and_scale(38, 37)
+            .unwrap();
+        let err = div(&a, &zero).unwrap_err().to_string();
+        assert_eq!(err, "Divide by zero error");
+    }
+
+    #[test]
+    fn test_decimal256_div_divisor_near_max() {
+        // A divisor past i256::MAX / 10 leaves no room to scale the running 
remainder either.
+        let a = Decimal256Array::from(vec![
+            i256::from_string(
+                
"5900000000000000000000000000000000000000000000000000000000000000000000000000",
+            )
+            .unwrap(),
+        ])
+        .with_precision_and_scale(76, 37)
+        .unwrap();
+        let b = Decimal256Array::from(vec![
+            i256::from_string(
+                
"6000000000000000000000000000000000000000000000000000000000000000000000000000",
+            )
+            .unwrap(),
+        ])
+        .with_precision_and_scale(76, 37)
+        .unwrap();
+
+        let result = div(&a, &b).unwrap();
+        assert_eq!(
+            result.as_primitive::<Decimal256Type>().value(0),
+            
i256::from_string("98333333333333333333333333333333333333333").unwrap()
+        );
+    }
+
+    #[test]
+    fn test_decimal128_div_wide_intermediate() {
+        // Same overflow one type down: 3.0 / 6.0 at scale 37 needs l * 10^38, 
76 digits in i128.
+        let a = 
Decimal128Array::from(vec![30000000000000000000000000000000000000i128])
+            .with_precision_and_scale(38, 37)
+            .unwrap();
+        let b = 
Decimal128Array::from(vec![60000000000000000000000000000000000000i128])
+            .with_precision_and_scale(38, 37)
+            .unwrap();
+
+        let result = div(&a, &b).unwrap();
+        assert_eq!(result.data_type(), &DataType::Decimal128(38, 38));
+        assert_eq!(
+            result.as_primitive::<Decimal128Type>().value(0),
+            50000000000000000000000000000000000000i128
+        );
+    }
+
+    #[test]
+    fn test_scaled_div_agrees_with_direct_division() {
+        for (l, r, mul_pow) in [
+            (7i128, 3i128, 4i8),
+            (-7, 3, 4),
+            (7, -3, 4),
+            (-7, -3, 4),
+            (1, 999_999_999, 9),
+            (i128::MAX / 10, 7, 1),
+            (i128::MAX / 10, -i128::MAX / 11, 1),
+            (0, 5, 6),
+        ] {
+            let scaled = l * 10i128.pow(mul_pow as u32);
+            assert_eq!(
+                scaled_div::<Decimal128Type>(l, r, mul_pow).unwrap(),
+                scaled / r,
+                "{l} * 10^{mul_pow} / {r}"
+            );
+        }
+    }
+
     #[test]
     fn test_decimal256_same_scale_add_sub() {
         let lhs = Decimal256Array::from(vec![

Reply via email to