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-11528-3983bed2f9b6b1a85514d15d56c358622b29468c in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit c80ada4937f614b5282e038c07e2d7b0203a645a Author: Matt Butrovich <[email protected]> AuthorDate: Wed Oct 14 19:57:31 2026 +0000 feat(arrow-array): add `div_rem_wrapping` and `div_rem_checked` to `ArrowNativeTypeOp` (#11528) # Which issue does this PR close? - Closes #11454. - Part of #11421. # Rationale for this change See #11454. # What changes are included in this PR? `ArrowNativeTypeOp` gains `div_rem_wrapping` and `div_rem_checked`, which return `(quotient, remainder)`. Their default implementations call `div_*` and `mod_*`. `i256` overrides them with `i256::wrapping_div_rem` and `i256::checked_div_rem` from #11425, and `div_rem_checked` returns the same errors as `div_checked`. `native_type_op!` takes optional trailing items so the `i256` impl can carry the overrides. The names follow the `*_wrapping` and `*_checked` suffixes of the other `ArrowNativeTypeOp` methods. `checked_div_rem` would share a name with `i256::checked_div_rem` but return `Result` instead of `Option`. `make_downscaler` and `scaled_div` call the new methods. `ArrowNativeType` is sealed, so adding trait methods with defaults doesn't break downstream code. # Are these changes tested? `test_i256_div_rem` and `test_i256_div_rem_wrapping_zero_panics` in `arrow-array/src/arithmetic.rs` check the `i256` overrides against `div_*` and `mod_*`, including the errors. The existing tests cover both callers with `Decimal256` and negative values, such as `test_cast_decimal_to_decimal_round` and `test_decimal256_div_wide_intermediate`. The benchmark bot ran the `Decimal256` cases of `decimal_downscale` in the `cast_decimal` benchmark from #11426 on [x86_64](https://github.com/apache/arrow-rs/pull/11528#issuecomment-6131548323) and [arm64](https://github.com/apache/arrow-rs/pull/11528#issuecomment-6131541220), and `decimal256_scalar/div_72_digits_by_4_digits` in the `decimal_division` benchmark from #11427, which reaches `scaled_div`, on [x86_64](https://github.com/apache/arrow-rs/pull/11528#issuecomment-6131473177) and [arm64](https://github.com/apache/arrow-rs/pull/11528#issuecomment-6131472782). The runners are a Xeon Platinum 8581C and a Neoverse-V2. <img width="1600" height="1124" alt="div-rem-bot" src="https://github.com/user-attachments/assets/e2d02705-8357-47b0-b473-447b8910eee2" /> <details><summary>Numbers behind the chart</summary> In us with no nulls, per 8192 values for the casts and per 1024 values for the division: | Case | x86_64: `main` | This PR | arm64: `main` | This PR | |---|---|---|---|---| | `Decimal256` (76, 10) to (76, 2), 38 digits | 260.7 | 172.0 | 467.7 | 265.5 | | `Decimal256` (76, 10) to (76, 2), 60 digits | 261.1 | 167.4 | 751.5 | 422.4 | | `Decimal256` (41, 20) to `Decimal128` (38, 6), 30 digits | 278.8 | 174.6 | 499.9 | 291.3 | | `Decimal256` (76, 20) to `Decimal128` (38, 6), 48 digits, fallible | 280.7 | 211.1 | 780.7 | 443.1 | | `Decimal256` (76, 40) to `Decimal128` (38, 6), 42 digits, fallible | 582.3 | 340.9 | 702.5 | 390.4 | | `Decimal256` 72 digits by 4 digits | 405.8 | 380.7 | 550.3 | 508.5 | </details> The cases with nulls and the strict casts changed by about the same amount. Most of the division case's time goes to the per-digit loop in `scaled_div` and to the error that #11479 stops building. <details><summary>Apple M5 Max</summary> With rustc 1.99.0, in us per 8192 values with no nulls: | Case | `main` | This PR | |---|---|---| | `Decimal256` (76, 10) to (76, 2), 38 digits | 178.9 | 104.4 | | `Decimal256` (76, 10) to (76, 2), 60 digits | 272.1 | 154.0 | | `Decimal256` (41, 20) to `Decimal128` (38, 6), 30 digits | 263.0 | 153.4 | | `Decimal256` (76, 20) to `Decimal128` (38, 6), 48 digits, fallible | 460.4 | 294.9 | | `Decimal256` (76, 40) to `Decimal128` (38, 6), 42 digits, fallible | 380.5 | 227.7 | `Decimal128` keeps the default methods, and its cases changed by 3% or less, within the difference between repeated runs. The division case went from 295 to 284 us per 1024 values, the mean of two alternating runs each. </details> # Are there any significant user-facing changes? `ArrowNativeTypeOp` has two new methods with default implementations. ## AI Disclosure I wrote this PR with help from Claude but understand and support the core approach, implementation, and test coverage. --- arrow-arith/src/numeric.rs | 3 +- arrow-array/src/arithmetic.rs | 109 +++++++++++++++++++++++++++++++++++++++-- arrow-cast/src/cast/decimal.rs | 3 +- 3 files changed, 107 insertions(+), 8 deletions(-) diff --git a/arrow-arith/src/numeric.rs b/arrow-arith/src/numeric.rs index ea2a51a6e4..acaa886d31 100644 --- a/arrow-arith/src/numeric.rs +++ b/arrow-arith/src/numeric.rs @@ -982,8 +982,7 @@ fn scaled_div<T: DecimalType>( 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)?; + let (mut quotient, mut remainder) = dividend.div_rem_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. diff --git a/arrow-array/src/arithmetic.rs b/arrow-array/src/arithmetic.rs index ea43ed320e..eccb91cda8 100644 --- a/arrow-array/src/arithmetic.rs +++ b/arrow-array/src/arithmetic.rs @@ -28,8 +28,9 @@ use std::cmp::Ordering; /// types they will wrap around the boundary of the type. For floating point types they /// will overflow to INF or -INF preserving the expected sign value /// -/// Note `div_wrapping` and `mod_wrapping` will panic for integer types if `rhs` is zero -/// although this may be subject to change <https://github.com/apache/arrow-rs/issues/2647> +/// Note `div_wrapping`, `mod_wrapping` and `div_rem_wrapping` will panic for integer types if +/// `rhs` is zero, although this may be subject to change +/// <https://github.com/apache/arrow-rs/issues/2647> /// /// The APIs with `_checked` suffix perform overflow-checking. For integer types /// these will return `Err` instead of wrapping. For floating point types they will @@ -93,6 +94,43 @@ pub trait ArrowNativeTypeOp: ArrowNativeType { /// Panics if `rhs` is zero for integer types fn mod_wrapping(self, rhs: Self) -> Self; + /// Checked division and remainder operation, returning `(quotient, remainder)` + /// + /// Returns the same results as [`Self::div_checked`] and [`Self::mod_checked`]. `i256` + /// computes both with one division. + /// + /// # Example + /// ``` + /// # use arrow_array::ArrowNativeTypeOp; + /// assert_eq!((-7_i32).div_rem_checked(2).unwrap(), (-3, -1)); + /// assert!(7_i32.div_rem_checked(0).is_err()); + /// assert!(i32::MIN.div_rem_checked(-1).is_err()); + /// ``` + #[inline] + fn div_rem_checked(self, rhs: Self) -> Result<(Self, Self), ArrowError> { + Ok((self.div_checked(rhs)?, self.mod_checked(rhs)?)) + } + + /// Wrapping division and remainder operation, returning `(quotient, remainder)` + /// + /// Returns the same results as [`Self::div_wrapping`] and [`Self::mod_wrapping`]. `i256` + /// computes both with one division. + /// + /// # Panics + /// + /// Panics if `rhs` is zero for integer types + /// + /// # Example + /// ``` + /// # use arrow_array::ArrowNativeTypeOp; + /// assert_eq!((-7_i32).div_rem_wrapping(2), (-3, -1)); + /// assert_eq!(i32::MIN.div_rem_wrapping(-1), (i32::MIN, 0)); + /// ``` + #[inline] + fn div_rem_wrapping(self, rhs: Self) -> (Self, Self) { + (self.div_wrapping(rhs), self.mod_wrapping(rhs)) + } + /// Checked negation operation fn neg_checked(self) -> Result<Self, ArrowError>; @@ -152,8 +190,10 @@ macro_rules! native_type_op { ($t:tt, $zero:expr, $one: expr) => { native_type_op!($t, $zero, $one, $t::MIN, $t::MAX); }; - ($t:tt, $zero:expr, $one: expr, $min: expr, $max: expr) => { + ($t:tt, $zero:expr, $one: expr, $min: expr, $max: expr $(, $extra:item)*) => { impl ArrowNativeTypeOp for $t { + $($extra)* + const ZERO: Self = $zero; const ONE: Self = $one; const MIN_TOTAL_ORDER: Self = $min; @@ -296,7 +336,26 @@ native_type_op!(u8); native_type_op!(u16); native_type_op!(u32); native_type_op!(u64); -native_type_op!(i256, i256::ZERO, i256::ONE); +native_type_op!( + i256, + i256::ZERO, + i256::ONE, + i256::MIN, + i256::MAX, + #[inline] + fn div_rem_checked(self, rhs: Self) -> Result<(Self, Self), ArrowError> { + if rhs.is_zero() { + return Err(ArrowError::DivideByZero); + } + self.checked_div_rem(rhs).ok_or_else(|| { + ArrowError::ArithmeticOverflow(format!("Overflow happened on: {self:?} / {rhs:?}")) + }) + }, + #[inline] + fn div_rem_wrapping(self, rhs: Self) -> (Self, Self) { + self.wrapping_div_rem(rhs) + } +); native_type_op!(IntervalDayTime, IntervalDayTime::ZERO, IntervalDayTime::ONE); native_type_op!( @@ -777,6 +836,48 @@ mod tests { assert_eq!(9.0_f64.mod_checked(2.0_f64).unwrap(), 1_f64); } + #[test] + fn test_i256_div_rem() { + let values = [ + i256::MIN, + i256::MIN + i256::ONE, + i256::from_i128(-7), + i256::MINUS_ONE, + i256::ZERO, + i256::ONE, + i256::from_i128(2), + i256::from_i128(7), + i256::from_i128(i128::MAX), + i256::MAX, + ]; + for n in values { + for d in values { + let checked = n + .div_checked(d) + .and_then(|q| Ok((q, n.mod_checked(d)?))) + .map_err(|e| e.to_string()); + assert_eq!( + n.div_rem_checked(d).map_err(|e| e.to_string()), + checked, + "{n} / {d}" + ); + if !d.is_zero() { + assert_eq!( + n.div_rem_wrapping(d), + (n.div_wrapping(d), n.mod_wrapping(d)), + "{n} / {d}" + ); + } + } + } + } + + #[test] + #[should_panic(expected = "attempt to divide by zero")] + fn test_i256_div_rem_wrapping_zero_panics() { + let _ = i256::ONE.div_rem_wrapping(i256::ZERO); + } + #[test] #[cfg_attr(miri, ignore)] // Unsupported inline assembly fn test_native_type_neg() { diff --git a/arrow-cast/src/cast/decimal.rs b/arrow-cast/src/cast/decimal.rs index cfc8e3e624..2a11a142a6 100644 --- a/arrow-cast/src/cast/decimal.rs +++ b/arrow-cast/src/cast/decimal.rs @@ -237,8 +237,7 @@ where let f_fallible = move |x: I::Native| { // div is >= 10 and so this cannot overflow - let d = x.div_wrapping(div); - let r = x.mod_wrapping(div); + let (d, r) = x.div_rem_wrapping(div); // Round result let adjusted = match x >= I::Native::ZERO {
