| // 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" |
| ); |
| } |
| } |