1#![doc(
19 html_logo_url = "https://raw.githubusercontent.com/apache/datafusion/19fe44cf2f30cbdd63d4a4f52c74055163c6cc38/docs/logos/standalone_logo/logo_original.svg",
20 html_favicon_url = "https://raw.githubusercontent.com/apache/datafusion/19fe44cf2f30cbdd63d4a4f52c74055163c6cc38/docs/logos/standalone_logo/logo_original.svg"
21)]
22#![cfg_attr(docsrs, feature(doc_cfg))]
23#![cfg_attr(not(test), deny(clippy::clone_on_ref_ptr))]
26#![cfg_attr(test, allow(clippy::needless_pass_by_value))]
27
28pub mod boundary_stream;
32pub mod decoder;
33pub mod display;
34pub mod file;
35pub mod file_compression_type;
36pub mod file_format;
37pub mod file_groups;
38pub mod file_scan_config;
39pub mod file_sink_config;
40pub mod file_stream;
41pub mod memory;
42pub mod morsel;
43pub mod projection;
44#[cfg(feature = "proto")]
47mod proto;
48pub mod schema_adapter;
49pub mod sink;
50pub mod source;
51mod statistics;
52pub mod table_schema;
53
54#[cfg(test)]
55pub mod test_util;
56
57pub mod url;
58pub mod write;
59pub use self::file::as_file_source;
60pub use self::url::ListingTableUrl;
61use crate::file_groups::FileGroup;
62use arrow::datatypes::SchemaRef;
63use chrono::TimeZone;
64use datafusion_common::stats::Precision;
65use datafusion_common::{ColumnStatistics, Result, TableReference};
66use datafusion_common::{ScalarValue, Statistics};
67use datafusion_physical_expr::LexOrdering;
68use futures::Stream;
69use object_store::{ObjectMeta, path::Path};
70pub use statistics::compute_all_files_statistics;
71use std::any::Any;
72use std::pin::Pin;
73use std::sync::Arc;
74pub use table_schema::{TableSchema, TableSchemaBuilder};
75
76pub type FileExtensions = datafusion_common::extensions::Extensions;
82
83#[deprecated(
85 since = "54.0.0",
86 note = "This type is unused and will be removed in a future release"
87)]
88pub type PartitionedFileStream =
89 Pin<Box<dyn Stream<Item = Result<PartitionedFile>> + Send + Sync + 'static>>;
90
91#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
95pub struct FileRange {
96 pub start: i64,
98 pub end: i64,
100}
101
102impl FileRange {
103 pub fn contains(&self, offset: i64) -> bool {
105 offset >= self.start && offset < self.end
106 }
107}
108
109#[derive(Debug, Clone)]
110pub struct PartitionedFile {
126 pub object_meta: ObjectMeta,
128 pub partition_values: Vec<ScalarValue>,
139 pub range: Option<FileRange>,
141 pub statistics: Option<Arc<Statistics>>,
150 pub ordering: Option<LexOrdering>,
160 pub extensions: FileExtensions,
164 pub metadata_size_hint: Option<usize>,
166 pub table_reference: Option<TableReference>,
167 pub arrow_schema: Option<SchemaRef>,
177}
178
179impl PartitionedFile {
180 pub fn new(path: impl Into<String>, size: u64) -> Self {
182 Self {
183 arrow_schema: None,
184 object_meta: ObjectMeta {
185 location: Path::from(path.into()),
186 last_modified: chrono::Utc.timestamp_nanos(0),
187 size,
188 e_tag: None,
189 version: None,
190 },
191 partition_values: vec![],
192 range: None,
193 statistics: None,
194 ordering: None,
195 extensions: FileExtensions::new(),
196 metadata_size_hint: None,
197 table_reference: None,
198 }
199 }
200
201 pub fn new_from_meta(object_meta: ObjectMeta) -> Self {
203 Self {
204 arrow_schema: None,
205 object_meta,
206 partition_values: vec![],
207 range: None,
208 statistics: None,
209 ordering: None,
210 extensions: FileExtensions::new(),
211 metadata_size_hint: None,
212 table_reference: None,
213 }
214 }
215
216 pub fn new_with_range(path: String, size: u64, start: i64, end: i64) -> Self {
218 Self {
219 arrow_schema: None,
220 object_meta: ObjectMeta {
221 location: Path::from(path),
222 last_modified: chrono::Utc.timestamp_nanos(0),
223 size,
224 e_tag: None,
225 version: None,
226 },
227 partition_values: vec![],
228 range: Some(FileRange { start, end }),
229 statistics: None,
230 ordering: None,
231 extensions: FileExtensions::new(),
232 metadata_size_hint: None,
233 table_reference: None,
234 }
235 .with_range(start, end)
236 }
237
238 pub fn with_arrow_schema(mut self, schema: SchemaRef) -> Self {
243 self.arrow_schema = Some(schema);
244 self
245 }
246
247 pub fn with_partition_values(mut self, partition_values: Vec<ScalarValue>) -> Self {
250 self.partition_values = partition_values;
251 self
252 }
253
254 pub fn with_table_reference(
255 mut self,
256 table_reference: Option<TableReference>,
257 ) -> Self {
258 self.table_reference = table_reference;
259 self
260 }
261
262 pub fn effective_size(&self) -> u64 {
264 if let Some(range) = &self.range {
265 (range.end - range.start) as u64
266 } else {
267 self.object_meta.size
268 }
269 }
270
271 pub fn range(&self) -> (u64, u64) {
273 if let Some(range) = &self.range {
274 (range.start as u64, range.end as u64)
275 } else {
276 (0, self.object_meta.size)
277 }
278 }
279
280 pub fn with_metadata_size_hint(mut self, metadata_size_hint: usize) -> Self {
284 self.metadata_size_hint = Some(metadata_size_hint);
285 self
286 }
287
288 pub fn from_path(path: String) -> Result<Self> {
290 let size = std::fs::metadata(path.clone())?.len();
291 Ok(Self::new(path, size))
292 }
293
294 pub fn path(&self) -> &Path {
296 &self.object_meta.location
297 }
298
299 pub fn with_range(mut self, start: i64, end: i64) -> Self {
301 self.range = Some(FileRange { start, end });
302 self
303 }
304
305 pub fn with_extension<T: Any + Send + Sync>(mut self, value: T) -> Self {
313 self.extensions.insert(value);
314 self
315 }
316
317 pub fn extension<T: Any + Send + Sync>(&self) -> Option<&T> {
319 self.extensions.get::<T>()
320 }
321
322 #[deprecated(
327 since = "54.0.0",
328 note = "use `with_extension`; the extension is keyed by its concrete type"
329 )]
330 pub fn with_extensions(mut self, extensions: Arc<dyn Any + Send + Sync>) -> Self {
331 #[expect(deprecated)]
332 self.extensions.insert_dyn(extensions);
333 self
334 }
335
336 pub fn with_statistics(mut self, file_statistics: Arc<Statistics>) -> Self {
345 if self.partition_values.is_empty() {
346 self.statistics = Some(file_statistics);
348 } else {
349 let mut stats = Arc::unwrap_or_clone(file_statistics);
351 for partition_value in &self.partition_values {
352 let col_stats = ColumnStatistics {
353 null_count: Precision::Exact(0),
354 max_value: Precision::Exact(partition_value.clone()),
355 min_value: Precision::Exact(partition_value.clone()),
356 distinct_count: Precision::Exact(1),
357 sum_value: Precision::Absent,
358 byte_size: partition_value
359 .data_type()
360 .primitive_width()
361 .map(|w| stats.num_rows.multiply(&Precision::Exact(w)))
362 .unwrap_or_else(|| Precision::Absent),
363 };
364 stats.column_statistics.push(col_stats);
365 }
366 self.statistics = Some(Arc::new(stats));
367 }
368 self
369 }
370
371 pub fn has_statistics(&self) -> bool {
375 if let Some(stats) = &self.statistics {
376 stats.column_statistics.iter().any(|col_stats| {
377 col_stats.null_count != Precision::Absent
378 || col_stats.max_value != Precision::Absent
379 || col_stats.min_value != Precision::Absent
380 || col_stats.sum_value != Precision::Absent
381 || col_stats.distinct_count != Precision::Absent
382 })
383 } else {
384 false
385 }
386 }
387
388 pub fn with_ordering(mut self, ordering: Option<LexOrdering>) -> Self {
393 self.ordering = ordering;
394 self
395 }
396}
397
398impl From<ObjectMeta> for PartitionedFile {
399 fn from(object_meta: ObjectMeta) -> Self {
400 PartitionedFile {
401 object_meta,
402 arrow_schema: None,
403 partition_values: vec![],
404 range: None,
405 statistics: None,
406 ordering: None,
407 extensions: FileExtensions::new(),
408 metadata_size_hint: None,
409 table_reference: None,
410 }
411 }
412}
413
414pub fn generate_test_files(num_files: usize, overlap_factor: f64) -> Vec<FileGroup> {
454 let mut files = Vec::with_capacity(num_files);
455 if num_files == 0 {
456 return vec![];
457 }
458 let range_size = if overlap_factor == 0.0 {
459 100 / num_files as i64
460 } else {
461 (100.0 / (overlap_factor * num_files as f64)).max(1.0) as i64
462 };
463
464 for i in 0..num_files {
465 let base = (i as f64 * range_size as f64 * (1.0 - overlap_factor)) as i64;
466 let min = base as f64;
467 let max = (base + range_size) as f64;
468
469 let file = PartitionedFile {
470 arrow_schema: None,
471 object_meta: ObjectMeta {
472 location: Path::from(format!("file_{i}.parquet")),
473 last_modified: chrono::Utc::now(),
474 size: 1000,
475 e_tag: None,
476 version: None,
477 },
478 partition_values: vec![],
479 range: None,
480 statistics: Some(Arc::new(Statistics {
481 num_rows: Precision::Exact(100),
482 total_byte_size: Precision::Exact(1000),
483 column_statistics: vec![ColumnStatistics {
484 null_count: Precision::Exact(0),
485 max_value: Precision::Exact(ScalarValue::Float64(Some(max))),
486 min_value: Precision::Exact(ScalarValue::Float64(Some(min))),
487 sum_value: Precision::Absent,
488 distinct_count: Precision::Absent,
489 byte_size: Precision::Absent,
490 }],
491 })),
492 ordering: None,
493 extensions: FileExtensions::new(),
494 metadata_size_hint: None,
495 table_reference: None,
496 };
497 files.push(file);
498 }
499
500 vec![FileGroup::new(files)]
501}
502
503pub fn verify_sort_integrity(file_groups: &[FileGroup]) -> bool {
506 for group in file_groups {
507 let files = group.iter().collect::<Vec<_>>();
508 for i in 1..files.len() {
509 let prev_file = files[i - 1];
510 let curr_file = files[i];
511
512 if let (Some(prev_stats), Some(curr_stats)) =
514 (&prev_file.statistics, &curr_file.statistics)
515 {
516 let prev_max = &prev_stats.column_statistics[0].max_value;
517 let curr_min = &curr_stats.column_statistics[0].min_value;
518 if curr_min.get_value().unwrap() <= prev_max.get_value().unwrap() {
519 return false;
520 }
521 }
522 }
523 }
524 true
525}
526
527#[cfg(test)]
528mod tests {
529 use super::ListingTableUrl;
530 use arrow::{
531 array::{ArrayRef, Int32Array, RecordBatch},
532 datatypes::{DataType, Field, Schema, SchemaRef},
533 };
534 use datafusion_execution::object_store::{
535 DefaultObjectStoreRegistry, ObjectStoreRegistry,
536 };
537 use object_store::{local::LocalFileSystem, path::Path};
538 use std::{collections::HashMap, ops::Not, sync::Arc};
539 use url::Url;
540
541 pub fn make_partition(sz: i32) -> RecordBatch {
543 let seq_start = 0;
544 let seq_end = sz;
545 let values = (seq_start..seq_end).collect::<Vec<_>>();
546 let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, true)]));
547 let arr = Arc::new(Int32Array::from(values));
548
549 RecordBatch::try_new(schema, vec![arr as ArrayRef]).unwrap()
550 }
551
552 pub fn aggr_test_schema() -> SchemaRef {
554 let mut f1 = Field::new("c1", DataType::Utf8, false);
555 f1.set_metadata(HashMap::from_iter(vec![("testing".into(), "test".into())]));
556 let schema = Schema::new(vec![
557 f1,
558 Field::new("c2", DataType::UInt32, false),
559 Field::new("c3", DataType::Int8, false),
560 Field::new("c4", DataType::Int16, false),
561 Field::new("c5", DataType::Int32, false),
562 Field::new("c6", DataType::Int64, false),
563 Field::new("c7", DataType::UInt8, false),
564 Field::new("c8", DataType::UInt16, false),
565 Field::new("c9", DataType::UInt32, false),
566 Field::new("c10", DataType::UInt64, false),
567 Field::new("c11", DataType::Float32, false),
568 Field::new("c12", DataType::Float64, false),
569 Field::new("c13", DataType::Utf8, false),
570 ]);
571
572 Arc::new(schema)
573 }
574
575 #[test]
576 fn test_object_store_listing_url() {
577 let listing = ListingTableUrl::parse("file:///").unwrap();
578 let store = listing.object_store();
579 assert_eq!(store.as_str(), "file:///");
580
581 let listing = ListingTableUrl::parse("s3://bucket/").unwrap();
582 let store = listing.object_store();
583 assert_eq!(store.as_str(), "s3://bucket/");
584 }
585
586 #[test]
587 fn test_get_store_hdfs() {
588 let sut = DefaultObjectStoreRegistry::default();
589 let url = Url::parse("hdfs://localhost:8020").unwrap();
590 sut.register_store(&url, Arc::new(LocalFileSystem::new()));
591 let url = ListingTableUrl::parse("hdfs://localhost:8020/key").unwrap();
592 sut.get_store(url.as_ref()).unwrap();
593 }
594
595 #[test]
596 fn test_get_store_s3() {
597 let sut = DefaultObjectStoreRegistry::default();
598 let url = Url::parse("s3://bucket/key").unwrap();
599 sut.register_store(&url, Arc::new(LocalFileSystem::new()));
600 let url = ListingTableUrl::parse("s3://bucket/key").unwrap();
601 sut.get_store(url.as_ref()).unwrap();
602 }
603
604 #[test]
605 fn test_get_store_file() {
606 let sut = DefaultObjectStoreRegistry::default();
607 let url = ListingTableUrl::parse("file:///bucket/key").unwrap();
608 sut.get_store(url.as_ref()).unwrap();
609 }
610
611 #[test]
612 fn test_get_store_local() {
613 let sut = DefaultObjectStoreRegistry::default();
614 let url = ListingTableUrl::parse("../").unwrap();
615 sut.get_store(url.as_ref()).unwrap();
616 }
617
618 #[test]
619 fn test_with_statistics_appends_partition_column_stats() {
620 use crate::PartitionedFile;
621 use datafusion_common::stats::Precision;
622 use datafusion_common::{ColumnStatistics, ScalarValue, Statistics};
623
624 let mut pf = PartitionedFile::new(
626 "test.parquet",
627 100, );
629 pf.partition_values = vec![
630 ScalarValue::Date32(Some(20148)), ];
632
633 let file_stats = Arc::new(Statistics {
635 num_rows: Precision::Exact(2),
636 total_byte_size: Precision::Exact(16),
637 column_statistics: vec![ColumnStatistics {
638 null_count: Precision::Exact(0),
639 max_value: Precision::Exact(ScalarValue::Int32(Some(4))),
640 min_value: Precision::Exact(ScalarValue::Int32(Some(3))),
641 sum_value: Precision::Absent,
642 distinct_count: Precision::Absent,
643 byte_size: Precision::Absent,
644 }],
645 });
646
647 let pf = pf.with_statistics(file_stats);
649
650 let stats = pf.statistics.unwrap();
652 assert_eq!(
653 stats.column_statistics.len(),
654 2,
655 "Expected 2 columns (id + date partition)"
656 );
657
658 let partition_col_stats = &stats.column_statistics[1];
660 assert_eq!(
661 partition_col_stats.null_count,
662 Precision::Exact(0),
663 "Partition column null_count should be Exact(0)"
664 );
665 assert_eq!(
666 partition_col_stats.min_value,
667 Precision::Exact(ScalarValue::Date32(Some(20148))),
668 "Partition column min should match partition value"
669 );
670 assert_eq!(
671 partition_col_stats.max_value,
672 Precision::Exact(ScalarValue::Date32(Some(20148))),
673 "Partition column max should match partition value"
674 );
675 assert_eq!(
676 partition_col_stats.distinct_count,
677 Precision::Exact(1),
678 "Partition column distinct_count should be Exact(1)"
679 );
680 }
681
682 #[test]
683 fn test_url_contains() {
684 let url = ListingTableUrl::parse("file:///var/data/mytable/").unwrap();
685
686 assert!(url.contains(
688 &Path::parse("/var/data/mytable/data.parquet").unwrap(),
689 true
690 ));
691
692 assert!(url.contains(
694 &Path::parse("/var/data/mytable/data.parquet").unwrap(),
695 false
696 ));
697
698 assert!(
701 url.contains(
702 &Path::parse("/var/data/mytable/mysubfolder/data.parquet").unwrap(),
703 true
704 )
705 .not()
706 );
707
708 assert!(url.contains(
710 &Path::parse("/var/data/mytable/mysubfolder/data.parquet").unwrap(),
711 false
712 ));
713
714 assert!(url.contains(
716 &Path::parse("/var/data/mytable/year=2024/data.parquet").unwrap(),
717 false
718 ));
719
720 assert!(url.contains(
724 &Path::parse("/var/data/mytable/year=2024/data.parquet").unwrap(),
725 true
726 ));
727
728 assert!(url.contains(&Path::parse("/var/data/mytable/").unwrap(), true));
730
731 assert!(url.contains(&Path::parse("/var/data/mytable/").unwrap(), false));
733 }
734}