@@ -16,11 +16,13 @@ use datafusion::logical_expr::{
1616} ;
1717use delta_kernel:: engine:: arrow_conversion:: TryIntoArrow ;
1818use delta_kernel:: engine:: arrow_data:: ArrowEngineData ;
19+ use delta_kernel:: engine:: arrow_expression:: evaluate_expression as kernel_expression;
1920use delta_kernel:: engine:: parse_json;
2021use delta_kernel:: expressions:: {
2122 BinaryExpression , BinaryExpressionOp , ColumnName as KernelColumnName ,
2223 Expression as KernelExpression , ExpressionRef , ExpressionStructPatch , MapToStructExpression ,
23- ParseJsonExpression , UnaryExpressionOp , VariadicExpression , VariadicExpressionOp ,
24+ MapToStructOptions , ParseJsonExpression , UnaryExpressionOp , VariadicExpression ,
25+ VariadicExpressionOp ,
2426} ;
2527use delta_kernel:: schema:: {
2628 DataType as KernelDataType , PrimitiveType , SchemaRef as KernelSchemaRef , StructField ,
@@ -366,6 +368,11 @@ fn map_to_struct_to_df_expr(
366368) -> DeltaResult < DFExpr > {
367369 let target = require_struct_output ( output_type, "MapToStruct" ) ?;
368370 let map = to_df_expr ( & map_to_struct. map_expr , input_schema, None ) ?;
371+ if let Some ( timestamp_timezone) = map_to_struct. options . timestamp_timezone ( ) {
372+ let udf =
373+ KernelMapToStructUdf :: try_new ( Arc :: new ( target. clone ( ) ) , timestamp_timezone. to_owned ( ) ) ?;
374+ return Ok ( ScalarUDF :: new_from_impl ( udf) . call ( vec ! [ map] ) ) ;
375+ }
369376
370377 let mut args = Vec :: with_capacity ( target. num_fields ( ) * 2 ) ;
371378 for field in target. fields ( ) {
@@ -394,6 +401,70 @@ fn map_to_struct_to_df_expr(
394401 Ok ( struct_null_when_not ( map. is_not_null ( ) , named_struct ( args) ) )
395402}
396403
404+ /// A DataFusion scalar UDF that delegates timezone-aware partition parsing to kernel's Arrow
405+ /// evaluator.
406+ #[ derive( Debug , PartialEq , Eq ) ]
407+ struct KernelMapToStructUdf {
408+ output_schema : KernelSchemaRef ,
409+ timestamp_timezone : String ,
410+ return_type : ArrowDataType ,
411+ signature : Signature ,
412+ }
413+
414+ impl std:: hash:: Hash for KernelMapToStructUdf {
415+ fn hash < H : std:: hash:: Hasher > ( & self , state : & mut H ) {
416+ for field in self . output_schema . fields ( ) {
417+ field. name ( ) . hash ( state) ;
418+ field. data_type ( ) . to_string ( ) . hash ( state) ;
419+ }
420+ self . timestamp_timezone . hash ( state) ;
421+ }
422+ }
423+
424+ impl KernelMapToStructUdf {
425+ fn try_new ( output_schema : KernelSchemaRef , timestamp_timezone : String ) -> DeltaResult < Self > {
426+ let arrow_schema: ArrowSchema = output_schema
427+ . as_ref ( )
428+ . try_into_arrow ( )
429+ . map_err ( Error :: generic_err) ?;
430+ Ok ( Self {
431+ return_type : ArrowDataType :: Struct ( arrow_schema. fields ( ) . clone ( ) ) ,
432+ output_schema,
433+ timestamp_timezone,
434+ signature : Signature :: any ( 1 , Volatility :: Immutable ) ,
435+ } )
436+ }
437+ }
438+
439+ impl ScalarUDFImpl for KernelMapToStructUdf {
440+ fn name ( & self ) -> & str {
441+ "kernel_map_to_struct_with_timestamp_timezone"
442+ }
443+
444+ fn signature ( & self ) -> & Signature {
445+ & self . signature
446+ }
447+
448+ fn return_type ( & self , _arg_types : & [ ArrowDataType ] ) -> Result < ArrowDataType , DataFusionError > {
449+ Ok ( self . return_type . clone ( ) )
450+ }
451+
452+ fn invoke_with_args ( & self , args : ScalarFunctionArgs ) -> Result < ColumnarValue , DataFusionError > {
453+ let num_rows = args. number_rows ;
454+ let [ map] = take_function_args ( self . name ( ) , args. args ) ?;
455+ let batch = RecordBatch :: try_from_iter ( [ ( "map" , map. into_array ( num_rows) ?) ] ) ?;
456+ let expression = KernelExpression :: map_to_struct_with_options (
457+ KernelExpression :: column ( [ "map" ] ) ,
458+ MapToStructOptions :: default ( ) . with_timestamp_timezone ( self . timestamp_timezone . clone ( ) ) ,
459+ ) ;
460+ let output_type = KernelDataType :: from ( self . output_schema . as_ref ( ) . clone ( ) ) ;
461+ let result =
462+ kernel_expression:: evaluate_expression ( & expression, & batch, Some ( & output_type) )
463+ . map_err ( |e| DataFusionError :: External ( Box :: new ( e) ) ) ?;
464+ Ok ( ColumnarValue :: Array ( result) )
465+ }
466+ }
467+
397468/// Lowers a `ParseJson` (parse a JSON-string column into a struct) to a call of the
398469/// [`ParseJsonUdf`] scalar UDF, which delegates to kernel's own JSON parser. Unlike the
399470/// struct-shaped arms, `ParseJson` is self-typed -- it carries its target `output_schema` -- so it
@@ -489,13 +560,17 @@ impl ScalarUDFImpl for ParseJsonUdf {
489560
490561#[ cfg( test) ]
491562mod tests {
492- use datafusion:: arrow:: array:: { Array , AsArray , StringArray } ;
563+ use datafusion:: arrow:: array:: {
564+ Array , AsArray , MapBuilder , StringArray , StringBuilder , TimestampMicrosecondArray ,
565+ } ;
566+ use datafusion:: arrow:: datatypes:: Field as ArrowField ;
493567 use datafusion:: assert_batches_eq;
494568 use datafusion:: common:: DFSchema ;
495569 use datafusion:: physical_expr:: create_physical_expr;
496570 use datafusion:: physical_expr:: execution_props:: ExecutionProps ;
497571 use delta_kernel:: expressions:: {
498572 col, lit, Expression as KernelExpr , ExpressionStructPatch , ExpressionStructPatchBuilder ,
573+ MapToStructOptions ,
499574 } ;
500575 use delta_kernel:: schema:: { ArrayType , DataType , MapType , StructField , StructType } ;
501576 use rstest:: rstest;
@@ -1041,6 +1116,55 @@ mod tests {
10411116 assert ! ( err. contains( expected_message) , "{err}" ) ;
10421117 }
10431118
1119+ #[ test]
1120+ fn map_to_struct_with_timezone_lowers_to_kernel_udf ( ) {
1121+ let target =
1122+ StructType :: try_new ( [ StructField :: nullable ( "ts" , DataType :: TIMESTAMP ) ] ) . unwrap ( ) ;
1123+ let map = KernelExpr :: map_to_struct_with_options (
1124+ col ! ( "pv" ) ,
1125+ MapToStructOptions :: default ( ) . with_timestamp_timezone ( "America/Los_Angeles" ) ,
1126+ ) ;
1127+ assert_eq ! (
1128+ to_df_expr( & map, & pv_map_schema( ) , Some ( & DataType :: from( target) ) )
1129+ . unwrap( )
1130+ . to_string( ) ,
1131+ "kernel_map_to_struct_with_timestamp_timezone(pv)"
1132+ ) ;
1133+ }
1134+
1135+ #[ test]
1136+ fn map_to_struct_with_timezone_executes_in_reader_timezone ( ) {
1137+ let mut maps = MapBuilder :: new ( None , StringBuilder :: new ( ) , StringBuilder :: new ( ) ) ;
1138+ maps. keys ( ) . append_value ( "ts" ) ;
1139+ maps. values ( ) . append_value ( "2024-06-15 09:30:00" ) ;
1140+ maps. append ( true ) . unwrap ( ) ;
1141+ let map = Arc :: new ( maps. finish ( ) ) as ArrayRef ;
1142+ let arrow_schema =
1143+ ArrowSchema :: new ( vec ! [ ArrowField :: new( "pv" , map. data_type( ) . clone( ) , true ) ] ) ;
1144+ let batch = RecordBatch :: try_new ( Arc :: new ( arrow_schema. clone ( ) ) , vec ! [ map] ) . unwrap ( ) ;
1145+ let target =
1146+ StructType :: try_new ( [ StructField :: nullable ( "ts" , DataType :: TIMESTAMP ) ] ) . unwrap ( ) ;
1147+ let logical = to_df_expr (
1148+ & KernelExpr :: map_to_struct_with_options (
1149+ col ! ( "pv" ) ,
1150+ MapToStructOptions :: default ( ) . with_timestamp_timezone ( "America/Los_Angeles" ) ,
1151+ ) ,
1152+ & pv_map_schema ( ) ,
1153+ Some ( & DataType :: from ( target) ) ,
1154+ )
1155+ . unwrap ( ) ;
1156+ let df_schema = DFSchema :: try_from ( arrow_schema) . unwrap ( ) ;
1157+ let physical = create_physical_expr ( & logical, & df_schema, & ExecutionProps :: new ( ) ) . unwrap ( ) ;
1158+ let result = physical. evaluate ( & batch) . unwrap ( ) . into_array ( 1 ) . unwrap ( ) ;
1159+ let timestamps = result
1160+ . as_struct ( )
1161+ . column ( 0 )
1162+ . as_any ( )
1163+ . downcast_ref :: < TimestampMicrosecondArray > ( )
1164+ . unwrap ( ) ;
1165+ assert_eq ! ( timestamps. value( 0 ) , 1_718_469_000_000_000 ) ;
1166+ }
1167+
10441168 // === ParseJson Shared Helpers ===
10451169
10461170 /// Input schema for JSON tests: `{ j: string }`.
0 commit comments