Skip to content

Commit e01fd3b

Browse files
feat: new #[derive(IntoEngineData)] proc macro (delta-io#830)
## What changes are proposed in this pull request? With the introduction of `EvaluationHandler::create_one`, we can create `EngineData` now. This PR adds a handy new procedural macro to implement a new `IntoEngineData` trait which allows for `self.into_engine_data(schema, &dyn Engine)` as long as all fields implement `Into<Scalar>` (for now just support the simple case of all-primitive fields) This would typically be used in conjunction with `ToSchema` to provide the schema for the output `EngineData`. ### Future work Need to implement tests (make doctest on `IntoEngineData` trait actually build): lump that in to delta-io#514 1. need to rename `crate::` to `delta_kernel::` in macros 2. need to make all the types we need conditionally pub (that is, if `cfg(doctest)` then make stuff `pub`) 3. need to do an `extern crate self as delta_kernel` 4. and finally, make the doctest a `no_run` (building) doctest ## How was this change tested? new UT
1 parent 3fa9f0b commit e01fd3b

5 files changed

Lines changed: 167 additions & 18 deletions

File tree

derive-macros/src/lib.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,58 @@ fn gen_schema_fields(data: &Data) -> TokenStream {
138138
quote! { #(#schema_fields),* }
139139
}
140140

141+
/// Derive an IntoEngineData trait for a struct that has all fields implement `Into<Scalar>`.
142+
///
143+
/// This is a relatively simple macro to produce the boilerplate for converting a struct into
144+
/// EngineData using the `create_one` method. TODO: (doc)tests included in the delta_kernel crate:
145+
/// `IntoEngineData` trait.
146+
#[proc_macro_derive(IntoEngineData)]
147+
pub fn into_engine_data_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
148+
let input = parse_macro_input!(input as DeriveInput);
149+
let struct_name = &input.ident;
150+
151+
let Data::Struct(DataStruct {
152+
fields: Fields::Named(fields),
153+
..
154+
}) = &input.data
155+
else {
156+
return Error::new(
157+
struct_name.span(),
158+
"IntoEngineData can only be derived for structs with named fields",
159+
)
160+
.to_compile_error()
161+
.into();
162+
};
163+
164+
let fields = &fields.named;
165+
let field_idents = fields.iter().map(|f| &f.ident);
166+
let field_types = fields.iter().map(|f| &f.ty);
167+
168+
let expanded = quote! {
169+
#[automatically_derived]
170+
impl crate::IntoEngineData for #struct_name
171+
where
172+
#(#field_types: Into<crate::expressions::Scalar>),*
173+
{
174+
fn into_engine_data(
175+
self,
176+
schema: crate::schema::SchemaRef,
177+
engine: &dyn crate::Engine)
178+
-> crate::DeltaResult<Box<dyn crate::EngineData>> {
179+
// NB: we `use` here to avoid polluting the caller's namespace
180+
use crate::EvaluationHandlerExtension as _;
181+
let values = [
182+
#(self.#field_idents.into()),*
183+
];
184+
let evaluator = engine.evaluation_handler();
185+
evaluator.create_one(schema, &values)
186+
}
187+
}
188+
};
189+
190+
proc_macro::TokenStream::from(expanded)
191+
}
192+
141193
/// Mark items as `internal_api` to make them public iff the `internal-api` feature is enabled.
142194
/// Note this doesn't work for inline module definitions (see `internal_mod!` macro in delta_kernel
143195
/// crate - can't export macro_rules! from proc macro crate).

kernel/src/actions/mod.rs

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ use crate::table_features::{
1414
};
1515
use crate::table_properties::TableProperties;
1616
use crate::utils::require;
17-
use crate::EvaluationHandlerExtension;
18-
use crate::{DeltaResult, Engine, EngineData, Error, FileMeta, RowVisitor as _};
17+
use crate::{DeltaResult, EngineData, Error, FileMeta, RowVisitor as _};
1918

2019
use url::Url;
2120
use visitors::{MetadataVisitor, ProtocolVisitor};
2221

