| // 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::datatypes::{DataType, TimeUnit, DECIMAL128_MAX_PRECISION}; |
| use arrow::{ |
| array::{ |
| cast::as_primitive_array, |
| types::{Int32Type, TimestampMicrosecondType}, |
| BooleanBufferBuilder, |
| }, |
| buffer::BooleanBuffer, |
| }; |
| use datafusion::logical_expr::EmitTo; |
| use std::sync::Arc; |
| |
| use crate::timezone::Tz; |
| use arrow::array::types::TimestampMillisecondType; |
| use arrow::array::TimestampMicrosecondArray; |
| use arrow::datatypes::{MAX_DECIMAL128_FOR_EACH_PRECISION, MIN_DECIMAL128_FOR_EACH_PRECISION}; |
| use arrow::error::ArrowError; |
| use arrow::{ |
| array::{as_dictionary_array, Array, ArrayRef, PrimitiveArray}, |
| temporal_conversions::as_datetime, |
| }; |
| use chrono::{DateTime, LocalResult, NaiveDateTime, Offset, TimeZone}; |
| |
| /// Preprocesses input arrays to add timezone information from Spark to Arrow array datatype or |
| /// to apply timezone offset. |
| // |
| // We consider the following cases: |
| // |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // | Conversion | Input array | Timezone | Output array | |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // | Timestamp -> | Array in UTC | Timezone of input | A timestamp with the timezone | |
| // | Utf8 or Date32 | | | offset applied and timezone | |
| // | | | | removed | |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // | Timestamp -> | Array in UTC | Timezone of input | Same as input array | |
| // | Timestamp w/Timezone| | | | |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // | Timestamp_ntz -> | Array in | Timezone of input | Same as input array | |
| // | Utf8 or Date32 | timezone | | | |
| // | | session local| | | |
| // | | timezone | | | |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // | Timestamp_ntz -> | Array in | Timezone of input | Array in UTC and timezone | |
| // | Timestamp w/Timezone | session local| | specified in input | |
| // | | timezone | | | |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // | Timestamp(_ntz) -> | | |
| // | Any other type | Not Supported | |
| // | --------------------- | ------------ | ----------------- | -------------------------------- | |
| // |
| pub fn array_with_timezone( |
| array: ArrayRef, |
| timezone: String, |
| to_type: Option<&DataType>, |
| ) -> Result<ArrayRef, ArrowError> { |
| match array.data_type() { |
| DataType::Timestamp(TimeUnit::Millisecond, None) => { |
| assert!(!timezone.is_empty()); |
| match to_type { |
| Some(DataType::Utf8) | Some(DataType::Date32) => Ok(array), |
| Some(DataType::Timestamp(_, Some(target_tz))) => { |
| // Interpret NTZ as local time in session TZ; annotate output with target TZ |
| // so the result has the exact annotation the caller expects. |
| timestamp_ntz_to_timestamp(array, timezone.as_str(), Some(target_tz.as_ref())) |
| } |
| Some(DataType::Timestamp(TimeUnit::Microsecond, None)) => { |
| // Convert from Timestamp(Millisecond, None) to Timestamp(Microsecond, None) |
| let millis_array = as_primitive_array::<TimestampMillisecondType>(&array); |
| let micros_array: TimestampMicrosecondArray = |
| arrow::compute::kernels::arity::unary(millis_array, |v| v * 1000); |
| Ok(Arc::new(micros_array)) |
| } |
| _ => { |
| // Not supported |
| Err(ArrowError::CastError(format!( |
| "Cannot convert from {:?} to {:?}", |
| array.data_type(), |
| to_type.unwrap() |
| ))) |
| } |
| } |
| } |
| DataType::Timestamp(TimeUnit::Microsecond, None) => { |
| assert!(!timezone.is_empty()); |
| match to_type { |
| Some(DataType::Utf8) | Some(DataType::Date32) => Ok(array), |
| Some(DataType::Timestamp(_, Some(target_tz))) => { |
| timestamp_ntz_to_timestamp(array, timezone.as_str(), Some(target_tz.as_ref())) |
| } |
| _ => { |
| // Not supported |
| Err(ArrowError::CastError(format!( |
| "Cannot convert from {:?} to {:?}", |
| array.data_type(), |
| to_type.unwrap() |
| ))) |
| } |
| } |
| } |
| DataType::Timestamp(_, None) => { |
| assert!(!timezone.is_empty()); |
| match to_type { |
| Some(DataType::Utf8) | Some(DataType::Date32) => Ok(array), |
| Some(DataType::Timestamp(_, Some(target_tz))) => { |
| timestamp_ntz_to_timestamp(array, timezone.as_str(), Some(target_tz.as_ref())) |
| } |
| _ => { |
| // Not supported |
| Err(ArrowError::CastError(format!( |
| "Cannot convert from {:?} to {:?}", |
| array.data_type(), |
| to_type.unwrap() |
| ))) |
| } |
| } |
| } |
| DataType::Timestamp(TimeUnit::Microsecond, Some(_)) => { |
| assert!(!timezone.is_empty()); |
| let array = as_primitive_array::<TimestampMicrosecondType>(&array); |
| let array_with_timezone = array.clone().with_timezone(timezone.clone()); |
| let array = Arc::new(array_with_timezone) as ArrayRef; |
| match to_type { |
| Some(DataType::Utf8) | Some(DataType::Date32) => { |
| pre_timestamp_cast(array, timezone) |
| } |
| _ => Ok(array), |
| } |
| } |
| DataType::Timestamp(TimeUnit::Millisecond, Some(_)) => { |
| assert!(!timezone.is_empty()); |
| let array = as_primitive_array::<TimestampMillisecondType>(&array); |
| let array_with_timezone = array.clone().with_timezone(timezone.clone()); |
| let array = Arc::new(array_with_timezone) as ArrayRef; |
| match to_type { |
| Some(DataType::Utf8) | Some(DataType::Date32) => { |
| pre_timestamp_cast(array, timezone) |
| } |
| _ => Ok(array), |
| } |
| } |
| DataType::Dictionary(_, value_type) |
| if matches!(value_type.as_ref(), &DataType::Timestamp(_, _)) => |
| { |
| let dict = as_dictionary_array::<Int32Type>(&array); |
| let array = as_primitive_array::<TimestampMicrosecondType>(dict.values()); |
| let array_with_timezone = |
| array_with_timezone(Arc::new(array.clone()) as ArrayRef, timezone, to_type)?; |
| let dict = dict.with_values(array_with_timezone); |
| Ok(Arc::new(dict)) |
| } |
| _ => Ok(array), |
| } |
| } |
| |
| fn datetime_cast_err(value: i64) -> ArrowError { |
| ArrowError::CastError(format!( |
| "Cannot convert TimestampMicrosecondType {value} to datetime. Comet only supports dates between Jan 1, 262145 BCE and Dec 31, 262143 CE", |
| )) |
| } |
| |
| /// Resolves a local datetime in the given timezone to an absolute DateTime, |
| /// handling DST ambiguity and spring-forward gaps. |
| /// Parameters: |
| /// tz - timezone used to interpret local_datetime |
| /// local_datetime - a naive local datetime to resolve |
| pub(crate) fn resolve_local_datetime(tz: &Tz, local_datetime: NaiveDateTime) -> DateTime<Tz> { |
| match tz.from_local_datetime(&local_datetime) { |
| LocalResult::Single(dt) => dt, |
| LocalResult::Ambiguous(dt, _) => dt, |
| LocalResult::None => { |
| // Determine offset before time-change |
| let probe = local_datetime - chrono::Duration::hours(3); |
| let pre_offset = match tz.from_local_datetime(&probe) { |
| LocalResult::Single(dt) => dt.offset().fix(), |
| LocalResult::Ambiguous(dt, _) => dt.offset().fix(), |
| LocalResult::None => { |
| // Cannot determine offset; fall back to UTC interpretation |
| return local_datetime.and_utc().with_timezone(tz); |
| } |
| }; |
| let offset_secs = pre_offset.local_minus_utc() as i64; |
| |
| let utc_naive = local_datetime - chrono::Duration::seconds(offset_secs); |
| utc_naive.and_utc().with_timezone(tz) |
| } |
| } |
| } |
| |
| /// Takes in a Timestamp(Microsecond, None) array and a timezone id, and returns |
| /// a Timestamp(Microsecond, Some<_>) array. |
| /// The understanding is that the input array has time in the timezone specified in the second |
| /// argument. |
| /// Parameters: |
| /// array - input array of timestamp without timezone |
| /// tz - timezone of the values in the input array |
| /// to_timezone - timezone to change the input values to |
| pub(crate) fn timestamp_ntz_to_timestamp( |
| array: ArrayRef, |
| tz: &str, |
| to_timezone: Option<&str>, |
| ) -> Result<ArrayRef, ArrowError> { |
| assert!(!tz.is_empty()); |
| match array.data_type() { |
| DataType::Timestamp(TimeUnit::Microsecond, None) => { |
| let array = as_primitive_array::<TimestampMicrosecondType>(&array); |
| let tz: Tz = tz.parse()?; |
| let array: PrimitiveArray<TimestampMicrosecondType> = array.try_unary(|value| { |
| as_datetime::<TimestampMicrosecondType>(value) |
| .ok_or_else(|| datetime_cast_err(value)) |
| .map(|local_datetime| { |
| let datetime = resolve_local_datetime(&tz, local_datetime); |
| |
| datetime.timestamp_micros() |
| }) |
| })?; |
| let array_with_tz = if let Some(to_tz) = to_timezone { |
| array.with_timezone(to_tz) |
| } else { |
| array |
| }; |
| Ok(Arc::new(array_with_tz)) |
| } |
| DataType::Timestamp(TimeUnit::Millisecond, None) => { |
| let array = as_primitive_array::<TimestampMillisecondType>(&array); |
| let tz: Tz = tz.parse()?; |
| let array: PrimitiveArray<TimestampMillisecondType> = array.try_unary(|value| { |
| as_datetime::<TimestampMillisecondType>(value) |
| .ok_or_else(|| datetime_cast_err(value)) |
| .map(|local_datetime| { |
| let datetime = resolve_local_datetime(&tz, local_datetime); |
| |
| datetime.timestamp_millis() |
| }) |
| })?; |
| let array_with_tz = if let Some(to_tz) = to_timezone { |
| array.with_timezone(to_tz) |
| } else { |
| array |
| }; |
| Ok(Arc::new(array_with_tz)) |
| } |
| _ => Ok(array), |
| } |
| } |
| |
| /// Converts a `Timestamp(Microsecond, Some(_))` array to `Timestamp(Microsecond, None)` |
| /// (TIMESTAMP_NTZ) by interpreting the UTC epoch value in the given session timezone and |
| /// storing the resulting local datetime as epoch-relative microseconds without a TZ annotation. |
| /// |
| /// Matches Spark: `convertTz(ts, ZoneOffset.UTC, zoneId)` |
| pub(crate) fn cast_timestamp_to_ntz( |
| array: ArrayRef, |
| timezone: &str, |
| ) -> Result<ArrayRef, ArrowError> { |
| assert!(!timezone.is_empty()); |
| let tz: Tz = timezone.parse()?; |
| match array.data_type() { |
| DataType::Timestamp(TimeUnit::Microsecond, Some(_)) => { |
| let array = as_primitive_array::<TimestampMicrosecondType>(&array); |
| let result: PrimitiveArray<TimestampMicrosecondType> = array.try_unary(|value| { |
| as_datetime::<TimestampMicrosecondType>(value) |
| .ok_or_else(|| datetime_cast_err(value)) |
| .map(|utc_naive| { |
| // Convert UTC naive datetime → local datetime in session TZ |
| let local_dt = tz.from_utc_datetime(&utc_naive); |
| // Re-encode as epoch-relative μs treating local time as UTC anchor. |
| // This produces the NTZ representation (no offset applied). |
| local_dt.naive_local().and_utc().timestamp_micros() |
| }) |
| })?; |
| // No timezone annotation on output = TIMESTAMP_NTZ |
| Ok(Arc::new(result)) |
| } |
| _ => Err(ArrowError::CastError(format!( |
| "cast_timestamp_to_ntz: unexpected input type {:?}", |
| array.data_type() |
| ))), |
| } |
| } |
| |
| /// This takes for special pre-casting cases of Spark. E.g., Timestamp to String. |
| fn pre_timestamp_cast(array: ArrayRef, timezone: String) -> Result<ArrayRef, ArrowError> { |
| assert!(!timezone.is_empty()); |
| match array.data_type() { |
| DataType::Timestamp(_, _) => { |
| // Spark doesn't output timezone while casting timestamp to string, but arrow's cast |
| // kernel does if timezone exists. So we need to apply offset of timezone to array |
| // timestamp value and remove timezone from array datatype. |
| let array = as_primitive_array::<TimestampMicrosecondType>(&array); |
| |
| let tz: Tz = timezone.parse()?; |
| let array: PrimitiveArray<TimestampMicrosecondType> = array.try_unary(|value| { |
| as_datetime::<TimestampMicrosecondType>(value) |
| .ok_or_else(|| datetime_cast_err(value)) |
| .map(|datetime| { |
| let offset = tz.offset_from_utc_datetime(&datetime).fix(); |
| let datetime = datetime + offset; |
| datetime.and_utc().timestamp_micros() |
| }) |
| })?; |
| |
| Ok(Arc::new(array)) |
| } |
| _ => Ok(array), |
| } |
| } |
| |
| /// Adapted from arrow-rs `validate_decimal_precision` but returns bool |
| /// instead of Err to avoid the cost of formatting the error strings and is |
| /// optimized to remove a memcpy that exists in the original function |
| /// we can remove this code once we upgrade to a version of arrow-rs that |
| /// includes https://github.com/apache/arrow-rs/pull/6419 |
| #[inline] |
| pub fn is_valid_decimal_precision(value: i128, precision: u8) -> bool { |
| precision <= DECIMAL128_MAX_PRECISION |
| && value >= MIN_DECIMAL128_FOR_EACH_PRECISION[precision as usize] |
| && value <= MAX_DECIMAL128_FOR_EACH_PRECISION[precision as usize] |
| } |
| |
| /// Build a boolean buffer from the state and reset the state, based on the emit_to |
| /// strategy. |
| pub fn build_bool_state(state: &mut BooleanBufferBuilder, emit_to: &EmitTo) -> BooleanBuffer { |
| let bool_state: BooleanBuffer = state.finish(); |
| |
| match emit_to { |
| EmitTo::All => bool_state, |
| EmitTo::First(n) => { |
| state.append_buffer(&bool_state.slice(*n, bool_state.len() - n)); |
| bool_state.slice(0, *n) |
| } |
| } |
| } |
| |
| // These are borrowed from hashbrown crate: |
| // https://github.com/rust-lang/hashbrown/blob/master/src/raw/mod.rs |
| |
| // On stable we can use #[cold] to get a equivalent effect: this attributes |
| // suggests that the function is unlikely to be called |
| #[inline] |
| #[cold] |
| pub fn cold() {} |
| |
| #[inline] |
| pub fn likely(b: bool) -> bool { |
| if !b { |
| cold(); |
| } |
| b |
| } |
| #[inline] |
| pub fn unlikely(b: bool) -> bool { |
| if b { |
| cold(); |
| } |
| b |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| fn array_containing(local_datetime: &str) -> ArrayRef { |
| let dt = NaiveDateTime::parse_from_str(local_datetime, "%Y-%m-%d %H:%M:%S").unwrap(); |
| let ts = dt.and_utc().timestamp_micros(); |
| Arc::new(TimestampMicrosecondArray::from(vec![ts])) |
| } |
| |
| fn micros_for(datetime: &str) -> i64 { |
| NaiveDateTime::parse_from_str(datetime, "%Y-%m-%d %H:%M:%S") |
| .unwrap() |
| .and_utc() |
| .timestamp_micros() |
| } |
| |
| #[test] |
| fn test_build_bool_state() { |
| let mut builder = BooleanBufferBuilder::new(0); |
| builder.append_packed_range(0..16, &[0x42u8, 0x39u8]); |
| |
| let mut first_nine = BooleanBufferBuilder::new(0); |
| first_nine.append_packed_range(0..9, &[0x42u8, 0x01u8]); |
| let first_nine = first_nine.finish(); |
| let mut last = BooleanBufferBuilder::new(0); |
| last.append_packed_range(0..7, &[0x1cu8]); |
| let last = last.finish(); |
| |
| assert_eq!( |
| first_nine, |
| build_bool_state(&mut builder, &EmitTo::First(9)) |
| ); |
| assert_eq!(last, build_bool_state(&mut builder, &EmitTo::All)); |
| } |
| |
| #[test] |
| fn test_timestamp_ntz_to_timestamp_handles_non_existent_time() { |
| let output = timestamp_ntz_to_timestamp( |
| array_containing("2024-03-31 01:30:00"), |
| "Europe/London", |
| None, |
| ) |
| .unwrap(); |
| |
| assert_eq!( |
| as_primitive_array::<TimestampMicrosecondType>(&output).value(0), |
| micros_for("2024-03-31 01:30:00") |
| ); |
| } |
| |
| #[test] |
| fn test_timestamp_ntz_to_timestamp_handles_ambiguous_time() { |
| let output = timestamp_ntz_to_timestamp( |
| array_containing("2024-10-27 01:30:00"), |
| "Europe/London", |
| None, |
| ) |
| .unwrap(); |
| |
| assert_eq!( |
| as_primitive_array::<TimestampMicrosecondType>(&output).value(0), |
| micros_for("2024-10-27 00:30:00") |
| ); |
| } |
| |
| // Helper: build a Timestamp(Microsecond, Some(tz)) array from a UTC datetime string |
| fn ts_with_tz(utc_datetime: &str, tz: &str) -> ArrayRef { |
| let dt = NaiveDateTime::parse_from_str(utc_datetime, "%Y-%m-%d %H:%M:%S").unwrap(); |
| let ts = dt.and_utc().timestamp_micros(); |
| Arc::new(TimestampMicrosecondArray::from(vec![ts]).with_timezone(tz.to_string())) |
| } |
| |
| #[test] |
| fn test_cast_timestamp_to_ntz_utc() { |
| // In UTC, local time == UTC time, so NTZ value == UTC epoch value |
| let input = ts_with_tz("2024-01-15 10:30:00", "UTC"); |
| let result = cast_timestamp_to_ntz(input, "UTC").unwrap(); |
| let out = as_primitive_array::<TimestampMicrosecondType>(&result); |
| // Expected NTZ value: epoch μs for "2024-01-15 10:30:00" as if it were UTC |
| let expected = NaiveDateTime::parse_from_str("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S") |
| .unwrap() |
| .and_utc() |
| .timestamp_micros(); |
| assert_eq!(out.value(0), expected); |
| assert_eq!(out.timezone(), None); // no TZ annotation = NTZ |
| } |
| |
| #[test] |
| fn test_cast_timestamp_to_ntz_offset_timezone() { |
| // UTC epoch for "2024-01-15 15:30:00 UTC" cast to NTZ with session TZ = America/New_York (UTC-5) |
| // Local time in NY = 10:30:00 → NTZ should store epoch μs for "2024-01-15 10:30:00" |
| let input = ts_with_tz("2024-01-15 15:30:00", "UTC"); |
| let result = cast_timestamp_to_ntz(input, "America/New_York").unwrap(); |
| let out = as_primitive_array::<TimestampMicrosecondType>(&result); |
| let expected = NaiveDateTime::parse_from_str("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S") |
| .unwrap() |
| .and_utc() |
| .timestamp_micros(); |
| assert_eq!(out.value(0), expected); |
| assert_eq!(out.timezone(), None); |
| } |
| |
| #[test] |
| fn test_cast_timestamp_to_ntz_dst() { |
| // During DST: UTC epoch for "2024-07-04 16:30:00 UTC", session TZ = America/New_York (UTC-4 in summer) |
| // Local time in NY = 12:30:00 → NTZ stores epoch μs for "2024-07-04 12:30:00" |
| let input = ts_with_tz("2024-07-04 16:30:00", "UTC"); |
| let result = cast_timestamp_to_ntz(input, "America/New_York").unwrap(); |
| let out = as_primitive_array::<TimestampMicrosecondType>(&result); |
| let expected = NaiveDateTime::parse_from_str("2024-07-04 12:30:00", "%Y-%m-%d %H:%M:%S") |
| .unwrap() |
| .and_utc() |
| .timestamp_micros(); |
| assert_eq!(out.value(0), expected); |
| } |
| } |