1use crate::{OptimizerConfig, OptimizerRule};
20use datafusion_common::{Column, DFSchema, Result};
21use datafusion_expr::logical_plan::{Join, JoinType, LogicalPlan};
22use datafusion_expr::{Expr, Filter, Operator};
23
24use crate::optimizer::ApplyOrder;
25use datafusion_common::tree_node::Transformed;
26use datafusion_expr::expr::{BinaryExpr, Cast, InList, Like, TryCast};
27use std::sync::Arc;
28
29#[derive(Default, Debug)]
52pub struct EliminateOuterJoin;
53
54impl EliminateOuterJoin {
55 #[expect(missing_docs)]
56 pub fn new() -> Self {
57 Self {}
58 }
59}
60
61impl OptimizerRule for EliminateOuterJoin {
63 fn name(&self) -> &str {
64 "eliminate_outer_join"
65 }
66
67 fn apply_order(&self) -> Option<ApplyOrder> {
68 Some(ApplyOrder::TopDown)
69 }
70
71 fn supports_rewrite(&self) -> bool {
72 true
73 }
74
75 fn rewrite(
76 &self,
77 plan: LogicalPlan,
78 _config: &dyn OptimizerConfig,
79 ) -> Result<Transformed<LogicalPlan>> {
80 match plan {
81 LogicalPlan::Filter(mut filter) => match Arc::unwrap_or_clone(filter.input) {
82 LogicalPlan::Join(join) => {
83 let mut null_rejecting_cols: Vec<Column> = vec![];
84
85 extract_null_rejecting_columns(
86 &filter.predicate,
87 &mut null_rejecting_cols,
88 join.left.schema(),
89 join.right.schema(),
90 true,
91 );
92
93 let new_join_type = if join.join_type.is_outer() {
94 let mut left_non_nullable = false;
95 let mut right_non_nullable = false;
96 for col in null_rejecting_cols.iter() {
97 if join.left.schema().has_column(col) {
98 left_non_nullable = true;
99 }
100 if join.right.schema().has_column(col) {
101 right_non_nullable = true;
102 }
103 }
104 eliminate_outer(
105 join.join_type,
106 left_non_nullable,
107 right_non_nullable,
108 )
109 } else {
110 join.join_type
111 };
112
113 let new_join = Arc::new(LogicalPlan::Join(Join {
114 left: join.left,
115 right: join.right,
116 join_type: new_join_type,
117 join_constraint: join.join_constraint,
118 on: join.on.clone(),
119 filter: join.filter.clone(),
120 schema: Arc::clone(&join.schema),
121 null_equality: join.null_equality,
122 null_aware: join.null_aware,
123 }));
124 Filter::try_new(filter.predicate, new_join)
125 .map(|f| Transformed::yes(LogicalPlan::Filter(f)))
126 }
127 filter_input => {
128 filter.input = Arc::new(filter_input);
129 Ok(Transformed::no(LogicalPlan::Filter(filter)))
130 }
131 },
132 _ => Ok(Transformed::no(plan)),
133 }
134 }
135}
136
137pub fn eliminate_outer(
138 join_type: JoinType,
139 left_non_nullable: bool,
140 right_non_nullable: bool,
141) -> JoinType {
142 let mut new_join_type = join_type;
143 match join_type {
144 JoinType::Left if right_non_nullable => {
145 new_join_type = JoinType::Inner;
146 }
147 JoinType::Left => {}
148 JoinType::Right if left_non_nullable => {
149 new_join_type = JoinType::Inner;
150 }
151 JoinType::Right => {}
152 JoinType::Full => {
153 if left_non_nullable && right_non_nullable {
154 new_join_type = JoinType::Inner;
155 } else if left_non_nullable {
156 new_join_type = JoinType::Left;
157 } else if right_non_nullable {
158 new_join_type = JoinType::Right;
159 }
160 }
161 _ => {}
162 }
163 new_join_type
164}
165
166fn extract_null_rejecting_columns(
180 expr: &Expr,
181 null_rejecting_cols: &mut Vec<Column>,
182 left_schema: &Arc<DFSchema>,
183 right_schema: &Arc<DFSchema>,
184 top_level: bool,
185) {
186 match expr {
187 Expr::Column(col) => {
188 null_rejecting_cols.push(col.clone());
189 }
190 Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op {
191 Operator::And | Operator::Or => {
192 if top_level && *op == Operator::And {
199 extract_null_rejecting_columns(
200 left,
201 null_rejecting_cols,
202 left_schema,
203 right_schema,
204 top_level,
205 );
206 extract_null_rejecting_columns(
207 right,
208 null_rejecting_cols,
209 left_schema,
210 right_schema,
211 top_level,
212 );
213 return;
214 }
215
216 let mut left_cols: Vec<Column> = vec![];
222 let mut right_cols: Vec<Column> = vec![];
223 extract_null_rejecting_columns(
224 left,
225 &mut left_cols,
226 left_schema,
227 right_schema,
228 top_level,
229 );
230 extract_null_rejecting_columns(
231 right,
232 &mut right_cols,
233 left_schema,
234 right_schema,
235 top_level,
236 );
237
238 let find_on = |cols: &[Column], schema: &DFSchema| {
239 cols.iter().find(|c| schema.has_column(c)).cloned()
240 };
241 for schema in [left_schema, right_schema] {
242 if let (Some(c), Some(_)) =
243 (find_on(&left_cols, schema), find_on(&right_cols, schema))
244 {
245 null_rejecting_cols.push(c);
246 }
247 }
248 }
249 op if op.returns_null_on_null() => {
252 extract_null_rejecting_columns(
253 left,
254 null_rejecting_cols,
255 left_schema,
256 right_schema,
257 false,
258 );
259 extract_null_rejecting_columns(
260 right,
261 null_rejecting_cols,
262 left_schema,
263 right_schema,
264 false,
265 )
266 }
267 _ => {}
271 },
272 Expr::Not(arg) | Expr::Negative(arg) => extract_null_rejecting_columns(
273 arg,
274 null_rejecting_cols,
275 left_schema,
276 right_schema,
277 false,
278 ),
279 Expr::IsNotNull(arg)
285 | Expr::IsTrue(arg)
286 | Expr::IsFalse(arg)
287 | Expr::IsNotUnknown(arg) => {
288 if !top_level {
289 return;
290 }
291 extract_null_rejecting_columns(
292 arg,
293 null_rejecting_cols,
294 left_schema,
295 right_schema,
296 false,
297 )
298 }
299 Expr::Cast(Cast { expr, field: _ })
300 | Expr::TryCast(TryCast { expr, field: _ }) => extract_null_rejecting_columns(
301 expr,
302 null_rejecting_cols,
303 left_schema,
304 right_schema,
305 false,
306 ),
307 Expr::InList(InList { expr, .. }) => extract_null_rejecting_columns(
311 expr,
312 null_rejecting_cols,
313 left_schema,
314 right_schema,
315 false,
316 ),
317 Expr::Between(between) => extract_null_rejecting_columns(
318 &between.expr,
319 null_rejecting_cols,
320 left_schema,
321 right_schema,
322 false,
323 ),
324 Expr::Like(Like { expr, pattern, .. }) => {
325 extract_null_rejecting_columns(
326 expr,
327 null_rejecting_cols,
328 left_schema,
329 right_schema,
330 false,
331 );
332 extract_null_rejecting_columns(
333 pattern,
334 null_rejecting_cols,
335 left_schema,
336 right_schema,
337 false,
338 );
339 }
340 _ => {}
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::OptimizerContext;
357 use crate::assert_optimized_plan_eq_snapshot;
358 use crate::test::*;
359 use arrow::datatypes::DataType;
360 use datafusion_common::ScalarValue;
361 use datafusion_expr::{
362 Operator::{And, Or},
363 binary_expr, cast, col, lit,
364 logical_plan::builder::LogicalPlanBuilder,
365 not, try_cast,
366 };
367
368 macro_rules! assert_optimized_plan_equal {
369 (
370 $plan:expr,
371 @ $expected:literal $(,)?
372 ) => {{
373 let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
374 let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(EliminateOuterJoin::new())];
375 assert_optimized_plan_eq_snapshot!(
376 optimizer_ctx,
377 rules,
378 $plan,
379 @ $expected,
380 )
381 }};
382 }
383
384 #[test]
385 fn eliminate_left_with_null() -> Result<()> {
386 let t1 = test_table_scan_with_name("t1")?;
387 let t2 = test_table_scan_with_name("t2")?;
388
389 let plan = LogicalPlanBuilder::from(t1)
391 .join(
392 t2,
393 JoinType::Left,
394 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
395 None,
396 )?
397 .filter(col("t2.b").is_null())?
398 .build()?;
399
400 assert_optimized_plan_equal!(plan, @r"
401 Filter: t2.b IS NULL
402 Left Join: t1.a = t2.a
403 TableScan: t1
404 TableScan: t2
405 ")
406 }
407
408 #[test]
409 fn eliminate_left_with_not_null() -> Result<()> {
410 let t1 = test_table_scan_with_name("t1")?;
411 let t2 = test_table_scan_with_name("t2")?;
412
413 let plan = LogicalPlanBuilder::from(t1)
415 .join(
416 t2,
417 JoinType::Left,
418 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
419 None,
420 )?
421 .filter(col("t2.b").is_not_null())?
422 .build()?;
423
424 assert_optimized_plan_equal!(plan, @r"
425 Filter: t2.b IS NOT NULL
426 Inner Join: t1.a = t2.a
427 TableScan: t1
428 TableScan: t2
429 ")
430 }
431
432 #[test]
433 fn eliminate_right_with_or() -> Result<()> {
434 let t1 = test_table_scan_with_name("t1")?;
435 let t2 = test_table_scan_with_name("t2")?;
436
437 let plan = LogicalPlanBuilder::from(t1)
439 .join(
440 t2,
441 JoinType::Right,
442 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
443 None,
444 )?
445 .filter(binary_expr(
446 col("t1.b").gt(lit(10u32)),
447 Or,
448 col("t1.c").lt(lit(20u32)),
449 ))?
450 .build()?;
451
452 assert_optimized_plan_equal!(plan, @r"
453 Filter: t1.b > UInt32(10) OR t1.c < UInt32(20)
454 Inner Join: t1.a = t2.a
455 TableScan: t1
456 TableScan: t2
457 ")
458 }
459
460 #[test]
461 fn eliminate_full_with_and() -> Result<()> {
462 let t1 = test_table_scan_with_name("t1")?;
463 let t2 = test_table_scan_with_name("t2")?;
464
465 let plan = LogicalPlanBuilder::from(t1)
467 .join(
468 t2,
469 JoinType::Full,
470 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
471 None,
472 )?
473 .filter(binary_expr(
474 col("t1.b").gt(lit(10u32)),
475 And,
476 col("t2.c").lt(lit(20u32)),
477 ))?
478 .build()?;
479
480 assert_optimized_plan_equal!(plan, @r"
481 Filter: t1.b > UInt32(10) AND t2.c < UInt32(20)
482 Inner Join: t1.a = t2.a
483 TableScan: t1
484 TableScan: t2
485 ")
486 }
487
488 #[test]
489 fn eliminate_left_with_in_list() -> Result<()> {
490 let t1 = test_table_scan_with_name("t1")?;
491 let t2 = test_table_scan_with_name("t2")?;
492
493 let plan = LogicalPlanBuilder::from(t1)
496 .join(
497 t2,
498 JoinType::Left,
499 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
500 None,
501 )?
502 .filter(col("t2.b").in_list(vec![lit(1u32), lit(2u32), lit(3u32)], false))?
503 .build()?;
504
505 assert_optimized_plan_equal!(plan, @r"
506 Filter: t2.b IN ([UInt32(1), UInt32(2), UInt32(3)])
507 Inner Join: t1.a = t2.a
508 TableScan: t1
509 TableScan: t2
510 ")
511 }
512
513 #[test]
514 fn eliminate_left_with_in_list_containing_null() -> Result<()> {
515 let t1 = test_table_scan_with_name("t1")?;
516 let t2 = test_table_scan_with_name("t2")?;
517
518 let plan = LogicalPlanBuilder::from(t1)
521 .join(
522 t2,
523 JoinType::Left,
524 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
525 None,
526 )?
527 .filter(
528 col("t2.b")
529 .in_list(vec![lit(1u32), lit(ScalarValue::UInt32(None))], false),
530 )?
531 .build()?;
532
533 assert_optimized_plan_equal!(plan, @r"
534 Filter: t2.b IN ([UInt32(1), UInt32(NULL)])
535 Inner Join: t1.a = t2.a
536 TableScan: t1
537 TableScan: t2
538 ")
539 }
540
541 #[test]
542 fn eliminate_left_with_not_in_list() -> Result<()> {
543 let t1 = test_table_scan_with_name("t1")?;
544 let t2 = test_table_scan_with_name("t2")?;
545
546 let plan = LogicalPlanBuilder::from(t1)
549 .join(
550 t2,
551 JoinType::Left,
552 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
553 None,
554 )?
555 .filter(col("t2.b").in_list(vec![lit(1u32), lit(2u32)], true))?
556 .build()?;
557
558 assert_optimized_plan_equal!(plan, @r"
559 Filter: t2.b NOT IN ([UInt32(1), UInt32(2)])
560 Inner Join: t1.a = t2.a
561 TableScan: t1
562 TableScan: t2
563 ")
564 }
565
566 #[test]
567 fn eliminate_left_with_between() -> Result<()> {
568 let t1 = test_table_scan_with_name("t1")?;
569 let t2 = test_table_scan_with_name("t2")?;
570
571 let plan = LogicalPlanBuilder::from(t1)
574 .join(
575 t2,
576 JoinType::Left,
577 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
578 None,
579 )?
580 .filter(col("t2.b").between(lit(1u32), lit(10u32)))?
581 .build()?;
582
583 assert_optimized_plan_equal!(plan, @r"
584 Filter: t2.b BETWEEN UInt32(1) AND UInt32(10)
585 Inner Join: t1.a = t2.a
586 TableScan: t1
587 TableScan: t2
588 ")
589 }
590
591 #[test]
592 fn eliminate_right_with_between() -> Result<()> {
593 let t1 = test_table_scan_with_name("t1")?;
594 let t2 = test_table_scan_with_name("t2")?;
595
596 let plan = LogicalPlanBuilder::from(t1)
598 .join(
599 t2,
600 JoinType::Right,
601 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
602 None,
603 )?
604 .filter(col("t1.b").between(lit(1u32), lit(10u32)))?
605 .build()?;
606
607 assert_optimized_plan_equal!(plan, @r"
608 Filter: t1.b BETWEEN UInt32(1) AND UInt32(10)
609 Inner Join: t1.a = t2.a
610 TableScan: t1
611 TableScan: t2
612 ")
613 }
614
615 #[test]
616 fn eliminate_full_with_between() -> Result<()> {
617 let t1 = test_table_scan_with_name("t1")?;
618 let t2 = test_table_scan_with_name("t2")?;
619
620 let plan = LogicalPlanBuilder::from(t1)
622 .join(
623 t2,
624 JoinType::Full,
625 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
626 None,
627 )?
628 .filter(binary_expr(
629 col("t1.b").between(lit(1u32), lit(10u32)),
630 And,
631 col("t2.b").between(lit(5u32), lit(20u32)),
632 ))?
633 .build()?;
634
635 assert_optimized_plan_equal!(plan, @r"
636 Filter: t1.b BETWEEN UInt32(1) AND UInt32(10) AND t2.b BETWEEN UInt32(5) AND UInt32(20)
637 Inner Join: t1.a = t2.a
638 TableScan: t1
639 TableScan: t2
640 ")
641 }
642
643 #[test]
644 fn eliminate_full_with_in_list() -> Result<()> {
645 let t1 = test_table_scan_with_name("t1")?;
646 let t2 = test_table_scan_with_name("t2")?;
647
648 let plan = LogicalPlanBuilder::from(t1)
650 .join(
651 t2,
652 JoinType::Full,
653 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
654 None,
655 )?
656 .filter(binary_expr(
657 col("t1.b").in_list(vec![lit(1u32), lit(2u32)], false),
658 And,
659 col("t2.b").in_list(vec![lit(3u32), lit(4u32)], false),
660 ))?
661 .build()?;
662
663 assert_optimized_plan_equal!(plan, @r"
664 Filter: t1.b IN ([UInt32(1), UInt32(2)]) AND t2.b IN ([UInt32(3), UInt32(4)])
665 Inner Join: t1.a = t2.a
666 TableScan: t1
667 TableScan: t2
668 ")
669 }
670
671 #[test]
672 fn no_eliminate_left_with_in_list_or_is_null() -> Result<()> {
673 let t1 = test_table_scan_with_name("t1")?;
674 let t2 = test_table_scan_with_name("t2")?;
675
676 let plan = LogicalPlanBuilder::from(t1)
681 .join(
682 t2,
683 JoinType::Left,
684 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
685 None,
686 )?
687 .filter(binary_expr(
688 col("t2.b").in_list(vec![lit(1u32), lit(2u32)], false),
689 Or,
690 col("t2.b").is_null(),
691 ))?
692 .build()?;
693
694 assert_optimized_plan_equal!(plan, @r"
696 Filter: t2.b IN ([UInt32(1), UInt32(2)]) OR t2.b IS NULL
697 Left Join: t1.a = t2.a
698 TableScan: t1
699 TableScan: t2
700 ")
701 }
702
703 #[test]
704 fn eliminate_left_with_like() -> Result<()> {
705 let t1 = test_table_scan_with_name("t1")?;
706 let t2 = test_table_scan_with_name("t2")?;
707
708 let plan = LogicalPlanBuilder::from(t1)
710 .join(
711 t2,
712 JoinType::Left,
713 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
714 None,
715 )?
716 .filter(col("t2.b").like(lit("%pattern%")))?
717 .build()?;
718
719 assert_optimized_plan_equal!(plan, @r#"
720 Filter: t2.b LIKE Utf8("%pattern%")
721 Inner Join: t1.a = t2.a
722 TableScan: t1
723 TableScan: t2
724 "#)
725 }
726
727 #[test]
728 fn eliminate_left_with_like_pattern_column() -> Result<()> {
729 let t1 = test_table_scan_with_name("t1")?;
730 let t2 = test_table_scan_with_name("t2")?;
731
732 let plan = LogicalPlanBuilder::from(t1)
735 .join(
736 t2,
737 JoinType::Left,
738 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
739 None,
740 )?
741 .filter(lit("x").like(col("t2.b")))?
742 .build()?;
743
744 assert_optimized_plan_equal!(plan, @r#"
745 Filter: Utf8("x") LIKE t2.b
746 Inner Join: t1.a = t2.a
747 TableScan: t1
748 TableScan: t2
749 "#)
750 }
751
752 #[test]
753 fn eliminate_full_with_like_cross_side() -> Result<()> {
754 let t1 = test_table_scan_with_name("t1")?;
755 let t2 = test_table_scan_with_name("t2")?;
756
757 let plan = LogicalPlanBuilder::from(t1)
762 .join(
763 t2,
764 JoinType::Full,
765 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
766 None,
767 )?
768 .filter(col("t1.c").like(col("t2.b")))?
769 .build()?;
770
771 assert_optimized_plan_equal!(plan, @r"
772 Filter: t1.c LIKE t2.b
773 Inner Join: t1.a = t2.a
774 TableScan: t1
775 TableScan: t2
776 ")
777 }
778
779 #[test]
780 fn eliminate_left_with_is_true() -> Result<()> {
781 let t1 = test_table_scan_with_name("t1")?;
782 let t2 = test_table_scan_with_name("t2")?;
783
784 let plan = LogicalPlanBuilder::from(t1)
786 .join(
787 t2,
788 JoinType::Left,
789 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
790 None,
791 )?
792 .filter(col("t2.b").gt(lit(10u32)).is_true())?
793 .build()?;
794
795 assert_optimized_plan_equal!(plan, @r"
796 Filter: t2.b > UInt32(10) IS TRUE
797 Inner Join: t1.a = t2.a
798 TableScan: t1
799 TableScan: t2
800 ")
801 }
802
803 #[test]
804 fn eliminate_left_with_is_false() -> Result<()> {
805 let t1 = test_table_scan_with_name("t1")?;
806 let t2 = test_table_scan_with_name("t2")?;
807
808 let plan = LogicalPlanBuilder::from(t1)
810 .join(
811 t2,
812 JoinType::Left,
813 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
814 None,
815 )?
816 .filter(col("t2.b").gt(lit(10u32)).is_false())?
817 .build()?;
818
819 assert_optimized_plan_equal!(plan, @r"
820 Filter: t2.b > UInt32(10) IS FALSE
821 Inner Join: t1.a = t2.a
822 TableScan: t1
823 TableScan: t2
824 ")
825 }
826
827 #[test]
828 fn eliminate_left_with_is_not_unknown() -> Result<()> {
829 let t1 = test_table_scan_with_name("t1")?;
830 let t2 = test_table_scan_with_name("t2")?;
831
832 let plan = LogicalPlanBuilder::from(t1)
834 .join(
835 t2,
836 JoinType::Left,
837 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
838 None,
839 )?
840 .filter(col("t2.b").gt(lit(10u32)).is_not_unknown())?
841 .build()?;
842
843 assert_optimized_plan_equal!(plan, @r"
844 Filter: t2.b > UInt32(10) IS NOT UNKNOWN
845 Inner Join: t1.a = t2.a
846 TableScan: t1
847 TableScan: t2
848 ")
849 }
850
851 #[test]
852 fn no_eliminate_left_with_is_not_true() -> Result<()> {
853 let t1 = test_table_scan_with_name("t1")?;
854 let t2 = test_table_scan_with_name("t2")?;
855
856 let plan = LogicalPlanBuilder::from(t1)
859 .join(
860 t2,
861 JoinType::Left,
862 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
863 None,
864 )?
865 .filter(col("t2.b").gt(lit(10u32)).is_not_true())?
866 .build()?;
867
868 assert_optimized_plan_equal!(plan, @r"
869 Filter: t2.b > UInt32(10) IS NOT TRUE
870 Left Join: t1.a = t2.a
871 TableScan: t1
872 TableScan: t2
873 ")
874 }
875
876 #[test]
877 fn no_eliminate_left_with_is_unknown() -> Result<()> {
878 let t1 = test_table_scan_with_name("t1")?;
879 let t2 = test_table_scan_with_name("t2")?;
880
881 let plan = LogicalPlanBuilder::from(t1)
884 .join(
885 t2,
886 JoinType::Left,
887 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
888 None,
889 )?
890 .filter(col("t2.b").gt(lit(10u32)).is_unknown())?
891 .build()?;
892
893 assert_optimized_plan_equal!(plan, @r"
894 Filter: t2.b > UInt32(10) IS UNKNOWN
895 Left Join: t1.a = t2.a
896 TableScan: t1
897 TableScan: t2
898 ")
899 }
900
901 #[test]
902 fn no_eliminate_left_with_not_is_true() -> Result<()> {
903 let t1 = test_table_scan_with_name("t1")?;
904 let t2 = test_table_scan_with_name("t2")?;
905
906 let plan = LogicalPlanBuilder::from(t1)
911 .join(
912 t2,
913 JoinType::Left,
914 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
915 None,
916 )?
917 .filter(not(col("t2.b").gt(lit(5u32)).is_true()))?
918 .build()?;
919
920 assert_optimized_plan_equal!(plan, @r"
921 Filter: NOT t2.b > UInt32(5) IS TRUE
922 Left Join: t1.a = t2.a
923 TableScan: t1
924 TableScan: t2
925 ")
926 }
927
928 #[test]
929 fn no_eliminate_left_with_not_is_false() -> Result<()> {
930 let t1 = test_table_scan_with_name("t1")?;
931 let t2 = test_table_scan_with_name("t2")?;
932
933 let plan = LogicalPlanBuilder::from(t1)
936 .join(
937 t2,
938 JoinType::Left,
939 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
940 None,
941 )?
942 .filter(not(col("t2.b").gt(lit(5u32)).is_false()))?
943 .build()?;
944
945 assert_optimized_plan_equal!(plan, @r"
946 Filter: NOT t2.b > UInt32(5) IS FALSE
947 Left Join: t1.a = t2.a
948 TableScan: t1
949 TableScan: t2
950 ")
951 }
952
953 #[test]
954 fn no_eliminate_left_with_not_is_not_unknown() -> Result<()> {
955 let t1 = test_table_scan_with_name("t1")?;
956 let t2 = test_table_scan_with_name("t2")?;
957
958 let plan = LogicalPlanBuilder::from(t1)
961 .join(
962 t2,
963 JoinType::Left,
964 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
965 None,
966 )?
967 .filter(not(col("t2.b").gt(lit(5u32)).is_not_unknown()))?
968 .build()?;
969
970 assert_optimized_plan_equal!(plan, @r"
971 Filter: NOT t2.b > UInt32(5) IS NOT UNKNOWN
972 Left Join: t1.a = t2.a
973 TableScan: t1
974 TableScan: t2
975 ")
976 }
977
978 #[test]
979 fn eliminate_full_with_type_cast() -> Result<()> {
980 let t1 = test_table_scan_with_name("t1")?;
981 let t2 = test_table_scan_with_name("t2")?;
982
983 let plan = LogicalPlanBuilder::from(t1)
985 .join(
986 t2,
987 JoinType::Full,
988 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
989 None,
990 )?
991 .filter(binary_expr(
992 cast(col("t1.b"), DataType::Int64).gt(lit(10u32)),
993 And,
994 try_cast(col("t2.c"), DataType::Int64).lt(lit(20u32)),
995 ))?
996 .build()?;
997
998 assert_optimized_plan_equal!(plan, @r"
999 Filter: CAST(t1.b AS Int64) > UInt32(10) AND TRY_CAST(t2.c AS Int64) < UInt32(20)
1000 Inner Join: t1.a = t2.a
1001 TableScan: t1
1002 TableScan: t2
1003 ")
1004 }
1005
1006 #[test]
1008 fn eliminate_full_to_left_with_left_filter() -> Result<()> {
1009 let t1 = test_table_scan_with_name("t1")?;
1010 let t2 = test_table_scan_with_name("t2")?;
1011
1012 let plan = LogicalPlanBuilder::from(t1)
1015 .join(
1016 t2,
1017 JoinType::Full,
1018 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1019 None,
1020 )?
1021 .filter(col("t1.b").gt(lit(10u32)))?
1022 .build()?;
1023
1024 assert_optimized_plan_equal!(plan, @r"
1025 Filter: t1.b > UInt32(10)
1026 Left Join: t1.a = t2.a
1027 TableScan: t1
1028 TableScan: t2
1029 ")
1030 }
1031
1032 #[test]
1033 fn eliminate_full_to_right_with_right_filter() -> Result<()> {
1034 let t1 = test_table_scan_with_name("t1")?;
1035 let t2 = test_table_scan_with_name("t2")?;
1036
1037 let plan = LogicalPlanBuilder::from(t1)
1039 .join(
1040 t2,
1041 JoinType::Full,
1042 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1043 None,
1044 )?
1045 .filter(col("t2.b").in_list(vec![lit(1u32), lit(2u32)], false))?
1046 .build()?;
1047
1048 assert_optimized_plan_equal!(plan, @r"
1049 Filter: t2.b IN ([UInt32(1), UInt32(2)])
1050 Right Join: t1.a = t2.a
1051 TableScan: t1
1052 TableScan: t2
1053 ")
1054 }
1055
1056 #[test]
1057 fn eliminate_full_to_left_with_like() -> Result<()> {
1058 let t1 = test_table_scan_with_name("t1")?;
1059 let t2 = test_table_scan_with_name("t2")?;
1060
1061 let plan = LogicalPlanBuilder::from(t1)
1063 .join(
1064 t2,
1065 JoinType::Full,
1066 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1067 None,
1068 )?
1069 .filter(col("t1.b").like(lit("%val%")))?
1070 .build()?;
1071
1072 assert_optimized_plan_equal!(plan, @r#"
1073 Filter: t1.b LIKE Utf8("%val%")
1074 Left Join: t1.a = t2.a
1075 TableScan: t1
1076 TableScan: t2
1077 "#)
1078 }
1079
1080 #[test]
1081 fn eliminate_full_to_right_with_is_true() -> Result<()> {
1082 let t1 = test_table_scan_with_name("t1")?;
1083 let t2 = test_table_scan_with_name("t2")?;
1084
1085 let plan = LogicalPlanBuilder::from(t1)
1087 .join(
1088 t2,
1089 JoinType::Full,
1090 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1091 None,
1092 )?
1093 .filter(col("t2.b").gt(lit(10u32)).is_true())?
1094 .build()?;
1095
1096 assert_optimized_plan_equal!(plan, @r"
1097 Filter: t2.b > UInt32(10) IS TRUE
1098 Right Join: t1.a = t2.a
1099 TableScan: t1
1100 TableScan: t2
1101 ")
1102 }
1103
1104 #[test]
1107 fn eliminate_left_with_and_multiple_null_rejecting() -> Result<()> {
1108 let t1 = test_table_scan_with_name("t1")?;
1109 let t2 = test_table_scan_with_name("t2")?;
1110
1111 let plan = LogicalPlanBuilder::from(t1)
1113 .join(
1114 t2,
1115 JoinType::Left,
1116 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1117 None,
1118 )?
1119 .filter(binary_expr(
1120 col("t2.b").in_list(vec![lit(1u32), lit(2u32)], false),
1121 And,
1122 col("t2.c").between(lit(5u32), lit(20u32)),
1123 ))?
1124 .build()?;
1125
1126 assert_optimized_plan_equal!(plan, @r"
1127 Filter: t2.b IN ([UInt32(1), UInt32(2)]) AND t2.c BETWEEN UInt32(5) AND UInt32(20)
1128 Inner Join: t1.a = t2.a
1129 TableScan: t1
1130 TableScan: t2
1131 ")
1132 }
1133
1134 #[test]
1135 fn eliminate_left_with_or_same_side() -> Result<()> {
1136 let t1 = test_table_scan_with_name("t1")?;
1137 let t2 = test_table_scan_with_name("t2")?;
1138
1139 let plan = LogicalPlanBuilder::from(t1)
1144 .join(
1145 t2,
1146 JoinType::Left,
1147 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1148 None,
1149 )?
1150 .filter(binary_expr(
1151 col("t2.b").gt(lit(10u32)),
1152 Or,
1153 col("t2.c").lt(lit(20u32)),
1154 ))?
1155 .build()?;
1156
1157 assert_optimized_plan_equal!(plan, @r"
1158 Filter: t2.b > UInt32(10) OR t2.c < UInt32(20)
1159 Inner Join: t1.a = t2.a
1160 TableScan: t1
1161 TableScan: t2
1162 ")
1163 }
1164
1165 #[test]
1166 fn no_eliminate_left_with_or_cross_side() -> Result<()> {
1167 let t1 = test_table_scan_with_name("t1")?;
1168 let t2 = test_table_scan_with_name("t2")?;
1169
1170 let plan = LogicalPlanBuilder::from(t1)
1174 .join(
1175 t2,
1176 JoinType::Left,
1177 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1178 None,
1179 )?
1180 .filter(binary_expr(
1181 col("t1.b").gt(lit(10u32)),
1182 Or,
1183 col("t2.b").lt(lit(20u32)),
1184 ))?
1185 .build()?;
1186
1187 assert_optimized_plan_equal!(plan, @r"
1188 Filter: t1.b > UInt32(10) OR t2.b < UInt32(20)
1189 Left Join: t1.a = t2.a
1190 TableScan: t1
1191 TableScan: t2
1192 ")
1193 }
1194
1195 #[test]
1198 fn eliminate_full_with_mixed_predicates() -> Result<()> {
1199 let t1 = test_table_scan_with_name("t1")?;
1200 let t2 = test_table_scan_with_name("t2")?;
1201
1202 let plan = LogicalPlanBuilder::from(t1)
1205 .join(
1206 t2,
1207 JoinType::Full,
1208 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1209 None,
1210 )?
1211 .filter(binary_expr(
1212 col("t1.b").like(lit("%pattern%")),
1213 And,
1214 col("t2.b").between(lit(1u32), lit(10u32)),
1215 ))?
1216 .build()?;
1217
1218 assert_optimized_plan_equal!(plan, @r#"
1219 Filter: t1.b LIKE Utf8("%pattern%") AND t2.b BETWEEN UInt32(1) AND UInt32(10)
1220 Inner Join: t1.a = t2.a
1221 TableScan: t1
1222 TableScan: t2
1223 "#)
1224 }
1225
1226 #[test]
1227 fn eliminate_left_with_is_true_and_in_list() -> Result<()> {
1228 let t1 = test_table_scan_with_name("t1")?;
1229 let t2 = test_table_scan_with_name("t2")?;
1230
1231 let plan = LogicalPlanBuilder::from(t1)
1233 .join(
1234 t2,
1235 JoinType::Left,
1236 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1237 None,
1238 )?
1239 .filter(binary_expr(
1240 col("t2.b").gt(lit(5u32)).is_true(),
1241 And,
1242 col("t2.c").in_list(vec![lit(1u32), lit(2u32)], false),
1243 ))?
1244 .build()?;
1245
1246 assert_optimized_plan_equal!(plan, @r"
1247 Filter: t2.b > UInt32(5) IS TRUE AND t2.c IN ([UInt32(1), UInt32(2)])
1248 Inner Join: t1.a = t2.a
1249 TableScan: t1
1250 TableScan: t2
1251 ")
1252 }
1253
1254 #[test]
1255 fn eliminate_left_with_arithmetic_predicate() -> Result<()> {
1256 let t1 = test_table_scan_with_name("t1")?;
1257 let t2 = test_table_scan_with_name("t2")?;
1258
1259 let plan = LogicalPlanBuilder::from(t1)
1263 .join(
1264 t2,
1265 JoinType::Left,
1266 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1267 None,
1268 )?
1269 .filter(
1270 binary_expr(
1271 binary_expr(col("t2.b"), Operator::Multiply, lit(2u32)),
1272 Operator::Plus,
1273 lit(1u32),
1274 )
1275 .gt(lit(10u32)),
1276 )?
1277 .build()?;
1278
1279 assert_optimized_plan_equal!(plan, @r"
1280 Filter: t2.b * UInt32(2) + UInt32(1) > UInt32(10)
1281 Inner Join: t1.a = t2.a
1282 TableScan: t1
1283 TableScan: t2
1284 ")
1285 }
1286
1287 #[test]
1288 fn eliminate_left_with_negative_predicate() -> Result<()> {
1289 let t1 = test_table_scan_with_name("t1")?;
1290 let t2 = test_table_scan_with_name("t2")?;
1291
1292 let plan = LogicalPlanBuilder::from(t1)
1295 .join(
1296 t2,
1297 JoinType::Left,
1298 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1299 None,
1300 )?
1301 .filter(Expr::Negative(Box::new(col("t2.b"))).gt(lit(0u32)))?
1302 .build()?;
1303
1304 assert_optimized_plan_equal!(plan, @r"
1305 Filter: (- t2.b) > UInt32(0)
1306 Inner Join: t1.a = t2.a
1307 TableScan: t1
1308 TableScan: t2
1309 ")
1310 }
1311
1312 #[test]
1313 fn no_eliminate_left_with_is_distinct_from() -> Result<()> {
1314 let t1 = test_table_scan_with_name("t1")?;
1315 let t2 = test_table_scan_with_name("t2")?;
1316
1317 let plan = LogicalPlanBuilder::from(t1)
1321 .join(
1322 t2,
1323 JoinType::Left,
1324 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1325 None,
1326 )?
1327 .filter(binary_expr(
1328 col("t2.b"),
1329 Operator::IsDistinctFrom,
1330 lit(5u32),
1331 ))?
1332 .build()?;
1333
1334 assert_optimized_plan_equal!(plan, @r"
1335 Filter: t2.b IS DISTINCT FROM UInt32(5)
1336 Left Join: t1.a = t2.a
1337 TableScan: t1
1338 TableScan: t2
1339 ")
1340 }
1341
1342 #[test]
1343 fn no_eliminate_left_with_is_not_distinct_from() -> Result<()> {
1344 let t1 = test_table_scan_with_name("t1")?;
1345 let t2 = test_table_scan_with_name("t2")?;
1346
1347 let plan = LogicalPlanBuilder::from(t1)
1351 .join(
1352 t2,
1353 JoinType::Left,
1354 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1355 None,
1356 )?
1357 .filter(binary_expr(
1358 col("t2.b"),
1359 Operator::IsNotDistinctFrom,
1360 lit(ScalarValue::UInt32(None)),
1361 ))?
1362 .build()?;
1363
1364 assert_optimized_plan_equal!(plan, @r"
1365 Filter: t2.b IS NOT DISTINCT FROM UInt32(NULL)
1366 Left Join: t1.a = t2.a
1367 TableScan: t1
1368 TableScan: t2
1369 ")
1370 }
1371}