1use std::fmt::Write as _;
21use std::sync::Arc;
22
23use async_trait::async_trait;
24use datafusion::arrow::datatypes::{
25 DataType as ArrowDataType, Field, Schema, SchemaRef as ArrowSchemaRef,
26};
27use datafusion::catalog::Session;
28use datafusion::datasource::sink::DataSinkExec;
29use datafusion::datasource::{TableProvider, TableType};
30use datafusion::error::{DataFusionError, Result as DFResult};
31use datafusion::logical_expr::dml::InsertOp;
32use datafusion::logical_expr::{Expr, TableProviderFilterPushDown};
33use datafusion::physical_plan::ExecutionPlan;
34use paimon::spec::{
35 BigIntType, CoreOptions, DataField, DataType, ROW_ID_FIELD_ID, ROW_ID_FIELD_NAME,
36};
37use paimon::table::Table;
38
39use crate::physical_plan::PaimonDataSink;
40use crate::BlobReaderRegistry;
41
42use crate::error::to_datafusion_error;
43#[cfg(test)]
44use crate::filter_pushdown::build_pushed_predicate;
45use crate::filter_pushdown::{analyze_filters, classify_filter_pushdown};
46use crate::physical_plan::PaimonTableScan;
47use crate::runtime::await_with_runtime;
48
49const PARQUET_FIELD_ID_META_KEY: &str = "PARQUET:field_id";
50
51pub(crate) fn datafusion_read_fields(table: &Table) -> Vec<DataField> {
52 let mut fields = table.schema().fields().to_vec();
53 if CoreOptions::new(table.schema().options()).data_evolution_enabled() {
54 fields.push(DataField::new(
55 ROW_ID_FIELD_ID,
56 ROW_ID_FIELD_NAME.to_string(),
57 DataType::BigInt(BigIntType::with_nullable(true)),
58 ));
59 }
60 fields
61}
62
63fn datafusion_arrow_schema(
64 fields: &[DataField],
65 schema_force_view_types: bool,
66) -> DFResult<ArrowSchemaRef> {
67 let paimon_schema =
68 paimon::arrow::build_target_arrow_schema(fields).map_err(to_datafusion_error)?;
69 let fields = paimon_schema
70 .fields()
71 .iter()
72 .map(|field| {
73 let mut metadata = field.metadata().clone();
74 metadata.remove(PARQUET_FIELD_ID_META_KEY);
75 let data_type = match field.data_type() {
76 ArrowDataType::Utf8 if schema_force_view_types => ArrowDataType::Utf8View,
77 data_type => data_type.clone(),
78 };
79 Arc::new(
80 field
81 .as_ref()
82 .clone()
83 .with_data_type(data_type)
84 .with_metadata(metadata),
85 )
86 })
87 .collect::<Vec<_>>();
88
89 Ok(Arc::new(Schema::new_with_metadata(
90 fields,
91 paimon_schema.metadata().clone(),
92 )))
93}
94
95#[derive(Debug, Clone)]
105pub struct PaimonTableProvider {
106 table: Table,
107 schema: ArrowSchemaRef,
108 table_definition: Option<String>,
109}
110
111impl PaimonTableProvider {
112 pub fn try_new(table: Table) -> DFResult<Self> {
116 let table_definition = build_table_definition(&table)?;
117 Self::try_new_with_table_definition(table, Some(table_definition))
118 }
119
120 fn try_new_with_table_definition(
121 table: Table,
122 table_definition: Option<String>,
123 ) -> DFResult<Self> {
124 let fields = datafusion_read_fields(&table);
125 let schema = datafusion_arrow_schema(&fields, true)?;
126 Ok(Self {
127 table,
128 schema,
129 table_definition,
130 })
131 }
132
133 pub fn try_new_with_blob_reader_registry(
134 table: Table,
135 blob_reader_registry: BlobReaderRegistry,
136 ) -> DFResult<Self> {
137 blob_reader_registry
138 .register_if_absent(table.location().to_string(), table.file_io().clone());
139 Self::try_new(table)
140 }
141
142 pub(crate) fn try_new_with_blob_reader_registry_and_definition(
143 table: Table,
144 blob_reader_registry: BlobReaderRegistry,
145 table_definition: Option<String>,
146 ) -> DFResult<Self> {
147 blob_reader_registry
148 .register_if_absent(table.location().to_string(), table.file_io().clone());
149 Self::try_new_with_table_definition(table, table_definition)
150 }
151
152 pub(crate) fn with_schema_force_view_types(
153 mut self,
154 schema_force_view_types: bool,
155 ) -> DFResult<Self> {
156 if schema_force_view_types {
157 return Ok(self);
158 }
159 let fields = datafusion_read_fields(&self.table);
160 self.schema = datafusion_arrow_schema(&fields, schema_force_view_types)?;
161 Ok(self)
162 }
163
164 pub fn table(&self) -> &Table {
165 &self.table
166 }
167}
168
169pub(crate) fn build_table_definition(table: &Table) -> DFResult<String> {
174 let identifier = table.identifier();
175 let schema = table.schema();
176 let mut ddl = String::new();
177 let _ = write!(
178 ddl,
179 "CREATE TABLE {}.{} (",
180 quote_identifier(identifier.database()),
181 quote_identifier(identifier.object())
182 );
183
184 for (i, field) in schema.fields().iter().enumerate() {
185 if i > 0 {
186 ddl.push_str(", ");
187 }
188 let ty = data_type_to_sql(field.data_type())?;
191 if field.data_type().is_nullable() {
192 let _ = write!(ddl, "{} {}", quote_identifier(field.name()), ty);
193 } else {
194 let _ = write!(ddl, "{} {} NOT NULL", quote_identifier(field.name()), ty);
195 }
196 }
197
198 let pks = schema.primary_keys();
199 if !pks.is_empty() {
200 ddl.push_str(", PRIMARY KEY (");
201 for (i, pk) in pks.iter().enumerate() {
202 if i > 0 {
203 ddl.push_str(", ");
204 }
205 let _ = write!(ddl, "{}", quote_identifier(pk));
206 }
207 ddl.push(')');
208 }
209 ddl.push(')');
210
211 let partition_keys = schema.partition_keys();
212 if !partition_keys.is_empty() {
213 ddl.push_str(" PARTITIONED BY (");
214 for (i, pk) in partition_keys.iter().enumerate() {
215 if i > 0 {
216 ddl.push_str(", ");
217 }
218 let _ = write!(ddl, "{}", quote_identifier(pk));
219 }
220 ddl.push(')');
221 }
222
223 let mut options: Vec<_> = schema.options().iter().collect();
224 options.sort_by_key(|(left, _)| *left);
225 if !options.is_empty() {
226 ddl.push_str(" WITH (");
227 for (i, (k, v)) in options.iter().enumerate() {
228 if i > 0 {
229 ddl.push_str(", ");
230 }
231 let _ = write!(
232 ddl,
233 "{} = {}",
234 quote_string_literal(k),
235 quote_string_literal(v)
236 );
237 }
238 ddl.push(')');
239 }
240
241 Ok(ddl)
242}
243
244fn quote_identifier(identifier: &str) -> String {
245 format!("\"{}\"", identifier.replace('"', "\"\""))
246}
247
248fn quote_string_literal(text: &str) -> String {
249 format!("'{}'", text.replace('\'', "''"))
250}
251
252pub(crate) fn data_type_to_sql(data_type: &DataType) -> DFResult<String> {
260 match data_type {
261 DataType::Boolean(_) => Ok("BOOLEAN".to_string()),
262 DataType::TinyInt(_) => Ok("TINYINT".to_string()),
263 DataType::SmallInt(_) => Ok("SMALLINT".to_string()),
264 DataType::Int(_) => Ok("INT".to_string()),
265 DataType::BigInt(_) => Ok("BIGINT".to_string()),
266 DataType::Decimal(t) => Ok(format!("DECIMAL({}, {})", t.precision(), t.scale())),
267 DataType::Double(_) => Ok("DOUBLE".to_string()),
268 DataType::Float(_) => Ok("FLOAT".to_string()),
269 DataType::Binary(t) => Ok(format!("BINARY({})", t.length())),
270 DataType::VarBinary(t) => Ok(format!("VARBINARY({})", t.length())),
271 DataType::Blob(_) => Ok("BLOB".to_string()),
272 DataType::Char(t) => Ok(format!("CHAR({})", t.length())),
273 DataType::VarChar(t) => Ok(format!("VARCHAR({})", t.length())),
274 DataType::Date(_) => Ok("DATE".to_string()),
275 DataType::Time(_) => Err(unsupported_show_create_table_type("TIME")),
276 DataType::Timestamp(t) => Ok(format!("TIMESTAMP({})", t.precision())),
277 DataType::Variant(_) => Ok("VARIANT".to_string()),
278 DataType::LocalZonedTimestamp(t) => {
279 Ok(format!("TIMESTAMP({}) WITH TIME ZONE", t.precision()))
280 }
281 DataType::Array(t) => Ok(format!("ARRAY<{}>", data_type_to_sql(t.element_type())?)),
282 DataType::Map(t) => Ok(format!(
283 "MAP({}, {})",
284 data_type_to_sql(t.key_type())?,
285 data_type_to_sql(t.value_type())?
286 )),
287 DataType::Multiset(_) => Err(unsupported_show_create_table_type("MULTISET")),
288 DataType::Row(t) => {
289 let inner: Vec<String> = t
290 .fields()
291 .iter()
292 .map(|f| {
293 let ty = data_type_to_sql(f.data_type())?;
294 if f.name().is_empty() {
295 Ok(ty)
296 } else {
297 Ok(format!("{} {}", quote_identifier(f.name()), ty))
298 }
299 })
300 .collect::<DFResult<_>>()?;
301 Ok(format!("STRUCT<{}>", inner.join(", ")))
302 }
303 DataType::Vector(_) => Err(unsupported_show_create_table_type("VECTOR")),
304 }
305}
306
307fn unsupported_show_create_table_type(type_name: &str) -> DataFusionError {
308 DataFusionError::NotImplemented(format!(
309 "SHOW CREATE TABLE does not support {type_name} columns because paimon-rust cannot round-trip this type in CREATE TABLE"
310 ))
311}
312
313pub(crate) fn bucket_round_robin<T>(items: Vec<T>, num_buckets: usize) -> Vec<Vec<T>> {
315 let mut buckets: Vec<Vec<T>> = (0..num_buckets).map(|_| Vec::new()).collect();
316 for (index, item) in items.into_iter().enumerate() {
317 buckets[index % num_buckets].push(item);
318 }
319 buckets
320}
321
322pub(crate) struct PaimonScanBuilder<'a> {
324 pub(crate) table: &'a Table,
325 pub(crate) schema: &'a ArrowSchemaRef,
326 pub(crate) plan: &'a paimon::table::Plan,
327 pub(crate) scan_trace: Option<paimon::table::ScanTrace>,
328 pub(crate) projection: Option<&'a Vec<usize>>,
329 pub(crate) pushed_predicate: Option<paimon::spec::Predicate>,
330 pub(crate) limit: Option<usize>,
331 pub(crate) target_partitions: usize,
332 pub(crate) filter_exact: bool,
333 pub(crate) case_sensitive: bool,
336}
337
338impl PaimonScanBuilder<'_> {
339 pub(crate) fn build(self) -> DFResult<Arc<dyn ExecutionPlan>> {
341 let read_fields = datafusion_read_fields(self.table);
342 self.build_with_read_fields(read_fields)
343 }
344
345 pub(crate) fn build_with_read_fields(
347 self,
348 read_fields: Vec<DataField>,
349 ) -> DFResult<Arc<dyn ExecutionPlan>> {
350 let (projected_schema, read_type) = if let Some(indices) = self.projection {
351 let fields: Vec<Field> = indices
352 .iter()
353 .map(|&i| self.schema.field(i).clone())
354 .collect();
355 let read_type = indices
356 .iter()
357 .map(|&i| read_fields[i].clone())
358 .collect::<Vec<_>>();
359 (Arc::new(Schema::new(fields)), read_type)
360 } else {
361 (self.schema.clone(), read_fields)
362 };
363
364 let splits = self.plan.splits().to_vec();
365 let planned_partitions: Vec<Arc<[_]>> = if splits.is_empty() {
366 vec![Arc::from(Vec::new())]
367 } else {
368 let num_partitions = splits.len().min(self.target_partitions.max(1));
369 bucket_round_robin(splits, num_partitions)
370 .into_iter()
371 .map(Arc::from)
372 .collect()
373 };
374
375 Ok(Arc::new(PaimonTableScan::new(
376 projected_schema,
377 self.table.clone(),
378 read_type,
379 self.pushed_predicate,
380 planned_partitions,
381 self.limit,
382 self.filter_exact,
383 self.scan_trace,
384 None,
385 self.case_sensitive,
386 )))
387 }
388}
389
390#[async_trait]
391impl TableProvider for PaimonTableProvider {
392 fn schema(&self) -> ArrowSchemaRef {
393 self.schema.clone()
394 }
395
396 fn table_type(&self) -> TableType {
397 TableType::Base
398 }
399
400 fn get_table_definition(&self) -> Option<&str> {
401 self.table_definition.as_deref()
402 }
403
404 async fn scan(
405 &self,
406 state: &dyn Session,
407 projection: Option<&Vec<usize>>,
408 filters: &[Expr],
409 limit: Option<usize>,
410 ) -> DFResult<Arc<dyn ExecutionPlan>> {
411 let case_sensitive = true;
423 let filter_analysis =
425 analyze_filters(filters, self.table.schema().fields(), case_sensitive);
426 let mut read_builder = self.table.new_read_builder();
427 read_builder.with_case_sensitive(case_sensitive);
428 if let Some(indices) = projection {
429 let read_fields = datafusion_read_fields(&self.table);
430 let read_type = indices
431 .iter()
432 .map(|&i| read_fields[i].clone())
433 .collect::<Vec<_>>();
434 read_builder.with_read_type(read_type);
435 }
436 if let Some(filter) = filter_analysis.pushed_predicate.clone() {
437 read_builder.with_filter(filter);
438 }
439 let pushed_limit = limit.filter(|_| !filter_analysis.requires_residual);
440 if let Some(limit) = pushed_limit {
441 read_builder.with_limit(limit);
442 }
443 let scan = read_builder.new_scan();
444 let (plan, scan_trace) = await_with_runtime(scan.plan_with_trace())
449 .await
450 .map_err(to_datafusion_error)?;
451
452 let target = state.config_options().execution.target_partitions;
453 let filter_exact = !filter_analysis.requires_residual
454 && filter_analysis
455 .pushed_predicate
456 .as_ref()
457 .is_none_or(|p| read_builder.is_exact_filter_pushdown(p));
458 PaimonScanBuilder {
459 table: &self.table,
460 schema: &self.schema,
461 plan: &plan,
462 scan_trace: Some(scan_trace),
463 projection,
464 pushed_predicate: filter_analysis.pushed_predicate,
465 limit: pushed_limit,
466 target_partitions: target,
467 filter_exact,
468 case_sensitive,
469 }
470 .build()
471 }
472
473 async fn insert_into(
474 &self,
475 _state: &dyn Session,
476 input: Arc<dyn ExecutionPlan>,
477 insert_op: InsertOp,
478 ) -> DFResult<Arc<dyn ExecutionPlan>> {
479 if self.table.is_branch_reference() {
480 return Err(datafusion::error::DataFusionError::NotImplemented(format!(
481 "Writing to Paimon branch '{}' is not supported",
482 self.table.branch()
483 )));
484 }
485 let overwrite = match insert_op {
486 InsertOp::Append => false,
487 InsertOp::Overwrite => true,
488 other => {
489 return Err(datafusion::error::DataFusionError::NotImplemented(format!(
490 "{other} is not supported for Paimon tables"
491 )));
492 }
493 };
494 let sink = PaimonDataSink::new(self.table.clone(), self.schema.clone(), overwrite);
495 Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None)))
496 }
497
498 fn supports_filters_pushdown(
499 &self,
500 filters: &[&Expr],
501 ) -> DFResult<Vec<TableProviderFilterPushDown>> {
502 let fields = self.table.schema().fields();
503 let case_sensitive = true;
506 let read_builder = self.table.new_read_builder();
507
508 Ok(filters
509 .iter()
510 .map(|filter| {
511 classify_filter_pushdown(filter, fields, case_sensitive, |predicate| {
512 read_builder.is_exact_filter_pushdown(predicate)
513 })
514 })
515 .collect())
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use std::collections::BTreeSet;
523 use std::sync::Arc;
524
525 use datafusion::datasource::TableProvider;
526 use datafusion::logical_expr::{col, lit, Expr};
527 use datafusion::prelude::{SessionConfig, SessionContext};
528 use paimon::catalog::Identifier;
529 use paimon::spec::{ArrayType, MapType, RowType, VarCharType};
530 use paimon::{Catalog, CatalogOptions, DataSplit, FileSystemCatalog, Options};
531
532 use crate::physical_plan::PaimonTableScan;
533
534 #[test]
535 fn test_bucket_round_robin_distributes_evenly() {
536 let result = bucket_round_robin(vec![0, 1, 2, 3, 4], 3);
537 assert_eq!(result, vec![vec![0, 3], vec![1, 4], vec![2]]);
538 }
539
540 #[test]
541 fn test_bucket_round_robin_fewer_items_than_buckets() {
542 let result = bucket_round_robin(vec![10, 20], 2);
543 assert_eq!(result, vec![vec![10], vec![20]]);
544 }
545
546 #[test]
547 fn test_bucket_round_robin_single_bucket() {
548 let result = bucket_round_robin(vec![1, 2, 3], 1);
549 assert_eq!(result, vec![vec![1, 2, 3]]);
550 }
551
552 fn get_test_warehouse() -> String {
553 std::env::var("PAIMON_TEST_WAREHOUSE")
554 .unwrap_or_else(|_| "/tmp/paimon-warehouse".to_string())
555 }
556
557 fn create_catalog() -> FileSystemCatalog {
558 let warehouse = get_test_warehouse();
559 let mut options = Options::new();
560 options.set(CatalogOptions::WAREHOUSE, warehouse);
561 FileSystemCatalog::new(options).expect("Failed to create catalog")
562 }
563
564 async fn create_provider(table_name: &str) -> PaimonTableProvider {
565 let catalog = create_catalog();
566 let identifier = Identifier::new("default", table_name);
567 let table = catalog
568 .get_table(&identifier)
569 .await
570 .expect("Failed to get table");
571
572 PaimonTableProvider::try_new(table).expect("Failed to create table provider")
573 }
574
575 async fn plan_partitions(
576 provider: &PaimonTableProvider,
577 filters: Vec<Expr>,
578 limit: Option<usize>,
579 ) -> Vec<Arc<[DataSplit]>> {
580 let plan = plan_scan(provider, filters, limit).await;
581 let scan = plan
582 .downcast_ref::<PaimonTableScan>()
583 .expect("Expected PaimonTableScan");
584
585 scan.planned_partitions().to_vec()
586 }
587
588 async fn plan_scan(
589 provider: &PaimonTableProvider,
590 filters: Vec<Expr>,
591 limit: Option<usize>,
592 ) -> Arc<dyn ExecutionPlan> {
593 let config = SessionConfig::new().with_target_partitions(8);
594 let ctx = SessionContext::new_with_config(config);
595 let state = ctx.state();
596 provider
597 .scan(&state, None, &filters, limit)
598 .await
599 .expect("scan() should succeed")
600 }
601
602 fn extract_dt_partition_set(planned_partitions: &[Arc<[DataSplit]>]) -> BTreeSet<String> {
603 planned_partitions
604 .iter()
605 .flat_map(|splits| splits.iter())
606 .map(|split| {
607 split
608 .partition()
609 .get_string(0)
610 .expect("Failed to decode dt")
611 .to_string()
612 })
613 .collect()
614 }
615
616 fn extract_dt_hr_partition_set(
617 planned_partitions: &[Arc<[DataSplit]>],
618 ) -> BTreeSet<(String, i32)> {
619 planned_partitions
620 .iter()
621 .flat_map(|splits| splits.iter())
622 .map(|split| {
623 let partition = split.partition();
624 (
625 partition
626 .get_string(0)
627 .expect("Failed to decode dt")
628 .to_string(),
629 partition.get_int(1).expect("Failed to decode hr"),
630 )
631 })
632 .collect()
633 }
634
635 fn empty_binary_stats_json() -> serde_json::Value {
636 let row = paimon::spec::EMPTY_SERIALIZED_ROW.as_slice().to_vec();
637 serde_json::json!({
638 "_MIN_VALUES": row,
639 "_MAX_VALUES": row,
640 "_NULL_COUNTS": [],
641 })
642 }
643
644 fn data_evolution_file(
645 file_name: &str,
646 file_size: i64,
647 row_count: i64,
648 first_row_id: i64,
649 write_cols: &[&str],
650 ) -> paimon::spec::DataFileMeta {
651 serde_json::from_value(serde_json::json!({
652 "_FILE_NAME": file_name,
653 "_FILE_SIZE": file_size,
654 "_ROW_COUNT": row_count,
655 "_MIN_KEY": [],
656 "_MAX_KEY": [],
657 "_KEY_STATS": empty_binary_stats_json(),
658 "_VALUE_STATS": empty_binary_stats_json(),
659 "_MIN_SEQUENCE_NUMBER": 0,
660 "_MAX_SEQUENCE_NUMBER": 0,
661 "_SCHEMA_ID": 0,
662 "_LEVEL": 1,
663 "_EXTRA_FILES": [],
664 "_CREATION_TIME": null,
665 "_DELETE_ROW_COUNT": null,
666 "_EMBEDDED_FILE_INDEX": null,
667 "_FILE_SOURCE": null,
668 "_VALUE_STATS_COLS": null,
669 "_FIRST_ROW_ID": first_row_id,
670 "_WRITE_COLS": write_cols,
671 "_EXTERNAL_PATH": null,
672 }))
673 .expect("test data file should deserialize")
674 }
675
676 fn manifest_file_meta(
677 file_name: &str,
678 file_size: i64,
679 num_added_files: i64,
680 ) -> paimon::spec::ManifestFileMeta {
681 serde_json::from_value(serde_json::json!({
682 "_VERSION": 2,
683 "_FILE_NAME": file_name,
684 "_FILE_SIZE": file_size,
685 "_NUM_ADDED_FILES": num_added_files,
686 "_NUM_DELETED_FILES": 0,
687 "_PARTITION_STATS": empty_binary_stats_json(),
688 "_SCHEMA_ID": 0,
689 }))
690 .expect("test manifest file meta should deserialize")
691 }
692
693 async fn data_evolution_projection_pruning_provider() -> PaimonTableProvider {
694 use paimon::io::FileIOBuilder;
695 use paimon::spec::{
696 CommitKind, DataType, FileKind, IntType, Manifest, ManifestEntry, ManifestList,
697 Schema as PaimonSchema, Snapshot, TableSchema,
698 };
699 use paimon::table::{SnapshotManager, Table};
700
701 let file_io = FileIOBuilder::new("memory").build().unwrap();
702 let table_path = format!("memory:/df_de_projection_pruning_{}", uuid::Uuid::new_v4());
703 file_io
704 .mkdirs(&format!("{table_path}/snapshot/"))
705 .await
706 .unwrap();
707 file_io
708 .mkdirs(&format!("{table_path}/manifest/"))
709 .await
710 .unwrap();
711
712 let schema = PaimonSchema::builder()
713 .column("id", DataType::Int(IntType::new()))
714 .column("name", DataType::Int(IntType::new()))
715 .option("data-evolution.enabled", "true")
716 .build()
717 .unwrap();
718 let table_schema = TableSchema::new(0, &schema);
719 let table = Table::new(
720 file_io.clone(),
721 Identifier::new("default", "df_de_projection_pruning"),
722 table_path.clone(),
723 table_schema,
724 None,
725 );
726
727 let partition = paimon::spec::EMPTY_SERIALIZED_ROW.as_slice().to_vec();
728 let entries = vec![
729 ManifestEntry::new(
730 FileKind::Add,
731 partition.clone(),
732 0,
733 1,
734 data_evolution_file("id.parquet", 11, 10, 0, &["id"]),
735 2,
736 ),
737 ManifestEntry::new(
738 FileKind::Add,
739 partition,
740 0,
741 1,
742 data_evolution_file("name.parquet", 13, 10, 0, &["name"]),
743 2,
744 ),
745 ];
746
747 let manifest_name = "manifest-de-projection-0";
748 let manifest_path = format!("{table_path}/manifest/{manifest_name}");
749 Manifest::write(&file_io, &manifest_path, &entries)
750 .await
751 .unwrap();
752 let manifest_size = file_io
753 .new_input(&manifest_path)
754 .unwrap()
755 .metadata()
756 .await
757 .unwrap()
758 .size;
759
760 let base_list_name = "base-list-de-projection";
761 let delta_list_name = "delta-list-de-projection";
762 ManifestList::write(
763 &file_io,
764 &format!("{table_path}/manifest/{base_list_name}"),
765 &[manifest_file_meta(
766 manifest_name,
767 manifest_size as i64,
768 entries.len() as i64,
769 )],
770 )
771 .await
772 .unwrap();
773 ManifestList::write(
774 &file_io,
775 &format!("{table_path}/manifest/{delta_list_name}"),
776 &[],
777 )
778 .await
779 .unwrap();
780
781 let snapshot = Snapshot::builder()
782 .version(3)
783 .id(1)
784 .schema_id(0)
785 .base_manifest_list(base_list_name.to_string())
786 .delta_manifest_list(delta_list_name.to_string())
787 .commit_user("test-user".to_string())
788 .commit_identifier(1)
789 .commit_kind(CommitKind::APPEND)
790 .time_millis(1)
791 .total_record_count(Some(10))
792 .delta_record_count(Some(10))
793 .build();
794 let snapshot_manager = SnapshotManager::new(file_io, table_path);
795 assert!(snapshot_manager.commit_snapshot(&snapshot).await.unwrap());
796
797 PaimonTableProvider::try_new(table).expect("provider should be created")
798 }
799
800 #[tokio::test]
801 async fn test_datafusion_schema_hides_paimon_field_ids() {
802 let provider = data_evolution_projection_pruning_provider().await;
803
804 for field in provider.schema().fields() {
805 assert!(
806 !field.metadata().contains_key("PARQUET:field_id"),
807 "storage field id leaked through DataFusion schema for {}",
808 field.name()
809 );
810 }
811 }
812
813 #[test]
814 fn test_datafusion_schema_uses_views_only_for_top_level_strings() {
815 let string_type = || DataType::VarChar(VarCharType::string_type());
816 let schema = datafusion_arrow_schema(
817 &[
818 DataField::new(0, "plain".to_string(), string_type()),
819 DataField::new(
820 1,
821 "array".to_string(),
822 DataType::Array(ArrayType::new(string_type())),
823 ),
824 DataField::new(
825 2,
826 "map".to_string(),
827 DataType::Map(MapType::new(string_type(), string_type())),
828 ),
829 DataField::new(
830 3,
831 "row".to_string(),
832 DataType::Row(RowType::new(vec![DataField::new(
833 4,
834 "nested".to_string(),
835 string_type(),
836 )])),
837 ),
838 ],
839 true,
840 )
841 .expect("DataFusion schema should be created");
842
843 assert_eq!(schema.field(0).data_type(), &ArrowDataType::Utf8View);
844
845 let ArrowDataType::List(element) = schema.field(1).data_type() else {
846 panic!("array field should map to an Arrow List");
847 };
848 assert_eq!(element.data_type(), &ArrowDataType::Utf8);
849
850 let ArrowDataType::Map(entries, _) = schema.field(2).data_type() else {
851 panic!("map field should map to an Arrow Map");
852 };
853 let ArrowDataType::Struct(map_fields) = entries.data_type() else {
854 panic!("map entries should map to an Arrow Struct");
855 };
856 assert_eq!(map_fields[0].data_type(), &ArrowDataType::Utf8);
857 assert_eq!(map_fields[1].data_type(), &ArrowDataType::Utf8);
858
859 let ArrowDataType::Struct(row_fields) = schema.field(3).data_type() else {
860 panic!("row field should map to an Arrow Struct");
861 };
862 assert_eq!(row_fields[0].data_type(), &ArrowDataType::Utf8);
863 }
864
865 fn planned_file_names(scan: &PaimonTableScan) -> Vec<String> {
866 let mut names = scan
867 .planned_partitions()
868 .iter()
869 .flat_map(|partition| partition.iter())
870 .flat_map(|split| split.data_files().iter())
871 .map(|file| file.file_name.clone())
872 .collect::<Vec<_>>();
873 names.sort();
874 names
875 }
876
877 #[tokio::test]
878 async fn test_scan_partition_filter_plans_matching_partition_set() {
879 let provider = create_provider("partitioned_log_table").await;
880 let planned_partitions =
881 plan_partitions(&provider, vec![col("dt").eq(lit("2024-01-01"))], None).await;
882
883 assert_eq!(
884 extract_dt_partition_set(&planned_partitions),
885 BTreeSet::from(["2024-01-01".to_string()]),
886 );
887 }
888
889 #[tokio::test]
890 async fn test_scan_mixed_and_filter_keeps_partition_pruning() {
891 let provider = create_provider("partitioned_log_table").await;
892 let planned_partitions = plan_partitions(
893 &provider,
894 vec![col("dt").eq(lit("2024-01-01")).and(col("id").gt(lit(1)))],
895 None,
896 )
897 .await;
898
899 assert_eq!(
900 extract_dt_partition_set(&planned_partitions),
901 BTreeSet::from(["2024-01-01".to_string()]),
902 );
903 }
904
905 #[tokio::test]
906 async fn test_scan_multi_partition_filter_plans_exact_partition_set() {
907 let provider = create_provider("multi_partitioned_log_table").await;
908
909 let dt_only_partitions =
910 plan_partitions(&provider, vec![col("dt").eq(lit("2024-01-01"))], None).await;
911 let dt_hr_partitions = plan_partitions(
912 &provider,
913 vec![col("dt").eq(lit("2024-01-01")).and(col("hr").eq(lit(10)))],
914 None,
915 )
916 .await;
917
918 assert_eq!(
919 extract_dt_hr_partition_set(&dt_only_partitions),
920 BTreeSet::from([
921 ("2024-01-01".to_string(), 10),
922 ("2024-01-01".to_string(), 20),
923 ]),
924 );
925 assert_eq!(
926 extract_dt_hr_partition_set(&dt_hr_partitions),
927 BTreeSet::from([("2024-01-01".to_string(), 10)]),
928 );
929 }
930
931 #[tokio::test]
932 async fn test_scan_partially_translated_not_filter_prunes_partitions_but_skips_limit_hint() {
933 let provider = create_provider("multi_partitioned_log_table").await;
934 let filter = col("dt")
935 .eq(lit("2024-01-01"))
936 .and(Expr::Not(Box::new(col("hr").eq(lit(10)))));
937 let full_plan = plan_partitions(&provider, vec![filter.clone()], None).await;
938 let plan = plan_scan(&provider, vec![filter], Some(1)).await;
939 let scan = plan
940 .downcast_ref::<PaimonTableScan>()
941 .expect("Expected PaimonTableScan");
942
943 assert_eq!(scan.limit(), None);
944 assert_eq!(
945 extract_dt_hr_partition_set(scan.planned_partitions()),
946 BTreeSet::from([("2024-01-01".to_string(), 20)]),
947 );
948 assert_eq!(
949 scan.planned_partitions()
950 .iter()
951 .map(|partition| partition.len())
952 .sum::<usize>(),
953 full_plan
954 .iter()
955 .map(|partition| partition.len())
956 .sum::<usize>()
957 );
958 }
959
960 #[tokio::test]
961 async fn test_scan_keeps_pushed_predicate_for_execute() {
962 let provider = create_provider("partitioned_log_table").await;
963 let filter = col("id").gt(lit(1));
964
965 let config = SessionConfig::new().with_target_partitions(8);
966 let ctx = SessionContext::new_with_config(config);
967 let state = ctx.state();
968 let plan = provider
969 .scan(&state, None, std::slice::from_ref(&filter), None)
970 .await
971 .expect("scan() should succeed");
972 let scan = plan
973 .downcast_ref::<PaimonTableScan>()
974 .expect("Expected PaimonTableScan");
975
976 let expected = build_pushed_predicate(&[filter], provider.table().schema().fields())
977 .expect("data filter should translate");
978
979 assert_eq!(scan.pushed_predicate(), Some(&expected));
980 }
981
982 #[tokio::test]
983 async fn test_scan_pushes_not_as_inexact_and_skips_limit_hint() {
984 let provider = data_evolution_projection_pruning_provider().await;
985 let filter = Expr::Not(Box::new(col("id").eq(lit(1))));
986 let plan = plan_scan(&provider, vec![filter.clone()], Some(1)).await;
987 let scan = plan
988 .downcast_ref::<PaimonTableScan>()
989 .expect("Expected PaimonTableScan");
990
991 let expected = build_pushed_predicate(&[filter], provider.table().schema().fields())
992 .expect("NOT filter should translate as inexact pushdown");
993
994 assert_eq!(scan.pushed_predicate(), Some(&expected));
995 assert!(!scan.filter_exact());
996 assert_eq!(scan.limit(), None);
997 }
998
999 #[tokio::test]
1000 async fn test_scan_applies_projection_to_data_evolution_planning() {
1001 let provider = data_evolution_projection_pruning_provider().await;
1002 let config = SessionConfig::new().with_target_partitions(8);
1003 let ctx = SessionContext::new_with_config(config);
1004 let state = ctx.state();
1005
1006 let full_plan = provider
1007 .scan(&state, None, &[], None)
1008 .await
1009 .expect("full scan should succeed");
1010 let full_scan = full_plan
1011 .downcast_ref::<PaimonTableScan>()
1012 .expect("Expected PaimonTableScan");
1013 assert_eq!(
1014 planned_file_names(full_scan),
1015 vec!["id.parquet".to_string(), "name.parquet".to_string()]
1016 );
1017
1018 let projection = vec![1];
1019 let projected_plan = provider
1020 .scan(&state, Some(&projection), &[], None)
1021 .await
1022 .expect("projected scan should succeed");
1023 let projected_scan = projected_plan
1024 .downcast_ref::<PaimonTableScan>()
1025 .expect("Expected PaimonTableScan");
1026
1027 assert_eq!(
1028 planned_file_names(projected_scan),
1029 vec!["name.parquet".to_string()]
1030 );
1031 }
1032
1033 #[tokio::test]
1034 async fn test_scan_applies_limit_hint_only_when_safe() {
1035 let provider = create_provider("partitioned_log_table").await;
1036 let full_plan = plan_partitions(&provider, vec![], None).await;
1037 let plan = plan_scan(&provider, vec![], Some(1)).await;
1038 let scan = plan
1039 .downcast_ref::<PaimonTableScan>()
1040 .expect("Expected PaimonTableScan");
1041
1042 assert_eq!(scan.limit(), Some(1));
1043 assert!(
1044 scan.planned_partitions()
1045 .iter()
1046 .map(|partition| partition.len())
1047 .sum::<usize>()
1048 < full_plan
1049 .iter()
1050 .map(|partition| partition.len())
1051 .sum::<usize>()
1052 );
1053 }
1054
1055 #[tokio::test]
1056 async fn test_scan_keeps_limit_but_skips_limit_pruning_for_data_filters() {
1057 let provider = create_provider("partitioned_log_table").await;
1058 let filter = col("id").gt(lit(1));
1059 let full_plan = plan_partitions(&provider, vec![filter.clone()], None).await;
1060 let plan = plan_scan(&provider, vec![filter], Some(1)).await;
1061 let scan = plan
1062 .downcast_ref::<PaimonTableScan>()
1063 .expect("Expected PaimonTableScan");
1064
1065 assert_eq!(scan.limit(), Some(1));
1066 assert_eq!(
1067 scan.planned_partitions()
1068 .iter()
1069 .map(|partition| partition.len())
1070 .sum::<usize>(),
1071 full_plan
1072 .iter()
1073 .map(|partition| partition.len())
1074 .sum::<usize>()
1075 );
1076 }
1077
1078 #[tokio::test]
1079 async fn test_insert_into_and_read_back() {
1080 use paimon::io::FileIOBuilder;
1081 use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema};
1082
1083 let file_io = FileIOBuilder::new("memory").build().unwrap();
1084 let table_path = "memory:/test_df_insert_into";
1085 file_io
1086 .mkdirs(&format!("{table_path}/snapshot/"))
1087 .await
1088 .unwrap();
1089 file_io
1090 .mkdirs(&format!("{table_path}/manifest/"))
1091 .await
1092 .unwrap();
1093
1094 let schema = PaimonSchema::builder()
1095 .column("id", DataType::Int(IntType::new()))
1096 .column("value", DataType::Int(IntType::new()))
1097 .build()
1098 .unwrap();
1099 let table_schema = TableSchema::new(0, &schema);
1100 let table = paimon::table::Table::new(
1101 file_io,
1102 Identifier::new("default", "test_insert"),
1103 table_path.to_string(),
1104 table_schema,
1105 None,
1106 );
1107
1108 let provider = PaimonTableProvider::try_new(table).unwrap();
1109 let ctx = SessionContext::new();
1110 ctx.register_table("t", Arc::new(provider)).unwrap();
1111
1112 let result = ctx
1114 .sql("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)")
1115 .await
1116 .unwrap()
1117 .collect()
1118 .await
1119 .unwrap();
1120
1121 let count_array = result[0]
1123 .column(0)
1124 .as_any()
1125 .downcast_ref::<datafusion::arrow::array::UInt64Array>()
1126 .unwrap();
1127 assert_eq!(count_array.value(0), 3);
1128
1129 let batches = ctx
1131 .sql("SELECT id, value FROM t ORDER BY id")
1132 .await
1133 .unwrap()
1134 .collect()
1135 .await
1136 .unwrap();
1137
1138 let mut rows = Vec::new();
1139 for batch in &batches {
1140 let ids = batch
1141 .column(0)
1142 .as_any()
1143 .downcast_ref::<datafusion::arrow::array::Int32Array>()
1144 .unwrap();
1145 let vals = batch
1146 .column(1)
1147 .as_any()
1148 .downcast_ref::<datafusion::arrow::array::Int32Array>()
1149 .unwrap();
1150 for i in 0..batch.num_rows() {
1151 rows.push((ids.value(i), vals.value(i)));
1152 }
1153 }
1154 assert_eq!(rows, vec![(1, 10), (2, 20), (3, 30)]);
1155 }
1156
1157 #[tokio::test]
1158 async fn test_insert_overwrite() {
1159 use paimon::io::FileIOBuilder;
1160 use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema, VarCharType};
1161
1162 let file_io = FileIOBuilder::new("memory").build().unwrap();
1163 let table_path = "memory:/test_df_insert_overwrite";
1164 file_io
1165 .mkdirs(&format!("{table_path}/snapshot/"))
1166 .await
1167 .unwrap();
1168 file_io
1169 .mkdirs(&format!("{table_path}/manifest/"))
1170 .await
1171 .unwrap();
1172
1173 let schema = PaimonSchema::builder()
1174 .column("pt", DataType::VarChar(VarCharType::string_type()))
1175 .column("id", DataType::Int(IntType::new()))
1176 .partition_keys(["pt"])
1177 .build()
1178 .unwrap();
1179 let table_schema = TableSchema::new(0, &schema);
1180 let table = paimon::table::Table::new(
1181 file_io,
1182 Identifier::new("default", "test_overwrite"),
1183 table_path.to_string(),
1184 table_schema,
1185 None,
1186 );
1187
1188 let provider = PaimonTableProvider::try_new(table).unwrap();
1189 let ctx = SessionContext::new();
1190 ctx.register_table("t", Arc::new(provider)).unwrap();
1191
1192 ctx.sql("INSERT INTO t VALUES ('a', 1), ('a', 2), ('b', 3), ('b', 4)")
1194 .await
1195 .unwrap()
1196 .collect()
1197 .await
1198 .unwrap();
1199
1200 ctx.sql("INSERT OVERWRITE t VALUES ('a', 10), ('a', 20)")
1203 .await
1204 .unwrap()
1205 .collect()
1206 .await
1207 .unwrap();
1208
1209 let batches = ctx
1211 .sql("SELECT pt, id FROM t ORDER BY pt, id")
1212 .await
1213 .unwrap()
1214 .collect()
1215 .await
1216 .unwrap();
1217
1218 let mut rows = Vec::new();
1219 for batch in &batches {
1220 let pts = batch
1221 .column(0)
1222 .as_any()
1223 .downcast_ref::<datafusion::arrow::array::StringViewArray>()
1224 .unwrap();
1225 let ids = batch
1226 .column(1)
1227 .as_any()
1228 .downcast_ref::<datafusion::arrow::array::Int32Array>()
1229 .unwrap();
1230 for i in 0..batch.num_rows() {
1231 rows.push((pts.value(i).to_string(), ids.value(i)));
1232 }
1233 }
1234 assert_eq!(
1236 rows,
1237 vec![
1238 ("a".to_string(), 10),
1239 ("a".to_string(), 20),
1240 ("b".to_string(), 3),
1241 ("b".to_string(), 4),
1242 ]
1243 );
1244 }
1245
1246 #[tokio::test]
1247 async fn test_insert_overwrite_unpartitioned() {
1248 use paimon::io::FileIOBuilder;
1249 use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema};
1250
1251 let file_io = FileIOBuilder::new("memory").build().unwrap();
1252 let table_path = "memory:/test_df_insert_overwrite_unpart";
1253 file_io
1254 .mkdirs(&format!("{table_path}/snapshot/"))
1255 .await
1256 .unwrap();
1257 file_io
1258 .mkdirs(&format!("{table_path}/manifest/"))
1259 .await
1260 .unwrap();
1261
1262 let schema = PaimonSchema::builder()
1263 .column("id", DataType::Int(IntType::new()))
1264 .column("value", DataType::Int(IntType::new()))
1265 .build()
1266 .unwrap();
1267 let table_schema = TableSchema::new(0, &schema);
1268 let table = paimon::table::Table::new(
1269 file_io,
1270 Identifier::new("default", "test_overwrite_unpart"),
1271 table_path.to_string(),
1272 table_schema,
1273 None,
1274 );
1275
1276 let provider = PaimonTableProvider::try_new(table).unwrap();
1277 let ctx = SessionContext::new();
1278 ctx.register_table("t", Arc::new(provider)).unwrap();
1279
1280 ctx.sql("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)")
1282 .await
1283 .unwrap()
1284 .collect()
1285 .await
1286 .unwrap();
1287
1288 ctx.sql("INSERT OVERWRITE t VALUES (4, 40), (5, 50)")
1290 .await
1291 .unwrap()
1292 .collect()
1293 .await
1294 .unwrap();
1295
1296 let batches = ctx
1297 .sql("SELECT id, value FROM t ORDER BY id")
1298 .await
1299 .unwrap()
1300 .collect()
1301 .await
1302 .unwrap();
1303
1304 let mut rows = Vec::new();
1305 for batch in &batches {
1306 let ids = batch
1307 .column(0)
1308 .as_any()
1309 .downcast_ref::<datafusion::arrow::array::Int32Array>()
1310 .unwrap();
1311 let vals = batch
1312 .column(1)
1313 .as_any()
1314 .downcast_ref::<datafusion::arrow::array::Int32Array>()
1315 .unwrap();
1316 for i in 0..batch.num_rows() {
1317 rows.push((ids.value(i), vals.value(i)));
1318 }
1319 }
1320 assert_eq!(rows, vec![(4, 40), (5, 50)]);
1322 }
1323}