Skip to content

Commit ac1ebc3

Browse files
committed
feat!: support map-to-struct options in engine adapters
1 parent 1dcd079 commit ac1ebc3

8 files changed

Lines changed: 398 additions & 115 deletions

File tree

datafusion-executor/src/expression.rs

Lines changed: 231 additions & 78 deletions
Large diffs are not rendered by default.

ffi/examples/visit-expression/engine_to_kernel_expression.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,17 @@ uintptr_t convert_engine_to_kernel_expression_item(
253253
assert(m2s->child_expr.len == 1);
254254
uintptr_t child = convert_engine_to_kernel_expression_item(
255255
state, m2s->child_expr.list[0]);
256-
return visit_expression_map_to_struct(state, child);
256+
struct OptionalValueKernelStringSlice timestamp_timezone = {
257+
.tag = NoneKernelStringSlice
258+
};
259+
if (m2s->timestamp_timezone != NULL) {
260+
timestamp_timezone.tag = SomeKernelStringSlice;
261+
timestamp_timezone.some = (struct KernelStringSlice) {
262+
.ptr = m2s->timestamp_timezone,
263+
.len = strlen(m2s->timestamp_timezone)
264+
};
265+
}
266+
return visit_expression_map_to_struct(state, child, timestamp_timezone);
257267
}
258268
case StructPatch:
259269
case FieldPatch:

