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-11611-87b8013f9f83e8f7681eefc01c452eb951754eff
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit f23b77b6b9daf157078d329079453be78ba9f626
Author: Manas Choksi <[email protected]>
AuthorDate: Tue Oct 27 01:56:13 2026 +0000

    fix(arrow-array): return overflow error instead of panicking when negating 
`i32::MIN` months (#11611)
    
    # Which issue does this PR close?
    
    - Closes #11600
    
    # Rationale for this change
    
    `Date32Type`/`Date64Type`::`subtract_year_months_opt` and
    `subtract_month_day_nano_opt` negate the interval months with a unary
    minus before handing them to `add_months_date`. When the months are
    `i32::MIN` the negation itself overflows, so in debug builds the `_opt`
    helpers panic instead of returning `None`:
    
    ```
    thread panicked at arrow-array/src/types.rs:1143:55:
    attempt to negate with overflow
    ```
    
    Every other out-of-range date arithmetic returns `None` from the `_opt`
    helpers, which `date_op` surfaces as `Date arithmetic overflow`. The
    timestamp kernels already handle `i32::MIN` months
    (`sub_months_datetime` subtracts through `unsigned_abs`), so only these
    four date helpers were affected.
    
    # What changes are included in this PR?
    
    The four negations now use `checked_neg()`, so the negation overflow
    returns `None` and the kernels report the same `Date arithmetic
    overflow` error as any other interval that moves the date out of range:
    
    - `Date32Type::subtract_year_months_opt`
    - `Date32Type::subtract_month_day_nano_opt`
    - `Date64Type::subtract_year_months_opt`
    - `Date64Type::subtract_month_day_nano_opt`
    
    `add_months_date` is untouched — it already handles months of any
    magnitude via `unsigned_abs`.
    
    # Are there any user-facing changes?
    
    No API changes. Subtracting an `i32::MIN`-months interval from a
    `Date32`/`Date64` array no longer panics in debug builds; it returns the
    existing `Date arithmetic overflow` error, matching the already-correct
    behavior of every other out-of-range interval.
    
    One note on release builds (overflow checks off): the wrapped
    `-i32::MIN` still lands in `add_months_date`'s `unsigned_abs` branch,
    which returns `None` for a ~179-million-year shift, so release builds
    were already reporting the overflow error. That part is reasoned from
    the code; everything below is from actual runs.
    
    # Testing
    
    - New kernel-level test `test_date_sub_interval_min_months` covers the
    repro from the issue for all four type/interval combinations and asserts
    the `Date arithmetic overflow` error. On unpatched `main` it panics with
    `attempt to negate with overflow` at `arrow-array/src/types.rs:1143:55`;
    with this patch it passes.
    - `test_year_month_ops!` and `test_month_day_nano_ops!` gain `i32::MIN`
    cases for both `Date32Type` and `Date64Type`: subtract must return
    `None`, and the corresponding `add` with `i32::MIN` months is pinned as
    the out-of-range control.
    - `cargo test -p arrow-arith`: 256 passed, 0 failed. `cargo test -p
    arrow-array`: 946 passed, 0 failed. `cargo fmt --all -- --check` and
    `cargo clippy -p arrow-arith -p arrow-array --all-targets` clean (rustc
    1.99.0, Linux).
    
    ---------
    
    Signed-off-by: choksi2212 <[email protected]>
    Co-authored-by: choksi2212 <[email protected]>
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-arith/src/numeric.rs | 25 +++++++++++++++++++++++++
 arrow-array/src/types.rs   | 45 +++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/arrow-arith/src/numeric.rs b/arrow-arith/src/numeric.rs
index acaa886d31..7168cefc47 100644
--- a/arrow-arith/src/numeric.rs
+++ b/arrow-arith/src/numeric.rs
@@ -2862,4 +2862,29 @@ mod tests {
         test_month_day_nano_ops!(Date32Type, date_to_days);
         test_month_day_nano_ops!(Date64Type, date_to_millis);
     }
+
+    #[test]
+    fn test_date_sub_interval_min_months() {
+        // Subtracting i32::MIN months requires negating i32::MIN, which
+        // overflows i32: the date kernels must return the Date arithmetic
+        // overflow error instead of panicking in debug builds
+        // https://github.com/apache/arrow-rs/issues/11600
+        let dates: [ArrayRef; 2] = [
+            Arc::new(Date32Array::from(vec![0])),
+            Arc::new(Date64Array::from(vec![0])),
+        ];
+        let year_month = IntervalYearMonthArray::from(vec![i32::MIN]);
+        let month_day_nano =
+            
IntervalMonthDayNanoArray::from(vec![IntervalMonthDayNano::new(i32::MIN, 0, 
0)]);
+        for dates in &dates {
+            assert_eq!(
+                sub(dates, &year_month).unwrap_err().to_string(),
+                "Compute error: Date arithmetic overflow: 0 - -2147483648 
months"
+            );
+            assert_eq!(
+                sub(dates, &month_day_nano).unwrap_err().to_string(),
+                "Compute error: Date arithmetic overflow: 0 - 
IntervalMonthDayNano { months: -2147483648, days: 0, nanoseconds: 0 }"
+            );
+        }
+    }
 }
