1pub(crate) mod sort_pushdown;
22
23use crate::file_groups::FileGroup;
24use crate::{
25 PartitionedFile, display::FileGroupsDisplay, file::FileSource,
26 file_compression_type::FileCompressionType, file_stream::FileStreamBuilder,
27 file_stream::work_source::SharedWorkSource, source::DataSource,
28 statistics::MinMaxStatistics,
29};
30use arrow::datatypes::FieldRef;
31use arrow::datatypes::{DataType, Schema, SchemaRef};
32use datafusion_common::config::ConfigOptions;
33use datafusion_common::{
34 Constraints, Result, ScalarValue, Statistics, internal_datafusion_err, internal_err,
35};
36use datafusion_execution::{
37 SendableRecordBatchStream, TaskContext, object_store::ObjectStoreUrl,
38};
39use datafusion_expr::Operator;
40
41use crate::source::OpenArgs;
42use datafusion_physical_expr::expressions::{BinaryExpr, Column};
43use datafusion_physical_expr::projection::ProjectionExprs;
44use datafusion_physical_expr::utils::reassign_expr_columns;
45use datafusion_physical_expr::{EquivalenceProperties, Partitioning, split_conjunction};
46use datafusion_physical_expr_adapter::PhysicalExprAdapterFactory;
47use datafusion_physical_expr_common::physical_expr::{PhysicalExpr, is_volatile};
48use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
49use datafusion_physical_plan::SortOrderPushdownResult;
50use datafusion_physical_plan::coop::cooperative;
51use datafusion_physical_plan::execution_plan::SchedulingType;
52use datafusion_physical_plan::{
53 DisplayAs, DisplayFormatType,
54 display::{ProjectSchemaDisplay, display_orderings},
55 filter_pushdown::FilterPushdownPropagation,
56 metrics::ExecutionPlanMetricsSet,
57};
58use log::{debug, warn};
59use std::any::Any;
60use std::{fmt::Debug, fmt::Formatter, fmt::Result as FmtResult, sync::Arc};
61
62#[derive(Clone)]
140pub struct FileScanConfig {
141 pub object_store_url: ObjectStoreUrl,
153 pub file_groups: Vec<FileGroup>,
169 pub constraints: Constraints,
171 pub limit: Option<usize>,
174 pub preserve_order: bool,
179 pub output_ordering: Vec<LexOrdering>,
187 pub file_compression_type: FileCompressionType,
189 pub file_source: Arc<dyn FileSource>,
191 pub batch_size: Option<usize>,
194 pub expr_adapter_factory: Option<Arc<dyn PhysicalExprAdapterFactory>>,
197 pub(crate) statistics: Statistics,
207 pub partitioned_by_file_group: bool,
215}
216
217#[derive(Clone)]
272pub struct FileScanConfigBuilder {
273 object_store_url: ObjectStoreUrl,
274 file_source: Arc<dyn FileSource>,
275 limit: Option<usize>,
276 preserve_order: bool,
277 constraints: Option<Constraints>,
278 file_groups: Vec<FileGroup>,
279 statistics: Option<Statistics>,
280 output_ordering: Vec<LexOrdering>,
281 file_compression_type: Option<FileCompressionType>,
282 batch_size: Option<usize>,
283 expr_adapter_factory: Option<Arc<dyn PhysicalExprAdapterFactory>>,
284 partitioned_by_file_group: bool,
285}
286
287impl FileScanConfigBuilder {
288 pub fn new(
295 object_store_url: ObjectStoreUrl,
296 file_source: Arc<dyn FileSource>,
297 ) -> Self {
298 Self {
299 object_store_url,
300 file_source,
301 file_groups: vec![],
302 statistics: None,
303 output_ordering: vec![],
304 file_compression_type: None,
305 limit: None,
306 preserve_order: false,
307 constraints: None,
308 batch_size: None,
309 expr_adapter_factory: None,
310 partitioned_by_file_group: false,
311 }
312 }
313
314 pub fn with_limit(mut self, limit: Option<usize>) -> Self {
318 self.limit = limit;
319 self
320 }
321
322 pub fn with_preserve_order(mut self, order_sensitive: bool) -> Self {
329 self.preserve_order = order_sensitive;
330 self
331 }
332
333 pub fn with_source(mut self, file_source: Arc<dyn FileSource>) -> Self {
338 self.file_source = file_source;
339 self
340 }
341
342 pub fn table_schema(&self) -> &SchemaRef {
344 self.file_source.table_schema().table_schema()
345 }
346
347 #[deprecated(since = "51.0.0", note = "Use with_projection_indices instead")]
353 pub fn with_projection(self, indices: Option<Vec<usize>>) -> Self {
354 match self.clone().with_projection_indices(indices) {
355 Ok(builder) => builder,
356 Err(e) => {
357 warn!(
358 "Failed to push down projection in FileScanConfigBuilder::with_projection: {e}"
359 );
360 self
361 }
362 }
363 }
364
365 pub fn with_projection_indices(
374 mut self,
375 indices: Option<Vec<usize>>,
376 ) -> Result<Self> {
377 let projection_exprs = indices.map(|indices| {
378 ProjectionExprs::from_indices(
379 &indices,
380 self.file_source.table_schema().table_schema(),
381 )
382 });
383 let Some(projection_exprs) = projection_exprs else {
384 return Ok(self);
385 };
386 let new_source = self
387 .file_source
388 .try_pushdown_projection(&projection_exprs)
389 .map_err(|e| {
390 internal_datafusion_err!(
391 "Failed to push down projection in FileScanConfigBuilder::build: {e}"
392 )
393 })?;
394 if let Some(new_source) = new_source {
395 self.file_source = new_source;
396 } else {
397 internal_err!(
398 "FileSource {} does not support projection pushdown",
399 self.file_source.file_type()
400 )?;
401 }
402 Ok(self)
403 }
404
405 pub fn with_constraints(mut self, constraints: Constraints) -> Self {
407 self.constraints = Some(constraints);
408 self
409 }
410
411 pub fn with_statistics(mut self, statistics: Statistics) -> Self {
424 self.statistics = Some(statistics);
425 self
426 }
427
428 pub fn with_file_groups(mut self, file_groups: Vec<FileGroup>) -> Self {
438 self.file_groups = file_groups;
439 self
440 }
441
442 pub fn with_file_group(mut self, file_group: FileGroup) -> Self {
446 self.file_groups.push(file_group);
447 self
448 }
449
450 pub fn with_file(self, partitioned_file: PartitionedFile) -> Self {
454 self.with_file_group(FileGroup::new(vec![partitioned_file]))
455 }
456
457 pub fn with_output_ordering(mut self, output_ordering: Vec<LexOrdering>) -> Self {
466 self.output_ordering = output_ordering;
467 self
468 }
469
470 pub fn with_file_compression_type(
472 mut self,
473 file_compression_type: FileCompressionType,
474 ) -> Self {
475 self.file_compression_type = Some(file_compression_type);
476 self
477 }
478
479 pub fn with_batch_size(mut self, batch_size: Option<usize>) -> Self {
481 self.batch_size = batch_size;
482 self
483 }
484
485 pub fn with_expr_adapter(
492 mut self,
493 expr_adapter: Option<Arc<dyn PhysicalExprAdapterFactory>>,
494 ) -> Self {
495 self.expr_adapter_factory = expr_adapter;
496 self
497 }
498
499 pub fn with_partitioned_by_file_group(
504 mut self,
505 partitioned_by_file_group: bool,
506 ) -> Self {
507 self.partitioned_by_file_group = partitioned_by_file_group;
508 self
509 }
510
511 pub fn build(self) -> FileScanConfig {
519 let Self {
520 object_store_url,
521 file_source,
522 limit,
523 preserve_order,
524 constraints,
525 file_groups,
526 statistics,
527 output_ordering,
528 file_compression_type,
529 batch_size,
530 expr_adapter_factory: expr_adapter,
531 partitioned_by_file_group,
532 } = self;
533
534 let constraints = constraints.unwrap_or_default();
535 let statistics = statistics.unwrap_or_else(|| {
536 Statistics::new_unknown(file_source.table_schema().table_schema())
537 });
538 let file_compression_type =
539 file_compression_type.unwrap_or(FileCompressionType::UNCOMPRESSED);
540
541 let preserve_order = preserve_order || !output_ordering.is_empty();
543
544 FileScanConfig {
545 object_store_url,
546 file_source,
547 limit,
548 preserve_order,
549 constraints,
550 file_groups,
551 output_ordering,
552 file_compression_type,
553 batch_size,
554 expr_adapter_factory: expr_adapter,
555 statistics,
556 partitioned_by_file_group,
557 }
558 }
559}
560
561impl From<FileScanConfig> for FileScanConfigBuilder {
562 fn from(config: FileScanConfig) -> Self {
563 Self {
564 object_store_url: config.object_store_url,
565 file_source: Arc::<dyn FileSource>::clone(&config.file_source),
566 file_groups: config.file_groups,
567 statistics: Some(config.statistics),
568 output_ordering: config.output_ordering,
569 file_compression_type: Some(config.file_compression_type),
570 limit: config.limit,
571 preserve_order: config.preserve_order,
572 constraints: Some(config.constraints),
573 batch_size: config.batch_size,
574 expr_adapter_factory: config.expr_adapter_factory,
575 partitioned_by_file_group: config.partitioned_by_file_group,
576 }
577 }
578}
579
580fn would_duplicate_volatile_exprs(
597 inner: &ProjectionExprs,
598 outer: &ProjectionExprs,
599) -> bool {
600 use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
601
602 let inner_exprs = inner.as_ref();
603
604 let mut ref_counts = vec![0usize; inner_exprs.len()];
605 for proj_expr in outer.as_ref() {
606 proj_expr
607 .expr
608 .apply(|e| {
609 if let Some(col) = e.as_ref().downcast_ref::<Column>()
610 && let Some(count) = ref_counts.get_mut(col.index())
611 {
612 *count += 1;
613 }
614 Ok(TreeNodeRecursion::Continue)
615 })
616 .expect("infallible closure should not fail");
617 }
618
619 ref_counts
620 .iter()
621 .enumerate()
622 .any(|(idx, &count)| count > 1 && is_volatile(&inner_exprs[idx].expr))
623}
624
625impl DataSource for FileScanConfig {
626 fn open(
627 &self,
628 partition: usize,
629 context: Arc<TaskContext>,
630 ) -> Result<SendableRecordBatchStream> {
631 self.open_with_args(OpenArgs::new(partition, context))
632 }
633
634 fn open_with_args(&self, args: OpenArgs) -> Result<SendableRecordBatchStream> {
635 let OpenArgs {
636 partition,
637 context,
638 sibling_state,
639 } = args;
640 let object_store = context.runtime_env().object_store(&self.object_store_url)?;
641 let batch_size = self
642 .batch_size
643 .unwrap_or_else(|| context.session_config().batch_size());
644
645 let source = self.file_source.with_batch_size(batch_size);
646
647 let morselizer = source.create_morselizer(object_store, self, partition)?;
648
649 let shared_work_source = sibling_state
653 .as_ref()
654 .and_then(|state| state.downcast_ref::<SharedWorkSource>())
655 .cloned();
656
657 let stream = FileStreamBuilder::new(self)
658 .with_partition(partition)
659 .with_shared_work_source(shared_work_source)
660 .with_morselizer(morselizer)
661 .with_metrics(source.metrics())
662 .build()?;
663 Ok(Box::pin(cooperative(stream)))
664 }
665
666 fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult {
667 match t {
668 DisplayFormatType::Default | DisplayFormatType::Verbose => {
669 let schema = self.projected_schema().map_err(|_| std::fmt::Error {})?;
670 let orderings =
671 sort_pushdown::get_projected_output_ordering(self, &schema);
672
673 write!(f, "file_groups=")?;
674 FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?;
675
676 if !schema.fields().is_empty() {
677 if let Some(projection) = self.file_source.projection() {
678 let expr: Vec<String> = projection
681 .as_ref()
682 .iter()
683 .map(|proj_expr| {
684 if let Some(column) =
685 proj_expr.expr.downcast_ref::<Column>()
686 {
687 if column.name() == proj_expr.alias {
688 column.name().to_string()
689 } else {
690 format!(
691 "{} as {}",
692 proj_expr.expr, proj_expr.alias
693 )
694 }
695 } else {
696 format!("{} as {}", proj_expr.expr, proj_expr.alias)
697 }
698 })
699 .collect();
700 write!(f, ", projection=[{}]", expr.join(", "))?;
701 } else {
702 write!(f, ", projection={}", ProjectSchemaDisplay(&schema))?;
703 }
704 }
705
706 if let Some(limit) = self.limit {
707 write!(f, ", limit={limit}")?;
708 }
709
710 display_orderings(f, &orderings)?;
711
712 if !self.constraints.is_empty() {
713 write!(f, ", {}", self.constraints)?;
714 }
715
716 self.fmt_file_source(t, f)
717 }
718 DisplayFormatType::TreeRender => {
719 writeln!(f, "format={}", self.file_source.file_type())?;
720 self.file_source.fmt_extra(t, f)?;
721 let num_files = self.file_groups.iter().map(|fg| fg.len()).sum::<usize>();
722 writeln!(f, "files={num_files}")?;
723 Ok(())
724 }
725 }
726 }
727
728 fn repartitioned(
730 &self,
731 target_partitions: usize,
732 repartition_file_min_size: usize,
733 output_ordering: Option<LexOrdering>,
734 ) -> Result<Option<Arc<dyn DataSource>>> {
735 if self.partitioned_by_file_group {
739 return Ok(None);
740 }
741
742 let source = self.file_source.repartitioned(
743 target_partitions,
744 repartition_file_min_size,
745 output_ordering,
746 self,
747 )?;
748
749 Ok(source.map(|s| Arc::new(s) as _))
750 }
751
752 fn output_partitioning(&self) -> Partitioning {
769 if self.partitioned_by_file_group {
770 let partition_cols = self.table_partition_cols();
771 if !partition_cols.is_empty() {
772 let projected_schema = match self.projected_schema() {
773 Ok(schema) => schema,
774 Err(_) => {
775 debug!(
776 "Could not get projected schema, falling back to UnknownPartitioning."
777 );
778 return Partitioning::UnknownPartitioning(self.file_groups.len());
779 }
780 };
781
782 let mut exprs: Vec<Arc<dyn PhysicalExpr>> = Vec::new();
785 for partition_col in partition_cols {
786 if let Some((idx, _)) = projected_schema
787 .fields()
788 .iter()
789 .enumerate()
790 .find(|(_, f)| f.name() == partition_col.name())
791 {
792 exprs.push(Arc::new(Column::new(partition_col.name(), idx)));
793 }
794 }
795
796 if exprs.len() == partition_cols.len() {
797 return Partitioning::Hash(exprs, self.file_groups.len());
798 }
799 }
800 }
801 Partitioning::UnknownPartitioning(self.file_groups.len())
802 }
803
804 fn eq_properties(&self) -> EquivalenceProperties {
808 let schema = self.file_source.table_schema().table_schema();
809 let mut eq_properties = EquivalenceProperties::new_with_orderings(
810 Arc::clone(schema),
811 self.validated_output_ordering(),
812 )
813 .with_constraints(self.constraints.clone());
814
815 if let Some(filter) = self.file_source.filter() {
816 match Self::add_filter_equivalence_info(&filter, &mut eq_properties, schema) {
819 Ok(()) => {}
820 Err(e) => {
821 warn!("Failed to add filter equivalence info: {e}");
822 #[cfg(debug_assertions)]
823 panic!("Failed to add filter equivalence info: {e}");
824 }
825 }
826 }
827
828 if let Some(projection) = self.file_source.projection() {
829 match (
830 projection.project_schema(schema),
831 projection.projection_mapping(schema),
832 ) {
833 (Ok(output_schema), Ok(mapping)) => {
834 eq_properties =
835 eq_properties.project(&mapping, Arc::new(output_schema));
836 }
837 (Err(e), _) | (_, Err(e)) => {
838 warn!("Failed to project equivalence properties: {e}");
839 #[cfg(debug_assertions)]
840 panic!("Failed to project equivalence properties: {e}");
841 }
842 }
843 }
844
845 eq_properties
846 }
847
848 fn scheduling_type(&self) -> SchedulingType {
849 SchedulingType::Cooperative
850 }
851
852 fn partition_statistics(&self, partition: Option<usize>) -> Result<Arc<Statistics>> {
853 if let Some(partition) = partition {
854 if let Some(file_group) = self.file_groups.get(partition)
857 && let Some(stat) = file_group.file_statistics(None)
858 {
859 let output_schema = self.projected_schema()?;
861 return if let Some(projection) = self.file_source.projection() {
862 Ok(Arc::new(
863 projection.project_statistics(stat.clone(), &output_schema)?,
864 ))
865 } else {
866 Ok(Arc::new(stat.clone()))
867 };
868 }
869 Ok(Arc::new(Statistics::new_unknown(
871 self.projected_schema()?.as_ref(),
872 )))
873 } else {
874 let statistics = self.statistics();
876 let projection = self.file_source.projection();
877 let output_schema = self.projected_schema()?;
878 if let Some(projection) = &projection {
879 Ok(Arc::new(
880 projection.project_statistics(statistics.clone(), &output_schema)?,
881 ))
882 } else {
883 Ok(Arc::new(statistics))
884 }
885 }
886 }
887
888 fn with_fetch(&self, limit: Option<usize>) -> Option<Arc<dyn DataSource>> {
889 let source = FileScanConfigBuilder::from(self.clone())
890 .with_limit(limit)
891 .build();
892 Some(Arc::new(source))
893 }
894
895 fn fetch(&self) -> Option<usize> {
896 self.limit
897 }
898
899 fn metrics(&self) -> ExecutionPlanMetricsSet {
900 self.file_source.metrics().clone()
901 }
902
903 fn try_swapping_with_projection(
904 &self,
905 projection: &ProjectionExprs,
906 ) -> Result<Option<Arc<dyn DataSource>>> {
907 if let Some(inner) = self.file_source.projection()
912 && would_duplicate_volatile_exprs(inner, projection)
913 {
914 return Ok(None);
915 }
916 match self.file_source.try_pushdown_projection(projection)? {
917 Some(new_source) => {
918 let mut new_file_scan_config = self.clone();
919 new_file_scan_config.file_source = new_source;
920 Ok(Some(Arc::new(new_file_scan_config) as Arc<dyn DataSource>))
921 }
922 None => Ok(None),
923 }
924 }
925
926 fn try_pushdown_filters(
927 &self,
928 filters: Vec<Arc<dyn PhysicalExpr>>,
929 config: &ConfigOptions,
930 ) -> Result<FilterPushdownPropagation<Arc<dyn DataSource>>> {
931 let table_schema = self.file_source.table_schema().table_schema();
940 let filters_to_remap = if let Some(projection) = self.file_source.projection() {
941 filters
942 .into_iter()
943 .map(|filter| projection.unproject_expr(&filter))
944 .collect::<Result<Vec<_>>>()?
945 } else {
946 filters
947 };
948 let remapped_filters = filters_to_remap
950 .into_iter()
951 .map(|filter| reassign_expr_columns(filter, table_schema))
952 .collect::<Result<Vec<_>>>()?;
953
954 let result = self
955 .file_source
956 .try_pushdown_filters(remapped_filters, config)?;
957 match result.updated_node {
958 Some(new_file_source) => {
959 let mut new_file_scan_config = self.clone();
960 new_file_scan_config.file_source = new_file_source;
961 Ok(FilterPushdownPropagation {
962 filters: result.filters,
963 updated_node: Some(Arc::new(new_file_scan_config) as _),
964 })
965 }
966 None => {
967 Ok(FilterPushdownPropagation {
969 filters: result.filters,
970 updated_node: None,
971 })
972 }
973 }
974 }
975
976 fn try_pushdown_sort(
1017 &self,
1018 order: &[PhysicalSortExpr],
1019 ) -> Result<SortOrderPushdownResult<Arc<dyn DataSource>>> {
1020 let pushdown_result = self
1021 .file_source
1022 .try_pushdown_sort(order, &self.eq_properties())?;
1023
1024 match pushdown_result {
1025 SortOrderPushdownResult::Exact { inner } => {
1026 let config = self.rebuild_with_source(inner, true, order)?;
1027 if config.output_ordering.is_empty() {
1031 Ok(SortOrderPushdownResult::Inexact {
1032 inner: Arc::new(config),
1033 })
1034 } else {
1035 Ok(SortOrderPushdownResult::Exact {
1036 inner: Arc::new(config),
1037 })
1038 }
1039 }
1040 SortOrderPushdownResult::Inexact { inner } => {
1041 let mut config = self.rebuild_with_source(inner, false, order)?;
1042 if config.output_ordering.is_empty() {
1051 return Ok(SortOrderPushdownResult::Inexact {
1052 inner: Arc::new(config),
1053 });
1054 }
1055 config.file_source = Arc::clone(&self.file_source);
1075 Ok(SortOrderPushdownResult::Exact {
1076 inner: Arc::new(config),
1077 })
1078 }
1079 SortOrderPushdownResult::Unsupported => {
1080 self.try_sort_file_groups_by_statistics(order)
1081 }
1082 }
1083 }
1084
1085 fn with_preserve_order(&self, preserve_order: bool) -> Option<Arc<dyn DataSource>> {
1086 if self.preserve_order == preserve_order {
1087 return Some(Arc::new(self.clone()));
1088 }
1089
1090 let new_config = FileScanConfig {
1091 preserve_order,
1092 ..self.clone()
1093 };
1094 Some(Arc::new(new_config))
1095 }
1096
1097 fn create_sibling_state(
1106 &self,
1107 config: &ConfigOptions,
1108 ) -> Option<Arc<dyn Any + Send + Sync>> {
1109 if self.preserve_order
1110 || self.partitioned_by_file_group
1111 || !config.execution.enable_file_stream_work_stealing
1112 {
1113 return None;
1114 }
1115
1116 Some(Arc::new(SharedWorkSource::from_config(self)) as Arc<dyn Any + Send + Sync>)
1117 }
1118}
1119
1120impl FileScanConfig {
1121 fn validated_output_ordering(&self) -> Vec<LexOrdering> {
1151 let schema = self.file_source.table_schema().table_schema();
1152 sort_pushdown::validate_orderings(
1153 &self.output_ordering,
1154 schema,
1155 &self.file_groups,
1156 None,
1157 )
1158 }
1159
1160 pub fn file_schema(&self) -> &SchemaRef {
1162 self.file_source.table_schema().file_schema()
1163 }
1164
1165 pub fn table_partition_cols(&self) -> &Vec<FieldRef> {
1167 self.file_source.table_schema().table_partition_cols()
1168 }
1169
1170 pub fn statistics(&self) -> Statistics {
1176 if self.file_source.filter().is_some() {
1177 self.statistics.clone().to_inexact()
1178 } else {
1179 self.statistics.clone()
1180 }
1181 }
1182
1183 pub fn projected_schema(&self) -> Result<Arc<Schema>> {
1184 let schema = self.file_source.table_schema().table_schema();
1185 match self.file_source.projection() {
1186 Some(proj) => Ok(Arc::new(proj.project_schema(schema)?)),
1187 None => Ok(Arc::clone(schema)),
1188 }
1189 }
1190
1191 fn add_filter_equivalence_info(
1192 filter: &Arc<dyn PhysicalExpr>,
1193 eq_properties: &mut EquivalenceProperties,
1194 schema: &Schema,
1195 ) -> Result<()> {
1196 let equal_pairs = split_conjunction(filter).into_iter().filter_map(|expr| {
1198 reassign_expr_columns(Arc::clone(expr), schema)
1201 .ok()
1202 .and_then(|expr| match expr.downcast_ref::<BinaryExpr>() {
1203 Some(expr) if expr.op() == &Operator::Eq => {
1204 Some((Arc::clone(expr.left()), Arc::clone(expr.right())))
1205 }
1206 _ => None,
1207 })
1208 });
1209
1210 for (lhs, rhs) in equal_pairs {
1211 eq_properties.add_equal_conditions(lhs, rhs)?
1212 }
1213
1214 Ok(())
1215 }
1216
1217 #[deprecated(
1226 since = "52.0.0",
1227 note = "newlines_in_values has moved to CsvSource. Access it via CsvSource::csv_options().newlines_in_values instead. It will be removed in 58.0.0 or 6 months after 52.0.0 is released, whichever comes first."
1228 )]
1229 pub fn newlines_in_values(&self) -> bool {
1230 false
1231 }
1232
1233 #[deprecated(
1234 since = "52.0.0",
1235 note = "This method is no longer used, use eq_properties instead. It will be removed in 58.0.0 or 6 months after 52.0.0 is released, whichever comes first."
1236 )]
1237 pub fn projected_constraints(&self) -> Constraints {
1238 let props = self.eq_properties();
1239 props.constraints().clone()
1240 }
1241
1242 #[deprecated(
1243 since = "52.0.0",
1244 note = "This method is no longer used, use eq_properties instead. It will be removed in 58.0.0 or 6 months after 52.0.0 is released, whichever comes first."
1245 )]
1246 pub fn file_column_projection_indices(&self) -> Option<Vec<usize>> {
1247 #[expect(deprecated)]
1248 self.file_source.projection().as_ref().map(|p| {
1249 p.ordered_column_indices()
1250 .into_iter()
1251 .filter(|&i| i < self.file_schema().fields().len())
1252 .collect::<Vec<_>>()
1253 })
1254 }
1255
1256 pub fn split_groups_by_statistics_with_target_partitions(
1278 table_schema: &SchemaRef,
1279 file_groups: &[FileGroup],
1280 sort_order: &LexOrdering,
1281 target_partitions: usize,
1282 ) -> Result<Vec<FileGroup>> {
1283 if target_partitions == 0 {
1284 return Err(internal_datafusion_err!(
1285 "target_partitions must be greater than 0"
1286 ));
1287 }
1288
1289 let flattened_files = file_groups
1290 .iter()
1291 .flat_map(FileGroup::iter)
1292 .collect::<Vec<_>>();
1293
1294 if flattened_files.is_empty() {
1295 return Ok(vec![]);
1296 }
1297
1298 let statistics = MinMaxStatistics::new_from_files(
1299 sort_order,
1300 table_schema,
1301 None,
1302 flattened_files.iter().copied(),
1303 )?;
1304
1305 let indices_sorted_by_min = statistics.min_values_sorted();
1306
1307 let mut file_groups_indices: Vec<Vec<usize>> = vec![vec![]; target_partitions];
1309
1310 for (idx, min) in indices_sorted_by_min {
1311 if let Some((_, group)) = file_groups_indices
1312 .iter_mut()
1313 .enumerate()
1314 .filter(|(_, group)| {
1315 group.is_empty()
1316 || min
1317 > statistics
1318 .max(*group.last().expect("groups should not be empty"))
1319 })
1320 .min_by_key(|(_, group)| group.len())
1321 {
1322 group.push(idx);
1323 } else {
1324 file_groups_indices.push(vec![idx]);
1326 }
1327 }
1328
1329 file_groups_indices.retain(|group| !group.is_empty());
1331
1332 Ok(file_groups_indices
1334 .into_iter()
1335 .map(|file_group_indices| {
1336 FileGroup::new(
1337 file_group_indices
1338 .into_iter()
1339 .map(|idx| flattened_files[idx].clone())
1340 .collect(),
1341 )
1342 })
1343 .collect())
1344 }
1345
1346 pub fn split_groups_by_statistics(
1350 table_schema: &SchemaRef,
1351 file_groups: &[FileGroup],
1352 sort_order: &LexOrdering,
1353 ) -> Result<Vec<FileGroup>> {
1354 let flattened_files = file_groups
1355 .iter()
1356 .flat_map(FileGroup::iter)
1357 .collect::<Vec<_>>();
1358 if flattened_files.is_empty() {
1370 return Ok(vec![]);
1371 }
1372
1373 let statistics = MinMaxStatistics::new_from_files(
1374 sort_order,
1375 table_schema,
1376 None,
1377 flattened_files.iter().copied(),
1378 )
1379 .map_err(|e| {
1380 e.context("construct min/max statistics for split_groups_by_statistics")
1381 })?;
1382
1383 let indices_sorted_by_min = statistics.min_values_sorted();
1384 let mut file_groups_indices: Vec<Vec<usize>> = vec![];
1385
1386 for (idx, min) in indices_sorted_by_min {
1387 let file_group_to_insert = file_groups_indices.iter_mut().find(|group| {
1388 min > statistics.max(
1391 *group
1392 .last()
1393 .expect("groups should be nonempty at construction"),
1394 )
1395 });
1396 match file_group_to_insert {
1397 Some(group) => group.push(idx),
1398 None => file_groups_indices.push(vec![idx]),
1399 }
1400 }
1401
1402 Ok(file_groups_indices
1404 .into_iter()
1405 .map(|file_group_indices| {
1406 file_group_indices
1407 .into_iter()
1408 .map(|idx| flattened_files[idx].clone())
1409 .collect()
1410 })
1411 .collect())
1412 }
1413
1414 fn fmt_file_source(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult {
1416 write!(f, ", file_type={}", self.file_source.file_type())?;
1417 self.file_source.fmt_extra(t, f)
1418 }
1419
1420 pub fn file_source(&self) -> &Arc<dyn FileSource> {
1422 &self.file_source
1423 }
1424
1425 }
1429
1430impl Debug for FileScanConfig {
1431 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1432 write!(f, "FileScanConfig {{")?;
1433 write!(f, "object_store_url={:?}, ", self.object_store_url)?;
1434
1435 write!(f, "statistics={:?}, ", self.statistics())?;
1436
1437 DisplayAs::fmt_as(self, DisplayFormatType::Verbose, f)?;
1438 write!(f, "}}")
1439 }
1440}
1441
1442impl DisplayAs for FileScanConfig {
1443 fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult {
1444 let schema = self.projected_schema().map_err(|_| std::fmt::Error {})?;
1445 let orderings = sort_pushdown::get_projected_output_ordering(self, &schema);
1446
1447 write!(f, "file_groups=")?;
1448 FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?;
1449
1450 if !schema.fields().is_empty() {
1451 write!(f, ", projection={}", ProjectSchemaDisplay(&schema))?;
1452 }
1453
1454 if let Some(limit) = self.limit {
1455 write!(f, ", limit={limit}")?;
1456 }
1457
1458 display_orderings(f, &orderings)?;
1459
1460 if !self.constraints.is_empty() {
1461 write!(f, ", {}", self.constraints)?;
1462 }
1463
1464 Ok(())
1465 }
1466}
1467
1468pub fn wrap_partition_type_in_dict(val_type: DataType) -> DataType {
1479 DataType::Dictionary(Box::new(DataType::UInt16), Box::new(val_type))
1480}
1481
1482pub fn wrap_partition_value_in_dict(val: ScalarValue) -> ScalarValue {
1486 ScalarValue::Dictionary(Box::new(DataType::UInt16), Box::new(val))
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use std::collections::HashMap;
1492
1493 use super::*;
1494 use crate::TableSchema;
1495 use crate::source::DataSourceExec;
1496 use crate::test_util::col;
1497 use crate::{
1498 generate_test_files, test_util::MockSource, tests::aggr_test_schema,
1499 verify_sort_integrity,
1500 };
1501
1502 use arrow::array::{Int32Array, RecordBatch};
1503 use arrow::datatypes::Field;
1504 use datafusion_common::ColumnStatistics;
1505 use datafusion_common::stats::Precision;
1506 use datafusion_common::{Result, assert_batches_eq, internal_err};
1507 use datafusion_execution::TaskContext;
1508 use datafusion_expr::SortExpr;
1509 use datafusion_physical_expr::create_physical_sort_expr;
1510 use datafusion_physical_expr::expressions::Literal;
1511 use datafusion_physical_expr::projection::ProjectionExpr;
1512 use datafusion_physical_expr::projection::ProjectionExprs;
1513 use datafusion_physical_plan::ExecutionPlan;
1514 use datafusion_physical_plan::execution_plan::collect;
1515 use futures::FutureExt as _;
1516 use futures::StreamExt as _;
1517 use futures::stream;
1518 use object_store::ObjectStore;
1519 use std::fmt::Debug;
1520
1521 #[derive(Clone)]
1522 struct InexactSortPushdownSource {
1523 metrics: ExecutionPlanMetricsSet,
1524 table_schema: TableSchema,
1525 }
1526
1527 impl InexactSortPushdownSource {
1528 fn new(table_schema: TableSchema) -> Self {
1529 Self {
1530 metrics: ExecutionPlanMetricsSet::new(),
1531 table_schema,
1532 }
1533 }
1534 }
1535
1536 impl FileSource for InexactSortPushdownSource {
1537 fn create_file_opener(
1538 &self,
1539 _object_store: Arc<dyn ObjectStore>,
1540 _base_config: &FileScanConfig,
1541 _partition: usize,
1542 ) -> Result<Arc<dyn crate::file_stream::FileOpener>> {
1543 unimplemented!()
1544 }
1545
1546 fn table_schema(&self) -> &TableSchema {
1547 &self.table_schema
1548 }
1549
1550 fn with_batch_size(&self, _batch_size: usize) -> Arc<dyn FileSource> {
1551 Arc::new(self.clone())
1552 }
1553
1554 fn metrics(&self) -> &ExecutionPlanMetricsSet {
1555 &self.metrics
1556 }
1557
1558 fn file_type(&self) -> &str {
1559 "mock"
1560 }
1561
1562 fn try_pushdown_sort(
1563 &self,
1564 _order: &[PhysicalSortExpr],
1565 _eq_properties: &EquivalenceProperties,
1566 ) -> Result<SortOrderPushdownResult<Arc<dyn FileSource>>> {
1567 Ok(SortOrderPushdownResult::Inexact {
1568 inner: Arc::new(self.clone()) as Arc<dyn FileSource>,
1569 })
1570 }
1571 }
1572
1573 #[test]
1574 fn physical_plan_config_no_projection_tab_cols_as_field() {
1575 let file_schema = aggr_test_schema();
1576
1577 let table_partition_col =
1579 Field::new("date", wrap_partition_type_in_dict(DataType::Utf8), true)
1580 .with_metadata(HashMap::from_iter(vec![(
1581 "key_whatever".to_owned(),
1582 "value_whatever".to_owned(),
1583 )]));
1584
1585 let conf = config_for_projection(
1586 Arc::clone(&file_schema),
1587 None,
1588 Statistics::new_unknown(&file_schema),
1589 vec![table_partition_col.clone()],
1590 );
1591
1592 let proj_schema = conf.projected_schema().unwrap();
1594 assert_eq!(proj_schema.fields().len(), file_schema.fields().len() + 1);
1595 assert_eq!(
1596 *proj_schema.field(file_schema.fields().len()),
1597 table_partition_col,
1598 "partition columns are the last columns and ust have all values defined in created field"
1599 );
1600 }
1601
1602 #[test]
1603 fn test_split_groups_by_statistics() -> Result<()> {
1604 use chrono::TimeZone;
1605 use datafusion_common::DFSchema;
1606 use datafusion_expr::execution_props::ExecutionProps;
1607 use object_store::{ObjectMeta, path::Path};
1608
1609 struct File {
1610 name: &'static str,
1611 date: &'static str,
1612 statistics: Vec<Option<(Option<f64>, Option<f64>)>>,
1613 }
1614 impl File {
1615 fn new(
1616 name: &'static str,
1617 date: &'static str,
1618 statistics: Vec<Option<(f64, f64)>>,
1619 ) -> Self {
1620 Self::new_nullable(
1621 name,
1622 date,
1623 statistics
1624 .into_iter()
1625 .map(|opt| opt.map(|(min, max)| (Some(min), Some(max))))
1626 .collect(),
1627 )
1628 }
1629
1630 fn new_nullable(
1631 name: &'static str,
1632 date: &'static str,
1633 statistics: Vec<Option<(Option<f64>, Option<f64>)>>,
1634 ) -> Self {
1635 Self {
1636 name,
1637 date,
1638 statistics,
1639 }
1640 }
1641 }
1642
1643 struct TestCase {
1644 name: &'static str,
1645 file_schema: Schema,
1646 files: Vec<File>,
1647 sort: Vec<SortExpr>,
1648 expected_result: Result<Vec<Vec<&'static str>>, &'static str>,
1649 }
1650
1651 use datafusion_expr::col;
1652 let cases = vec![
1653 TestCase {
1654 name: "test sort",
1655 file_schema: Schema::new(vec![Field::new(
1656 "value".to_string(),
1657 DataType::Float64,
1658 false,
1659 )]),
1660 files: vec![
1661 File::new("0", "2023-01-01", vec![Some((0.00, 0.49))]),
1662 File::new("1", "2023-01-01", vec![Some((0.50, 1.00))]),
1663 File::new("2", "2023-01-02", vec![Some((0.00, 1.00))]),
1664 ],
1665 sort: vec![col("value").sort(true, false)],
1666 expected_result: Ok(vec![vec!["0", "1"], vec!["2"]]),
1667 },
1668 TestCase {
1671 name: "test sort with files ordered differently",
1672 file_schema: Schema::new(vec![Field::new(
1673 "value".to_string(),
1674 DataType::Float64,
1675 false,
1676 )]),
1677 files: vec![
1678 File::new("0", "2023-01-01", vec![Some((0.00, 0.49))]),
1679 File::new("2", "2023-01-02", vec![Some((0.00, 1.00))]),
1680 File::new("1", "2023-01-01", vec![Some((0.50, 1.00))]),
1681 ],
1682 sort: vec![col("value").sort(true, false)],
1683 expected_result: Ok(vec![vec!["0", "1"], vec!["2"]]),
1684 },
1685 TestCase {
1686 name: "reverse sort",
1687 file_schema: Schema::new(vec![Field::new(
1688 "value".to_string(),
1689 DataType::Float64,
1690 false,
1691 )]),
1692 files: vec![
1693 File::new("0", "2023-01-01", vec![Some((0.00, 0.49))]),
1694 File::new("1", "2023-01-01", vec![Some((0.50, 1.00))]),
1695 File::new("2", "2023-01-02", vec![Some((0.00, 1.00))]),
1696 ],
1697 sort: vec![col("value").sort(false, true)],
1698 expected_result: Ok(vec![vec!["1", "0"], vec!["2"]]),
1699 },
1700 TestCase {
1701 name: "nullable sort columns, nulls last",
1702 file_schema: Schema::new(vec![Field::new(
1703 "value".to_string(),
1704 DataType::Float64,
1705 true,
1706 )]),
1707 files: vec![
1708 File::new_nullable(
1709 "0",
1710 "2023-01-01",
1711 vec![Some((Some(0.00), Some(0.49)))],
1712 ),
1713 File::new_nullable("1", "2023-01-01", vec![Some((Some(0.50), None))]),
1714 File::new_nullable("2", "2023-01-02", vec![Some((Some(0.00), None))]),
1715 ],
1716 sort: vec![col("value").sort(true, false)],
1717 expected_result: Ok(vec![vec!["0", "1"], vec!["2"]]),
1718 },
1719 TestCase {
1720 name: "nullable sort columns, nulls first",
1721 file_schema: Schema::new(vec![Field::new(
1722 "value".to_string(),
1723 DataType::Float64,
1724 true,
1725 )]),
1726 files: vec![
1727 File::new_nullable("0", "2023-01-01", vec![Some((None, Some(0.49)))]),
1728 File::new_nullable(
1729 "1",
1730 "2023-01-01",
1731 vec![Some((Some(0.50), Some(1.00)))],
1732 ),
1733 File::new_nullable("2", "2023-01-02", vec![Some((None, Some(1.00)))]),
1734 ],
1735 sort: vec![col("value").sort(true, true)],
1736 expected_result: Ok(vec![vec!["0", "1"], vec!["2"]]),
1737 },
1738 TestCase {
1739 name: "all three non-overlapping",
1740 file_schema: Schema::new(vec![Field::new(
1741 "value".to_string(),
1742 DataType::Float64,
1743 false,
1744 )]),
1745 files: vec![
1746 File::new("0", "2023-01-01", vec![Some((0.00, 0.49))]),
1747 File::new("1", "2023-01-01", vec![Some((0.50, 0.99))]),
1748 File::new("2", "2023-01-02", vec![Some((1.00, 1.49))]),
1749 ],
1750 sort: vec![col("value").sort(true, false)],
1751 expected_result: Ok(vec![vec!["0", "1", "2"]]),
1752 },
1753 TestCase {
1754 name: "all three overlapping",
1755 file_schema: Schema::new(vec![Field::new(
1756 "value".to_string(),
1757 DataType::Float64,
1758 false,
1759 )]),
1760 files: vec![
1761 File::new("0", "2023-01-01", vec![Some((0.00, 0.49))]),
1762 File::new("1", "2023-01-01", vec![Some((0.00, 0.49))]),
1763 File::new("2", "2023-01-02", vec![Some((0.00, 0.49))]),
1764 ],
1765 sort: vec![col("value").sort(true, false)],
1766 expected_result: Ok(vec![vec!["0"], vec!["1"], vec!["2"]]),
1767 },
1768 TestCase {
1769 name: "empty input",
1770 file_schema: Schema::new(vec![Field::new(
1771 "value".to_string(),
1772 DataType::Float64,
1773 false,
1774 )]),
1775 files: vec![],
1776 sort: vec![col("value").sort(true, false)],
1777 expected_result: Ok(vec![]),
1778 },
1779 TestCase {
1780 name: "one file missing statistics",
1781 file_schema: Schema::new(vec![Field::new(
1782 "value".to_string(),
1783 DataType::Float64,
1784 false,
1785 )]),
1786 files: vec![
1787 File::new("0", "2023-01-01", vec![Some((0.00, 0.49))]),
1788 File::new("1", "2023-01-01", vec![Some((0.00, 0.49))]),
1789 File::new("2", "2023-01-02", vec![None]),
1790 ],
1791 sort: vec![col("value").sort(true, false)],
1792 expected_result: Err(
1793 "construct min/max statistics for split_groups_by_statistics\ncaused by\ncollect min/max values\ncaused by\nget min/max for column: 'value'\ncaused by\nError during planning: statistics not found",
1794 ),
1795 },
1796 ];
1797
1798 for case in cases {
1799 let table_schema = Arc::new(Schema::new(
1800 case.file_schema
1801 .fields()
1802 .clone()
1803 .into_iter()
1804 .cloned()
1805 .chain(Some(Arc::new(Field::new(
1806 "date".to_string(),
1807 DataType::Utf8,
1808 false,
1809 ))))
1810 .collect::<Vec<_>>(),
1811 ));
1812 let Some(sort_order) = LexOrdering::new(
1813 case.sort
1814 .into_iter()
1815 .map(|expr| {
1816 create_physical_sort_expr(
1817 &expr,
1818 &DFSchema::try_from(Arc::clone(&table_schema))?,
1819 &ExecutionProps::default(),
1820 )
1821 })
1822 .collect::<Result<Vec<_>>>()?,
1823 ) else {
1824 return internal_err!("This test should always use an ordering");
1825 };
1826
1827 let partitioned_files = FileGroup::new(
1828 case.files.into_iter().map(From::from).collect::<Vec<_>>(),
1829 );
1830 let result = FileScanConfig::split_groups_by_statistics(
1831 &table_schema,
1832 std::slice::from_ref(&partitioned_files),
1833 &sort_order,
1834 );
1835 let results_by_name = result
1836 .as_ref()
1837 .map(|file_groups| {
1838 file_groups
1839 .iter()
1840 .map(|file_group| {
1841 file_group
1842 .iter()
1843 .map(|file| {
1844 partitioned_files
1845 .iter()
1846 .find_map(|f| {
1847 if f.object_meta == file.object_meta {
1848 Some(
1849 f.object_meta
1850 .location
1851 .as_ref()
1852 .rsplit('/')
1853 .next()
1854 .unwrap()
1855 .trim_end_matches(".parquet"),
1856 )
1857 } else {
1858 None
1859 }
1860 })
1861 .unwrap()
1862 })
1863 .collect::<Vec<_>>()
1864 })
1865 .collect::<Vec<_>>()
1866 })
1867 .map_err(|e| e.strip_backtrace().leak() as &'static str);
1868
1869 assert_eq!(results_by_name, case.expected_result, "{}", case.name);
1870 }
1871
1872 return Ok(());
1873
1874 impl From<File> for PartitionedFile {
1875 fn from(file: File) -> Self {
1876 let object_meta = ObjectMeta {
1877 location: Path::from(format!(
1878 "data/date={}/{}.parquet",
1879 file.date, file.name
1880 )),
1881 last_modified: chrono::Utc.timestamp_nanos(0),
1882 size: 0,
1883 e_tag: None,
1884 version: None,
1885 };
1886 let statistics = Arc::new(Statistics {
1887 num_rows: Precision::Absent,
1888 total_byte_size: Precision::Absent,
1889 column_statistics: file
1890 .statistics
1891 .into_iter()
1892 .map(|stats| {
1893 stats
1894 .map(|(min, max)| ColumnStatistics {
1895 min_value: Precision::Exact(ScalarValue::Float64(
1896 min,
1897 )),
1898 max_value: Precision::Exact(ScalarValue::Float64(
1899 max,
1900 )),
1901 ..Default::default()
1902 })
1903 .unwrap_or_default()
1904 })
1905 .collect::<Vec<_>>(),
1906 });
1907 PartitionedFile::new_from_meta(object_meta)
1908 .with_partition_values(vec![ScalarValue::from(file.date)])
1909 .with_statistics(statistics)
1910 }
1911 }
1912 }
1913
1914 fn config_for_projection(
1916 file_schema: SchemaRef,
1917 projection: Option<Vec<usize>>,
1918 statistics: Statistics,
1919 table_partition_cols: Vec<Field>,
1920 ) -> FileScanConfig {
1921 let table_schema = TableSchema::new(
1922 file_schema,
1923 table_partition_cols.into_iter().map(Arc::new).collect(),
1924 );
1925 FileScanConfigBuilder::new(
1926 ObjectStoreUrl::parse("test:///").unwrap(),
1927 Arc::new(MockSource::new(table_schema.clone())),
1928 )
1929 .with_projection_indices(projection)
1930 .unwrap()
1931 .with_statistics(statistics)
1932 .build()
1933 }
1934
1935 #[test]
1936 fn test_file_scan_config_builder() {
1937 let file_schema = aggr_test_schema();
1938 let object_store_url = ObjectStoreUrl::parse("test:///").unwrap();
1939
1940 let table_schema = TableSchema::new(
1941 Arc::clone(&file_schema),
1942 vec![Arc::new(Field::new(
1943 "date",
1944 wrap_partition_type_in_dict(DataType::Utf8),
1945 false,
1946 ))],
1947 );
1948
1949 let file_source: Arc<dyn FileSource> =
1950 Arc::new(MockSource::new(table_schema.clone()));
1951
1952 let builder = FileScanConfigBuilder::new(
1954 object_store_url.clone(),
1955 Arc::clone(&file_source),
1956 );
1957
1958 let config = builder
1960 .with_limit(Some(1000))
1961 .with_projection_indices(Some(vec![0, 1]))
1962 .unwrap()
1963 .with_statistics(Statistics::new_unknown(&file_schema))
1964 .with_file_groups(vec![FileGroup::new(vec![PartitionedFile::new(
1965 "test.parquet".to_string(),
1966 1024,
1967 )])])
1968 .with_output_ordering(vec![
1969 [PhysicalSortExpr::new_default(Arc::new(Column::new(
1970 "date", 0,
1971 )))]
1972 .into(),
1973 ])
1974 .with_file_compression_type(FileCompressionType::UNCOMPRESSED)
1975 .build();
1976
1977 assert_eq!(config.object_store_url, object_store_url);
1979 assert_eq!(*config.file_schema(), file_schema);
1980 assert_eq!(config.limit, Some(1000));
1981 assert_eq!(
1982 config
1983 .file_source
1984 .projection()
1985 .as_ref()
1986 .map(|p| p.column_indices()),
1987 Some(vec![0, 1])
1988 );
1989 assert_eq!(config.table_partition_cols().len(), 1);
1990 assert_eq!(config.table_partition_cols()[0].name(), "date");
1991 assert_eq!(config.file_groups.len(), 1);
1992 assert_eq!(config.file_groups[0].len(), 1);
1993 assert_eq!(
1994 config.file_groups[0][0].object_meta.location.as_ref(),
1995 "test.parquet"
1996 );
1997 assert_eq!(
1998 config.file_compression_type,
1999 FileCompressionType::UNCOMPRESSED
2000 );
2001 assert_eq!(config.output_ordering.len(), 1);
2002 }
2003
2004 #[test]
2005 fn equivalence_properties_after_schema_change() {
2006 let file_schema = aggr_test_schema();
2007 let object_store_url = ObjectStoreUrl::parse("test:///").unwrap();
2008
2009 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2010
2011 let file_source: Arc<dyn FileSource> = Arc::new(
2013 MockSource::new(table_schema.clone()).with_filter(Arc::new(BinaryExpr::new(
2014 col("c2", &file_schema).unwrap(),
2015 Operator::Eq,
2016 Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
2017 ))),
2018 );
2019
2020 let config = FileScanConfigBuilder::new(
2021 object_store_url.clone(),
2022 Arc::clone(&file_source),
2023 )
2024 .with_projection_indices(Some(vec![0, 1, 2]))
2025 .unwrap()
2026 .build();
2027
2028 let exprs = ProjectionExprs::new(vec![ProjectionExpr::new(
2031 col("c1", &file_schema).unwrap(),
2032 "c1",
2033 )]);
2034 let data_source = config
2035 .try_swapping_with_projection(&exprs)
2036 .unwrap()
2037 .unwrap();
2038
2039 let eq_properties = data_source.eq_properties();
2042 let eq_group = eq_properties.eq_group();
2043
2044 for class in eq_group.iter() {
2045 for expr in class.iter() {
2046 if let Some(col) = expr.downcast_ref::<Column>() {
2047 assert_ne!(
2048 col.name(),
2049 "c2",
2050 "c2 should not be present in any equivalence class"
2051 );
2052 }
2053 }
2054 }
2055 }
2056
2057 #[test]
2058 fn test_file_scan_config_builder_defaults() {
2059 let file_schema = aggr_test_schema();
2060 let object_store_url = ObjectStoreUrl::parse("test:///").unwrap();
2061
2062 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2063
2064 let file_source: Arc<dyn FileSource> =
2065 Arc::new(MockSource::new(table_schema.clone()));
2066
2067 let config = FileScanConfigBuilder::new(
2069 object_store_url.clone(),
2070 Arc::clone(&file_source),
2071 )
2072 .build();
2073
2074 assert_eq!(config.object_store_url, object_store_url);
2076 assert_eq!(*config.file_schema(), file_schema);
2077 assert_eq!(config.limit, None);
2078 let expected_projection: Vec<usize> = (0..file_schema.fields().len()).collect();
2081 assert_eq!(
2082 config
2083 .file_source
2084 .projection()
2085 .as_ref()
2086 .map(|p| p.column_indices()),
2087 Some(expected_projection)
2088 );
2089 assert!(config.table_partition_cols().is_empty());
2090 assert!(config.file_groups.is_empty());
2091 assert_eq!(
2092 config.file_compression_type,
2093 FileCompressionType::UNCOMPRESSED
2094 );
2095 assert!(config.output_ordering.is_empty());
2096 assert!(config.constraints.is_empty());
2097
2098 assert_eq!(config.statistics().num_rows, Precision::Absent);
2100 assert_eq!(config.statistics().total_byte_size, Precision::Absent);
2101 assert_eq!(
2102 config.statistics().column_statistics.len(),
2103 file_schema.fields().len()
2104 );
2105 for stat in config.statistics().column_statistics {
2106 assert_eq!(stat.distinct_count, Precision::Absent);
2107 assert_eq!(stat.min_value, Precision::Absent);
2108 assert_eq!(stat.max_value, Precision::Absent);
2109 assert_eq!(stat.null_count, Precision::Absent);
2110 }
2111 }
2112
2113 #[test]
2114 fn test_file_scan_config_builder_new_from() {
2115 let schema = aggr_test_schema();
2116 let object_store_url = ObjectStoreUrl::parse("test:///").unwrap();
2117 let partition_cols = vec![Field::new(
2118 "date",
2119 wrap_partition_type_in_dict(DataType::Utf8),
2120 false,
2121 )];
2122 let file = PartitionedFile::new("test_file.parquet", 100);
2123
2124 let table_schema = TableSchema::new(
2125 Arc::clone(&schema),
2126 partition_cols.iter().map(|f| Arc::new(f.clone())).collect(),
2127 );
2128
2129 let file_source: Arc<dyn FileSource> =
2130 Arc::new(MockSource::new(table_schema.clone()));
2131
2132 let original_config = FileScanConfigBuilder::new(
2134 object_store_url.clone(),
2135 Arc::clone(&file_source),
2136 )
2137 .with_projection_indices(Some(vec![0, 2]))
2138 .unwrap()
2139 .with_limit(Some(10))
2140 .with_file(file.clone())
2141 .with_constraints(Constraints::default())
2142 .build();
2143
2144 let new_builder = FileScanConfigBuilder::from(original_config);
2146
2147 let new_config = new_builder.build();
2149
2150 let partition_cols = partition_cols.into_iter().map(Arc::new).collect::<Vec<_>>();
2152 assert_eq!(new_config.object_store_url, object_store_url);
2153 assert_eq!(*new_config.file_schema(), schema);
2154 assert_eq!(
2155 new_config
2156 .file_source
2157 .projection()
2158 .as_ref()
2159 .map(|p| p.column_indices()),
2160 Some(vec![0, 2])
2161 );
2162 assert_eq!(new_config.limit, Some(10));
2163 assert_eq!(*new_config.table_partition_cols(), partition_cols);
2164 assert_eq!(new_config.file_groups.len(), 1);
2165 assert_eq!(new_config.file_groups[0].len(), 1);
2166 assert_eq!(
2167 new_config.file_groups[0][0].object_meta.location.as_ref(),
2168 "test_file.parquet"
2169 );
2170 assert_eq!(new_config.constraints, Constraints::default());
2171 }
2172
2173 #[test]
2174 fn test_split_groups_by_statistics_with_target_partitions() -> Result<()> {
2175 use datafusion_common::DFSchema;
2176 use datafusion_expr::{col, execution_props::ExecutionProps};
2177
2178 let schema = Arc::new(Schema::new(vec![Field::new(
2179 "value",
2180 DataType::Float64,
2181 false,
2182 )]));
2183
2184 let exec_props = ExecutionProps::new();
2186 let df_schema = DFSchema::try_from_qualified_schema("test", schema.as_ref())?;
2187 let sort_expr = [col("value").sort(true, false)];
2188 let sort_ordering = sort_expr
2189 .map(|expr| {
2190 create_physical_sort_expr(&expr, &df_schema, &exec_props).unwrap()
2191 })
2192 .into();
2193
2194 struct TestCase {
2196 name: String,
2197 file_count: usize,
2198 overlap_factor: f64,
2199 target_partitions: usize,
2200 expected_partition_count: usize,
2201 }
2202
2203 let test_cases = vec![
2204 TestCase {
2206 name: "no_overlap_10_files_4_partitions".to_string(),
2207 file_count: 10,
2208 overlap_factor: 0.0,
2209 target_partitions: 4,
2210 expected_partition_count: 4,
2211 },
2212 TestCase {
2213 name: "medium_overlap_20_files_5_partitions".to_string(),
2214 file_count: 20,
2215 overlap_factor: 0.5,
2216 target_partitions: 5,
2217 expected_partition_count: 5,
2218 },
2219 TestCase {
2220 name: "high_overlap_30_files_3_partitions".to_string(),
2221 file_count: 30,
2222 overlap_factor: 0.8,
2223 target_partitions: 3,
2224 expected_partition_count: 7,
2225 },
2226 TestCase {
2228 name: "fewer_files_than_partitions".to_string(),
2229 file_count: 3,
2230 overlap_factor: 0.0,
2231 target_partitions: 10,
2232 expected_partition_count: 3, },
2234 TestCase {
2235 name: "single_file".to_string(),
2236 file_count: 1,
2237 overlap_factor: 0.0,
2238 target_partitions: 5,
2239 expected_partition_count: 1, },
2241 TestCase {
2242 name: "empty_files".to_string(),
2243 file_count: 0,
2244 overlap_factor: 0.0,
2245 target_partitions: 3,
2246 expected_partition_count: 0, },
2248 ];
2249
2250 for case in test_cases {
2251 println!("Running test case: {}", case.name);
2252
2253 let file_groups = generate_test_files(case.file_count, case.overlap_factor);
2255
2256 let result =
2258 FileScanConfig::split_groups_by_statistics_with_target_partitions(
2259 &schema,
2260 &file_groups,
2261 &sort_ordering,
2262 case.target_partitions,
2263 )?;
2264
2265 println!(
2267 "Created {} partitions (target was {})",
2268 result.len(),
2269 case.target_partitions
2270 );
2271
2272 assert_eq!(
2274 result.len(),
2275 case.expected_partition_count,
2276 "Case '{}': Unexpected partition count",
2277 case.name
2278 );
2279
2280 assert!(
2282 verify_sort_integrity(&result),
2283 "Case '{}': Files within partitions are not properly ordered",
2284 case.name
2285 );
2286
2287 if case.file_count > 1 && case.expected_partition_count > 1 {
2289 let group_sizes: Vec<usize> = result.iter().map(FileGroup::len).collect();
2290 let max_size = *group_sizes.iter().max().unwrap();
2291 let min_size = *group_sizes.iter().min().unwrap();
2292
2293 let avg_files_per_partition =
2295 case.file_count as f64 / case.expected_partition_count as f64;
2296 assert!(
2297 (max_size as f64) < 2.0 * avg_files_per_partition,
2298 "Case '{}': Unbalanced distribution. Max partition size {} exceeds twice the average {}",
2299 case.name,
2300 max_size,
2301 avg_files_per_partition
2302 );
2303
2304 println!("Distribution - min files: {min_size}, max files: {max_size}");
2305 }
2306 }
2307
2308 let empty_groups: Vec<FileGroup> = vec![];
2310 let err = FileScanConfig::split_groups_by_statistics_with_target_partitions(
2311 &schema,
2312 &empty_groups,
2313 &sort_ordering,
2314 0,
2315 )
2316 .unwrap_err();
2317
2318 assert!(
2319 err.to_string()
2320 .contains("target_partitions must be greater than 0"),
2321 "Expected error for zero target partitions"
2322 );
2323
2324 Ok(())
2325 }
2326
2327 #[test]
2328 fn test_partition_statistics_projection() {
2329 use crate::source::DataSourceExec;
2335 use datafusion_physical_plan::ExecutionPlan;
2336
2337 let schema = Arc::new(Schema::new(vec![
2339 Field::new("col0", DataType::Int32, false),
2340 Field::new("col1", DataType::Int32, false),
2341 Field::new("col2", DataType::Int32, false),
2342 Field::new("col3", DataType::Int32, false),
2343 ]));
2344
2345 let file_group_stats = Statistics {
2347 num_rows: Precision::Exact(100),
2348 total_byte_size: Precision::Exact(1024),
2349 column_statistics: vec![
2350 ColumnStatistics {
2351 null_count: Precision::Exact(0),
2352 ..ColumnStatistics::new_unknown()
2353 },
2354 ColumnStatistics {
2355 null_count: Precision::Exact(5),
2356 ..ColumnStatistics::new_unknown()
2357 },
2358 ColumnStatistics {
2359 null_count: Precision::Exact(10),
2360 ..ColumnStatistics::new_unknown()
2361 },
2362 ColumnStatistics {
2363 null_count: Precision::Exact(15),
2364 ..ColumnStatistics::new_unknown()
2365 },
2366 ],
2367 };
2368
2369 let file_group = FileGroup::new(vec![PartitionedFile::new("test.parquet", 1024)])
2371 .with_statistics(Arc::new(file_group_stats));
2372
2373 let table_schema = TableSchema::new(Arc::clone(&schema), vec![]);
2374
2375 let config = FileScanConfigBuilder::new(
2377 ObjectStoreUrl::parse("test:///").unwrap(),
2378 Arc::new(MockSource::new(table_schema.clone())),
2379 )
2380 .with_projection_indices(Some(vec![0, 2]))
2381 .unwrap() .with_file_groups(vec![file_group])
2383 .build();
2384
2385 let exec = DataSourceExec::from_data_source(config);
2387
2388 let partition_stats = exec.partition_statistics(Some(0)).unwrap();
2390
2391 assert_eq!(
2393 partition_stats.column_statistics.len(),
2394 2,
2395 "Expected 2 column statistics (projected), but got {}",
2396 partition_stats.column_statistics.len()
2397 );
2398
2399 assert_eq!(
2401 partition_stats.column_statistics[0].null_count,
2402 Precision::Exact(0),
2403 "First projected column should be col0 with 0 nulls"
2404 );
2405 assert_eq!(
2406 partition_stats.column_statistics[1].null_count,
2407 Precision::Exact(10),
2408 "Second projected column should be col2 with 10 nulls"
2409 );
2410
2411 assert_eq!(partition_stats.num_rows, Precision::Exact(100));
2413 assert_eq!(partition_stats.total_byte_size, Precision::Exact(800));
2414 }
2415
2416 #[tokio::test]
2423 async fn reset_state_recreates_shared_work_source() -> Result<()> {
2424 let schema = Arc::new(Schema::new(vec![Field::new(
2425 "value",
2426 DataType::Int32,
2427 false,
2428 )]));
2429 let file_source = Arc::new(
2430 MockSource::new(Arc::clone(&schema))
2431 .with_file_opener(Arc::new(ResetStateTestFileOpener { schema })),
2432 );
2433
2434 let config =
2435 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2436 .with_file_group(FileGroup::new(vec![
2437 PartitionedFile::new("file1.parquet", 100),
2438 PartitionedFile::new("file2.parquet", 100),
2439 ]))
2440 .build();
2441
2442 let exec: Arc<dyn ExecutionPlan> = DataSourceExec::from_data_source(config);
2443 let task_ctx = Arc::new(TaskContext::default());
2444
2445 let first_run = collect(Arc::clone(&exec), Arc::clone(&task_ctx)).await?;
2448 let reset_exec = exec.reset_state()?;
2449 let second_run = collect(reset_exec, task_ctx).await?;
2450
2451 let expected = [
2452 "+-------+",
2453 "| value |",
2454 "+-------+",
2455 "| 1 |",
2456 "| 2 |",
2457 "+-------+",
2458 ];
2459 assert_batches_eq!(expected, &first_run);
2460 assert_batches_eq!(expected, &second_run);
2461
2462 Ok(())
2463 }
2464
2465 #[derive(Debug)]
2468 struct ResetStateTestFileOpener {
2469 schema: SchemaRef,
2470 }
2471
2472 impl crate::file_stream::FileOpener for ResetStateTestFileOpener {
2473 fn open(
2474 &self,
2475 file: PartitionedFile,
2476 ) -> Result<crate::file_stream::FileOpenFuture> {
2477 let value = file
2478 .object_meta
2479 .location
2480 .as_ref()
2481 .trim_start_matches("file")
2482 .trim_end_matches(".parquet")
2483 .parse::<i32>()
2484 .expect("invalid test file name");
2485 let schema = Arc::clone(&self.schema);
2486 Ok(async move {
2487 let batch = RecordBatch::try_new(
2488 schema,
2489 vec![Arc::new(Int32Array::from(vec![value]))],
2490 )
2491 .expect("test batch should be valid");
2492 Ok(stream::iter(vec![Ok(batch)]).boxed())
2493 }
2494 .boxed())
2495 }
2496 }
2497
2498 #[test]
2499 fn test_output_partitioning_not_partitioned_by_file_group() {
2500 let file_schema = aggr_test_schema();
2501 let partition_col =
2502 Field::new("date", wrap_partition_type_in_dict(DataType::Utf8), false);
2503
2504 let config = config_for_projection(
2505 Arc::clone(&file_schema),
2506 None,
2507 Statistics::new_unknown(&file_schema),
2508 vec![partition_col],
2509 );
2510
2511 let partitioning = config.output_partitioning();
2513 assert!(matches!(partitioning, Partitioning::UnknownPartitioning(_)));
2514 }
2515
2516 #[test]
2517 fn test_output_partitioning_no_partition_columns() {
2518 let file_schema = aggr_test_schema();
2519 let mut config = config_for_projection(
2520 Arc::clone(&file_schema),
2521 None,
2522 Statistics::new_unknown(&file_schema),
2523 vec![], );
2525 config.partitioned_by_file_group = true;
2526
2527 let partitioning = config.output_partitioning();
2528 assert!(matches!(partitioning, Partitioning::UnknownPartitioning(_)));
2529 }
2530
2531 #[test]
2532 fn test_output_partitioning_with_partition_columns() {
2533 let file_schema = aggr_test_schema();
2534
2535 let single_partition_col = vec![Field::new(
2537 "date",
2538 wrap_partition_type_in_dict(DataType::Utf8),
2539 false,
2540 )];
2541
2542 let mut config = config_for_projection(
2543 Arc::clone(&file_schema),
2544 None,
2545 Statistics::new_unknown(&file_schema),
2546 single_partition_col,
2547 );
2548 config.partitioned_by_file_group = true;
2549 config.file_groups = vec![
2550 FileGroup::new(vec![PartitionedFile::new("f1.parquet".to_string(), 1024)]),
2551 FileGroup::new(vec![PartitionedFile::new("f2.parquet".to_string(), 1024)]),
2552 FileGroup::new(vec![PartitionedFile::new("f3.parquet".to_string(), 1024)]),
2553 ];
2554
2555 let partitioning = config.output_partitioning();
2556 match partitioning {
2557 Partitioning::Hash(exprs, num_partitions) => {
2558 assert_eq!(num_partitions, 3);
2559 assert_eq!(exprs.len(), 1);
2560 assert_eq!(exprs[0].downcast_ref::<Column>().unwrap().name(), "date");
2561 }
2562 _ => panic!("Expected Hash partitioning"),
2563 }
2564
2565 let multiple_partition_cols = vec![
2567 Field::new("year", wrap_partition_type_in_dict(DataType::Utf8), false),
2568 Field::new("month", wrap_partition_type_in_dict(DataType::Utf8), false),
2569 ];
2570
2571 config = config_for_projection(
2572 Arc::clone(&file_schema),
2573 None,
2574 Statistics::new_unknown(&file_schema),
2575 multiple_partition_cols,
2576 );
2577 config.partitioned_by_file_group = true;
2578 config.file_groups = vec![
2579 FileGroup::new(vec![PartitionedFile::new("f1.parquet".to_string(), 1024)]),
2580 FileGroup::new(vec![PartitionedFile::new("f2.parquet".to_string(), 1024)]),
2581 ];
2582
2583 let partitioning = config.output_partitioning();
2584 match partitioning {
2585 Partitioning::Hash(exprs, num_partitions) => {
2586 assert_eq!(num_partitions, 2);
2587 assert_eq!(exprs.len(), 2);
2588 let col_names: Vec<_> = exprs
2589 .iter()
2590 .map(|e| e.downcast_ref::<Column>().unwrap().name())
2591 .collect();
2592 assert_eq!(col_names, vec!["year", "month"]);
2593 }
2594 _ => panic!("Expected Hash partitioning"),
2595 }
2596 }
2597
2598 #[test]
2599 fn try_pushdown_sort_reverses_file_groups_only_when_requested_is_reverse()
2600 -> Result<()> {
2601 let file_schema =
2602 Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
2603
2604 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2605 let file_source = Arc::new(InexactSortPushdownSource::new(table_schema));
2606
2607 let file_groups = vec![FileGroup::new(vec![
2608 PartitionedFile::new("file1", 1),
2609 PartitionedFile::new("file2", 1),
2610 ])];
2611
2612 let sort_expr_asc = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2613 let config =
2614 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2615 .with_file_groups(file_groups)
2616 .with_output_ordering(vec![
2617 LexOrdering::new(vec![sort_expr_asc.clone()]).unwrap(),
2618 ])
2619 .build();
2620
2621 let requested_asc = vec![sort_expr_asc.clone()];
2622 let result = config.try_pushdown_sort(&requested_asc)?;
2623 let SortOrderPushdownResult::Inexact { inner } = result else {
2624 panic!("Expected Inexact result");
2625 };
2626 let pushed_config = inner
2627 .downcast_ref::<FileScanConfig>()
2628 .expect("Expected FileScanConfig");
2629 let pushed_files = pushed_config.file_groups[0].files();
2630 assert_eq!(pushed_files[0].object_meta.location.as_ref(), "file1");
2631 assert_eq!(pushed_files[1].object_meta.location.as_ref(), "file2");
2632
2633 let requested_desc = vec![sort_expr_asc.reverse()];
2634 let result = config.try_pushdown_sort(&requested_desc)?;
2635 let SortOrderPushdownResult::Inexact { inner } = result else {
2636 panic!("Expected Inexact result");
2637 };
2638 let pushed_config = inner
2639 .downcast_ref::<FileScanConfig>()
2640 .expect("Expected FileScanConfig");
2641 let pushed_files = pushed_config.file_groups[0].files();
2642 assert_eq!(pushed_files[0].object_meta.location.as_ref(), "file2");
2643 assert_eq!(pushed_files[1].object_meta.location.as_ref(), "file1");
2644
2645 Ok(())
2646 }
2647
2648 fn make_file_with_stats(name: &str, min: f64, max: f64) -> PartitionedFile {
2649 PartitionedFile::new(name.to_string(), 1024).with_statistics(Arc::new(
2650 Statistics {
2651 num_rows: Precision::Exact(100),
2652 total_byte_size: Precision::Exact(1024),
2653 column_statistics: vec![ColumnStatistics {
2654 null_count: Precision::Exact(0),
2655 min_value: Precision::Exact(ScalarValue::Float64(Some(min))),
2656 max_value: Precision::Exact(ScalarValue::Float64(Some(max))),
2657 ..Default::default()
2658 }],
2659 },
2660 ))
2661 }
2662
2663 #[derive(Clone)]
2664 struct ExactSortPushdownSource {
2665 metrics: ExecutionPlanMetricsSet,
2666 table_schema: TableSchema,
2667 }
2668
2669 impl ExactSortPushdownSource {
2670 fn new(table_schema: TableSchema) -> Self {
2671 Self {
2672 metrics: ExecutionPlanMetricsSet::new(),
2673 table_schema,
2674 }
2675 }
2676 }
2677
2678 impl FileSource for ExactSortPushdownSource {
2679 fn create_file_opener(
2680 &self,
2681 _object_store: Arc<dyn ObjectStore>,
2682 _base_config: &FileScanConfig,
2683 _partition: usize,
2684 ) -> Result<Arc<dyn crate::file_stream::FileOpener>> {
2685 unimplemented!()
2686 }
2687
2688 fn table_schema(&self) -> &TableSchema {
2689 &self.table_schema
2690 }
2691
2692 fn with_batch_size(&self, _batch_size: usize) -> Arc<dyn FileSource> {
2693 Arc::new(self.clone())
2694 }
2695
2696 fn metrics(&self) -> &ExecutionPlanMetricsSet {
2697 &self.metrics
2698 }
2699
2700 fn file_type(&self) -> &str {
2701 "mock_exact"
2702 }
2703
2704 fn try_pushdown_sort(
2705 &self,
2706 _order: &[PhysicalSortExpr],
2707 _eq_properties: &EquivalenceProperties,
2708 ) -> Result<SortOrderPushdownResult<Arc<dyn FileSource>>> {
2709 Ok(SortOrderPushdownResult::Exact {
2710 inner: Arc::new(self.clone()) as Arc<dyn FileSource>,
2711 })
2712 }
2713 }
2714
2715 #[test]
2716 fn sort_pushdown_unsupported_source_files_get_sorted() -> Result<()> {
2717 let file_schema =
2718 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2719 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2720 let file_source = Arc::new(MockSource::new(table_schema));
2721
2722 let file_groups = vec![FileGroup::new(vec![
2723 make_file_with_stats("file3", 20.0, 30.0),
2724 make_file_with_stats("file1", 0.0, 9.0),
2725 make_file_with_stats("file2", 10.0, 19.0),
2726 ])];
2727
2728 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2729 let config =
2730 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2731 .with_file_groups(file_groups)
2732 .build();
2733
2734 let result = config.try_pushdown_sort(&[sort_expr])?;
2735 let SortOrderPushdownResult::Inexact { inner } = result else {
2736 panic!("Expected Inexact result, got {result:?}");
2737 };
2738 let pushed_config = inner
2739 .downcast_ref::<FileScanConfig>()
2740 .expect("Expected FileScanConfig");
2741 let files = pushed_config.file_groups[0].files();
2742 assert_eq!(files[0].object_meta.location.as_ref(), "file1");
2743 assert_eq!(files[1].object_meta.location.as_ref(), "file2");
2744 assert_eq!(files[2].object_meta.location.as_ref(), "file3");
2745 assert!(pushed_config.output_ordering.is_empty());
2746 Ok(())
2747 }
2748
2749 #[test]
2750 fn sort_pushdown_unsupported_source_already_sorted() -> Result<()> {
2751 let file_schema =
2752 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2753 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2754 let file_source = Arc::new(MockSource::new(table_schema));
2755
2756 let file_groups = vec![FileGroup::new(vec![
2757 make_file_with_stats("file1", 0.0, 9.0),
2758 make_file_with_stats("file2", 10.0, 19.0),
2759 make_file_with_stats("file3", 20.0, 30.0),
2760 ])];
2761
2762 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2763 let config =
2764 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2765 .with_file_groups(file_groups)
2766 .build();
2767
2768 let result = config.try_pushdown_sort(&[sort_expr])?;
2769 assert!(matches!(result, SortOrderPushdownResult::Unsupported));
2770 Ok(())
2771 }
2772
2773 #[test]
2774 fn sort_pushdown_unsupported_source_descending_sort() -> Result<()> {
2775 let file_schema =
2776 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2777 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2778 let file_source = Arc::new(MockSource::new(table_schema));
2779
2780 let file_groups = vec![FileGroup::new(vec![
2781 make_file_with_stats("file1", 0.0, 9.0),
2782 make_file_with_stats("file3", 20.0, 30.0),
2783 make_file_with_stats("file2", 10.0, 19.0),
2784 ])];
2785
2786 let sort_expr = PhysicalSortExpr::new(
2787 Arc::new(Column::new("a", 0)),
2788 arrow::compute::SortOptions {
2789 descending: true,
2790 nulls_first: true,
2791 },
2792 );
2793 let config =
2794 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2795 .with_file_groups(file_groups)
2796 .build();
2797
2798 let result = config.try_pushdown_sort(&[sort_expr])?;
2799 let SortOrderPushdownResult::Inexact { inner } = result else {
2800 panic!("Expected Inexact result");
2801 };
2802 let pushed_config = inner
2803 .downcast_ref::<FileScanConfig>()
2804 .expect("Expected FileScanConfig");
2805 let files = pushed_config.file_groups[0].files();
2806 assert_eq!(files[0].object_meta.location.as_ref(), "file3");
2807 assert_eq!(files[1].object_meta.location.as_ref(), "file2");
2808 assert_eq!(files[2].object_meta.location.as_ref(), "file1");
2809 Ok(())
2810 }
2811
2812 #[test]
2813 fn sort_pushdown_exact_source_non_overlapping_returns_exact() -> Result<()> {
2814 let file_schema =
2815 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2816 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2817 let file_source = Arc::new(ExactSortPushdownSource::new(table_schema));
2818
2819 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2820
2821 let file_groups = vec![FileGroup::new(vec![
2822 make_file_with_stats("file1", 0.0, 9.0),
2823 make_file_with_stats("file2", 10.0, 19.0),
2824 make_file_with_stats("file3", 20.0, 30.0),
2825 ])];
2826
2827 let config =
2828 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2829 .with_file_groups(file_groups)
2830 .with_output_ordering(vec![
2831 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
2832 ])
2833 .build();
2834
2835 let result = config.try_pushdown_sort(&[sort_expr])?;
2836 let SortOrderPushdownResult::Exact { inner } = result else {
2837 panic!("Expected Exact result, got {result:?}");
2838 };
2839 let pushed_config = inner
2840 .downcast_ref::<FileScanConfig>()
2841 .expect("Expected FileScanConfig");
2842 assert!(!pushed_config.output_ordering.is_empty());
2843 Ok(())
2844 }
2845
2846 #[test]
2847 fn sort_pushdown_exact_source_overlapping_downgraded_to_inexact() -> Result<()> {
2848 let file_schema =
2849 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2850 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2851 let file_source = Arc::new(ExactSortPushdownSource::new(table_schema));
2852
2853 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2854
2855 let file_groups = vec![FileGroup::new(vec![
2856 make_file_with_stats("file1", 0.0, 15.0),
2857 make_file_with_stats("file2", 10.0, 25.0),
2858 make_file_with_stats("file3", 20.0, 30.0),
2859 ])];
2860
2861 let config =
2862 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2863 .with_file_groups(file_groups)
2864 .with_output_ordering(vec![
2865 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
2866 ])
2867 .build();
2868
2869 let result = config.try_pushdown_sort(&[sort_expr])?;
2870 let SortOrderPushdownResult::Inexact { inner } = result else {
2871 panic!("Expected Inexact (downgraded), got {result:?}");
2872 };
2873 let pushed_config = inner
2874 .downcast_ref::<FileScanConfig>()
2875 .expect("Expected FileScanConfig");
2876 assert!(pushed_config.output_ordering.is_empty());
2877 Ok(())
2878 }
2879
2880 #[test]
2881 fn sort_pushdown_exact_source_out_of_order_returns_exact() -> Result<()> {
2882 let file_schema =
2883 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2884 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2885 let file_source = Arc::new(ExactSortPushdownSource::new(table_schema));
2886
2887 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2888
2889 let file_groups = vec![FileGroup::new(vec![
2890 make_file_with_stats("file3", 20.0, 30.0),
2891 make_file_with_stats("file1", 0.0, 9.0),
2892 make_file_with_stats("file2", 10.0, 19.0),
2893 ])];
2894
2895 let config =
2896 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2897 .with_file_groups(file_groups)
2898 .with_output_ordering(vec![
2899 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
2900 ])
2901 .build();
2902
2903 let result = config.try_pushdown_sort(&[sort_expr])?;
2904 let SortOrderPushdownResult::Exact { inner } = result else {
2905 panic!("Expected Exact result, got {result:?}");
2906 };
2907 let pushed_config = inner
2908 .downcast_ref::<FileScanConfig>()
2909 .expect("Expected FileScanConfig");
2910 let files = pushed_config.file_groups[0].files();
2911 assert_eq!(files[0].object_meta.location.as_ref(), "file1");
2912 assert_eq!(files[1].object_meta.location.as_ref(), "file2");
2913 assert_eq!(files[2].object_meta.location.as_ref(), "file3");
2914 assert!(!pushed_config.output_ordering.is_empty());
2915 Ok(())
2916 }
2917
2918 #[test]
2919 fn sort_pushdown_unsupported_source_single_file_groups() -> Result<()> {
2920 let file_schema =
2921 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2922 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2923 let file_source = Arc::new(MockSource::new(table_schema));
2924
2925 let file_groups = vec![
2926 FileGroup::new(vec![make_file_with_stats("file1", 0.0, 9.0)]),
2927 FileGroup::new(vec![make_file_with_stats("file2", 10.0, 19.0)]),
2928 ];
2929
2930 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2931 let config =
2932 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2933 .with_file_groups(file_groups)
2934 .build();
2935
2936 let result = config.try_pushdown_sort(&[sort_expr])?;
2937 assert!(
2938 matches!(result, SortOrderPushdownResult::Unsupported),
2939 "Expected Unsupported for single-file groups"
2940 );
2941 Ok(())
2942 }
2943
2944 #[test]
2945 fn sort_pushdown_unsupported_source_multiple_groups() -> Result<()> {
2946 let file_schema =
2947 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2948 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2949 let file_source = Arc::new(MockSource::new(table_schema));
2950
2951 let file_groups = vec![
2952 FileGroup::new(vec![
2953 make_file_with_stats("file_b", 10.0, 19.0),
2954 make_file_with_stats("file_a", 0.0, 9.0),
2955 ]),
2956 FileGroup::new(vec![
2957 make_file_with_stats("file_d", 30.0, 39.0),
2958 make_file_with_stats("file_c", 20.0, 29.0),
2959 ]),
2960 ];
2961
2962 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
2963 let config =
2964 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
2965 .with_file_groups(file_groups)
2966 .build();
2967
2968 let result = config.try_pushdown_sort(&[sort_expr])?;
2969 let SortOrderPushdownResult::Inexact { inner } = result else {
2970 panic!("Expected Inexact result");
2971 };
2972 let pushed_config = inner
2973 .downcast_ref::<FileScanConfig>()
2974 .expect("Expected FileScanConfig");
2975 let files0 = pushed_config.file_groups[0].files();
2976 assert_eq!(files0[0].object_meta.location.as_ref(), "file_a");
2977 assert_eq!(files0[1].object_meta.location.as_ref(), "file_b");
2978 let files1 = pushed_config.file_groups[1].files();
2979 assert_eq!(files1[0].object_meta.location.as_ref(), "file_c");
2980 assert_eq!(files1[1].object_meta.location.as_ref(), "file_d");
2981 Ok(())
2982 }
2983
2984 #[test]
2985 fn sort_pushdown_unsupported_source_partial_statistics() -> Result<()> {
2986 let file_schema =
2987 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
2988 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
2989 let file_source = Arc::new(MockSource::new(table_schema));
2990
2991 let file_groups = vec![
2992 FileGroup::new(vec![
2993 make_file_with_stats("file_b", 10.0, 19.0),
2994 make_file_with_stats("file_a", 0.0, 9.0),
2995 ]),
2996 FileGroup::new(vec![
2997 PartitionedFile::new("file_d".to_string(), 1024),
2998 PartitionedFile::new("file_c".to_string(), 1024),
2999 ]),
3000 ];
3001
3002 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
3003 let config =
3004 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
3005 .with_file_groups(file_groups)
3006 .build();
3007
3008 let result = config.try_pushdown_sort(&[sort_expr])?;
3009 let SortOrderPushdownResult::Inexact { inner } = result else {
3010 panic!("Expected Inexact result");
3011 };
3012 let pushed_config = inner
3013 .downcast_ref::<FileScanConfig>()
3014 .expect("Expected FileScanConfig");
3015 let files0 = pushed_config.file_groups[0].files();
3016 assert_eq!(files0[0].object_meta.location.as_ref(), "file_a");
3017 assert_eq!(files0[1].object_meta.location.as_ref(), "file_b");
3018 let files1 = pushed_config.file_groups[1].files();
3019 assert_eq!(files1[0].object_meta.location.as_ref(), "file_d");
3020 assert_eq!(files1[1].object_meta.location.as_ref(), "file_c");
3021 Ok(())
3022 }
3023
3024 #[test]
3025 fn sort_pushdown_inexact_source_with_statistics_sorting() -> Result<()> {
3026 let file_schema =
3027 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
3028 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
3029 let file_source = Arc::new(InexactSortPushdownSource::new(table_schema));
3030
3031 let file_groups = vec![FileGroup::new(vec![
3032 make_file_with_stats("file2", 10.0, 19.0),
3033 make_file_with_stats("file1", 0.0, 9.0),
3034 ])];
3035
3036 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
3037 let config =
3038 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
3039 .with_file_groups(file_groups)
3040 .build();
3041
3042 let result = config.try_pushdown_sort(&[sort_expr])?;
3043 let SortOrderPushdownResult::Inexact { inner } = result else {
3044 panic!("Expected Inexact result");
3045 };
3046 let pushed_config = inner
3047 .downcast_ref::<FileScanConfig>()
3048 .expect("Expected FileScanConfig");
3049 let files = pushed_config.file_groups[0].files();
3050 assert_eq!(files[0].object_meta.location.as_ref(), "file1");
3051 assert_eq!(files[1].object_meta.location.as_ref(), "file2");
3052 assert!(pushed_config.output_ordering.is_empty());
3053 Ok(())
3054 }
3055
3056 #[test]
3057 fn sort_pushdown_exact_multi_group_preserves_parallelism() -> Result<()> {
3058 let file_schema =
3064 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
3065 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
3066 let file_source = Arc::new(ExactSortPushdownSource::new(table_schema));
3067
3068 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
3069
3070 let file_groups = vec![
3074 FileGroup::new(vec![
3075 make_file_with_stats("file_01", 0.0, 9.0),
3076 make_file_with_stats("file_03", 20.0, 29.0),
3077 ]),
3078 FileGroup::new(vec![
3079 make_file_with_stats("file_02", 10.0, 19.0),
3080 make_file_with_stats("file_04", 30.0, 39.0),
3081 ]),
3082 ];
3083
3084 let config =
3085 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
3086 .with_file_groups(file_groups)
3087 .with_output_ordering(vec![
3088 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
3089 ])
3090 .build();
3091
3092 let result = config.try_pushdown_sort(&[sort_expr])?;
3093 let SortOrderPushdownResult::Exact { inner } = result else {
3094 panic!("Expected Exact result, got {result:?}");
3095 };
3096 let pushed_config = inner
3097 .downcast_ref::<FileScanConfig>()
3098 .expect("Expected FileScanConfig");
3099
3100 assert_eq!(pushed_config.file_groups.len(), 2);
3102
3103 let files0 = pushed_config.file_groups[0].files();
3106 assert_eq!(files0[0].object_meta.location.as_ref(), "file_01");
3107 assert_eq!(files0[1].object_meta.location.as_ref(), "file_03");
3108 let files1 = pushed_config.file_groups[1].files();
3109 assert_eq!(files1[0].object_meta.location.as_ref(), "file_02");
3110 assert_eq!(files1[1].object_meta.location.as_ref(), "file_04");
3111
3112 assert!(!pushed_config.output_ordering.is_empty());
3114 Ok(())
3115 }
3116
3117 #[test]
3118 fn sort_pushdown_reverse_preserves_file_order_with_stats() -> Result<()> {
3119 let file_schema =
3122 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, false)]));
3123 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
3124 let file_source = Arc::new(InexactSortPushdownSource::new(table_schema));
3125
3126 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
3127
3128 let file_groups = vec![FileGroup::new(vec![
3130 make_file_with_stats("file1", 0.0, 9.0),
3131 make_file_with_stats("file2", 10.0, 19.0),
3132 make_file_with_stats("file3", 20.0, 30.0),
3133 ])];
3134
3135 let config =
3136 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
3137 .with_file_groups(file_groups)
3138 .with_output_ordering(vec![
3139 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
3140 ])
3141 .build();
3142
3143 let result = config.try_pushdown_sort(&[sort_expr.reverse()])?;
3145 let SortOrderPushdownResult::Inexact { inner } = result else {
3146 panic!("Expected Inexact for reverse scan, got {result:?}");
3147 };
3148 let pushed_config = inner
3149 .downcast_ref::<FileScanConfig>()
3150 .expect("Expected FileScanConfig");
3151
3152 let files = pushed_config.file_groups[0].files();
3154 assert_eq!(files[0].object_meta.location.as_ref(), "file3");
3155 assert_eq!(files[1].object_meta.location.as_ref(), "file2");
3156 assert_eq!(files[2].object_meta.location.as_ref(), "file1");
3157
3158 assert!(pushed_config.output_ordering.is_empty());
3160 Ok(())
3161 }
3162
3163 fn make_file_with_null_stats(
3165 name: &str,
3166 min: f64,
3167 max: f64,
3168 null_count: usize,
3169 ) -> PartitionedFile {
3170 PartitionedFile::new(name.to_string(), 1024).with_statistics(Arc::new(
3171 Statistics {
3172 num_rows: Precision::Exact(100),
3173 total_byte_size: Precision::Exact(1024),
3174 column_statistics: vec![ColumnStatistics {
3175 null_count: Precision::Exact(null_count),
3176 min_value: Precision::Exact(ScalarValue::Float64(Some(min))),
3177 max_value: Precision::Exact(ScalarValue::Float64(Some(max))),
3178 ..Default::default()
3179 }],
3180 },
3181 ))
3182 }
3183
3184 #[test]
3185 fn sort_pushdown_unsupported_with_nulls_does_not_upgrade_to_exact() -> Result<()> {
3186 let file_schema =
3189 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, true)]));
3190 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
3191 let file_source = Arc::new(MockSource::new(table_schema));
3192
3193 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
3194
3195 let file_groups = vec![FileGroup::new(vec![
3197 make_file_with_null_stats("b_no_nulls", 10.0, 19.0, 0),
3198 make_file_with_null_stats("a_with_nulls", 0.0, 9.0, 5), ])];
3200
3201 let config =
3202 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
3203 .with_file_groups(file_groups)
3204 .with_output_ordering(vec![
3205 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
3206 ])
3207 .build();
3208
3209 let result = config.try_pushdown_sort(&[sort_expr])?;
3210 assert!(
3212 matches!(result, SortOrderPushdownResult::Inexact { .. }),
3213 "Expected Inexact due to NULLs, got {result:?}"
3214 );
3215 Ok(())
3216 }
3217
3218 #[test]
3219 fn sort_pushdown_unsupported_no_nulls_upgrades_to_exact() -> Result<()> {
3220 let file_schema =
3222 Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, true)]));
3223 let table_schema = TableSchema::new(Arc::clone(&file_schema), vec![]);
3224 let file_source = Arc::new(MockSource::new(table_schema));
3225
3226 let sort_expr = PhysicalSortExpr::new_default(Arc::new(Column::new("a", 0)));
3227
3228 let file_groups = vec![FileGroup::new(vec![
3229 make_file_with_null_stats("b_high", 10.0, 19.0, 0),
3230 make_file_with_null_stats("a_low", 0.0, 9.0, 0),
3231 ])];
3232
3233 let config =
3234 FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), file_source)
3235 .with_file_groups(file_groups)
3236 .with_output_ordering(vec![
3237 LexOrdering::new(vec![sort_expr.clone()]).unwrap(),
3238 ])
3239 .build();
3240
3241 let result = config.try_pushdown_sort(&[sort_expr])?;
3242 assert!(
3243 matches!(result, SortOrderPushdownResult::Exact { .. }),
3244 "Expected Exact (no NULLs), got {result:?}"
3245 );
3246 Ok(())
3247 }
3248
3249 fn make_projection(pairs: Vec<(Arc<dyn PhysicalExpr>, &str)>) -> ProjectionExprs {
3251 ProjectionExprs::new(
3252 pairs
3253 .into_iter()
3254 .map(|(expr, alias)| ProjectionExpr::new(expr, alias)),
3255 )
3256 }
3257
3258 fn make_volatile_expr() -> Arc<dyn PhysicalExpr> {
3261 use datafusion_common::config::ConfigOptions;
3262 use datafusion_expr::ScalarUDF;
3263 use datafusion_functions::math::random::RandomFunc;
3264 use datafusion_physical_expr::ScalarFunctionExpr;
3265
3266 Arc::new(ScalarFunctionExpr::new(
3267 "random",
3268 Arc::new(ScalarUDF::from(RandomFunc::new())),
3269 vec![],
3270 Arc::new(Field::new("random", DataType::Float64, false)),
3271 Arc::new(ConfigOptions::default()),
3272 ))
3273 }
3274
3275 #[test]
3278 fn test_would_duplicate_allows_column_only_inner() {
3279 let col_a: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
3280 let col_b: Arc<dyn PhysicalExpr> = Arc::new(Column::new("b", 1));
3281
3282 let inner =
3283 make_projection(vec![(Arc::clone(&col_a), "a"), (Arc::clone(&col_b), "b")]);
3284
3285 let outer = make_projection(vec![
3287 (Arc::new(Column::new("a", 0)), "x"),
3288 (Arc::new(Column::new("a", 0)), "y"),
3289 ]);
3290
3291 assert!(!would_duplicate_volatile_exprs(&inner, &outer));
3292 }
3293
3294 #[test]
3297 fn test_would_duplicate_allows_deterministic_computed_multi_ref() {
3298 let col_a: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
3299 let col_b: Arc<dyn PhysicalExpr> = Arc::new(Column::new("b", 1));
3300 let inner = make_projection(vec![
3302 (
3303 Arc::new(BinaryExpr::new(
3304 Arc::clone(&col_a),
3305 Operator::Plus,
3306 Arc::clone(&col_b),
3307 )),
3308 "sum",
3309 ),
3310 (Arc::clone(&col_b), "b"),
3311 ]);
3312
3313 let outer = make_projection(vec![
3315 (Arc::new(Column::new("sum", 0)), "x"),
3316 (Arc::new(Column::new("sum", 0)), "y"),
3317 ]);
3318
3319 assert!(!would_duplicate_volatile_exprs(&inner, &outer));
3321 }
3322
3323 #[test]
3326 fn test_would_duplicate_allows_unreferenced_volatile() {
3327 let col_a: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
3328 let inner =
3330 make_projection(vec![(make_volatile_expr(), "r"), (Arc::clone(&col_a), "a")]);
3331
3332 let outer = make_projection(vec![(Arc::new(Column::new("a", 1)), "a")]);
3334
3335 assert!(!would_duplicate_volatile_exprs(&inner, &outer));
3336 }
3337
3338 #[test]
3342 fn test_would_duplicate_blocks_multi_ref_volatile() {
3343 let inner = make_projection(vec![(make_volatile_expr(), "r")]);
3345
3346 let outer = make_projection(vec![
3348 (Arc::new(Column::new("r", 0)), "x"),
3349 (Arc::new(Column::new("r", 0)), "y"),
3350 ]);
3351
3352 assert!(would_duplicate_volatile_exprs(&inner, &outer));
3353 }
3354
3355 #[test]
3358 fn test_would_duplicate_allows_single_ref_volatile() {
3359 let col_a: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
3360 let inner =
3362 make_projection(vec![(make_volatile_expr(), "r"), (Arc::clone(&col_a), "a")]);
3363
3364 let outer = make_projection(vec![
3366 (Arc::new(Column::new("r", 0)), "x"),
3367 (Arc::new(Column::new("a", 1)), "a"),
3368 ]);
3369
3370 assert!(!would_duplicate_volatile_exprs(&inner, &outer));
3371 }
3372
3373 #[test]
3376 fn test_would_duplicate_blocks_single_expr_self_ref_volatile() {
3377 let inner = make_projection(vec![(make_volatile_expr(), "r")]);
3379
3380 let outer = make_projection(vec![(
3382 Arc::new(BinaryExpr::new(
3383 Arc::new(Column::new("r", 0)),
3384 Operator::Plus,
3385 Arc::new(Column::new("r", 0)),
3386 )),
3387 "x",
3388 )]);
3389
3390 assert!(would_duplicate_volatile_exprs(&inner, &outer));
3391 }
3392
3393 #[test]
3396 fn test_would_duplicate_blocks_volatile_nested_in_arithmetic() {
3397 let inner = make_projection(vec![(
3399 Arc::new(BinaryExpr::new(
3400 make_volatile_expr(),
3401 Operator::Plus,
3402 Arc::new(Literal::new(ScalarValue::Float64(Some(1.0)))),
3403 )),
3404 "expr",
3405 )]);
3406
3407 let outer = make_projection(vec![
3409 (Arc::new(Column::new("expr", 0)), "x"),
3410 (Arc::new(Column::new("expr", 0)), "y"),
3411 ]);
3412
3413 assert!(would_duplicate_volatile_exprs(&inner, &outer));
3414 }
3415
3416 #[test]
3418 fn test_would_duplicate_empty_projections() {
3419 let inner = make_projection(vec![]);
3420 let outer = make_projection(vec![]);
3421 assert!(!would_duplicate_volatile_exprs(&inner, &outer));
3422 }
3423}