Skip to content

Commit f545df3

Browse files
committed
feat!: add element-at expression
1 parent 0994bf4 commit f545df3

9 files changed

Lines changed: 525 additions & 13 deletions

File tree

datafusion-executor/src/expression.rs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use delta_kernel::engine::arrow_data::ArrowEngineData;
1919
use delta_kernel::engine::arrow_expression::evaluate_expression as kernel_expression;
2020
use delta_kernel::engine::parse_json;
2121
use 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

ffi/src/expressions/engine_visitor.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,9 @@ fn visit_expression_impl(
693693
visit_expression_impl(visitor, map_expr, child_list_id);
694694
call!(visitor, visit_map_to_struct, sibling_list_id, child_list_id);
695695
}
696+
// The FFI expression visitor has no ElementAt callback. Surface an explicit unknown node
697+
// so engines cannot silently interpret it as another expression.
698+
Expression::ElementAt(_) => visit_unknown(visitor, sibling_list_id, "element_at"),
696699
// TODO(#2975): Add a dedicated visitor callback for cast expressions.
697700
Expression::Cast(cast) => visit_unknown(
698701
visitor,
@@ -984,4 +987,25 @@ mod tests {
984987
}]
985988
);
986989
}
990+
991+
#[test]
992+
fn element_at_visits_unknown() {
993+
let expression = Expression::element_at(
994+
Expression::column(["partitionValues"]),
995+
Expression::literal("key"),
996+
);
997+
let mut builder = TestExpressionBuilder::default();
998+
let mut visitor = test_visitor(&mut builder);
999+
1000+
let top_level_id = visit_expression_internal(&expression, &mut visitor);
1001+
1002+
assert_eq!(top_level_id, 0);
1003+
assert_eq!(
1004+
builder.events,
1005+
vec![LiteralEvent::Unknown {
1006+
sibling_list_id: 0,
1007+
name: "element_at".to_string(),
1008+
}]
1009+
);
1010+
}
9871011
}

kernel/proto/expressions.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ message MapToStructExpression {
161161
MapToStructOptions options = 2;
162162
}
163163

164+
message ElementAtExpression {
165+
Expression map_expr = 1;
166+
Expression key_expr = 2;
167+
}
168+
164169
// `nullability_predicate` is optional: when set and it evaluates to false/null, the whole
165170
// struct is null.
166171
message StructExpression {
@@ -246,5 +251,6 @@ message Expression {
246251
ParseJsonExpression parse_json = 11;
247252
MapToStructExpression map_to_struct = 12;
248253
string unknown = 13;
254+
ElementAtExpression element_at = 14;
249255
}
250256
}

0 commit comments

Comments
 (0)