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);