diff --git a/arrow-array/src/types.rs b/arrow-array/src/types.rs
index c7f2d51989..2c031de2ea 100644
--- a/arrow-array/src/types.rs
+++ b/arrow-array/src/types.rs
@@ -1140,7 +1140,9 @@ impl Date32Type {
         delta: <IntervalYearMonthType as ArrowPrimitiveType>::Native,
     ) -> Option<<Date32Type as ArrowPrimitiveType>::Native> {
         let prior = Date32Type::to_naive_date_opt(date)?;
-        let months = IntervalYearMonthType::to_months(-delta);
+        // -i32::MIN overflows i32, but a 2^31 month shift is outside
+        // NaiveDate's range anyway: the subtraction would overflow regardless
+        let months = IntervalYearMonthType::to_months(delta.checked_neg()?);
         let posterior = add_months_date(prior, months)?;
         Some(Date32Type::from_naive_date(posterior))
     }
@@ -1226,7 +1228,9 @@ impl Date32Type {
     ) -> Option<<Date32Type as ArrowPrimitiveType>::Native> {
         let (months, days, nanos) = IntervalMonthDayNanoType::to_parts(delta);
         let res = Date32Type::to_naive_date_opt(date)?;
-        let res = add_months_date(res, -months)?;
+        // -i32::MIN overflows i32, but a 2^31 month shift is outside
+        // NaiveDate's range anyway: the subtraction would overflow regardless
+        let res = add_months_date(res, months.checked_neg()?)?;
         let res = res.checked_sub_signed(Duration::try_days(days as i64)?)?;
         let res = res.checked_sub_signed(Duration::nanoseconds(nanos))?;
         Some(Date32Type::from_naive_date(res))
@@ -1330,7 +1334,9 @@ impl Date64Type {
         delta: <IntervalYearMonthType as ArrowPrimitiveType>::Native,
     ) -> Option<<Date64Type as ArrowPrimitiveType>::Native> {
         let prior = Date64Type::to_naive_date_opt(date)?;
-        let months = IntervalYearMonthType::to_months(-delta);
+        // -i32::MIN overflows i32, but a 2^31 month shift is outside
+        // NaiveDate's range anyway: the subtraction would overflow regardless
+        let months = IntervalYearMonthType::to_months(delta.checked_neg()?);
         let posterior = add_months_date(prior, months)?;
         Some(Date64Type::from_naive_date(posterior))
     }
@@ -1368,7 +1374,9 @@ impl Date64Type {
     ) -> Option<<Date64Type as ArrowPrimitiveType>::Native> {
         let (months, days, nanos) = IntervalMonthDayNanoType::to_parts(delta);
         let res = Date64Type::to_naive_date_opt(date)?;
-        let res = add_months_date(res, -months)?;
+        // -i32::MIN overflows i32, but a 2^31 month shift is outside
+        // NaiveDate's range anyway: the subtraction would overflow regardless
+        let res = add_months_date(res, months.checked_neg()?)?;
         let res = res.checked_sub_signed(Duration::try_days(days as i64)?)?;
         let res = res.checked_sub_signed(Duration::nanoseconds(nanos))?;
         Some(Date64Type::from_naive_date(res))
@@ -1860,6 +1868,35 @@ mod tests {
         assert_eq!(IntervalYearMonthType::to_months(value), -14);
     }
 
+    #[test]
+    fn date_subtract_i32_min_months_overflows() {
+        // Negating i32::MIN months overflows i32: the *_opt helpers must
+        // return None instead of panicking in debug builds
+        // https://github.com/apache/arrow-rs/issues/11600
+        assert!(Date32Type::subtract_year_months_opt(0, i32::MIN).is_none());
+        assert!(Date64Type::subtract_year_months_opt(0, i32::MIN).is_none());
+        // adding i32::MIN months moves any date out of NaiveDate's range
+        assert!(Date32Type::add_year_months_opt(0, i32::MIN).is_none());
+        assert!(Date64Type::add_year_months_opt(0, i32::MIN).is_none());
+        assert!(
+            Date32Type::subtract_month_day_nano_opt(0, 
IntervalMonthDayNano::new(i32::MIN, 0, 0))
+                .is_none()
+        );
+        assert!(
+            Date64Type::subtract_month_day_nano_opt(0, 
IntervalMonthDayNano::new(i32::MIN, 0, 0))
+                .is_none()
+        );
+        // other parts must not mask the overflowing months
+        assert!(
+            Date32Type::subtract_month_day_nano_opt(0, 
IntervalMonthDayNano::new(i32::MIN, 1, 1))
+                .is_none()
+        );
+        assert!(
+            Date64Type::subtract_month_day_nano_opt(0, 
IntervalMonthDayNano::new(i32::MIN, 1, 1))
+                .is_none()
+        );
+    }
+
     fn test_layout<T: ArrowPrimitiveType>() {
         let layout = layout(&T::DATA_TYPE);
 

Reply via email to