paimon_datafusion/physical_plan/
scan.rs1use std::any::Any;
19use std::sync::Arc;
20
21use datafusion::arrow::datatypes::SchemaRef as ArrowSchemaRef;
22use datafusion::common::stats::Precision;
23use datafusion::common::Statistics;
24use datafusion::error::Result as DFResult;
25use datafusion::execution::{SendableRecordBatchStream, TaskContext};
26use datafusion::physical_expr::EquivalenceProperties;
27use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
28use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
29use datafusion::physical_plan::{DisplayAs, ExecutionPlan, Partitioning, PlanProperties};
30use futures::{StreamExt, TryStreamExt};
31use paimon::spec::Predicate;
32use paimon::table::Table;
33use paimon::DataSplit;
34
35use crate::error::to_datafusion_error;
36
37#[derive(Debug)]
43pub struct PaimonTableScan {
44 table: Table,
45 projected_columns: Option<Vec<String>>,
47 pushed_predicate: Option<Predicate>,
50 planned_partitions: Vec<Arc<[DataSplit]>>,
54 plan_properties: Arc<PlanProperties>,
55 limit: Option<usize>,
57 filter_exact: bool,
60}
61
62impl PaimonTableScan {
63 pub(crate) fn new(
64 schema: ArrowSchemaRef,
65 table: Table,
66 projected_columns: Option<Vec<String>>,
67 pushed_predicate: Option<Predicate>,
68 planned_partitions: Vec<Arc<[DataSplit]>>,
69 limit: Option<usize>,
70 filter_exact: bool,
71 ) -> Self {
72 let plan_properties = Arc::new(PlanProperties::new(
73 EquivalenceProperties::new(schema.clone()),
74 Partitioning::UnknownPartitioning(planned_partitions.len()),
75 EmissionType::Incremental,
76 Boundedness::Bounded,
77 ));
78 Self {
79 table,
80 projected_columns,
81 pushed_predicate,
82 planned_partitions,
83 plan_properties,
84 limit,
85 filter_exact,
86 }
87 }
88
89 pub fn table(&self) -> &Table {
90 &self.table
91 }
92
93 #[cfg(test)]
94 pub(crate) fn planned_partitions(&self) -> &[Arc<[DataSplit]>] {
95 &self.planned_partitions
96 }
97
98 #[cfg(test)]
99 pub(crate) fn pushed_predicate(&self) -> Option<&Predicate> {
100 self.pushed_predicate.as_ref()
101 }
102
103 pub fn limit(&self) -> Option<usize> {
104 self.limit
105 }
106}
107
108impl ExecutionPlan for PaimonTableScan {
109 fn name(&self) -> &str {
110 "PaimonTableScan"
111 }
112
113 fn as_any(&self) -> &dyn Any {
114 self
115 }
116
117 fn properties(&self) -> &Arc<PlanProperties> {
118 &self.plan_properties
119 }
120
121 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan + 'static>> {
122 vec![]
123 }
124
125 fn with_new_children(
126 self: Arc<Self>,
127 _children: Vec<Arc<dyn ExecutionPlan>>,
128 ) -> DFResult<Arc<dyn ExecutionPlan>> {
129 Ok(self)
130 }
131
132 fn execute(
133 &self,
134 partition: usize,
135 _context: Arc<TaskContext>,
136 ) -> DFResult<SendableRecordBatchStream> {
137 let splits = Arc::clone(self.planned_partitions.get(partition).ok_or_else(|| {
138 datafusion::error::DataFusionError::Internal(format!(
139 "PaimonTableScan: partition index {partition} out of range (total {})",
140 self.planned_partitions.len()
141 ))
142 })?);
143
144 let table = self.table.clone();
145 let schema = self.schema();
146 let projected_columns = self.projected_columns.clone();
147 let pushed_predicate = self.pushed_predicate.clone();
148
149 let fut = async move {
150 let mut read_builder = table.new_read_builder();
151
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 if let Some(filter) = pushed_predicate {
157 read_builder.with_filter(filter);
158 }
159
160 let read = read_builder.new_read().map_err(to_datafusion_error)?;
161 let stream = read.to_arrow(&splits).map_err(to_datafusion_error)?;
162 let stream = stream.map(|r| r.map_err(to_datafusion_error));
163
164 Ok::<_, datafusion::error::DataFusionError>(RecordBatchStreamAdapter::new(
165 schema,
166 Box::pin(stream),
167 ))
168 };
169
170 Ok(Box::pin(RecordBatchStreamAdapter::new(
171 self.schema(),
172 futures::stream::once(fut).try_flatten(),
173 )))
174 }
175
176 fn partition_statistics(&self, partition: Option<usize>) -> DFResult<Statistics> {
177 let partitions: &[Arc<[DataSplit]>] = match partition {
178 Some(idx) => std::slice::from_ref(&self.planned_partitions[idx]),
179 None => &self.planned_partitions,
180 };
181
182 let mut total_rows: usize = 0;
183 let mut total_bytes: usize = 0;
184 let mut all_row_counts_known = true;
185 for splits in partitions {
186 for split in splits.iter() {
187 if let Some(row_count) = split.merged_row_count() {
188 total_rows += row_count as usize;
189 } else {
190 all_row_counts_known = false;
191 total_rows += split.row_count() as usize;
192 }
193 for file in split.data_files() {
194 total_bytes += file.file_size as usize;
195 }
196 }
197 }
198
199 let num_rows_precision =
204 if all_row_counts_known && self.limit.is_none() && self.filter_exact {
205 Precision::Exact(total_rows)
206 } else {
207 Precision::Inexact(total_rows)
208 };
209
210 Ok(Statistics {
211 num_rows: num_rows_precision,
212 total_byte_size: Precision::Inexact(total_bytes),
213 column_statistics: Statistics::unknown_column(&self.schema()),
214 })
215 }
216}
217
218impl DisplayAs for PaimonTableScan {
219 fn fmt_as(
220 &self,
221 _t: datafusion::physical_plan::DisplayFormatType,
222 f: &mut std::fmt::Formatter,
223 ) -> std::fmt::Result {
224 write!(f, "PaimonTableScan: table={}", self.table.identifier())?;
225
226 let total_splits: usize = self.planned_partitions.iter().map(|p| p.len()).sum();
227 let total_files: usize = self
228 .planned_partitions
229 .iter()
230 .flat_map(|p| p.iter())
231 .map(|s| s.data_files().len())
232 .sum();
233 write!(
234 f,
235 ", partitions={}, splits={total_splits}, files={total_files}",
236 self.planned_partitions.len()
237 )?;
238
239 if let Some(ref columns) = self.projected_columns {
240 write!(f, ", projection=[{}]", columns.join(", "))?;
241 }
242 if let Some(ref predicate) = self.pushed_predicate {
243 write!(f, ", predicate={predicate}")?;
244 }
245 if let Some(limit) = self.limit {
246 write!(f, ", limit={limit}")?;
247 }
248 Ok(())
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 mod test_utils {
256 include!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../test_utils.rs"));
257 }
258
259 use datafusion::arrow::array::Int32Array;
260 use datafusion::arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
261 use datafusion::physical_plan::ExecutionPlan;
262 use datafusion::prelude::SessionContext;
263 use futures::TryStreamExt;
264 use paimon::catalog::Identifier;
265 use paimon::io::FileIOBuilder;
266 use paimon::spec::{
267 BinaryRow, DataType, Datum, IntType, PredicateBuilder, Schema as PaimonSchema, TableSchema,
268 };
269 use paimon::table::Table;
270 use std::fs;
271 use tempfile::tempdir;
272 use test_utils::{local_file_path, test_data_file, write_int_parquet_file};
273
274 fn test_schema() -> ArrowSchemaRef {
275 Arc::new(ArrowSchema::new(vec![Field::new(
276 "id",
277 ArrowDataType::Int32,
278 false,
279 )]))
280 }
281
282 #[test]
283 fn test_partition_count_empty_plan() {
284 let schema = test_schema();
285 let scan = PaimonTableScan::new(
286 schema,
287 dummy_table(),
288 None,
289 None,
290 vec![Arc::from(Vec::new())],
291 None,
292 false,
293 );
294 assert_eq!(scan.properties().output_partitioning().partition_count(), 1);
295 }
296
297 #[test]
298 fn test_partition_count_multiple_partitions() {
299 let schema = test_schema();
300 let planned_partitions = vec![
301 Arc::from(Vec::new()),
302 Arc::from(Vec::new()),
303 Arc::from(Vec::new()),
304 ];
305 let scan = PaimonTableScan::new(
306 schema,
307 dummy_table(),
308 None,
309 None,
310 planned_partitions,
311 None,
312 false,
313 );
314 assert_eq!(scan.properties().output_partitioning().partition_count(), 3);
315 }
316
317 fn dummy_table() -> Table {
320 let file_io = FileIOBuilder::new("file").build().unwrap();
321 let schema = PaimonSchema::builder().build().unwrap();
322 let table_schema = TableSchema::new(0, &schema);
323 Table::new(
324 file_io,
325 Identifier::new("test_db", "test_table"),
326 "/tmp/test-table".to_string(),
327 table_schema,
328 None,
329 )
330 }
331
332 #[tokio::test]
333 async fn test_execute_applies_pushed_filter_during_read() {
334 let tempdir = tempdir().unwrap();
335 let table_path = local_file_path(tempdir.path());
336 let bucket_dir = tempdir.path().join("bucket-0");
337 fs::create_dir_all(&bucket_dir).unwrap();
338
339 write_int_parquet_file(
340 &bucket_dir.join("data.parquet"),
341 vec![("id", vec![1, 2, 3, 4]), ("value", vec![5, 20, 30, 40])],
342 Some(2),
343 );
344 let file_size = fs::metadata(bucket_dir.join("data.parquet")).unwrap().len() as i64;
345
346 let file_io = FileIOBuilder::new("file").build().unwrap();
347 let table_schema = TableSchema::new(
348 0,
349 &paimon::spec::Schema::builder()
350 .column("id", DataType::Int(IntType::new()))
351 .column("value", DataType::Int(IntType::new()))
352 .build()
353 .unwrap(),
354 );
355 let table = Table::new(
356 file_io,
357 Identifier::new("default", "t"),
358 table_path,
359 table_schema,
360 None,
361 );
362
363 let split = paimon::DataSplitBuilder::new()
364 .with_snapshot(1)
365 .with_partition(BinaryRow::new(0))
366 .with_bucket(0)
367 .with_bucket_path(local_file_path(&bucket_dir))
368 .with_total_buckets(1)
369 .with_data_files(vec![test_data_file("data.parquet", 4, file_size)])
370 .build()
371 .unwrap();
372
373 let pushed_predicate = PredicateBuilder::new(table.schema().fields())
374 .greater_or_equal("value", Datum::Int(10))
375 .unwrap();
376
377 let schema = Arc::new(ArrowSchema::new(vec![Field::new(
378 "id",
379 ArrowDataType::Int32,
380 false,
381 )]));
382 let scan = PaimonTableScan::new(
383 schema,
384 table,
385 Some(vec!["id".to_string()]),
386 Some(pushed_predicate),
387 vec![Arc::from(vec![split])],
388 None,
389 false,
390 );
391
392 let ctx = SessionContext::new();
393 let stream = scan
394 .execute(0, ctx.task_ctx())
395 .expect("execute should succeed");
396 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
397
398 let actual_ids: Vec<i32> = batches
399 .iter()
400 .flat_map(|batch| {
401 let ids = batch
402 .column(0)
403 .as_any()
404 .downcast_ref::<Int32Array>()
405 .expect("id column should be Int32Array");
406 (0..ids.len()).map(|idx| ids.value(idx)).collect::<Vec<_>>()
407 })
408 .collect();
409
410 assert_eq!(actual_ids, vec![2, 3, 4]);
411 }
412}