@@ -33,9 +33,14 @@ use std::sync::Arc;
3333
3434const MICROS_PER_SECOND : i64 = 1_000_000 ;
3535
36- /// Spark-compatible `cast` function for type conversions.
36+ /// Convert seconds to microseconds with saturating overflow behavior
37+ fn secs_to_micros ( secs : impl Into < i64 > ) -> i64 {
38+ secs. into ( ) . saturating_mul ( MICROS_PER_SECOND )
39+ }
40+
41+ /// Spark-compatible `cast` function for type conversions
3742///
38- /// This implements Spark's CAST expression with a target type parameter.
43+ /// This implements Spark's CAST expression with a target type parameter
3944///
4045/// # Usage
4146/// ```sql
@@ -46,11 +51,11 @@ const MICROS_PER_SECOND: i64 = 1_000_000;
4651/// - Int8/Int16/Int32/Int64 -> Timestamp (target_type = 'timestamp')
4752///
4853/// The integer value is interpreted as seconds since the Unix epoch (1970-01-01 00:00:00 UTC)
49- /// and converted to a timestamp with microsecond precision.
54+ /// and converted to a timestamp with microsecond precision (matches spark's spec)
5055///
5156/// # Overflow behavior
5257/// Uses saturating multiplication to handle overflow - values that would overflow
53- /// i64 when multiplied by 1,000,000 are clamped to i64::MAX or i64::MIN.
58+ /// i64 when multiplied by 1,000,000 are clamped to i64::MAX or i64::MIN
5459///
5560/// # References
5661/// - <https://spark.apache.org/docs/latest/api/sql/index.html#cast>
@@ -81,7 +86,7 @@ impl SparkCast {
8186/// Parse target type string into a DataType
8287fn parse_target_type ( type_str : & str ) -> DataFusionResult < DataType > {
8388 match type_str. to_lowercase ( ) . as_str ( ) {
84- // could add further data type support in future
89+ // further data type support in future
8590 "timestamp" => Ok ( DataType :: Timestamp ( TimeUnit :: Microsecond , None ) ) ,
8691 other => exec_err ! (
8792 "Unsupported spark_cast target type '{}'. Supported types: timestamp" ,
@@ -108,6 +113,7 @@ fn get_target_type_from_scalar_args(
108113
109114fn cast_int_to_timestamp < T : ArrowPrimitiveType > (
110115 array : & ArrayRef ,
116+ timezone : Option < Arc < str > > ,
111117) -> DataFusionResult < ArrayRef >
112118where
113119 T :: Native : Into < i64 > ,
@@ -120,12 +126,12 @@ where
120126 builder. append_null ( ) ;
121127 } else {
122128 // spark saturates to i64 min/max
123- let micros = ( arr. value ( i) . into ( ) ) . saturating_mul ( MICROS_PER_SECOND ) ;
129+ let micros = secs_to_micros ( arr. value ( i) . into ( ) ) ;
124130 builder. append_value ( micros) ;
125131 }
126132 }
127133
128- Ok ( Arc :: new ( builder. finish ( ) ) )
134+ Ok ( Arc :: new ( builder. finish ( ) . with_timezone_opt ( timezone ) ) )
129135}
130136
131137impl ScalarUDFImpl for SparkCast {
@@ -159,35 +165,47 @@ impl ScalarUDFImpl for SparkCast {
159165 args : ScalarFunctionArgs ,
160166 ) -> DataFusionResult < ColumnarValue > {
161167 let target_type = args. return_field . data_type ( ) ;
168+ // Use session timezone, fallback to UTC if not set
169+ let session_tz: Arc < str > = args
170+ . config_options
171+ . execution
172+ . time_zone
173+ . clone ( )
174+ . map ( |s| Arc :: from ( s. as_str ( ) ) )
175+ . unwrap_or_else ( || Arc :: from ( "UTC" ) ) ;
162176
163177 match target_type {
164178 DataType :: Timestamp ( TimeUnit :: Microsecond , None ) => {
165- cast_to_timestamp ( & args. args [ 0 ] )
179+ cast_to_timestamp ( & args. args [ 0 ] , Some ( session_tz ) )
166180 }
167181 other => exec_err ! ( "Unsupported spark_cast target type: {:?}" , other) ,
168182 }
169183 }
170184}
171185
172- /// Cast value to timestamp
173- fn cast_to_timestamp ( input : & ColumnarValue ) -> DataFusionResult < ColumnarValue > {
186+ /// Cast value to timestamp internal function
187+ fn cast_to_timestamp (
188+ input : & ColumnarValue ,
189+ timezone : Option < Arc < str > > ,
190+ ) -> DataFusionResult < ColumnarValue > {
174191 match input {
175192 ColumnarValue :: Array ( array) => match array. data_type ( ) {
176193 DataType :: Null => Ok ( ColumnarValue :: Array ( Arc :: new (
177- arrow:: array:: TimestampMicrosecondArray :: new_null ( array. len ( ) ) ,
194+ arrow:: array:: TimestampMicrosecondArray :: new_null ( array. len ( ) )
195+ . with_timezone_opt ( timezone) ,
178196 ) ) ) ,
179197 DataType :: Int8 => Ok ( ColumnarValue :: Array (
180- cast_int_to_timestamp :: < Int8Type > ( array) ?,
198+ cast_int_to_timestamp :: < Int8Type > ( array, timezone ) ?,
181199 ) ) ,
182200 DataType :: Int16 => Ok ( ColumnarValue :: Array ( cast_int_to_timestamp :: <
183201 Int16Type ,
184- > ( array) ?) ) ,
202+ > ( array, timezone ) ?) ) ,
185203 DataType :: Int32 => Ok ( ColumnarValue :: Array ( cast_int_to_timestamp :: <
186204 Int32Type ,
187- > ( array) ?) ) ,
205+ > ( array, timezone ) ?) ) ,
188206 DataType :: Int64 => Ok ( ColumnarValue :: Array ( cast_int_to_timestamp :: <
189207 Int64Type ,
190- > ( array) ?) ) ,
208+ > ( array, timezone ) ?) ) ,
191209 other => exec_err ! ( "Unsupported cast from {:?} to timestamp" , other) ,
192210 } ,
193211 ColumnarValue :: Scalar ( scalar) => {
@@ -197,22 +215,16 @@ fn cast_to_timestamp(input: &ColumnarValue) -> DataFusionResult<ColumnarValue> {
197215 | ScalarValue :: Int16 ( None )
198216 | ScalarValue :: Int32 ( None )
199217 | ScalarValue :: Int64 ( None ) => None ,
200- ScalarValue :: Int8 ( Some ( v) ) => {
201- Some ( ( * v as i64 ) . saturating_mul ( MICROS_PER_SECOND ) )
202- }
203- ScalarValue :: Int16 ( Some ( v) ) => {
204- Some ( ( * v as i64 ) . saturating_mul ( MICROS_PER_SECOND ) )
205- }
206- ScalarValue :: Int32 ( Some ( v) ) => {
207- Some ( ( * v as i64 ) . saturating_mul ( MICROS_PER_SECOND ) )
208- }
209- ScalarValue :: Int64 ( Some ( v) ) => Some ( v. saturating_mul ( MICROS_PER_SECOND ) ) ,
218+ ScalarValue :: Int8 ( Some ( v) ) => Some ( secs_to_micros ( * v) ) ,
219+ ScalarValue :: Int16 ( Some ( v) ) => Some ( secs_to_micros ( * v) ) ,
220+ ScalarValue :: Int32 ( Some ( v) ) => Some ( secs_to_micros ( * v) ) ,
221+ ScalarValue :: Int64 ( Some ( v) ) => Some ( secs_to_micros ( * v) ) ,
210222 other => {
211223 return exec_err ! ( "Unsupported cast from {:?} to timestamp" , other) ;
212224 }
213225 } ;
214226 Ok ( ColumnarValue :: Scalar ( ScalarValue :: TimestampMicrosecond (
215- micros, None ,
227+ micros, timezone ,
216228 ) ) )
217229 }
218230 }
@@ -224,12 +236,25 @@ mod tests {
224236 use arrow:: array:: { Int8Array , Int16Array , Int32Array , Int64Array } ;
225237 use arrow:: datatypes:: TimestampMicrosecondType ;
226238
239+ // helpers to make testing easier
227240 fn make_args ( input : ColumnarValue , target_type : & str ) -> ScalarFunctionArgs {
241+ make_args_with_timezone ( input, target_type, None )
242+ }
243+
244+ fn make_args_with_timezone (
245+ input : ColumnarValue ,
246+ target_type : & str ,
247+ timezone : Option < & str > ,
248+ ) -> ScalarFunctionArgs {
228249 let return_field = Arc :: new ( Field :: new (
229250 "result" ,
230251 DataType :: Timestamp ( TimeUnit :: Microsecond , None ) ,
231252 true ,
232253 ) ) ;
254+ let mut config = datafusion_common:: config:: ConfigOptions :: default ( ) ;
255+ if let Some ( tz) = timezone {
256+ config. execution . time_zone = Some ( tz. to_string ( ) ) ;
257+ }
233258 ScalarFunctionArgs {
234259 args : vec ! [
235260 input,
@@ -238,24 +263,46 @@ mod tests {
238263 arg_fields : vec ! [ ] ,
239264 number_rows : 0 ,
240265 return_field,
241- config_options : Arc :: new ( Default :: default ( ) ) ,
266+ config_options : Arc :: new ( config ) ,
242267 }
243268 }
244269
245270 fn assert_scalar_timestamp ( result : ColumnarValue , expected : i64 ) {
271+ assert_scalar_timestamp_with_tz ( result, expected, "UTC" ) ;
272+ }
273+
274+ fn assert_scalar_timestamp_with_tz (
275+ result : ColumnarValue ,
276+ expected : i64 ,
277+ expected_tz : & str ,
278+ ) {
246279 match result {
247- ColumnarValue :: Scalar ( ScalarValue :: TimestampMicrosecond ( Some ( val) , None ) ) => {
280+ ColumnarValue :: Scalar ( ScalarValue :: TimestampMicrosecond (
281+ Some ( val) ,
282+ Some ( tz) ,
283+ ) ) => {
248284 assert_eq ! ( val, expected) ;
285+ assert_eq ! ( tz. as_ref( ) , expected_tz) ;
286+ }
287+ _ => {
288+ panic ! (
289+ "Expected scalar timestamp with value {expected} and {expected_tz} timezone"
290+ )
249291 }
250- _ => panic ! ( "Expected scalar timestamp with value {expected}" ) ,
251292 }
252293 }
253294
254295 fn assert_scalar_null ( result : ColumnarValue ) {
255- assert ! ( matches!(
256- result,
257- ColumnarValue :: Scalar ( ScalarValue :: TimestampMicrosecond ( None , None ) )
258- ) ) ;
296+ assert_scalar_null_with_tz ( result, "UTC" ) ;
297+ }
298+
299+ fn assert_scalar_null_with_tz ( result : ColumnarValue , expected_tz : & str ) {
300+ match result {
301+ ColumnarValue :: Scalar ( ScalarValue :: TimestampMicrosecond ( None , Some ( tz) ) ) => {
302+ assert_eq ! ( tz. as_ref( ) , expected_tz) ;
303+ }
304+ _ => panic ! ( "Expected null scalar timestamp with {expected_tz} timezone" ) ,
305+ }
259306 }
260307
261308 #[ test]
@@ -521,4 +568,65 @@ mod tests {
521568 _ => panic ! ( "Expected array result" ) ,
522569 }
523570 }
571+
572+ #[ test]
573+ fn test_cast_int_to_timestamp_with_timezones ( ) {
574+ // Test with various timezones like Comet does
575+ let timezones = [
576+ "UTC" ,
577+ "America/New_York" ,
578+ "America/Los_Angeles" ,
579+ "Europe/London" ,
580+ "Asia/Tokyo" ,
581+ "Australia/Sydney" ,
582+ ] ;
583+
584+ let cast = SparkCast :: new ( ) ;
585+ let test_value: i64 = 1704067200 ; // 2024-01-01 00:00:00 UTC
586+ let expected_micros = test_value * MICROS_PER_SECOND ;
587+
588+ for tz in timezones {
589+ // scalar
590+ let args = make_args_with_timezone (
591+ ColumnarValue :: Scalar ( ScalarValue :: Int64 ( Some ( test_value) ) ) ,
592+ "timestamp" ,
593+ Some ( tz) ,
594+ ) ;
595+ let result = cast. invoke_with_args ( args) . unwrap ( ) ;
596+ assert_scalar_timestamp_with_tz ( result, expected_micros, tz) ;
597+
598+ // array input
599+ let array: ArrayRef =
600+ Arc :: new ( Int64Array :: from ( vec ! [ Some ( test_value) , None ] ) ) ;
601+ let args = make_args_with_timezone (
602+ ColumnarValue :: Array ( array) ,
603+ "timestamp" ,
604+ Some ( tz) ,
605+ ) ;
606+ let result = cast. invoke_with_args ( args) . unwrap ( ) ;
607+
608+ match result {
609+ ColumnarValue :: Array ( result_array) => {
610+ let ts_array =
611+ result_array. as_primitive :: < TimestampMicrosecondType > ( ) ;
612+ assert_eq ! ( ts_array. value( 0 ) , expected_micros) ;
613+ assert ! ( ts_array. is_null( 1 ) ) ;
614+ assert_eq ! ( ts_array. timezone( ) , Some ( tz) ) ;
615+ }
616+ _ => panic ! ( "Expected array result for timezone {tz}" ) ,
617+ }
618+ }
619+ }
620+
621+ #[ test]
622+ fn test_cast_int_to_timestamp_default_timezone ( ) {
623+ let cast = SparkCast :: new ( ) ;
624+ let args = make_args (
625+ ColumnarValue :: Scalar ( ScalarValue :: Int64 ( Some ( 0 ) ) ) ,
626+ "timestamp" ,
627+ ) ;
628+ let result = cast. invoke_with_args ( args) . unwrap ( ) ;
629+ // Defaults to UTC
630+ assert_scalar_timestamp_with_tz ( result, 0 , "UTC" ) ;
631+ }
524632}
0 commit comments