Skip to main content

datafusion_optimizer/
eliminate_join.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//! [`EliminateJoin`] rewrites joins to simpler forms to make them cheaper
19//! to evaluate. We implement three distinct rewrites:
20//!
21//! * An inner join can be rewritten to an empty relation if the join condition
22//!   is trivially false.
23//!
24//! * An inner join `L ⋈ R` can be rewritten to a left semi join `L ⋉ R`
25//!   (`LeftSemi`), which keeps the rows of L that have a match in R and outputs
26//!   only L's columns. The rewrite to `L ⋉ R` is valid when both of the
27//!   following are true:
28//!
29//!     1. None of R's columns are referenced above the join.
30//!     2. R does not observably multiply L's rows. This holds when either the
31//!        join's ancestors are duplicate-insensitive (e.g., DISTINCT) or we can use
32//!        functional dependencies to prove that each L row matches at most one R
33//!        row (R is provably unique on the join keys).
34//!
35//! * A left outer join `L ⟕ R` can be removed entirely, i.e. replaced by `L`,
36//!   under the same two conditions. Unlike an inner join, a left join
37//!   preserves every row of L whether or not it has a match in R, so when R's
38//!   columns are unused and R cannot multiply L's rows the join has no
39//!   observable effect at all. Such joins commonly appear in generated SQL
40//!   and in queries over views that join in lookup tables the query does not
41//!   read. A join filter does not prevent this rewrite: for a left join it
42//!   only decides whether a left row is matched or null-padded, and either
43//!   way the row is emitted. Symmetrically, a right outer join `L ⟖ R` can be
44//!   replaced by `R` when L's columns are unused and L cannot multiply R's
45//!   rows.
46//!
47//! # Overview
48//!
49//! `rewrite_subtree` walks the plan top-down, threading two pieces of context
50//! down to each join:
51//!
52//! * `live` — which of the join's output columns are referenced above it. It is
53//!   propagated top-down: each node asks its children only for the columns it
54//!   needs from them, so a projection or aggregate asks for just the columns its
55//!   expressions reference, dropping the rest (the narrowing); a join splits the
56//!   set across its two inputs.
57//! * `duplicate_insensitive` — whether emitting each row once instead of many
58//!   times will not change the output. A duplicate-collapsing node (e.g.,
59//!   DISTINCT, GROUP BY with no aggregate functions, or the existence side of a
60//!   semi/anti/mark join) sets it `true` for its subtree, and it propagates
61//!   downward until a node that makes the row count observable again (a `LIMIT`,
62//!   a top-N sort, ...) clears it. It is therefore fixed by the nearest such
63//!   node, not by the whole ancestor chain: a collapsing node shields its subtree,
64//!   so a duplicate-sensitive node further above does not matter.
65//!
66//! At each join, `rewritten_join_type` combines this context with the side's
67//! functional dependencies to choose `Inner`, `LeftSemi`, or `RightSemi`, or
68//! to eliminate the join entirely in favor of its preserved input. Most
69//! node types just forward the context to their single child via
70//! `rewrite_single_input`; nodes that alter column requirements or
71//! duplicate-sensitivity (projection, aggregate, sort, ...) adjust it first.
72use crate::utils::for_each_referenced_index;
73use crate::{OptimizerConfig, OptimizerRule};
74use datafusion_common::tree_node::{Transformed, TreeNode};
75use datafusion_common::{
76    DFSchema, Dependency, HashSet, NullEquality, Result, ScalarValue,
77};
78use datafusion_expr::{
79    Expr, JoinType,
80    logical_plan::{
81        Aggregate, Distinct, DistinctOn, EmptyRelation, Filter, Join, Limit, LogicalPlan,
82        Partitioning, Projection, Repartition, Sort, SubqueryAlias,
83    },
84};
85use std::sync::Arc;
86
87/// The columns that are "live" at a plan node, i.e., which of its output
88/// columns are referenced by an ancestor node. Represented as a set of column
89/// indices, relative to the node's schema.
90///
91/// See the module-level docs for how this set is threaded down the plan and
92/// narrowed or split at each node.
93#[derive(Debug, Default, Clone)]
94struct LiveColumns(HashSet<usize>);
95
96impl LiveColumns {
97    fn new() -> Self {
98        Self(HashSet::new())
99    }
100
101    /// Every column of `schema` is live.
102    fn all(schema: &DFSchema) -> Self {
103        Self((0..schema.fields().len()).collect())
104    }
105
106    /// The columns of `schema` referenced by any of `exprs`.
107    fn try_new<'a>(
108        exprs: impl IntoIterator<Item = &'a Expr>,
109        schema: &DFSchema,
110    ) -> Result<Self> {
111        let mut live = Self::new();
112        live.extend_from(exprs, schema)?;
113        Ok(live)
114    }
115
116    /// Inserts the index, within `schema`, of every column referenced by any of
117    /// `exprs`, including columns reached through correlated subquery outer
118    /// references.
119    fn extend_from<'a>(
120        &mut self,
121        exprs: impl IntoIterator<Item = &'a Expr>,
122        schema: &DFSchema,
123    ) -> Result<()> {
124        for expr in exprs {
125            for_each_referenced_index(expr, schema, |idx| {
126                self.0.insert(idx);
127            })?;
128        }
129        Ok(())
130    }
131
132    fn insert(&mut self, idx: usize) {
133        self.0.insert(idx);
134    }
135
136    fn is_empty(&self) -> bool {
137        self.0.is_empty()
138    }
139
140    /// Splits live columns spanning a join's combined output (the left input's
141    /// columns first, then the right input's) into the per-side sets, rebasing
142    /// the right side's indices to start at zero. `left_len` is the number of
143    /// columns contributed by the left input.
144    fn split_at(&self, left_len: usize) -> (Self, Self) {
145        let mut left = Self::new();
146        let mut right = Self::new();
147        for &idx in &self.0 {
148            if idx < left_len {
149                left.insert(idx);
150            } else {
151                right.insert(idx - left_len);
152            }
153        }
154        (left, right)
155    }
156}
157
158/// Rewrites an inner join to a semi join when one input only filters the
159/// other, removes an outer join whose non-preserved side is unused and cannot
160/// multiply the preserved side's rows, and replaces an always-false inner join
161/// with an empty relation.
162#[derive(Default, Debug)]
163pub struct EliminateJoin;
164
165impl EliminateJoin {
166    pub fn new() -> Self {
167        Self {}
168    }
169}
170
171impl OptimizerRule for EliminateJoin {
172    fn name(&self) -> &str {
173        "eliminate_join"
174    }
175
176    fn rewrite(
177        &self,
178        plan: LogicalPlan,
179        _config: &dyn OptimizerConfig,
180    ) -> Result<Transformed<LogicalPlan>> {
181        let live = LiveColumns::all(plan.schema());
182        rewrite_subtree(plan, live, false)
183    }
184}
185
186/// Rewrites `plan` and everything below it, including joins nested inside
187/// subquery expressions.
188///
189/// [`rewrite_node`] handles the node itself and recurses into its plan
190/// children; this wrapper additionally descends into the node's own subquery
191/// expressions.  Each subquery is seeded as a fresh root, since its columns are
192/// independent of the enclosing plan's `live` set.
193fn rewrite_subtree(
194    plan: LogicalPlan,
195    live: LiveColumns,
196    duplicate_insensitive: bool,
197) -> Result<Transformed<LogicalPlan>> {
198    rewrite_node(plan, live, duplicate_insensitive)?.transform_data(|plan| {
199        plan.map_subqueries(|subquery| {
200            let live = LiveColumns::all(subquery.schema());
201            rewrite_subtree(subquery, live, false)
202        })
203    })
204}
205
206fn rewrite_node(
207    plan: LogicalPlan,
208    live: LiveColumns,
209    duplicate_insensitive: bool,
210) -> Result<Transformed<LogicalPlan>> {
211    match plan {
212        // The only arm that rewrites a join; the rest just thread context down to one.
213        LogicalPlan::Join(join) => rewrite_join(join, &live, duplicate_insensitive),
214        LogicalPlan::Projection(Projection {
215            expr,
216            input,
217            schema,
218            ..
219        }) => {
220            // Narrows `live` to the columns the projection's expressions reference.
221            let child_live = LiveColumns::try_new(&expr, input.schema())?;
222            rewrite_single_input(input, child_live, duplicate_insensitive, |input| {
223                Ok(LogicalPlan::Projection(Projection::try_new_with_schema(
224                    expr, input, schema,
225                )?))
226            })
227        }
228        LogicalPlan::Filter(Filter {
229            predicate, input, ..
230        }) => {
231            // Adds the predicate's columns to `live` (a side used only by the filter stays live).
232            let mut child_live = live;
233            child_live.extend_from([&predicate], input.schema())?;
234            rewrite_single_input(input, child_live, duplicate_insensitive, |input| {
235                Ok(LogicalPlan::Filter(Filter::new(predicate, input)))
236            })
237        }
238        LogicalPlan::Aggregate(Aggregate {
239            input,
240            group_expr,
241            aggr_expr,
242            schema,
243            ..
244        }) => {
245            // Narrows `live` to the grouping and aggregate expressions' columns.
246            let child_live = LiveColumns::try_new(
247                group_expr.iter().chain(&aggr_expr),
248                input.schema(),
249            )?;
250
251            // A grouping aggregate with no aggregate functions (`GROUP BY` with
252            // an empty `aggr_expr`) only observes which group-key values exist,
253            // not how many rows produced them, so its input is duplicate-
254            // insensitive.
255            let child_duplicate_insensitive =
256                !group_expr.is_empty() && aggr_expr.is_empty();
257
258            rewrite_single_input(
259                input,
260                child_live,
261                child_duplicate_insensitive,
262                |input| {
263                    Ok(LogicalPlan::Aggregate(Aggregate::try_new_with_schema(
264                        input, group_expr, aggr_expr, schema,
265                    )?))
266                },
267            )
268        }
269        LogicalPlan::Distinct(Distinct::All(input)) => {
270            // `SELECT DISTINCT *` is equivalent to a no-aggregate `GROUP BY`
271            // over every input column, so the input is duplicate-insensitive,
272            // but every column is part of the dedup key.
273            let child_live = LiveColumns::all(input.schema());
274            rewrite_single_input(input, child_live, true, |input| {
275                Ok(LogicalPlan::Distinct(Distinct::All(input)))
276            })
277        }
278        LogicalPlan::Distinct(Distinct::On(DistinctOn {
279            on_expr,
280            select_expr,
281            sort_expr,
282            input,
283            schema,
284        })) => {
285            // `DISTINCT ON (on) select [ORDER BY sort]` is a no-aggregate
286            // `GROUP BY` on the columns it reads, so its input is duplicate-
287            // insensitive; the live columns are exactly those of the
288            // ON/SELECT/ORDER BY expressions.
289            let mut child_live =
290                LiveColumns::try_new(on_expr.iter().chain(&select_expr), input.schema())?;
291            if let Some(sort_expr) = &sort_expr {
292                child_live
293                    .extend_from(sort_expr.iter().map(|s| &s.expr), input.schema())?;
294            }
295
296            rewrite_single_input(input, child_live, true, |input| {
297                Ok(LogicalPlan::Distinct(Distinct::On(DistinctOn {
298                    on_expr,
299                    select_expr,
300                    sort_expr,
301                    input,
302                    schema,
303                })))
304            })
305        }
306        LogicalPlan::Sort(Sort { expr, input, fetch }) => {
307            // Adds the sort-key columns to `live`.
308            let mut child_live = live;
309            child_live.extend_from(expr.iter().map(|s| &s.expr), input.schema())?;
310
311            // A `fetch` (top-N) makes the row count observable, so duplicate-
312            // insensitivity does not survive past it.
313            let child_duplicate_insensitive = duplicate_insensitive && fetch.is_none();
314            rewrite_single_input(
315                input,
316                child_live,
317                child_duplicate_insensitive,
318                |input| Ok(LogicalPlan::Sort(Sort { expr, input, fetch })),
319            )
320        }
321        LogicalPlan::Limit(Limit { skip, fetch, input }) => {
322            // LIMIT makes the row count observable, so it clears duplicate-insensitivity.
323            rewrite_single_input(input, live, false, |input| {
324                Ok(LogicalPlan::Limit(Limit { skip, fetch, input }))
325            })
326        }
327        LogicalPlan::SubqueryAlias(SubqueryAlias { input, alias, .. }) => {
328            // Re-aliases columns 1:1, so `live` and duplicate-sensitivity pass through unchanged.
329            rewrite_single_input(input, live, duplicate_insensitive, |input| {
330                Ok(LogicalPlan::SubqueryAlias(SubqueryAlias::try_new(
331                    input, alias,
332                )?))
333            })
334        }
335        LogicalPlan::Repartition(Repartition {
336            input,
337            partitioning_scheme,
338        }) => {
339            // Adds any partitioning-key columns to `live`; duplicate-sensitivity is unchanged.
340            let mut child_live = live;
341            match &partitioning_scheme {
342                Partitioning::Hash(exprs, _) | Partitioning::DistributeBy(exprs) => {
343                    child_live.extend_from(exprs, input.schema())?;
344                }
345                Partitioning::Range(range) => {
346                    child_live.extend_from(
347                        range.ordering().iter().map(|sort_expr| &sort_expr.expr),
348                        input.schema(),
349                    )?;
350                }
351                Partitioning::RoundRobinBatch(_) => {}
352            }
353            rewrite_single_input(input, child_live, duplicate_insensitive, |input| {
354                Ok(LogicalPlan::Repartition(Repartition {
355                    input,
356                    partitioning_scheme,
357                }))
358            })
359        }
360        // Conservatively treat any other plan node as a fresh root, since we are
361        // not sure of its semantics with respect to duplicates or live columns.
362        _ => plan.map_children(|child| {
363            let live = LiveColumns::all(child.schema());
364            rewrite_subtree(child, live, false)
365        }),
366    }
367}
368
369/// Recurses into a single-input node's child, threading `child_live` and
370/// `duplicate_insensitive` down, then rebuilds the node from the (possibly
371/// rewritten) child via `rebuild`. The child's `Transformed` flag is preserved,
372/// so the node is reported as changed exactly when its child changed.
373fn rewrite_single_input<F>(
374    input: Arc<LogicalPlan>,
375    child_live: LiveColumns,
376    duplicate_insensitive: bool,
377    rebuild: F,
378) -> Result<Transformed<LogicalPlan>>
379where
380    F: FnOnce(Arc<LogicalPlan>) -> Result<LogicalPlan>,
381{
382    rewrite_subtree(
383        Arc::unwrap_or_clone(input),
384        child_live,
385        duplicate_insensitive,
386    )?
387    .map_data(|input| rebuild(Arc::new(input)))
388}
389
390fn rewrite_join(
391    join: Join,
392    live: &LiveColumns,
393    duplicate_insensitive: bool,
394) -> Result<Transformed<LogicalPlan>> {
395    if join.join_type == JoinType::Inner
396        && join.on.is_empty()
397        && matches!(
398            join.filter.as_ref(),
399            Some(Expr::Literal(ScalarValue::Boolean(Some(false)), _))
400        )
401    {
402        return Ok(Transformed::yes(LogicalPlan::EmptyRelation(
403            EmptyRelation {
404                produce_one_row: false,
405                schema: join.schema,
406            },
407        )));
408    }
409
410    let (visible_left, visible_right) = split_join_output_columns(&join, live);
411
412    let rewritten_join_type = match rewritten_join_type(
413        &join,
414        &visible_left,
415        &visible_right,
416        duplicate_insensitive,
417    ) {
418        JoinRewrite::ReplaceWithLeft => {
419            let left = rewrite_subtree(
420                Arc::unwrap_or_clone(join.left),
421                visible_left,
422                duplicate_insensitive,
423            )?;
424            return Ok(Transformed::yes(left.data));
425        }
426        JoinRewrite::ReplaceWithRight => {
427            let right = rewrite_subtree(
428                Arc::unwrap_or_clone(join.right),
429                visible_right,
430                duplicate_insensitive,
431            )?;
432            return Ok(Transformed::yes(right.data));
433        }
434        JoinRewrite::Join(join_type) => join_type,
435    };
436
437    let (mut left_live, mut right_live) = match rewritten_join_type {
438        JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
439            (visible_left, LiveColumns::new())
440        }
441        JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
442            (LiveColumns::new(), visible_right)
443        }
444        _ => (visible_left, visible_right),
445    };
446
447    add_join_condition_columns(&join, &mut left_live, &mut right_live)?;
448
449    let (left_dup_insensitive, right_dup_insensitive) =
450        child_duplicate_insensitivity(rewritten_join_type, duplicate_insensitive);
451
452    let left = rewrite_subtree(
453        Arc::unwrap_or_clone(join.left),
454        left_live,
455        left_dup_insensitive,
456    )?;
457    let right = rewrite_subtree(
458        Arc::unwrap_or_clone(join.right),
459        right_live,
460        right_dup_insensitive,
461    )?;
462
463    let changed =
464        left.transformed || right.transformed || rewritten_join_type != join.join_type;
465    let left = Arc::new(left.data);
466    let right = Arc::new(right.data);
467
468    if changed {
469        // The join type or an input changed, so the output schema may have
470        // narrowed; recompute it via `try_new`.
471        Ok(Transformed::yes(LogicalPlan::Join(Join::try_new(
472            left,
473            right,
474            join.on,
475            join.filter,
476            rewritten_join_type,
477            join.join_constraint,
478            join.null_equality,
479            join.null_aware,
480        )?)))
481    } else {
482        // Nothing changed; reassemble the join reusing its existing schema rather
483        // than recomputing it.
484        Ok(Transformed::no(LogicalPlan::Join(Join {
485            left,
486            right,
487            on: join.on,
488            filter: join.filter,
489            join_type: join.join_type,
490            join_constraint: join.join_constraint,
491            schema: join.schema,
492            null_equality: join.null_equality,
493            null_aware: join.null_aware,
494        })))
495    }
496}
497
498/// Returns which join inputs can safely ignore duplicate rows from their own
499/// descendants. For semi/anti/mark joins, duplicates from the existence side do
500/// not change the result even when the parent itself is duplicate-sensitive.
501fn child_duplicate_insensitivity(
502    join_type: JoinType,
503    duplicate_insensitive: bool,
504) -> (bool, bool) {
505    match join_type {
506        JoinType::Inner => (duplicate_insensitive, duplicate_insensitive),
507        JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
508            (duplicate_insensitive, true)
509        }
510        JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
511            (true, duplicate_insensitive)
512        }
513        JoinType::Left | JoinType::Right | JoinType::Full => (false, false),
514    }
515}
516
517/// The rewrite chosen for a join by [`rewritten_join_type`].
518enum JoinRewrite {
519    /// Keep the join, with this (possibly rewritten) join type.
520    Join(JoinType),
521    /// The join has no observable effect; replace it with its left input.
522    ReplaceWithLeft,
523    /// The join has no observable effect; replace it with its right input.
524    ReplaceWithRight,
525}
526
527/// Chooses a cheaper form for a join: removes an outer join whose non-preserved
528/// side is redundant, or rewrites an inner join to a semi join when the
529/// removed side has no parent-visible columns and either the parent ignores
530/// duplicate output rows or the removed side is unique on the join keys.
531fn rewritten_join_type(
532    join: &Join,
533    visible_left: &LiveColumns,
534    visible_right: &LiveColumns,
535    duplicate_insensitive: bool,
536) -> JoinRewrite {
537    // A side is redundant when nothing above the join references its columns
538    // and it cannot multiply the other side's rows (the ancestors are
539    // duplicate-insensitive, or the side is unique on the join keys).
540    let can_remove_right = visible_right.is_empty()
541        && (duplicate_insensitive
542            || side_unique_on_join(
543                join.right.schema(),
544                join.on.iter().map(|(_, right)| right),
545                join.null_equality,
546            ));
547
548    // A LEFT JOIN preserves every left row, so with a redundant right side the
549    // join has no observable effect and can be replaced by its left input. A
550    // join filter cannot prevent this: it only decides whether a left row is
551    // matched or null-padded, and either way the row is emitted.
552    if join.join_type == JoinType::Left && can_remove_right {
553        return JoinRewrite::ReplaceWithLeft;
554    }
555    let can_remove_left = visible_left.is_empty()
556        && (duplicate_insensitive
557            || side_unique_on_join(
558                join.left.schema(),
559                join.on.iter().map(|(left, _)| left),
560                join.null_equality,
561            ));
562
563    // Symmetrical rule for RIGHT JOIN removal (same explanation as above for the left-join case)
564    if join.join_type == JoinType::Right && can_remove_left {
565        return JoinRewrite::ReplaceWithRight;
566    }
567
568    if join.join_type != JoinType::Inner || join.on.is_empty() {
569        return JoinRewrite::Join(join.join_type);
570    }
571
572    if can_remove_right {
573        return JoinRewrite::Join(JoinType::LeftSemi);
574    }
575    if can_remove_left {
576        return JoinRewrite::Join(JoinType::RightSemi);
577    }
578
579    JoinRewrite::Join(JoinType::Inner)
580}
581
582fn add_join_condition_columns(
583    join: &Join,
584    left_live: &mut LiveColumns,
585    right_live: &mut LiveColumns,
586) -> Result<()> {
587    left_live.extend_from(join.on.iter().map(|(l, _)| l), join.left.schema())?;
588    right_live.extend_from(join.on.iter().map(|(_, r)| r), join.right.schema())?;
589
590    if let Some(filter) = &join.filter {
591        left_live.extend_from([filter], join.left.schema())?;
592        right_live.extend_from([filter], join.right.schema())?;
593    }
594
595    Ok(())
596}
597
598fn split_join_output_columns(
599    join: &Join,
600    live: &LiveColumns,
601) -> (LiveColumns, LiveColumns) {
602    let left_len = join.left.schema().fields().len();
603    match join.join_type {
604        JoinType::Inner | JoinType::Left | JoinType::Right | JoinType::Full => {
605            live.split_at(left_len)
606        }
607        // A semi/anti/mark join outputs only the surviving side's columns, with
608        // the same index space, so `live` passes straight through to that side.
609        JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
610            (live.clone(), LiveColumns::new())
611        }
612        JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
613            (LiveColumns::new(), live.clone())
614        }
615    }
616}
617
618fn side_unique_on_join<'a>(
619    schema: &DFSchema,
620    join_exprs: impl Iterator<Item = &'a Expr>,
621    null_equality: NullEquality,
622) -> bool {
623    let join_key_indices = join_exprs
624        .filter_map(|expr| match expr {
625            Expr::Alias(alias) => alias.expr.as_ref().try_as_col(),
626            _ => expr.try_as_col(),
627        })
628        .filter_map(|column| schema.maybe_index_of_column(column))
629        .collect::<Vec<usize>>();
630
631    schema.functional_dependencies().iter().any(|dependency| {
632        dependency.mode == Dependency::Single
633            && (!dependency.nullable || null_equality == NullEquality::NullEqualsNothing)
634            && dependency
635                .source_indices
636                .iter()
637                .all(|idx| join_key_indices.contains(idx))
638    })
639}
640
641#[cfg(test)]
642mod tests {
643    use crate::OptimizerContext;
644    use crate::assert_optimized_plan_eq_snapshot;
645    use crate::eliminate_join::EliminateJoin;
646    use arrow::datatypes::{DataType, Field, Schema};
647    use datafusion_common::{
648        Constraint, Constraints, NullEquality, Result, ScalarValue, SplitPoint,
649    };
650    use datafusion_expr::JoinType::Inner;
651    use datafusion_expr::{
652        Expr, JoinType, Partitioning, RangePartitioning, col, exists, lit,
653        logical_plan::builder::{
654            LogicalPlanBuilder, table_scan, table_source_with_constraints,
655        },
656        out_ref_col,
657    };
658    use datafusion_functions_aggregate::expr_fn::count;
659    use std::sync::Arc;
660
661    macro_rules! assert_optimized_plan_equal {
662        (
663            $plan:expr,
664            @ $expected:literal $(,)?
665        ) => {{
666            let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
667            let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(EliminateJoin::new())];
668            assert_optimized_plan_eq_snapshot!(
669                optimizer_ctx,
670                rules,
671                $plan,
672                @ $expected,
673            )
674        }};
675    }
676
677    #[test]
678    fn join_on_false() -> Result<()> {
679        let plan = LogicalPlanBuilder::empty(false)
680            .join_on(
681                LogicalPlanBuilder::empty(false).build()?,
682                Inner,
683                Some(lit(false)),
684            )?
685            .build()?;
686
687        assert_optimized_plan_equal!(plan, @"EmptyRelation: rows=0")
688    }
689
690    #[test]
691    fn inner_to_left_semi_when_removed_side_is_unique() -> Result<()> {
692        let plan = left_join_right_with_constraints(primary_key_on_id())?
693            .project(vec![col("l.x")])?
694            .build()?;
695
696        assert_optimized_plan_equal!(plan, @r"
697        Projection: l.x
698          LeftSemi Join: l.id = r.id
699            TableScan: l
700            TableScan: r
701        ")
702    }
703
704    #[test]
705    fn inner_to_left_semi_when_removed_side_is_unique_with_join_filter() -> Result<()> {
706        let right = scan("r", &test_schema(), primary_key_on_id())?;
707        let plan =
708            LogicalPlanBuilder::from(scan("l", &test_schema(), Constraints::default())?)
709                .join(
710                    right,
711                    Inner,
712                    (vec!["l.id"], vec!["r.id"]),
713                    Some(col("r.y").gt(col("l.x"))),
714                )?
715                .project(vec![col("l.x")])?
716                .build()?;
717
718        assert_optimized_plan_equal!(plan, @r"
719        Projection: l.x
720          LeftSemi Join: l.id = r.id Filter: r.y > l.x
721            TableScan: l
722            TableScan: r
723        ")
724    }
725
726    #[test]
727    fn inner_to_right_semi_when_removed_side_is_unique() -> Result<()> {
728        let plan = left_with_constraints_join_right(primary_key_on_id())?
729            .project(vec![col("r.y")])?
730            .build()?;
731
732        assert_optimized_plan_equal!(plan, @r"
733        Projection: r.y
734          RightSemi Join: l.id = r.id
735            TableScan: l
736            TableScan: r
737        ")
738    }
739
740    #[test]
741    fn inner_to_left_semi_for_duplicate_insensitive_parent() -> Result<()> {
742        let plan = left_join_right()?
743            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
744            .build()?;
745
746        assert_optimized_plan_equal!(plan, @r"
747        Aggregate: groupBy=[[l.x]], aggr=[[]]
748          LeftSemi Join: l.id = r.id
749            TableScan: l
750            TableScan: r
751        ")
752    }
753
754    #[test]
755    fn aggregate_with_aggregates_is_not_duplicate_insensitive() -> Result<()> {
756        // A `GROUP BY` *with* aggregate functions observes how many rows fall in
757        // each group, so its input is not duplicate-insensitive. With a non-unique
758        // right side the join must stay an inner join: collapsing it to a semi
759        // join would drop matching duplicates and undercount `count(l.id)`.
760        let plan = left_join_right()?
761            .aggregate(vec![col("l.x")], vec![count(col("l.id"))])?
762            .build()?;
763
764        assert_optimized_plan_equal!(plan, @r"
765        Aggregate: groupBy=[[l.x]], aggr=[[count(l.id)]]
766          Inner Join: l.id = r.id
767            TableScan: l
768            TableScan: r
769        ")
770    }
771
772    #[test]
773    fn duplicate_insensitive_context_propagates_through_join_tree() -> Result<()> {
774        let left = scan("l", &test_schema(), Constraints::default())?;
775        let middle = scan("m", &test_schema(), Constraints::default())?;
776        let right = scan("r", &test_schema(), Constraints::default())?;
777
778        let left_join_middle = LogicalPlanBuilder::from(left)
779            .join(middle, Inner, (vec!["l.id"], vec!["m.id"]), None)?
780            .build()?;
781
782        let plan = LogicalPlanBuilder::from(left_join_middle)
783            .join(right, Inner, (vec!["l.id"], vec!["r.id"]), None)?
784            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
785            .build()?;
786
787        assert_optimized_plan_equal!(plan, @r"
788        Aggregate: groupBy=[[l.x]], aggr=[[]]
789          LeftSemi Join: l.id = r.id
790            LeftSemi Join: l.id = m.id
791              TableScan: l
792              TableScan: m
793            TableScan: r
794        ")
795    }
796
797    #[test]
798    fn projection_does_not_rewrite_without_uniqueness() -> Result<()> {
799        let plan = left_join_right()?.project(vec![col("l.x")])?.build()?;
800
801        assert_optimized_plan_equal!(plan, @r"
802        Projection: l.x
803          Inner Join: l.id = r.id
804            TableScan: l
805            TableScan: r
806        ")
807    }
808
809    #[test]
810    fn required_filter_column_prevents_duplicate_insensitive_rewrite() -> Result<()> {
811        let plan = left_join_right()?
812            .filter(col("r.y").gt(lit(10_i32)))?
813            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
814            .build()?;
815
816        assert_optimized_plan_equal!(plan, @r"
817        Aggregate: groupBy=[[l.x]], aggr=[[]]
818          Filter: r.y > Int32(10)
819            Inner Join: l.id = r.id
820              TableScan: l
821              TableScan: r
822        ")
823    }
824
825    #[test]
826    fn distinct_star_keeps_unreferenced_side() -> Result<()> {
827        // `SELECT DISTINCT *` deduplicates on every join-output column, including
828        // the right side's. With a non-unique right side the inner join can
829        // multiply left rows into distinct `(l, r)` combinations, so the join
830        // must not be rewritten to a semi join (which would drop the right
831        // columns from the DISTINCT key and undercount the result). This holds
832        // even when the right side is unique on the join keys: its columns are
833        // part of the DISTINCT key regardless.
834        let plan = left_join_right()?
835            .distinct()?
836            .project(vec![col("l.x")])?
837            .build()?;
838
839        assert_optimized_plan_equal!(plan, @r"
840        Projection: l.x
841          Distinct:
842            Inner Join: l.id = r.id
843              TableScan: l
844              TableScan: r
845        ")
846    }
847
848    #[test]
849    fn distinct_drops_unreferenced_side_when_projected() -> Result<()> {
850        // `SELECT DISTINCT l.x` projects the right side away below the DISTINCT,
851        // leaving it outside the dedup key. Like a no-aggregate `GROUP BY l.x`,
852        // the DISTINCT makes the input duplicate-insensitive, so the inner join
853        // collapses to a semi join even though the right side is not unique.
854        let plan = left_join_right()?
855            .project(vec![col("l.x")])?
856            .distinct()?
857            .build()?;
858
859        assert_optimized_plan_equal!(plan, @r"
860        Distinct:
861          Projection: l.x
862            LeftSemi Join: l.id = r.id
863              TableScan: l
864              TableScan: r
865        ")
866    }
867
868    #[test]
869    fn correlated_subquery_outer_ref_prevents_rewrite() -> Result<()> {
870        // The aggregate makes the parent duplicate-insensitive, so absent any
871        // other use of the right side the join would collapse to a semi join.
872        // But the `EXISTS` subquery correlates on `r.y`, so the right side is
873        // still needed and the join must stay an inner join. Otherwise the
874        // semi join would drop `r`, orphaning the correlated `r.y` reference.
875        let subquery =
876            LogicalPlanBuilder::from(scan("s", &test_schema(), Constraints::default())?)
877                .filter(col("s.id").eq(out_ref_col(DataType::Int32, "r.y")))?
878                .project(vec![lit(1)])?
879                .build()?;
880
881        let plan = left_join_right()?
882            .filter(exists(Arc::new(subquery)))?
883            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
884            .build()?;
885
886        assert_optimized_plan_equal!(plan, @r"
887        Aggregate: groupBy=[[l.x]], aggr=[[]]
888          Filter: EXISTS (<subquery>)
889            Subquery:
890              Projection: Int32(1)
891                Filter: s.id = outer_ref(r.y)
892                  TableScan: s
893            Inner Join: l.id = r.id
894              TableScan: l
895              TableScan: r
896        ")
897    }
898
899    #[test]
900    fn inner_to_semi_inside_uncorrelated_subquery() -> Result<()> {
901        // A join nested inside a (not-yet-decorrelated) subquery is still
902        // rewritten, because `rewrite_subtree` descends into subquery plans
903        // itself via `map_subqueries`. Here the subquery's projection keeps
904        // only `l.x` and the removed side `r` is unique (PK), so the inner join
905        // collapses to a semi join.
906        let subquery = left_join_right_with_constraints(primary_key_on_id())?
907            .project(vec![col("l.x")])?
908            .build()?;
909
910        let plan = LogicalPlanBuilder::from(scan(
911            "outer",
912            &test_schema(),
913            Constraints::default(),
914        )?)
915        .filter(exists(Arc::new(subquery)))?
916        .build()?;
917
918        assert_optimized_plan_equal!(plan, @r"
919        Filter: EXISTS (<subquery>)
920          Subquery:
921            Projection: l.x
922              LeftSemi Join: l.id = r.id
923                TableScan: l
924                TableScan: r
925          TableScan: outer
926        ")
927    }
928
929    #[test]
930    fn inner_to_semi_inside_correlated_subquery() -> Result<()> {
931        // `map_subqueries` descends into correlated subqueries too, not just
932        // uncorrelated ones, so a join inside one is still rewritten. The
933        // subquery correlates on `outer.id` (via the filter), but that reference
934        // and the projection touch only `l`; `r` is unique (PK) and unreferenced,
935        // so the inner join inside the subquery collapses to a semi join.
936        let subquery = left_join_right_with_constraints(primary_key_on_id())?
937            .filter(col("l.x").eq(out_ref_col(DataType::Int32, "outer.id")))?
938            .project(vec![col("l.x")])?
939            .build()?;
940
941        let plan = LogicalPlanBuilder::from(scan(
942            "outer",
943            &test_schema(),
944            Constraints::default(),
945        )?)
946        .filter(exists(Arc::new(subquery)))?
947        .build()?;
948
949        assert_optimized_plan_equal!(plan, @r"
950        Filter: EXISTS (<subquery>)
951          Subquery:
952            Projection: l.x
953              Filter: l.x = outer_ref(outer.id)
954                LeftSemi Join: l.id = r.id
955                  TableScan: l
956                  TableScan: r
957          TableScan: outer
958        ")
959    }
960
961    #[test]
962    fn nullable_unique_rewrites_under_null_equals_nothing() -> Result<()> {
963        // A `UNIQUE` (rather than `PRIMARY KEY`) constraint marks the key as
964        // nullable. Under the default `NullEqualsNothing` join semantics a null
965        // key matches nothing, so a unique side still yields at most one match
966        // per left row and the inner join can become a semi join.
967        let left = scan("l", &test_schema(), Constraints::default())?;
968        let right = scan("r", &test_schema(), unique_on_x())?;
969        let plan = LogicalPlanBuilder::from(left)
970            .join(right, Inner, (vec!["l.x"], vec!["r.x"]), None)?
971            .project(vec![col("l.id")])?
972            .build()?;
973
974        assert_optimized_plan_equal!(plan, @r"
975        Projection: l.id
976          LeftSemi Join: l.x = r.x
977            TableScan: l
978            TableScan: r
979        ")
980    }
981
982    #[test]
983    fn nullable_unique_does_not_rewrite_under_null_equals_null() -> Result<()> {
984        // With `NullEqualsNull` semantics two null keys compare equal, so a
985        // nullable `UNIQUE` key no longer guarantees at most one match per left
986        // row: several null-keyed right rows could match a null-keyed left row.
987        // Uniqueness on the join keys is therefore not established and the inner
988        // join must be preserved.
989        let left = scan("l", &test_schema(), Constraints::default())?;
990        let right = scan("r", &test_schema(), unique_on_x())?;
991        let plan = LogicalPlanBuilder::from(left)
992            .join_detailed(
993                right,
994                Inner,
995                (vec!["l.x"], vec!["r.x"]),
996                None,
997                NullEquality::NullEqualsNull,
998            )?
999            .project(vec![col("l.id")])?
1000            .build()?;
1001
1002        assert_optimized_plan_equal!(plan, @r"
1003        Projection: l.id
1004          Inner Join: l.x = r.x
1005            TableScan: l
1006            TableScan: r
1007        ")
1008    }
1009
1010    #[test]
1011    fn composite_unique_rewrites_when_join_covers_all_key_columns() -> Result<()> {
1012        // The removed side is unique on the composite key `(id, x)`. The join
1013        // equates both key columns, so each left row matches at most one right
1014        // row and the inner join can become a semi join.
1015        let left = scan("l", &test_schema(), Constraints::default())?;
1016        let right = scan("r", &test_schema(), composite_primary_key_on_id_x())?;
1017        let plan = LogicalPlanBuilder::from(left)
1018            .join(
1019                right,
1020                Inner,
1021                (vec!["l.id", "l.x"], vec!["r.id", "r.x"]),
1022                None,
1023            )?
1024            .project(vec![col("l.y")])?
1025            .build()?;
1026
1027        assert_optimized_plan_equal!(plan, @r"
1028        Projection: l.y
1029          LeftSemi Join: l.id = r.id, l.x = r.x
1030            TableScan: l
1031            TableScan: r
1032        ")
1033    }
1034
1035    #[test]
1036    fn composite_unique_does_not_rewrite_when_join_misses_a_key_column() -> Result<()> {
1037        // The removed side is unique only on the *composite* key `(id, x)`. The
1038        // join equates `id` but not `x`, so a left row may match many right rows
1039        // (those sharing its `id` but differing in `x`). Uniqueness on the join
1040        // keys is not established, so the inner join must be preserved. This
1041        // guards the requirement that the join cover *every* column of the
1042        // unique key, not just some.
1043        let left = scan("l", &test_schema(), Constraints::default())?;
1044        let right = scan("r", &test_schema(), composite_primary_key_on_id_x())?;
1045        let plan = LogicalPlanBuilder::from(left)
1046            .join(right, Inner, (vec!["l.id"], vec!["r.id"]), None)?
1047            .project(vec![col("l.y")])?
1048            .build()?;
1049
1050        assert_optimized_plan_equal!(plan, @r"
1051        Projection: l.y
1052          Inner Join: l.id = r.id
1053            TableScan: l
1054            TableScan: r
1055        ")
1056    }
1057
1058    #[test]
1059    fn top_n_sort_blocks_duplicate_insensitive_rewrite() -> Result<()> {
1060        // A top-N `Sort` (one with a `fetch`) makes the row count observable, so
1061        // the duplicate-insensitivity established by the `GROUP BY` does not survive
1062        // past it. With a non-unique right side the join must stay an inner join: a
1063        // semi join could drop matching duplicates and change which rows fall within
1064        // the top N.
1065        let plan = left_join_right()?
1066            .sort_with_limit(vec![col("l.x").sort(true, false)], Some(5))?
1067            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
1068            .build()?;
1069
1070        assert_optimized_plan_equal!(plan, @r"
1071        Aggregate: groupBy=[[l.x]], aggr=[[]]
1072          Sort: l.x ASC NULLS LAST, fetch=5
1073            Inner Join: l.id = r.id
1074              TableScan: l
1075              TableScan: r
1076        ")
1077    }
1078
1079    #[test]
1080    fn sort_without_fetch_preserves_duplicate_insensitive_rewrite() -> Result<()> {
1081        // A `Sort` without a `fetch` does not make the row count observable, so it
1082        // forwards the parent's duplicate-insensitivity to the join unchanged
1083        // (sorting before or after duplicate removal is equivalent). The non-unique
1084        // right side is unreferenced, so the inner join collapses to a semi join.
1085        let plan = left_join_right()?
1086            .sort(vec![col("l.x").sort(true, false)])?
1087            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
1088            .build()?;
1089
1090        assert_optimized_plan_equal!(plan, @r"
1091        Aggregate: groupBy=[[l.x]], aggr=[[]]
1092          Sort: l.x ASC NULLS LAST
1093            LeftSemi Join: l.id = r.id
1094              TableScan: l
1095              TableScan: r
1096        ")
1097    }
1098
1099    #[test]
1100    fn limit_blocks_duplicate_insensitive_rewrite() -> Result<()> {
1101        // `LIMIT` makes the row count observable, clearing the duplicate-
1102        // insensitivity established by the `GROUP BY`. With a non-unique right side
1103        // the join must stay an inner join, since a semi join could drop matching
1104        // duplicates and change which rows the limit returns.
1105        let plan = left_join_right()?
1106            .limit(0, Some(5))?
1107            .aggregate(vec![col("l.x")], Vec::<Expr>::new())?
1108            .build()?;
1109
1110        assert_optimized_plan_equal!(plan, @r"
1111        Aggregate: groupBy=[[l.x]], aggr=[[]]
1112          Limit: skip=0, fetch=5
1113            Inner Join: l.id = r.id
1114              TableScan: l
1115              TableScan: r
1116        ")
1117    }
1118
1119    #[test]
1120    fn repartition_hash_key_keeps_removed_side_live() -> Result<()> {
1121        // The projection keeps only `l.x`, and the right side is unique (PK), so
1122        // absent any other use of `r` the inner join would collapse to a semi join.
1123        // But the `Repartition` hashes on `r.y`, which keeps the right side live, so
1124        // the join must stay an inner join to preserve `r.y` for the partitioning.
1125        let plan = left_join_right_with_constraints(primary_key_on_id())?
1126            .repartition(Partitioning::Hash(vec![col("r.y")], 4))?
1127            .project(vec![col("l.x")])?
1128            .build()?;
1129
1130        assert_optimized_plan_equal!(plan, @r"
1131        Projection: l.x
1132          Repartition: Hash(r.y) partition_count=4
1133            Inner Join: l.id = r.id
1134              TableScan: l
1135              TableScan: r
1136        ")
1137    }
1138
1139    #[test]
1140    fn repartition_range_key_keeps_removed_side_live() -> Result<()> {
1141        // The projection keeps only `l.x`, and the right side is unique (PK), so
1142        // absent any other use of `r` the inner join would collapse to a semi join.
1143        // But the `Repartition` ranges on `r.y`, which keeps the right side live, so
1144        // the join must stay an inner join to preserve `r.y` for the partitioning.
1145        let plan = left_join_right_with_constraints(primary_key_on_id())?
1146            .repartition(Partitioning::Range(RangePartitioning::try_new(
1147                vec![col("r.y").sort(true, true)],
1148                vec![SplitPoint::new(vec![ScalarValue::Int32(Some(10))])],
1149            )?))?
1150            .project(vec![col("l.x")])?
1151            .build()?;
1152
1153        assert_optimized_plan_equal!(plan, @r"
1154        Projection: l.x
1155          Repartition: Range([r.y ASC NULLS FIRST], [(10)], 2)
1156            Inner Join: l.id = r.id
1157              TableScan: l
1158              TableScan: r
1159        ")
1160    }
1161
1162    #[test]
1163    fn distinct_on_enables_semi_join_rewrite() -> Result<()> {
1164        // `DISTINCT ON (l.x)` is a no-aggregate `GROUP BY` on the columns it reads,
1165        // so it makes its input duplicate-insensitive. The non-unique right side is
1166        // unreferenced, so the inner join collapses to a semi join.
1167        let plan = left_join_right()?
1168            .distinct_on(vec![col("l.x")], vec![col("l.x")], None)?
1169            .build()?;
1170
1171        assert_optimized_plan_equal!(plan, @r"
1172        DistinctOn: on_expr=[[l.x]], select_expr=[[l.x]], sort_expr=[[]]
1173          LeftSemi Join: l.id = r.id
1174            TableScan: l
1175            TableScan: r
1176        ")
1177    }
1178
1179    #[test]
1180    fn existing_semi_join_passes_through_unchanged() -> Result<()> {
1181        // A join that is already a semi join is threaded through unchanged: the rule
1182        // only rewrites inner joins. This exercises the context-propagation paths for
1183        // a non-inner join type, whose existence side contributes no live columns.
1184        let left = scan("l", &test_schema(), Constraints::default())?;
1185        let right = scan("r", &test_schema(), Constraints::default())?;
1186        let plan = LogicalPlanBuilder::from(left)
1187            .join(
1188                right,
1189                JoinType::LeftSemi,
1190                (vec!["l.id"], vec!["r.id"]),
1191                None,
1192            )?
1193            .project(vec![col("l.x")])?
1194            .build()?;
1195
1196        assert_optimized_plan_equal!(plan, @r"
1197        Projection: l.x
1198          LeftSemi Join: l.id = r.id
1199            TableScan: l
1200            TableScan: r
1201        ")
1202    }
1203
1204    fn left_join_right() -> Result<LogicalPlanBuilder> {
1205        left_join_right_with_constraints(Constraints::default())
1206    }
1207
1208    fn left_join_right_with_constraints(
1209        right_constraints: Constraints,
1210    ) -> Result<LogicalPlanBuilder> {
1211        let left = scan("l", &test_schema(), Constraints::default())?;
1212        let right = scan("r", &test_schema(), right_constraints)?;
1213
1214        LogicalPlanBuilder::from(left).join(
1215            right,
1216            Inner,
1217            (vec!["l.id"], vec!["r.id"]),
1218            None,
1219        )
1220    }
1221
1222    fn left_with_constraints_join_right(
1223        left_constraints: Constraints,
1224    ) -> Result<LogicalPlanBuilder> {
1225        let left = scan("l", &test_schema(), left_constraints)?;
1226        let right = scan("r", &test_schema(), Constraints::default())?;
1227
1228        LogicalPlanBuilder::from(left).join(
1229            right,
1230            Inner,
1231            (vec!["l.id"], vec!["r.id"]),
1232            None,
1233        )
1234    }
1235
1236    fn scan(
1237        name: &str,
1238        schema: &Schema,
1239        constraints: Constraints,
1240    ) -> Result<datafusion_expr::logical_plan::LogicalPlan> {
1241        if constraints.is_empty() {
1242            table_scan(Some(name), schema, None)?.build()
1243        } else {
1244            LogicalPlanBuilder::scan(
1245                name,
1246                table_source_with_constraints(schema, constraints),
1247                None,
1248            )?
1249            .build()
1250        }
1251    }
1252
1253    fn test_schema() -> Schema {
1254        Schema::new(vec![
1255            Field::new("id", DataType::Int32, false),
1256            Field::new("x", DataType::Int32, true),
1257            Field::new("y", DataType::Int32, true),
1258        ])
1259    }
1260
1261    fn primary_key_on_id() -> Constraints {
1262        Constraints::new_unverified(vec![Constraint::PrimaryKey(vec![0])])
1263    }
1264
1265    /// A nullable unique key on column `x` (index 1). `Unique` (unlike
1266    /// `PrimaryKey`) marks the dependency as nullable, which is what gates the
1267    /// rewrite on the join's `null_equality`.
1268    fn unique_on_x() -> Constraints {
1269        Constraints::new_unverified(vec![Constraint::Unique(vec![1])])
1270    }
1271
1272    /// A composite primary key spanning columns `id` and `x` (indices 0 and 1).
1273    fn composite_primary_key_on_id_x() -> Constraints {
1274        Constraints::new_unverified(vec![Constraint::PrimaryKey(vec![0, 1])])
1275    }
1276}