diff --git a/datafusion/spark/src/function/datetime/next_day.rs b/datafusion/spark/src/function/datetime/next_day.rs index 2ef222526f387..09d7de7b4a4de 100644 --- a/datafusion/spark/src/function/datetime/next_day.rs +++ b/datafusion/spark/src/function/datetime/next_day.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use arrow::array::{ArrayRef, AsArray, Date32Array, StringArrayType}; use arrow::datatypes::{DataType, Date32Type, Field, FieldRef}; -use chrono::{Datelike, Duration, Weekday}; +use chrono::{Datelike, Weekday}; use datafusion_common::{Result, ScalarValue, exec_err, internal_err}; use datafusion_expr::{ ColumnarValue, ReturnFieldArgs, ScalarFunctionArgs, ScalarUDFImpl, Signature, @@ -229,11 +229,17 @@ fn spark_next_day(days: i32, day_of_week: &str) -> Option { if let Some(day_of_week) = day_of_week { let day_of_week = day_of_week.parse::(); match day_of_week { - Ok(day_of_week) => Some(Date32Type::from_naive_date( - date + Duration::days( - (7 - date.weekday().days_since(day_of_week)) as i64, - ), - )), + Ok(day_of_week) => { + // Advance 1..=7 days from `days` to the next occurrence of + // `day_of_week`. Compute the result on the epoch day directly + // instead of constructing a `NaiveDate`: the result can land + // past `NaiveDate::MAX` (epoch day 95026236), and building that + // date panics (`NaiveDate + TimeDelta overflowed`). Spark's + // `DateTimeUtils.getNextDateForDayOfWeek` is pure `Int` + // arithmetic and keeps producing a value up to `Int.MaxValue`. + let delta = 7 - date.weekday().days_since(day_of_week) as i32; + days.checked_add(delta) + } Err(_) => { // TODO: if spark.sql.ansi.enabled is false, // returns NULL instead of an error for a malformed dayOfWeek. @@ -285,4 +291,17 @@ mod tests { let monday = 19723; // 2024-01-01 assert_eq!(spark_next_day(monday, " MO "), None); } + + #[test] + fn next_day_handles_far_future_start_dates() { + // Regression for #23891: for start dates near the end of the + // representable `Date32` range, the next occurrence can land past + // `chrono::NaiveDate::MAX` (epoch day 95026236). Computing the result + // on the epoch day directly (as Spark does) must return a value rather + // than panicking with `NaiveDate + TimeDelta overflowed`. + // + // 95026236 is a Monday, so `next_day(.., "Mon")` advances a full week. + assert_eq!(spark_next_day(95026236, "Mon"), Some(95026243)); + assert_eq!(spark_next_day(95026230, "Tue"), Some(95026237)); + } } diff --git a/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt b/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt index b0ffd7d0e412f..74fc12e21e6d4 100644 --- a/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt +++ b/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt @@ -85,3 +85,18 @@ FROM VALUES NULL NULL NULL + +# https://github.com/apache/datafusion/issues/23891 +# Far-future start dates whose next occurrence lands past chrono::NaiveDate::MAX +# (epoch day 95026236) must still return a value, matching Spark's integer +# arithmetic, rather than panicking. Cast the Date32 result to Int32 to assert +# the epoch day directly (these dates are past the printable range). +query I +SELECT arrow_cast(next_day(arrow_cast(95026236, 'Date32'), 'Mon'::string), 'Int32'); +---- +95026243 + +query I +SELECT arrow_cast(next_day(arrow_cast(95026230, 'Date32'), 'Tue'::string), 'Int32'); +---- +95026237