1use crate::utils::for_each_referenced_index;
73use crate::{OptimizerConfig, OptimizerRule};
74use datafusion_common::tree_node::{Transformed, TreeNode};
75use datafusion_common::{
76 DFSchema, Dependency, HashSet, NullEquality, Result, ScalarValue,
77};
78use datafusion_expr::{
79 Expr, JoinType,
80 logical_plan::{
81 Aggregate, Distinct, DistinctOn, EmptyRelation, Filter, Join, Limit, LogicalPlan,
82 Partitioning, Projection, Repartition, Sort, SubqueryAlias,
83 },
84};
85use std::sync::Arc;
86
87#[derive(Debug, Default, Clone)]
94struct LiveColumns(HashSet<usize>);
95
96impl LiveColumns {
97 fn new() -> Self {
98 Self(HashSet::new())
99 }
100
101 fn all(schema: &DFSchema) -> Self {
103 Self((0..schema.fields().len()).collect())
104 }
105
106 fn try_new<'a>(
108 exprs: impl IntoIterator<Item = &'a Expr>,
109 schema: &DFSchema,
110 ) -> Result<Self> {
111 let mut live = Self::new();
112 live.extend_from(exprs, schema)?;
113 Ok(live)
114 }
115
116 fn extend_from<'a>(
120 &mut self,
121 exprs: impl IntoIterator<Item = &'a Expr>,
122 schema: &DFSchema,
123 ) -> Result<()> {
124 for expr in exprs {
125 for_each_referenced_index(expr, schema, |idx| {
126 self.0.insert(idx);
127 })?;
128 }
129 Ok(())
130 }
131
132 fn insert(&mut self, idx: usize) {
133 self.0.insert(idx);
134 }
135
136 fn is_empty(&self) -> bool {
137 self.0.is_empty()
138 }
139
140 fn split_at(&self, left_len: usize) -> (Self, Self) {
145 let mut left = Self::new();
146 let mut right = Self::new();
147 for &idx in &self.0 {
148 if idx < left_len {
149 left.insert(idx);
150 } else {
151 right.insert(idx - left_len);
152 }
153 }
154 (left, right)
155 }
156}
157
158#[derive(Default, Debug)]
163pub struct EliminateJoin;
164
165impl EliminateJoin {
166 pub fn new() -> Self {
167 Self {}
168 }
169}
170
171impl OptimizerRule for EliminateJoin {
172 fn name(&self) -> &str {
173 "eliminate_join"
174 }
175
176 fn rewrite(
177 &self,
178 plan: LogicalPlan,
179 _config: &dyn OptimizerConfig,
180 ) -> Result<Transformed<LogicalPlan>> {
181 let live = LiveColumns::all(plan.schema());
182 rewrite_subtree(plan, live, false)
183 }
184}
185
186fn rewrite_subtree(
194 plan: LogicalPlan,
195 live: LiveColumns,
196 duplicate_insensitive: bool,
197) -> Result<Transformed<LogicalPlan>> {
198 rewrite_node(plan, live, duplicate_insensitive)?.transform_data(|plan| {
199 plan.map_subqueries(|subquery| {
200 let live = LiveColumns::all(subquery.schema());
201 rewrite_subtree(subquery, live, false)
202 })
203 })
204}
205
206fn rewrite_node(
207 plan: LogicalPlan,
208 live: LiveColumns,
209 duplicate_insensitive: bool,
210) -> Result<Transformed<LogicalPlan>> {
211 match plan {
212 LogicalPlan::Join(join) => rewrite_join(join, &live, duplicate_insensitive),
214 LogicalPlan::Projection(Projection {
215 expr,
216 input,
217 schema,
218 ..
219 }) => {
220 let child_live = LiveColumns::try_new(&expr, input.schema())?;
222 rewrite_single_input(input, child_live, duplicate_insensitive, |input| {
223 Ok(LogicalPlan::Projection(Projection::try_new_with_schema(
224 expr, input, schema,
225 )?))
226 })
227 }
228 LogicalPlan::Filter(Filter {
229 predicate, input, ..
230 }) => {
231 let mut child_live = live;
233 child_live.extend_from([&predicate], input.schema())?;
234 rewrite_single_input(input, child_live, duplicate_insensitive, |input| {
235 Ok(LogicalPlan::Filter(Filter::new(predicate, input)))
236 })
237 }
238 LogicalPlan::Aggregate(Aggregate {
239 input,
240 group_expr,
241 aggr_expr,
242 schema,
243 ..
244 }) => {
245 let child_live = LiveColumns::try_new(
247 group_expr.iter().chain(&aggr_expr),
248 input.schema(),
249 )?;
250
251 let child_duplicate_insensitive =
256 !group_expr.is_empty() && aggr_expr.is_empty();
257
258 rewrite_single_input(
259 input,
260 child_live,
261 child_duplicate_insensitive,
262 |input| {
263 Ok(LogicalPlan::Aggregate(Aggregate::try_new_with_schema(
264 input, group_expr, aggr_expr, schema,
265 )?))
266 },
267 )
268 }
269 LogicalPlan::Distinct(Distinct::All(input)) => {
270 let child_live = LiveColumns::all(input.schema());
274 rewrite_single_input(input, child_live, true, |input| {
275 Ok(LogicalPlan::Distinct(Distinct::All(input)))
276 })
277 }
278 LogicalPlan::Distinct(Distinct::On(DistinctOn {
279 on_expr,
280 select_expr,
281 sort_expr,
282 input,
283 schema,
284 })) => {
285 let mut child_live =
290 LiveColumns::try_new(on_expr.iter().chain(&select_expr), input.schema())?;
291 if let Some(sort_expr) = &sort_expr {
292 child_live
293 .extend_from(sort_expr.iter().map(|s| &s.expr), input.schema())?;
294 }
295
296 rewrite_single_input(input, child_live, true, |input| {
297 Ok(LogicalPlan::Distinct(Distinct::On(DistinctOn {
298 on_expr,
299 select_expr,
300 sort_expr,
301 input,
302 schema,
303 })))
304 })
305 }
306 LogicalPlan::Sort(Sort { expr, input, fetch }) => {
307 let mut child_live = live;
309 child_live.extend_from(expr.iter().map(|s| &s.expr), input.schema())?;
310
311 let child_duplicate_insensitive = duplicate_insensitive && fetch.is_none();
314 rewrite_single_input(
315 input,
316 child_live,
317 child_duplicate_insensitive,
318 |input| Ok(LogicalPlan::Sort(Sort { expr, input, fetch })),
319 )
320 }
321 LogicalPlan::Limit(Limit { skip, fetch, input }) => {
322 rewrite_single_input(input, live, false, |input| {
324 Ok(LogicalPlan::Limit(Limit { skip, fetch, input }))
325 })
326 }
327 LogicalPlan::SubqueryAlias(SubqueryAlias { input, alias, .. }) => {
328 rewrite_single_input(input, live, duplicate_insensitive, |input| {
330 Ok(LogicalPlan::SubqueryAlias(SubqueryAlias::try_new(
331 input, alias,
332 )?))
333 })
334 }
335 LogicalPlan::Repartition(Repartition {
336 input,
337 partitioning_scheme,
338 }) => {
339 let mut child_live = live;
341 match &partitioning_scheme {
342 Partitioning::Hash(exprs, _) | Partitioning::DistributeBy(exprs) => {
343 child_live.extend_from(exprs, input.schema())?;
344 }
345 Partitioning::Range(range) => {
346 child_live.extend_from(
347 range.ordering().iter().map(|sort_expr| &sort_expr.expr),
348 input.schema(),
349 )?;
350 }
351 Partitioning::RoundRobinBatch(_) => {}
352 }
353 rewrite_single_input(input, child_live, duplicate_insensitive, |input| {
354 Ok(LogicalPlan::Repartition(Repartition {
355 input,
356 partitioning_scheme,
357 }))
358 })
359 }
360 _ => plan.map_children(|child| {
363 let live = LiveColumns::all(child.schema());
364 rewrite_subtree(child, live, false)
365 }),
366 }
367}
368
369fn rewrite_single_input<F>(
374 input: Arc<LogicalPlan>,
375 child_live: LiveColumns,
376 duplicate_insensitive: bool,
377 rebuild: F,
378) -> Result<Transformed<LogicalPlan>>
379where
380 F: FnOnce(Arc<LogicalPlan>) -> Result<LogicalPlan>,
381{
382 rewrite_subtree(
383 Arc::unwrap_or_clone(input),
384 child_live,
385 duplicate_insensitive,
386 )?
387 .map_data(|input| rebuild(Arc::new(input)))
388}
389
390fn rewrite_join(
391 join: Join,
392 live: &LiveColumns,
393 duplicate_insensitive: bool,
394) -> Result<Transformed<LogicalPlan>> {
395 if join.join_type == JoinType::Inner
396 && join.on.is_empty()
397 && matches!(
398 join.filter.as_ref(),
399 Some(Expr::Literal(ScalarValue::Boolean(Some(false)), _))
400 )
401 {
402 return Ok(Transformed::yes(LogicalPlan::EmptyRelation(
403 EmptyRelation {
404 produce_one_row: false,
405 schema: join.schema,
406 },
407 )));
408 }
409
410 let (visible_left, visible_right) = split_join_output_columns(&join, live);
411
412 let rewritten_join_type = match rewritten_join_type(
413 &join,
414 &visible_left,
415 &visible_right,
416 duplicate_insensitive,
417 ) {
418 JoinRewrite::ReplaceWithLeft => {
419 let left = rewrite_subtree(
420 Arc::unwrap_or_clone(join.left),
421 visible_left,
422 duplicate_insensitive,
423 )?;
424 return Ok(Transformed::yes(left.data));
425 }
426 JoinRewrite::ReplaceWithRight => {
427 let right = rewrite_subtree(
428 Arc::unwrap_or_clone(join.right),
429 visible_right,
430 duplicate_insensitive,
431 )?;
432 return Ok(Transformed::yes(right.data));
433 }
434 JoinRewrite::Join(join_type) => join_type,
435 };
436
437 let (mut left_live, mut right_live) = match rewritten_join_type {
438 JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
439 (visible_left, LiveColumns::new())
440 }
441 JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
442 (LiveColumns::new(), visible_right)
443 }
444 _ => (visible_left, visible_right),
445 };
446
447 add_join_condition_columns(&join, &mut left_live, &mut right_live)?;
448
449 let (left_dup_insensitive, right_dup_insensitive) =
450 child_duplicate_insensitivity(rewritten_join_type, duplicate_insensitive);
451
452 let left = rewrite_subtree(
453 Arc::unwrap_or_clone(join.left),
454 left_live,
455 left_dup_insensitive,
456 )?;
457 let right = rewrite_subtree(
458 Arc::unwrap_or_clone(join.right),
459 right_live,
460 right_dup_insensitive,
461 )?;
462
463 let changed =
464 left.transformed || right.transformed || rewritten_join_type != join.join_type;
465 let left = Arc::new(left.data);
466 let right = Arc::new(right.data);
467
468 if changed {
469 Ok(Transformed::yes(LogicalPlan::Join(Join::try_new(
472 left,
473 right,
474 join.on,
475 join.filter,
476 rewritten_join_type,
477 join.join_constraint,
478 join.null_equality,
479 join.null_aware,
480 )?)))
481 } else {
482 Ok(Transformed::no(LogicalPlan::Join(Join {
485 left,
486 right,
487 on: join.on,
488 filter: join.filter,
489 join_type: join.join_type,
490 join_constraint: join.join_constraint,
491 schema: join.schema,
492 null_equality: join.null_equality,
493 null_aware: join.null_aware,
494 })))
495 }
496}
497
498fn child_duplicate_insensitivity(
502 join_type: JoinType,
503 duplicate_insensitive: bool,
504) -> (bool, bool) {
505 match join_type {
506 JoinType::Inner => (duplicate_insensitive, duplicate_insensitive),
507 JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
508 (duplicate_insensitive, true)
509 }
510 JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
511 (true, duplicate_insensitive)
512 }
513 JoinType::Left | JoinType::Right | JoinType::Full => (false, false),
514 }
515}
516
517enum JoinRewrite {
519 Join(JoinType),
521 ReplaceWithLeft,
523 ReplaceWithRight,
525}
526
527fn rewritten_join_type(
532 join: &Join,
533 visible_left: &LiveColumns,
534 visible_right: &LiveColumns,
535 duplicate_insensitive: bool,
536) -> JoinRewrite {
537 let can_remove_right = visible_right.is_empty()
541 && (duplicate_insensitive
542 || side_unique_on_join(
543 join.right.schema(),
544 join.on.iter().map(|(_, right)| right),
545 join.null_equality,
546 ));
547
548 if join.join_type == JoinType::Left && can_remove_right {
553 return JoinRewrite::ReplaceWithLeft;
554 }
555 let can_remove_left = visible_left.is_empty()
556 && (duplicate_insensitive
557 || side_unique_on_join(
558 join.left.schema(),
559 join.on.iter().map(|(left, _)| left),
560 join.null_equality,
561 ));
562
563 if join.join_type == JoinType::Right && can_remove_left {
565 return JoinRewrite::ReplaceWithRight;
566 }
567
568 if join.join_type != JoinType::Inner || join.on.is_empty() {
569 return JoinRewrite::Join(join.join_type);
570 }
571
572 if can_remove_right {
573 return JoinRewrite::Join(JoinType::LeftSemi);
574 }
575 if can_remove_left {
576 return JoinRewrite::Join(JoinType::RightSemi);
577 }
578
579 JoinRewrite::Join(JoinType::Inner)
580}
581
582fn add_join_condition_columns(
583 join: &Join,
584 left_live: &mut LiveColumns,
585 right_live: &mut LiveColumns,
586) -> Result<()> {
587 left_live.extend_from(join.on.iter().map(|(l, _)| l), join.left.schema())?;
588 right_live.extend_from(join.on.iter().map(|(_, r)| r), join.right.schema())?;
589
590 if let Some(filter) = &join.filter {
591 left_live.extend_from([filter], join.left.schema())?;
592 right_live.extend_from([filter], join.right.schema())?;
593 }
594
595 Ok(())
596}
597
598fn split_join_output_columns(
599 join: &Join,
600 live: &LiveColumns,
601) -> (LiveColumns, LiveColumns) {
602 let left_len = join.left.schema().fields().len();
603 match join.join_type {
604 JoinType::Inner | JoinType::Left | JoinType::Right | JoinType::Full => {
605 live.split_at(left_len)
606 }
607 JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
610 (live.clone(), LiveColumns::new())
611 }
612 JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
613 (LiveColumns::new(), live.clone())
614 }
615 }
616}
617
618fn side_unique_on_join<'a>(
619 schema: &DFSchema,
620 join_exprs: impl Iterator<Item = &'a Expr>,
621 null_equality: NullEquality,
622) -> bool {
623 let join_key_indices = join_exprs
624 .filter_map(|expr| match expr {
625 Expr::Alias(alias) => alias.expr.as_ref().try_as_col(),
626 _ => expr.try_as_col(),
627 })
628 .filter_map(|column| schema.maybe_index_of_column(column))
629 .collect::<Vec<usize>>();
630
631 schema.functional_dependencies().iter().any(|dependency| {
632 dependency.mode == Dependency::Single
633 && (!dependency.nullable || null_equality == NullEquality::NullEqualsNothing)
634 && dependency
635 .source_indices
636 .iter()
637 .all(|idx| join_key_indices.contains(idx))
638 })
639}
640
641#[cfg(test)]
642mod tests {
643 use crate::OptimizerContext;
644 use crate::assert_optimized_plan_eq_snapshot;
645 use crate::eliminate_join::EliminateJoin;
646 use arrow::datatypes::{DataType, Field, Schema};
647 use datafusion_common::{
648 Constraint, Constraints, NullEquality, Result, ScalarValue, SplitPoint,
649 };
650 use datafusion_expr::JoinType::Inner;
651 use datafusion_expr::{
652 Expr, JoinType, Partitioning, RangePartitioning, col, exists, lit,
653 logical_plan::builder::{
654 LogicalPlanBuilder, table_scan, table_source_with_constraints,
655 },
656 out_ref_col,
657 };
658 use datafusion_functions_aggregate::expr_fn::count;
659 use std::sync::Arc;
660
661 macro_rules! assert_optimized_plan_equal {
662 (
663 $plan:expr,
664 @ $expected:literal $(,)?
665 ) => {{
666 let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
667 let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(EliminateJoin::new())];
668 assert_optimized_plan_eq_snapshot!(
669 optimizer_ctx,
670 rules,
671 $plan,
672 @ $expected,
673 )
674 }};
675 }
676
677 #[test]
678 fn join_on_false() -> Result<()> {
679 let plan = LogicalPlanBuilder::empty(false)
680 .join_on(
681 LogicalPlanBuilder::empty(false).build()?,
682 Inner,
683 Some(lit(false)),
684 )?
685 .build()?;
686
687 assert_optimized_plan_equal!(plan, @"EmptyRelation: rows=0")
688 }
689
690 #[test]
691 fn inner_to_left_semi_when_removed_side_is_unique() -> Result<()> {
692 let plan = left_join_right_with_constraints(primary_key_on_id())?
693 .project(vec![col("l.x")])?
694 .build()?;
695
696 assert_optimized_plan_equal!(plan, @r"
697 Projection: l.x
698 LeftSemi Join: l.id = r.id
699 TableScan: l
700 TableScan: r
701 ")
702 }
703
704 #[test]
705 fn inner_to_left_semi_when_removed_side_is_unique_with_join_filter() -> Result<()> {
706 let right = scan("r", &test_schema(), primary_key_on_id())?;
707 let plan =
708 LogicalPlanBuilder::from(scan("l", &test_schema(), Constraints::default())?)
709 .join(
710 right,
711 Inner,
712 (vec!["l.id"], vec!["r.id"]),
713 Some(col("r.y").gt(col("l.x"))),
714 )?
715 .project(vec![col("l.x")])?
716 .build()?;
717
718 assert_optimized_plan_equal!(plan, @r"
719 Projection: l.x
720 LeftSemi Join: l.id = r.id Filter: r.y > l.x
721 TableScan: l
722 TableScan: r
723 ")
724 }
725
726 #[test]
727 fn inner_to_right_semi_when_removed_side_is_unique() -> Result<()> {
728 let plan = left_with_constraints_join_right(primary_key_on_id())?
729 .project(vec![col("r.y")])?
730 .build()?;
731
732 assert_optimized_plan_equal!(plan, @r"
733 Projection: r.y
734 RightSemi Join: l.id = r.id
735 TableScan: l
736 TableScan: r
737 ")
738 }
739
740 #[test]
741 fn inner_to_left_semi_for_duplicate_insensitive_parent() -> Result<()> {
742 let plan = left_join_right()?
743 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
744 .build()?;
745
746 assert_optimized_plan_equal!(plan, @r"
747 Aggregate: groupBy=[[l.x]], aggr=[[]]
748 LeftSemi Join: l.id = r.id
749 TableScan: l
750 TableScan: r
751 ")
752 }
753
754 #[test]
755 fn aggregate_with_aggregates_is_not_duplicate_insensitive() -> Result<()> {
756 let plan = left_join_right()?
761 .aggregate(vec![col("l.x")], vec![count(col("l.id"))])?
762 .build()?;
763
764 assert_optimized_plan_equal!(plan, @r"
765 Aggregate: groupBy=[[l.x]], aggr=[[count(l.id)]]
766 Inner Join: l.id = r.id
767 TableScan: l
768 TableScan: r
769 ")
770 }
771
772 #[test]
773 fn duplicate_insensitive_context_propagates_through_join_tree() -> Result<()> {
774 let left = scan("l", &test_schema(), Constraints::default())?;
775 let middle = scan("m", &test_schema(), Constraints::default())?;
776 let right = scan("r", &test_schema(), Constraints::default())?;
777
778 let left_join_middle = LogicalPlanBuilder::from(left)
779 .join(middle, Inner, (vec!["l.id"], vec!["m.id"]), None)?
780 .build()?;
781
782 let plan = LogicalPlanBuilder::from(left_join_middle)
783 .join(right, Inner, (vec!["l.id"], vec!["r.id"]), None)?
784 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
785 .build()?;
786
787 assert_optimized_plan_equal!(plan, @r"
788 Aggregate: groupBy=[[l.x]], aggr=[[]]
789 LeftSemi Join: l.id = r.id
790 LeftSemi Join: l.id = m.id
791 TableScan: l
792 TableScan: m
793 TableScan: r
794 ")
795 }
796
797 #[test]
798 fn projection_does_not_rewrite_without_uniqueness() -> Result<()> {
799 let plan = left_join_right()?.project(vec![col("l.x")])?.build()?;
800
801 assert_optimized_plan_equal!(plan, @r"
802 Projection: l.x
803 Inner Join: l.id = r.id
804 TableScan: l
805 TableScan: r
806 ")
807 }
808
809 #[test]
810 fn required_filter_column_prevents_duplicate_insensitive_rewrite() -> Result<()> {
811 let plan = left_join_right()?
812 .filter(col("r.y").gt(lit(10_i32)))?
813 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
814 .build()?;
815
816 assert_optimized_plan_equal!(plan, @r"
817 Aggregate: groupBy=[[l.x]], aggr=[[]]
818 Filter: r.y > Int32(10)
819 Inner Join: l.id = r.id
820 TableScan: l
821 TableScan: r
822 ")
823 }
824
825 #[test]
826 fn distinct_star_keeps_unreferenced_side() -> Result<()> {
827 let plan = left_join_right()?
835 .distinct()?
836 .project(vec![col("l.x")])?
837 .build()?;
838
839 assert_optimized_plan_equal!(plan, @r"
840 Projection: l.x
841 Distinct:
842 Inner Join: l.id = r.id
843 TableScan: l
844 TableScan: r
845 ")
846 }
847
848 #[test]
849 fn distinct_drops_unreferenced_side_when_projected() -> Result<()> {
850 let plan = left_join_right()?
855 .project(vec![col("l.x")])?
856 .distinct()?
857 .build()?;
858
859 assert_optimized_plan_equal!(plan, @r"
860 Distinct:
861 Projection: l.x
862 LeftSemi Join: l.id = r.id
863 TableScan: l
864 TableScan: r
865 ")
866 }
867
868 #[test]
869 fn correlated_subquery_outer_ref_prevents_rewrite() -> Result<()> {
870 let subquery =
876 LogicalPlanBuilder::from(scan("s", &test_schema(), Constraints::default())?)
877 .filter(col("s.id").eq(out_ref_col(DataType::Int32, "r.y")))?
878 .project(vec![lit(1)])?
879 .build()?;
880
881 let plan = left_join_right()?
882 .filter(exists(Arc::new(subquery)))?
883 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
884 .build()?;
885
886 assert_optimized_plan_equal!(plan, @r"
887 Aggregate: groupBy=[[l.x]], aggr=[[]]
888 Filter: EXISTS (<subquery>)
889 Subquery:
890 Projection: Int32(1)
891 Filter: s.id = outer_ref(r.y)
892 TableScan: s
893 Inner Join: l.id = r.id
894 TableScan: l
895 TableScan: r
896 ")
897 }
898
899 #[test]
900 fn inner_to_semi_inside_uncorrelated_subquery() -> Result<()> {
901 let subquery = left_join_right_with_constraints(primary_key_on_id())?
907 .project(vec![col("l.x")])?
908 .build()?;
909
910 let plan = LogicalPlanBuilder::from(scan(
911 "outer",
912 &test_schema(),
913 Constraints::default(),
914 )?)
915 .filter(exists(Arc::new(subquery)))?
916 .build()?;
917
918 assert_optimized_plan_equal!(plan, @r"
919 Filter: EXISTS (<subquery>)
920 Subquery:
921 Projection: l.x
922 LeftSemi Join: l.id = r.id
923 TableScan: l
924 TableScan: r
925 TableScan: outer
926 ")
927 }
928
929 #[test]
930 fn inner_to_semi_inside_correlated_subquery() -> Result<()> {
931 let subquery = left_join_right_with_constraints(primary_key_on_id())?
937 .filter(col("l.x").eq(out_ref_col(DataType::Int32, "outer.id")))?
938 .project(vec![col("l.x")])?
939 .build()?;
940
941 let plan = LogicalPlanBuilder::from(scan(
942 "outer",
943 &test_schema(),
944 Constraints::default(),
945 )?)
946 .filter(exists(Arc::new(subquery)))?
947 .build()?;
948
949 assert_optimized_plan_equal!(plan, @r"
950 Filter: EXISTS (<subquery>)
951 Subquery:
952 Projection: l.x
953 Filter: l.x = outer_ref(outer.id)
954 LeftSemi Join: l.id = r.id
955 TableScan: l
956 TableScan: r
957 TableScan: outer
958 ")
959 }
960
961 #[test]
962 fn nullable_unique_rewrites_under_null_equals_nothing() -> Result<()> {
963 let left = scan("l", &test_schema(), Constraints::default())?;
968 let right = scan("r", &test_schema(), unique_on_x())?;
969 let plan = LogicalPlanBuilder::from(left)
970 .join(right, Inner, (vec!["l.x"], vec!["r.x"]), None)?
971 .project(vec![col("l.id")])?
972 .build()?;
973
974 assert_optimized_plan_equal!(plan, @r"
975 Projection: l.id
976 LeftSemi Join: l.x = r.x
977 TableScan: l
978 TableScan: r
979 ")
980 }
981
982 #[test]
983 fn nullable_unique_does_not_rewrite_under_null_equals_null() -> Result<()> {
984 let left = scan("l", &test_schema(), Constraints::default())?;
990 let right = scan("r", &test_schema(), unique_on_x())?;
991 let plan = LogicalPlanBuilder::from(left)
992 .join_detailed(
993 right,
994 Inner,
995 (vec!["l.x"], vec!["r.x"]),
996 None,
997 NullEquality::NullEqualsNull,
998 )?
999 .project(vec![col("l.id")])?
1000 .build()?;
1001
1002 assert_optimized_plan_equal!(plan, @r"
1003 Projection: l.id
1004 Inner Join: l.x = r.x
1005 TableScan: l
1006 TableScan: r
1007 ")
1008 }
1009
1010 #[test]
1011 fn composite_unique_rewrites_when_join_covers_all_key_columns() -> Result<()> {
1012 let left = scan("l", &test_schema(), Constraints::default())?;
1016 let right = scan("r", &test_schema(), composite_primary_key_on_id_x())?;
1017 let plan = LogicalPlanBuilder::from(left)
1018 .join(
1019 right,
1020 Inner,
1021 (vec!["l.id", "l.x"], vec!["r.id", "r.x"]),
1022 None,
1023 )?
1024 .project(vec![col("l.y")])?
1025 .build()?;
1026
1027 assert_optimized_plan_equal!(plan, @r"
1028 Projection: l.y
1029 LeftSemi Join: l.id = r.id, l.x = r.x
1030 TableScan: l
1031 TableScan: r
1032 ")
1033 }
1034
1035 #[test]
1036 fn composite_unique_does_not_rewrite_when_join_misses_a_key_column() -> Result<()> {
1037 let left = scan("l", &test_schema(), Constraints::default())?;
1044 let right = scan("r", &test_schema(), composite_primary_key_on_id_x())?;
1045 let plan = LogicalPlanBuilder::from(left)
1046 .join(right, Inner, (vec!["l.id"], vec!["r.id"]), None)?
1047 .project(vec![col("l.y")])?
1048 .build()?;
1049
1050 assert_optimized_plan_equal!(plan, @r"
1051 Projection: l.y
1052 Inner Join: l.id = r.id
1053 TableScan: l
1054 TableScan: r
1055 ")
1056 }
1057
1058 #[test]
1059 fn top_n_sort_blocks_duplicate_insensitive_rewrite() -> Result<()> {
1060 let plan = left_join_right()?
1066 .sort_with_limit(vec![col("l.x").sort(true, false)], Some(5))?
1067 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
1068 .build()?;
1069
1070 assert_optimized_plan_equal!(plan, @r"
1071 Aggregate: groupBy=[[l.x]], aggr=[[]]
1072 Sort: l.x ASC NULLS LAST, fetch=5
1073 Inner Join: l.id = r.id
1074 TableScan: l
1075 TableScan: r
1076 ")
1077 }
1078
1079 #[test]
1080 fn sort_without_fetch_preserves_duplicate_insensitive_rewrite() -> Result<()> {
1081 let plan = left_join_right()?
1086 .sort(vec![col("l.x").sort(true, false)])?
1087 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
1088 .build()?;
1089
1090 assert_optimized_plan_equal!(plan, @r"
1091 Aggregate: groupBy=[[l.x]], aggr=[[]]
1092 Sort: l.x ASC NULLS LAST
1093 LeftSemi Join: l.id = r.id
1094 TableScan: l
1095 TableScan: r
1096 ")
1097 }
1098
1099 #[test]
1100 fn limit_blocks_duplicate_insensitive_rewrite() -> Result<()> {
1101 let plan = left_join_right()?
1106 .limit(0, Some(5))?
1107 .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
1108 .build()?;
1109
1110 assert_optimized_plan_equal!(plan, @r"
1111 Aggregate: groupBy=[[l.x]], aggr=[[]]
1112 Limit: skip=0, fetch=5
1113 Inner Join: l.id = r.id
1114 TableScan: l
1115 TableScan: r
1116 ")
1117 }
1118
1119 #[test]
1120 fn repartition_hash_key_keeps_removed_side_live() -> Result<()> {
1121 let plan = left_join_right_with_constraints(primary_key_on_id())?
1126 .repartition(Partitioning::Hash(vec![col("r.y")], 4))?
1127 .project(vec![col("l.x")])?
1128 .build()?;
1129
1130 assert_optimized_plan_equal!(plan, @r"
1131 Projection: l.x
1132 Repartition: Hash(r.y) partition_count=4
1133 Inner Join: l.id = r.id
1134 TableScan: l
1135 TableScan: r
1136 ")
1137 }
1138
1139 #[test]
1140 fn repartition_range_key_keeps_removed_side_live() -> Result<()> {
1141 let plan = left_join_right_with_constraints(primary_key_on_id())?
1146 .repartition(Partitioning::Range(RangePartitioning::try_new(
1147 vec![col("r.y").sort(true, true)],
1148 vec![SplitPoint::new(vec![ScalarValue::Int32(Some(10))])],
1149 )?))?
1150 .project(vec![col("l.x")])?
1151 .build()?;
1152
1153 assert_optimized_plan_equal!(plan, @r"
1154 Projection: l.x
1155 Repartition: Range([r.y ASC NULLS FIRST], [(10)], 2)
1156 Inner Join: l.id = r.id
1157 TableScan: l
1158 TableScan: r
1159 ")
1160 }
1161
1162 #[test]
1163 fn distinct_on_enables_semi_join_rewrite() -> Result<()> {
1164 let plan = left_join_right()?
1168 .distinct_on(vec![col("l.x")], vec![col("l.x")], None)?
1169 .build()?;
1170
1171 assert_optimized_plan_equal!(plan, @r"
1172 DistinctOn: on_expr=[[l.x]], select_expr=[[l.x]], sort_expr=[[]]
1173 LeftSemi Join: l.id = r.id
1174 TableScan: l
1175 TableScan: r
1176 ")
1177 }
1178
1179 #[test]
1180 fn existing_semi_join_passes_through_unchanged() -> Result<()> {
1181 let left = scan("l", &test_schema(), Constraints::default())?;
1185 let right = scan("r", &test_schema(), Constraints::default())?;
1186 let plan = LogicalPlanBuilder::from(left)
1187 .join(
1188 right,
1189 JoinType::LeftSemi,
1190 (vec!["l.id"], vec!["r.id"]),
1191 None,
1192 )?
1193 .project(vec![col("l.x")])?
1194 .build()?;
1195
1196 assert_optimized_plan_equal!(plan, @r"
1197 Projection: l.x
1198 LeftSemi Join: l.id = r.id
1199 TableScan: l
1200 TableScan: r
1201 ")
1202 }
1203
1204 fn left_join_right() -> Result<LogicalPlanBuilder> {
1205 left_join_right_with_constraints(Constraints::default())
1206 }
1207
1208 fn left_join_right_with_constraints(
1209 right_constraints: Constraints,
1210 ) -> Result<LogicalPlanBuilder> {
1211 let left = scan("l", &test_schema(), Constraints::default())?;
1212 let right = scan("r", &test_schema(), right_constraints)?;
1213
1214 LogicalPlanBuilder::from(left).join(
1215 right,
1216 Inner,
1217 (vec!["l.id"], vec!["r.id"]),
1218 None,
1219 )
1220 }
1221
1222 fn left_with_constraints_join_right(
1223 left_constraints: Constraints,
1224 ) -> Result<LogicalPlanBuilder> {
1225 let left = scan("l", &test_schema(), left_constraints)?;
1226 let right = scan("r", &test_schema(), Constraints::default())?;
1227
1228 LogicalPlanBuilder::from(left).join(
1229 right,
1230 Inner,
1231 (vec!["l.id"], vec!["r.id"]),
1232 None,
1233 )
1234 }
1235
1236 fn scan(
1237 name: &str,
1238 schema: &Schema,
1239 constraints: Constraints,
1240 ) -> Result<datafusion_expr::logical_plan::LogicalPlan> {
1241 if constraints.is_empty() {
1242 table_scan(Some(name), schema, None)?.build()
1243 } else {
1244 LogicalPlanBuilder::scan(
1245 name,
1246 table_source_with_constraints(schema, constraints),
1247 None,
1248 )?
1249 .build()
1250 }
1251 }
1252
1253 fn test_schema() -> Schema {
1254 Schema::new(vec![
1255 Field::new("id", DataType::Int32, false),
1256 Field::new("x", DataType::Int32, true),
1257 Field::new("y", DataType::Int32, true),
1258 ])
1259 }
1260
1261 fn primary_key_on_id() -> Constraints {
1262 Constraints::new_unverified(vec![Constraint::PrimaryKey(vec![0])])
1263 }
1264
1265 fn unique_on_x() -> Constraints {
1269 Constraints::new_unverified(vec![Constraint::Unique(vec![1])])
1270 }
1271
1272 fn composite_primary_key_on_id_x() -> Constraints {
1274 Constraints::new_unverified(vec![Constraint::PrimaryKey(vec![0, 1])])
1275 }
1276}