Skip to main content

datafusion_optimizer/
propagate_empty_relation.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! [`PropagateEmptyRelation`] eliminates nodes fed by `EmptyRelation`
19
20use std::sync::Arc;
21
22use datafusion_common::JoinType;
23use datafusion_common::tree_node::Transformed;
24use datafusion_common::{Column, DFSchemaRef, Result, ScalarValue, plan_err};
25use datafusion_expr::logical_plan::LogicalPlan;
26use datafusion_expr::{EmptyRelation, Expr, GroupingSet, Projection, Union, cast, lit};
27
28use crate::optimizer::ApplyOrder;
29use crate::{OptimizerConfig, OptimizerRule};
30
31/// Optimization rule that bottom-up to eliminate plan by propagating empty_relation.
32#[derive(Default, Debug)]
33pub struct PropagateEmptyRelation;
34
35impl PropagateEmptyRelation {
36    #[expect(missing_docs)]
37    pub fn new() -> Self {
38        Self {}
39    }
40}
41
42impl OptimizerRule for PropagateEmptyRelation {
43    fn name(&self) -> &str {
44        "propagate_empty_relation"
45    }
46
47    fn apply_order(&self) -> Option<ApplyOrder> {
48        Some(ApplyOrder::BottomUp)
49    }
50
51    fn supports_rewrite(&self) -> bool {
52        true
53    }
54
55    fn rewrite(
56        &self,
57        plan: LogicalPlan,
58        _config: &dyn OptimizerConfig,
59    ) -> Result<Transformed<LogicalPlan>> {
60        match plan {
61            LogicalPlan::EmptyRelation(_) => Ok(Transformed::no(plan)),
62            LogicalPlan::Projection(_)
63            | LogicalPlan::Filter(_)
64            | LogicalPlan::Window(_)
65            | LogicalPlan::Sort(_)
66            | LogicalPlan::SubqueryAlias(_)
67            | LogicalPlan::Repartition(_)
68            | LogicalPlan::Limit(_) => {
69                let empty = empty_child(&plan)?;
70                if let Some(empty_plan) = empty {
71                    return Ok(Transformed::yes(empty_plan));
72                }
73                Ok(Transformed::no(plan))
74            }
75            LogicalPlan::Join(ref join) => {
76                let (left_empty, right_empty) = binary_plan_children_is_empty(&plan)?;
77                let left_field_count = join.left.schema().fields().len();
78
79                match join.join_type {
80                    // For Full Join, only both sides are empty, the Join result is empty.
81                    JoinType::Full if left_empty && right_empty => Ok(Transformed::yes(
82                        LogicalPlan::EmptyRelation(EmptyRelation {
83                            produce_one_row: false,
84                            schema: Arc::clone(&join.schema),
85                        }),
86                    )),
87                    // For Full Join, if one side is empty, replace with a
88                    // Projection that null-pads the empty side's columns.
89                    JoinType::Full if right_empty => {
90                        Ok(Transformed::yes(build_null_padded_projection(
91                            Arc::clone(&join.left),
92                            &join.schema,
93                            left_field_count,
94                            true,
95                        )?))
96                    }
97                    JoinType::Full if left_empty => {
98                        Ok(Transformed::yes(build_null_padded_projection(
99                            Arc::clone(&join.right),
100                            &join.schema,
101                            left_field_count,
102                            false,
103                        )?))
104                    }
105                    JoinType::Inner if left_empty || right_empty => Ok(Transformed::yes(
106                        LogicalPlan::EmptyRelation(EmptyRelation {
107                            produce_one_row: false,
108                            schema: Arc::clone(&join.schema),
109                        }),
110                    )),
111                    JoinType::Left if left_empty => Ok(Transformed::yes(
112                        LogicalPlan::EmptyRelation(EmptyRelation {
113                            produce_one_row: false,
114                            schema: Arc::clone(&join.schema),
115                        }),
116                    )),
117                    // Left Join with empty right: all left rows survive
118                    // with NULLs for right columns.
119                    JoinType::Left if right_empty => {
120                        Ok(Transformed::yes(build_null_padded_projection(
121                            Arc::clone(&join.left),
122                            &join.schema,
123                            left_field_count,
124                            true,
125                        )?))
126                    }
127                    JoinType::Right if right_empty => Ok(Transformed::yes(
128                        LogicalPlan::EmptyRelation(EmptyRelation {
129                            produce_one_row: false,
130                            schema: Arc::clone(&join.schema),
131                        }),
132                    )),
133                    // Right Join with empty left: all right rows survive
134                    // with NULLs for left columns.
135                    JoinType::Right if left_empty => {
136                        Ok(Transformed::yes(build_null_padded_projection(
137                            Arc::clone(&join.right),
138                            &join.schema,
139                            left_field_count,
140                            false,
141                        )?))
142                    }
143                    JoinType::LeftSemi if left_empty || right_empty => Ok(
144                        Transformed::yes(LogicalPlan::EmptyRelation(EmptyRelation {
145                            produce_one_row: false,
146                            schema: Arc::clone(&join.schema),
147                        })),
148                    ),
149                    JoinType::RightSemi if left_empty || right_empty => Ok(
150                        Transformed::yes(LogicalPlan::EmptyRelation(EmptyRelation {
151                            produce_one_row: false,
152                            schema: Arc::clone(&join.schema),
153                        })),
154                    ),
155                    JoinType::LeftAnti if left_empty => Ok(Transformed::yes(
156                        LogicalPlan::EmptyRelation(EmptyRelation {
157                            produce_one_row: false,
158                            schema: Arc::clone(&join.schema),
159                        }),
160                    )),
161                    JoinType::LeftAnti if right_empty => {
162                        Ok(Transformed::yes((*join.left).clone()))
163                    }
164                    JoinType::RightAnti if left_empty => {
165                        Ok(Transformed::yes((*join.right).clone()))
166                    }
167                    JoinType::RightAnti if right_empty => Ok(Transformed::yes(
168                        LogicalPlan::EmptyRelation(EmptyRelation {
169                            produce_one_row: false,
170                            schema: Arc::clone(&join.schema),
171                        }),
172                    )),
173                    _ => Ok(Transformed::no(plan)),
174                }
175            }
176            LogicalPlan::Aggregate(ref agg) => {
177                // An aggregate over an empty input can be eliminated only when
178                // there is no empty grouping set.  An empty grouping set `()`
179                // (from `GROUPING SETS(())`, `ROLLUP(...)`, or `CUBE(...)`)
180                // always produces exactly one row even on empty input, so it
181                // must not be replaced by an empty relation.
182                if !agg.group_expr.is_empty()
183                    && !has_empty_grouping_set(&agg.group_expr)
184                    && let Some(empty_plan) = empty_child(&plan)?
185                {
186                    return Ok(Transformed::yes(empty_plan));
187                }
188                Ok(Transformed::no(LogicalPlan::Aggregate(agg.clone())))
189            }
190            LogicalPlan::Union(ref union) => {
191                let new_inputs = union
192                    .inputs
193                    .iter()
194                    .filter(|input| match &***input {
195                        LogicalPlan::EmptyRelation(empty) => empty.produce_one_row,
196                        _ => true,
197                    })
198                    .cloned()
199                    .collect::<Vec<_>>();
200
201                if new_inputs.len() == union.inputs.len() {
202                    Ok(Transformed::no(plan))
203                } else if new_inputs.is_empty() {
204                    Ok(Transformed::yes(LogicalPlan::EmptyRelation(
205                        EmptyRelation {
206                            produce_one_row: false,
207                            schema: Arc::clone(plan.schema()),
208                        },
209                    )))
210                } else if new_inputs.len() == 1 {
211                    let mut new_inputs = new_inputs;
212                    let input_plan = new_inputs.pop().unwrap(); // length checked
213                    let child = Arc::unwrap_or_clone(input_plan);
214                    if child.schema().eq(plan.schema()) {
215                        Ok(Transformed::yes(child))
216                    } else {
217                        Ok(Transformed::yes(LogicalPlan::Projection(
218                            Projection::new_from_schema(
219                                Arc::new(child),
220                                Arc::clone(plan.schema()),
221                            ),
222                        )))
223                    }
224                } else {
225                    Ok(Transformed::yes(LogicalPlan::Union(Union {
226                        inputs: new_inputs,
227                        schema: Arc::clone(&union.schema),
228                    })))
229                }
230            }
231
232            _ => Ok(Transformed::no(plan)),
233        }
234    }
235}
236
237fn binary_plan_children_is_empty(plan: &LogicalPlan) -> Result<(bool, bool)> {
238    match plan.inputs()[..] {
239        [left, right] => {
240            let left_empty = match left {
241                LogicalPlan::EmptyRelation(empty) => !empty.produce_one_row,
242                _ => false,
243            };
244            let right_empty = match right {
245                LogicalPlan::EmptyRelation(empty) => !empty.produce_one_row,
246                _ => false,
247            };
248            Ok((left_empty, right_empty))
249        }
250        _ => plan_err!("plan just can have two child"),
251    }
252}
253
254fn empty_child(plan: &LogicalPlan) -> Result<Option<LogicalPlan>> {
255    match plan.inputs()[..] {
256        [child] => match child {
257            LogicalPlan::EmptyRelation(empty) => {
258                if !empty.produce_one_row {
259                    Ok(Some(LogicalPlan::EmptyRelation(EmptyRelation {
260                        produce_one_row: false,
261                        schema: Arc::clone(plan.schema()),
262                    })))
263                } else {
264                    Ok(None)
265                }
266            }
267            _ => Ok(None),
268        },
269        _ => plan_err!("plan just can have one child"),
270    }
271}
272
273/// Builds a Projection that replaces one side of an outer join with NULL literals.
274///
275/// When one side of an outer join is an `EmptyRelation`, the join can be eliminated
276/// by projecting the surviving side's columns as-is and replacing the empty side's
277/// columns with `CAST(NULL AS <type>)`.
278///
279/// The join schema is used as the projection's output schema to preserve nullability
280/// guarantees (important for FULL JOIN where the surviving side's columns are marked
281/// nullable in the join schema even if they aren't in the source schema).
282///
283/// # Example
284///
285/// For a `LEFT JOIN` where the right side is empty:
286/// ```text
287/// Left Join (orders.id = returns.order_id)        Projection(orders.id, orders.amount,
288///   ├── TableScan: orders                   =>      CAST(NULL AS Int64) AS order_id,
289///   └── EmptyRelation                               CAST(NULL AS Utf8) AS reason)
290///                                                   └── TableScan: orders
291/// ```
292fn build_null_padded_projection(
293    surviving_plan: Arc<LogicalPlan>,
294    join_schema: &DFSchemaRef,
295    left_field_count: usize,
296    empty_side_is_right: bool,
297) -> Result<LogicalPlan> {
298    let exprs = join_schema
299        .iter()
300        .enumerate()
301        .map(|(i, (qualifier, field))| {
302            let on_empty_side = if empty_side_is_right {
303                i >= left_field_count
304            } else {
305                i < left_field_count
306            };
307
308            if on_empty_side {
309                cast(lit(ScalarValue::Null), field.data_type().clone())
310                    .alias_qualified(qualifier.cloned(), field.name())
311            } else {
312                Expr::Column(Column::new(qualifier.cloned(), field.name()))
313            }
314        })
315        .collect::<Vec<_>>();
316
317    Ok(LogicalPlan::Projection(Projection::try_new_with_schema(
318        exprs,
319        surviving_plan,
320        Arc::clone(join_schema),
321    )?))
322}
323
324/// Returns `true` if any grouping set in the list of GROUP BY expressions is
325/// the empty set `()`.
326///
327/// An empty grouping set acts as a "grand total" group: the aggregate must
328/// always produce **exactly one row** for it, even when the input is empty.
329/// This means an aggregate with an empty grouping set cannot be replaced by
330/// an empty relation.
331///
332/// The three forms that can contain an empty grouping set:
333/// - `GROUPING SETS (…, (), …)` — explicitly listed.
334/// - `ROLLUP(exprs)` — always expands to include `()`.
335/// - `CUBE(exprs)`  — always expands to include `()`.
336fn has_empty_grouping_set(group_expr: &[Expr]) -> bool {
337    match group_expr.first() {
338        Some(Expr::GroupingSet(GroupingSet::GroupingSets(groups))) => {
339            groups.iter().any(|g| g.is_empty())
340        }
341        // Both ROLLUP and CUBE always include the empty grouping set ().
342        Some(Expr::GroupingSet(GroupingSet::Rollup(_)))
343        | Some(Expr::GroupingSet(GroupingSet::Cube(_))) => true,
344        _ => false,
345    }
346}
347
348#[cfg(test)]
349mod tests {
350
351    use arrow::datatypes::{DataType, Field, Schema};
352
353    use datafusion_common::{Column, DFSchema};
354    use datafusion_expr::logical_plan::table_scan;
355    use datafusion_expr::{
356        Operator, binary_expr, col, lit, logical_plan::builder::LogicalPlanBuilder,
357    };
358
359    use crate::OptimizerContext;
360    use crate::assert_optimized_plan_eq_snapshot;
361    use crate::eliminate_filter::EliminateFilter;
362    use crate::optimize_unions::OptimizeUnions;
363    use crate::test::{
364        assert_optimized_plan_with_rules, test_table_scan, test_table_scan_fields,
365        test_table_scan_with_name,
366    };
367
368    use super::*;
369
370    macro_rules! assert_optimized_plan_equal {
371        (
372            $plan:expr,
373            @ $expected:literal $(,)?
374        ) => {{
375            let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
376            let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(PropagateEmptyRelation::new())];
377            assert_optimized_plan_eq_snapshot!(
378                optimizer_ctx,
379                rules,
380                $plan,
381                @ $expected,
382            )
383        }};
384    }
385
386    fn assert_together_optimized_plan(
387        plan: LogicalPlan,
388        expected: &str,
389        eq: bool,
390    ) -> Result<()> {
391        assert_optimized_plan_with_rules(
392            vec![
393                Arc::new(EliminateFilter::new()),
394                Arc::new(OptimizeUnions::new()),
395                Arc::new(PropagateEmptyRelation::new()),
396            ],
397            plan,
398            expected,
399            eq,
400        )
401    }
402
403    #[test]
404    fn propagate_empty() -> Result<()> {
405        let plan = LogicalPlanBuilder::empty(false)
406            .filter(lit(true))?
407            .limit(10, None)?
408            .project(vec![binary_expr(lit(1), Operator::Plus, lit(1))])?
409            .build()?;
410
411        assert_optimized_plan_equal!(plan, @"EmptyRelation: rows=0")
412    }
413
414    #[test]
415    fn cooperate_with_eliminate_filter() -> Result<()> {
416        let table_scan = test_table_scan()?;
417        let left = LogicalPlanBuilder::from(table_scan).build()?;
418        let right_table_scan = test_table_scan_with_name("test2")?;
419        let right = LogicalPlanBuilder::from(right_table_scan)
420            .project(vec![col("a")])?
421            .filter(lit(false))?
422            .build()?;
423
424        let plan = LogicalPlanBuilder::from(left)
425            .join_using(
426                right,
427                JoinType::Inner,
428                vec![Column::from_name("a".to_string())],
429            )?
430            .filter(col("a").lt_eq(lit(1i64)))?
431            .build()?;
432
433        let expected = "EmptyRelation: rows=0";
434        assert_together_optimized_plan(plan, expected, true)
435    }
436
437    #[test]
438    fn propagate_union_empty() -> Result<()> {
439        let left = LogicalPlanBuilder::from(test_table_scan()?).build()?;
440        let right = LogicalPlanBuilder::from(test_table_scan_with_name("test2")?)
441            .filter(lit(false))?
442            .build()?;
443
444        let plan = LogicalPlanBuilder::from(left).union(right)?.build()?;
445
446        let expected = "Projection: a, b, c\n  TableScan: test";
447        assert_together_optimized_plan(plan, expected, true)
448    }
449
450    #[test]
451    fn propagate_union_multi_empty() -> Result<()> {
452        let one =
453            LogicalPlanBuilder::from(test_table_scan_with_name("test1")?).build()?;
454        let two = LogicalPlanBuilder::from(test_table_scan_with_name("test2")?)
455            .filter(lit(false))?
456            .build()?;
457        let three = LogicalPlanBuilder::from(test_table_scan_with_name("test3")?)
458            .filter(lit(false))?
459            .build()?;
460        let four =
461            LogicalPlanBuilder::from(test_table_scan_with_name("test4")?).build()?;
462
463        let plan = LogicalPlanBuilder::from(one)
464            .union(two)?
465            .union(three)?
466            .union(four)?
467            .build()?;
468
469        let expected = "Union\
470            \n  TableScan: test1\
471            \n  TableScan: test4";
472        assert_together_optimized_plan(plan, expected, true)
473    }
474
475    #[test]
476    fn propagate_union_all_empty() -> Result<()> {
477        let one = LogicalPlanBuilder::from(test_table_scan_with_name("test1")?)
478            .filter(lit(false))?
479            .build()?;
480        let two = LogicalPlanBuilder::from(test_table_scan_with_name("test2")?)
481            .filter(lit(false))?
482            .build()?;
483        let three = LogicalPlanBuilder::from(test_table_scan_with_name("test3")?)
484            .filter(lit(false))?
485            .build()?;
486        let four = LogicalPlanBuilder::from(test_table_scan_with_name("test4")?)
487            .filter(lit(false))?
488            .build()?;
489
490        let plan = LogicalPlanBuilder::from(one)
491            .union(two)?
492            .union(three)?
493            .union(four)?
494            .build()?;
495
496        let expected = "EmptyRelation: rows=0";
497        assert_together_optimized_plan(plan, expected, true)
498    }
499
500    #[test]
501    fn propagate_union_children_different_schema() -> Result<()> {
502        let one_schema = Schema::new(vec![Field::new("t1a", DataType::UInt32, false)]);
503        let t1_scan = table_scan(Some("test1"), &one_schema, None)?.build()?;
504        let one = LogicalPlanBuilder::from(t1_scan)
505            .filter(lit(false))?
506            .build()?;
507
508        let two_schema = Schema::new(vec![Field::new("t2a", DataType::UInt32, false)]);
509        let t2_scan = table_scan(Some("test2"), &two_schema, None)?.build()?;
510        let two = LogicalPlanBuilder::from(t2_scan).build()?;
511
512        let three_schema = Schema::new(vec![Field::new("t3a", DataType::UInt32, false)]);
513        let t3_scan = table_scan(Some("test3"), &three_schema, None)?.build()?;
514        let three = LogicalPlanBuilder::from(t3_scan).build()?;
515
516        let plan = LogicalPlanBuilder::from(one)
517            .union(two)?
518            .union(three)?
519            .build()?;
520
521        let expected = "Union\
522            \n  TableScan: test2\
523            \n  TableScan: test3";
524        assert_together_optimized_plan(plan, expected, true)
525    }
526
527    #[test]
528    fn propagate_union_alias() -> Result<()> {
529        let left = LogicalPlanBuilder::from(test_table_scan()?).build()?;
530        let right = LogicalPlanBuilder::from(test_table_scan_with_name("test2")?)
531            .filter(lit(false))?
532            .build()?;
533
534        let plan = LogicalPlanBuilder::from(left).union(right)?.build()?;
535
536        let expected = "Projection: a, b, c\n  TableScan: test";
537        assert_together_optimized_plan(plan, expected, true)
538    }
539
540    #[test]
541    fn cross_join_empty() -> Result<()> {
542        let table_scan = test_table_scan()?;
543        let left = LogicalPlanBuilder::from(table_scan).build()?;
544        let right = LogicalPlanBuilder::empty(false).build()?;
545
546        let plan = LogicalPlanBuilder::from(left)
547            .cross_join(right)?
548            .filter(col("a").lt_eq(lit(1i64)))?
549            .build()?;
550
551        let expected = "EmptyRelation: rows=0";
552        assert_together_optimized_plan(plan, expected, true)
553    }
554
555    fn assert_empty_left_empty_right_lp(
556        left_empty: bool,
557        right_empty: bool,
558        join_type: JoinType,
559        eq: bool,
560    ) -> Result<()> {
561        let left_lp = if left_empty {
562            let left_table_scan = test_table_scan()?;
563
564            LogicalPlanBuilder::from(left_table_scan)
565                .filter(lit(false))?
566                .build()
567        } else {
568            let scan = test_table_scan_with_name("left").unwrap();
569            LogicalPlanBuilder::from(scan).build()
570        }?;
571
572        let right_lp = if right_empty {
573            let right_table_scan = test_table_scan_with_name("right")?;
574
575            LogicalPlanBuilder::from(right_table_scan)
576                .filter(lit(false))?
577                .build()
578        } else {
579            let scan = test_table_scan_with_name("right").unwrap();
580            LogicalPlanBuilder::from(scan).build()
581        }?;
582
583        let plan = LogicalPlanBuilder::from(left_lp)
584            .join_using(
585                right_lp,
586                join_type,
587                vec![Column::from_name("a".to_string())],
588            )?
589            .build()?;
590
591        let expected = "EmptyRelation: rows=0";
592        assert_together_optimized_plan(plan, expected, eq)
593    }
594
595    // TODO: fix this long name
596    fn assert_anti_join_empty_join_table_is_base_table(
597        anti_left_join: bool,
598    ) -> Result<()> {
599        // if we have an anti join with an empty join table, then the result is the base_table
600        let (left, right, join_type, expected) = if anti_left_join {
601            let left = test_table_scan()?;
602            let right = LogicalPlanBuilder::from(test_table_scan()?)
603                .filter(lit(false))?
604                .build()?;
605            let expected = left.display_indent().to_string();
606            (left, right, JoinType::LeftAnti, expected)
607        } else {
608            let right = test_table_scan()?;
609            let left = LogicalPlanBuilder::from(test_table_scan()?)
610                .filter(lit(false))?
611                .build()?;
612            let expected = right.display_indent().to_string();
613            (left, right, JoinType::RightAnti, expected)
614        };
615
616        let plan = LogicalPlanBuilder::from(left)
617            .join_using(right, join_type, vec![Column::from_name("a".to_string())])?
618            .build()?;
619
620        assert_together_optimized_plan(plan, &expected, true)
621    }
622
623    #[test]
624    fn test_join_empty_propagation_rules() -> Result<()> {
625        // test full join with empty left and empty right
626        assert_empty_left_empty_right_lp(true, true, JoinType::Full, true)?;
627
628        // test left join with empty left
629        assert_empty_left_empty_right_lp(true, false, JoinType::Left, true)?;
630
631        // test right join with empty right
632        assert_empty_left_empty_right_lp(false, true, JoinType::Right, true)?;
633
634        // test left semi join with empty left
635        assert_empty_left_empty_right_lp(true, false, JoinType::LeftSemi, true)?;
636
637        // test left semi join with empty right
638        assert_empty_left_empty_right_lp(false, true, JoinType::LeftSemi, true)?;
639
640        // test right semi join with empty left
641        assert_empty_left_empty_right_lp(true, false, JoinType::RightSemi, true)?;
642
643        // test right semi join with empty right
644        assert_empty_left_empty_right_lp(false, true, JoinType::RightSemi, true)?;
645
646        // test left anti join empty left
647        assert_empty_left_empty_right_lp(true, false, JoinType::LeftAnti, true)?;
648
649        // test right anti join empty right
650        assert_empty_left_empty_right_lp(false, true, JoinType::RightAnti, true)?;
651
652        // test left anti join empty right
653        assert_anti_join_empty_join_table_is_base_table(true)?;
654
655        // test right anti join empty left
656        assert_anti_join_empty_join_table_is_base_table(false)
657    }
658
659    #[test]
660    fn test_join_empty_propagation_rules_noop() -> Result<()> {
661        // these cases should not result in an empty relation
662
663        // test left join with empty right
664        assert_empty_left_empty_right_lp(false, true, JoinType::Left, false)?;
665
666        // test right join with empty left
667        assert_empty_left_empty_right_lp(true, false, JoinType::Right, false)?;
668
669        // test left semi with non-empty left and right
670        assert_empty_left_empty_right_lp(false, false, JoinType::LeftSemi, false)?;
671
672        // test right semi with non-empty left and right
673        assert_empty_left_empty_right_lp(false, false, JoinType::RightSemi, false)?;
674
675        // test left anti join with non-empty left and right
676        assert_empty_left_empty_right_lp(false, false, JoinType::LeftAnti, false)?;
677
678        // test left anti with non-empty left and empty right
679        assert_empty_left_empty_right_lp(false, true, JoinType::LeftAnti, false)?;
680
681        // test right anti join with non-empty left and right
682        assert_empty_left_empty_right_lp(false, false, JoinType::RightAnti, false)?;
683
684        // test right anti with empty left and non-empty right
685        assert_empty_left_empty_right_lp(true, false, JoinType::RightAnti, false)
686    }
687
688    #[test]
689    fn test_left_join_right_empty_null_pad() -> Result<()> {
690        let left =
691            LogicalPlanBuilder::from(test_table_scan_with_name("left")?).build()?;
692        let right_empty = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
693            .filter(lit(false))?
694            .build()?;
695
696        let plan = LogicalPlanBuilder::from(left)
697            .join_using(
698                right_empty,
699                JoinType::Left,
700                vec![Column::from_name("a".to_string())],
701            )?
702            .build()?;
703
704        let expected = "Projection: left.a, left.b, left.c, CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c\n  TableScan: left";
705        assert_together_optimized_plan(plan, expected, true)
706    }
707
708    #[test]
709    fn test_right_join_left_empty_null_pad() -> Result<()> {
710        let left_empty = LogicalPlanBuilder::from(test_table_scan_with_name("left")?)
711            .filter(lit(false))?
712            .build()?;
713        let right =
714            LogicalPlanBuilder::from(test_table_scan_with_name("right")?).build()?;
715
716        let plan = LogicalPlanBuilder::from(left_empty)
717            .join_using(
718                right,
719                JoinType::Right,
720                vec![Column::from_name("a".to_string())],
721            )?
722            .build()?;
723
724        let expected = "Projection: CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c, right.a, right.b, right.c\n  TableScan: right";
725        assert_together_optimized_plan(plan, expected, true)
726    }
727
728    #[test]
729    fn test_full_join_right_empty_null_pad() -> Result<()> {
730        let left =
731            LogicalPlanBuilder::from(test_table_scan_with_name("left")?).build()?;
732        let right_empty = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
733            .filter(lit(false))?
734            .build()?;
735
736        let plan = LogicalPlanBuilder::from(left)
737            .join_using(
738                right_empty,
739                JoinType::Full,
740                vec![Column::from_name("a".to_string())],
741            )?
742            .build()?;
743
744        let expected = "Projection: left.a, left.b, left.c, CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c\n  TableScan: left";
745        assert_together_optimized_plan(plan, expected, true)
746    }
747
748    #[test]
749    fn test_full_join_left_empty_null_pad() -> Result<()> {
750        let left_empty = LogicalPlanBuilder::from(test_table_scan_with_name("left")?)
751            .filter(lit(false))?
752            .build()?;
753        let right =
754            LogicalPlanBuilder::from(test_table_scan_with_name("right")?).build()?;
755
756        let plan = LogicalPlanBuilder::from(left_empty)
757            .join_using(
758                right,
759                JoinType::Full,
760                vec![Column::from_name("a".to_string())],
761            )?
762            .build()?;
763
764        let expected = "Projection: CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c, right.a, right.b, right.c\n  TableScan: right";
765        assert_together_optimized_plan(plan, expected, true)
766    }
767
768    #[test]
769    fn test_left_join_complex_on_right_empty_null_pad() -> Result<()> {
770        let left =
771            LogicalPlanBuilder::from(test_table_scan_with_name("left")?).build()?;
772        let right_empty = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
773            .filter(lit(false))?
774            .build()?;
775
776        // Complex ON condition: left.a = right.a AND left.b > right.b
777        let plan = LogicalPlanBuilder::from(left)
778            .join(
779                right_empty,
780                JoinType::Left,
781                (
782                    vec![Column::from_name("a".to_string())],
783                    vec![Column::from_name("a".to_string())],
784                ),
785                Some(col("left.b").gt(col("right.b"))),
786            )?
787            .build()?;
788
789        let expected = "Projection: left.a, left.b, left.c, CAST(NULL AS UInt32) AS a, CAST(NULL AS UInt32) AS b, CAST(NULL AS UInt32) AS c\n  TableScan: left";
790        assert_together_optimized_plan(plan, expected, true)
791    }
792
793    #[test]
794    fn test_empty_with_non_empty() -> Result<()> {
795        let table_scan = test_table_scan()?;
796
797        let fields = test_table_scan_fields();
798
799        let empty = LogicalPlan::EmptyRelation(EmptyRelation {
800            produce_one_row: false,
801            schema: Arc::new(DFSchema::from_unqualified_fields(
802                fields.into(),
803                Default::default(),
804            )?),
805        });
806
807        let one = LogicalPlanBuilder::from(empty.clone()).build()?;
808        let two = LogicalPlanBuilder::from(table_scan).build()?;
809        let three = LogicalPlanBuilder::from(empty).build()?;
810
811        // Union
812        //  EmptyRelation
813        //  TableScan: test
814        //  EmptyRelation
815        let plan = LogicalPlanBuilder::from(one)
816            .union(two)?
817            .union(three)?
818            .build()?;
819
820        let expected = "Projection: a, b, c\
821        \n  TableScan: test";
822
823        assert_together_optimized_plan(plan, expected, true)
824    }
825}