1use indexmap::{IndexMap, IndexSet};
24use std::collections::{BTreeSet, HashMap};
25use std::sync::Arc;
26
27use datafusion_common::alias::AliasGenerator;
28use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
29use datafusion_common::{Column, DFSchema, Result, qualified_name};
30use datafusion_expr::logical_plan::LogicalPlan;
31use datafusion_expr::{Expr, ExpressionPlacement, Projection};
32
33use crate::optimizer::ApplyOrder;
34use crate::push_down_filter::replace_cols_by_name;
35use crate::utils::{ColumnReference, has_all_column_refs, schema_columns};
36use crate::{OptimizerConfig, OptimizerRule};
37
38const EXTRACTED_EXPR_PREFIX: &str = "__datafusion_extracted";
45
46fn has_extractable_expr(exprs: &[Expr]) -> bool {
54 exprs.iter().any(|expr| {
55 expr.exists(|e| Ok(e.placement() == ExpressionPlacement::MoveTowardsLeafNodes))
56 .unwrap_or(false)
57 })
58}
59
60#[derive(Default, Debug)]
103pub struct ExtractLeafExpressions {}
104
105impl ExtractLeafExpressions {
106 pub fn new() -> Self {
108 Self {}
109 }
110}
111
112impl OptimizerRule for ExtractLeafExpressions {
113 fn name(&self) -> &str {
114 "extract_leaf_expressions"
115 }
116
117 fn rewrite(
118 &self,
119 plan: LogicalPlan,
120 config: &dyn OptimizerConfig,
121 ) -> Result<Transformed<LogicalPlan>> {
122 if !config.options().optimizer.enable_leaf_expression_pushdown {
123 return Ok(Transformed::no(plan));
124 }
125 let alias_generator = config.alias_generator();
126
127 advance_generator_past_existing(&plan, alias_generator)?;
130
131 plan.transform_down_with_subqueries(|plan| {
132 extract_from_plan(plan, alias_generator)
133 })
134 }
135}
136
137fn advance_generator_past_existing(
141 plan: &LogicalPlan,
142 alias_generator: &AliasGenerator,
143) -> Result<()> {
144 plan.apply(|plan| {
145 plan.expressions().iter().try_for_each(|expr| {
146 expr.apply(|e| {
147 if let Expr::Alias(alias) = e
148 && let Some(id) = alias
149 .name
150 .strip_prefix(EXTRACTED_EXPR_PREFIX)
151 .and_then(|s| s.strip_prefix('_'))
152 .and_then(|s| s.parse().ok())
153 {
154 alias_generator.update_min_id(id);
155 }
156 Ok(TreeNodeRecursion::Continue)
157 })?;
158 Ok::<(), datafusion_common::error::DataFusionError>(())
159 })?;
160 Ok(TreeNodeRecursion::Continue)
161 })
162 .map(|_| ())
163}
164
165fn extract_from_plan(
172 plan: LogicalPlan,
173 alias_generator: &Arc<AliasGenerator>,
174) -> Result<Transformed<LogicalPlan>> {
175 if !matches!(
180 &plan,
181 LogicalPlan::Aggregate(_)
182 | LogicalPlan::Filter(_)
183 | LogicalPlan::Sort(_)
184 | LogicalPlan::Limit(_)
185 | LogicalPlan::Join(_)
186 ) {
187 return Ok(Transformed::no(plan));
188 }
189
190 let inputs = plan.inputs();
191 if inputs.is_empty() {
192 return Ok(Transformed::no(plan));
193 }
194
195 if !has_extractable_expr(&plan.expressions()) {
197 return Ok(Transformed::no(plan));
198 }
199
200 let original_schema = Arc::clone(plan.schema());
202
203 let input_schemas: Vec<Arc<DFSchema>> =
207 inputs.iter().map(|i| Arc::clone(i.schema())).collect();
208
209 let mut extractors: Vec<LeafExpressionExtractor> = input_schemas
211 .iter()
212 .map(|schema| LeafExpressionExtractor::new(schema.as_ref(), alias_generator))
213 .collect();
214
215 let input_column_sets: Vec<std::collections::HashSet<ColumnReference>> =
217 input_schemas
218 .iter()
219 .map(|schema| schema_columns(schema.as_ref()))
220 .collect();
221
222 let transformed = plan.map_expressions(|expr| {
224 routing_extract(expr, &mut extractors, &input_column_sets)
225 })?;
226
227 if !transformed.transformed {
229 return Ok(transformed);
230 }
231
232 let owned_inputs: Vec<Arc<LogicalPlan>> = transformed
235 .data
236 .inputs()
237 .into_iter()
238 .map(|i| Arc::new(i.clone()))
239 .collect();
240
241 let new_inputs: Vec<LogicalPlan> = owned_inputs
243 .into_iter()
244 .zip(extractors.iter())
245 .map(|(input_arc, extractor)| {
246 match extractor.build_extraction_projection(&input_arc)? {
247 Some(plan) => Ok(plan),
248 None => Ok(Arc::unwrap_or_clone(input_arc)),
251 }
252 })
253 .collect::<Result<Vec<_>>>()?;
254
255 let new_plan = transformed
258 .data
259 .with_new_exprs(transformed.data.expressions(), new_inputs)?;
260
261 let recovered = build_recovery_projection(original_schema.as_ref(), new_plan)?;
263
264 Ok(Transformed::yes(recovered))
265}
266
267fn find_owning_input(
273 expr: &Expr,
274 input_column_sets: &[std::collections::HashSet<ColumnReference>],
275) -> Option<usize> {
276 let mut found = None;
277 for (idx, cols) in input_column_sets.iter().enumerate() {
278 if has_all_column_refs(expr, cols) {
279 if found.is_some() {
280 return None;
282 }
283 found = Some(idx);
284 }
285 }
286 found
287}
288
289fn routing_extract(
292 expr: Expr,
293 extractors: &mut [LeafExpressionExtractor],
294 input_column_sets: &[std::collections::HashSet<ColumnReference>],
295) -> Result<Transformed<Expr>> {
296 expr.transform_down(|e| {
297 if let Expr::Alias(alias) = &e
299 && alias.name.starts_with(EXTRACTED_EXPR_PREFIX)
300 {
301 return Ok(Transformed {
302 data: e,
303 transformed: false,
304 tnr: TreeNodeRecursion::Jump,
305 });
306 }
307
308 if matches!(&e, Expr::Alias(_)) {
311 return Ok(Transformed::no(e));
312 }
313
314 match e.placement() {
315 ExpressionPlacement::MoveTowardsLeafNodes => {
316 if let Some(idx) = find_owning_input(&e, input_column_sets) {
317 let col_ref = extractors[idx].add_extracted(e)?;
318 Ok(Transformed::yes(col_ref))
319 } else {
320 Ok(Transformed::no(e))
322 }
323 }
324 ExpressionPlacement::Column => {
325 if let Expr::Column(col) = &e
331 && let Some(idx) = find_owning_input(&e, input_column_sets)
332 {
333 extractors[idx].columns_needed.insert(col.clone());
334 }
335 Ok(Transformed::no(e))
336 }
337 _ => Ok(Transformed::no(e)),
338 }
339 })
340}
341
342fn remap_pairs_and_columns(
352 pairs: &[(Expr, String)],
353 columns: &IndexSet<Column>,
354 from_schema: &DFSchema,
355 to_schema: &DFSchema,
356) -> Result<ExtractionTarget> {
357 let mut replace_map = HashMap::new();
358 for ((from_q, from_f), (to_q, to_f)) in from_schema.iter().zip(to_schema.iter()) {
359 replace_map.insert(
360 qualified_name(from_q, from_f.name()),
361 Expr::Column(Column::new(to_q.cloned(), to_f.name())),
362 );
363 }
364 let remapped_pairs: Vec<(Expr, String)> = pairs
365 .iter()
366 .map(|(expr, alias)| {
367 Ok((
368 replace_cols_by_name(expr.clone(), &replace_map)?,
369 alias.clone(),
370 ))
371 })
372 .collect::<Result<_>>()?;
373 let remapped_columns: IndexSet<Column> = columns
374 .iter()
375 .filter_map(|col| {
376 let rewritten =
377 replace_cols_by_name(Expr::Column(col.clone()), &replace_map).ok()?;
378 if let Expr::Column(c) = rewritten {
379 Some(c)
380 } else {
381 Some(col.clone())
382 }
383 })
384 .collect();
385 Ok(ExtractionTarget {
386 pairs: remapped_pairs,
387 columns: remapped_columns,
388 })
389}
390
391struct ExtractionTarget {
398 pairs: Vec<(Expr, String)>,
400 columns: IndexSet<Column>,
402}
403
404fn build_projection_replace_map(projection: &Projection) -> HashMap<String, Expr> {
409 projection
410 .schema
411 .iter()
412 .zip(projection.expr.iter())
413 .map(|((qualifier, field), expr)| {
414 let key = Column::from((qualifier, field)).flat_name();
415 (key, expr.clone().unalias())
416 })
417 .collect()
418}
419
420fn build_recovery_projection(
444 original_schema: &DFSchema,
445 input: LogicalPlan,
446) -> Result<LogicalPlan> {
447 let new_schema = input.schema();
448 let orig_len = original_schema.fields().len();
449 let new_len = new_schema.fields().len();
450
451 if orig_len == new_len {
452 let schemas_match = original_schema.iter().zip(new_schema.iter()).all(
454 |((orig_q, orig_f), (new_q, new_f))| {
455 orig_f.name() == new_f.name() && orig_q == new_q
456 },
457 );
458 if schemas_match {
459 return Ok(input);
460 }
461
462 debug_assert!(
470 orig_len == new_len,
471 "build_recovery_projection: positional mapping requires same field count, \
472 got original={orig_len} vs new={new_len}"
473 );
474 let mut proj_exprs = Vec::with_capacity(orig_len);
475 for (i, (orig_qualifier, orig_field)) in original_schema.iter().enumerate() {
476 let (new_qualifier, new_field) = new_schema.qualified_field(i);
477 if orig_field.name() == new_field.name() && orig_qualifier == new_qualifier {
478 proj_exprs.push(Expr::from((orig_qualifier, orig_field)));
479 } else {
480 let new_col = Expr::Column(Column::from((new_qualifier, new_field)));
481 proj_exprs.push(
482 new_col.alias_qualified(orig_qualifier.cloned(), orig_field.name()),
483 );
484 }
485 }
486 let projection = Projection::try_new(proj_exprs, Arc::new(input))?;
487 Ok(LogicalPlan::Projection(projection))
488 } else {
489 let col_exprs: Vec<Expr> = original_schema.iter().map(Expr::from).collect();
492 let projection = Projection::try_new(col_exprs, Arc::new(input))?;
493 Ok(LogicalPlan::Projection(projection))
494 }
495}
496
497struct LeafExpressionExtractor<'a> {
508 extracted: IndexMap<Expr, String>,
510 columns_needed: IndexSet<Column>,
513 input_schema: &'a DFSchema,
515 alias_generator: &'a Arc<AliasGenerator>,
517}
518
519impl<'a> LeafExpressionExtractor<'a> {
520 fn new(input_schema: &'a DFSchema, alias_generator: &'a Arc<AliasGenerator>) -> Self {
521 Self {
522 extracted: IndexMap::new(),
523 columns_needed: IndexSet::new(),
524 input_schema,
525 alias_generator,
526 }
527 }
528
529 fn add_extracted(&mut self, expr: Expr) -> Result<Expr> {
531 if let Some(alias) = self.extracted.get(&expr) {
533 return Ok(Expr::Column(Column::new_unqualified(alias)));
534 }
535
536 for col in expr.column_refs() {
538 self.columns_needed.insert(col.clone());
539 }
540
541 let alias = self.alias_generator.next(EXTRACTED_EXPR_PREFIX);
543 self.extracted.insert(expr, alias.clone());
544
545 Ok(Expr::Column(Column::new_unqualified(&alias)))
546 }
547
548 fn build_extraction_projection(
554 &self,
555 input: &Arc<LogicalPlan>,
556 ) -> Result<Option<LogicalPlan>> {
557 if self.extracted.is_empty() {
558 return Ok(None);
559 }
560 let pairs: Vec<(Expr, String)> = self
561 .extracted
562 .iter()
563 .map(|(e, a)| (e.clone(), a.clone()))
564 .collect();
565 let proj = build_extraction_projection_impl(
566 &pairs,
567 &self.columns_needed,
568 input,
569 self.input_schema,
570 )?;
571 Ok(Some(LogicalPlan::Projection(proj)))
572 }
573}
574
575fn build_extraction_projection_impl(
587 extracted_exprs: &[(Expr, String)],
588 columns_needed: &IndexSet<Column>,
589 target: &Arc<LogicalPlan>,
590 target_schema: &DFSchema,
591) -> Result<Projection> {
592 if let LogicalPlan::Projection(existing) = target.as_ref() {
593 let mut proj_exprs = existing.expr.clone();
595
596 let existing_extractions: IndexMap<Expr, String> = existing
598 .expr
599 .iter()
600 .filter_map(|e| {
601 if let Expr::Alias(alias) = e
602 && alias.name.starts_with(EXTRACTED_EXPR_PREFIX)
603 {
604 return Some((*alias.expr.clone(), alias.name.clone()));
605 }
606 None
607 })
608 .collect();
609
610 let replace_map = build_projection_replace_map(existing);
612
613 for (expr, alias) in extracted_exprs {
615 let resolved = replace_cols_by_name(expr.clone().alias(alias), &replace_map)?;
616 let resolved_inner = if let Expr::Alias(a) = &resolved {
617 a.expr.as_ref()
618 } else {
619 &resolved
620 };
621 if let Some(existing_alias) = existing_extractions.get(resolved_inner) {
622 if existing_alias != alias {
628 proj_exprs.push(resolved);
629 }
630 } else {
631 proj_exprs.push(resolved);
632 }
633 }
634
635 let existing_cols: IndexSet<Column> = existing
641 .expr
642 .iter()
643 .filter_map(|e| {
644 if let Expr::Column(c) = e {
645 Some(c.clone())
646 } else {
647 None
648 }
649 })
650 .collect();
651
652 let input_schema = existing.input.schema();
653 for col in columns_needed {
654 let col_expr = Expr::Column(col.clone());
655 let resolved = replace_cols_by_name(col_expr, &replace_map)?;
656 if let Expr::Column(resolved_col) = &resolved
657 && !existing_cols.contains(resolved_col)
658 && input_schema.has_column(resolved_col)
659 {
660 proj_exprs.push(Expr::Column(resolved_col.clone()));
661 }
662 }
664
665 Projection::try_new(proj_exprs, Arc::clone(&existing.input))
666 } else {
667 let mut proj_exprs = Vec::new();
669 for (expr, alias) in extracted_exprs {
670 proj_exprs.push(expr.clone().alias(alias));
671 }
672 for (qualifier, field) in target_schema.iter() {
673 proj_exprs.push(Expr::from((qualifier, field)));
674 }
675 Projection::try_new(proj_exprs, Arc::clone(target))
676 }
677}
678
679#[derive(Default, Debug)]
713pub struct PushDownLeafProjections {}
714
715impl PushDownLeafProjections {
716 pub fn new() -> Self {
717 Self {}
718 }
719}
720
721impl OptimizerRule for PushDownLeafProjections {
722 fn name(&self) -> &str {
723 "push_down_leaf_projections"
724 }
725
726 fn apply_order(&self) -> Option<ApplyOrder> {
727 Some(ApplyOrder::TopDown)
728 }
729
730 fn rewrite(
731 &self,
732 plan: LogicalPlan,
733 config: &dyn OptimizerConfig,
734 ) -> Result<Transformed<LogicalPlan>> {
735 if !config.options().optimizer.enable_leaf_expression_pushdown {
736 return Ok(Transformed::no(plan));
737 }
738 let alias_generator = config.alias_generator();
739 match try_push_input(&plan, alias_generator)? {
740 Some(new_plan) => Ok(Transformed::yes(new_plan)),
741 None => Ok(Transformed::no(plan)),
742 }
743 }
744}
745
746fn try_push_input(
751 input: &LogicalPlan,
752 alias_generator: &Arc<AliasGenerator>,
753) -> Result<Option<LogicalPlan>> {
754 let LogicalPlan::Projection(proj) = input else {
755 return Ok(None);
756 };
757 split_and_push_projection(proj, alias_generator)
758}
759
760fn split_and_push_projection(
792 proj: &Projection,
793 alias_generator: &Arc<AliasGenerator>,
794) -> Result<Option<LogicalPlan>> {
795 let has_existing_extracted = proj.expr.iter().any(|e| {
798 matches!(e, Expr::Alias(alias) if alias.name.starts_with(EXTRACTED_EXPR_PREFIX))
799 });
800 if !has_existing_extracted && !has_extractable_expr(&proj.expr) {
801 return Ok(None);
802 }
803
804 let input = &proj.input;
805 let input_schema = input.schema();
806
807 let mut extractors = vec![LeafExpressionExtractor::new(
823 input_schema.as_ref(),
824 alias_generator,
825 )];
826 let input_column_sets = vec![schema_columns(input_schema.as_ref())];
827
828 let original_schema = proj.schema.as_ref();
829 let mut recovery_exprs: Vec<Expr> = Vec::with_capacity(proj.expr.len());
830 let mut has_new_extractions = false;
831 let mut proj_exprs_captured: usize = 0;
832
833 for (expr, (qualifier, field)) in proj.expr.iter().zip(original_schema.iter()) {
834 if let Expr::Alias(alias) = expr
835 && alias.name.starts_with(EXTRACTED_EXPR_PREFIX)
836 {
837 let alias_name = alias.name.clone();
840
841 for col_ref in alias.expr.column_refs() {
842 extractors[0].columns_needed.insert(col_ref.clone());
843 }
844
845 extractors[0]
846 .extracted
847 .insert(expr.clone(), alias_name.clone());
848 recovery_exprs.push(Expr::Column(Column::new_unqualified(&alias_name)));
849 proj_exprs_captured += 1;
850 } else if let Expr::Column(col) = expr {
851 extractors[0].columns_needed.insert(col.clone());
853 recovery_exprs.push(expr.clone());
854 proj_exprs_captured += 1;
855 } else {
856 let transformed =
858 routing_extract(expr.clone(), &mut extractors, &input_column_sets)?;
859 if transformed.transformed {
860 has_new_extractions = true;
861 }
862 let transformed_expr = transformed.data;
863
864 let original_name = field.name();
866 let needs_alias = if let Expr::Column(col) = &transformed_expr {
867 col.name.as_str() != original_name
868 } else {
869 let expr_name = transformed_expr.schema_name().to_string();
870 original_name != &expr_name
871 };
872 let recovery_expr = if needs_alias {
873 transformed_expr
874 .clone()
875 .alias_qualified(qualifier.cloned(), original_name)
876 } else {
877 transformed_expr.clone()
878 };
879
880 recovery_exprs.push(recovery_expr);
881 }
882 }
883
884 let extractor = &extractors[0];
887 let extraction_pairs: Vec<(Expr, String)> = extractor
888 .extracted
889 .iter()
890 .map(|(e, a)| match e {
891 Expr::Alias(alias) => (*alias.expr.clone(), a.clone()),
892 _ => (e.clone(), a.clone()),
893 })
894 .collect();
895 let columns_needed = &extractor.columns_needed;
896
897 if extraction_pairs.is_empty() {
899 return Ok(None);
900 }
901
902 let proj_input = Arc::clone(&proj.input);
904 let pushed = push_extraction_pairs(
905 &extraction_pairs,
906 columns_needed,
907 proj,
908 &proj_input,
909 alias_generator,
910 proj_exprs_captured,
911 )?;
912
913 let base_plan = match pushed {
916 Some(plan) => plan,
917 None => {
918 if !has_new_extractions {
919 return Ok(None);
924 }
925 let input_arc = Arc::clone(input);
927 let extraction = build_extraction_projection_impl(
928 &extraction_pairs,
929 columns_needed,
930 &input_arc,
931 input_schema.as_ref(),
932 )?;
933 LogicalPlan::Projection(extraction)
934 }
935 };
936
937 let base_names: BTreeSet<&str> = base_plan
956 .schema()
957 .fields()
958 .iter()
959 .map(|f| f.name().as_str())
960 .collect();
961 let original_names: BTreeSet<&str> = original_schema
962 .fields()
963 .iter()
964 .map(|f| f.name().as_str())
965 .collect();
966 let needs_recovery = base_names != original_names;
967
968 if needs_recovery {
970 let recovery = LogicalPlan::Projection(Projection::try_new(
971 recovery_exprs,
972 Arc::new(base_plan),
973 )?);
974 Ok(Some(recovery))
975 } else {
976 Ok(Some(base_plan))
977 }
978}
979
980fn is_pure_extraction_projection(plan: &LogicalPlan) -> bool {
984 let LogicalPlan::Projection(proj) = plan else {
985 return false;
986 };
987 let mut has_extraction = false;
988 for expr in &proj.expr {
989 match expr {
990 Expr::Alias(alias) if alias.name.starts_with(EXTRACTED_EXPR_PREFIX) => {
991 has_extraction = true;
992 }
993 Expr::Column(_) => {}
994 _ => return false,
995 }
996 }
997 has_extraction
998}
999
1000fn push_extraction_pairs(
1003 pairs: &[(Expr, String)],
1004 columns_needed: &IndexSet<Column>,
1005 proj: &Projection,
1006 proj_input: &Arc<LogicalPlan>,
1007 alias_generator: &Arc<AliasGenerator>,
1008 proj_exprs_captured: usize,
1009) -> Result<Option<LogicalPlan>> {
1010 match proj_input.as_ref() {
1011 LogicalPlan::Projection(_) if proj_exprs_captured == proj.expr.len() => {
1018 let target_schema = Arc::clone(proj_input.schema());
1019 let merged = build_extraction_projection_impl(
1020 pairs,
1021 columns_needed,
1022 proj_input,
1023 target_schema.as_ref(),
1024 )?;
1025 let merged_plan = LogicalPlan::Projection(merged);
1026
1027 if is_pure_extraction_projection(&merged_plan)
1036 && let Some(pushed) = try_push_input(&merged_plan, alias_generator)?
1037 {
1038 return Ok(Some(pushed));
1039 }
1040 Ok(Some(merged_plan))
1041 }
1042 _ => try_push_into_inputs(
1048 pairs,
1049 columns_needed,
1050 proj_input.as_ref(),
1051 alias_generator,
1052 ),
1053 }
1054}
1055
1056fn route_to_inputs(
1064 pairs: &[(Expr, String)],
1065 columns: &IndexSet<Column>,
1066 node: &LogicalPlan,
1067 input_column_sets: &[std::collections::HashSet<ColumnReference>],
1068 input_schemas: &[Arc<DFSchema>],
1069) -> Result<Option<Vec<ExtractionTarget>>> {
1070 let num_inputs = input_schemas.len();
1071 let mut per_input: Vec<ExtractionTarget> = (0..num_inputs)
1072 .map(|_| ExtractionTarget {
1073 pairs: vec![],
1074 columns: IndexSet::new(),
1075 })
1076 .collect();
1077
1078 if matches!(node, LogicalPlan::Union(_)) {
1079 let union_schema = node.schema();
1083 for (idx, input_schema) in input_schemas.iter().enumerate() {
1084 per_input[idx] =
1085 remap_pairs_and_columns(pairs, columns, union_schema, input_schema)?;
1086 }
1087 } else {
1088 for (expr, alias) in pairs {
1089 match find_owning_input(expr, input_column_sets) {
1090 Some(idx) => per_input[idx].pairs.push((expr.clone(), alias.clone())),
1091 None => return Ok(None), }
1093 }
1094 for col in columns {
1095 let col_expr = Expr::Column(col.clone());
1096 match find_owning_input(&col_expr, input_column_sets) {
1097 Some(idx) => {
1098 per_input[idx].columns.insert(col.clone());
1099 }
1100 None => return Ok(None), }
1102 }
1103 }
1104
1105 if per_input.iter().all(|t| t.pairs.is_empty()) {
1107 return Ok(None);
1108 }
1109
1110 Ok(Some(per_input))
1111}
1112
1113fn try_push_into_inputs(
1142 pairs: &[(Expr, String)],
1143 columns_needed: &IndexSet<Column>,
1144 node: &LogicalPlan,
1145 alias_generator: &Arc<AliasGenerator>,
1146) -> Result<Option<LogicalPlan>> {
1147 let inputs = node.inputs();
1148 if inputs.is_empty() {
1149 return Ok(None);
1150 }
1151
1152 if matches!(node, LogicalPlan::Unnest(_)) {
1155 return Ok(None);
1156 }
1157
1158 let remapped = if let LogicalPlan::SubqueryAlias(sa) = node {
1161 remap_pairs_and_columns(pairs, columns_needed, &sa.schema, sa.input.schema())?
1162 } else {
1163 ExtractionTarget {
1164 pairs: pairs.to_vec(),
1165 columns: columns_needed.clone(),
1166 }
1167 };
1168 let pairs = &remapped.pairs[..];
1169 let columns_needed = &remapped.columns;
1170
1171 let input_schemas: Vec<Arc<DFSchema>> =
1173 inputs.iter().map(|i| Arc::clone(i.schema())).collect();
1174 let input_column_sets: Vec<std::collections::HashSet<ColumnReference>> =
1175 input_schemas.iter().map(|s| schema_columns(s)).collect();
1176
1177 let per_input = match route_to_inputs(
1179 pairs,
1180 columns_needed,
1181 node,
1182 &input_column_sets,
1183 &input_schemas,
1184 )? {
1185 Some(routed) => routed,
1186 None => return Ok(None),
1187 };
1188
1189 let num_inputs = inputs.len();
1190
1191 let mut new_inputs: Vec<LogicalPlan> = Vec::with_capacity(num_inputs);
1196 for (idx, input) in inputs.into_iter().enumerate() {
1197 if per_input[idx].pairs.is_empty() {
1198 new_inputs.push(input.clone());
1199 } else {
1200 let input_arc = Arc::new(input.clone());
1201 let target_schema = Arc::clone(input.schema());
1202 let proj = build_extraction_projection_impl(
1203 &per_input[idx].pairs,
1204 &per_input[idx].columns,
1205 &input_arc,
1206 target_schema.as_ref(),
1207 )?;
1208 let proj_schema = proj.schema.as_ref();
1212 for (_expr, alias) in &per_input[idx].pairs {
1213 if !proj_schema.fields().iter().any(|f| f.name() == alias) {
1214 return Ok(None);
1215 }
1216 }
1217 let proj_plan = LogicalPlan::Projection(proj);
1218 match try_push_input(&proj_plan, alias_generator)? {
1223 Some(pushed) => new_inputs.push(pushed),
1224 None => new_inputs.push(proj_plan),
1225 }
1226 }
1227 }
1228
1229 let new_node = node.with_new_exprs(node.expressions(), new_inputs)?;
1231
1232 let output_schema = new_node.schema();
1236 for (_expr, alias) in pairs {
1237 if !output_schema.fields().iter().any(|f| f.name() == alias) {
1238 return Ok(None);
1239 }
1240 }
1241
1242 Ok(Some(new_node))
1243}
1244
1245#[cfg(test)]
1246mod tests {
1247
1248 use super::*;
1249 use crate::optimize_projections::OptimizeProjections;
1250 use crate::test::udfs::PlacementTestUDF;
1251 use crate::test::*;
1252 use crate::{Optimizer, OptimizerContext};
1253 use datafusion_expr::expr::ScalarFunction;
1254 use datafusion_expr::{
1255 ScalarUDF, col, lit, logical_plan::builder::LogicalPlanBuilder,
1256 };
1257
1258 fn leaf_udf(expr: Expr, name: &str) -> Expr {
1259 Expr::ScalarFunction(ScalarFunction::new_udf(
1260 Arc::new(ScalarUDF::new_from_impl(
1261 PlacementTestUDF::new()
1262 .with_placement(ExpressionPlacement::MoveTowardsLeafNodes),
1263 )),
1264 vec![expr, lit(name)],
1265 ))
1266 }
1267
1268 fn format_optimization_stages(plan: &LogicalPlan) -> Result<String> {
1282 let run = |rules: Vec<Arc<dyn OptimizerRule + Send + Sync>>| -> Result<String> {
1283 let ctx = OptimizerContext::new().with_max_passes(1);
1284 let optimizer = Optimizer::with_rules(rules);
1285 let optimized = optimizer.optimize(plan.clone(), &ctx, |_, _| {})?;
1286 Ok(format!("{optimized}"))
1287 };
1288
1289 let original = run(vec![Arc::new(OptimizeProjections::new())])?;
1290
1291 let after_extract = run(vec![
1292 Arc::new(OptimizeProjections::new()),
1293 Arc::new(ExtractLeafExpressions::new()),
1294 ])?;
1295
1296 let after_pushdown = run(vec![
1297 Arc::new(OptimizeProjections::new()),
1298 Arc::new(ExtractLeafExpressions::new()),
1299 Arc::new(PushDownLeafProjections::new()),
1300 ])?;
1301
1302 let optimized = run(vec![
1303 Arc::new(OptimizeProjections::new()),
1304 Arc::new(ExtractLeafExpressions::new()),
1305 Arc::new(PushDownLeafProjections::new()),
1306 Arc::new(OptimizeProjections::new()),
1307 ])?;
1308
1309 let mut out = format!("## Original Plan\n{original}");
1310
1311 out.push_str("\n\n## After Extraction\n");
1312 if after_extract == original {
1313 out.push_str("(same as original)");
1314 } else {
1315 out.push_str(&after_extract);
1316 }
1317
1318 out.push_str("\n\n## After Pushdown\n");
1319 if after_pushdown == after_extract {
1320 out.push_str("(same as after extraction)");
1321 } else {
1322 out.push_str(&after_pushdown);
1323 }
1324
1325 out.push_str("\n\n## Optimized\n");
1326 if optimized == after_pushdown {
1327 out.push_str("(same as after pushdown)");
1328 } else {
1329 out.push_str(&optimized);
1330 }
1331
1332 Ok(out)
1333 }
1334
1335 macro_rules! assert_stages {
1337 ($plan:expr, @ $expected:literal $(,)?) => {{
1338 let result = format_optimization_stages(&$plan)?;
1339 insta::assert_snapshot!(result, @ $expected);
1340 Ok::<(), datafusion_common::DataFusionError>(())
1341 }};
1342 }
1343
1344 #[test]
1345 fn test_extract_from_filter() -> Result<()> {
1346 let table_scan = test_table_scan_with_struct()?;
1347 let plan = LogicalPlanBuilder::from(table_scan.clone())
1348 .filter(leaf_udf(col("user"), "status").eq(lit("active")))?
1349 .select(vec![
1350 table_scan
1351 .schema()
1352 .index_of_column_by_name(None, "id")
1353 .unwrap(),
1354 ])?
1355 .build()?;
1356
1357 assert_stages!(plan, @r#"
1358 ## Original Plan
1359 Projection: test.id
1360 Filter: leaf_udf(test.user, Utf8("status")) = Utf8("active")
1361 TableScan: test projection=[id, user]
1362
1363 ## After Extraction
1364 Projection: test.id
1365 Projection: test.id, test.user
1366 Filter: __datafusion_extracted_1 = Utf8("active")
1367 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
1368 TableScan: test projection=[id, user]
1369
1370 ## After Pushdown
1371 (same as after extraction)
1372
1373 ## Optimized
1374 Projection: test.id
1375 Filter: __datafusion_extracted_1 = Utf8("active")
1376 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id
1377 TableScan: test projection=[id, user]
1378 "#)
1379 }
1380
1381 #[test]
1382 fn test_no_extraction_for_column() -> Result<()> {
1383 let table_scan = test_table_scan()?;
1384 let plan = LogicalPlanBuilder::from(table_scan)
1385 .filter(col("a").eq(lit(1)))?
1386 .build()?;
1387
1388 assert_stages!(plan, @"
1389 ## Original Plan
1390 Filter: test.a = Int32(1)
1391 TableScan: test projection=[a, b, c]
1392
1393 ## After Extraction
1394 (same as original)
1395
1396 ## After Pushdown
1397 (same as after extraction)
1398
1399 ## Optimized
1400 (same as after pushdown)
1401 ")
1402 }
1403
1404 #[test]
1405 fn test_extract_from_projection() -> Result<()> {
1406 let table_scan = test_table_scan_with_struct()?;
1407 let plan = LogicalPlanBuilder::from(table_scan)
1408 .project(vec![leaf_udf(col("user"), "name")])?
1409 .build()?;
1410
1411 assert_stages!(plan, @r#"
1412 ## Original Plan
1413 Projection: leaf_udf(test.user, Utf8("name"))
1414 TableScan: test projection=[user]
1415
1416 ## After Extraction
1417 (same as original)
1418
1419 ## After Pushdown
1420 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name"))
1421 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1422 TableScan: test projection=[user]
1423
1424 ## Optimized
1425 Projection: leaf_udf(test.user, Utf8("name"))
1426 TableScan: test projection=[user]
1427 "#)
1428 }
1429
1430 #[test]
1431 fn test_extract_from_projection_with_subexpression() -> Result<()> {
1432 let table_scan = test_table_scan_with_struct()?;
1433 let plan = LogicalPlanBuilder::from(table_scan)
1434 .project(vec![
1435 leaf_udf(col("user"), "name")
1436 .is_not_null()
1437 .alias("has_name"),
1438 ])?
1439 .build()?;
1440
1441 assert_stages!(plan, @r#"
1442 ## Original Plan
1443 Projection: leaf_udf(test.user, Utf8("name")) IS NOT NULL AS has_name
1444 TableScan: test projection=[user]
1445
1446 ## After Extraction
1447 (same as original)
1448
1449 ## After Pushdown
1450 Projection: __datafusion_extracted_1 IS NOT NULL AS has_name
1451 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1452 TableScan: test projection=[user]
1453
1454 ## Optimized
1455 Projection: leaf_udf(test.user, Utf8("name")) IS NOT NULL AS has_name
1456 TableScan: test projection=[user]
1457 "#)
1458 }
1459
1460 #[test]
1461 fn test_projection_no_extraction_for_column() -> Result<()> {
1462 let table_scan = test_table_scan()?;
1463 let plan = LogicalPlanBuilder::from(table_scan)
1464 .project(vec![col("a"), col("b")])?
1465 .build()?;
1466
1467 assert_stages!(plan, @"
1468 ## Original Plan
1469 TableScan: test projection=[a, b]
1470
1471 ## After Extraction
1472 (same as original)
1473
1474 ## After Pushdown
1475 (same as after extraction)
1476
1477 ## Optimized
1478 (same as after pushdown)
1479 ")
1480 }
1481
1482 #[test]
1483 fn test_filter_with_deduplication() -> Result<()> {
1484 let table_scan = test_table_scan_with_struct()?;
1485 let field_access = leaf_udf(col("user"), "name");
1486 let plan = LogicalPlanBuilder::from(table_scan)
1488 .filter(
1489 field_access
1490 .clone()
1491 .is_not_null()
1492 .and(field_access.is_null()),
1493 )?
1494 .build()?;
1495
1496 assert_stages!(plan, @r#"
1497 ## Original Plan
1498 Filter: leaf_udf(test.user, Utf8("name")) IS NOT NULL AND leaf_udf(test.user, Utf8("name")) IS NULL
1499 TableScan: test projection=[id, user]
1500
1501 ## After Extraction
1502 Projection: test.id, test.user
1503 Filter: __datafusion_extracted_1 IS NOT NULL AND __datafusion_extracted_1 IS NULL
1504 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.id, test.user
1505 TableScan: test projection=[id, user]
1506
1507 ## After Pushdown
1508 (same as after extraction)
1509
1510 ## Optimized
1511 (same as after pushdown)
1512 "#)
1513 }
1514
1515 #[test]
1516 fn test_already_leaf_expression_in_filter() -> Result<()> {
1517 let table_scan = test_table_scan_with_struct()?;
1518 let plan = LogicalPlanBuilder::from(table_scan)
1519 .filter(leaf_udf(col("user"), "name").eq(lit("test")))?
1520 .build()?;
1521
1522 assert_stages!(plan, @r#"
1523 ## Original Plan
1524 Filter: leaf_udf(test.user, Utf8("name")) = Utf8("test")
1525 TableScan: test projection=[id, user]
1526
1527 ## After Extraction
1528 Projection: test.id, test.user
1529 Filter: __datafusion_extracted_1 = Utf8("test")
1530 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.id, test.user
1531 TableScan: test projection=[id, user]
1532
1533 ## After Pushdown
1534 (same as after extraction)
1535
1536 ## Optimized
1537 (same as after pushdown)
1538 "#)
1539 }
1540
1541 #[test]
1542 fn test_extract_from_aggregate_group_by() -> Result<()> {
1543 use datafusion_expr::test::function_stub::count;
1544
1545 let table_scan = test_table_scan_with_struct()?;
1546 let plan = LogicalPlanBuilder::from(table_scan)
1547 .aggregate(vec![leaf_udf(col("user"), "status")], vec![count(lit(1))])?
1548 .build()?;
1549
1550 assert_stages!(plan, @r#"
1551 ## Original Plan
1552 Aggregate: groupBy=[[leaf_udf(test.user, Utf8("status"))]], aggr=[[COUNT(Int32(1))]]
1553 TableScan: test projection=[user]
1554
1555 ## After Extraction
1556 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("status")), COUNT(Int32(1))
1557 Aggregate: groupBy=[[__datafusion_extracted_1]], aggr=[[COUNT(Int32(1))]]
1558 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.user
1559 TableScan: test projection=[user]
1560
1561 ## After Pushdown
1562 (same as after extraction)
1563
1564 ## Optimized
1565 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("status")), COUNT(Int32(1))
1566 Aggregate: groupBy=[[__datafusion_extracted_1]], aggr=[[COUNT(Int32(1))]]
1567 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1
1568 TableScan: test projection=[user]
1569 "#)
1570 }
1571
1572 #[test]
1573 fn test_extract_from_aggregate_args() -> Result<()> {
1574 use datafusion_expr::test::function_stub::count;
1575
1576 let table_scan = test_table_scan_with_struct()?;
1577 let plan = LogicalPlanBuilder::from(table_scan)
1578 .aggregate(
1579 vec![col("user")],
1580 vec![count(leaf_udf(col("user"), "value"))],
1581 )?
1582 .build()?;
1583
1584 assert_stages!(plan, @r#"
1585 ## Original Plan
1586 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(leaf_udf(test.user, Utf8("value")))]]
1587 TableScan: test projection=[user]
1588
1589 ## After Extraction
1590 Projection: test.user, COUNT(__datafusion_extracted_1) AS COUNT(leaf_udf(test.user,Utf8("value")))
1591 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(__datafusion_extracted_1)]]
1592 Projection: leaf_udf(test.user, Utf8("value")) AS __datafusion_extracted_1, test.user
1593 TableScan: test projection=[user]
1594
1595 ## After Pushdown
1596 (same as after extraction)
1597
1598 ## Optimized
1599 (same as after pushdown)
1600 "#)
1601 }
1602
1603 #[test]
1604 fn test_projection_with_filter_combined() -> Result<()> {
1605 let table_scan = test_table_scan_with_struct()?;
1606 let plan = LogicalPlanBuilder::from(table_scan)
1607 .filter(leaf_udf(col("user"), "status").eq(lit("active")))?
1608 .project(vec![leaf_udf(col("user"), "name")])?
1609 .build()?;
1610
1611 assert_stages!(plan, @r#"
1612 ## Original Plan
1613 Projection: leaf_udf(test.user, Utf8("name"))
1614 Filter: leaf_udf(test.user, Utf8("status")) = Utf8("active")
1615 TableScan: test projection=[user]
1616
1617 ## After Extraction
1618 Projection: leaf_udf(test.user, Utf8("name"))
1619 Projection: test.user
1620 Filter: __datafusion_extracted_1 = Utf8("active")
1621 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.user
1622 TableScan: test projection=[user]
1623
1624 ## After Pushdown
1625 Projection: __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name"))
1626 Projection: test.user, __datafusion_extracted_2
1627 Filter: __datafusion_extracted_1 = Utf8("active")
1628 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.user, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2
1629 TableScan: test projection=[user]
1630
1631 ## Optimized
1632 Projection: __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name"))
1633 Filter: __datafusion_extracted_1 = Utf8("active")
1634 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2
1635 TableScan: test projection=[user]
1636 "#)
1637 }
1638
1639 #[test]
1640 fn test_projection_preserves_alias() -> Result<()> {
1641 let table_scan = test_table_scan_with_struct()?;
1642 let plan = LogicalPlanBuilder::from(table_scan)
1643 .project(vec![leaf_udf(col("user"), "name").alias("username")])?
1644 .build()?;
1645
1646 assert_stages!(plan, @r#"
1647 ## Original Plan
1648 Projection: leaf_udf(test.user, Utf8("name")) AS username
1649 TableScan: test projection=[user]
1650
1651 ## After Extraction
1652 (same as original)
1653
1654 ## After Pushdown
1655 Projection: __datafusion_extracted_1 AS username
1656 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1657 TableScan: test projection=[user]
1658
1659 ## Optimized
1660 Projection: leaf_udf(test.user, Utf8("name")) AS username
1661 TableScan: test projection=[user]
1662 "#)
1663 }
1664
1665 #[test]
1669 fn test_projection_different_field_from_filter() -> Result<()> {
1670 let table_scan = test_table_scan_with_struct()?;
1671 let plan = LogicalPlanBuilder::from(table_scan)
1672 .filter(leaf_udf(col("user"), "value").gt(lit(150)))?
1673 .project(vec![col("user"), leaf_udf(col("user"), "label")])?
1674 .build()?;
1675
1676 assert_stages!(plan, @r#"
1677 ## Original Plan
1678 Projection: test.user, leaf_udf(test.user, Utf8("label"))
1679 Filter: leaf_udf(test.user, Utf8("value")) > Int32(150)
1680 TableScan: test projection=[user]
1681
1682 ## After Extraction
1683 Projection: test.user, leaf_udf(test.user, Utf8("label"))
1684 Projection: test.user
1685 Filter: __datafusion_extracted_1 > Int32(150)
1686 Projection: leaf_udf(test.user, Utf8("value")) AS __datafusion_extracted_1, test.user
1687 TableScan: test projection=[user]
1688
1689 ## After Pushdown
1690 Projection: test.user, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("label"))
1691 Projection: test.user, __datafusion_extracted_2
1692 Filter: __datafusion_extracted_1 > Int32(150)
1693 Projection: leaf_udf(test.user, Utf8("value")) AS __datafusion_extracted_1, test.user, leaf_udf(test.user, Utf8("label")) AS __datafusion_extracted_2
1694 TableScan: test projection=[user]
1695
1696 ## Optimized
1697 Projection: test.user, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("label"))
1698 Filter: __datafusion_extracted_1 > Int32(150)
1699 Projection: leaf_udf(test.user, Utf8("value")) AS __datafusion_extracted_1, test.user, leaf_udf(test.user, Utf8("label")) AS __datafusion_extracted_2
1700 TableScan: test projection=[user]
1701 "#)
1702 }
1703
1704 #[test]
1705 fn test_projection_deduplication() -> Result<()> {
1706 let table_scan = test_table_scan_with_struct()?;
1707 let field = leaf_udf(col("user"), "name");
1708 let plan = LogicalPlanBuilder::from(table_scan)
1709 .project(vec![field.clone(), field.clone().alias("name2")])?
1710 .build()?;
1711
1712 assert_stages!(plan, @r#"
1713 ## Original Plan
1714 Projection: leaf_udf(test.user, Utf8("name")), leaf_udf(test.user, Utf8("name")) AS name2
1715 TableScan: test projection=[user]
1716
1717 ## After Extraction
1718 (same as original)
1719
1720 ## After Pushdown
1721 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name")), __datafusion_extracted_1 AS name2
1722 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1723 TableScan: test projection=[user]
1724
1725 ## Optimized
1726 Projection: leaf_udf(test.user, Utf8("name")), leaf_udf(test.user, Utf8("name")) AS name2
1727 TableScan: test projection=[user]
1728 "#)
1729 }
1730
1731 #[test]
1737 fn test_extract_through_sort() -> Result<()> {
1738 let table_scan = test_table_scan_with_struct()?;
1739 let plan = LogicalPlanBuilder::from(table_scan)
1740 .sort(vec![col("user").sort(true, true)])?
1741 .project(vec![leaf_udf(col("user"), "name")])?
1742 .build()?;
1743
1744 assert_stages!(plan, @r#"
1745 ## Original Plan
1746 Projection: leaf_udf(test.user, Utf8("name"))
1747 Sort: test.user ASC NULLS FIRST
1748 TableScan: test projection=[user]
1749
1750 ## After Extraction
1751 (same as original)
1752
1753 ## After Pushdown
1754 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name"))
1755 Sort: test.user ASC NULLS FIRST
1756 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1757 TableScan: test projection=[user]
1758
1759 ## Optimized
1760 (same as after pushdown)
1761 "#)
1762 }
1763
1764 #[test]
1766 fn test_extract_through_limit() -> Result<()> {
1767 let table_scan = test_table_scan_with_struct()?;
1768 let plan = LogicalPlanBuilder::from(table_scan)
1769 .limit(0, Some(10))?
1770 .project(vec![leaf_udf(col("user"), "name")])?
1771 .build()?;
1772
1773 assert_stages!(plan, @r#"
1774 ## Original Plan
1775 Projection: leaf_udf(test.user, Utf8("name"))
1776 Limit: skip=0, fetch=10
1777 TableScan: test projection=[user]
1778
1779 ## After Extraction
1780 (same as original)
1781
1782 ## After Pushdown
1783 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name"))
1784 Limit: skip=0, fetch=10
1785 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1786 TableScan: test projection=[user]
1787
1788 ## Optimized
1789 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name"))
1790 Limit: skip=0, fetch=10
1791 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1
1792 TableScan: test projection=[user]
1793 "#)
1794 }
1795
1796 #[test]
1798 fn test_extract_from_aliased_aggregate() -> Result<()> {
1799 use datafusion_expr::test::function_stub::count;
1800
1801 let table_scan = test_table_scan_with_struct()?;
1802 let plan = LogicalPlanBuilder::from(table_scan)
1803 .aggregate(
1804 vec![col("user")],
1805 vec![count(leaf_udf(col("user"), "value")).alias("cnt")],
1806 )?
1807 .build()?;
1808
1809 assert_stages!(plan, @r#"
1810 ## Original Plan
1811 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(leaf_udf(test.user, Utf8("value"))) AS cnt]]
1812 TableScan: test projection=[user]
1813
1814 ## After Extraction
1815 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(__datafusion_extracted_1) AS cnt]]
1816 Projection: leaf_udf(test.user, Utf8("value")) AS __datafusion_extracted_1, test.user
1817 TableScan: test projection=[user]
1818
1819 ## After Pushdown
1820 (same as after extraction)
1821
1822 ## Optimized
1823 (same as after pushdown)
1824 "#)
1825 }
1826
1827 #[test]
1829 fn test_aggregate_no_extraction() -> Result<()> {
1830 use datafusion_expr::test::function_stub::count;
1831
1832 let table_scan = test_table_scan()?;
1833 let plan = LogicalPlanBuilder::from(table_scan)
1834 .aggregate(vec![col("a")], vec![count(col("b"))])?
1835 .build()?;
1836
1837 assert_stages!(plan, @"
1838 ## Original Plan
1839 Aggregate: groupBy=[[test.a]], aggr=[[COUNT(test.b)]]
1840 TableScan: test projection=[a, b]
1841
1842 ## After Extraction
1843 (same as original)
1844
1845 ## After Pushdown
1846 (same as after extraction)
1847
1848 ## Optimized
1849 (same as after pushdown)
1850 ")
1851 }
1852
1853 #[test]
1855 fn test_skip_extracted_projection() -> Result<()> {
1856 let table_scan = test_table_scan_with_struct()?;
1857 let plan = LogicalPlanBuilder::from(table_scan)
1858 .project(vec![
1859 leaf_udf(col("user"), "name").alias("__datafusion_extracted_manual"),
1860 col("user"),
1861 ])?
1862 .build()?;
1863
1864 assert_stages!(plan, @r#"
1865 ## Original Plan
1866 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_manual, test.user
1867 TableScan: test projection=[user]
1868
1869 ## After Extraction
1870 (same as original)
1871
1872 ## After Pushdown
1873 (same as after extraction)
1874
1875 ## Optimized
1876 (same as after pushdown)
1877 "#)
1878 }
1879
1880 #[test]
1882 fn test_merge_into_existing_extracted_projection() -> Result<()> {
1883 let table_scan = test_table_scan_with_struct()?;
1884 let plan = LogicalPlanBuilder::from(table_scan)
1885 .filter(leaf_udf(col("user"), "status").eq(lit("active")))?
1886 .filter(leaf_udf(col("user"), "name").is_not_null())?
1887 .build()?;
1888
1889 assert_stages!(plan, @r#"
1890 ## Original Plan
1891 Filter: leaf_udf(test.user, Utf8("name")) IS NOT NULL
1892 Filter: leaf_udf(test.user, Utf8("status")) = Utf8("active")
1893 TableScan: test projection=[id, user]
1894
1895 ## After Extraction
1896 Projection: test.id, test.user
1897 Filter: __datafusion_extracted_1 IS NOT NULL
1898 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.id, test.user
1899 Projection: test.id, test.user
1900 Filter: __datafusion_extracted_2 = Utf8("active")
1901 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2, test.id, test.user
1902 TableScan: test projection=[id, user]
1903
1904 ## After Pushdown
1905 Projection: test.id, test.user
1906 Filter: __datafusion_extracted_1 IS NOT NULL
1907 Projection: test.id, test.user, __datafusion_extracted_1
1908 Filter: __datafusion_extracted_2 = Utf8("active")
1909 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2, test.id, test.user, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1
1910 TableScan: test projection=[id, user]
1911
1912 ## Optimized
1913 (same as after pushdown)
1914 "#)
1915 }
1916
1917 #[test]
1919 fn test_extract_through_passthrough_projection() -> Result<()> {
1920 let table_scan = test_table_scan_with_struct()?;
1921 let plan = LogicalPlanBuilder::from(table_scan)
1922 .project(vec![col("user")])?
1923 .project(vec![leaf_udf(col("user"), "name")])?
1924 .build()?;
1925
1926 assert_stages!(plan, @r#"
1927 ## Original Plan
1928 Projection: leaf_udf(test.user, Utf8("name"))
1929 TableScan: test projection=[user]
1930
1931 ## After Extraction
1932 (same as original)
1933
1934 ## After Pushdown
1935 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name"))
1936 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
1937 TableScan: test projection=[user]
1938
1939 ## Optimized
1940 Projection: leaf_udf(test.user, Utf8("name"))
1941 TableScan: test projection=[user]
1942 "#)
1943 }
1944
1945 #[test]
1947 fn test_projection_early_return_no_extraction() -> Result<()> {
1948 let table_scan = test_table_scan()?;
1949 let plan = LogicalPlanBuilder::from(table_scan)
1950 .project(vec![col("a").alias("x"), col("b")])?
1951 .build()?;
1952
1953 assert_stages!(plan, @"
1954 ## Original Plan
1955 Projection: test.a AS x, test.b
1956 TableScan: test projection=[a, b]
1957
1958 ## After Extraction
1959 (same as original)
1960
1961 ## After Pushdown
1962 (same as after extraction)
1963
1964 ## Optimized
1965 (same as after pushdown)
1966 ")
1967 }
1968
1969 #[test]
1971 fn test_projection_with_arithmetic_no_extraction() -> Result<()> {
1972 let table_scan = test_table_scan()?;
1973 let plan = LogicalPlanBuilder::from(table_scan)
1974 .project(vec![(col("a") + col("b")).alias("sum")])?
1975 .build()?;
1976
1977 assert_stages!(plan, @"
1978 ## Original Plan
1979 Projection: test.a + test.b AS sum
1980 TableScan: test projection=[a, b]
1981
1982 ## After Extraction
1983 (same as original)
1984
1985 ## After Pushdown
1986 (same as after extraction)
1987
1988 ## Optimized
1989 (same as after pushdown)
1990 ")
1991 }
1992
1993 #[test]
1995 fn test_aggregate_merge_into_extracted_projection() -> Result<()> {
1996 use datafusion_expr::test::function_stub::count;
1997
1998 let table_scan = test_table_scan_with_struct()?;
1999 let plan = LogicalPlanBuilder::from(table_scan)
2000 .filter(leaf_udf(col("user"), "status").eq(lit("active")))?
2001 .aggregate(vec![leaf_udf(col("user"), "name")], vec![count(lit(1))])?
2002 .build()?;
2003
2004 assert_stages!(plan, @r#"
2005 ## Original Plan
2006 Aggregate: groupBy=[[leaf_udf(test.user, Utf8("name"))]], aggr=[[COUNT(Int32(1))]]
2007 Filter: leaf_udf(test.user, Utf8("status")) = Utf8("active")
2008 TableScan: test projection=[user]
2009
2010 ## After Extraction
2011 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name")), COUNT(Int32(1))
2012 Aggregate: groupBy=[[__datafusion_extracted_1]], aggr=[[COUNT(Int32(1))]]
2013 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
2014 Projection: test.user
2015 Filter: __datafusion_extracted_2 = Utf8("active")
2016 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2, test.user
2017 TableScan: test projection=[user]
2018
2019 ## After Pushdown
2020 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name")), COUNT(Int32(1))
2021 Aggregate: groupBy=[[__datafusion_extracted_1]], aggr=[[COUNT(Int32(1))]]
2022 Projection: test.user, __datafusion_extracted_1
2023 Filter: __datafusion_extracted_2 = Utf8("active")
2024 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2, test.user, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1
2025 TableScan: test projection=[user]
2026
2027 ## Optimized
2028 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("name")), COUNT(Int32(1))
2029 Aggregate: groupBy=[[__datafusion_extracted_1]], aggr=[[COUNT(Int32(1))]]
2030 Projection: __datafusion_extracted_1
2031 Filter: __datafusion_extracted_2 = Utf8("active")
2032 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1
2033 TableScan: test projection=[user]
2034 "#)
2035 }
2036
2037 #[test]
2041 fn test_projection_with_leaf_expr_above_aggregate() -> Result<()> {
2042 use datafusion_expr::test::function_stub::count;
2043
2044 let table_scan = test_table_scan_with_struct()?;
2045 let plan = LogicalPlanBuilder::from(table_scan)
2046 .aggregate(vec![col("user")], vec![count(lit(1))])?
2047 .project(vec![
2048 leaf_udf(col("user"), "name")
2049 .is_not_null()
2050 .alias("has_name"),
2051 col("COUNT(Int32(1))"),
2052 ])?
2053 .build()?;
2054
2055 assert_stages!(plan, @r#"
2056 ## Original Plan
2057 Projection: leaf_udf(test.user, Utf8("name")) IS NOT NULL AS has_name, COUNT(Int32(1))
2058 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(Int32(1))]]
2059 TableScan: test projection=[user]
2060
2061 ## After Extraction
2062 (same as original)
2063
2064 ## After Pushdown
2065 Projection: __datafusion_extracted_1 IS NOT NULL AS has_name, COUNT(Int32(1))
2066 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user, COUNT(Int32(1))
2067 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(Int32(1))]]
2068 TableScan: test projection=[user]
2069
2070 ## Optimized
2071 Projection: leaf_udf(test.user, Utf8("name")) IS NOT NULL AS has_name, COUNT(Int32(1))
2072 Aggregate: groupBy=[[test.user]], aggr=[[COUNT(Int32(1))]]
2073 TableScan: test projection=[user]
2074 "#)
2075 }
2076
2077 #[test]
2079 fn test_merge_with_new_columns() -> Result<()> {
2080 let table_scan = test_table_scan()?;
2081 let plan = LogicalPlanBuilder::from(table_scan)
2082 .filter(leaf_udf(col("a"), "x").eq(lit(1)))?
2083 .filter(leaf_udf(col("b"), "y").eq(lit(2)))?
2084 .build()?;
2085
2086 assert_stages!(plan, @r#"
2087 ## Original Plan
2088 Filter: leaf_udf(test.b, Utf8("y")) = Int32(2)
2089 Filter: leaf_udf(test.a, Utf8("x")) = Int32(1)
2090 TableScan: test projection=[a, b, c]
2091
2092 ## After Extraction
2093 Projection: test.a, test.b, test.c
2094 Filter: __datafusion_extracted_1 = Int32(2)
2095 Projection: leaf_udf(test.b, Utf8("y")) AS __datafusion_extracted_1, test.a, test.b, test.c
2096 Projection: test.a, test.b, test.c
2097 Filter: __datafusion_extracted_2 = Int32(1)
2098 Projection: leaf_udf(test.a, Utf8("x")) AS __datafusion_extracted_2, test.a, test.b, test.c
2099 TableScan: test projection=[a, b, c]
2100
2101 ## After Pushdown
2102 Projection: test.a, test.b, test.c
2103 Filter: __datafusion_extracted_1 = Int32(2)
2104 Projection: test.a, test.b, test.c, __datafusion_extracted_1
2105 Filter: __datafusion_extracted_2 = Int32(1)
2106 Projection: leaf_udf(test.a, Utf8("x")) AS __datafusion_extracted_2, test.a, test.b, test.c, leaf_udf(test.b, Utf8("y")) AS __datafusion_extracted_1
2107 TableScan: test projection=[a, b, c]
2108
2109 ## Optimized
2110 (same as after pushdown)
2111 "#)
2112 }
2113
2114 fn test_table_scan_with_struct_named(name: &str) -> Result<LogicalPlan> {
2120 use arrow::datatypes::Schema;
2121 let schema = Schema::new(test_table_scan_with_struct_fields());
2122 datafusion_expr::logical_plan::table_scan(Some(name), &schema, None)?.build()
2123 }
2124
2125 #[test]
2127 fn test_extract_from_join_on() -> Result<()> {
2128 use datafusion_expr::JoinType;
2129
2130 let left = test_table_scan_with_struct()?;
2131 let right = test_table_scan_with_struct_named("right")?;
2132
2133 let plan = LogicalPlanBuilder::from(left)
2134 .join_with_expr_keys(
2135 right,
2136 JoinType::Inner,
2137 (
2138 vec![leaf_udf(col("user"), "id")],
2139 vec![leaf_udf(col("user"), "id")],
2140 ),
2141 None,
2142 )?
2143 .build()?;
2144
2145 assert_stages!(plan, @r#"
2146 ## Original Plan
2147 Inner Join: leaf_udf(test.user, Utf8("id")) = leaf_udf(right.user, Utf8("id"))
2148 TableScan: test projection=[id, user]
2149 TableScan: right projection=[id, user]
2150
2151 ## After Extraction
2152 Projection: test.id, test.user, right.id, right.user
2153 Inner Join: __datafusion_extracted_1 = __datafusion_extracted_2
2154 Projection: leaf_udf(test.user, Utf8("id")) AS __datafusion_extracted_1, test.id, test.user
2155 TableScan: test projection=[id, user]
2156 Projection: leaf_udf(right.user, Utf8("id")) AS __datafusion_extracted_2, right.id, right.user
2157 TableScan: right projection=[id, user]
2158
2159 ## After Pushdown
2160 (same as after extraction)
2161
2162 ## Optimized
2163 (same as after pushdown)
2164 "#)
2165 }
2166
2167 #[test]
2169 fn test_extract_from_join_filter() -> Result<()> {
2170 use datafusion_expr::JoinType;
2171
2172 let left = test_table_scan_with_struct()?;
2173 let right = test_table_scan_with_struct_named("right")?;
2174
2175 let plan = LogicalPlanBuilder::from(left)
2176 .join_on(
2177 right,
2178 JoinType::Inner,
2179 vec![
2180 col("test.user").eq(col("right.user")),
2181 leaf_udf(col("test.user"), "status").eq(lit("active")),
2182 ],
2183 )?
2184 .build()?;
2185
2186 assert_stages!(plan, @r#"
2187 ## Original Plan
2188 Inner Join: Filter: test.user = right.user AND leaf_udf(test.user, Utf8("status")) = Utf8("active")
2189 TableScan: test projection=[id, user]
2190 TableScan: right projection=[id, user]
2191
2192 ## After Extraction
2193 Projection: test.id, test.user, right.id, right.user
2194 Inner Join: Filter: test.user = right.user AND __datafusion_extracted_1 = Utf8("active")
2195 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2196 TableScan: test projection=[id, user]
2197 TableScan: right projection=[id, user]
2198
2199 ## After Pushdown
2200 (same as after extraction)
2201
2202 ## Optimized
2203 (same as after pushdown)
2204 "#)
2205 }
2206
2207 #[test]
2209 fn test_extract_from_join_both_sides() -> Result<()> {
2210 use datafusion_expr::JoinType;
2211
2212 let left = test_table_scan_with_struct()?;
2213 let right = test_table_scan_with_struct_named("right")?;
2214
2215 let plan = LogicalPlanBuilder::from(left)
2216 .join_on(
2217 right,
2218 JoinType::Inner,
2219 vec![
2220 col("test.user").eq(col("right.user")),
2221 leaf_udf(col("test.user"), "status").eq(lit("active")),
2222 leaf_udf(col("right.user"), "role").eq(lit("admin")),
2223 ],
2224 )?
2225 .build()?;
2226
2227 assert_stages!(plan, @r#"
2228 ## Original Plan
2229 Inner Join: Filter: test.user = right.user AND leaf_udf(test.user, Utf8("status")) = Utf8("active") AND leaf_udf(right.user, Utf8("role")) = Utf8("admin")
2230 TableScan: test projection=[id, user]
2231 TableScan: right projection=[id, user]
2232
2233 ## After Extraction
2234 Projection: test.id, test.user, right.id, right.user
2235 Inner Join: Filter: test.user = right.user AND __datafusion_extracted_1 = Utf8("active") AND __datafusion_extracted_2 = Utf8("admin")
2236 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2237 TableScan: test projection=[id, user]
2238 Projection: leaf_udf(right.user, Utf8("role")) AS __datafusion_extracted_2, right.id, right.user
2239 TableScan: right projection=[id, user]
2240
2241 ## After Pushdown
2242 (same as after extraction)
2243
2244 ## Optimized
2245 (same as after pushdown)
2246 "#)
2247 }
2248
2249 #[test]
2251 fn test_extract_from_join_no_extraction() -> Result<()> {
2252 use datafusion_expr::JoinType;
2253
2254 let left = test_table_scan()?;
2255 let right = test_table_scan_with_name("right")?;
2256
2257 let plan = LogicalPlanBuilder::from(left)
2258 .join(right, JoinType::Inner, (vec!["a"], vec!["a"]), None)?
2259 .build()?;
2260
2261 assert_stages!(plan, @"
2262 ## Original Plan
2263 Inner Join: test.a = right.a
2264 TableScan: test projection=[a, b, c]
2265 TableScan: right projection=[a, b, c]
2266
2267 ## After Extraction
2268 (same as original)
2269
2270 ## After Pushdown
2271 (same as after extraction)
2272
2273 ## Optimized
2274 (same as after pushdown)
2275 ")
2276 }
2277
2278 #[test]
2280 fn test_extract_from_filter_above_join() -> Result<()> {
2281 use datafusion_expr::JoinType;
2282
2283 let left = test_table_scan_with_struct()?;
2284 let right = test_table_scan_with_struct_named("right")?;
2285
2286 let plan = LogicalPlanBuilder::from(left)
2287 .join_with_expr_keys(
2288 right,
2289 JoinType::Inner,
2290 (
2291 vec![leaf_udf(col("user"), "id")],
2292 vec![leaf_udf(col("user"), "id")],
2293 ),
2294 None,
2295 )?
2296 .filter(leaf_udf(col("test.user"), "status").eq(lit("active")))?
2297 .build()?;
2298
2299 assert_stages!(plan, @r#"
2300 ## Original Plan
2301 Filter: leaf_udf(test.user, Utf8("status")) = Utf8("active")
2302 Inner Join: leaf_udf(test.user, Utf8("id")) = leaf_udf(right.user, Utf8("id"))
2303 TableScan: test projection=[id, user]
2304 TableScan: right projection=[id, user]
2305
2306 ## After Extraction
2307 Projection: test.id, test.user, right.id, right.user
2308 Filter: __datafusion_extracted_1 = Utf8("active")
2309 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user, right.id, right.user
2310 Projection: test.id, test.user, right.id, right.user
2311 Inner Join: __datafusion_extracted_2 = __datafusion_extracted_3
2312 Projection: leaf_udf(test.user, Utf8("id")) AS __datafusion_extracted_2, test.id, test.user
2313 TableScan: test projection=[id, user]
2314 Projection: leaf_udf(right.user, Utf8("id")) AS __datafusion_extracted_3, right.id, right.user
2315 TableScan: right projection=[id, user]
2316
2317 ## After Pushdown
2318 Projection: test.id, test.user, right.id, right.user
2319 Filter: __datafusion_extracted_1 = Utf8("active")
2320 Projection: test.id, test.user, right.id, right.user, __datafusion_extracted_1
2321 Inner Join: __datafusion_extracted_2 = __datafusion_extracted_3
2322 Projection: leaf_udf(test.user, Utf8("id")) AS __datafusion_extracted_2, test.id, test.user, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1
2323 TableScan: test projection=[id, user]
2324 Projection: leaf_udf(right.user, Utf8("id")) AS __datafusion_extracted_3, right.id, right.user
2325 TableScan: right projection=[id, user]
2326
2327 ## Optimized
2328 (same as after pushdown)
2329 "#)
2330 }
2331
2332 #[test]
2335 fn test_extract_projection_above_join() -> Result<()> {
2336 use datafusion_expr::JoinType;
2337
2338 let left = test_table_scan_with_struct()?;
2339 let right = test_table_scan_with_struct_named("right")?;
2340
2341 let plan = LogicalPlanBuilder::from(left)
2342 .join(right, JoinType::Inner, (vec!["id"], vec!["id"]), None)?
2343 .project(vec![
2344 leaf_udf(col("test.user"), "status"),
2345 leaf_udf(col("right.user"), "role"),
2346 ])?
2347 .build()?;
2348
2349 assert_stages!(plan, @r#"
2350 ## Original Plan
2351 Projection: leaf_udf(test.user, Utf8("status")), leaf_udf(right.user, Utf8("role"))
2352 Inner Join: test.id = right.id
2353 TableScan: test projection=[id, user]
2354 TableScan: right projection=[id, user]
2355
2356 ## After Extraction
2357 (same as original)
2358
2359 ## After Pushdown
2360 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("status")), __datafusion_extracted_2 AS leaf_udf(right.user,Utf8("role"))
2361 Inner Join: test.id = right.id
2362 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2363 TableScan: test projection=[id, user]
2364 Projection: leaf_udf(right.user, Utf8("role")) AS __datafusion_extracted_2, right.id, right.user
2365 TableScan: right projection=[id, user]
2366
2367 ## Optimized
2368 Projection: __datafusion_extracted_1 AS leaf_udf(test.user,Utf8("status")), __datafusion_extracted_2 AS leaf_udf(right.user,Utf8("role"))
2369 Inner Join: test.id = right.id
2370 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id
2371 TableScan: test projection=[id, user]
2372 Projection: leaf_udf(right.user, Utf8("role")) AS __datafusion_extracted_2, right.id
2373 TableScan: right projection=[id, user]
2374 "#)
2375 }
2376
2377 #[test]
2380 fn test_extract_from_join_qualified_right_side() -> Result<()> {
2381 use datafusion_expr::JoinType;
2382
2383 let left = test_table_scan_with_struct()?;
2384 let right = test_table_scan_with_struct_named("right")?;
2385
2386 let plan = LogicalPlanBuilder::from(left)
2388 .join_on(
2389 right,
2390 JoinType::Inner,
2391 vec![
2392 col("test.id").eq(col("right.id")),
2393 leaf_udf(col("right.user"), "status").eq(lit("active")),
2394 ],
2395 )?
2396 .build()?;
2397
2398 assert_stages!(plan, @r#"
2399 ## Original Plan
2400 Inner Join: Filter: test.id = right.id AND leaf_udf(right.user, Utf8("status")) = Utf8("active")
2401 TableScan: test projection=[id, user]
2402 TableScan: right projection=[id, user]
2403
2404 ## After Extraction
2405 Projection: test.id, test.user, right.id, right.user
2406 Inner Join: Filter: test.id = right.id AND __datafusion_extracted_1 = Utf8("active")
2407 TableScan: test projection=[id, user]
2408 Projection: leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_1, right.id, right.user
2409 TableScan: right projection=[id, user]
2410
2411 ## After Pushdown
2412 (same as after extraction)
2413
2414 ## Optimized
2415 (same as after pushdown)
2416 "#)
2417 }
2418
2419 #[test]
2423 fn test_find_owning_input_ambiguous_unqualified_column() {
2424 use std::collections::HashSet;
2425
2426 let relation = "test".into();
2430 let left_cols: HashSet<ColumnReference> = [
2431 ColumnReference::new(Some(&relation), "user"),
2432 ColumnReference::new_unqualified("user"),
2433 ]
2434 .into_iter()
2435 .collect();
2436
2437 let relation = "right".into();
2438 let right_cols: HashSet<ColumnReference> = [
2439 ColumnReference::new(Some(&relation), "user"),
2440 ColumnReference::new_unqualified("user"),
2441 ]
2442 .into_iter()
2443 .collect();
2444
2445 let input_column_sets = vec![left_cols, right_cols];
2446
2447 let unqualified = Expr::Column(Column::new_unqualified("user"));
2449 assert_eq!(find_owning_input(&unqualified, &input_column_sets), None);
2450
2451 let qualified_right = Expr::Column(Column::new(Some("right"), "user"));
2453 assert_eq!(
2454 find_owning_input(&qualified_right, &input_column_sets),
2455 Some(1)
2456 );
2457
2458 let qualified_left = Expr::Column(Column::new(Some("test"), "user"));
2460 assert_eq!(
2461 find_owning_input(&qualified_left, &input_column_sets),
2462 Some(0)
2463 );
2464 }
2465
2466 #[test]
2469 fn test_extract_from_join_cross_input_expression() -> Result<()> {
2470 let left = test_table_scan_with_struct()?;
2471 let right = test_table_scan_with_struct_named("right")?;
2472
2473 let plan = LogicalPlanBuilder::from(left)
2474 .join_on(
2475 right,
2476 datafusion_expr::JoinType::Inner,
2477 vec![col("test.id").eq(col("right.id"))],
2478 )?
2479 .filter(
2480 leaf_udf(col("test.user"), "status")
2481 .eq(leaf_udf(col("right.user"), "status")),
2482 )?
2483 .build()?;
2484
2485 assert_stages!(plan, @r#"
2486 ## Original Plan
2487 Filter: leaf_udf(test.user, Utf8("status")) = leaf_udf(right.user, Utf8("status"))
2488 Inner Join: Filter: test.id = right.id
2489 TableScan: test projection=[id, user]
2490 TableScan: right projection=[id, user]
2491
2492 ## After Extraction
2493 Projection: test.id, test.user, right.id, right.user
2494 Filter: __datafusion_extracted_1 = __datafusion_extracted_2
2495 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_2, test.id, test.user, right.id, right.user
2496 Inner Join: Filter: test.id = right.id
2497 TableScan: test projection=[id, user]
2498 TableScan: right projection=[id, user]
2499
2500 ## After Pushdown
2501 Projection: test.id, test.user, right.id, right.user
2502 Filter: __datafusion_extracted_1 = __datafusion_extracted_2
2503 Inner Join: Filter: test.id = right.id
2504 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2505 TableScan: test projection=[id, user]
2506 Projection: leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_2, right.id, right.user
2507 TableScan: right projection=[id, user]
2508
2509 ## Optimized
2510 (same as after pushdown)
2511 "#)
2512 }
2513
2514 #[test]
2520 fn test_extract_through_filter_with_column_rename() -> Result<()> {
2521 let table_scan = test_table_scan_with_struct()?;
2522 let plan = LogicalPlanBuilder::from(table_scan)
2523 .project(vec![col("user").alias("x")])?
2524 .filter(col("x").is_not_null())?
2525 .project(vec![leaf_udf(col("x"), "a")])?
2526 .build()?;
2527
2528 assert_stages!(plan, @r#"
2529 ## Original Plan
2530 Projection: leaf_udf(x, Utf8("a"))
2531 Filter: x IS NOT NULL
2532 Projection: test.user AS x
2533 TableScan: test projection=[user]
2534
2535 ## After Extraction
2536 (same as original)
2537
2538 ## After Pushdown
2539 Projection: __datafusion_extracted_1 AS leaf_udf(x,Utf8("a"))
2540 Filter: x IS NOT NULL
2541 Projection: test.user AS x, leaf_udf(test.user, Utf8("a")) AS __datafusion_extracted_1, test.user
2542 TableScan: test projection=[user]
2543
2544 ## Optimized
2545 Projection: __datafusion_extracted_1 AS leaf_udf(x,Utf8("a"))
2546 Filter: x IS NOT NULL
2547 Projection: test.user AS x, leaf_udf(test.user, Utf8("a")) AS __datafusion_extracted_1
2548 TableScan: test projection=[user]
2549 "#)
2550 }
2551
2552 #[test]
2554 fn test_extract_partial_through_filter_with_column_rename() -> Result<()> {
2555 let table_scan = test_table_scan_with_struct()?;
2556 let plan = LogicalPlanBuilder::from(table_scan)
2557 .project(vec![col("user").alias("x")])?
2558 .filter(col("x").is_not_null())?
2559 .project(vec![leaf_udf(col("x"), "a").is_not_null()])?
2560 .build()?;
2561
2562 assert_stages!(plan, @r#"
2563 ## Original Plan
2564 Projection: leaf_udf(x, Utf8("a")) IS NOT NULL
2565 Filter: x IS NOT NULL
2566 Projection: test.user AS x
2567 TableScan: test projection=[user]
2568
2569 ## After Extraction
2570 (same as original)
2571
2572 ## After Pushdown
2573 Projection: __datafusion_extracted_1 IS NOT NULL AS leaf_udf(x,Utf8("a")) IS NOT NULL
2574 Filter: x IS NOT NULL
2575 Projection: test.user AS x, leaf_udf(test.user, Utf8("a")) AS __datafusion_extracted_1, test.user
2576 TableScan: test projection=[user]
2577
2578 ## Optimized
2579 Projection: __datafusion_extracted_1 IS NOT NULL AS leaf_udf(x,Utf8("a")) IS NOT NULL
2580 Filter: x IS NOT NULL
2581 Projection: test.user AS x, leaf_udf(test.user, Utf8("a")) AS __datafusion_extracted_1
2582 TableScan: test projection=[user]
2583 "#)
2584 }
2585
2586 #[test]
2588 fn test_extract_from_filter_above_renaming_projection() -> Result<()> {
2589 let table_scan = test_table_scan_with_struct()?;
2590 let plan = LogicalPlanBuilder::from(table_scan)
2591 .project(vec![col("user").alias("x")])?
2592 .filter(leaf_udf(col("x"), "a").eq(lit("active")))?
2593 .build()?;
2594
2595 assert_stages!(plan, @r#"
2596 ## Original Plan
2597 Filter: leaf_udf(x, Utf8("a")) = Utf8("active")
2598 Projection: test.user AS x
2599 TableScan: test projection=[user]
2600
2601 ## After Extraction
2602 Projection: x
2603 Filter: __datafusion_extracted_1 = Utf8("active")
2604 Projection: test.user AS x, leaf_udf(test.user, Utf8("a")) AS __datafusion_extracted_1, test.user
2605 TableScan: test projection=[user]
2606
2607 ## After Pushdown
2608 (same as after extraction)
2609
2610 ## Optimized
2611 Projection: x
2612 Filter: __datafusion_extracted_1 = Utf8("active")
2613 Projection: test.user AS x, leaf_udf(test.user, Utf8("a")) AS __datafusion_extracted_1
2614 TableScan: test projection=[user]
2615 "#)
2616 }
2617
2618 #[test]
2624 fn test_extract_through_subquery_alias() -> Result<()> {
2625 let table_scan = test_table_scan_with_struct()?;
2626 let plan = LogicalPlanBuilder::from(table_scan)
2627 .alias("sub")?
2628 .project(vec![leaf_udf(col("sub.user"), "name")])?
2629 .build()?;
2630
2631 assert_stages!(plan, @r#"
2632 ## Original Plan
2633 Projection: leaf_udf(sub.user, Utf8("name"))
2634 SubqueryAlias: sub
2635 TableScan: test projection=[user]
2636
2637 ## After Extraction
2638 (same as original)
2639
2640 ## After Pushdown
2641 Projection: __datafusion_extracted_1 AS leaf_udf(sub.user,Utf8("name"))
2642 SubqueryAlias: sub
2643 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
2644 TableScan: test projection=[user]
2645
2646 ## Optimized
2647 Projection: __datafusion_extracted_1 AS leaf_udf(sub.user,Utf8("name"))
2648 SubqueryAlias: sub
2649 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1
2650 TableScan: test projection=[user]
2651 "#)
2652 }
2653
2654 #[test]
2656 fn test_extract_through_subquery_alias_with_filter() -> Result<()> {
2657 let table_scan = test_table_scan_with_struct()?;
2658 let plan = LogicalPlanBuilder::from(table_scan)
2659 .alias("sub")?
2660 .filter(leaf_udf(col("sub.user"), "status").eq(lit("active")))?
2661 .project(vec![leaf_udf(col("sub.user"), "name")])?
2662 .build()?;
2663
2664 assert_stages!(plan, @r#"
2665 ## Original Plan
2666 Projection: leaf_udf(sub.user, Utf8("name"))
2667 Filter: leaf_udf(sub.user, Utf8("status")) = Utf8("active")
2668 SubqueryAlias: sub
2669 TableScan: test projection=[user]
2670
2671 ## After Extraction
2672 Projection: leaf_udf(sub.user, Utf8("name"))
2673 Projection: sub.user
2674 Filter: __datafusion_extracted_1 = Utf8("active")
2675 Projection: leaf_udf(sub.user, Utf8("status")) AS __datafusion_extracted_1, sub.user
2676 SubqueryAlias: sub
2677 TableScan: test projection=[user]
2678
2679 ## After Pushdown
2680 Projection: __datafusion_extracted_2 AS leaf_udf(sub.user,Utf8("name"))
2681 Projection: sub.user, __datafusion_extracted_2
2682 Filter: __datafusion_extracted_1 = Utf8("active")
2683 SubqueryAlias: sub
2684 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2, test.user
2685 TableScan: test projection=[user]
2686
2687 ## Optimized
2688 Projection: __datafusion_extracted_2 AS leaf_udf(sub.user,Utf8("name"))
2689 Filter: __datafusion_extracted_1 = Utf8("active")
2690 SubqueryAlias: sub
2691 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2
2692 TableScan: test projection=[user]
2693 "#)
2694 }
2695
2696 #[test]
2698 fn test_extract_through_nested_subquery_alias() -> Result<()> {
2699 let table_scan = test_table_scan_with_struct()?;
2700 let plan = LogicalPlanBuilder::from(table_scan)
2701 .alias("inner_sub")?
2702 .alias("outer_sub")?
2703 .project(vec![leaf_udf(col("outer_sub.user"), "name")])?
2704 .build()?;
2705
2706 assert_stages!(plan, @r#"
2707 ## Original Plan
2708 Projection: leaf_udf(outer_sub.user, Utf8("name"))
2709 SubqueryAlias: outer_sub
2710 SubqueryAlias: inner_sub
2711 TableScan: test projection=[user]
2712
2713 ## After Extraction
2714 (same as original)
2715
2716 ## After Pushdown
2717 Projection: __datafusion_extracted_1 AS leaf_udf(outer_sub.user,Utf8("name"))
2718 SubqueryAlias: outer_sub
2719 SubqueryAlias: inner_sub
2720 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1, test.user
2721 TableScan: test projection=[user]
2722
2723 ## Optimized
2724 Projection: __datafusion_extracted_1 AS leaf_udf(outer_sub.user,Utf8("name"))
2725 SubqueryAlias: outer_sub
2726 SubqueryAlias: inner_sub
2727 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_1
2728 TableScan: test projection=[user]
2729 "#)
2730 }
2731
2732 #[test]
2734 fn test_subquery_alias_no_extraction() -> Result<()> {
2735 let table_scan = test_table_scan()?;
2736 let plan = LogicalPlanBuilder::from(table_scan)
2737 .alias("sub")?
2738 .project(vec![col("sub.a"), col("sub.b")])?
2739 .build()?;
2740
2741 assert_stages!(plan, @"
2742 ## Original Plan
2743 SubqueryAlias: sub
2744 TableScan: test projection=[a, b]
2745
2746 ## After Extraction
2747 (same as original)
2748
2749 ## After Pushdown
2750 (same as after extraction)
2751
2752 ## Optimized
2753 (same as after pushdown)
2754 ")
2755 }
2756
2757 #[test]
2761 fn test_different_udfs_same_schema_name_not_deduplicated() -> Result<()> {
2762 let udf_a = Arc::new(ScalarUDF::new_from_impl(
2763 PlacementTestUDF::new()
2764 .with_placement(ExpressionPlacement::MoveTowardsLeafNodes)
2765 .with_id(1),
2766 ));
2767 let udf_b = Arc::new(ScalarUDF::new_from_impl(
2768 PlacementTestUDF::new()
2769 .with_placement(ExpressionPlacement::MoveTowardsLeafNodes)
2770 .with_id(2),
2771 ));
2772
2773 let expr_a = Expr::ScalarFunction(ScalarFunction::new_udf(
2774 udf_a,
2775 vec![col("user"), lit("field")],
2776 ));
2777 let expr_b = Expr::ScalarFunction(ScalarFunction::new_udf(
2778 udf_b,
2779 vec![col("user"), lit("field")],
2780 ));
2781
2782 assert_eq!(
2784 expr_a.schema_name().to_string(),
2785 expr_b.schema_name().to_string(),
2786 "Both expressions should have the same schema_name"
2787 );
2788 assert_ne!(
2789 expr_a, expr_b,
2790 "Expressions should NOT be equal (different UDF instances)"
2791 );
2792
2793 let table_scan = test_table_scan_with_struct()?;
2794 let plan = LogicalPlanBuilder::from(table_scan.clone())
2795 .filter(expr_a.clone().eq(lit("a")).and(expr_b.clone().eq(lit("b"))))?
2796 .select(vec![
2797 table_scan
2798 .schema()
2799 .index_of_column_by_name(None, "id")
2800 .unwrap(),
2801 ])?
2802 .build()?;
2803
2804 assert_stages!(plan, @r#"
2805 ## Original Plan
2806 Projection: test.id
2807 Filter: leaf_udf(test.user, Utf8("field")) = Utf8("a") AND leaf_udf(test.user, Utf8("field")) = Utf8("b")
2808 TableScan: test projection=[id, user]
2809
2810 ## After Extraction
2811 Projection: test.id
2812 Projection: test.id, test.user
2813 Filter: __datafusion_extracted_1 = Utf8("a") AND __datafusion_extracted_2 = Utf8("b")
2814 Projection: leaf_udf(test.user, Utf8("field")) AS __datafusion_extracted_1, leaf_udf(test.user, Utf8("field")) AS __datafusion_extracted_2, test.id, test.user
2815 TableScan: test projection=[id, user]
2816
2817 ## After Pushdown
2818 (same as after extraction)
2819
2820 ## Optimized
2821 Projection: test.id
2822 Filter: __datafusion_extracted_1 = Utf8("a") AND __datafusion_extracted_2 = Utf8("b")
2823 Projection: leaf_udf(test.user, Utf8("field")) AS __datafusion_extracted_1, leaf_udf(test.user, Utf8("field")) AS __datafusion_extracted_2, test.id
2824 TableScan: test projection=[id, user]
2825 "#)
2826 }
2827
2828 #[test]
2835 fn test_extraction_pushdown_through_filter_with_extracted_predicate() -> Result<()> {
2836 let table_scan = test_table_scan_with_struct()?;
2837 let plan = LogicalPlanBuilder::from(table_scan)
2838 .filter(leaf_udf(col("user"), "status").eq(lit("active")))?
2839 .project(vec![col("id"), leaf_udf(col("user"), "name")])?
2840 .build()?;
2841
2842 assert_stages!(plan, @r#"
2843 ## Original Plan
2844 Projection: test.id, leaf_udf(test.user, Utf8("name"))
2845 Filter: leaf_udf(test.user, Utf8("status")) = Utf8("active")
2846 TableScan: test projection=[id, user]
2847
2848 ## After Extraction
2849 Projection: test.id, leaf_udf(test.user, Utf8("name"))
2850 Projection: test.id, test.user
2851 Filter: __datafusion_extracted_1 = Utf8("active")
2852 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2853 TableScan: test projection=[id, user]
2854
2855 ## After Pushdown
2856 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name"))
2857 Projection: test.id, test.user, __datafusion_extracted_2
2858 Filter: __datafusion_extracted_1 = Utf8("active")
2859 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2
2860 TableScan: test projection=[id, user]
2861
2862 ## Optimized
2863 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name"))
2864 Filter: __datafusion_extracted_1 = Utf8("active")
2865 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2
2866 TableScan: test projection=[id, user]
2867 "#)
2868 }
2869
2870 #[test]
2872 fn test_extraction_pushdown_same_expr_in_filter_and_projection() -> Result<()> {
2873 let table_scan = test_table_scan_with_struct()?;
2874 let field_expr = leaf_udf(col("user"), "status");
2875 let plan = LogicalPlanBuilder::from(table_scan)
2876 .filter(field_expr.clone().gt(lit(5)))?
2877 .project(vec![col("id"), field_expr])?
2878 .build()?;
2879
2880 assert_stages!(plan, @r#"
2881 ## Original Plan
2882 Projection: test.id, leaf_udf(test.user, Utf8("status"))
2883 Filter: leaf_udf(test.user, Utf8("status")) > Int32(5)
2884 TableScan: test projection=[id, user]
2885
2886 ## After Extraction
2887 Projection: test.id, leaf_udf(test.user, Utf8("status"))
2888 Projection: test.id, test.user
2889 Filter: __datafusion_extracted_1 > Int32(5)
2890 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2891 TableScan: test projection=[id, user]
2892
2893 ## After Pushdown
2894 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("status"))
2895 Projection: test.id, test.user, __datafusion_extracted_2
2896 Filter: __datafusion_extracted_1 > Int32(5)
2897 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2
2898 TableScan: test projection=[id, user]
2899
2900 ## Optimized
2901 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("status"))
2902 Filter: __datafusion_extracted_1 > Int32(5)
2903 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2
2904 TableScan: test projection=[id, user]
2905 "#)
2906 }
2907
2908 #[test]
2911 fn test_left_join_with_filter_and_projection_extraction() -> Result<()> {
2912 use datafusion_expr::JoinType;
2913
2914 let left = test_table_scan_with_struct()?;
2915 let right = test_table_scan_with_struct_named("right")?;
2916
2917 let plan = LogicalPlanBuilder::from(left)
2918 .join_on(
2919 right,
2920 JoinType::Left,
2921 vec![
2922 col("test.id").eq(col("right.id")),
2923 leaf_udf(col("right.user"), "status").gt(lit(5)),
2924 ],
2925 )?
2926 .project(vec![
2927 col("test.id"),
2928 leaf_udf(col("test.user"), "name"),
2929 leaf_udf(col("right.user"), "status"),
2930 ])?
2931 .build()?;
2932
2933 assert_stages!(plan, @r#"
2934 ## Original Plan
2935 Projection: test.id, leaf_udf(test.user, Utf8("name")), leaf_udf(right.user, Utf8("status"))
2936 Left Join: Filter: test.id = right.id AND leaf_udf(right.user, Utf8("status")) > Int32(5)
2937 TableScan: test projection=[id, user]
2938 TableScan: right projection=[id, user]
2939
2940 ## After Extraction
2941 Projection: test.id, leaf_udf(test.user, Utf8("name")), leaf_udf(right.user, Utf8("status"))
2942 Projection: test.id, test.user, right.id, right.user
2943 Left Join: Filter: test.id = right.id AND __datafusion_extracted_1 > Int32(5)
2944 TableScan: test projection=[id, user]
2945 Projection: leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_1, right.id, right.user
2946 TableScan: right projection=[id, user]
2947
2948 ## After Pushdown
2949 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name")), __datafusion_extracted_3 AS leaf_udf(right.user,Utf8("status"))
2950 Projection: test.id, test.user, right.id, right.user, __datafusion_extracted_2, __datafusion_extracted_3
2951 Left Join: Filter: test.id = right.id AND __datafusion_extracted_1 > Int32(5)
2952 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2, test.id, test.user
2953 TableScan: test projection=[id, user]
2954 Projection: leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_1, right.id, right.user, leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_3
2955 TableScan: right projection=[id, user]
2956
2957 ## Optimized
2958 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name")), __datafusion_extracted_3 AS leaf_udf(right.user,Utf8("status"))
2959 Left Join: Filter: test.id = right.id AND __datafusion_extracted_1 > Int32(5)
2960 Projection: leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2, test.id
2961 TableScan: test projection=[id, user]
2962 Projection: leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_1, right.id, leaf_udf(right.user, Utf8("status")) AS __datafusion_extracted_3
2963 TableScan: right projection=[id, user]
2964 "#)
2965 }
2966
2967 #[test]
2970 fn test_pure_extraction_proj_push_through_filter() -> Result<()> {
2971 let table_scan = test_table_scan_with_struct()?;
2972 let plan = LogicalPlanBuilder::from(table_scan)
2973 .filter(leaf_udf(col("user"), "status").gt(lit(5)))?
2974 .project(vec![
2975 col("id"),
2976 leaf_udf(col("user"), "name"),
2977 leaf_udf(col("user"), "status"),
2978 ])?
2979 .build()?;
2980
2981 assert_stages!(plan, @r#"
2982 ## Original Plan
2983 Projection: test.id, leaf_udf(test.user, Utf8("name")), leaf_udf(test.user, Utf8("status"))
2984 Filter: leaf_udf(test.user, Utf8("status")) > Int32(5)
2985 TableScan: test projection=[id, user]
2986
2987 ## After Extraction
2988 Projection: test.id, leaf_udf(test.user, Utf8("name")), leaf_udf(test.user, Utf8("status"))
2989 Projection: test.id, test.user
2990 Filter: __datafusion_extracted_1 > Int32(5)
2991 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user
2992 TableScan: test projection=[id, user]
2993
2994 ## After Pushdown
2995 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name")), __datafusion_extracted_3 AS leaf_udf(test.user,Utf8("status"))
2996 Projection: test.id, test.user, __datafusion_extracted_2, __datafusion_extracted_3
2997 Filter: __datafusion_extracted_1 > Int32(5)
2998 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_3
2999 TableScan: test projection=[id, user]
3000
3001 ## Optimized
3002 Projection: test.id, __datafusion_extracted_2 AS leaf_udf(test.user,Utf8("name")), __datafusion_extracted_3 AS leaf_udf(test.user,Utf8("status"))
3003 Filter: __datafusion_extracted_1 > Int32(5)
3004 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, leaf_udf(test.user, Utf8("name")) AS __datafusion_extracted_2, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_3
3005 TableScan: test projection=[id, user]
3006 "#)
3007 }
3008
3009 #[test]
3013 fn test_merge_extraction_into_projection_with_column_ref_inflation() -> Result<()> {
3014 let table_scan = test_table_scan_with_struct()?;
3015
3016 let inner = LogicalPlanBuilder::from(table_scan)
3018 .project(vec![col("user"), col("id")])?
3019 .build()?;
3020
3021 let plan = LogicalPlanBuilder::from(inner)
3024 .project(vec![
3025 leaf_udf(col("user"), "status")
3026 .alias(format!("{EXTRACTED_EXPR_PREFIX}_1")),
3027 col("id"),
3028 ])?
3029 .build()?;
3030
3031 let ctx = OptimizerContext::new().with_max_passes(1);
3033 let optimizer =
3034 Optimizer::with_rules(vec![Arc::new(PushDownLeafProjections::new())]);
3035 let result = optimizer.optimize(plan, &ctx, |_, _| {})?;
3036
3037 insta::assert_snapshot!(format!("{result}"), @r#"
3040 Projection: __datafusion_extracted_1, test.id
3041 Projection: test.user, test.id, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1
3042 TableScan: test
3043 "#);
3044
3045 Ok(())
3046 }
3047
3048 #[test]
3058 fn test_no_push_through_unnest() -> Result<()> {
3059 use arrow::datatypes::{DataType, Field, Schema};
3060
3061 let schema = Schema::new(vec![
3062 Field::new("list_col", DataType::new_list(DataType::Int32, true), true),
3063 Field::new("other_col", DataType::Int32, true),
3064 ]);
3065 let table_scan =
3066 datafusion_expr::logical_plan::table_scan(Some("t"), &schema, None)?
3067 .build()?;
3068 let plan = LogicalPlanBuilder::from(table_scan)
3069 .unnest_column("list_col")?
3070 .filter(leaf_udf(col("list_col"), "x").eq(lit(1i32)))?
3071 .build()?;
3072
3073 let ctx = OptimizerContext::new().with_max_passes(1);
3074 let optimizer = Optimizer::with_rules(vec![
3075 Arc::new(ExtractLeafExpressions::new()),
3076 Arc::new(PushDownLeafProjections::new()),
3077 ]);
3078 let optimized = optimizer.optimize(plan, &ctx, |_, _| {})?;
3079
3080 insta::assert_snapshot!(format!("{optimized}"), @r#"
3081 Projection: list_col, t.other_col
3082 Filter: __datafusion_extracted_1 = Int32(1)
3083 Projection: leaf_udf(list_col, Utf8("x")) AS __datafusion_extracted_1, list_col, t.other_col
3084 Unnest: lists[t.list_col|depth=1] structs[]
3085 TableScan: t
3086 "#);
3087
3088 Ok(())
3089 }
3090
3091 #[test]
3107 fn test_subquery_alias_with_unconsumed_column() -> Result<()> {
3108 let table_scan = test_table_scan_with_struct()?;
3109
3110 let inner = LogicalPlanBuilder::from(table_scan)
3121 .filter(leaf_udf(col("user"), "status").is_not_null())?
3122 .project(vec![
3123 col("user"),
3124 col("id"),
3125 (col("id") + lit(1u32)).alias("synth"),
3126 ])?
3127 .alias("v")?
3128 .build()?;
3129
3130 let plan = LogicalPlanBuilder::from(inner)
3132 .project(vec![
3133 leaf_udf(col("v.user"), "status"),
3134 col("v.user"),
3135 col("v.id"),
3136 ])?
3137 .build()?;
3138
3139 let ctx = OptimizerContext::new();
3145 let optimizer = Optimizer::with_rules(vec![
3146 Arc::new(ExtractLeafExpressions::new()),
3147 Arc::new(PushDownLeafProjections::new()),
3148 Arc::new(OptimizeProjections::new()),
3149 ]);
3150 let optimized = optimizer.optimize(plan, &ctx, |_, _| {})?;
3151
3152 insta::assert_snapshot!(format!("{optimized}"), @r#"
3156 Projection: __datafusion_extracted_2 AS leaf_udf(v.user,Utf8("status")), v.user, v.id
3157 SubqueryAlias: v
3158 Projection: test.user, test.id, __datafusion_extracted_2
3159 Filter: __datafusion_extracted_1 IS NOT NULL
3160 Projection: leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_1, test.id, test.user, leaf_udf(test.user, Utf8("status")) AS __datafusion_extracted_2
3161 TableScan: test projection=[id, user]
3162 "#);
3163
3164 Ok(())
3165 }
3166}