blob: 257030c75edc635fdd591fc1b127a0c23f9f19b1 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use arrow::array::{Array, Date32Array, Int32Array};
use arrow::compute::cast;
use arrow::datatypes::DataType;
use datafusion::common::{utils::take_function_args, DataFusionError, Result};
use datafusion::logical_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use std::sync::Arc;
use crate::{conversion_funcs::ymd_to_epoch_day, SparkError};
/// Spark-compatible make_date function.
/// Creates a date from year, month, and day columns.
/// For an invalid `(year, month, day)` triple Spark returns NULL when `spark.sql.ansi.enabled` is
/// false, and throws otherwise. The ANSI flag is carried here as `fail_on_error`.
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct SparkMakeDate {
signature: Signature,
fail_on_error: bool,
}
impl SparkMakeDate {
pub fn new(fail_on_error: bool) -> Self {
Self {
// Accept any numeric type - we'll cast to Int32 internally
signature: Signature::any(3, Volatility::Immutable),
fail_on_error,
}
}
}
impl Default for SparkMakeDate {
fn default() -> Self {
Self::new(false)
}
}
/// Build the error message Spark surfaces for an invalid date under ANSI mode. Spark wraps the
/// `java.time.DateTimeException` raised by `LocalDate.of` (via `ansiDateTimeArgumentOutOfRange` /
/// `ansiDateTimeError`), so we reproduce `java.time`'s messages and validation order: year range,
/// month range, day range, then the day-vs-month check.
fn invalid_date_message(year: i32, month: i32, day: i32) -> String {
const MONTH_NAMES: [&str; 12] = [
"JANUARY",
"FEBRUARY",
"MARCH",
"APRIL",
"MAY",
"JUNE",
"JULY",
"AUGUST",
"SEPTEMBER",
"OCTOBER",
"NOVEMBER",
"DECEMBER",
];
if !(-999_999_999..=999_999_999).contains(&year) {
return format!("Invalid value for Year (valid values -999999999 - 999999999): {year}");
}
if !(1..=12).contains(&month) {
return format!("Invalid value for MonthOfYear (valid values 1 - 12): {month}");
}
if !(1..=31).contains(&day) {
return format!("Invalid value for DayOfMonth (valid values 1 - 28/31): {day}");
}
if day == 29 && month == 2 {
return format!("Invalid date 'February 29' as '{year}' is not a leap year");
}
format!("Invalid date '{} {day}'", MONTH_NAMES[(month - 1) as usize])
}
/// Cast an array to Int32Array if it's not already Int32.
fn cast_to_int32(arr: &Arc<dyn Array>) -> Result<Arc<dyn Array>> {
if arr.data_type() == &DataType::Int32 {
Ok(Arc::clone(arr))
} else {
cast(arr.as_ref(), &DataType::Int32)
.map_err(|e| DataFusionError::Execution(format!("Failed to cast to Int32: {e}")))
}
}
/// Convert year, month, day to days since Unix epoch (1970-01-01).
/// Returns None if the date is invalid or its epoch day does not fit Date32.
fn make_date(year: i32, month: i32, day: i32) -> Option<i32> {
ymd_to_epoch_day(year.into(), month.into(), day.into())
.and_then(|days| i32::try_from(days).ok())
}
impl ScalarUDFImpl for SparkMakeDate {
fn name(&self) -> &str {
"make_date"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _: &[DataType]) -> Result<DataType> {
Ok(DataType::Date32)
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let [year, month, day] = take_function_args(self.name(), args.args)?;
// Determine the batch size from array arguments (scalars have no inherent size)
let num_rows = [&year, &month, &day]
.iter()
.find_map(|arg| match arg {
ColumnarValue::Array(array) => Some(array.len()),
ColumnarValue::Scalar(_) => None,
})
.unwrap_or(1);
// Convert scalars to arrays for uniform processing, using the correct batch size
let year_arr = year.into_array(num_rows)?;
let month_arr = month.into_array(num_rows)?;
let day_arr = day.into_array(num_rows)?;
// Cast to Int32 if needed (handles Int64 literals from SQL)
let year_arr = cast_to_int32(&year_arr)?;
let month_arr = cast_to_int32(&month_arr)?;
let day_arr = cast_to_int32(&day_arr)?;
let year_array = year_arr
.as_any()
.downcast_ref::<Int32Array>()
.ok_or_else(|| {
DataFusionError::Execution("make_date: failed to cast year to Int32".to_string())
})?;
let month_array = month_arr
.as_any()
.downcast_ref::<Int32Array>()
.ok_or_else(|| {
DataFusionError::Execution("make_date: failed to cast month to Int32".to_string())
})?;
let day_array = day_arr
.as_any()
.downcast_ref::<Int32Array>()
.ok_or_else(|| {
DataFusionError::Execution("make_date: failed to cast day to Int32".to_string())
})?;
let len = year_array.len();
let mut builder = Date32Array::builder(len);
for i in 0..len {
if year_array.is_null(i) || month_array.is_null(i) || day_array.is_null(i) {
builder.append_null();
} else {
let y = year_array.value(i);
let m = month_array.value(i);
let d = day_array.value(i);
match make_date(y, m, d) {
Some(days) => builder.append_value(days),
None => {
if self.fail_on_error {
return Err(SparkError::DatetimeFieldOutOfBounds {
range_message: invalid_date_message(y, m, d),
}
.into());
}
builder.append_null();
}
}
}
}
Ok(ColumnarValue::Array(Arc::new(builder.finish())))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_date_valid() {
// Unix epoch
assert_eq!(make_date(1970, 1, 1), Some(0));
// Day after epoch
assert_eq!(make_date(1970, 1, 2), Some(1));
// Day before epoch
assert_eq!(make_date(1969, 12, 31), Some(-1));
// Leap years - just verify they return Some (valid dates)
assert!(make_date(2000, 2, 29).is_some()); // 2000 is a leap year
assert!(make_date(2004, 2, 29).is_some()); // 2004 is a leap year
// Regular date
assert!(make_date(2023, 6, 15).is_some());
}
#[test]
fn test_make_date_invalid_month() {
assert_eq!(make_date(2023, 0, 15), None);
assert_eq!(make_date(2023, 13, 15), None);
assert_eq!(make_date(2023, -1, 15), None);
}
#[test]
fn test_make_date_invalid_day() {
assert_eq!(make_date(2023, 6, 0), None);
assert_eq!(make_date(2023, 6, 32), None);
assert_eq!(make_date(2023, 6, -1), None);
}
#[test]
fn test_make_date_invalid_dates() {
// Feb 30 never exists
assert_eq!(make_date(2023, 2, 30), None);
// Feb 29 on non-leap year
assert_eq!(make_date(2023, 2, 29), None);
// 1900 is not a leap year (divisible by 100 but not 400)
assert_eq!(make_date(1900, 2, 29), None);
// 2100 will not be a leap year
assert_eq!(make_date(2100, 2, 29), None);
// April has 30 days
assert_eq!(make_date(2023, 4, 31), None);
}
#[test]
fn test_make_date_wide_year_range() {
assert_eq!(make_date(0, 1, 1), Some(-719_528));
assert_eq!(make_date(-1, 1, 1), Some(-719_893));
assert_eq!(make_date(300_000, 6, 15), Some(108_853_388));
assert_eq!(make_date(300_000, 2, 29), Some(108_853_281));
assert_eq!(make_date(5_881_580, 7, 11), Some(i32::MAX));
assert_eq!(make_date(-5_877_641, 6, 23), Some(i32::MIN));
}
}