1use std::any::Any;
21use std::sync::Arc;
22
23use async_trait::async_trait;
24use datafusion::arrow::datatypes::{Field, Schema, SchemaRef as ArrowSchemaRef};
25use datafusion::catalog::Session;
26use datafusion::datasource::sink::DataSinkExec;
27use datafusion::datasource::{TableProvider, TableType};
28use datafusion::error::Result as DFResult;
29use datafusion::logical_expr::dml::InsertOp;
30use datafusion::logical_expr::{Expr, TableProviderFilterPushDown};
31use datafusion::physical_plan::ExecutionPlan;
32use paimon::table::Table;
33
34use crate::physical_plan::PaimonDataSink;
35
36use crate::error::to_datafusion_error;
37#[cfg(test)]
38use crate::filter_pushdown::build_pushed_predicate;
39use crate::filter_pushdown::{analyze_filters, classify_filter_pushdown};
40use crate::physical_plan::PaimonTableScan;
41use crate::runtime::await_with_runtime;
42
43#[derive(Debug, Clone)]
53pub struct PaimonTableProvider {
54 table: Table,
55 schema: ArrowSchemaRef,
56}
57
58impl PaimonTableProvider {
59 pub fn try_new(table: Table) -> DFResult<Self> {
63 let mut fields = table.schema().fields().to_vec();
64 let core_options = paimon::spec::CoreOptions::new(table.schema().options());
65 if core_options.data_evolution_enabled() {
66 fields.push(paimon::spec::DataField::new(
67 paimon::spec::ROW_ID_FIELD_ID,
68 paimon::spec::ROW_ID_FIELD_NAME.to_string(),
69 paimon::spec::DataType::BigInt(paimon::spec::BigIntType::with_nullable(true)),
70 ));
71 }
72 let schema =
73 paimon::arrow::build_target_arrow_schema(&fields).map_err(to_datafusion_error)?;
74 Ok(Self { table, schema })
75 }
76
77 pub fn table(&self) -> &Table {
78 &self.table
79 }
80}
81
82pub(crate) fn bucket_round_robin<T>(items: Vec<T>, num_buckets: usize) -> Vec<Vec<T>> {
84 let mut buckets: Vec<Vec<T>> = (0..num_buckets).map(|_| Vec::new()).collect();
85 for (index, item) in items.into_iter().enumerate() {
86 buckets[index % num_buckets].push(item);
87 }
88 buckets
89}
90
91pub(crate) struct PaimonScanBuilder<'a> {
93 pub(crate) table: &'a Table,
94 pub(crate) schema: &'a ArrowSchemaRef,
95 pub(crate) plan: &'a paimon::table::Plan,
96 pub(crate) projection: Option<&'a Vec<usize>>,
97 pub(crate) pushed_predicate: Option<paimon::spec::Predicate>,
98 pub(crate) limit: Option<usize>,
99 pub(crate) target_partitions: usize,
100 pub(crate) filter_exact: bool,
101}
102
103impl PaimonScanBuilder<'_> {
104 pub(crate) fn build(self) -> DFResult<Arc<dyn ExecutionPlan>> {
106 let (projected_schema, projected_columns) = if let Some(indices) = self.projection {
107 let fields: Vec<Field> = indices
108 .iter()
109 .map(|&i| self.schema.field(i).clone())
110 .collect();
111 let column_names: Vec<String> = fields.iter().map(|f| f.name().clone()).collect();
112 (Arc::new(Schema::new(fields)), Some(column_names))
113 } else {
114 let column_names: Vec<String> = self
115 .schema
116 .fields()
117 .iter()
118 .map(|f| f.name().clone())
119 .collect();
120 (self.schema.clone(), Some(column_names))
121 };
122
123 let splits = self.plan.splits().to_vec();
124 let planned_partitions: Vec<Arc<[_]>> = if splits.is_empty() {
125 vec![Arc::from(Vec::new())]
126 } else {
127 let num_partitions = splits.len().min(self.target_partitions.max(1));
128 bucket_round_robin(splits, num_partitions)
129 .into_iter()
130 .map(Arc::from)
131 .collect()
132 };
133
134 Ok(Arc::new(PaimonTableScan::new(
135 projected_schema,
136 self.table.clone(),
137 projected_columns,
138 self.pushed_predicate,
139 planned_partitions,
140 self.limit,
141 self.filter_exact,
142 )))
143 }
144}
145
146#[async_trait]
147impl TableProvider for PaimonTableProvider {
148 fn as_any(&self) -> &dyn Any {
149 self
150 }
151
152 fn schema(&self) -> ArrowSchemaRef {
153 self.schema.clone()
154 }
155
156 fn table_type(&self) -> TableType {
157 TableType::Base
158 }
159
160 async fn scan(
161 &self,
162 state: &dyn Session,
163 projection: Option<&Vec<usize>>,
164 filters: &[Expr],
165 limit: Option<usize>,
166 ) -> DFResult<Arc<dyn ExecutionPlan>> {
167 let filter_analysis = analyze_filters(filters, self.table.schema().fields());
169 let mut read_builder = self.table.new_read_builder();
170 if let Some(filter) = filter_analysis.pushed_predicate.clone() {
171 read_builder.with_filter(filter);
172 }
173 let pushed_limit = limit.filter(|_| !filter_analysis.has_untranslated_residual);
174 if let Some(limit) = pushed_limit {
175 read_builder.with_limit(limit);
176 }
177 let scan = read_builder.new_scan();
178 let plan = await_with_runtime(scan.plan())
183 .await
184 .map_err(to_datafusion_error)?;
185
186 let target = state.config_options().execution.target_partitions;
187 let filter_exact = !filter_analysis.has_untranslated_residual
188 && filter_analysis
189 .pushed_predicate
190 .as_ref()
191 .is_none_or(|p| read_builder.is_exact_filter_pushdown(p));
192 PaimonScanBuilder {
193 table: &self.table,
194 schema: &self.schema,
195 plan: &plan,
196 projection,
197 pushed_predicate: filter_analysis.pushed_predicate,
198 limit: pushed_limit,
199 target_partitions: target,
200 filter_exact,
201 }
202 .build()
203 }
204
205 async fn insert_into(
206 &self,
207 _state: &dyn Session,
208 input: Arc<dyn ExecutionPlan>,
209 insert_op: InsertOp,
210 ) -> DFResult<Arc<dyn ExecutionPlan>> {
211 let overwrite = match insert_op {
212 InsertOp::Append => false,
213 InsertOp::Overwrite => true,
214 other => {
215 return Err(datafusion::error::DataFusionError::NotImplemented(format!(
216 "{other} is not supported for Paimon tables"
217 )));
218 }
219 };
220 let sink = PaimonDataSink::new(self.table.clone(), self.schema.clone(), overwrite);
221 Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None)))
222 }
223
224 fn supports_filters_pushdown(
225 &self,
226 filters: &[&Expr],
227 ) -> DFResult<Vec<TableProviderFilterPushDown>> {
228 let fields = self.table.schema().fields();
229 let read_builder = self.table.new_read_builder();
230
231 Ok(filters
232 .iter()
233 .map(|filter| {
234 classify_filter_pushdown(filter, fields, |predicate| {
235 read_builder.is_exact_filter_pushdown(predicate)
236 })
237 })
238 .collect())
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use std::collections::BTreeSet;
246 use std::sync::Arc;
247
248 use datafusion::datasource::TableProvider;
249 use datafusion::logical_expr::{col, lit, Expr};
250 use datafusion::prelude::{SessionConfig, SessionContext};
251 use paimon::catalog::Identifier;
252 use paimon::{Catalog, CatalogOptions, DataSplit, FileSystemCatalog, Options};
253
254 use crate::physical_plan::PaimonTableScan;
255
256 #[test]
257 fn test_bucket_round_robin_distributes_evenly() {
258 let result = bucket_round_robin(vec![0, 1, 2, 3, 4], 3);
259 assert_eq!(result, vec![vec![0, 3], vec![1, 4], vec![2]]);
260 }
261
262 #[test]
263 fn test_bucket_round_robin_fewer_items_than_buckets() {
264 let result = bucket_round_robin(vec![10, 20], 2);
265 assert_eq!(result, vec![vec![10], vec![20]]);
266 }
267
268 #[test]
269 fn test_bucket_round_robin_single_bucket() {
270 let result = bucket_round_robin(vec![1, 2, 3], 1);
271 assert_eq!(result, vec![vec![1, 2, 3]]);
272 }
273
274 fn get_test_warehouse() -> String {
275 std::env::var("PAIMON_TEST_WAREHOUSE")
276 .unwrap_or_else(|_| "/tmp/paimon-warehouse".to_string())
277 }
278
279 fn create_catalog() -> FileSystemCatalog {
280 let warehouse = get_test_warehouse();
281 let mut options = Options::new();
282 options.set(CatalogOptions::WAREHOUSE, warehouse);
283 FileSystemCatalog::new(options).expect("Failed to create catalog")
284 }
285
286 async fn create_provider(table_name: &str) -> PaimonTableProvider {
287 let catalog = create_catalog();
288 let identifier = Identifier::new("default", table_name);
289 let table = catalog
290 .get_table(&identifier)
291 .await
292 .expect("Failed to get table");
293
294 PaimonTableProvider::try_new(table).expect("Failed to create table provider")
295 }
296
297 async fn plan_partitions(
298 provider: &PaimonTableProvider,
299 filters: Vec<Expr>,
300 limit: Option<usize>,
301 ) -> Vec<Arc<[DataSplit]>> {
302 let plan = plan_scan(provider, filters, limit).await;
303 let scan = plan
304 .as_any()
305 .downcast_ref::<PaimonTableScan>()
306 .expect("Expected PaimonTableScan");
307
308 scan.planned_partitions().to_vec()
309 }
310
311 async fn plan_scan(
312 provider: &PaimonTableProvider,
313 filters: Vec<Expr>,
314 limit: Option<usize>,
315 ) -> Arc<dyn ExecutionPlan> {
316 let config = SessionConfig::new().with_target_partitions(8);
317 let ctx = SessionContext::new_with_config(config);
318 let state = ctx.state();
319 provider
320 .scan(&state, None, &filters, limit)
321 .await
322 .expect("scan() should succeed")
323 }
324
325 fn extract_dt_partition_set(planned_partitions: &[Arc<[DataSplit]>]) -> BTreeSet<String> {
326 planned_partitions
327 .iter()
328 .flat_map(|splits| splits.iter())
329 .map(|split| {
330 split
331 .partition()
332 .get_string(0)
333 .expect("Failed to decode dt")
334 .to_string()
335 })
336 .collect()
337 }
338
339 fn extract_dt_hr_partition_set(
340 planned_partitions: &[Arc<[DataSplit]>],
341 ) -> BTreeSet<(String, i32)> {
342 planned_partitions
343 .iter()
344 .flat_map(|splits| splits.iter())
345 .map(|split| {
346 let partition = split.partition();
347 (
348 partition
349 .get_string(0)
350 .expect("Failed to decode dt")
351 .to_string(),
352 partition.get_int(1).expect("Failed to decode hr"),
353 )
354 })
355 .collect()
356 }
357
358 #[tokio::test]
359 async fn test_scan_partition_filter_plans_matching_partition_set() {
360 let provider = create_provider("partitioned_log_table").await;
361 let planned_partitions =
362 plan_partitions(&provider, vec![col("dt").eq(lit("2024-01-01"))], None).await;
363
364 assert_eq!(
365 extract_dt_partition_set(&planned_partitions),
366 BTreeSet::from(["2024-01-01".to_string()]),
367 );
368 }
369
370 #[tokio::test]
371 async fn test_scan_mixed_and_filter_keeps_partition_pruning() {
372 let provider = create_provider("partitioned_log_table").await;
373 let planned_partitions = plan_partitions(
374 &provider,
375 vec![col("dt").eq(lit("2024-01-01")).and(col("id").gt(lit(1)))],
376 None,
377 )
378 .await;
379
380 assert_eq!(
381 extract_dt_partition_set(&planned_partitions),
382 BTreeSet::from(["2024-01-01".to_string()]),
383 );
384 }
385
386 #[tokio::test]
387 async fn test_scan_multi_partition_filter_plans_exact_partition_set() {
388 let provider = create_provider("multi_partitioned_log_table").await;
389
390 let dt_only_partitions =
391 plan_partitions(&provider, vec![col("dt").eq(lit("2024-01-01"))], None).await;
392 let dt_hr_partitions = plan_partitions(
393 &provider,
394 vec![col("dt").eq(lit("2024-01-01")).and(col("hr").eq(lit(10)))],
395 None,
396 )
397 .await;
398
399 assert_eq!(
400 extract_dt_hr_partition_set(&dt_only_partitions),
401 BTreeSet::from([
402 ("2024-01-01".to_string(), 10),
403 ("2024-01-01".to_string(), 20),
404 ]),
405 );
406 assert_eq!(
407 extract_dt_hr_partition_set(&dt_hr_partitions),
408 BTreeSet::from([("2024-01-01".to_string(), 10)]),
409 );
410 }
411
412 #[tokio::test]
413 async fn test_scan_partially_translated_filter_keeps_partition_pruning_but_skips_limit_hint() {
414 let provider = create_provider("multi_partitioned_log_table").await;
415 let filter = col("dt")
416 .eq(lit("2024-01-01"))
417 .and(Expr::Not(Box::new(col("hr").eq(lit(10)))));
418 let full_plan = plan_partitions(&provider, vec![filter.clone()], None).await;
419 let plan = plan_scan(&provider, vec![filter], Some(1)).await;
420 let scan = plan
421 .as_any()
422 .downcast_ref::<PaimonTableScan>()
423 .expect("Expected PaimonTableScan");
424
425 assert_eq!(scan.limit(), None);
426 assert_eq!(
427 extract_dt_hr_partition_set(scan.planned_partitions()),
428 BTreeSet::from([
429 ("2024-01-01".to_string(), 10),
430 ("2024-01-01".to_string(), 20),
431 ]),
432 );
433 assert_eq!(
434 scan.planned_partitions()
435 .iter()
436 .map(|partition| partition.len())
437 .sum::<usize>(),
438 full_plan
439 .iter()
440 .map(|partition| partition.len())
441 .sum::<usize>()
442 );
443 }
444
445 #[tokio::test]
446 async fn test_scan_keeps_pushed_predicate_for_execute() {
447 let provider = create_provider("partitioned_log_table").await;
448 let filter = col("id").gt(lit(1));
449
450 let config = SessionConfig::new().with_target_partitions(8);
451 let ctx = SessionContext::new_with_config(config);
452 let state = ctx.state();
453 let plan = provider
454 .scan(&state, None, std::slice::from_ref(&filter), None)
455 .await
456 .expect("scan() should succeed");
457 let scan = plan
458 .as_any()
459 .downcast_ref::<PaimonTableScan>()
460 .expect("Expected PaimonTableScan");
461
462 let expected = build_pushed_predicate(&[filter], provider.table().schema().fields())
463 .expect("data filter should translate");
464
465 assert_eq!(scan.pushed_predicate(), Some(&expected));
466 }
467
468 #[tokio::test]
469 async fn test_scan_applies_limit_hint_only_when_safe() {
470 let provider = create_provider("partitioned_log_table").await;
471 let full_plan = plan_partitions(&provider, vec![], None).await;
472 let plan = plan_scan(&provider, vec![], Some(1)).await;
473 let scan = plan
474 .as_any()
475 .downcast_ref::<PaimonTableScan>()
476 .expect("Expected PaimonTableScan");
477
478 assert_eq!(scan.limit(), Some(1));
479 assert!(
480 scan.planned_partitions()
481 .iter()
482 .map(|partition| partition.len())
483 .sum::<usize>()
484 < full_plan
485 .iter()
486 .map(|partition| partition.len())
487 .sum::<usize>()
488 );
489 }
490
491 #[tokio::test]
492 async fn test_scan_keeps_limit_but_skips_limit_pruning_for_data_filters() {
493 let provider = create_provider("partitioned_log_table").await;
494 let filter = col("id").gt(lit(1));
495 let full_plan = plan_partitions(&provider, vec![filter.clone()], None).await;
496 let plan = plan_scan(&provider, vec![filter], Some(1)).await;
497 let scan = plan
498 .as_any()
499 .downcast_ref::<PaimonTableScan>()
500 .expect("Expected PaimonTableScan");
501
502 assert_eq!(scan.limit(), Some(1));
503 assert_eq!(
504 scan.planned_partitions()
505 .iter()
506 .map(|partition| partition.len())
507 .sum::<usize>(),
508 full_plan
509 .iter()
510 .map(|partition| partition.len())
511 .sum::<usize>()
512 );
513 }
514
515 #[tokio::test]
516 async fn test_insert_into_and_read_back() {
517 use paimon::io::FileIOBuilder;
518 use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema};
519
520 let file_io = FileIOBuilder::new("memory").build().unwrap();
521 let table_path = "memory:/test_df_insert_into";
522 file_io
523 .mkdirs(&format!("{table_path}/snapshot/"))
524 .await
525 .unwrap();
526 file_io
527 .mkdirs(&format!("{table_path}/manifest/"))
528 .await
529 .unwrap();
530
531 let schema = PaimonSchema::builder()
532 .column("id", DataType::Int(IntType::new()))
533 .column("value", DataType::Int(IntType::new()))
534 .build()
535 .unwrap();
536 let table_schema = TableSchema::new(0, &schema);
537 let table = paimon::table::Table::new(
538 file_io,
539 Identifier::new("default", "test_insert"),
540 table_path.to_string(),
541 table_schema,
542 None,
543 );
544
545 let provider = PaimonTableProvider::try_new(table).unwrap();
546 let ctx = SessionContext::new();
547 ctx.register_table("t", Arc::new(provider)).unwrap();
548
549 let result = ctx
551 .sql("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)")
552 .await
553 .unwrap()
554 .collect()
555 .await
556 .unwrap();
557
558 let count_array = result[0]
560 .column(0)
561 .as_any()
562 .downcast_ref::<datafusion::arrow::array::UInt64Array>()
563 .unwrap();
564 assert_eq!(count_array.value(0), 3);
565
566 let batches = ctx
568 .sql("SELECT id, value FROM t ORDER BY id")
569 .await
570 .unwrap()
571 .collect()
572 .await
573 .unwrap();
574
575 let mut rows = Vec::new();
576 for batch in &batches {
577 let ids = batch
578 .column(0)
579 .as_any()
580 .downcast_ref::<datafusion::arrow::array::Int32Array>()
581 .unwrap();
582 let vals = batch
583 .column(1)
584 .as_any()
585 .downcast_ref::<datafusion::arrow::array::Int32Array>()
586 .unwrap();
587 for i in 0..batch.num_rows() {
588 rows.push((ids.value(i), vals.value(i)));
589 }
590 }
591 assert_eq!(rows, vec![(1, 10), (2, 20), (3, 30)]);
592 }
593
594 #[tokio::test]
595 async fn test_insert_overwrite() {
596 use paimon::io::FileIOBuilder;
597 use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema, VarCharType};
598
599 let file_io = FileIOBuilder::new("memory").build().unwrap();
600 let table_path = "memory:/test_df_insert_overwrite";
601 file_io
602 .mkdirs(&format!("{table_path}/snapshot/"))
603 .await
604 .unwrap();
605 file_io
606 .mkdirs(&format!("{table_path}/manifest/"))
607 .await
608 .unwrap();
609
610 let schema = PaimonSchema::builder()
611 .column("pt", DataType::VarChar(VarCharType::string_type()))
612 .column("id", DataType::Int(IntType::new()))
613 .partition_keys(["pt"])
614 .build()
615 .unwrap();
616 let table_schema = TableSchema::new(0, &schema);
617 let table = paimon::table::Table::new(
618 file_io,
619 Identifier::new("default", "test_overwrite"),
620 table_path.to_string(),
621 table_schema,
622 None,
623 );
624
625 let provider = PaimonTableProvider::try_new(table).unwrap();
626 let ctx = SessionContext::new();
627 ctx.register_table("t", Arc::new(provider)).unwrap();
628
629 ctx.sql("INSERT INTO t VALUES ('a', 1), ('a', 2), ('b', 3), ('b', 4)")
631 .await
632 .unwrap()
633 .collect()
634 .await
635 .unwrap();
636
637 ctx.sql("INSERT OVERWRITE t VALUES ('a', 10), ('a', 20)")
640 .await
641 .unwrap()
642 .collect()
643 .await
644 .unwrap();
645
646 let batches = ctx
648 .sql("SELECT pt, id FROM t ORDER BY pt, id")
649 .await
650 .unwrap()
651 .collect()
652 .await
653 .unwrap();
654
655 let mut rows = Vec::new();
656 for batch in &batches {
657 let pts = batch
658 .column(0)
659 .as_any()
660 .downcast_ref::<datafusion::arrow::array::StringArray>()
661 .unwrap();
662 let ids = batch
663 .column(1)
664 .as_any()
665 .downcast_ref::<datafusion::arrow::array::Int32Array>()
666 .unwrap();
667 for i in 0..batch.num_rows() {
668 rows.push((pts.value(i).to_string(), ids.value(i)));
669 }
670 }
671 assert_eq!(
673 rows,
674 vec![
675 ("a".to_string(), 10),
676 ("a".to_string(), 20),
677 ("b".to_string(), 3),
678 ("b".to_string(), 4),
679 ]
680 );
681 }
682
683 #[tokio::test]
684 async fn test_insert_overwrite_unpartitioned() {
685 use paimon::io::FileIOBuilder;
686 use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema};
687
688 let file_io = FileIOBuilder::new("memory").build().unwrap();
689 let table_path = "memory:/test_df_insert_overwrite_unpart";
690 file_io
691 .mkdirs(&format!("{table_path}/snapshot/"))
692 .await
693 .unwrap();
694 file_io
695 .mkdirs(&format!("{table_path}/manifest/"))
696 .await
697 .unwrap();
698
699 let schema = PaimonSchema::builder()
700 .column("id", DataType::Int(IntType::new()))
701 .column("value", DataType::Int(IntType::new()))
702 .build()
703 .unwrap();
704 let table_schema = TableSchema::new(0, &schema);
705 let table = paimon::table::Table::new(
706 file_io,
707 Identifier::new("default", "test_overwrite_unpart"),
708 table_path.to_string(),
709 table_schema,
710 None,
711 );
712
713 let provider = PaimonTableProvider::try_new(table).unwrap();
714 let ctx = SessionContext::new();
715 ctx.register_table("t", Arc::new(provider)).unwrap();
716
717 ctx.sql("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)")
719 .await
720 .unwrap()
721 .collect()
722 .await
723 .unwrap();
724
725 ctx.sql("INSERT OVERWRITE t VALUES (4, 40), (5, 50)")
727 .await
728 .unwrap()
729 .collect()
730 .await
731 .unwrap();
732
733 let batches = ctx
734 .sql("SELECT id, value FROM t ORDER BY id")
735 .await
736 .unwrap()
737 .collect()
738 .await
739 .unwrap();
740
741 let mut rows = Vec::new();
742 for batch in &batches {
743 let ids = batch
744 .column(0)
745 .as_any()
746 .downcast_ref::<datafusion::arrow::array::Int32Array>()
747 .unwrap();
748 let vals = batch
749 .column(1)
750 .as_any()
751 .downcast_ref::<datafusion::arrow::array::Int32Array>()
752 .unwrap();
753 for i in 0..batch.num_rows() {
754 rows.push((ids.value(i), vals.value(i)));
755 }
756 }
757 assert_eq!(rows, vec![(4, 40), (5, 50)]);
759 }
760}