ffi/examples/visit-expression/expression.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ struct Column {
131131
};
132132
struct MapToStructExpr {
133133
ExpressionItemList child_expr;
134+
char* timestamp_timezone;
134135
};
135136
struct BinaryData {
136137
uint8_t* buf;
@@ -428,9 +429,13 @@ void visit_unknown(void *data, uintptr_t sibling_list_id, struct KernelStringSli
428429

429430
void visit_map_to_struct_expr(void* data,
430431
uintptr_t sibling_list_id,
431-
uintptr_t child_list_id) {
432+
uintptr_t child_list_id,
433+
struct OptionalValueKernelStringSlice timestamp_timezone) {
432434
struct MapToStructExpr* m2s = malloc(sizeof(struct MapToStructExpr));
433435
m2s->child_expr = get_expr_list(data, child_list_id);
436+
m2s->timestamp_timezone = timestamp_timezone.tag == SomeKernelStringSlice
437+
? allocate_string(timestamp_timezone.some)
438+
: NULL;
434439
put_expr_item(data, sibling_list_id, m2s, MapToStruct);
435440
}
436441

@@ -729,6 +734,7 @@ void free_expression_item(ExpressionItem ref) {
729734
case MapToStruct: {
730735
struct MapToStructExpr* m2s = ref.ref;
731736
free_expression_list(m2s->child_expr);
737+
free(m2s->timestamp_timezone);
732738
free(m2s);
733739
break;
734740
}

ffi/examples/visit-expression/expression_print.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,11 @@ void print_tree_helper(ExpressionItem ref, int depth) {
295295
}
296296
case MapToStruct: {
297297
struct MapToStructExpr* m2s = ref.ref;
298-
printf("MapToStruct\n");
298+
if (m2s->timestamp_timezone == NULL) {
299+
printf("MapToStruct\n");
300+
} else {
301+
printf("MapToStruct(timestamp_timezone=%s)\n", m2s->timestamp_timezone);
302+
}
299303
print_expression_item_list(m2s->child_expr, depth + 1);
300304
break;
301305
}

ffi/src/expressions/engine_visitor.rs

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ use std::ffi::c_void;
55
use delta_kernel::expressions::{
66
ArrayData, BinaryExpression, BinaryExpressionOp, BinaryPredicate, BinaryPredicateOp,
77
ColumnName, Expression, ExpressionRef, ExpressionStructPatch, JunctionPredicate,
8-
JunctionPredicateOp, MapData, MapToStructExpression, MapToStructOptions, OpaqueExpression,
9-
OpaqueExpressionOpRef, OpaquePredicate, OpaquePredicateOpRef, ParseJsonExpression, Predicate,
10-
Scalar, StructData, UnaryExpression, UnaryExpressionOp, UnaryPredicate, UnaryPredicateOp,
11-
VariadicExpression, VariadicExpressionOp,
8+
JunctionPredicateOp, MapData, MapToStructExpression, OpaqueExpression, OpaqueExpressionOpRef,
9+
OpaquePredicate, OpaquePredicateOpRef, ParseJsonExpression, Predicate, Scalar, StructData,
10+
UnaryExpression, UnaryExpressionOp, UnaryPredicate, UnaryPredicateOp, VariadicExpression,
11+
VariadicExpressionOp,
1212
};
1313

1414
use super::kernel_visitor::NullTypeTag;
1515
use crate::expressions::{
1616
SharedExpression, SharedOpaqueExpressionOp, SharedOpaquePredicateOp, SharedPredicate,
1717
};
1818
use crate::handle::Handle;
19-
use crate::{kernel_string_slice, KernelStringSlice, SharedSchema};
19+
use crate::{kernel_string_slice, KernelStringSlice, OptionalValue, SharedSchema};
2020

2121
type VisitLiteralFn<T> = extern "C" fn(data: *mut c_void, sibling_list_id: usize, value: T);
2222
type VisitUnaryFn = extern "C" fn(data: *mut c_void, sibling_list_id: usize, child_list_id: usize);
@@ -31,6 +31,12 @@ type VisitParseJsonFn = extern "C" fn(
3131
child_list_id: usize,
3232
output_schema: Handle<SharedSchema>,
3333
);
34+
type VisitMapToStructFn = extern "C" fn(
35+
data: *mut c_void,
36+
sibling_list_id: usize,
37+
child_list_id: usize,
38+
timestamp_timezone: OptionalValue<KernelStringSlice>,
39+
);
3440
type VisitColumnFn = extern "C" fn(
3541
data: *mut c_void,
3642
sibling_list_id: usize,
@@ -174,10 +180,10 @@ pub struct EngineExpressionVisitor {
174180
/// `child_list_id`. The `output_schema` handle specifies the schema to parse the JSON
175181
/// into.
176182
pub visit_parse_json: VisitParseJsonFn,
177-
/// Visits a `MapToStruct` expression with default options. Expressions with configured options
178-
/// are reported through `visit_unknown` without visiting the child expression. The
179-
/// sub-expression is in the one-item list identified by `child_list_id`.
180-
pub visit_map_to_struct: VisitUnaryFn,
183+
/// Visits a `MapToStruct` expression. The sub-expression is in the one-item list identified by
184+
/// `child_list_id`. `timestamp_timezone` carries the configured reader timezone, or is `None`
185+
/// when the expression uses the default UTC interpretation.
186+
pub visit_map_to_struct: VisitMapToStructFn,
181187
/// Visits the `LessThan` binary operator belonging to the list identified by
182188
/// `sibling_list_id`. The operands will be in a _two_ item list identified by
183189
/// `child_list_id`
@@ -696,15 +702,22 @@ fn visit_expression_impl(
696702
schema_handle
697703
);
698704
}
699-
Expression::MapToStruct(map_to_struct)
700-
if map_to_struct.options != MapToStructOptions::default() =>
701-
{
702-
visit_unknown(visitor, sibling_list_id, "map_to_struct")
703-
}
704-
Expression::MapToStruct(MapToStructExpression { map_expr, .. }) => {
705+
Expression::MapToStruct(MapToStructExpression { map_expr, options }) => {
705706
let child_list_id = call!(visitor, make_field_list, 1);
706707
visit_expression_impl(visitor, map_expr, child_list_id);
707-
call!(visitor, visit_map_to_struct, sibling_list_id, child_list_id);
708+
let timestamp_timezone = match options.timestamp_timezone() {
709+
Some(timestamp_timezone) => {
710+
OptionalValue::Some(kernel_string_slice!(timestamp_timezone))
711+
}
712+
None => OptionalValue::None,
713+
};
714+
call!(
715+
visitor,
716+
visit_map_to_struct,
717+
sibling_list_id,
718+
child_list_id,
719+
timestamp_timezone
720+
);
708721
}
709722
// TODO(#2975): Add a dedicated visitor callback for cast expressions.
710723
Expression::Cast(cast) => visit_unknown(
@@ -800,6 +813,11 @@ mod tests {
800813
sibling_list_id: usize,
801814
name: String,
802815
},
816+
MapToStruct {
817+
sibling_list_id: usize,
818+
child_list_id: usize,
819+
timestamp_timezone: Option<String>,
820+
},
803821
}
804822

805823
#[derive(Default)]
@@ -815,6 +833,22 @@ mod tests {
815833
list_id
816834
}
817835

836+
extern "C" fn visit_map_to_struct(
837+
data: *mut c_void,
838+
sibling_list_id: usize,
839+
child_list_id: usize,
840+
timestamp_timezone: OptionalValue<KernelStringSlice>,
841+
) {
842+
let builder = unsafe { &mut *(data as *mut TestExpressionBuilder) };
843+
let timestamp_timezone = Option::from(timestamp_timezone)
844+
.map(|timezone| unsafe { String::try_from_slice(&timezone).unwrap() });
845+
builder.events.push(LiteralEvent::MapToStruct {
846+
sibling_list_id,
847+
child_list_id,
848+
timestamp_timezone,
849+
});
850+
}
851+
818852
extern "C" fn visit_literal_interval_year_month(
819853
data: *mut c_void,
820854
sibling_list_id: usize,
@@ -928,7 +962,7 @@ mod tests {
928962
visit_is_null: ignore_child_list,
929963
visit_to_json: ignore_child_list,
930964
visit_parse_json: ignore_parse_json,
931-
visit_map_to_struct: ignore_child_list,
965+
visit_map_to_struct,
932966
visit_lt: ignore_child_list,
933967
visit_gt: ignore_child_list,
934968
visit_eq: ignore_child_list,
@@ -1014,12 +1048,15 @@ mod tests {
10141048
assert_eq!(builder.events, vec![expected]);
10151049
}
10161050

1017-
#[test]
1018-
fn timezone_aware_map_to_struct_visits_unknown() {
1019-
let expression = Expression::map_to_struct(
1020-
Expression::column(["partitionValues"]),
1021-
MapToStructOptions::default().with_timestamp_timezone("America/Los_Angeles"),
1022-
);
1051+
#[rstest]
1052+
#[case::default(None)]
1053+
#[case::configured(Some("America/Los_Angeles"))]
1054+
fn map_to_struct_visits_options(#[case] timestamp_timezone: Option<&str>) {
1055+
let options = timestamp_timezone.map_or_else(MapToStructOptions::default, |timezone| {
1056+
MapToStructOptions::default().with_timestamp_timezone(timezone)
1057+
});
1058+
let expression =
1059+
Expression::map_to_struct(Expression::column(["partitionValues"]), options);
10231060
let mut builder = TestExpressionBuilder::default();
10241061
let mut visitor = test_visitor(&mut builder);
10251062

@@ -1028,10 +1065,17 @@ mod tests {
10281065
assert_eq!(top_level_id, 0);
10291066
assert_eq!(
10301067
builder.events,
1031-
vec![LiteralEvent::Unknown {
1032-
sibling_list_id: 0,
1033-
name: "map_to_struct".to_string(),
1034-
}]
1068+
vec![
1069+
LiteralEvent::Column {
1070+
sibling_list_id: 1,
1071+
parts: vec!["partitionValues".to_string()],
1072+
},
1073+
LiteralEvent::MapToStruct {
1074+
sibling_list_id: 0,
1075+
child_list_id: 1,
1076+
timestamp_timezone: timestamp_timezone.map(str::to_string),
1077+
}
1078+
]
10351079
);
10361080
}
10371081
}

ffi/src/expressions/kernel_visitor.rs

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::handle::Handle;
2020
use crate::scan::{EngineExpression, EnginePredicate};
2121
use crate::{
2222
AllocateErrorFn, EngineIterator, ExternResult, IntoExternResult, KernelStringSlice,
23-
ReferenceSet, TryFromStringSlice,
23+
OptionalValue, ReferenceSet, TryFromStringSlice,
2424
};
2525

2626
pub(crate) enum ExpressionOrPredicate {
@@ -685,17 +685,34 @@ pub extern "C" fn visit_expression_struct(
685685
wrap_expression(state, Expression::struct_from(exprs))
686686
}
687687

688-
/// Visit a MapToStruct expression. The `child_expr` is the map expression.
688+
/// Builds a `MapToStruct` expression from its map child and optional reader timezone.
689+
///
690+
/// `timestamp_timezone` is `None` for the default UTC interpretation. A provided string is copied
691+
/// into the expression before this function returns.
692+
///
693+
/// Returns zero when `child_expr` is invalid or the timezone is not valid UTF-8.
694+
///
695+
/// # Safety
696+
///
697+
/// A provided `timestamp_timezone` slice must point to its declared number of initialized bytes
698+
/// and remain valid for this call.
689699
#[no_mangle]
690-
pub extern "C" fn visit_expression_map_to_struct(
700+
pub unsafe extern "C" fn visit_expression_map_to_struct(
691701
state: &mut KernelExpressionVisitorState,
692702
child_expr: usize,
703+
timestamp_timezone: OptionalValue<KernelStringSlice>,
693704
) -> usize {
705+
let options = match Option::from(timestamp_timezone) {
706+
Some(timestamp_timezone) => match unsafe { String::try_from_slice(&timestamp_timezone) } {
707+
Ok(timestamp_timezone) => {
708+
MapToStructOptions::default().with_timestamp_timezone(timestamp_timezone)
709+
}
710+
Err(_) => return 0,
711+
},
712+
None => MapToStructOptions::default(),
713+
};
694714
unwrap_kernel_expression(state, child_expr).map_or(0, |expr| {
695-
wrap_expression(
696-
state,
697-
Expression::map_to_struct(expr, MapToStructOptions::default()),
698-
)
715+
wrap_expression(state, Expression::map_to_struct(expr, options))
699716
})
700717
}
701718

@@ -871,6 +888,49 @@ mod tests {
871888

872889
use super::*;
873890

891+
#[rstest]
892+
#[case::default(None)]
893+
#[case::configured(Some("America/Los_Angeles"))]
894+
fn map_to_struct_preserves_options(#[case] timestamp_timezone: Option<&str>) {
895+
let mut state = KernelExpressionVisitorState::default();
896+
let child = wrap_expression(&mut state, col!("partitionValues"));
897+
let ffi_timezone = match timestamp_timezone {
898+
Some(timestamp_timezone) => {
899+
OptionalValue::Some(crate::kernel_string_slice!(timestamp_timezone))
900+
}
901+
None => OptionalValue::None,
902+
};
903+
904+
let expression_id =
905+
unsafe { visit_expression_map_to_struct(&mut state, child, ffi_timezone) };
906+
let expression = unwrap_kernel_expression(&mut state, expression_id).unwrap();
907+
let options = timestamp_timezone.map_or_else(MapToStructOptions::default, |timezone| {
908+
MapToStructOptions::default().with_timestamp_timezone(timezone)
909+
});
910+
911+
assert_eq!(
912+
expression,
913+
Expression::map_to_struct(col!("partitionValues"), options)
914+
);
915+
}
916+
917+
#[test]
918+
fn map_to_struct_rejects_invalid_timezone_utf8() {
919+
let mut state = KernelExpressionVisitorState::default();
920+
let child = wrap_expression(&mut state, col!("partitionValues"));
921+
let invalid_utf8 = [0xff_u8];
922+
let timezone = KernelStringSlice {
923+
ptr: invalid_utf8.as_ptr().cast(),
924+
len: invalid_utf8.len(),
925+
};
926+
927+
let expression_id = unsafe {
928+
visit_expression_map_to_struct(&mut state, child, OptionalValue::Some(timezone))
929+
};
930+
931+
assert_eq!(expression_id, 0);
932+
}
933+
874934
// ============================================================================
875935
// NullTypeTag::from_data_type
876936
// ============================================================================

ffi/src/test_ffi.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ pub unsafe extern "C" fn get_testing_kernel_expression() -> Handle<SharedExpress
160160
Expr::opaque(OpaqueTestOp("foo".to_string()), vec![lit(42), lit(1.111)]),
161161
Expr::unknown("mystery"),
162162
Expr::map_to_struct(col!("pv"), MapToStructOptions::default()),
163+
Expr::map_to_struct(
164+
col!("pv"),
165+
MapToStructOptions::default().with_timestamp_timezone("America/Los_Angeles"),
166+
),
163167
Expr::coalesce([col!("col"), lit(0_i32)]),
164168
Expr::array([lit(1_i32), lit(2_i32)]),
165169
];

ffi/tests/test-expression-visitor/expected.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ StructExpression
103103
Unknown(mystery)
104104
MapToStruct
105105
Column(pv)
106+
MapToStruct(timestamp_timezone=America/Los_Angeles)
107+
Column(pv)
106108
Coalesce
107109
Column(col)
108110
Integer(0)

0 commit comments

Comments
 (0)