diff --git a/datafusion/spark/src/function/datetime/mod.rs b/datafusion/spark/src/function/datetime/mod.rs index 70ab024c329aa..ed74d96a38a8d 100644 --- a/datafusion/spark/src/function/datetime/mod.rs +++ b/datafusion/spark/src/function/datetime/mod.rs @@ -126,11 +126,9 @@ pub mod expr_fn { "Returns the three-letter abbreviated month name from a date or timestamp.", arg1 )); - // TODO: add once ANSI support is added: - // "When both of the input parameters are not NULL and day_of_week is an invalid input, the function throws SparkIllegalArgumentException if spark.sql.ansi.enabled is set to true, otherwise NULL." export_functions!(( next_day, - "Returns the first date which is later than start_date and named as indicated. The function returns NULL if at least one of the input parameters is NULL.", + "Returns the first date which is later than start_date and named as indicated. The function returns NULL if at least one of the input parameters is NULL. When both input parameters are not NULL and day_of_week is an invalid input, the function raises an error if `datafusion.execution.enable_ansi_mode` is set to true, otherwise it returns NULL.", arg1 arg2 )); export_functions!(( diff --git a/datafusion/spark/src/function/datetime/next_day.rs b/datafusion/spark/src/function/datetime/next_day.rs index 2ef222526f387..18588250316b1 100644 --- a/datafusion/spark/src/function/datetime/next_day.rs +++ b/datafusion/spark/src/function/datetime/next_day.rs @@ -17,7 +17,7 @@ use std::sync::Arc; -use arrow::array::{ArrayRef, AsArray, Date32Array, StringArrayType}; +use arrow::array::{Array, ArrayRef, AsArray, Date32Array, StringArrayType}; use arrow::datatypes::{DataType, Date32Type, Field, FieldRef}; use chrono::{Datelike, Duration, Weekday}; use datafusion_common::{Result, ScalarValue, exec_err, internal_err}; @@ -69,7 +69,12 @@ impl ScalarUDFImpl for SparkNextDay { } fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { - let ScalarFunctionArgs { args, .. } = args; + let ScalarFunctionArgs { + args, + config_options, + .. + } = args; + let ansi_mode = config_options.execution.enable_ansi_mode; let [date, day_of_week] = args.as_slice() else { return exec_err!( "Spark `next_day` function requires 2 arguments, got {}", @@ -86,15 +91,21 @@ impl ScalarUDFImpl for SparkNextDay { | ScalarValue::LargeUtf8(day_of_week) | ScalarValue::Utf8View(day_of_week), ) => { - if let Some(days) = days { - if let Some(day_of_week) = day_of_week { - Ok(ColumnarValue::Scalar(ScalarValue::Date32( - spark_next_day(*days, day_of_week.as_str()), - ))) - } else { - // TODO: if spark.sql.ansi.enabled is false, - // returns NULL instead of an error for a malformed dayOfWeek. - Ok(ColumnarValue::Scalar(ScalarValue::Date32(None))) + // Spark's `NextDay` is null intolerant, so a NULL in either + // argument short circuits to NULL even under ANSI mode. + if let (Some(days), Some(day_of_week)) = (days, day_of_week) { + match parse_day_of_week(day_of_week.as_str()) { + Some(weekday) => { + Ok(ColumnarValue::Scalar(ScalarValue::Date32( + next_date_for_day_of_week(*days, weekday), + ))) + } + None if ansi_mode => { + illegal_day_of_week_err(day_of_week.as_str()) + } + None => { + Ok(ColumnarValue::Scalar(ScalarValue::Date32(None))) + } } } else { Ok(ColumnarValue::Scalar(ScalarValue::Date32(None))) @@ -113,18 +124,35 @@ impl ScalarUDFImpl for SparkNextDay { | ScalarValue::LargeUtf8(day_of_week) | ScalarValue::Utf8View(day_of_week), ) => { - if let Some(day_of_week) = day_of_week { - let result: Date32Array = date_array - .as_primitive::() - .unary_opt(|days| { - spark_next_day(days, day_of_week.as_str()) - }) - .with_data_type(DataType::Date32); - Ok(ColumnarValue::Array(Arc::new(result) as ArrayRef)) - } else { - // TODO: if spark.sql.ansi.enabled is false, - // returns NULL instead of an error for a malformed dayOfWeek. - Ok(ColumnarValue::Scalar(ScalarValue::Date32(None))) + match day_of_week + .as_ref() + .map(|d| (d.as_str(), parse_day_of_week(d.as_str()))) + { + Some((_, Some(weekday))) => { + let result: Date32Array = date_array + .as_primitive::() + .unary_opt(|days| { + next_date_for_day_of_week(days, weekday) + }) + .with_data_type(DataType::Date32); + Ok(ColumnarValue::Array(Arc::new(result) as ArrayRef)) + } + // The day name is unparsable. Spark's `NextDay` is null + // intolerant per row, so a NULL start date short circuits + // to NULL before the day name is validated. Raise only if + // at least one row has a non-NULL start date, which is + // exactly when the row-wise `process_next_day_arrays` path + // raises for the same inputs. + Some((raw, None)) + if ansi_mode + && date_array.null_count() < date_array.len() => + { + illegal_day_of_week_err(raw) + } + // Every remaining case yields NULL for every row: an + // unparsable day name with ANSI mode off or with no + // non-NULL start date, or a NULL `day_of_week`. + _ => Ok(ColumnarValue::Scalar(ScalarValue::Date32(None))), } } _ => exec_err!( @@ -132,117 +160,115 @@ impl ScalarUDFImpl for SparkNextDay { ), } } + (ColumnarValue::Scalar(date), ColumnarValue::Array(day_of_week_array)) => { + let date_array = date.to_array_of_size(day_of_week_array.len())?; + Ok(ColumnarValue::Array(next_day_arrays( + &date_array, + day_of_week_array, + ansi_mode, + )?)) + } ( ColumnarValue::Array(date_array), ColumnarValue::Array(day_of_week_array), - ) => { - let result = match (date_array.data_type(), day_of_week_array.data_type()) - { - ( - DataType::Date32, - DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View, - ) => { - let date_array: &Date32Array = - date_array.as_primitive::(); - match day_of_week_array.data_type() { - DataType::Utf8 => { - let day_of_week_array = - day_of_week_array.as_string::(); - process_next_day_arrays(date_array, day_of_week_array) - } - DataType::LargeUtf8 => { - let day_of_week_array = - day_of_week_array.as_string::(); - process_next_day_arrays(date_array, day_of_week_array) - } - DataType::Utf8View => { - let day_of_week_array = - day_of_week_array.as_string_view(); - process_next_day_arrays(date_array, day_of_week_array) - } - other => { - exec_err!( - "Spark `next_day` function: second arg must be string. Got {other:?}" - ) - } - } - } - (left, right) => { - exec_err!( - "Spark `next_day` function: first arg must be date, second arg must be string. Got {left:?}, {right:?}" - ) - } - }?; - Ok(ColumnarValue::Array(result)) - } - _ => exec_err!("Unsupported args {args:?} for Spark function `next_day`"), + ) => Ok(ColumnarValue::Array(next_day_arrays( + date_array, + day_of_week_array, + ansi_mode, + )?)), } } } +fn next_day_arrays( + date_array: &ArrayRef, + day_of_week_array: &ArrayRef, + ansi_mode: bool, +) -> Result { + match (date_array.data_type(), day_of_week_array.data_type()) { + (DataType::Date32, DataType::Utf8) => process_next_day_arrays( + date_array.as_primitive::(), + day_of_week_array.as_string::(), + ansi_mode, + ), + (DataType::Date32, DataType::LargeUtf8) => process_next_day_arrays( + date_array.as_primitive::(), + day_of_week_array.as_string::(), + ansi_mode, + ), + (DataType::Date32, DataType::Utf8View) => process_next_day_arrays( + date_array.as_primitive::(), + day_of_week_array.as_string_view(), + ansi_mode, + ), + (left, right) => exec_err!( + "Spark `next_day` function: first arg must be date, second arg must be string. Got {left:?}, {right:?}" + ), + } +} + fn process_next_day_arrays<'a, S>( date_array: &Date32Array, day_of_week_array: &'a S, + ansi_mode: bool, ) -> Result where &'a S: StringArrayType<'a>, { - let result = date_array - .iter() - .zip(day_of_week_array.iter()) - .map(|(days, day_of_week)| { - if let Some(days) = days { - if let Some(day_of_week) = day_of_week { - spark_next_day(days, day_of_week) - } else { - // TODO: if spark.sql.ansi.enabled is false, - // returns NULL instead of an error for a malformed dayOfWeek. - None - } - } else { - None + let mut builder = Date32Array::builder(date_array.len()); + for (days, day_of_week) in date_array.iter().zip(day_of_week_array.iter()) { + // Spark's `NextDay` is null intolerant, so a NULL in either argument + // short circuits to NULL even under ANSI mode. + let (Some(days), Some(day_of_week)) = (days, day_of_week) else { + builder.append_null(); + continue; + }; + match parse_day_of_week(day_of_week) { + Some(weekday) => { + builder.append_option(next_date_for_day_of_week(days, weekday)) } - }) - .collect::(); - Ok(Arc::new(result) as ArrayRef) + None if ansi_mode => return illegal_day_of_week_err(day_of_week), + None => builder.append_null(), + } + } + Ok(Arc::new(builder.finish()) as ArrayRef) } -fn spark_next_day(days: i32, day_of_week: &str) -> Option { - let date = Date32Type::to_naive_date_opt(days)?; +/// Mirrors Spark's `DateTimeUtils.getDayOfWeekFromString`: case insensitive, and +/// with no whitespace trimming. +fn parse_day_of_week(day_of_week: &str) -> Option { + match day_of_week.to_uppercase().as_str() { + "SU" | "SUN" | "SUNDAY" => Some(Weekday::Sun), + "MO" | "MON" | "MONDAY" => Some(Weekday::Mon), + "TU" | "TUE" | "TUESDAY" => Some(Weekday::Tue), + "WE" | "WED" | "WEDNESDAY" => Some(Weekday::Wed), + "TH" | "THU" | "THURSDAY" => Some(Weekday::Thu), + "FR" | "FRI" | "FRIDAY" => Some(Weekday::Fri), + "SA" | "SAT" | "SATURDAY" => Some(Weekday::Sat), + _ => None, + } +} - let day_of_week = day_of_week.to_uppercase(); - let day_of_week = match day_of_week.as_str() { - "MO" | "MON" | "MONDAY" => Some("MONDAY"), - "TU" | "TUE" | "TUESDAY" => Some("TUESDAY"), - "WE" | "WED" | "WEDNESDAY" => Some("WEDNESDAY"), - "TH" | "THU" | "THURSDAY" => Some("THURSDAY"), - "FR" | "FRI" | "FRIDAY" => Some("FRIDAY"), - "SA" | "SAT" | "SATURDAY" => Some("SATURDAY"), - "SU" | "SUN" | "SUNDAY" => Some("SUNDAY"), - _ => { - // TODO: if spark.sql.ansi.enabled is false, - // returns NULL instead of an error for a malformed dayOfWeek. - None - } - }; +/// The first date strictly after `days` that falls on `weekday`. Equivalent to +/// Spark's `DateTimeUtils.getNextDateForDayOfWeek`, so a start date already on +/// `weekday` advances a full week. +/// +/// Returns `None` when `days` is not a representable date, and also when the +/// result would fall past `chrono::NaiveDate::MAX`. Spark computes the answer +/// with plain `Int` arithmetic and has no calendar range limit, so it keeps +/// returning values in that far-future range where this returns NULL. +fn next_date_for_day_of_week(days: i32, weekday: Weekday) -> Option { + let date = Date32Type::to_naive_date_opt(days)?; + let delta = (7 - date.weekday().days_since(weekday)) as i64; + Some(Date32Type::from_naive_date( + date.checked_add_signed(Duration::days(delta))?, + )) +} - 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, - ), - )), - Err(_) => { - // TODO: if spark.sql.ansi.enabled is false, - // returns NULL instead of an error for a malformed dayOfWeek. - None - } - } - } else { - None - } +/// Matches Spark's `ILLEGAL_DAY_OF_WEEK` error, raised when ANSI mode is enabled +/// and `day_of_week` does not name a day. +fn illegal_day_of_week_err(day_of_week: &str) -> Result { + exec_err!("Illegal input for day of week: {day_of_week}.") } #[cfg(test)] @@ -282,7 +308,37 @@ mod tests { #[test] fn next_day_rejects_whitespace_padded_day_names() { + assert_eq!(parse_day_of_week(" MO "), None); + assert_eq!(parse_day_of_week("MO "), None); + assert_eq!(parse_day_of_week(""), None); + assert_eq!(parse_day_of_week("mo"), Some(Weekday::Mon)); + } + + #[test] + fn next_day_advances_a_full_week_on_the_same_weekday() { let monday = 19723; // 2024-01-01 - assert_eq!(spark_next_day(monday, " MO "), None); + assert_eq!( + next_date_for_day_of_week(monday, Weekday::Mon), + Some(monday + 7) + ); + assert_eq!( + next_date_for_day_of_week(monday, Weekday::Tue), + Some(monday + 1) + ); + } + + #[test] + fn next_day_returns_null_past_the_last_representable_date() { + // `chrono::NaiveDate::MAX` is +262142-12-31, which is a Monday. + let max = 95026236; + assert_eq!(next_date_for_day_of_week(max, Weekday::Mon), None); + assert_eq!(next_date_for_day_of_week(max - 1, Weekday::Mon), Some(max)); + // A start date already on the requested weekday advances a full week, + // so it overflows from six days earlier. + assert_eq!(next_date_for_day_of_week(max - 6, Weekday::Tue), None); + assert_eq!(next_date_for_day_of_week(max - 6, Weekday::Mon), Some(max)); + // Values that are not representable dates at all still return NULL. + assert_eq!(next_date_for_day_of_week(i32::MAX, Weekday::Mon), None); + assert_eq!(next_date_for_day_of_week(i32::MIN, Weekday::Mon), None); } } diff --git a/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt b/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt index b0ffd7d0e412f..53203124f597f 100644 --- a/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt +++ b/datafusion/sqllogictest/test_files/spark/datetime/next_day.slt @@ -21,6 +21,14 @@ # For more information, please see: # https://github.com/apache/datafusion/issues/15914 +# Audited against Spark 4.2.0 on 2026-07-25. +# Expected values verified with pyspark==4.2.0. +# Known divergences are captured as commented-out queries below. + +############################################################################## +# Scalar inputs, non-ANSI mode (the DataFusion default) +############################################################################## + query D SELECT next_day('2015-01-14'::DATE, 'TU'::string); ---- @@ -36,42 +44,146 @@ SELECT next_day('2015-07-27'::DATE, 'Sat'::string); ---- 2015-08-01 +# All three name lengths are accepted, case insensitively. +query D +SELECT next_day('2015-07-23'::DATE, 'MONDAY'::string); +---- +2015-07-27 + +query D +SELECT next_day('2015-07-23'::DATE, 'monday'::string); +---- +2015-07-27 + +query D +SELECT next_day('2015-07-23'::DATE, 'mo'::string); +---- +2015-07-27 + +# A start date already on the requested weekday advances a full week. +# 2015-07-23 is a Thursday. +query D +SELECT next_day('2015-07-23'::DATE, 'Thu'::string); +---- +2015-07-30 + # Whitespace-padded day names should be rejected (return NULL) per Spark behavior query D SELECT next_day('2015-01-14'::DATE, ' MO '::string); ---- NULL -query error Failed to coerce arguments to satisfy a call to 'next_day' function -SELECT next_day('2015-07-27'::DATE); +query D +SELECT next_day('2020-01-01'::DATE, 'invalid_day'::string); +---- +NULL -query error Failed to coerce arguments to satisfy a call to 'next_day' function -SELECT next_day('Sun'::string); +query D +SELECT next_day('2015-01-14'::DATE, ''::string); +---- +NULL -query error 'next_day' does not support zero arguments -SELECT next_day(); +query D +SELECT next_day('2015-07-23'::DATE, 'MÖ'::string); +---- +NULL -query error Failed to coerce arguments to satisfy a call to 'next_day' function -SELECT next_day(1::int, 'Sun'::string); +############################################################################## +# NULL propagation +############################################################################## -query error Failed to coerce arguments to satisfy a call to 'next_day' function -SELECT next_day('2015-07-27'::DATE, 'Sat'::string, 'Sun'::string); +query D +SELECT next_day(NULL::DATE, 'Mon'::string); +---- +NULL -query error Failed to coerce arguments to satisfy a call to 'next_day' function -SELECT next_day('invalid_date'::string, 'Mon'::string); +query D +SELECT next_day('2015-01-14'::DATE, NULL::string); +---- +NULL +# Spark's `NextDay` is null intolerant, so a NULL start date short circuits to +# NULL before the day-of-week name is ever validated. This holds in both modes. query D -SELECT next_day('2000-01-01'::DATE, 2.0::float); +SELECT next_day(NULL::DATE, 'xx'::string); ---- NULL +############################################################################## +# Boundary dates +############################################################################## + query D -SELECT next_day('2020-01-01'::DATE, 'invalid_day'::string); +SELECT next_day('0001-01-01'::DATE, 'Mon'::string); +---- +0001-01-08 + +query D +SELECT next_day('1000-01-01'::DATE, 'Mon'::string); +---- +1000-01-06 + +query D +SELECT next_day('1969-12-31'::DATE, 'Mon'::string); +---- +1970-01-05 + +# Crossing out of the four-digit year range does not overflow or error. +query D +SELECT next_day('9999-12-31'::DATE, 'Mon'::string); +---- ++10000-01-03 + +# The last date DataFusion can represent is +262142-12-31, epoch day 95026236, +# which is a Monday. Results that would land past it return NULL rather than +# overflowing. Spark computes next_day with plain Int arithmetic and has no +# calendar range limit, so it keeps returning a value where the queries below +# return NULL. That residual divergence is a known limitation of the Date32 +# representation, not of the day-of-week logic. +query D +SELECT next_day(arrow_cast(95026235, 'Date32'), 'Mon'::string); +---- ++262142-12-31 + +query D +SELECT next_day(arrow_cast(95026236, 'Date32'), 'Mon'::string); ---- NULL -query error Cast error: Cannot cast string '2015-13-32' to value of Date32 type -SELECT next_day('2015-13-32'::DATE, 'Sun'::string); +# A start date already on the requested weekday advances a full week, so the +# overflow begins six days earlier for that weekday. +query D +SELECT next_day(arrow_cast(95026230, 'Date32'), 'Mon'::string); +---- ++262142-12-31 + +query D +SELECT next_day(arrow_cast(95026230, 'Date32'), 'Tue'::string); +---- +NULL + +# The array path returns NULL per overflowing row rather than failing the batch. +query D +SELECT next_day(a, 'Mon'::string) +FROM VALUES (arrow_cast(95026235, 'Date32')), (arrow_cast(95026236, 'Date32')) as t(a); +---- ++262142-12-31 +NULL + +# Date32 values that are not representable dates at all also return NULL. +query D +SELECT next_day(arrow_cast(2147483647, 'Date32'), 'Mon'::string); +---- +NULL + +query D +SELECT next_day(arrow_cast(-2147483648, 'Date32'), 'Mon'::string); +---- +NULL + +############################################################################## +# Array and mixed scalar/array inputs +############################################################################## query D SELECT next_day(a, b) @@ -85,3 +197,205 @@ FROM VALUES NULL NULL NULL + +# Array start date with a scalar day of week. +query D +SELECT next_day(a, 'Mon'::string) +FROM VALUES ('2000-01-01'::DATE), ('2000-01-02'::DATE), ('2000-01-03'::DATE) as t(a); +---- +2000-01-03 +2000-01-03 +2000-01-10 + +# Array start date with a NULL scalar day of week returns one NULL per row. +query D +SELECT next_day(a, NULL::string) +FROM VALUES ('2000-01-01'::DATE), ('2000-01-02'::DATE), ('2000-01-03'::DATE) as t(a); +---- +NULL +NULL +NULL + +# Array start date with an unparsable scalar day of week. +query D +SELECT next_day(a, 'xx'::string) +FROM VALUES ('2000-01-01'::DATE), ('2000-01-02'::DATE), ('2000-01-03'::DATE) as t(a); +---- +NULL +NULL +NULL + +# Scalar start date with an array day of week. +query D +SELECT next_day('2018-01-01'::DATE, b) +FROM VALUES ('Mon'::string), (NULL::string), ('xx'::string) as t(b); +---- +2018-01-08 +NULL +NULL + +############################################################################## +# ANSI mode +# +# Spark 3.5.8 defaults `spark.sql.ansi.enabled` to false, so an unparsable +# day of week returns NULL there. Spark 4.0 and later default it to true and +# raise ILLEGAL_DAY_OF_WEEK instead. Spark 3.5.8 also used the error class +# _LEGACY_ERROR_TEMP_2000 rather than ILLEGAL_DAY_OF_WEEK. The expectations +# below assert Spark 4.2.0. Version-specific expectations are tracked by +# https://github.com/apache/datafusion/issues/23887 and this specific +# divergence is recorded in +# https://github.com/apache/datafusion/issues/23890 +############################################################################## + +statement ok +set datafusion.execution.enable_ansi_mode = true; + +query error Illegal input for day of week: NOT_A_DAY\. +SELECT next_day('2015-01-14'::DATE, 'NOT_A_DAY'::string); + +# Spark 4.2.0's ILLEGAL_DAY_OF_WEEK message ends with a period, which pins down +# where the echoed day name stops and makes the padded and empty names +# assertable. The runner collapses runs of whitespace in the expected-error +# regex, so the two spaces in " MO " cannot be written literally, but `\s+` +# expresses them and carries no literal whitespace of its own. +query error Illegal input for day of week:\s+MO \. +SELECT next_day('2015-01-14'::DATE, ' MO '::string); + +query error Illegal input for day of week: \. +SELECT next_day('2015-01-14'::DATE, ''::string); + +query error Illegal input for day of week: xx\. +SELECT next_day(a, 'xx'::string) +FROM VALUES ('2000-01-01'::DATE), ('2000-01-02'::DATE) as t(a); + +query error Illegal input for day of week: xx\. +SELECT next_day(a, b) +FROM VALUES ('2018-01-01'::DATE, 'Mon'::string), ('2018-01-02'::DATE, 'xx'::string) as t(a, b); + +query error Illegal input for day of week: xx\. +SELECT next_day('2018-01-01'::DATE, b) +FROM VALUES ('Mon'::string), (NULL::string), ('xx'::string) as t(b); + +# NULL inputs still short circuit to NULL under ANSI mode. Spark's `NextDay` is +# null intolerant, so a NULL start date suppresses the day-of-week validation +# that would otherwise raise. This holds per row, in every argument shape: an +# all-NULL start date column yields NULLs rather than an error, while a single +# non-NULL row in the same column is enough to raise. +query D +SELECT next_day(NULL::DATE, 'xx'::string); +---- +NULL + +query D +SELECT next_day('2015-01-14'::DATE, NULL::string); +---- +NULL + +query D +SELECT next_day(a, 'xx'::string) +FROM VALUES (NULL::DATE), (NULL::DATE) as t(a); +---- +NULL +NULL + +query error Illegal input for day of week: xx\. +SELECT next_day(a, 'xx'::string) +FROM VALUES (NULL::DATE), ('2000-01-01'::DATE) as t(a); + +query D +SELECT next_day(a, b) +FROM VALUES (NULL::DATE, 'xx'::string), (NULL::DATE, 'xx'::string) as t(a, b); +---- +NULL +NULL + +query D +SELECT next_day(NULL::DATE, b) +FROM VALUES ('Mon'::string), ('xx'::string) as t(b); +---- +NULL +NULL + +query D +SELECT next_day(a, NULL::string) +FROM VALUES (NULL::DATE), (NULL::DATE) as t(a); +---- +NULL +NULL + +# Deliberately duplicates the first query in this file. ANSI mode must not turn +# a valid day name into an error, so the happy path is asserted in both modes. +query D +SELECT next_day('2015-01-14'::DATE, 'TU'::string); +---- +2015-01-20 + +statement ok +set datafusion.execution.enable_ansi_mode = false; + +############################################################################## +# Argument type and arity errors +############################################################################## + +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day('2015-07-27'::DATE); + +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day('Sun'::string); + +query error 'next_day' does not support zero arguments +SELECT next_day(); + +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day(1::int, 'Sun'::string); + +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day('2015-07-27'::DATE, 'Sat'::string, 'Sun'::string); + +query D +SELECT next_day('2000-01-01'::DATE, 2.0::float); +---- +NULL + +query error Cast error: Cannot cast string '2015-13-32' to value of Date32 type +SELECT next_day('2015-13-32'::DATE, 'Sun'::string); + +# DataFusion's signature is Exact(Date32, Utf8), so a STRING or TIMESTAMP start +# date is rejected during coercion. Spark's `NextDay` uses ImplicitCastInputTypes +# and accepts both, casting to DATE first. The three queries below assert +# DataFusion's current behavior, and the commented-out queries below them carry +# the Spark 4.2.0 result. +# https://github.com/apache/datafusion/issues/23889 +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day('invalid_date'::string, 'Mon'::string); + +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day('2015-07-23'::string, 'Mon'::string); + +query error Failed to coerce arguments to satisfy a call to 'next_day' function +SELECT next_day('2015-07-23 12:12:12'::timestamp, 'Mon'::string); + +# Spark 4.2.0 returns 2015-07-27 for the first two of these. The third is a +# STRING that is not a date, which Spark casts to NULL with ANSI mode off and +# rejects with CAST_INVALID_INPUT with ANSI mode on, so it needs one +# expectation per mode. DataFusion raises a coercion error for all three. +# Uncomment once the signature accepts Spark's implicit casts. +# https://github.com/apache/datafusion/issues/23889 +# query D +# SELECT next_day('2015-07-23'::string, 'Mon'::string); +# ---- +# 2015-07-27 +# +# query D +# SELECT next_day('2015-07-23 12:12:12'::timestamp, 'Mon'::string); +# ---- +# 2015-07-27 +# +# query D +# SELECT next_day('invalid_date'::string, 'Mon'::string); +# ---- +# NULL +# +# The same query under `set datafusion.execution.enable_ansi_mode = true`: +# query error CAST_INVALID_INPUT +# SELECT next_day('invalid_date'::string, 'Mon'::string);