23-
use delta_kernel_derive::{internal_api, ToSchema};
22+
use delta_kernel_derive::{internal_api, IntoEngineData, ToSchema};
2423
use itertools::Itertools;
2524
use serde::{Deserialize, Serialize};
2625

@@ -597,7 +596,7 @@ pub(crate) struct Cdc {
597596
pub tags: Option<HashMap<String, String>>,
598597
}
599598

600-
#[derive(Debug, Clone, PartialEq, Eq, ToSchema)]
599+
#[derive(Debug, Clone, PartialEq, Eq, ToSchema, IntoEngineData)]
601600
#[internal_api]
602601
pub(crate) struct SetTransaction {
603602
/// A unique identifier for the application performing the transaction.
@@ -618,16 +617,6 @@ impl SetTransaction {
618617
last_updated,
619618
}
620619
}
621-
622-
pub(crate) fn into_engine_data(self, engine: &dyn Engine) -> DeltaResult<Box<dyn EngineData>> {
623-
let values = [
624-
self.app_id.into(),
625-
self.version.into(),
626-
self.last_updated.into(),
627-
];
628-
let evaluator = engine.evaluation_handler();
629-
evaluator.create_one(get_log_txn_schema().clone(), &values)
630-
}
631620
}
632621

633622
/// The sidecar action references a sidecar file which provides some of the checkpoint's
@@ -1125,4 +1114,79 @@ mod tests {
11251114
]);
11261115
assert_eq!(parse_features::<ReaderFeature>(features), expected);
11271116
}
1117+
1118+
#[test]
1119+
fn test_into_engine_data() {
1120+
use crate::arrow::array::{Int64Array, StringArray};
1121+
use crate::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1122+
use crate::arrow::record_batch::RecordBatch;
1123+
1124+
use crate::engine::arrow_data::ArrowEngineData;
1125+
use crate::engine::arrow_expression::ArrowEvaluationHandler;
1126+
use crate::IntoEngineData;
1127+
use crate::{Engine, EvaluationHandler, JsonHandler, ParquetHandler, StorageHandler};
1128+
1129+
// duplicated
1130+
struct ExprEngine(Arc<dyn EvaluationHandler>);
1131+
1132+
impl ExprEngine {
1133+
fn new() -> Self {
1134+
ExprEngine(Arc::new(ArrowEvaluationHandler))
1135+
}
1136+
}
1137+
1138+
impl Engine for ExprEngine {
1139+
fn evaluation_handler(&self) -> Arc<dyn EvaluationHandler> {
1140+
self.0.clone()
1141+
}
1142+
1143+
fn json_handler(&self) -> Arc<dyn JsonHandler> {
1144+
unimplemented!()
1145+
}
1146+
1147+
fn parquet_handler(&self) -> Arc<dyn ParquetHandler> {
1148+
unimplemented!()
1149+
}
1150+
1151+
fn storage_handler(&self) -> Arc<dyn StorageHandler> {
1152+
unimplemented!()
1153+
}
1154+
}
1155+
1156+
let engine = ExprEngine::new();
1157+
1158+
let set_transaction = SetTransaction {
1159+
app_id: "app_id".to_string(),
1160+
version: 0,
1161+
last_updated: None,
1162+
};
1163+
1164+
let engine_data =
1165+
set_transaction.into_engine_data(SetTransaction::to_schema().into(), &engine);
1166+
1167+
let record_batch: crate::arrow::array::RecordBatch = engine_data
1168+
.unwrap()
1169+
.into_any()
1170+
.downcast::<ArrowEngineData>()
1171+
.unwrap()
1172+
.into();
1173+
1174+
let schema = Arc::new(Schema::new(vec![
1175+
Field::new("appId", ArrowDataType::Utf8, false),
1176+
Field::new("version", ArrowDataType::Int64, false),
1177+
Field::new("lastUpdated", ArrowDataType::Int64, true),
1178+
]));
1179+
1180+
let expected = RecordBatch::try_new(
1181+
schema,
1182+
vec![
1183+
Arc::new(StringArray::from(vec!["app_id"])),
1184+
Arc::new(Int64Array::from(vec![0_i64])),
1185+
Arc::new(Int64Array::from(vec![None::<i64>])),
1186+
],
1187+
)
1188+
.unwrap();
1189+
1190+
assert_eq!(record_batch, expected);
1191+
}
11281192
}

