blob: 58e41085801a30aba30b5e7d587ce93aff4b6846 [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 chrono::NaiveDate;
use datafusion::common::{utils::take_function_args, DataFusionError, Result};
use datafusion::logical_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use std::any::Any;
use std::sync::Arc;
/// Spark-compatible make_date function.
/// Creates a date from year, month, and day columns.
/// Returns NULL for invalid dates (e.g., Feb 30, month 13, etc.) instead of throwing an error.
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct SparkMakeDate {
signature: Signature,
}
impl SparkMakeDate {
pub fn new() -> Self {
Self {
// Accept any numeric type - we'll cast to Int32 internally
signature: Signature::any(3, Volatility::Immutable),
}
}
}
impl Default for SparkMakeDate {
fn default() -> Self {
Self::new()
}
}
/// 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.
fn make_date(year: i32, month: i32, day: i32) -> Option<i32> {
// Validate month and day ranges first
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
// Try to create a valid date
NaiveDate::from_ymd_opt(year, month as u32, day as u32).map(|date| {
date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap())
.num_days() as i32
})
}
impl ScalarUDFImpl for SparkMakeDate {
fn as_any(&self) -> &dyn Any {
self
}
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 => 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_extreme_years() {
// Spark supports dates from 0001-01-01 to 9999-12-31 (Proleptic Gregorian calendar)
// Minimum valid date in Spark: 0001-01-01
assert!(make_date(1, 1, 1).is_some(), "Year 1 should be valid");
// Maximum valid date in Spark: 9999-12-31
assert!(
make_date(9999, 12, 31).is_some(),
"Year 9999 should be valid"
);
// Year 0 - In Proleptic Gregorian calendar, year 0 = 1 BCE
// Spark returns NULL for year 0 in make_date
// chrono supports year 0, but we should match Spark's behavior
// For now, chrono allows it - this may need adjustment for full Spark compatibility
let year_0_result = make_date(0, 1, 1);
// chrono allows year 0 (1 BCE in proleptic Gregorian)
assert!(year_0_result.is_some(), "chrono allows year 0");
// Negative years - Spark returns NULL for negative years
// chrono supports negative years (BCE dates)
let negative_year_result = make_date(-1, 1, 1);
// chrono allows negative years
assert!(
negative_year_result.is_some(),
"chrono allows negative years"
);
}
}