1use crate::push_down_filter::replace_cols_by_name;
22use crate::{OptimizerConfig, OptimizerRule};
23use datafusion_common::{Column, DFSchema, Result, qualified_name};
24use datafusion_expr::logical_plan::{Join, JoinType, LogicalPlan, Projection};
25use datafusion_expr::{Expr, Filter, Operator};
26
27use crate::optimizer::ApplyOrder;
28use datafusion_common::tree_node::Transformed;
29use datafusion_expr::expr::{BinaryExpr, Cast, InList, Like, TryCast};
30use std::collections::HashMap;
31use std::sync::Arc;
32
33#[derive(Default, Debug)]
79pub struct EliminateOuterJoin;
80
81impl EliminateOuterJoin {
82 #[expect(missing_docs)]
83 pub fn new() -> Self {
84 Self {}
85 }
86}
87
88impl OptimizerRule for EliminateOuterJoin {
90 fn name(&self) -> &str {
91 "eliminate_outer_join"
92 }
93
94 fn apply_order(&self) -> Option<ApplyOrder> {
95 Some(ApplyOrder::TopDown)
96 }
97
98 fn supports_rewrite(&self) -> bool {
99 true
100 }
101
102 fn rewrite(
103 &self,
104 plan: LogicalPlan,
105 _config: &dyn OptimizerConfig,
106 ) -> Result<Transformed<LogicalPlan>> {
107 let LogicalPlan::Filter(filter) = plan else {
108 return Ok(Transformed::no(plan));
109 };
110
111 let mut rewritten_predicate = filter.predicate.clone();
118 let mut projections: Vec<Projection> = Vec::new();
119 let mut cur = Arc::clone(&filter.input);
120
121 let new_join = loop {
122 match cur.as_ref() {
123 LogicalPlan::Projection(p) => {
124 rewritten_predicate =
125 inline_through_projection(rewritten_predicate, p)?;
126 let next = Arc::clone(&p.input);
127 projections.push(p.clone());
128 cur = next;
129 }
130 LogicalPlan::Join(join) => {
131 let Some(new_join) = try_simplify_join(join, &rewritten_predicate)
132 else {
133 return Ok(Transformed::no(LogicalPlan::Filter(filter)));
134 };
135 break new_join;
136 }
137 _ => {
138 return Ok(Transformed::no(LogicalPlan::Filter(filter)));
139 }
140 }
141 };
142
143 let rebuilt_inner = rewrap_projections(new_join, projections);
144 Filter::try_new(filter.predicate, Arc::new(rebuilt_inner))
145 .map(|f| Transformed::yes(LogicalPlan::Filter(f)))
146 }
147}
148
149fn try_simplify_join(join: &Join, predicate: &Expr) -> Option<LogicalPlan> {
154 if !join.join_type.is_outer() {
155 return None;
156 }
157
158 let null_rejecting_sides = extract_null_rejecting_sides(
159 predicate,
160 join.left.schema(),
161 join.right.schema(),
162 true,
163 );
164
165 let new_join_type = eliminate_outer(
166 join.join_type,
167 null_rejecting_sides.left,
168 null_rejecting_sides.right,
169 );
170 if new_join_type == join.join_type {
171 return None;
172 }
173
174 Some(LogicalPlan::Join(Join {
175 left: Arc::clone(&join.left),
176 right: Arc::clone(&join.right),
177 join_type: new_join_type,
178 join_constraint: join.join_constraint,
179 on: join.on.clone(),
180 filter: join.filter.clone(),
181 schema: Arc::clone(&join.schema),
182 null_equality: join.null_equality,
183 null_aware: join.null_aware,
184 }))
185}
186
187fn inline_through_projection(predicate: Expr, p: &Projection) -> Result<Expr> {
196 let mut map: HashMap<String, Expr> = HashMap::new();
197 for ((qualifier, field), expr) in p.schema.iter().zip(p.expr.iter()) {
198 map.insert(
199 qualified_name(qualifier, field.name()),
200 unalias(expr).clone(),
201 );
202 }
203 replace_cols_by_name(predicate, &map)
204}
205
206fn rewrap_projections(
212 new_inner: LogicalPlan,
213 projections: Vec<Projection>,
214) -> LogicalPlan {
215 let mut current = new_inner;
216 for mut p in projections.into_iter().rev() {
217 p.input = Arc::new(current);
218 current = LogicalPlan::Projection(p);
219 }
220 current
221}
222
223fn unalias(expr: &Expr) -> &Expr {
224 if let Expr::Alias(a) = expr {
225 unalias(&a.expr)
226 } else {
227 expr
228 }
229}
230
231pub fn eliminate_outer(
232 join_type: JoinType,
233 left_non_nullable: bool,
234 right_non_nullable: bool,
235) -> JoinType {
236 match (join_type, left_non_nullable, right_non_nullable) {
237 (JoinType::Left, _, true) => JoinType::Inner,
238 (JoinType::Right, true, _) => JoinType::Inner,
239 (JoinType::Full, true, true) => JoinType::Inner,
240 (JoinType::Full, true, false) => JoinType::Left,
241 (JoinType::Full, false, true) => JoinType::Right,
242 _ => join_type,
243 }
244}
245
246#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
247struct NullRejectingSides {
248 left: bool,
249 right: bool,
250}
251
252impl NullRejectingSides {
253 fn for_column(col: &Column, left_schema: &DFSchema, right_schema: &DFSchema) -> Self {
259 Self {
260 left: left_schema.has_column(col),
261 right: right_schema.has_column(col),
262 }
263 }
264
265 fn union(self, other: Self) -> Self {
266 Self {
267 left: self.left || other.left,
268 right: self.right || other.right,
269 }
270 }
271
272 fn intersection(self, other: Self) -> Self {
273 Self {
274 left: self.left && other.left,
275 right: self.right && other.right,
276 }
277 }
278}
279
280fn extract_null_rejecting_sides(
289 expr: &Expr,
290 left_schema: &Arc<DFSchema>,
291 right_schema: &Arc<DFSchema>,
292 top_level: bool,
293) -> NullRejectingSides {
294 match expr {
295 Expr::Column(col) => {
296 NullRejectingSides::for_column(col, left_schema, right_schema)
297 }
298 Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op {
299 Operator::And | Operator::Or => {
300 let left_sides = extract_null_rejecting_sides(
301 left,
302 left_schema,
303 right_schema,
304 top_level,
305 );
306 let right_sides = extract_null_rejecting_sides(
307 right,
308 left_schema,
309 right_schema,
310 top_level,
311 );
312
313 if top_level && *op == Operator::And {
318 left_sides.union(right_sides)
319 } else {
320 left_sides.intersection(right_sides)
323 }
324 }
325 op if op.returns_null_on_null() => {
328 let left_sides =
329 extract_null_rejecting_sides(left, left_schema, right_schema, false);
330 let right_sides =
331 extract_null_rejecting_sides(right, left_schema, right_schema, false);
332 left_sides.union(right_sides)
333 }
334 _ => NullRejectingSides::default(),
337 },
338 Expr::Not(arg) | Expr::Negative(arg) => {
339 extract_null_rejecting_sides(arg, left_schema, right_schema, false)
340 }
341 Expr::IsNotNull(arg)
346 | Expr::IsTrue(arg)
347 | Expr::IsFalse(arg)
348 | Expr::IsNotUnknown(arg) => {
349 if top_level {
350 extract_null_rejecting_sides(arg, left_schema, right_schema, false)
351 } else {
352 NullRejectingSides::default()
353 }
354 }
355 Expr::Cast(Cast { expr, field: _ })
356 | Expr::TryCast(TryCast { expr, field: _ }) => {
357 extract_null_rejecting_sides(expr, left_schema, right_schema, false)
358 }
359 Expr::InList(InList { expr, .. }) => {
362 extract_null_rejecting_sides(expr, left_schema, right_schema, false)
363 }
364 Expr::Between(between) => {
365 extract_null_rejecting_sides(&between.expr, left_schema, right_schema, false)
366 }
367 Expr::Like(Like { expr, pattern, .. }) => {
368 let expr_sides =
369 extract_null_rejecting_sides(expr, left_schema, right_schema, false);
370 let pattern_sides =
371 extract_null_rejecting_sides(pattern, left_schema, right_schema, false);
372 expr_sides.union(pattern_sides)
373 }
374 Expr::ScalarFunction(func) if func.func.is_strict() => func
378 .args
379 .iter()
380 .map(|arg| {
381 extract_null_rejecting_sides(arg, left_schema, right_schema, false)
382 })
383 .fold(NullRejectingSides::default(), NullRejectingSides::union),
384 _ => NullRejectingSides::default(),
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::OptimizerContext;
396 use crate::assert_optimized_plan_eq_snapshot;
397 use crate::test::*;
398 use arrow::datatypes::DataType;
399 use datafusion_common::ScalarValue;
400 use datafusion_expr::{
401 ColumnarValue,
402 Operator::{And, Or},
403 ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, Volatility, binary_expr,
404 cast, col, lit,
405 logical_plan::builder::LogicalPlanBuilder,
406 not, try_cast,
407 };
408
409 #[test]
410 fn null_rejecting_sides_union() {
411 let left_side = NullRejectingSides {
412 left: true,
413 right: false,
414 };
415 let right_side = NullRejectingSides {
416 left: false,
417 right: true,
418 };
419
420 assert_eq!(
421 left_side.union(right_side),
422 NullRejectingSides {
423 left: true,
424 right: true,
425 }
426 );
427 }
428
429 #[test]
430 fn null_rejecting_sides_intersection() {
431 let both_sides = NullRejectingSides {
432 left: true,
433 right: true,
434 };
435 let right_side = NullRejectingSides {
436 left: false,
437 right: true,
438 };
439
440 assert_eq!(
441 both_sides.intersection(right_side),
442 NullRejectingSides {
443 left: false,
444 right: true,
445 }
446 );
447 }
448
449 macro_rules! assert_optimized_plan_equal {
450 (
451 $plan:expr,
452 @ $expected:literal $(,)?
453 ) => {{
454 let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
455 let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(EliminateOuterJoin::new())];
456 assert_optimized_plan_eq_snapshot!(
457 optimizer_ctx,
458 rules,
459 $plan,
460 @ $expected,
461 )
462 }};
463 }
464
465 #[derive(Debug, PartialEq, Eq, Hash)]
466 struct TestUdf {
467 name: &'static str,
468 signature: Signature,
469 strict: bool,
470 }
471
472 impl TestUdf {
473 fn new(name: &'static str, strict: bool) -> Self {
474 Self {
475 name,
476 signature: Signature::uniform(
477 1,
478 vec![DataType::UInt32],
479 Volatility::Immutable,
480 ),
481 strict,
482 }
483 }
484 }
485
486 impl ScalarUDFImpl for TestUdf {
487 fn name(&self) -> &str {
488 self.name
489 }
490
491 fn signature(&self) -> &Signature {
492 &self.signature
493 }
494
495 fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
496 Ok(DataType::UInt32)
497 }
498
499 fn is_strict(&self) -> bool {
500 self.strict
501 }
502
503 fn invoke_with_args(&self, _args: ScalarFunctionArgs) -> Result<ColumnarValue> {
504 unimplemented!()
505 }
506 }
507
508 fn strict_udf(arg: Expr) -> Expr {
509 ScalarUDF::from(TestUdf::new("strict_test", true)).call(vec![arg])
510 }
511
512 fn non_strict_udf(arg: Expr) -> Expr {
513 ScalarUDF::from(TestUdf::new("non_strict_test", false)).call(vec![arg])
514 }
515
516 #[test]
517 fn eliminate_left_with_null() -> Result<()> {
518 let t1 = test_table_scan_with_name("t1")?;
519 let t2 = test_table_scan_with_name("t2")?;
520
521 let plan = LogicalPlanBuilder::from(t1)
523 .join(
524 t2,
525 JoinType::Left,
526 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
527 None,
528 )?
529 .filter(col("t2.b").is_null())?
530 .build()?;
531
532 assert_optimized_plan_equal!(plan, @r"
533 Filter: t2.b IS NULL
534 Left Join: t1.a = t2.a
535 TableScan: t1
536 TableScan: t2
537 ")
538 }
539
540 #[test]
541 fn eliminate_left_with_not_null() -> Result<()> {
542 let t1 = test_table_scan_with_name("t1")?;
543 let t2 = test_table_scan_with_name("t2")?;
544
545 let plan = LogicalPlanBuilder::from(t1)
547 .join(
548 t2,
549 JoinType::Left,
550 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
551 None,
552 )?
553 .filter(col("t2.b").is_not_null())?
554 .build()?;
555
556 assert_optimized_plan_equal!(plan, @r"
557 Filter: t2.b IS NOT NULL
558 Inner Join: t1.a = t2.a
559 TableScan: t1
560 TableScan: t2
561 ")
562 }
563
564 #[test]
565 fn eliminate_left_with_strict_function() -> Result<()> {
566 let t1 = test_table_scan_with_name("t1")?;
567 let t2 = test_table_scan_with_name("t2")?;
568
569 let plan = LogicalPlanBuilder::from(t1)
570 .join(
571 t2,
572 JoinType::Left,
573 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
574 None,
575 )?
576 .filter(strict_udf(col("t2.b")).gt(lit(5u32)))?
577 .build()?;
578
579 assert_optimized_plan_equal!(plan, @r"
580 Filter: strict_test(t2.b) > UInt32(5)
581 Inner Join: t1.a = t2.a
582 TableScan: t1
583 TableScan: t2
584 ")
585 }
586
587 #[test]
588 fn no_eliminate_left_with_non_strict_function() -> Result<()> {
589 let t1 = test_table_scan_with_name("t1")?;
590 let t2 = test_table_scan_with_name("t2")?;
591
592 let plan = LogicalPlanBuilder::from(t1)
593 .join(
594 t2,
595 JoinType::Left,
596 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
597 None,
598 )?
599 .filter(non_strict_udf(col("t2.b")).gt(lit(5u32)))?
600 .build()?;
601
602 assert_optimized_plan_equal!(plan, @r"
603 Filter: non_strict_test(t2.b) > UInt32(5)
604 Left Join: t1.a = t2.a
605 TableScan: t1
606 TableScan: t2
607 ")
608 }
609
610 #[test]
611 fn eliminate_left_with_nested_strict_is_not_null() -> Result<()> {
612 let t1 = test_table_scan_with_name("t1")?;
613 let t2 = test_table_scan_with_name("t2")?;
614
615 let plan = LogicalPlanBuilder::from(t1)
616 .join(
617 t2,
618 JoinType::Left,
619 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
620 None,
621 )?
622 .filter(strict_udf(strict_udf(col("t2.b"))).is_not_null())?
623 .build()?;
624
625 assert_optimized_plan_equal!(plan, @r"
626 Filter: strict_test(strict_test(t2.b)) IS NOT NULL
627 Inner Join: t1.a = t2.a
628 TableScan: t1
629 TableScan: t2
630 ")
631 }
632
633 #[test]
634 fn no_eliminate_left_with_strict_function_is_null() -> Result<()> {
635 let t1 = test_table_scan_with_name("t1")?;
636 let t2 = test_table_scan_with_name("t2")?;
637
638 let plan = LogicalPlanBuilder::from(t1)
639 .join(
640 t2,
641 JoinType::Left,
642 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
643 None,
644 )?
645 .filter(strict_udf(col("t2.b")).is_null())?
646 .build()?;
647
648 assert_optimized_plan_equal!(plan, @r"
649 Filter: strict_test(t2.b) IS NULL
650 Left Join: t1.a = t2.a
651 TableScan: t1
652 TableScan: t2
653 ")
654 }
655
656 #[test]
657 fn eliminate_right_with_or() -> Result<()> {
658 let t1 = test_table_scan_with_name("t1")?;
659 let t2 = test_table_scan_with_name("t2")?;
660
661 let plan = LogicalPlanBuilder::from(t1)
663 .join(
664 t2,
665 JoinType::Right,
666 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
667 None,
668 )?
669 .filter(binary_expr(
670 col("t1.b").gt(lit(10u32)),
671 Or,
672 col("t1.c").lt(lit(20u32)),
673 ))?
674 .build()?;
675
676 assert_optimized_plan_equal!(plan, @r"
677 Filter: t1.b > UInt32(10) OR t1.c < UInt32(20)
678 Inner Join: t1.a = t2.a
679 TableScan: t1
680 TableScan: t2
681 ")
682 }
683
684 #[test]
685 fn eliminate_full_with_and() -> Result<()> {
686 let t1 = test_table_scan_with_name("t1")?;
687 let t2 = test_table_scan_with_name("t2")?;
688
689 let plan = LogicalPlanBuilder::from(t1)
691 .join(
692 t2,
693 JoinType::Full,
694 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
695 None,
696 )?
697 .filter(binary_expr(
698 col("t1.b").gt(lit(10u32)),
699 And,
700 col("t2.c").lt(lit(20u32)),
701 ))?
702 .build()?;
703
704 assert_optimized_plan_equal!(plan, @r"
705 Filter: t1.b > UInt32(10) AND t2.c < UInt32(20)
706 Inner Join: t1.a = t2.a
707 TableScan: t1
708 TableScan: t2
709 ")
710 }
711
712 #[test]
713 fn eliminate_left_with_in_list() -> Result<()> {
714 let t1 = test_table_scan_with_name("t1")?;
715 let t2 = test_table_scan_with_name("t2")?;
716
717 let plan = LogicalPlanBuilder::from(t1)
720 .join(
721 t2,
722 JoinType::Left,
723 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
724 None,
725 )?
726 .filter(col("t2.b").in_list(vec![lit(1u32), lit(2u32), lit(3u32)], false))?
727 .build()?;
728
729 assert_optimized_plan_equal!(plan, @r"
730 Filter: t2.b IN ([UInt32(1), UInt32(2), UInt32(3)])
731 Inner Join: t1.a = t2.a
732 TableScan: t1
733 TableScan: t2
734 ")
735 }
736
737 #[test]
738 fn eliminate_left_with_in_list_containing_null() -> Result<()> {
739 let t1 = test_table_scan_with_name("t1")?;
740 let t2 = test_table_scan_with_name("t2")?;
741
742 let plan = LogicalPlanBuilder::from(t1)
745 .join(
746 t2,
747 JoinType::Left,
748 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
749 None,
750 )?
751 .filter(
752 col("t2.b")
753 .in_list(vec![lit(1u32), lit(ScalarValue::UInt32(None))], false),
754 )?
755 .build()?;
756
757 assert_optimized_plan_equal!(plan, @r"
758 Filter: t2.b IN ([UInt32(1), UInt32(NULL)])
759 Inner Join: t1.a = t2.a
760 TableScan: t1
761 TableScan: t2
762 ")
763 }
764
765 #[test]
766 fn eliminate_left_with_not_in_list() -> Result<()> {
767 let t1 = test_table_scan_with_name("t1")?;
768 let t2 = test_table_scan_with_name("t2")?;
769
770 let plan = LogicalPlanBuilder::from(t1)
773 .join(
774 t2,
775 JoinType::Left,
776 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
777 None,
778 )?
779 .filter(col("t2.b").in_list(vec![lit(1u32), lit(2u32)], true))?
780 .build()?;
781
782 assert_optimized_plan_equal!(plan, @r"
783 Filter: t2.b NOT IN ([UInt32(1), UInt32(2)])
784 Inner Join: t1.a = t2.a
785 TableScan: t1
786 TableScan: t2
787 ")
788 }
789
790 #[test]
791 fn eliminate_left_with_between() -> Result<()> {
792 let t1 = test_table_scan_with_name("t1")?;
793 let t2 = test_table_scan_with_name("t2")?;
794
795 let plan = LogicalPlanBuilder::from(t1)
798 .join(
799 t2,
800 JoinType::Left,
801 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
802 None,
803 )?
804 .filter(col("t2.b").between(lit(1u32), lit(10u32)))?
805 .build()?;
806
807 assert_optimized_plan_equal!(plan, @r"
808 Filter: t2.b BETWEEN UInt32(1) AND UInt32(10)
809 Inner Join: t1.a = t2.a
810 TableScan: t1
811 TableScan: t2
812 ")
813 }
814
815 #[test]
816 fn eliminate_right_with_between() -> Result<()> {
817 let t1 = test_table_scan_with_name("t1")?;
818 let t2 = test_table_scan_with_name("t2")?;
819
820 let plan = LogicalPlanBuilder::from(t1)
822 .join(
823 t2,
824 JoinType::Right,
825 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
826 None,
827 )?
828 .filter(col("t1.b").between(lit(1u32), lit(10u32)))?
829 .build()?;
830
831 assert_optimized_plan_equal!(plan, @r"
832 Filter: t1.b BETWEEN UInt32(1) AND UInt32(10)
833 Inner Join: t1.a = t2.a
834 TableScan: t1
835 TableScan: t2
836 ")
837 }
838
839 #[test]
840 fn eliminate_full_with_between() -> Result<()> {
841 let t1 = test_table_scan_with_name("t1")?;
842 let t2 = test_table_scan_with_name("t2")?;
843
844 let plan = LogicalPlanBuilder::from(t1)
846 .join(
847 t2,
848 JoinType::Full,
849 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
850 None,
851 )?
852 .filter(binary_expr(
853 col("t1.b").between(lit(1u32), lit(10u32)),
854 And,
855 col("t2.b").between(lit(5u32), lit(20u32)),
856 ))?
857 .build()?;
858
859 assert_optimized_plan_equal!(plan, @r"
860 Filter: t1.b BETWEEN UInt32(1) AND UInt32(10) AND t2.b BETWEEN UInt32(5) AND UInt32(20)
861 Inner Join: t1.a = t2.a
862 TableScan: t1
863 TableScan: t2
864 ")
865 }
866
867 #[test]
868 fn eliminate_full_with_in_list() -> Result<()> {
869 let t1 = test_table_scan_with_name("t1")?;
870 let t2 = test_table_scan_with_name("t2")?;
871
872 let plan = LogicalPlanBuilder::from(t1)
874 .join(
875 t2,
876 JoinType::Full,
877 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
878 None,
879 )?
880 .filter(binary_expr(
881 col("t1.b").in_list(vec![lit(1u32), lit(2u32)], false),
882 And,
883 col("t2.b").in_list(vec![lit(3u32), lit(4u32)], false),
884 ))?
885 .build()?;
886
887 assert_optimized_plan_equal!(plan, @r"
888 Filter: t1.b IN ([UInt32(1), UInt32(2)]) AND t2.b IN ([UInt32(3), UInt32(4)])
889 Inner Join: t1.a = t2.a
890 TableScan: t1
891 TableScan: t2
892 ")
893 }
894
895 #[test]
896 fn no_eliminate_left_with_in_list_or_is_null() -> Result<()> {
897 let t1 = test_table_scan_with_name("t1")?;
898 let t2 = test_table_scan_with_name("t2")?;
899
900 let plan = LogicalPlanBuilder::from(t1)
905 .join(
906 t2,
907 JoinType::Left,
908 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
909 None,
910 )?
911 .filter(binary_expr(
912 col("t2.b").in_list(vec![lit(1u32), lit(2u32)], false),
913 Or,
914 col("t2.b").is_null(),
915 ))?
916 .build()?;
917
918 assert_optimized_plan_equal!(plan, @r"
920 Filter: t2.b IN ([UInt32(1), UInt32(2)]) OR t2.b IS NULL
921 Left Join: t1.a = t2.a
922 TableScan: t1
923 TableScan: t2
924 ")
925 }
926
927 #[test]
928 fn eliminate_left_with_like() -> Result<()> {
929 let t1 = test_table_scan_with_name("t1")?;
930 let t2 = test_table_scan_with_name("t2")?;
931
932 let plan = LogicalPlanBuilder::from(t1)
934 .join(
935 t2,
936 JoinType::Left,
937 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
938 None,
939 )?
940 .filter(col("t2.b").like(lit("%pattern%")))?
941 .build()?;
942
943 assert_optimized_plan_equal!(plan, @r#"
944 Filter: t2.b LIKE Utf8("%pattern%")
945 Inner Join: t1.a = t2.a
946 TableScan: t1
947 TableScan: t2
948 "#)
949 }
950
951 #[test]
952 fn eliminate_left_with_like_pattern_column() -> Result<()> {
953 let t1 = test_table_scan_with_name("t1")?;
954 let t2 = test_table_scan_with_name("t2")?;
955
956 let plan = LogicalPlanBuilder::from(t1)
959 .join(
960 t2,
961 JoinType::Left,
962 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
963 None,
964 )?
965 .filter(lit("x").like(col("t2.b")))?
966 .build()?;
967
968 assert_optimized_plan_equal!(plan, @r#"
969 Filter: Utf8("x") LIKE t2.b
970 Inner Join: t1.a = t2.a
971 TableScan: t1
972 TableScan: t2
973 "#)
974 }
975
976 #[test]
977 fn eliminate_full_with_like_cross_side() -> Result<()> {
978 let t1 = test_table_scan_with_name("t1")?;
979 let t2 = test_table_scan_with_name("t2")?;
980
981 let plan = LogicalPlanBuilder::from(t1)
986 .join(
987 t2,
988 JoinType::Full,
989 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
990 None,
991 )?
992 .filter(col("t1.c").like(col("t2.b")))?
993 .build()?;
994
995 assert_optimized_plan_equal!(plan, @r"
996 Filter: t1.c LIKE t2.b
997 Inner Join: t1.a = t2.a
998 TableScan: t1
999 TableScan: t2
1000 ")
1001 }
1002
1003 #[test]
1004 fn eliminate_left_with_is_true() -> Result<()> {
1005 let t1 = test_table_scan_with_name("t1")?;
1006 let t2 = test_table_scan_with_name("t2")?;
1007
1008 let plan = LogicalPlanBuilder::from(t1)
1010 .join(
1011 t2,
1012 JoinType::Left,
1013 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1014 None,
1015 )?
1016 .filter(col("t2.b").gt(lit(10u32)).is_true())?
1017 .build()?;
1018
1019 assert_optimized_plan_equal!(plan, @r"
1020 Filter: t2.b > UInt32(10) IS TRUE
1021 Inner Join: t1.a = t2.a
1022 TableScan: t1
1023 TableScan: t2
1024 ")
1025 }
1026
1027 #[test]
1028 fn eliminate_left_with_is_false() -> Result<()> {
1029 let t1 = test_table_scan_with_name("t1")?;
1030 let t2 = test_table_scan_with_name("t2")?;
1031
1032 let plan = LogicalPlanBuilder::from(t1)
1034 .join(
1035 t2,
1036 JoinType::Left,
1037 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1038 None,
1039 )?
1040 .filter(col("t2.b").gt(lit(10u32)).is_false())?
1041 .build()?;
1042
1043 assert_optimized_plan_equal!(plan, @r"
1044 Filter: t2.b > UInt32(10) IS FALSE
1045 Inner Join: t1.a = t2.a
1046 TableScan: t1
1047 TableScan: t2
1048 ")
1049 }
1050
1051 #[test]
1052 fn eliminate_left_with_is_not_unknown() -> Result<()> {
1053 let t1 = test_table_scan_with_name("t1")?;
1054 let t2 = test_table_scan_with_name("t2")?;
1055
1056 let plan = LogicalPlanBuilder::from(t1)
1058 .join(
1059 t2,
1060 JoinType::Left,
1061 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1062 None,
1063 )?
1064 .filter(col("t2.b").gt(lit(10u32)).is_not_unknown())?
1065 .build()?;
1066
1067 assert_optimized_plan_equal!(plan, @r"
1068 Filter: t2.b > UInt32(10) IS NOT UNKNOWN
1069 Inner Join: t1.a = t2.a
1070 TableScan: t1
1071 TableScan: t2
1072 ")
1073 }
1074
1075 #[test]
1076 fn no_eliminate_left_with_is_not_true() -> Result<()> {
1077 let t1 = test_table_scan_with_name("t1")?;
1078 let t2 = test_table_scan_with_name("t2")?;
1079
1080 let plan = LogicalPlanBuilder::from(t1)
1083 .join(
1084 t2,
1085 JoinType::Left,
1086 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1087 None,
1088 )?
1089 .filter(col("t2.b").gt(lit(10u32)).is_not_true())?
1090 .build()?;
1091
1092 assert_optimized_plan_equal!(plan, @r"
1093 Filter: t2.b > UInt32(10) IS NOT TRUE
1094 Left Join: t1.a = t2.a
1095 TableScan: t1
1096 TableScan: t2
1097 ")
1098 }
1099
1100 #[test]
1101 fn no_eliminate_left_with_is_unknown() -> Result<()> {
1102 let t1 = test_table_scan_with_name("t1")?;
1103 let t2 = test_table_scan_with_name("t2")?;
1104
1105 let plan = LogicalPlanBuilder::from(t1)
1108 .join(
1109 t2,
1110 JoinType::Left,
1111 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1112 None,
1113 )?
1114 .filter(col("t2.b").gt(lit(10u32)).is_unknown())?
1115 .build()?;
1116
1117 assert_optimized_plan_equal!(plan, @r"
1118 Filter: t2.b > UInt32(10) IS UNKNOWN
1119 Left Join: t1.a = t2.a
1120 TableScan: t1
1121 TableScan: t2
1122 ")
1123 }
1124
1125 #[test]
1126 fn no_eliminate_left_with_not_is_true() -> Result<()> {
1127 let t1 = test_table_scan_with_name("t1")?;
1128 let t2 = test_table_scan_with_name("t2")?;
1129
1130 let plan = LogicalPlanBuilder::from(t1)
1135 .join(
1136 t2,
1137 JoinType::Left,
1138 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1139 None,
1140 )?
1141 .filter(not(col("t2.b").gt(lit(5u32)).is_true()))?
1142 .build()?;
1143
1144 assert_optimized_plan_equal!(plan, @r"
1145 Filter: NOT t2.b > UInt32(5) IS TRUE
1146 Left Join: t1.a = t2.a
1147 TableScan: t1
1148 TableScan: t2
1149 ")
1150 }
1151
1152 #[test]
1153 fn no_eliminate_left_with_not_is_false() -> Result<()> {
1154 let t1 = test_table_scan_with_name("t1")?;
1155 let t2 = test_table_scan_with_name("t2")?;
1156
1157 let plan = LogicalPlanBuilder::from(t1)
1160 .join(
1161 t2,
1162 JoinType::Left,
1163 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1164 None,
1165 )?
1166 .filter(not(col("t2.b").gt(lit(5u32)).is_false()))?
1167 .build()?;
1168
1169 assert_optimized_plan_equal!(plan, @r"
1170 Filter: NOT t2.b > UInt32(5) IS FALSE
1171 Left Join: t1.a = t2.a
1172 TableScan: t1
1173 TableScan: t2
1174 ")
1175 }
1176
1177 #[test]
1178 fn no_eliminate_left_with_not_is_not_unknown() -> Result<()> {
1179 let t1 = test_table_scan_with_name("t1")?;
1180 let t2 = test_table_scan_with_name("t2")?;
1181
1182 let plan = LogicalPlanBuilder::from(t1)
1185 .join(
1186 t2,
1187 JoinType::Left,
1188 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1189 None,
1190 )?
1191 .filter(not(col("t2.b").gt(lit(5u32)).is_not_unknown()))?
1192 .build()?;
1193
1194 assert_optimized_plan_equal!(plan, @r"
1195 Filter: NOT t2.b > UInt32(5) IS NOT UNKNOWN
1196 Left Join: t1.a = t2.a
1197 TableScan: t1
1198 TableScan: t2
1199 ")
1200 }
1201
1202 #[test]
1203 fn eliminate_full_with_type_cast() -> Result<()> {
1204 let t1 = test_table_scan_with_name("t1")?;
1205 let t2 = test_table_scan_with_name("t2")?;
1206
1207 let plan = LogicalPlanBuilder::from(t1)
1209 .join(
1210 t2,
1211 JoinType::Full,
1212 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1213 None,
1214 )?
1215 .filter(binary_expr(
1216 cast(col("t1.b"), DataType::Int64).gt(lit(10u32)),
1217 And,
1218 try_cast(col("t2.c"), DataType::Int64).lt(lit(20u32)),
1219 ))?
1220 .build()?;
1221
1222 assert_optimized_plan_equal!(plan, @r"
1223 Filter: CAST(t1.b AS Int64) > UInt32(10) AND TRY_CAST(t2.c AS Int64) < UInt32(20)
1224 Inner Join: t1.a = t2.a
1225 TableScan: t1
1226 TableScan: t2
1227 ")
1228 }
1229
1230 #[test]
1232 fn eliminate_full_to_left_with_left_filter() -> Result<()> {
1233 let t1 = test_table_scan_with_name("t1")?;
1234 let t2 = test_table_scan_with_name("t2")?;
1235
1236 let plan = LogicalPlanBuilder::from(t1)
1239 .join(
1240 t2,
1241 JoinType::Full,
1242 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1243 None,
1244 )?
1245 .filter(col("t1.b").gt(lit(10u32)))?
1246 .build()?;
1247
1248 assert_optimized_plan_equal!(plan, @r"
1249 Filter: t1.b > UInt32(10)
1250 Left Join: t1.a = t2.a
1251 TableScan: t1
1252 TableScan: t2
1253 ")
1254 }
1255
1256 #[test]
1257 fn eliminate_full_to_right_with_right_filter() -> Result<()> {
1258 let t1 = test_table_scan_with_name("t1")?;
1259 let t2 = test_table_scan_with_name("t2")?;
1260
1261 let plan = LogicalPlanBuilder::from(t1)
1263 .join(
1264 t2,
1265 JoinType::Full,
1266 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1267 None,
1268 )?
1269 .filter(col("t2.b").in_list(vec![lit(1u32), lit(2u32)], false))?
1270 .build()?;
1271
1272 assert_optimized_plan_equal!(plan, @r"
1273 Filter: t2.b IN ([UInt32(1), UInt32(2)])
1274 Right Join: t1.a = t2.a
1275 TableScan: t1
1276 TableScan: t2
1277 ")
1278 }
1279
1280 #[test]
1281 fn eliminate_full_to_left_with_like() -> Result<()> {
1282 let t1 = test_table_scan_with_name("t1")?;
1283 let t2 = test_table_scan_with_name("t2")?;
1284
1285 let plan = LogicalPlanBuilder::from(t1)
1287 .join(
1288 t2,
1289 JoinType::Full,
1290 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1291 None,
1292 )?
1293 .filter(col("t1.b").like(lit("%val%")))?
1294 .build()?;
1295
1296 assert_optimized_plan_equal!(plan, @r#"
1297 Filter: t1.b LIKE Utf8("%val%")
1298 Left Join: t1.a = t2.a
1299 TableScan: t1
1300 TableScan: t2
1301 "#)
1302 }
1303
1304 #[test]
1305 fn eliminate_full_to_right_with_is_true() -> Result<()> {
1306 let t1 = test_table_scan_with_name("t1")?;
1307 let t2 = test_table_scan_with_name("t2")?;
1308
1309 let plan = LogicalPlanBuilder::from(t1)
1311 .join(
1312 t2,
1313 JoinType::Full,
1314 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1315 None,
1316 )?
1317 .filter(col("t2.b").gt(lit(10u32)).is_true())?
1318 .build()?;
1319
1320 assert_optimized_plan_equal!(plan, @r"
1321 Filter: t2.b > UInt32(10) IS TRUE
1322 Right Join: t1.a = t2.a
1323 TableScan: t1
1324 TableScan: t2
1325 ")
1326 }
1327
1328 #[test]
1331 fn eliminate_left_with_and_multiple_null_rejecting() -> Result<()> {
1332 let t1 = test_table_scan_with_name("t1")?;
1333 let t2 = test_table_scan_with_name("t2")?;
1334
1335 let plan = LogicalPlanBuilder::from(t1)
1337 .join(
1338 t2,
1339 JoinType::Left,
1340 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1341 None,
1342 )?
1343 .filter(binary_expr(
1344 col("t2.b").in_list(vec![lit(1u32), lit(2u32)], false),
1345 And,
1346 col("t2.c").between(lit(5u32), lit(20u32)),
1347 ))?
1348 .build()?;
1349
1350 assert_optimized_plan_equal!(plan, @r"
1351 Filter: t2.b IN ([UInt32(1), UInt32(2)]) AND t2.c BETWEEN UInt32(5) AND UInt32(20)
1352 Inner Join: t1.a = t2.a
1353 TableScan: t1
1354 TableScan: t2
1355 ")
1356 }
1357
1358 #[test]
1359 fn eliminate_left_with_or_same_side() -> Result<()> {
1360 let t1 = test_table_scan_with_name("t1")?;
1361 let t2 = test_table_scan_with_name("t2")?;
1362
1363 let plan = LogicalPlanBuilder::from(t1)
1368 .join(
1369 t2,
1370 JoinType::Left,
1371 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1372 None,
1373 )?
1374 .filter(binary_expr(
1375 col("t2.b").gt(lit(10u32)),
1376 Or,
1377 col("t2.c").lt(lit(20u32)),
1378 ))?
1379 .build()?;
1380
1381 assert_optimized_plan_equal!(plan, @r"
1382 Filter: t2.b > UInt32(10) OR t2.c < UInt32(20)
1383 Inner Join: t1.a = t2.a
1384 TableScan: t1
1385 TableScan: t2
1386 ")
1387 }
1388
1389 #[test]
1390 fn no_eliminate_left_with_or_cross_side() -> Result<()> {
1391 let t1 = test_table_scan_with_name("t1")?;
1392 let t2 = test_table_scan_with_name("t2")?;
1393
1394 let plan = LogicalPlanBuilder::from(t1)
1398 .join(
1399 t2,
1400 JoinType::Left,
1401 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1402 None,
1403 )?
1404 .filter(binary_expr(
1405 col("t1.b").gt(lit(10u32)),
1406 Or,
1407 col("t2.b").lt(lit(20u32)),
1408 ))?
1409 .build()?;
1410
1411 assert_optimized_plan_equal!(plan, @r"
1412 Filter: t1.b > UInt32(10) OR t2.b < UInt32(20)
1413 Left Join: t1.a = t2.a
1414 TableScan: t1
1415 TableScan: t2
1416 ")
1417 }
1418
1419 #[test]
1422 fn eliminate_full_with_mixed_predicates() -> Result<()> {
1423 let t1 = test_table_scan_with_name("t1")?;
1424 let t2 = test_table_scan_with_name("t2")?;
1425
1426 let plan = LogicalPlanBuilder::from(t1)
1429 .join(
1430 t2,
1431 JoinType::Full,
1432 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1433 None,
1434 )?
1435 .filter(binary_expr(
1436 col("t1.b").like(lit("%pattern%")),
1437 And,
1438 col("t2.b").between(lit(1u32), lit(10u32)),
1439 ))?
1440 .build()?;
1441
1442 assert_optimized_plan_equal!(plan, @r#"
1443 Filter: t1.b LIKE Utf8("%pattern%") AND t2.b BETWEEN UInt32(1) AND UInt32(10)
1444 Inner Join: t1.a = t2.a
1445 TableScan: t1
1446 TableScan: t2
1447 "#)
1448 }
1449
1450 #[test]
1451 fn eliminate_left_with_is_true_and_in_list() -> Result<()> {
1452 let t1 = test_table_scan_with_name("t1")?;
1453 let t2 = test_table_scan_with_name("t2")?;
1454
1455 let plan = LogicalPlanBuilder::from(t1)
1457 .join(
1458 t2,
1459 JoinType::Left,
1460 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1461 None,
1462 )?
1463 .filter(binary_expr(
1464 col("t2.b").gt(lit(5u32)).is_true(),
1465 And,
1466 col("t2.c").in_list(vec![lit(1u32), lit(2u32)], false),
1467 ))?
1468 .build()?;
1469
1470 assert_optimized_plan_equal!(plan, @r"
1471 Filter: t2.b > UInt32(5) IS TRUE AND t2.c IN ([UInt32(1), UInt32(2)])
1472 Inner Join: t1.a = t2.a
1473 TableScan: t1
1474 TableScan: t2
1475 ")
1476 }
1477
1478 #[test]
1481 fn eliminate_left_through_projection() -> Result<()> {
1482 let t1 = test_table_scan_with_name("t1")?;
1483 let t2 = test_table_scan_with_name("t2")?;
1484
1485 let plan = LogicalPlanBuilder::from(t1)
1489 .join(
1490 t2,
1491 JoinType::Left,
1492 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1493 None,
1494 )?
1495 .project(vec![col("t1.a"), col("t2.b").alias("bb")])?
1496 .filter(col("bb").gt(lit(10u32)))?
1497 .build()?;
1498
1499 assert_optimized_plan_equal!(plan, @r"
1500 Filter: bb > UInt32(10)
1501 Projection: t1.a, t2.b AS bb
1502 Inner Join: t1.a = t2.a
1503 TableScan: t1
1504 TableScan: t2
1505 ")
1506 }
1507
1508 #[test]
1509 fn no_eliminate_left_through_projection_with_or_cross_side() -> Result<()> {
1510 let t1 = test_table_scan_with_name("t1")?;
1511 let t2 = test_table_scan_with_name("t2")?;
1512
1513 let plan = LogicalPlanBuilder::from(t1)
1517 .join(
1518 t2,
1519 JoinType::Left,
1520 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1521 None,
1522 )?
1523 .project(vec![col("t1.b").alias("x"), col("t2.b").alias("y")])?
1524 .filter(binary_expr(
1525 col("x").gt(lit(10u32)),
1526 Or,
1527 col("y").lt(lit(20u32)),
1528 ))?
1529 .build()?;
1530
1531 assert_optimized_plan_equal!(plan, @r"
1532 Filter: x > UInt32(10) OR y < UInt32(20)
1533 Projection: t1.b AS x, t2.b AS y
1534 Left Join: t1.a = t2.a
1535 TableScan: t1
1536 TableScan: t2
1537 ")
1538 }
1539
1540 #[test]
1541 fn no_eliminate_left_through_projection_with_only_left_filter() -> Result<()> {
1542 let t1 = test_table_scan_with_name("t1")?;
1543 let t2 = test_table_scan_with_name("t2")?;
1544
1545 let plan = LogicalPlanBuilder::from(t1)
1550 .join(
1551 t2,
1552 JoinType::Left,
1553 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1554 None,
1555 )?
1556 .project(vec![col("t1.b").alias("x"), col("t2.b")])?
1557 .filter(col("x").gt(lit(10u32)))?
1558 .build()?;
1559
1560 assert_optimized_plan_equal!(plan, @r"
1561 Filter: x > UInt32(10)
1562 Projection: t1.b AS x, t2.b
1563 Left Join: t1.a = t2.a
1564 TableScan: t1
1565 TableScan: t2
1566 ")
1567 }
1568
1569 #[test]
1570 fn eliminate_left_with_arithmetic_predicate() -> Result<()> {
1571 let t1 = test_table_scan_with_name("t1")?;
1572 let t2 = test_table_scan_with_name("t2")?;
1573
1574 let plan = LogicalPlanBuilder::from(t1)
1578 .join(
1579 t2,
1580 JoinType::Left,
1581 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1582 None,
1583 )?
1584 .filter(
1585 binary_expr(
1586 binary_expr(col("t2.b"), Operator::Multiply, lit(2u32)),
1587 Operator::Plus,
1588 lit(1u32),
1589 )
1590 .gt(lit(10u32)),
1591 )?
1592 .build()?;
1593
1594 assert_optimized_plan_equal!(plan, @r"
1595 Filter: t2.b * UInt32(2) + UInt32(1) > UInt32(10)
1596 Inner Join: t1.a = t2.a
1597 TableScan: t1
1598 TableScan: t2
1599 ")
1600 }
1601 #[test]
1602 fn eliminate_left_with_negative_predicate() -> Result<()> {
1603 let t1 = test_table_scan_with_name("t1")?;
1604 let t2 = test_table_scan_with_name("t2")?;
1605
1606 let plan = LogicalPlanBuilder::from(t1)
1609 .join(
1610 t2,
1611 JoinType::Left,
1612 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1613 None,
1614 )?
1615 .filter(Expr::Negative(Box::new(col("t2.b"))).gt(lit(0u32)))?
1616 .build()?;
1617
1618 assert_optimized_plan_equal!(plan, @r"
1619 Filter: (- t2.b) > UInt32(0)
1620 Inner Join: t1.a = t2.a
1621 TableScan: t1
1622 TableScan: t2
1623 ")
1624 }
1625
1626 #[test]
1627 fn no_eliminate_left_with_is_distinct_from() -> Result<()> {
1628 let t1 = test_table_scan_with_name("t1")?;
1629 let t2 = test_table_scan_with_name("t2")?;
1630
1631 let plan = LogicalPlanBuilder::from(t1)
1635 .join(
1636 t2,
1637 JoinType::Left,
1638 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1639 None,
1640 )?
1641 .filter(binary_expr(
1642 col("t2.b"),
1643 Operator::IsDistinctFrom,
1644 lit(5u32),
1645 ))?
1646 .build()?;
1647
1648 assert_optimized_plan_equal!(plan, @r"
1649 Filter: t2.b IS DISTINCT FROM UInt32(5)
1650 Left Join: t1.a = t2.a
1651 TableScan: t1
1652 TableScan: t2
1653 ")
1654 }
1655
1656 #[test]
1657 fn no_eliminate_left_with_is_not_distinct_from() -> Result<()> {
1658 let t1 = test_table_scan_with_name("t1")?;
1659 let t2 = test_table_scan_with_name("t2")?;
1660
1661 let plan = LogicalPlanBuilder::from(t1)
1665 .join(
1666 t2,
1667 JoinType::Left,
1668 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1669 None,
1670 )?
1671 .filter(binary_expr(
1672 col("t2.b"),
1673 Operator::IsNotDistinctFrom,
1674 lit(ScalarValue::UInt32(None)),
1675 ))?
1676 .build()?;
1677
1678 assert_optimized_plan_equal!(plan, @r"
1679 Filter: t2.b IS NOT DISTINCT FROM UInt32(NULL)
1680 Left Join: t1.a = t2.a
1681 TableScan: t1
1682 TableScan: t2
1683 ")
1684 }
1685
1686 #[test]
1687 fn no_eliminate_through_non_transparent() -> Result<()> {
1688 let t1 = test_table_scan_with_name("t1")?;
1689 let t2 = test_table_scan_with_name("t2")?;
1690
1691 let plan = LogicalPlanBuilder::from(t1)
1696 .join(
1697 t2,
1698 JoinType::Left,
1699 (vec![Column::from_name("a")], vec![Column::from_name("a")]),
1700 None,
1701 )?
1702 .limit(0, Some(5))?
1703 .filter(col("t2.b").gt(lit(10u32)))?
1704 .build()?;
1705
1706 assert_optimized_plan_equal!(plan, @r"
1707 Filter: t2.b > UInt32(10)
1708 Limit: skip=0, fetch=5
1709 Left Join: t1.a = t2.a
1710 TableScan: t1
1711 TableScan: t2
1712 ")
1713 }
1714}