| // 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, Decimal128Array, Int32Array, Time64NanosecondArray}; | |
| use arrow::compute::cast; | |
| use arrow::datatypes::{DataType, TimeUnit}; | |
| use datafusion::common::{utils::take_function_args, DataFusionError, Result}; | |
| use datafusion::logical_expr::{ | |
| ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility, | |
| }; | |
| use std::sync::Arc; | |
| const MICROS_PER_SECOND: i128 = 1_000_000; | |
| const NANOS_PER_MICRO: i64 = 1_000; | |
| const NANOS_PER_SECOND: i64 = 1_000_000_000; | |
| #[derive(Debug, PartialEq, Eq, Hash)] | |
| pub struct SparkMakeTime { | |
| signature: Signature, | |
| } | |
| impl SparkMakeTime { | |
| pub fn new() -> Self { | |
| Self { | |
| signature: Signature::any(3, Volatility::Immutable), | |
| } | |
| } | |
| } | |
| impl Default for SparkMakeTime { | |
| fn default() -> Self { | |
| Self::new() | |
| } | |
| } | |
| /// Converts hours, minutes, and fractional seconds (Decimal(16,6)) to nanoseconds from midnight. | |
| /// Returns an error for invalid inputs (matching Spark's always-throw behavior). | |
| fn make_time(hours: i32, minutes: i32, secs_and_micros_unscaled: i128) -> Result<i64> { | |
| let full_secs = secs_and_micros_unscaled.div_euclid(MICROS_PER_SECOND); | |
| let frac_micros = secs_and_micros_unscaled.rem_euclid(MICROS_PER_SECOND); | |
| if full_secs > i32::MAX as i128 || full_secs < 0 { | |
| return Err(DataFusionError::Execution(format!( | |
| "Invalid value for SecondOfMinute (valid values 0 - 59): {}", | |
| secs_and_micros_unscaled / MICROS_PER_SECOND | |
| ))); | |
| } | |
| let secs = full_secs as i32; | |
| let nanos = (frac_micros as i64) * NANOS_PER_MICRO; | |
| if !(0..=23).contains(&hours) { | |
| return Err(DataFusionError::Execution(format!( | |
| "Invalid value for HourOfDay (valid values 0 - 23): {hours}" | |
| ))); | |
| } | |
| if !(0..=59).contains(&minutes) { | |
| return Err(DataFusionError::Execution(format!( | |
| "Invalid value for MinuteOfHour (valid values 0 - 59): {minutes}" | |
| ))); | |
| } | |
| if !(0..=59).contains(&secs) { | |
| return Err(DataFusionError::Execution(format!( | |
| "Invalid value for SecondOfMinute (valid values 0 - 59): {secs}" | |
| ))); | |
| } | |
| let total_nanos = hours as i64 * 3_600 * NANOS_PER_SECOND | |
| + minutes as i64 * 60 * NANOS_PER_SECOND | |
| + secs as i64 * NANOS_PER_SECOND | |
| + nanos; | |
| Ok(total_nanos) | |
| } | |
| impl ScalarUDFImpl for SparkMakeTime { | |
| fn name(&self) -> &str { | |
| "make_time" | |
| } | |
| fn signature(&self) -> &Signature { | |
| &self.signature | |
| } | |
| fn return_type(&self, _: &[DataType]) -> Result<DataType> { | |
| Ok(DataType::Time64(TimeUnit::Nanosecond)) | |
| } | |
| fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> { | |
| let [hours, minutes, secs_and_micros] = take_function_args(self.name(), args.args)?; | |
| let num_rows = [&hours, &minutes, &secs_and_micros] | |
| .iter() | |
| .find_map(|arg| match arg { | |
| ColumnarValue::Array(array) => Some(array.len()), | |
| ColumnarValue::Scalar(_) => None, | |
| }) | |
| .unwrap_or(1); | |
| let hours_arr = hours.into_array(num_rows)?; | |
| let minutes_arr = minutes.into_array(num_rows)?; | |
| let secs_arr = secs_and_micros.into_array(num_rows)?; | |
| let hours_arr = cast_to_int32(&hours_arr)?; | |
| let minutes_arr = cast_to_int32(&minutes_arr)?; | |
| let hours_array = hours_arr | |
| .as_any() | |
| .downcast_ref::<Int32Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Execution("make_time: failed to cast hours to Int32".to_string()) | |
| })?; | |
| let minutes_array = minutes_arr | |
| .as_any() | |
| .downcast_ref::<Int32Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Execution("make_time: failed to cast minutes to Int32".to_string()) | |
| })?; | |
| let secs_array = secs_arr | |
| .as_any() | |
| .downcast_ref::<Decimal128Array>() | |
| .ok_or_else(|| { | |
| DataFusionError::Execution( | |
| "make_time: expected Decimal128 for seconds argument".to_string(), | |
| ) | |
| })?; | |
| let len = hours_array.len(); | |
| let mut builder = Time64NanosecondArray::builder(len); | |
| for i in 0..len { | |
| if hours_array.is_null(i) || minutes_array.is_null(i) || secs_array.is_null(i) { | |
| builder.append_null(); | |
| } else { | |
| let h = hours_array.value(i); | |
| let m = minutes_array.value(i); | |
| let s = secs_array.value(i); | |
| let nanos = make_time(h, m, s)?; | |
| builder.append_value(nanos); | |
| } | |
| } | |
| Ok(ColumnarValue::Array(Arc::new(builder.finish()))) | |
| } | |
| } | |
| 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}"))) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[test] | |
| fn test_make_time_valid() { | |
| // Midnight | |
| assert_eq!(make_time(0, 0, 0).unwrap(), 0); | |
| // 1 hour | |
| assert_eq!(make_time(1, 0, 0).unwrap(), 3_600_000_000_000); | |
| // 1 minute | |
| assert_eq!(make_time(0, 1, 0).unwrap(), 60_000_000_000); | |
| // 1 second (unscaled: 1_000_000) | |
| assert_eq!(make_time(0, 0, 1_000_000).unwrap(), 1_000_000_000); | |
| // 1.5 seconds (unscaled: 1_500_000) | |
| assert_eq!(make_time(0, 0, 1_500_000).unwrap(), 1_500_000_000); | |
| // 23:59:59.999999 (unscaled: 59_999_999) | |
| assert_eq!(make_time(23, 59, 59_999_999).unwrap(), 86_399_999_999_000); | |
| // 12:30:45.123456 (unscaled: 45_123_456) | |
| assert_eq!( | |
| make_time(12, 30, 45_123_456).unwrap(), | |
| 12 * 3_600_000_000_000 + 30 * 60_000_000_000 + 45_123_456_000 | |
| ); | |
| } | |
| #[test] | |
| fn test_make_time_invalid_hours() { | |
| assert!(make_time(24, 0, 0).is_err()); | |
| assert!(make_time(25, 0, 0).is_err()); | |
| assert!(make_time(-1, 0, 0).is_err()); | |
| } | |
| #[test] | |
| fn test_make_time_invalid_minutes() { | |
| assert!(make_time(0, 60, 0).is_err()); | |
| assert!(make_time(0, -1, 0).is_err()); | |
| } | |
| #[test] | |
| fn test_make_time_invalid_seconds() { | |
| // 60 seconds (unscaled: 60_000_000) | |
| assert!(make_time(0, 0, 60_000_000).is_err()); | |
| // 100.5 seconds (unscaled: 100_500_000) | |
| assert!(make_time(0, 0, 100_500_000).is_err()); | |
| // negative seconds (unscaled: -1_000_000) | |
| assert!(make_time(0, 0, -1_000_000).is_err()); | |
| } | |
| #[test] | |
| fn test_make_time_overflow_seconds() { | |
| // Very large value that overflows i32 | |
| let large = (i32::MAX as i128 + 1) * MICROS_PER_SECOND; | |
| assert!(make_time(0, 0, large).is_err()); | |
| } | |
| } |