databendlabs/databend · error

month arithmetic produced an invalid month

Error message

month arithmetic produced an invalid month

What it means

days_in_month (src/query/functions/src/scalars/timestamp/src/interval.rs:829), called by apply_interval_to_civil, returns the day count for months 1-12 with leap-year handling for February. A month value outside 1..=12 hits `unreachable!("month arithmetic produced an invalid month")`. The invariant is that month arithmetic (adding intervals to civil dates with normalization) always yields a month in 1..=12 before day-count lookup.

Solutions

  1. Reproduce with the failing interval value and check month normalization in apply_interval_to_civil for overflow or off-by-one carry handling.
  2. Add explicit modulo/checked arithmetic ((m - 1) % 12 + 1 style) when normalizing months before calling days_in_month.
  3. Validate month range at the boundary (1..=12) and return an overflow/query error for extreme intervals instead of panicking.
  4. Add a regression test with boundary interval values (max month/year offsets).

Example fix

// before
_ => unreachable!("month arithmetic produced an invalid month"),
// after
_ => return Err(ErrorCode::Overflow(format!(
    "month arithmetic produced an invalid month: {}", month))),
// plus in apply_interval_to_civil:
let total = year * 12 + (month as i64 - 1) + delta_months;
let (year, month) = (total.div_euclid(12), (total.rem_euclid(12) + 1) as u8);
Defensive patterns

Strategy: validation

Validate before calling

fn validate_interval_months(delta_months: i64) -> Result<(), String> {
    // bound interval magnitude so month normalization cannot overflow
    if delta_months.abs() > 12 * 100_000_000 {
        Err(format!("interval too large: {} months", delta_months))
    } else { Ok(()) }
}

Type guard

fn is_valid_month(month: u8) -> bool { (1..=12).contains(&month) }

Try / catch

// Normalize months defensively before day lookup:
let total = year * 12 + (month as i64) - 1 + delta;
let (year, month) = (total.div_euclid(12), (total.rem_euclid(12) + 1) as u8);
debug_assert!((1..=12).contains(&month));

Prevention

When it happens

Trigger: Applying an interval whose month component drives the civil-date month outside 1..=12 — e.g. a bug or integer overflow in the month-normalization step of apply_interval_to_civil (such as very large month offsets overflowing i64/u8 arithmetic), producing month 0, 13, or beyond.

Common situations: Extremely large INTERVAL values (e.g. INTERVAL '999999999999' MONTH) that overflow month arithmetic; a regression in the carry/borrow logic that normalizes month+year after interval application; fuzzer-generated huge interval inputs.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of databendlabs/databend@288d84d76e (2026-09-11). Data as JSON: /api/errors/e3e3fe2c2cef1337. Report an issue: GitHub.

Appendix: source

Thrown at src/query/functions/src/scalars/timestamp/src/interval.rs:829

        local,
        LocalTimeResolution::Compatible,
        preferred_offset,
    )
    .ok_or_else(|| "Invalid date: calendar arithmetic is out of range".to_string())?;
    resolved
        .unix_seconds
        .checked_mul(1_000_000)
        .and_then(|seconds| seconds.checked_add(i64::from(micro)))
        .ok_or_else(|| "Invalid date: calendar arithmetic is out of range".to_string())
}

fn days_in_month(year: i64, month: u8) -> u8 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
        2 => 28,
        _ => unreachable!("month arithmetic produced an invalid month"),
    }
}

pub(crate) fn civil_date_to_days(year: i64, month: u8, day: u8) -> i128 {
    let mut year = i128::from(year);
    let month = i128::from(month);
    let day = i128::from(day);
    year -= i128::from(month <= 2);
    let era = year.div_euclid(400);
    let year_of_era = year - era * 400;
    let month_prime = month + if month > 2 { -3 } else { 9 };
    let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
    era * 146_097 + day_of_era - 719_468
}

pub(crate) fn civil_date_from_days(days: i128) -> (i128, u8, u8) {
    let days = days + 719_468;

View on GitHub (pinned to 288d84d76e)