1use crate::config::SchemaSource;
19use crate::helpers::{expr_applicable_for_cols, pruned_partition_list};
20use crate::{ListingOptions, ListingTableConfig};
21use arrow::datatypes::{Field, Schema, SchemaBuilder, SchemaRef};
22use async_trait::async_trait;
23use datafusion_catalog::{ScanArgs, ScanResult, Session, TableProvider};
24use datafusion_common::stats::Precision;
25use datafusion_common::{
26 Constraints, SchemaExt, Statistics, internal_datafusion_err, plan_err, project_schema,
27};
28use datafusion_datasource::file::FileSource;
29use datafusion_datasource::file_groups::FileGroup;
30use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
31use datafusion_datasource::file_sink_config::{FileOutputMode, FileSinkConfig};
32#[expect(deprecated)]
33use datafusion_datasource::schema_adapter::SchemaAdapterFactory;
34use datafusion_datasource::{
35 ListingTableUrl, PartitionedFile, TableSchema, compute_all_files_statistics,
36};
37use datafusion_execution::cache::TableScopedPath;
38use datafusion_execution::cache::cache_manager::FileStatisticsCache;
39use datafusion_expr::dml::InsertOp;
40use datafusion_expr::execution_props::ExecutionProps;
41use datafusion_expr::{Expr, TableProviderFilterPushDown, TableType};
42use datafusion_physical_expr::create_lex_ordering;
43use datafusion_physical_expr_adapter::PhysicalExprAdapterFactory;
44use datafusion_physical_expr_common::sort_expr::LexOrdering;
45use datafusion_physical_plan::ExecutionPlan;
46use datafusion_physical_plan::empty::EmptyExec;
47use futures::{Stream, StreamExt, TryStreamExt, future, stream};
48use object_store::ObjectStore;
49use std::collections::HashMap;
50use std::sync::Arc;
51
52#[derive(Debug)]
54pub struct ListFilesResult {
55 pub file_groups: Vec<FileGroup>,
57 pub statistics: Statistics,
59 pub grouped_by_partition: bool,
61}
62
63#[derive(Debug, Clone)]
171pub struct ListingTable {
172 table_paths: Vec<ListingTableUrl>,
173 file_schema: SchemaRef,
177 table_schema: SchemaRef,
181 schema_source: SchemaSource,
183 options: ListingOptions,
186 definition: Option<String>,
188 collected_statistics: Option<Arc<dyn FileStatisticsCache>>,
190 constraints: Constraints,
192 column_defaults: HashMap<String, Expr>,
194 expr_adapter_factory: Option<Arc<dyn PhysicalExprAdapterFactory>>,
196}
197
198impl ListingTable {
199 pub fn try_new(config: ListingTableConfig) -> datafusion_common::Result<Self> {
203 let schema_source = config.schema_source();
205
206 let file_schema = config
207 .file_schema
208 .ok_or_else(|| internal_datafusion_err!("No schema provided."))?;
209
210 let options = config
211 .options
212 .ok_or_else(|| internal_datafusion_err!("No ListingOptions provided"))?;
213
214 let mut builder = SchemaBuilder::from(file_schema.as_ref().to_owned());
216 for (part_col_name, part_col_type) in &options.table_partition_cols {
217 builder.push(Field::new(part_col_name, part_col_type.clone(), false));
218 }
219
220 let table_schema = Arc::new(
221 builder
222 .finish()
223 .with_metadata(file_schema.metadata().clone()),
224 );
225
226 let table = Self {
227 table_paths: config.table_paths,
228 file_schema,
229 table_schema,
230 schema_source,
231 options,
232 definition: None,
233 collected_statistics: None,
234 constraints: Constraints::default(),
235 column_defaults: HashMap::new(),
236 expr_adapter_factory: config.expr_adapter_factory,
237 };
238
239 Ok(table)
240 }
241
242 pub fn with_constraints(mut self, constraints: Constraints) -> Self {
244 self.constraints = constraints;
245 self
246 }
247
248 pub fn with_column_defaults(
250 mut self,
251 column_defaults: HashMap<String, Expr>,
252 ) -> Self {
253 self.column_defaults = column_defaults;
254 self
255 }
256
257 pub fn with_cache(mut self, cache: Option<Arc<dyn FileStatisticsCache>>) -> Self {
263 self.collected_statistics = cache;
264 self
265 }
266
267 fn statistics_cache(
268 &self,
269 has_table_reference: bool,
270 ) -> Option<&Arc<dyn FileStatisticsCache>> {
271 let shared_cache = self.collected_statistics.as_ref()?;
272 if has_table_reference || self.schema_source == SchemaSource::Inferred {
273 Some(shared_cache)
274 } else {
275 None
279 }
280 }
281
282 pub fn with_definition(mut self, definition: Option<String>) -> Self {
284 self.definition = definition;
285 self
286 }
287
288 pub fn table_paths(&self) -> &Vec<ListingTableUrl> {
290 &self.table_paths
291 }
292
293 pub fn options(&self) -> &ListingOptions {
295 &self.options
296 }
297
298 pub fn schema_source(&self) -> SchemaSource {
300 self.schema_source
301 }
302
303 #[deprecated(
310 since = "52.0.0",
311 note = "SchemaAdapterFactory has been removed. Use ListingTableConfig::with_expr_adapter_factory and PhysicalExprAdapterFactory instead. See upgrading.md for more details."
312 )]
313 #[expect(deprecated)]
314 pub fn with_schema_adapter_factory(
315 self,
316 _schema_adapter_factory: Arc<dyn SchemaAdapterFactory>,
317 ) -> Self {
318 self
320 }
321
322 #[deprecated(
329 since = "52.0.0",
330 note = "SchemaAdapterFactory has been removed. Use PhysicalExprAdapterFactory instead. See upgrading.md for more details."
331 )]
332 #[expect(deprecated)]
333 pub fn schema_adapter_factory(&self) -> Option<Arc<dyn SchemaAdapterFactory>> {
334 None
335 }
336
337 fn create_file_source(&self) -> Arc<dyn FileSource> {
339 let table_schema = TableSchema::new(
340 Arc::clone(&self.file_schema),
341 self.options
342 .table_partition_cols
343 .iter()
344 .map(|(col, field)| Arc::new(Field::new(col, field.clone(), false)))
345 .collect(),
346 );
347
348 self.options.format.file_source(table_schema)
349 }
350
351 pub fn try_create_output_ordering(
358 &self,
359 execution_props: &ExecutionProps,
360 file_groups: &[FileGroup],
361 ) -> datafusion_common::Result<Vec<LexOrdering>> {
362 if !self.options.file_sort_order.is_empty() {
364 return create_lex_ordering(
365 &self.table_schema,
366 &self.options.file_sort_order,
367 execution_props,
368 );
369 }
370 if let Some(ordering) = derive_common_ordering_from_files(file_groups) {
371 return Ok(vec![ordering]);
372 }
373 Ok(vec![])
374 }
375}
376
377fn derive_common_ordering_from_files(file_groups: &[FileGroup]) -> Option<LexOrdering> {
386 enum CurrentOrderingState {
387 FirstFile,
389 SomeOrdering(LexOrdering),
391 NoOrdering,
393 }
394 let mut state = CurrentOrderingState::FirstFile;
395
396 for group in file_groups {
398 for file in group.iter() {
399 state = match (&state, &file.ordering) {
400 (CurrentOrderingState::FirstFile, Some(ordering)) => {
402 CurrentOrderingState::SomeOrdering(ordering.clone())
403 }
404 (CurrentOrderingState::FirstFile, None) => {
405 CurrentOrderingState::NoOrdering
406 }
407 (CurrentOrderingState::SomeOrdering(current), Some(ordering)) => {
409 let prefix_len = current
411 .as_ref()
412 .iter()
413 .zip(ordering.as_ref().iter())
414 .take_while(|(a, b)| a == b)
415 .count();
416 if prefix_len == 0 {
417 log::trace!(
418 "Cannot derive common ordering: no common prefix between orderings {current:?} and {ordering:?}"
419 );
420 return None;
421 } else {
422 let ordering =
423 LexOrdering::new(current.as_ref()[..prefix_len].to_vec())
424 .expect("prefix_len > 0, so ordering must be valid");
425 CurrentOrderingState::SomeOrdering(ordering)
426 }
427 }
428 (CurrentOrderingState::SomeOrdering(ordering), None)
431 | (CurrentOrderingState::NoOrdering, Some(ordering)) => {
432 log::trace!(
433 "Cannot derive common ordering: some files have ordering {ordering:?}, others don't"
434 );
435 return None;
436 }
437 (CurrentOrderingState::NoOrdering, None) => {
439 CurrentOrderingState::NoOrdering
440 }
441 };
442 }
443 }
444
445 match state {
446 CurrentOrderingState::SomeOrdering(ordering) => Some(ordering),
447 _ => None,
448 }
449}
450
451fn can_be_evaluated_for_partition_pruning(
454 partition_column_names: &[&str],
455 expr: &Expr,
456) -> bool {
457 !partition_column_names.is_empty()
458 && expr_applicable_for_cols(partition_column_names, expr)
459}
460
461#[async_trait]
462impl TableProvider for ListingTable {
463 fn schema(&self) -> SchemaRef {
464 Arc::clone(&self.table_schema)
465 }
466
467 fn constraints(&self) -> Option<&Constraints> {
468 Some(&self.constraints)
469 }
470
471 fn table_type(&self) -> TableType {
472 TableType::Base
473 }
474
475 async fn scan(
476 &self,
477 state: &dyn Session,
478 projection: Option<&Vec<usize>>,
479 filters: &[Expr],
480 limit: Option<usize>,
481 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
482 let options = ScanArgs::default()
483 .with_projection(projection.map(|p| p.as_slice()))
484 .with_filters(Some(filters))
485 .with_limit(limit);
486 Ok(self.scan_with_args(state, options).await?.into_inner())
487 }
488
489 async fn scan_with_args<'a>(
490 &self,
491 state: &dyn Session,
492 args: ScanArgs<'a>,
493 ) -> datafusion_common::Result<ScanResult> {
494 let projection = args.projection().map(|p| p.to_vec());
495 let filters = args.filters().map(|f| f.to_vec()).unwrap_or_default();
496 let limit = args.limit();
497
498 let table_partition_cols = self
500 .options
501 .table_partition_cols
502 .iter()
503 .map(|col| Ok(Arc::new(self.table_schema.field_with_name(&col.0)?.clone())))
504 .collect::<datafusion_common::Result<Vec<_>>>()?;
505
506 let table_partition_col_names = table_partition_cols
507 .iter()
508 .map(|field| field.name().as_str())
509 .collect::<Vec<_>>();
510
511 let (partition_filters, filters): (Vec<_>, Vec<_>) =
514 filters.iter().cloned().partition(|filter| {
515 can_be_evaluated_for_partition_pruning(&table_partition_col_names, filter)
516 });
517
518 let statistic_file_limit = if filters.is_empty() { limit } else { None };
521
522 let ListFilesResult {
523 file_groups: mut partitioned_file_lists,
524 statistics,
525 grouped_by_partition: partitioned_by_file_group,
526 } = self
527 .list_files_for_scan(state, &partition_filters, statistic_file_limit)
528 .await?;
529
530 if partitioned_file_lists.is_empty() {
532 let projected_schema = project_schema(&self.schema(), projection.as_ref())?;
533 return Ok(ScanResult::new(Arc::new(EmptyExec::new(projected_schema))));
534 }
535
536 let output_ordering = self.try_create_output_ordering(
537 state.execution_props(),
538 &partitioned_file_lists,
539 )?;
540 match state
541 .config_options()
542 .execution
543 .split_file_groups_by_statistics
544 .then(|| {
545 output_ordering.first().map(|output_ordering| {
546 FileScanConfig::split_groups_by_statistics_with_target_partitions(
547 &self.table_schema,
548 &partitioned_file_lists,
549 output_ordering,
550 self.options.target_partitions,
551 )
552 })
553 })
554 .flatten()
555 {
556 Some(Err(e)) => log::debug!("failed to split file groups by statistics: {e}"),
557 Some(Ok(new_groups)) => {
558 if new_groups.len() <= self.options.target_partitions {
559 partitioned_file_lists = new_groups;
560 } else {
561 log::debug!(
562 "attempted to split file groups by statistics, but there were more file groups than target_partitions; falling back to unordered"
563 )
564 }
565 }
566 None => {} };
568
569 let Some(object_store_url) =
570 self.table_paths.first().map(ListingTableUrl::object_store)
571 else {
572 return Ok(ScanResult::new(Arc::new(EmptyExec::new(Arc::new(
573 Schema::empty(),
574 )))));
575 };
576
577 let file_source = self.create_file_source();
578
579 let plan = self
581 .options
582 .format
583 .create_physical_plan(
584 state,
585 FileScanConfigBuilder::new(object_store_url, file_source)
586 .with_file_groups(partitioned_file_lists)
587 .with_constraints(self.constraints.clone())
588 .with_statistics(statistics)
589 .with_projection_indices(projection)?
590 .with_limit(limit)
591 .with_output_ordering(output_ordering)
592 .with_expr_adapter(self.expr_adapter_factory.clone())
593 .with_partitioned_by_file_group(partitioned_by_file_group)
594 .build(),
595 )
596 .await?;
597
598 Ok(ScanResult::new(plan))
599 }
600
601 fn supports_filters_pushdown(
602 &self,
603 filters: &[&Expr],
604 ) -> datafusion_common::Result<Vec<TableProviderFilterPushDown>> {
605 let partition_column_names = self
606 .options
607 .table_partition_cols
608 .iter()
609 .map(|col| col.0.as_str())
610 .collect::<Vec<_>>();
611 filters
612 .iter()
613 .map(|filter| {
614 if can_be_evaluated_for_partition_pruning(&partition_column_names, filter)
615 {
616 return Ok(TableProviderFilterPushDown::Exact);
618 }
619
620 Ok(TableProviderFilterPushDown::Inexact)
621 })
622 .collect()
623 }
624
625 fn get_table_definition(&self) -> Option<&str> {
626 self.definition.as_deref()
627 }
628
629 async fn insert_into(
630 &self,
631 state: &dyn Session,
632 input: Arc<dyn ExecutionPlan>,
633 insert_op: InsertOp,
634 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
635 self.schema()
637 .logically_equivalent_names_and_types(&input.schema())?;
638
639 let table_path = &self.table_paths()[0];
640 if !table_path.is_collection() {
641 return plan_err!(
642 "Inserting into a ListingTable backed by a single file is not supported, URL is possibly missing a trailing `/`. \
643 To append to an existing file use StreamTable, e.g. by using CREATE UNBOUNDED EXTERNAL TABLE"
644 );
645 }
646
647 let store = state.runtime_env().object_store(table_path)?;
649
650 let file_list_stream = pruned_partition_list(
651 state,
652 store.as_ref(),
653 table_path,
654 &[],
655 &self.options.file_extension,
656 &self.options.table_partition_cols,
657 )
658 .await?;
659
660 let file_group = file_list_stream.try_collect::<Vec<_>>().await?.into();
661 let keep_partition_by_columns =
662 state.config_options().execution.keep_partition_by_columns;
663
664 if let Some(lfc) = state.runtime_env().cache_manager.get_list_files_cache() {
666 let key = TableScopedPath {
667 table: table_path.get_table_ref().clone(),
668 path: table_path.prefix().clone(),
669 };
670 let _ = lfc.remove(&key);
671 }
672
673 let config = FileSinkConfig {
675 original_url: String::default(),
676 object_store_url: self.table_paths()[0].object_store(),
677 table_paths: self.table_paths().clone(),
678 file_group,
679 output_schema: self.schema(),
680 table_partition_cols: self.options.table_partition_cols.clone(),
681 insert_op,
682 keep_partition_by_columns,
683 file_extension: self.options().format.get_ext(),
684 file_output_mode: FileOutputMode::Automatic,
685 };
686
687 let orderings = self.try_create_output_ordering(state.execution_props(), &[])?;
689 let order_requirements = orderings.into_iter().next().map(Into::into);
691
692 self.options()
693 .format
694 .create_writer_physical_plan(input, state, config, order_requirements)
695 .await
696 }
697
698 fn get_column_default(&self, column: &str) -> Option<&Expr> {
699 self.column_defaults.get(column)
700 }
701}
702
703impl ListingTable {
704 pub async fn list_files_for_scan<'a>(
708 &'a self,
709 ctx: &'a dyn Session,
710 filters: &'a [Expr],
711 limit: Option<usize>,
712 ) -> datafusion_common::Result<ListFilesResult> {
713 let store = if let Some(url) = self.table_paths.first() {
714 ctx.runtime_env().object_store(url)?
715 } else {
716 return Ok(ListFilesResult {
717 file_groups: vec![],
718 statistics: Statistics::new_unknown(&self.file_schema),
719 grouped_by_partition: false,
720 });
721 };
722 let file_list = future::try_join_all(self.table_paths.iter().map(|table_path| {
724 pruned_partition_list(
725 ctx,
726 store.as_ref(),
727 table_path,
728 filters,
729 &self.options.file_extension,
730 &self.options.table_partition_cols,
731 )
732 }))
733 .await?;
734 let meta_fetch_concurrency =
735 ctx.config_options().execution.meta_fetch_concurrency;
736 let file_list = stream::iter(file_list).flatten_unordered(meta_fetch_concurrency);
737 let files = file_list
739 .map(|part_file| async {
740 let part_file = part_file?;
741 let (statistics, ordering) = if self.options.collect_stat {
742 self.do_collect_statistics_and_ordering(ctx, &store, &part_file)
743 .await?
744 } else {
745 (Arc::new(Statistics::new_unknown(&self.file_schema)), None)
746 };
747 Ok(part_file
748 .with_statistics(statistics)
749 .with_ordering(ordering))
750 })
751 .boxed()
752 .buffer_unordered(ctx.config_options().execution.meta_fetch_concurrency);
753
754 let (file_group, inexact_stats) =
755 get_files_with_limit(files, limit, self.options.collect_stat).await?;
756
757 let threshold = ctx.config_options().optimizer.preserve_file_partitions;
763
764 let (file_groups, grouped_by_partition) = if threshold > 0
765 && !self.options.table_partition_cols.is_empty()
766 {
767 let grouped =
768 file_group.group_by_partition_values(self.options.target_partitions);
769 if grouped.len() >= threshold {
770 (grouped, true)
771 } else {
772 let all_files: Vec<_> =
773 grouped.into_iter().flat_map(|g| g.into_inner()).collect();
774 (
775 FileGroup::new(all_files).split_files(self.options.target_partitions),
776 false,
777 )
778 }
779 } else {
780 (
781 file_group.split_files(self.options.target_partitions),
782 false,
783 )
784 };
785
786 let (file_groups, stats) = compute_all_files_statistics(
787 file_groups,
788 self.schema(),
789 self.options.collect_stat,
790 inexact_stats,
791 )?;
792
793 Ok(ListFilesResult {
798 file_groups,
799 statistics: stats,
800 grouped_by_partition,
801 })
802 }
803
804 async fn do_collect_statistics_and_ordering(
810 &self,
811 ctx: &dyn Session,
812 store: &Arc<dyn ObjectStore>,
813 part_file: &PartitionedFile,
814 ) -> datafusion_common::Result<(Arc<Statistics>, Option<LexOrdering>)> {
815 use datafusion_execution::cache::cache_manager::CachedFileMetadata;
816
817 let path = TableScopedPath {
818 table: part_file.table_reference.clone(),
819 path: part_file.object_meta.location.clone(),
820 };
821 let meta = &part_file.object_meta;
822
823 if let Some(cache) = self.statistics_cache(path.table.is_some())
825 && let Some(cached) = cache.get(&path)
826 && cached.is_valid_for(meta)
827 {
828 return Ok((Arc::clone(&cached.statistics), cached.ordering.clone()));
830 }
831
832 let file_meta = self
834 .options
835 .format
836 .infer_stats_and_ordering(ctx, store, Arc::clone(&self.file_schema), meta)
837 .await?;
838
839 let statistics = Arc::new(file_meta.statistics);
840
841 if let Some(cache) = self.statistics_cache(path.table.is_some()) {
843 cache.put(
844 &path,
845 CachedFileMetadata::new(
846 meta.clone(),
847 Arc::clone(&statistics),
848 file_meta.ordering.clone(),
849 ),
850 );
851 }
852
853 Ok((statistics, file_meta.ordering))
854 }
855}
856
857async fn get_files_with_limit(
878 files: impl Stream<Item = datafusion_common::Result<PartitionedFile>>,
879 limit: Option<usize>,
880 collect_stats: bool,
881) -> datafusion_common::Result<(FileGroup, bool)> {
882 let mut file_group = FileGroup::default();
883 let mut all_files = Box::pin(files.fuse());
885 enum ProcessingState {
886 ReadingFiles,
887 ReachedLimit,
888 }
889
890 let mut state = ProcessingState::ReadingFiles;
891 let mut num_rows = Precision::Absent;
892
893 while let Some(file_result) = all_files.next().await {
894 if matches!(state, ProcessingState::ReachedLimit) {
896 break;
897 }
898
899 let file = file_result?;
900
901 if collect_stats && let Some(file_stats) = &file.statistics {
903 num_rows = if file_group.is_empty() {
904 file_stats.num_rows
906 } else {
907 num_rows.add(&file_stats.num_rows)
909 };
910 }
911
912 file_group.push(file);
914
915 if let Some(limit) = limit
917 && let Precision::Exact(row_count) = num_rows
918 && row_count > limit
919 {
920 state = ProcessingState::ReachedLimit;
921 }
922 }
923 let inexact_stats = all_files.next().await.is_some();
927 Ok((file_group, inexact_stats))
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use arrow::compute::SortOptions;
934 use datafusion_physical_expr::expressions::Column;
935 use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
936
937 fn sort_expr(
939 name: &str,
940 idx: usize,
941 descending: bool,
942 nulls_first: bool,
943 ) -> PhysicalSortExpr {
944 PhysicalSortExpr::new(
945 Arc::new(Column::new(name, idx)),
946 SortOptions {
947 descending,
948 nulls_first,
949 },
950 )
951 }
952
953 fn lex_ordering(exprs: Vec<PhysicalSortExpr>) -> LexOrdering {
955 LexOrdering::new(exprs).expect("expected non-empty ordering")
956 }
957
958 fn create_file(name: &str, ordering: Option<LexOrdering>) -> PartitionedFile {
960 PartitionedFile::new(name.to_string(), 1024).with_ordering(ordering)
961 }
962
963 #[test]
964 fn test_derive_common_ordering_all_files_same_ordering() {
965 let ordering = lex_ordering(vec![
967 sort_expr("a", 0, false, true),
968 sort_expr("b", 1, true, false),
969 ]);
970
971 let file_groups = vec![
972 FileGroup::new(vec![
973 create_file("f1.parquet", Some(ordering.clone())),
974 create_file("f2.parquet", Some(ordering.clone())),
975 ]),
976 FileGroup::new(vec![create_file("f3.parquet", Some(ordering.clone()))]),
977 ];
978
979 let result = derive_common_ordering_from_files(&file_groups);
980 assert_eq!(result, Some(ordering));
981 }
982
983 #[test]
984 fn test_derive_common_ordering_common_prefix() {
985 let ordering_abc = lex_ordering(vec![
987 sort_expr("a", 0, false, true),
988 sort_expr("b", 1, false, true),
989 sort_expr("c", 2, false, true),
990 ]);
991 let ordering_ab = lex_ordering(vec![
992 sort_expr("a", 0, false, true),
993 sort_expr("b", 1, false, true),
994 ]);
995
996 let file_groups = vec![FileGroup::new(vec![
997 create_file("f1.parquet", Some(ordering_abc)),
998 create_file("f2.parquet", Some(ordering_ab.clone())),
999 ])];
1000
1001 let result = derive_common_ordering_from_files(&file_groups);
1002 assert_eq!(result, Some(ordering_ab));
1003 }
1004
1005 #[test]
1006 fn test_derive_common_ordering_no_common_prefix() {
1007 let ordering_a = lex_ordering(vec![sort_expr("a", 0, false, true)]);
1009 let ordering_b = lex_ordering(vec![sort_expr("b", 1, false, true)]);
1010
1011 let file_groups = vec![FileGroup::new(vec![
1012 create_file("f1.parquet", Some(ordering_a)),
1013 create_file("f2.parquet", Some(ordering_b)),
1014 ])];
1015
1016 let result = derive_common_ordering_from_files(&file_groups);
1017 assert_eq!(result, None);
1018 }
1019
1020 #[test]
1021 fn test_derive_common_ordering_mixed_with_none() {
1022 let ordering = lex_ordering(vec![sort_expr("a", 0, false, true)]);
1024
1025 let file_groups = vec![FileGroup::new(vec![
1026 create_file("f1.parquet", Some(ordering)),
1027 create_file("f2.parquet", None),
1028 ])];
1029
1030 let result = derive_common_ordering_from_files(&file_groups);
1031 assert_eq!(result, None);
1032 }
1033
1034 #[test]
1035 fn test_derive_common_ordering_all_none() {
1036 let file_groups = vec![FileGroup::new(vec![
1038 create_file("f1.parquet", None),
1039 create_file("f2.parquet", None),
1040 ])];
1041
1042 let result = derive_common_ordering_from_files(&file_groups);
1043 assert_eq!(result, None);
1044 }
1045
1046 #[test]
1047 fn test_derive_common_ordering_empty_groups() {
1048 let file_groups: Vec<FileGroup> = vec![];
1050 let result = derive_common_ordering_from_files(&file_groups);
1051 assert_eq!(result, None);
1052 }
1053
1054 #[test]
1055 fn test_derive_common_ordering_single_file() {
1056 let ordering = lex_ordering(vec![
1058 sort_expr("a", 0, false, true),
1059 sort_expr("b", 1, true, false),
1060 ]);
1061
1062 let file_groups = vec![FileGroup::new(vec![create_file(
1063 "f1.parquet",
1064 Some(ordering.clone()),
1065 )])];
1066
1067 let result = derive_common_ordering_from_files(&file_groups);
1068 assert_eq!(result, Some(ordering));
1069 }
1070}