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
- Reproduce with the failing interval value and check month normalization in apply_interval_to_civil for overflow or off-by-one carry handling.
- Add explicit modulo/checked arithmetic ((m - 1) % 12 + 1 style) when normalizing months before calling days_in_month.
- Validate month range at the boundary (1..=12) and return an overflow/query error for extreme intervals instead of panicking.
- 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
- Use checked/div_euclid arithmetic when adding month intervals to civil dates.
- Test apply_interval_to_civil with extreme INTERVAL values (i64 bounds).
- Fuzz interval application with large offsets to surface overflow early.
- Keep month normalization in one place so days_in_month always sees 1..=12.
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)