1use crate::config::SchemaSource;
19use crate::helpers::{
20 expr_applicable_for_cols, filter_partitioned_file, pruned_partition_list,
21};
22use crate::{ListingOptions, ListingTableConfig};
23use arrow::datatypes::{Field, Schema, SchemaBuilder, SchemaRef};
24use async_trait::async_trait;
25use datafusion_catalog::{ScanArgs, ScanResult, Session, TableProvider};
26use datafusion_common::stats::Precision;
27use datafusion_common::{
28 Constraints, DFSchema, SchemaExt, Statistics, internal_datafusion_err, plan_err,
29 project_schema,
30};
31use datafusion_datasource::file::FileSource;
32use datafusion_datasource::file_groups::FileGroup;
33use datafusion_datasource::file_scan_config::{
34 FileScanConfig, FileScanConfigBuilder, output_partitioning_from_partition_fields,
35};
36use datafusion_datasource::file_sink_config::{FileOutputMode, FileSinkConfig};
37#[expect(deprecated)]
38use datafusion_datasource::schema_adapter::SchemaAdapterFactory;
39use datafusion_datasource::{
40 ListingTableUrl, PartitionedFile, TableSchemaBuilder, compute_all_files_statistics,
41};
42use datafusion_execution::cache::cache_manager::{
43 CachedFileMetadata, FileStatisticsCache, SchemaFingerprint, TableScopedPath,
44};
45use datafusion_expr::dml::InsertOp;
46use datafusion_expr::execution_props::ExecutionProps;
47use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
48use datafusion_expr::{
49 Expr, Partitioning as LogicalPartitioning, TableProviderFilterPushDown, TableType,
50};
51use datafusion_physical_expr::{create_lex_ordering, create_physical_partitioning};
52use datafusion_physical_expr_adapter::PhysicalExprAdapterFactory;
53use datafusion_physical_expr_common::sort_expr::LexOrdering;
54use datafusion_physical_plan::ExecutionPlan;
55use datafusion_physical_plan::empty::EmptyExec;
56use futures::{Stream, StreamExt, TryStreamExt, future, stream};
57use object_store::ObjectStore;
58use std::collections::{HashMap, HashSet};
59use std::sync::Arc;
60
61#[derive(Debug)]
63pub struct ListFilesResult {
64 pub file_groups: Vec<FileGroup>,
66 pub statistics: Statistics,
68 pub grouped_by_partition: bool,
70}
71
72#[derive(Debug, Clone)]
180pub struct ListingTable {
181 table_paths: Vec<ListingTableUrl>,
182 file_schema: SchemaRef,
186 table_schema: SchemaRef,
190 schema_source: SchemaSource,
192 options: ListingOptions,
195 definition: Option<String>,
197 collected_statistics: Option<Arc<FileStatisticsCache>>,
199 constraints: Constraints,
201 column_defaults: HashMap<String, Expr>,
203 expr_adapter_factory: Option<Arc<dyn PhysicalExprAdapterFactory>>,
205 file_schema_fingerprint: Arc<SchemaFingerprint>,
209}
210
211impl ListingTable {
212 pub fn try_new(config: ListingTableConfig) -> datafusion_common::Result<Self> {
216 let schema_source = config.schema_source();
218
219 let file_schema = config
220 .file_schema
221 .ok_or_else(|| internal_datafusion_err!("No schema provided."))?;
222
223 let options = config
224 .options
225 .ok_or_else(|| internal_datafusion_err!("No ListingOptions provided"))?;
226
227 let mut builder = SchemaBuilder::from(file_schema.as_ref().to_owned());
229 for (part_col_name, part_col_type) in &options.table_partition_cols {
230 builder.push(Field::new(part_col_name, part_col_type.clone(), false));
231 }
232
233 let table_schema = Arc::new(
234 builder
235 .finish()
236 .with_metadata(file_schema.metadata().clone()),
237 );
238
239 let file_schema_fingerprint =
240 Arc::new(SchemaFingerprint::from_schema(&file_schema));
241
242 let table = Self {
243 table_paths: config.table_paths,
244 file_schema,
245 table_schema,
246 schema_source,
247 options,
248 definition: None,
249 collected_statistics: None,
250 constraints: Constraints::default(),
251 column_defaults: HashMap::new(),
252 expr_adapter_factory: config.expr_adapter_factory,
253 file_schema_fingerprint,
254 };
255
256 Ok(table)
257 }
258
259 pub fn with_constraints(mut self, constraints: Constraints) -> Self {
261 self.constraints = constraints;
262 self
263 }
264
265 pub fn with_column_defaults(
267 mut self,
268 column_defaults: HashMap<String, Expr>,
269 ) -> Self {
270 self.column_defaults = column_defaults;
271 self
272 }
273
274 pub fn with_cache(mut self, cache: Option<Arc<FileStatisticsCache>>) -> Self {
280 self.collected_statistics = cache;
281 self
282 }
283
284 pub fn with_definition(mut self, definition: Option<String>) -> Self {
286 self.definition = definition;
287 self
288 }
289
290 pub fn table_paths(&self) -> &Vec<ListingTableUrl> {
292 &self.table_paths
293 }
294
295 pub fn options(&self) -> &ListingOptions {
297 &self.options
298 }
299
300 pub fn schema_source(&self) -> SchemaSource {
302 self.schema_source
303 }
304
305 #[deprecated(
312 since = "52.0.0",
313 note = "SchemaAdapterFactory has been removed. Use ListingTableConfig::with_expr_adapter_factory and PhysicalExprAdapterFactory instead. See upgrading.md for more details."
314 )]
315 #[expect(deprecated)]
316 pub fn with_schema_adapter_factory(
317 self,
318 _schema_adapter_factory: Arc<dyn SchemaAdapterFactory>,
319 ) -> Self {
320 self
322 }
323
324 #[deprecated(
331 since = "52.0.0",
332 note = "SchemaAdapterFactory has been removed. Use PhysicalExprAdapterFactory instead. See upgrading.md for more details."
333 )]
334 #[expect(deprecated)]
335 pub fn schema_adapter_factory(&self) -> Option<Arc<dyn SchemaAdapterFactory>> {
336 None
337 }
338
339 fn create_file_source(&self) -> Arc<dyn FileSource> {
341 let table_schema = TableSchemaBuilder::from(&self.file_schema)
342 .with_table_partition_cols(
343 self.options
344 .table_partition_cols
345 .iter()
346 .map(|(col, field)| Arc::new(Field::new(col, field.clone(), false)))
347 .collect::<Vec<_>>(),
348 )
349 .build();
350
351 self.options.format.file_source(table_schema)
352 }
353
354 pub fn try_create_output_ordering(
361 &self,
362 execution_props: &ExecutionProps,
363 file_groups: &[FileGroup],
364 ) -> datafusion_common::Result<Vec<LexOrdering>> {
365 if !self.options.file_sort_order.is_empty() {
367 return create_lex_ordering(
368 &self.table_schema,
369 &self.options.file_sort_order,
370 execution_props,
371 );
372 }
373 if let Some(ordering) = derive_common_ordering_from_files(file_groups) {
374 return Ok(vec![ordering]);
375 }
376 Ok(vec![])
377 }
378}
379
380fn derive_common_ordering_from_files(file_groups: &[FileGroup]) -> Option<LexOrdering> {
389 enum CurrentOrderingState {
390 FirstFile,
392 SomeOrdering(LexOrdering),
394 NoOrdering,
396 }
397 let mut state = CurrentOrderingState::FirstFile;
398
399 for group in file_groups {
401 for file in group.iter() {
402 state = match (&state, &file.ordering) {
403 (CurrentOrderingState::FirstFile, Some(ordering)) => {
405 CurrentOrderingState::SomeOrdering(ordering.clone())
406 }
407 (CurrentOrderingState::FirstFile, None) => {
408 CurrentOrderingState::NoOrdering
409 }
410 (CurrentOrderingState::SomeOrdering(current), Some(ordering)) => {
412 let prefix_len = current
414 .as_ref()
415 .iter()
416 .zip(ordering.as_ref().iter())
417 .take_while(|(a, b)| a == b)
418 .count();
419 if prefix_len == 0 {
420 log::trace!(
421 "Cannot derive common ordering: no common prefix between orderings {current:?} and {ordering:?}"
422 );
423 return None;
424 } else {
425 let ordering =
426 LexOrdering::new(current.as_ref()[..prefix_len].to_vec())
427 .expect("prefix_len > 0, so ordering must be valid");
428 CurrentOrderingState::SomeOrdering(ordering)
429 }
430 }
431 (CurrentOrderingState::SomeOrdering(ordering), None)
434 | (CurrentOrderingState::NoOrdering, Some(ordering)) => {
435 log::trace!(
436 "Cannot derive common ordering: some files have ordering {ordering:?}, others don't"
437 );
438 return None;
439 }
440 (CurrentOrderingState::NoOrdering, None) => {
442 CurrentOrderingState::NoOrdering
443 }
444 };
445 }
446 }
447
448 match state {
449 CurrentOrderingState::SomeOrdering(ordering) => Some(ordering),
450 _ => None,
451 }
452}
453
454fn filter_file_group_by_partition_filters(
455 file_group: FileGroup,
456 filters: &[Expr],
457 df_schema: &DFSchema,
458) -> datafusion_common::Result<FileGroup> {
459 let files = file_group
460 .into_inner()
461 .into_iter()
462 .map(|file| filter_partitioned_file(file, filters, df_schema))
463 .filter_map(Result::transpose)
464 .collect::<datafusion_common::Result<Vec<_>>>()?;
465 Ok(FileGroup::new(files))
466}
467
468fn can_be_evaluated_for_partition_pruning(
471 partition_column_names: &[&str],
472 expr: &Expr,
473) -> bool {
474 !partition_column_names.is_empty()
475 && expr_applicable_for_cols(partition_column_names, expr)
476}
477
478#[async_trait]
479impl TableProvider for ListingTable {
480 fn schema(&self) -> SchemaRef {
481 Arc::clone(&self.table_schema)
482 }
483
484 fn constraints(&self) -> Option<&Constraints> {
485 Some(&self.constraints)
486 }
487
488 fn table_type(&self) -> TableType {
489 TableType::Base
490 }
491
492 async fn scan(
493 &self,
494 state: &dyn Session,
495 projection: Option<&Vec<usize>>,
496 filters: &[Expr],
497 limit: Option<usize>,
498 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
499 let options = ScanArgs::default()
500 .with_projection(projection.map(|p| p.as_slice()))
501 .with_filters(Some(filters))
502 .with_limit(limit);
503 Ok(self.scan_with_args(state, options).await?.into_inner())
504 }
505
506 async fn scan_with_args<'a>(
507 &self,
508 state: &dyn Session,
509 args: ScanArgs<'a>,
510 ) -> datafusion_common::Result<ScanResult> {
511 let projection = args.projection().map(|p| p.to_vec());
512 let filters = args.filters().map(|f| f.to_vec()).unwrap_or_default();
513 let limit = args.limit();
514
515 let table_partition_cols = self
517 .options
518 .table_partition_cols
519 .iter()
520 .map(|col| Ok(Arc::new(self.table_schema.field_with_name(&col.0)?.clone())))
521 .collect::<datafusion_common::Result<Vec<_>>>()?;
522
523 let table_partition_col_names = table_partition_cols
524 .iter()
525 .map(|field| field.name().as_str())
526 .collect::<Vec<_>>();
527
528 let (partition_filters, filters): (Vec<_>, Vec<_>) =
531 filters.iter().cloned().partition(|filter| {
532 can_be_evaluated_for_partition_pruning(&table_partition_col_names, filter)
533 });
534
535 let declared_output_partitioning = self.options.output_partitioning.as_ref();
536
537 let statistic_file_limit =
540 if filters.is_empty() && declared_output_partitioning.is_none() {
541 limit
542 } else {
543 None
544 };
545 let file_group_count = declared_output_partitioning
546 .and_then(LogicalPartitioning::partition_count)
547 .unwrap_or_else(|| state.config().target_partitions());
548
549 let ListFilesResult {
550 file_groups: mut partitioned_file_lists,
551 statistics,
552 grouped_by_partition: partitioned_by_file_group,
553 } = self
554 .list_files_for_scan(state, &partition_filters, statistic_file_limit)
555 .await?;
556
557 if partitioned_file_lists.is_empty() {
559 let projected_schema = project_schema(&self.schema(), projection.as_ref())?;
560 return Ok(ScanResult::new(Arc::new(EmptyExec::new(projected_schema))));
561 }
562
563 let output_ordering = self.try_create_output_ordering(
564 state.execution_props(),
565 &partitioned_file_lists,
566 )?;
567 let split_file_groups_by_statistics = declared_output_partitioning.is_none()
568 && state
569 .config_options()
570 .execution
571 .split_file_groups_by_statistics;
572 match split_file_groups_by_statistics
573 .then(|| {
574 output_ordering.first().map(|output_ordering| {
575 FileScanConfig::split_groups_by_statistics_with_target_partitions(
576 &self.table_schema,
577 &partitioned_file_lists,
578 output_ordering,
579 file_group_count,
580 )
581 })
582 })
583 .flatten()
584 {
585 Some(Err(e)) => log::debug!("failed to split file groups by statistics: {e}"),
586 Some(Ok(new_groups)) => {
587 if new_groups.len() <= file_group_count {
588 partitioned_file_lists = new_groups;
589 } else {
590 log::debug!(
591 "attempted to split file groups by statistics, but there were more file groups than target_partitions; falling back to unordered"
592 )
593 }
594 }
595 None => {} };
597
598 let output_partitioning = if let Some(output_partitioning) =
599 declared_output_partitioning
600 {
601 let output_partitioning = match output_partitioning {
602 LogicalPartitioning::RoundRobinBatch(_) => {
603 return datafusion_common::not_impl_err!(
604 "RoundRobinBatch output partitioning is not supported for ListingTable"
605 );
606 }
607 LogicalPartitioning::DistributeBy(_) => {
608 return datafusion_common::not_impl_err!(
609 "DistributeBy output partitioning is not supported for ListingTable"
610 );
611 }
612 LogicalPartitioning::Hash(_, _) | LogicalPartitioning::Range(_) => {
613 let df_schema = DFSchema::try_from(Arc::clone(&self.table_schema))?;
614 create_physical_partitioning(
615 output_partitioning,
616 &df_schema,
617 state.execution_props(),
618 &PhysicalPlanningContext::default(),
619 )?
620 }
621 };
622 let partition_count = output_partitioning.partition_count();
623 if partitioned_file_lists.len() != partition_count {
624 return plan_err!(
625 "ListingTable output_partitioning has {partition_count} partitions, but the scan has {} file groups",
626 partitioned_file_lists.len()
627 );
628 }
629 Some(output_partitioning)
630 } else if partitioned_by_file_group {
631 output_partitioning_from_partition_fields(
635 &self.table_schema,
636 &table_partition_cols.clone().into(),
637 partitioned_file_lists.len(),
638 )
639 } else {
640 None
641 };
642
643 let Some(object_store_url) =
644 self.table_paths.first().map(ListingTableUrl::object_store)
645 else {
646 return Ok(ScanResult::new(Arc::new(EmptyExec::new(Arc::new(
647 Schema::empty(),
648 )))));
649 };
650
651 let file_source = self.create_file_source();
652 let scan_config = FileScanConfigBuilder::new(object_store_url, file_source)
653 .with_file_groups(partitioned_file_lists)
654 .with_constraints(self.constraints.clone())
655 .with_statistics(statistics)
656 .with_projection_indices(projection)?
657 .with_limit(limit)
658 .with_output_ordering(output_ordering)
659 .with_output_partitioning(output_partitioning)
660 .with_expr_adapter(self.expr_adapter_factory.clone())
661 .build();
662
663 let plan = self
665 .options
666 .format
667 .create_physical_plan(state, scan_config)
668 .await?;
669
670 Ok(ScanResult::new(plan))
671 }
672
673 fn supports_filters_pushdown(
674 &self,
675 filters: &[&Expr],
676 ) -> datafusion_common::Result<Vec<TableProviderFilterPushDown>> {
677 let partition_column_names = self
678 .options
679 .table_partition_cols
680 .iter()
681 .map(|col| col.0.as_str())
682 .collect::<Vec<_>>();
683 filters
684 .iter()
685 .map(|filter| {
686 if can_be_evaluated_for_partition_pruning(&partition_column_names, filter)
687 {
688 return Ok(TableProviderFilterPushDown::Exact);
690 }
691
692 Ok(TableProviderFilterPushDown::Inexact)
693 })
694 .collect()
695 }
696
697 fn get_table_definition(&self) -> Option<&str> {
698 self.definition.as_deref()
699 }
700
701 async fn insert_into(
702 &self,
703 state: &dyn Session,
704 input: Arc<dyn ExecutionPlan>,
705 insert_op: InsertOp,
706 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
707 self.schema()
709 .logically_equivalent_names_and_types(&input.schema())?;
710
711 let table_path = &self.table_paths()[0];
712 if !table_path.is_collection() {
713 return plan_err!(
714 "Inserting into a ListingTable backed by a single file is not supported, URL is possibly missing a trailing `/`. \
715 To append to an existing file use StreamTable, e.g. by using CREATE UNBOUNDED EXTERNAL TABLE"
716 );
717 }
718
719 let store = state.runtime_env().object_store(table_path)?;
721
722 let file_list_stream = pruned_partition_list(
723 state,
724 store.as_ref(),
725 table_path,
726 &[],
727 &self.options.file_extension,
728 &self.options.table_partition_cols,
729 )
730 .await?;
731
732 let file_group = file_list_stream.try_collect::<Vec<_>>().await?.into();
733 let keep_partition_by_columns =
734 state.config_options().execution.keep_partition_by_columns;
735
736 if let Some(lfc) = state.runtime_env().cache_manager.get_list_files_cache() {
738 let key = TableScopedPath {
739 table: table_path.get_table_ref().clone(),
740 path: table_path.prefix().clone(),
741 };
742 let _ = lfc.remove(&key);
743 }
744
745 let config = FileSinkConfig {
747 original_url: String::default(),
748 object_store_url: self.table_paths()[0].object_store(),
749 table_paths: self.table_paths().clone(),
750 file_group,
751 output_schema: self.schema(),
752 table_partition_cols: self.options.table_partition_cols.clone(),
753 insert_op,
754 keep_partition_by_columns,
755 file_extension: self.options().format.get_ext(),
756 file_output_mode: FileOutputMode::Automatic,
757 };
758
759 let orderings = self.try_create_output_ordering(state.execution_props(), &[])?;
761 let order_requirements = orderings.into_iter().next().map(Into::into);
763
764 self.options()
765 .format
766 .create_writer_physical_plan(input, state, config, order_requirements)
767 .await
768 }
769
770 fn get_column_default(&self, column: &str) -> Option<&Expr> {
771 self.column_defaults.get(column)
772 }
773}
774
775impl ListingTable {
776 pub async fn list_files_for_scan<'a>(
783 &'a self,
784 ctx: &'a dyn Session,
785 filters: &'a [Expr],
786 limit: Option<usize>,
787 ) -> datafusion_common::Result<ListFilesResult> {
788 if let Some(output_partitioning) = self.options.output_partitioning.as_ref() {
789 self.list_files_for_declared_output_partitioning(
790 ctx,
791 output_partitioning,
792 filters,
793 )
794 .await
795 } else {
796 self.list_files_for_regular_scan(ctx, filters, limit).await
797 }
798 }
799
800 async fn collect_files_for_scan<'a>(
801 &'a self,
802 ctx: &'a dyn Session,
803 store: &'a Arc<dyn ObjectStore>,
804 listing_time_filters: &'a [Expr],
805 file_limit: Option<usize>,
806 ) -> datafusion_common::Result<(FileGroup, bool)> {
807 let file_list = future::try_join_all(self.table_paths.iter().map(|table_path| {
809 pruned_partition_list(
810 ctx,
811 store.as_ref(),
812 table_path,
813 listing_time_filters,
814 &self.options.file_extension,
815 &self.options.table_partition_cols,
816 )
817 }))
818 .await?;
819 let meta_fetch_concurrency =
820 ctx.config_options().execution.meta_fetch_concurrency.get();
821 let mut seen_files = HashSet::new();
825 let file_list = stream::iter(file_list)
826 .flatten_unordered(meta_fetch_concurrency)
827 .try_filter(move |file| {
828 future::ready(seen_files.insert(file.object_meta.location.clone()))
829 });
830 let files = file_list
832 .map(|part_file| async {
833 let part_file = part_file?;
834 let (statistics, ordering) = if ctx.config().collect_statistics() {
835 self.do_collect_statistics_and_ordering(ctx, store, &part_file)
836 .await?
837 } else {
838 (Arc::new(Statistics::new_unknown(&self.file_schema)), None)
839 };
840 Ok(part_file
841 .with_statistics(statistics)
842 .with_ordering(ordering))
843 })
844 .boxed()
845 .buffer_unordered(
846 ctx.config_options().execution.meta_fetch_concurrency.get(),
847 );
848
849 get_files_with_limit(files, file_limit, ctx.config().collect_statistics()).await
850 }
851
852 async fn list_files_for_regular_scan<'a>(
853 &'a self,
854 ctx: &'a dyn Session,
855 filters: &'a [Expr],
856 limit: Option<usize>,
857 ) -> datafusion_common::Result<ListFilesResult> {
858 let file_group_count = ctx.config().target_partitions();
859 if file_group_count == 0 {
860 return plan_err!(
861 "ListingTable requires target_partitions to be greater than zero"
862 );
863 }
864
865 let store = if let Some(url) = self.table_paths.first() {
866 ctx.runtime_env().object_store(url)?
867 } else {
868 return Ok(ListFilesResult {
869 file_groups: vec![],
870 statistics: Statistics::new_unknown(&self.file_schema),
871 grouped_by_partition: false,
872 });
873 };
874 let (file_group, inexact_stats) = self
875 .collect_files_for_scan(ctx, &store, filters, limit)
876 .await?;
877
878 let threshold = ctx.config_options().optimizer.preserve_file_partitions;
884
885 let (file_groups, grouped_by_partition) =
886 if threshold > 0 && !self.options.table_partition_cols.is_empty() {
887 let grouped = file_group.group_by_partition_values(file_group_count);
888 if grouped.len() >= threshold {
889 (grouped, true)
890 } else {
891 let all_files: Vec<_> =
892 grouped.into_iter().flat_map(|g| g.into_inner()).collect();
893 (
894 FileGroup::new(all_files).split_files(file_group_count),
895 false,
896 )
897 }
898 } else {
899 (file_group.split_files(file_group_count), false)
900 };
901
902 self.list_files_result_from_groups(
903 ctx,
904 file_groups,
905 inexact_stats,
906 grouped_by_partition,
907 )
908 }
909
910 async fn list_files_for_declared_output_partitioning<'a>(
911 &'a self,
912 ctx: &'a dyn Session,
913 output_partitioning: &LogicalPartitioning,
914 filters: &'a [Expr],
915 ) -> datafusion_common::Result<ListFilesResult> {
916 let Some(file_group_count) = output_partitioning.partition_count() else {
917 return datafusion_common::not_impl_err!(
918 "DistributeBy output partitioning is not supported for ListingTable"
919 );
920 };
921 if file_group_count == 0 {
922 return plan_err!(
923 "ListingTable output_partitioning requires at least one partition"
924 );
925 }
926
927 let store = if let Some(url) = self.table_paths.first() {
928 ctx.runtime_env().object_store(url)?
929 } else {
930 return Ok(ListFilesResult {
931 file_groups: vec![],
932 statistics: Statistics::new_unknown(&self.file_schema),
933 grouped_by_partition: false,
934 });
935 };
936 let (file_group, inexact_stats) =
937 self.collect_files_for_scan(ctx, &store, &[], None).await?;
938 let mut file_groups = file_group.split_files(file_group_count);
939 if !file_groups.is_empty() {
940 file_groups.resize_with(file_group_count, || FileGroup::new(vec![]));
941 }
942 let file_groups =
943 self.filter_declared_file_groups_by_partition_filters(file_groups, filters)?;
944
945 self.list_files_result_from_groups(ctx, file_groups, inexact_stats, false)
946 }
947
948 fn filter_declared_file_groups_by_partition_filters(
949 &self,
950 file_groups: Vec<FileGroup>,
951 filters: &[Expr],
952 ) -> datafusion_common::Result<Vec<FileGroup>> {
953 if filters.is_empty() {
954 return Ok(file_groups);
955 }
956
957 let df_schema = DFSchema::from_unqualified_fields(
958 self.options
959 .table_partition_cols
960 .iter()
961 .map(|(name, data_type)| Field::new(name, data_type.clone(), true))
962 .collect(),
963 Default::default(),
964 )?;
965
966 file_groups
967 .into_iter()
968 .map(|file_group| {
969 filter_file_group_by_partition_filters(file_group, filters, &df_schema)
970 })
971 .collect::<datafusion_common::Result<Vec<_>>>()
972 }
973
974 fn list_files_result_from_groups(
975 &self,
976 ctx: &dyn Session,
977 file_groups: Vec<FileGroup>,
978 inexact_stats: bool,
979 grouped_by_partition: bool,
980 ) -> datafusion_common::Result<ListFilesResult> {
981 let (file_groups, stats) = compute_all_files_statistics(
982 file_groups,
983 self.schema(),
984 ctx.config().collect_statistics(),
985 inexact_stats,
986 )?;
987
988 Ok(ListFilesResult {
993 file_groups,
994 statistics: stats,
995 grouped_by_partition,
996 })
997 }
998
999 async fn do_collect_statistics_and_ordering(
1005 &self,
1006 ctx: &dyn Session,
1007 store: &Arc<dyn ObjectStore>,
1008 part_file: &PartitionedFile,
1009 ) -> datafusion_common::Result<(Arc<Statistics>, Option<LexOrdering>)> {
1010 let path = TableScopedPath {
1011 table: part_file.table_reference.clone(),
1012 path: part_file.object_meta.location.clone(),
1013 };
1014 let meta = &part_file.object_meta;
1015
1016 if let Some(cache) = &self.collected_statistics
1020 && let Some(cached) = cache.get(&path)
1021 && cached.is_valid_for(meta, &self.file_schema_fingerprint)
1022 {
1023 return Ok((Arc::clone(&cached.statistics), cached.ordering.clone()));
1025 }
1026
1027 let file_meta = self
1029 .options
1030 .format
1031 .infer_stats_and_ordering(ctx, store, Arc::clone(&self.file_schema), meta)
1032 .await?;
1033
1034 let statistics = Arc::new(file_meta.statistics);
1035
1036 if let Some(cache) = &self.collected_statistics {
1038 cache.put(
1039 &path,
1040 CachedFileMetadata::new(
1041 meta.clone(),
1042 Arc::clone(&self.file_schema_fingerprint),
1043 Arc::clone(&statistics),
1044 file_meta.ordering.clone(),
1045 ),
1046 );
1047 }
1048
1049 Ok((statistics, file_meta.ordering))
1050 }
1051}
1052
1053async fn get_files_with_limit(
1074 files: impl Stream<Item = datafusion_common::Result<PartitionedFile>>,
1075 limit: Option<usize>,
1076 collect_stats: bool,
1077) -> datafusion_common::Result<(FileGroup, bool)> {
1078 let mut file_group = FileGroup::default();
1079 let mut all_files = Box::pin(files.fuse());
1081 enum ProcessingState {
1082 ReadingFiles,
1083 ReachedLimit,
1084 }
1085
1086 let mut state = ProcessingState::ReadingFiles;
1087 let mut num_rows = Precision::Absent;
1088
1089 while let Some(file_result) = all_files.next().await {
1090 if matches!(state, ProcessingState::ReachedLimit) {
1092 break;
1093 }
1094
1095 let file = file_result?;
1096
1097 if collect_stats && let Some(file_stats) = &file.statistics {
1099 num_rows = if file_group.is_empty() {
1100 file_stats.num_rows
1102 } else {
1103 num_rows.add(&file_stats.num_rows)
1105 };
1106 }
1107
1108 file_group.push(file);
1110
1111 if let Some(limit) = limit
1113 && let Precision::Exact(row_count) = num_rows
1114 && row_count > limit
1115 {
1116 state = ProcessingState::ReachedLimit;
1117 }
1118 }
1119 let inexact_stats = all_files.next().await.is_some();
1123 Ok((file_group, inexact_stats))
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129 use arrow::compute::SortOptions;
1130 use datafusion_physical_expr::expressions::Column;
1131 use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
1132
1133 fn sort_expr(
1135 name: &str,
1136 idx: usize,
1137 descending: bool,
1138 nulls_first: bool,
1139 ) -> PhysicalSortExpr {
1140 PhysicalSortExpr::new(
1141 Arc::new(Column::new(name, idx)),
1142 SortOptions {
1143 descending,
1144 nulls_first,
1145 },
1146 )
1147 }
1148
1149 fn lex_ordering(exprs: Vec<PhysicalSortExpr>) -> LexOrdering {
1151 LexOrdering::new(exprs).expect("expected non-empty ordering")
1152 }
1153
1154 fn create_file(name: &str, ordering: Option<LexOrdering>) -> PartitionedFile {
1156 PartitionedFile::new(name.to_string(), 1024).with_ordering(ordering)
1157 }
1158
1159 #[test]
1160 fn test_derive_common_ordering_all_files_same_ordering() {
1161 let ordering = lex_ordering(vec![
1163 sort_expr("a", 0, false, true),
1164 sort_expr("b", 1, true, false),
1165 ]);
1166
1167 let file_groups = vec![
1168 FileGroup::new(vec![
1169 create_file("f1.parquet", Some(ordering.clone())),
1170 create_file("f2.parquet", Some(ordering.clone())),
1171 ]),
1172 FileGroup::new(vec![create_file("f3.parquet", Some(ordering.clone()))]),
1173 ];
1174
1175 let result = derive_common_ordering_from_files(&file_groups);
1176 assert_eq!(result, Some(ordering));
1177 }
1178
1179 #[test]
1180 fn test_derive_common_ordering_common_prefix() {
1181 let ordering_abc = lex_ordering(vec![
1183 sort_expr("a", 0, false, true),
1184 sort_expr("b", 1, false, true),
1185 sort_expr("c", 2, false, true),
1186 ]);
1187 let ordering_ab = lex_ordering(vec![
1188 sort_expr("a", 0, false, true),
1189 sort_expr("b", 1, false, true),
1190 ]);
1191
1192 let file_groups = vec![FileGroup::new(vec![
1193 create_file("f1.parquet", Some(ordering_abc)),
1194 create_file("f2.parquet", Some(ordering_ab.clone())),
1195 ])];
1196
1197 let result = derive_common_ordering_from_files(&file_groups);
1198 assert_eq!(result, Some(ordering_ab));
1199 }
1200
1201 #[test]
1202 fn test_derive_common_ordering_no_common_prefix() {
1203 let ordering_a = lex_ordering(vec![sort_expr("a", 0, false, true)]);
1205 let ordering_b = lex_ordering(vec![sort_expr("b", 1, false, true)]);
1206
1207 let file_groups = vec![FileGroup::new(vec![
1208 create_file("f1.parquet", Some(ordering_a)),
1209 create_file("f2.parquet", Some(ordering_b)),
1210 ])];
1211
1212 let result = derive_common_ordering_from_files(&file_groups);
1213 assert_eq!(result, None);
1214 }
1215
1216 #[test]
1217 fn test_derive_common_ordering_mixed_with_none() {
1218 let ordering = lex_ordering(vec![sort_expr("a", 0, false, true)]);
1220
1221 let file_groups = vec![FileGroup::new(vec![
1222 create_file("f1.parquet", Some(ordering)),
1223 create_file("f2.parquet", None),
1224 ])];
1225
1226 let result = derive_common_ordering_from_files(&file_groups);
1227 assert_eq!(result, None);
1228 }
1229
1230 #[test]
1231 fn test_derive_common_ordering_all_none() {
1232 let file_groups = vec![FileGroup::new(vec![
1234 create_file("f1.parquet", None),
1235 create_file("f2.parquet", None),
1236 ])];
1237
1238 let result = derive_common_ordering_from_files(&file_groups);
1239 assert_eq!(result, None);
1240 }
1241
1242 #[test]
1243 fn test_derive_common_ordering_empty_groups() {
1244 let file_groups: Vec<FileGroup> = vec![];
1246 let result = derive_common_ordering_from_files(&file_groups);
1247 assert_eq!(result, None);
1248 }
1249
1250 #[test]
1251 fn test_derive_common_ordering_single_file() {
1252 let ordering = lex_ordering(vec![
1254 sort_expr("a", 0, false, true),
1255 sort_expr("b", 1, true, false),
1256 ]);
1257
1258 let file_groups = vec![FileGroup::new(vec![create_file(
1259 "f1.parquet",
1260 Some(ordering.clone()),
1261 )])];
1262
1263 let result = derive_common_ordering_from_files(&file_groups);
1264 assert_eq!(result, Some(ordering));
1265 }
1266}