kernel/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,39 @@ trait EvaluationHandlerExtension: EvaluationHandler {
432432
// Auto-implement the extension trait for all EvaluationHandlers
433433
impl<T: EvaluationHandler + ?Sized> EvaluationHandlerExtension for T {}
434434

435+
/// A trait that allows converting a type into (single-row) EngineData
436+
///
437+
/// This is typically used with the `#[derive(IntoEngineData)]` macro
438+
/// which leverages the traits `ToDataType` and `Into<Scalar>` for struct fields
439+
/// to convert a struct into EngineData.
440+
///
441+
/// # Example
442+
/// ```ignore
443+
/// # use std::sync::Arc;
444+
/// # use delta_kernel_derive::{Schema, IntoEngineData};
445+
///
446+
/// #[derive(Schema, IntoEngineData)]
447+
/// struct MyStruct {
448+
/// a: i32,
449+
/// b: String,
450+
/// }
451+
///
452+
/// let my_struct = MyStruct { a: 42, b: "Hello".to_string() };
453+
/// // typically used with ToSchema
454+
/// let schema = Arc::new(MyStruct::to_schema());
455+
/// // single-row EngineData
456+
/// let engine = todo!(); // create an engine
457+
/// let engine_data = my_struct.into_engine_data(schema, engine);
458+
/// ```
459+
pub(crate) trait IntoEngineData {
460+
/// Consume this type to produce a single-row EngineData using the provided schema.
461+
fn into_engine_data(
462+
self,
463+
schema: SchemaRef,
464+
engine: &dyn Engine,
465+
) -> DeltaResult<Box<dyn EngineData>>;
466+
}
467+
435468
/// Provides file system related functionalities to Delta Kernel.
436469
///
437470
/// Delta Kernel uses this handler whenever it needs to access the underlying

kernel/src/transaction.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ use std::time::{SystemTime, UNIX_EPOCH};
55

66
use crate::actions::SetTransaction;
77
use crate::actions::COMMIT_INFO_NAME;
8-
use crate::actions::{get_log_add_schema, get_log_commit_info_schema};
8+
use crate::actions::{get_log_add_schema, get_log_commit_info_schema, get_log_txn_schema};
99
use crate::error::Error;
1010
use crate::expressions::{column_expr, Scalar, StructData};
1111
use crate::path::ParsedLogPath;
1212
use crate::schema::{MapType, SchemaRef, StructField, StructType};
1313
use crate::snapshot::Snapshot;
14-
use crate::{DataType, DeltaResult, Engine, EngineData, Expression, Version};
14+
use crate::{DataType, DeltaResult, Engine, EngineData, Expression, IntoEngineData, Version};
1515

1616
use url::Url;
1717

@@ -132,7 +132,7 @@ impl Transaction {
132132
.set_transactions
133133
.clone()
134134
.into_iter()
135-
.map(|txn| txn.into_engine_data(engine));
135+
.map(|txn| txn.into_engine_data(get_log_txn_schema().clone(), engine));
136136

137137
// step one: construct the iterator of commit info + file actions we want to commit
138138
let engine_commit_info = self

kernel/tests/write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ async fn test_write_txn_actions() -> Result<(), Box<dyn std::error::Error>> {
741741
// commit!
742742
txn.commit(&engine)?;
743743

744-
let snapshot = Arc::new(table.snapshot(&engine, None)?);
744+
let snapshot = Arc::new(table.snapshot(&engine, 1.into())?);
745745
assert_eq!(
746746
snapshot.clone().get_app_id_version("app_id1", &engine)?,
747747
Some(1)

0 commit comments

Comments
 (0)