Skip to content

Commit b7f573b

Browse files
authored
feat: push down variant extractions in DataFusion (#460)
1 parent 63f283b commit b7f573b

25 files changed

Lines changed: 1961 additions & 172 deletions

bindings/c/src/table.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@ pub unsafe extern "C" fn paimon_read_builder_free(rb: *mut paimon_read_builder)
112112
///
113113
/// The `columns` parameter is a null-terminated array of null-terminated C strings.
114114
/// Output order follows the caller-specified order. Unknown or duplicate names
115-
/// cause `paimon_read_builder_new_read()` to fail; an empty list is a valid
116-
/// zero-column projection.
115+
/// are validated immediately; an empty list is a valid zero-column projection.
117116
///
118117
/// # Safety
119118
/// `rb` must be a valid pointer from `paimon_table_new_read_builder`, or null (returns error).
@@ -149,6 +148,11 @@ pub unsafe extern "C" fn paimon_read_builder_with_projection(
149148
ptr = ptr.add(1);
150149
}
151150

151+
let col_refs: Vec<&str> = col_names.iter().map(String::as_str).collect();
152+
if let Err(e) = state.table.new_read_builder().with_projection(&col_refs) {
153+
return paimon_error::from_paimon(e);
154+
}
155+
152156
state.projected_columns = Some(col_names);
153157
std::ptr::null_mut()
154158
}
@@ -229,7 +233,12 @@ pub unsafe extern "C" fn paimon_read_builder_new_read(
229233
// Apply projection if set
230234
if let Some(ref columns) = state.projected_columns {
231235
let col_refs: Vec<&str> = columns.iter().map(|s| s.as_str()).collect();
232-
rb_rust.with_projection(&col_refs);
236+
if let Err(e) = rb_rust.with_projection(&col_refs) {
237+
return paimon_result_new_read {
238+
read: std::ptr::null_mut(),
239+
error: paimon_error::from_paimon(e),
240+
};
241+
}
233242
}
234243

235244
// Apply filter if set

bindings/python/src/read.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,18 @@ fn apply_read_config(
6969
projection: &Option<Vec<String>>,
7070
limit: Option<usize>,
7171
filter: &Option<Predicate>,
72-
) {
72+
) -> PyResult<()> {
7373
if let Some(projection) = projection {
7474
let cols: Vec<&str> = projection.iter().map(String::as_str).collect();
75-
builder.with_projection(&cols);
75+
builder.with_projection(&cols).map_err(to_py_err)?;
7676
}
7777
if let Some(limit) = limit {
7878
builder.with_limit(limit);
7979
}
8080
if let Some(filter) = filter {
8181
builder.with_filter(filter.clone());
8282
}
83+
Ok(())
8384
}
8485

8586
/// Extract a sequence of Python `Split` objects into core `DataSplit`s. Accepts
@@ -220,7 +221,7 @@ impl PyTableScan {
220221
let splits = py.detach(|| {
221222
rt.block_on(async {
222223
let mut builder = self.table.new_read_builder();
223-
apply_read_config(&mut builder, &self.projection, self.limit, &self.filter);
224+
apply_read_config(&mut builder, &self.projection, self.limit, &self.filter)?;
224225
let plan = builder.new_scan().plan().await.map_err(to_py_err)?;
225226
Ok::<_, PyErr>(plan.splits().to_vec())
226227
})
@@ -246,7 +247,7 @@ impl PyTableRead {
246247
let batches = py.detach(|| {
247248
rt.block_on(async {
248249
let mut builder = self.table.new_read_builder();
249-
apply_read_config(&mut builder, &self.projection, self.limit, &self.filter);
250+
apply_read_config(&mut builder, &self.projection, self.limit, &self.filter)?;
250251
// Validate config (e.g. projection) before the empty-splits fast
251252
// path so an invalid projection fails consistently regardless of
252253
// how many splits are passed.

crates/integration_tests/tests/append_tables.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ async fn test_unpartitioned_projection() {
253253

254254
// Read with projection
255255
let mut rb = table.new_read_builder();
256-
rb.with_projection(&["value"]);
256+
rb.with_projection(&["value"])
257+
.expect("Projection should succeed");
257258
let plan = rb.new_scan().plan().await.unwrap();
258259
let read = rb.new_read().unwrap();
259260
let result: Vec<RecordBatch> = read

crates/integration_tests/tests/read_tables.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ async fn scan_and_read<C: Catalog + ?Sized>(
4646

4747
let mut read_builder = table.new_read_builder();
4848
if let Some(cols) = projection {
49-
read_builder.with_projection(cols);
49+
read_builder
50+
.with_projection(cols)
51+
.expect("Invalid projection");
5052
}
5153
let scan = read_builder.new_scan();
5254
let plan = scan.plan().await.expect("Failed to plan scan");
@@ -107,7 +109,9 @@ async fn scan_and_read_with_projection_and_filter(
107109
) -> (Plan, Vec<RecordBatch>) {
108110
let mut read_builder = table.new_read_builder();
109111
if let Some(cols) = projection {
110-
read_builder.with_projection(cols);
112+
read_builder
113+
.with_projection(cols)
114+
.expect("Invalid projection");
111115
}
112116
read_builder.with_filter(filter);
113117
let scan = read_builder.new_scan();
@@ -486,7 +490,9 @@ async fn test_read_projection_empty() {
486490
let table = get_table_from_catalog(&catalog, "simple_log_table").await;
487491

488492
let mut read_builder = table.new_read_builder();
489-
read_builder.with_projection(&[]);
493+
read_builder
494+
.with_projection(&[])
495+
.expect("Empty projection should succeed");
490496
let read = read_builder
491497
.new_read()
492498
.expect("Empty projection should succeed");
@@ -528,9 +534,8 @@ async fn test_read_projection_unknown_column() {
528534
let table = get_table_from_catalog(&catalog, "simple_log_table").await;
529535

530536
let mut read_builder = table.new_read_builder();
531-
read_builder.with_projection(&["id", "nonexistent_column"]);
532537
let err = read_builder
533-
.new_read()
538+
.with_projection(&["id", "nonexistent_column"])
534539
.expect_err("Unknown columns should fail");
535540

536541
assert!(
@@ -551,9 +556,8 @@ async fn test_read_projection_all_invalid() {
551556
let table = get_table_from_catalog(&catalog, "simple_log_table").await;
552557

553558
let mut read_builder = table.new_read_builder();
554-
read_builder.with_projection(&["nonexistent_a", "nonexistent_b"]);
555559
let err = read_builder
556-
.new_read()
560+
.with_projection(&["nonexistent_a", "nonexistent_b"])
557561
.expect_err("All-invalid projection should fail");
558562

559563
assert!(
@@ -574,9 +578,8 @@ async fn test_read_projection_duplicate_column() {
574578
let table = get_table_from_catalog(&catalog, "simple_log_table").await;
575579

576580
let mut read_builder = table.new_read_builder();
577-
read_builder.with_projection(&["id", "id"]);
578581
let err = read_builder
579-
.new_read()
582+
.with_projection(&["id", "id"])
580583
.expect_err("Duplicate projection should fail");
581584

582585
assert!(
@@ -3164,7 +3167,9 @@ async fn test_read_data_evolution_table_with_row_id_projection() {
31643167

31653168
// Project _ROW_ID along with regular columns
31663169
let mut read_builder = table.new_read_builder();
3167-
read_builder.with_projection(&["_ROW_ID", "id", "name"]);
3170+
read_builder
3171+
.with_projection(&["_ROW_ID", "id", "name"])
3172+
.expect("Row ID projection should succeed");
31683173
let scan = read_builder.new_scan();
31693174
let plan = scan.plan().await.expect("Failed to plan scan");
31703175

@@ -3233,7 +3238,9 @@ async fn test_read_data_evolution_table_only_row_id_with_row_ranges() {
32333238
// Project only _ROW_ID with a partial row range
32343239
let mid = min_row_id + (max_row_id - min_row_id) / 2;
32353240
let mut read_builder = table.new_read_builder();
3236-
read_builder.with_projection(&["_ROW_ID"]);
3241+
read_builder
3242+
.with_projection(&["_ROW_ID"])
3243+
.expect("Row ID projection should succeed");
32373244
read_builder.with_row_ranges(vec![RowRange::new(min_row_id, mid)]);
32383245
let scan = read_builder.new_scan();
32393246
let plan = scan.plan().await.expect("plan");
@@ -3267,7 +3274,9 @@ async fn test_read_data_evolution_mixed_format_row_id_projection() {
32673274
let table = get_table_from_catalog(&catalog, "data_evolution_mixed_format_add_column").await;
32683275

32693276
let mut read_builder = table.new_read_builder();
3270-
read_builder.with_projection(&["_ROW_ID", "id"]);
3277+
read_builder
3278+
.with_projection(&["_ROW_ID", "id"])
3279+
.expect("Row ID projection should succeed");
32713280
let scan = read_builder.new_scan();
32723281
let plan = scan.plan().await.expect("Failed to plan scan");
32733282

crates/integrations/datafusion/src/lateral_vector_search.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ impl QueryPlanner for PaimonQueryPlanner {
7272
logical_plan: &LogicalPlan,
7373
session_state: &SessionState,
7474
) -> DFResult<Arc<dyn ExecutionPlan>> {
75-
let planner = DefaultPhysicalPlanner::with_extension_planners(vec![Arc::new(
76-
LateralVectorSearchExtensionPlanner,
77-
)]);
75+
let planner = DefaultPhysicalPlanner::with_extension_planners(vec![
76+
Arc::new(LateralVectorSearchExtensionPlanner),
77+
Arc::new(crate::variant_pushdown::VariantExtractionExtensionPlanner),
78+
]);
7879
planner
7980
.create_physical_plan(logical_plan, session_state)
8081
.await
@@ -91,8 +92,10 @@ impl RewriteLateralVectorSearch {
9192
}
9293

9394
pub(crate) fn optimizer_rules() -> Vec<Arc<dyn OptimizerRule + Send + Sync>> {
94-
let mut rules: Vec<Arc<dyn OptimizerRule + Send + Sync>> =
95-
vec![Arc::new(RewriteLateralVectorSearch::new())];
95+
let mut rules: Vec<Arc<dyn OptimizerRule + Send + Sync>> = vec![
96+
Arc::new(crate::variant_pushdown::RewriteVariantExtractions::new()),
97+
Arc::new(RewriteLateralVectorSearch::new()),
98+
];
9699
rules.extend(Optimizer::default().rules);
97100
rules
98101
}
@@ -611,6 +614,7 @@ async fn read_target_rows(
611614
let mut read_builder = table.new_read_builder();
612615
read_builder
613616
.with_projection(&projection_refs)
617+
.map_err(to_datafusion_error)?
614618
.with_row_ranges(row_ranges);
615619
let plan = read_builder
616620
.new_scan()

crates/integrations/datafusion/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ mod table;
5555
mod table_function_args;
5656
mod update;
5757
mod variant_functions;
58+
mod variant_pushdown;
5859
mod vector_search;
5960

6061
use std::collections::HashMap;

crates/integrations/datafusion/src/physical_plan/scan.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
2727
use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
2828
use datafusion::physical_plan::{DisplayAs, ExecutionPlan, Partitioning, PlanProperties};
2929
use futures::{StreamExt, TryStreamExt};
30-
use paimon::spec::Predicate;
30+
use paimon::spec::{DataField, Predicate};
3131
use paimon::table::{ScanTrace, Table};
3232
use paimon::DataSplit;
3333

@@ -41,8 +41,8 @@ use crate::error::to_datafusion_error;
4141
#[derive(Debug)]
4242
pub struct PaimonTableScan {
4343
table: Table,
44-
/// Projected column names (if None, reads all columns).
45-
projected_columns: Option<Vec<String>>,
44+
/// Full Paimon read type for nested or connector-defined projections.
45+
read_type: Vec<DataField>,
4646
/// Filter translated from DataFusion expressions and reused during execute()
4747
/// so reader-side pruning reaches the actual read path.
4848
pushed_predicate: Option<Predicate>,
@@ -58,19 +58,22 @@ pub struct PaimonTableScan {
5858
filter_exact: bool,
5959
/// Metadata-pruning trace captured during eager scan planning.
6060
scan_trace: Option<ScanTrace>,
61+
/// Human-readable Variant extraction summary for explain output.
62+
pushed_variants: Option<String>,
6163
}
6264

6365
impl PaimonTableScan {
6466
#[allow(clippy::too_many_arguments)]
6567
pub(crate) fn new(
6668
schema: ArrowSchemaRef,
6769
table: Table,
68-
projected_columns: Option<Vec<String>>,
70+
read_type: Vec<DataField>,
6971
pushed_predicate: Option<Predicate>,
7072
planned_partitions: Vec<Arc<[DataSplit]>>,
7173
limit: Option<usize>,
7274
filter_exact: bool,
7375
scan_trace: Option<ScanTrace>,
76+
pushed_variants: Option<String>,
7477
) -> Self {
7578
let plan_properties = Arc::new(PlanProperties::new(
7679
EquivalenceProperties::new(schema.clone()),
@@ -80,13 +83,14 @@ impl PaimonTableScan {
8083
));
8184
Self {
8285
table,
83-
projected_columns,
86+
read_type,
8487
pushed_predicate,
8588
planned_partitions,
8689
plan_properties,
8790
limit,
8891
filter_exact,
8992
scan_trace,
93+
pushed_variants,
9094
}
9195
}
9296

@@ -143,16 +147,13 @@ impl ExecutionPlan for PaimonTableScan {
143147

144148
let table = self.table.clone();
145149
let schema = self.schema();
146-
let projected_columns = self.projected_columns.clone();
150+
let read_type = self.read_type.clone();
147151
let pushed_predicate = self.pushed_predicate.clone();
148152

149153
let fut = async move {
150154
let mut read_builder = table.new_read_builder();
151155

152-
if let Some(ref columns) = projected_columns {
153-
let col_refs: Vec<&str> = columns.iter().map(|s| s.as_str()).collect();
154-
read_builder.with_projection(&col_refs);
155-
}
156+
read_builder.with_read_type(read_type);
156157
if let Some(filter) = pushed_predicate {
157158
read_builder.with_filter(filter);
158159
}
@@ -236,9 +237,12 @@ impl DisplayAs for PaimonTableScan {
236237
self.planned_partitions.len()
237238
)?;
238239

239-
if let Some(ref columns) = self.projected_columns {
240-
write!(f, ", projection=[{}]", columns.join(", "))?;
241-
}
240+
let columns = self
241+
.read_type
242+
.iter()
243+
.map(|field| field.name())
244+
.collect::<Vec<_>>();
245+
write!(f, ", projection=[{}]", columns.join(", "))?;
242246
if let Some(ref predicate) = self.pushed_predicate {
243247
write!(f, ", predicate={predicate}")?;
244248
}
@@ -248,6 +252,9 @@ impl DisplayAs for PaimonTableScan {
248252
if let Some(ref trace) = self.scan_trace {
249253
write!(f, ", trace={trace}")?;
250254
}
255+
if let Some(ref pushed_variants) = self.pushed_variants {
256+
write!(f, ", PushedVariants=[{pushed_variants}]")?;
257+
}
251258
Ok(())
252259
}
253260
}
@@ -282,18 +289,27 @@ mod tests {
282289
)]))
283290
}
284291

292+
fn test_read_type() -> Vec<DataField> {
293+
vec![DataField::new(
294+
0,
295+
"id".to_string(),
296+
DataType::Int(IntType::new()),
297+
)]
298+
}
299+
285300
#[test]
286301
fn test_partition_count_empty_plan() {
287302
let schema = test_schema();
288303
let scan = PaimonTableScan::new(
289304
schema,
290305
dummy_table(),
291-
None,
306+
test_read_type(),
292307
None,
293308
vec![Arc::from(Vec::new())],
294309
None,
295310
false,
296311
None,
312+
None,
297313
);
298314
assert_eq!(scan.properties().output_partitioning().partition_count(), 1);
299315
}
@@ -309,12 +325,13 @@ mod tests {
309325
let scan = PaimonTableScan::new(
310326
schema,
311327
dummy_table(),
312-
None,
328+
test_read_type(),
313329
None,
314330
planned_partitions,
315331
None,
316332
false,
317333
None,
334+
None,
318335
);
319336
assert_eq!(scan.properties().output_partitioning().partition_count(), 3);
320337
}
@@ -387,12 +404,17 @@ mod tests {
387404
let scan = PaimonTableScan::new(
388405
schema,
389406
table,
390-
Some(vec!["id".to_string()]),
407+
vec![DataField::new(
408+
0,
409+
"id".to_string(),
410+
DataType::Int(IntType::new()),
411+
)],
391412
Some(pushed_predicate),
392413
vec![Arc::from(vec![split])],
393414
None,
394415
false,
395416
None,
417+
None,
396418
);
397419

398420
let ctx = SessionContext::new();

0 commit comments

Comments
 (0)