@@ -19,7 +19,7 @@ use delta_kernel::engine::arrow_data::ArrowEngineData;
1919use delta_kernel:: engine:: arrow_expression:: evaluate_expression as kernel_expression;
2020use delta_kernel:: engine:: parse_json;
2121use delta_kernel:: expressions:: {
22- BinaryExpression , BinaryExpressionOp , ColumnName as KernelColumnName ,
22+ BinaryExpression , BinaryExpressionOp , ColumnName as KernelColumnName , ElementAtExpression ,
2323 Expression as KernelExpression , ExpressionRef , ExpressionStructPatch , MapToStructExpression ,
2424 MapToStructOptions , ParseJsonExpression , UnaryExpressionOp , VariadicExpression ,
2525 VariadicExpressionOp ,
@@ -69,6 +69,7 @@ pub fn to_df_expr(
6969 KernelExpression :: MapToStruct ( map_to_struct) => {
7070 map_to_struct_to_df_expr ( map_to_struct, input_schema, output_type)
7171 }
72+ KernelExpression :: ElementAt ( element_at) => element_at_to_df_expr ( element_at, input_schema) ,
7273 KernelExpression :: ParseJson ( parse) => parse_json_to_df_expr ( parse, input_schema) ,
7374
7475 KernelExpression :: Unary ( u) => match u. op {
@@ -91,6 +92,72 @@ pub fn to_df_expr(
9192 }
9293}
9394
95+ /// Lowers an `ElementAt` map lookup to a UDF backed by kernel's Arrow evaluator.
96+ ///
97+ /// # Errors
98+ /// Returns an error when either child expression cannot be converted.
99+ fn element_at_to_df_expr (
100+ element_at : & ElementAtExpression ,
101+ input_schema : & StructType ,
102+ ) -> DeltaResult < DFExpr > {
103+ let map = to_df_expr ( & element_at. map_expr , input_schema, None ) ?;
104+ let key = to_df_expr (
105+ & element_at. key_expr ,
106+ input_schema,
107+ Some ( & KernelDataType :: STRING ) ,
108+ ) ?;
109+ Ok ( ScalarUDF :: new_from_impl ( ElementAtUdf :: new ( ) ) . call ( vec ! [ map, key] ) )
110+ }
111+
112+ /// A DataFusion scalar UDF that delegates `ElementAt` evaluation to kernel, preserving dynamic-key
113+ /// and rightmost-duplicate semantics across executors.
114+ #[ derive( Debug , PartialEq , Eq , Hash ) ]
115+ struct ElementAtUdf {
116+ signature : Signature ,
117+ }
118+
119+ impl ElementAtUdf {
120+ fn new ( ) -> Self {
121+ Self {
122+ signature : Signature :: any ( 2 , Volatility :: Immutable ) ,
123+ }
124+ }
125+ }
126+
127+ impl ScalarUDFImpl for ElementAtUdf {
128+ fn name ( & self ) -> & str {
129+ "kernel_element_at"
130+ }
131+
132+ fn signature ( & self ) -> & Signature {
133+ & self . signature
134+ }
135+
136+ fn return_type ( & self , _arg_types : & [ ArrowDataType ] ) -> Result < ArrowDataType , DataFusionError > {
137+ Ok ( ArrowDataType :: Utf8 )
138+ }
139+
140+ fn invoke_with_args ( & self , args : ScalarFunctionArgs ) -> Result < ColumnarValue , DataFusionError > {
141+ let num_rows = args. number_rows ;
142+ let [ map, key] = take_function_args ( self . name ( ) , args. args ) ?;
143+ let batch = RecordBatch :: try_from_iter ( [
144+ ( "map" , map. into_array ( num_rows) ?) ,
145+ ( "key" , key. into_array ( num_rows) ?) ,
146+ ] ) ?;
147+ let expression = KernelExpression :: element_at (
148+ KernelExpression :: column ( [ "map" ] ) ,
149+ KernelExpression :: column ( [ "key" ] ) ,
150+ ) ;
151+ let result = kernel_expression:: evaluate_expression (
152+ & expression,
153+ & batch,
154+ Some ( & KernelDataType :: STRING ) ,
155+ )
156+ . map_err ( |e| DataFusionError :: External ( Box :: new ( e) ) ) ?;
157+ Ok ( ColumnarValue :: Array ( result) )
158+ }
159+ }
160+
94161/// Lowers a column reference to a nested field access, e.g. `a.b.c` becomes a single
95162/// `get_field(col("a"), "b", "c")` call. The path is resolved against `input_schema` (via
96163/// [`StructType::field_at`]) to fail fast, but the resolved field is otherwise unused.
@@ -1053,6 +1120,69 @@ mod tests {
10531120 . to_string ( )
10541121 }
10551122
1123+ #[ test]
1124+ fn element_at_lowers_to_kernel_udf ( ) {
1125+ let kernel = KernelExpr :: element_at ( col ! ( "pv" ) , lit ( "region" ) ) ;
1126+ let rendered = to_df_expr ( & kernel, & pv_map_schema ( ) , Some ( & DataType :: STRING ) )
1127+ . unwrap ( )
1128+ . to_string ( ) ;
1129+ assert_eq ! ( rendered, "kernel_element_at(pv, Utf8(\" region\" ))" ) ;
1130+ }
1131+
1132+ #[ test]
1133+ fn element_at_executes_dynamic_keys_and_rightmost_duplicates ( ) {
1134+ let mut maps = MapBuilder :: new ( None , StringBuilder :: new ( ) , StringBuilder :: new ( ) ) ;
1135+ maps. keys ( ) . append_value ( "region" ) ;
1136+ maps. values ( ) . append_value ( "first" ) ;
1137+ maps. keys ( ) . append_value ( "region" ) ;
1138+ maps. values ( ) . append_value ( "last" ) ;
1139+ maps. append ( true ) . unwrap ( ) ;
1140+ maps. keys ( ) . append_value ( "a" ) ;
1141+ maps. values ( ) . append_value ( "A" ) ;
1142+ maps. keys ( ) . append_value ( "b" ) ;
1143+ maps. values ( ) . append_value ( "B" ) ;
1144+ maps. append ( true ) . unwrap ( ) ;
1145+ maps. append ( false ) . unwrap ( ) ;
1146+ maps. keys ( ) . append_value ( "a" ) ;
1147+ maps. values ( ) . append_value ( "A" ) ;
1148+ maps. append ( true ) . unwrap ( ) ;
1149+
1150+ let map = Arc :: new ( maps. finish ( ) ) as ArrayRef ;
1151+ let key = Arc :: new ( StringArray :: from ( vec ! [
1152+ Some ( "region" ) ,
1153+ Some ( "b" ) ,
1154+ Some ( "a" ) ,
1155+ None ,
1156+ ] ) ) as ArrayRef ;
1157+ let logical = to_df_expr (
1158+ & KernelExpr :: element_at ( col ! ( "pv" ) , col ! ( "key" ) ) ,
1159+ & StructType :: try_new ( [
1160+ StructField :: nullable ( "pv" , MapType :: new ( DataType :: STRING , DataType :: STRING , true ) ) ,
1161+ StructField :: nullable ( "key" , DataType :: STRING ) ,
1162+ ] )
1163+ . unwrap ( ) ,
1164+ Some ( & DataType :: STRING ) ,
1165+ )
1166+ . unwrap ( ) ;
1167+ let input_schema = ArrowSchema :: new ( vec ! [
1168+ ArrowField :: new( "pv" , map. data_type( ) . clone( ) , true ) ,
1169+ ArrowField :: new( "key" , ArrowDataType :: Utf8 , true ) ,
1170+ ] ) ;
1171+ let batch = RecordBatch :: try_new ( Arc :: new ( input_schema. clone ( ) ) , vec ! [ map, key] ) . unwrap ( ) ;
1172+ let df_schema = DFSchema :: try_from ( input_schema) . unwrap ( ) ;
1173+ let physical = create_physical_expr ( & logical, & df_schema, & ExecutionProps :: new ( ) ) . unwrap ( ) ;
1174+ let result = physical
1175+ . evaluate ( & batch)
1176+ . unwrap ( )
1177+ . into_array ( batch. num_rows ( ) )
1178+ . unwrap ( ) ;
1179+ let result = result. as_string :: < i32 > ( ) ;
1180+ assert_eq ! (
1181+ result,
1182+ & StringArray :: from( vec![ Some ( "last" ) , Some ( "B" ) , None , None ] )
1183+ ) ;
1184+ }
1185+
10561186 /// Each target field extracts its value with `cast(get_field(pv, name), T)`, and the whole
10571187 /// rebuild is wrapped in a null-map guard. Runtime cast/parse semantics (empty-string,
10581188 /// temporal, decimal, duplicate keys, null masking) are arrow's, verified end-to-end rather
0 commit comments