Skip to content

Commit d90ec03

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

8 files changed

Lines changed: 387 additions & 141 deletions

File tree

datafusion-executor/src/expression.rs

Lines changed: 232 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: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ 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,13 +702,22 @@ fn visit_expression_impl(
696702
schema_handle
697703
);
698704
}
699-
Expression::MapToStruct(map_to_struct) if !can_visit_map_to_struct(map_to_struct) => {
700-
visit_unknown(visitor, sibling_list_id, "configured_map_to_struct")
701-
}
702-
Expression::MapToStruct(MapToStructExpression { map_expr, .. }) => {
705+
Expression::MapToStruct(MapToStructExpression { map_expr, options }) => {
703706
let child_list_id = call!(visitor, make_field_list, 1);
704707
visit_expression_impl(visitor, map_expr, child_list_id);
705-
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+
);
706721
}
707722
// TODO(#2975): Add a dedicated visitor callback for cast expressions.
708723
Expression::Cast(cast) => visit_unknown(
@@ -714,10 +729,6 @@ fn visit_expression_impl(
714729
}
715730
}
716731

717-
fn can_visit_map_to_struct(map_to_struct: &MapToStructExpression) -> bool {
718-
map_to_struct.options.is_default()
719-
}
720-
721732
fn visit_predicate_impl(
722733
visitor: &mut EngineExpressionVisitor,
723734
predicate: &Predicate,
@@ -805,6 +816,7 @@ mod tests {
805816
MapToStruct {
806817
sibling_list_id: usize,
807818
child_list_id: usize,
819+
timestamp_timezone: Option<String>,
808820
},
809821
}
810822

@@ -821,6 +833,22 @@ mod tests {
821833
list_id
822834
}
823835

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+
824852
extern "C" fn visit_literal_interval_year_month(
825853
data: *mut c_void,
826854
sibling_list_id: usize,
@@ -875,17 +903,6 @@ mod tests {
875903
});
876904
}
877905

878-
extern "C" fn visit_map_to_struct(
879-
data: *mut c_void,
880-
sibling_list_id: usize,
881-
child_list_id: usize,
882-
) {
883-
let builder = unsafe { &mut *(data as *mut TestExpressionBuilder) };
884-
builder.events.push(LiteralEvent::MapToStruct {
885-
sibling_list_id,
886-
child_list_id,
887-
});
888-
}
889906
macro_rules! ignore_fn {
890907
($fn_name:ident $(, $arg_type:ty)*) => {
891908
extern "C" fn $fn_name(
@@ -1032,33 +1049,15 @@ mod tests {
10321049
assert_eq!(builder.events, vec![expected]);
10331050
}
10341051

1035-
#[test]
1036-
fn timezone_aware_map_to_struct_visits_unknown() {
1037-
let expression = Expression::map_to_struct(
1038-
Expression::column(["partitionValues"]),
1039-
MapToStructOptions::default().with_timestamp_timezone("America/Los_Angeles"),
1040-
);
1041-
let mut builder = TestExpressionBuilder::default();
1042-
let mut visitor = test_visitor(&mut builder);
1043-
1044-
let top_level_id = visit_expression_internal(&expression, &mut visitor);
1045-
1046-
assert_eq!(top_level_id, 0);
1047-
assert_eq!(
1048-
builder.events,
1049-
vec![LiteralEvent::Unknown {
1050-
sibling_list_id: 0,
1051-
name: "configured_map_to_struct".to_string(),
1052-
}]
1053-
);
1054-
}
1055-
1056-
#[test]
1057-
fn default_map_to_struct_visits_child_then_map_to_struct() {
1058-
let expression = Expression::map_to_struct(
1059-
Expression::column(["partitionValues"]),
1060-
MapToStructOptions::default(),
1061-
);
1052+
#[rstest]
1053+
#[case::default(None)]
1054+
#[case::configured(Some("America/Los_Angeles"))]
1055+
fn map_to_struct_visits_options(#[case] timestamp_timezone: Option<&str>) {
1056+
let options = timestamp_timezone.map_or_else(MapToStructOptions::default, |timezone| {
1057+
MapToStructOptions::default().with_timestamp_timezone(timezone)
1058+
});
1059+
let expression =
1060+
Expression::map_to_struct(Expression::column(["partitionValues"]), options);
10621061
let mut builder = TestExpressionBuilder::default();
10631062
let mut visitor = test_visitor(&mut builder);
10641063

@@ -1075,7 +1074,8 @@ mod tests {
10751074
LiteralEvent::MapToStruct {
10761075
sibling_list_id: 0,
10771076
child_list_id: 1,
1078-
},
1077+
timestamp_timezone: timestamp_timezone.map(str::to_string),
1078+
}
10791079
]
10801080
);
10811081
}

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 have a non-null pointer to a readable buffer of its
698+
/// declared number of initialized bytes 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: 8 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
];
@@ -256,6 +260,10 @@ pub unsafe extern "C" fn get_simple_testing_kernel_expression() -> Handle<Shared
256260
Expr::binary(BinaryExpressionOp::Divide, lit(100), lit(4)),
257261
Expr::struct_from([lit(1_i32), lit(2_i64), lit(3.0_f64)]),
258262
Expr::map_to_struct(col!("partitionValues"), MapToStructOptions::default()),
263+
Expr::map_to_struct(
264+
col!("partitionValues"),
265+
MapToStructOptions::default().with_timestamp_timezone("America/Los_Angeles"),
266+
),
259267
];
260268
Arc::new(Expr::struct_from(sub_exprs)).into()
261269
}

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

Lines changed: 4 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)
@@ -216,6 +218,8 @@ StructExpression
216218
Double(3.000000)
217219
MapToStruct
218220
Column(partitionValues)
221+
MapToStruct(timestamp_timezone=America/Los_Angeles)
222+
Column(partitionValues)
219223

220224
=== Expression Round-trip Test ===
221225
SUCCESS: Round-trip expression matches original!

0 commit comments

Comments
 (0)