Skip to main content

datafusion_optimizer/
optimizer.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//! [`Optimizer`] and [`OptimizerRule`]
19
20use std::fmt::Debug;
21use std::sync::Arc;
22
23use chrono::{DateTime, Utc};
24use datafusion_expr::registry::FunctionRegistry;
25use datafusion_expr::{InvariantLevel, assert_expected_schema};
26use log::{debug, warn};
27
28use datafusion_common::alias::AliasGenerator;
29use datafusion_common::config::ConfigOptions;
30use datafusion_common::instant::Instant;
31use datafusion_common::tree_node::{
32    Transformed, TreeNode, TreeNodeRecursion, TreeNodeRewriter,
33};
34use datafusion_common::{DFSchema, DataFusionError, HashSet, Result, internal_err};
35use datafusion_expr::dml::CopyTo;
36use datafusion_expr::logical_plan::LogicalPlan;
37use datafusion_expr::{
38    Aggregate, Analyze, CreateMemoryTable, CreateView, DdlStatement, Distinct,
39    DistinctOn, DmlStatement, Explain, Expr, Extension, Filter, Join, Limit, Projection,
40    RecursiveQuery, Repartition, Sort, Statement, Subquery, SubqueryAlias, Union, Unnest,
41    Window,
42};
43
44use crate::common_subexpr_eliminate::CommonSubexprEliminate;
45use crate::decorrelate_lateral_join::DecorrelateLateralJoin;
46use crate::decorrelate_predicate_subquery::DecorrelatePredicateSubquery;
47use crate::eliminate_cross_join::EliminateCrossJoin;
48use crate::eliminate_duplicated_expr::EliminateDuplicatedExpr;
49use crate::eliminate_filter::EliminateFilter;
50use crate::eliminate_group_by_constant::EliminateGroupByConstant;
51use crate::eliminate_join::EliminateJoin;
52use crate::eliminate_limit::EliminateLimit;
53use crate::eliminate_outer_join::EliminateOuterJoin;
54use crate::extract_equijoin_predicate::ExtractEquijoinPredicate;
55use crate::extract_leaf_expressions::{ExtractLeafExpressions, PushDownLeafProjections};
56use crate::filter_null_join_keys::FilterNullJoinKeys;
57use crate::optimize_projections::OptimizeProjections;
58use crate::optimize_unions::OptimizeUnions;
59use crate::plan_signature::LogicalPlanSignature;
60use crate::propagate_empty_relation::PropagateEmptyRelation;
61use crate::push_down_filter::PushDownFilter;
62use crate::push_down_limit::PushDownLimit;
63use crate::replace_distinct_aggregate::ReplaceDistinctWithAggregate;
64use crate::rewrite_set_comparison::RewriteSetComparison;
65use crate::scalar_subquery_to_join::ScalarSubqueryToJoin;
66use crate::simplify_expressions::SimplifyExpressions;
67use crate::single_distinct_to_groupby::SingleDistinctToGroupBy;
68use crate::unions_to_filter::UnionsToFilter;
69use crate::utils::log_plan;
70
71/// Transforms one [`LogicalPlan`] into another which computes the same results,
72/// but in a potentially more efficient way.
73///
74/// See notes on [`Self::rewrite`] for details on how to implement an `OptimizerRule`.
75///
76/// To change the semantics of a `LogicalPlan`, see [`AnalyzerRule`].
77///
78/// Use [`SessionState::add_optimizer_rule`] to register additional
79/// `OptimizerRule`s.
80///
81/// [`AnalyzerRule`]: crate::analyzer::AnalyzerRule
82/// [`SessionState::add_optimizer_rule`]: https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionState.html#method.add_optimizer_rule
83pub trait OptimizerRule: Debug {
84    /// A human readable name for this optimizer rule
85    fn name(&self) -> &str;
86
87    /// How should the rule be applied by the optimizer? See comments on
88    /// [`ApplyOrder`] for details.
89    ///
90    /// If returns `None`, the default, the rule must handle recursion itself
91    fn apply_order(&self) -> Option<ApplyOrder> {
92        None
93    }
94
95    /// Does this rule support rewriting owned plans (rather than by reference)?
96    #[deprecated(since = "47.0.0", note = "This method is no longer used")]
97    fn supports_rewrite(&self) -> bool {
98        true
99    }
100
101    /// Try to rewrite `plan` to an optimized form, returning [`Transformed::yes`]
102    /// if the plan was rewritten and [`Transformed::no`] if it was not.
103    ///
104    /// # Notes for implementations:
105    ///
106    /// ## Return the same plan if no changes were made
107    ///
108    /// If there are no suitable transformations for the input plan,
109    /// the optimizer should simply return it unmodified.
110    ///
111    /// The optimizer will call `rewrite` several times until a fixed point is
112    /// reached, so it is important that `rewrite` return [`Transformed::no`] if
113    /// the output is the same.
114    ///
115    /// ## Matching on functions
116    ///
117    /// The rule should avoid function-specific transformations, and instead use
118    /// methods on [`ScalarUDFImpl`] and [`AggregateUDFImpl`]. Specifically, the
119    /// rule should not check function names as functions can be overridden, and
120    /// may not have the same semantics as the functions provided with
121    /// DataFusion.
122    ///
123    /// For example, if a rule rewrites a function based on the check
124    /// `func.name() == "sum"`, it may rewrite the plan incorrectly if the
125    /// registered `sum` function has different semantics (for example, the
126    /// `sum` function from the `datafusion-spark` crate).
127    ///
128    /// There are still several cases that rely on function name checking in
129    /// the rules included with DataFusion. Please see [#18643] for more details
130    /// and to help remove these cases.
131    ///
132    /// [`ScalarUDFImpl`]: datafusion_expr::ScalarUDFImpl
133    /// [`AggregateUDFImpl`]: datafusion_expr::ScalarUDFImpl
134    /// [#18643]: https://github.com/apache/datafusion/issues/18643
135    fn rewrite(
136        &self,
137        _plan: LogicalPlan,
138        _config: &dyn OptimizerConfig,
139    ) -> Result<Transformed<LogicalPlan>, DataFusionError> {
140        internal_err!("rewrite is not implemented for {}", self.name())
141    }
142}
143
144/// Options to control the DataFusion Optimizer.
145pub trait OptimizerConfig {
146    /// Return the time at which the query execution started. This
147    /// time is used as the value for `now()`. If `None`, time-dependent
148    /// functions like `now()` will not be simplified during optimization.
149    fn query_execution_start_time(&self) -> Option<DateTime<Utc>>;
150
151    /// Return alias generator used to generate unique aliases for subqueries
152    fn alias_generator(&self) -> &Arc<AliasGenerator>;
153
154    fn options(&self) -> Arc<ConfigOptions>;
155
156    fn function_registry(&self) -> Option<&dyn FunctionRegistry> {
157        None
158    }
159}
160
161/// A standalone [`OptimizerConfig`] that can be used independently
162/// of DataFusion's config management
163#[derive(Debug)]
164pub struct OptimizerContext {
165    /// Query execution start time that can be used to rewrite
166    /// expressions such as `now()` to use a literal value instead.
167    /// If `None`, time-dependent functions will not be simplified.
168    query_execution_start_time: Option<DateTime<Utc>>,
169
170    /// Alias generator used to generate unique aliases for subqueries
171    alias_generator: Arc<AliasGenerator>,
172
173    options: Arc<ConfigOptions>,
174}
175
176impl OptimizerContext {
177    /// Create optimizer config
178    pub fn new() -> Self {
179        let mut options = ConfigOptions::default();
180        options.optimizer.filter_null_join_keys = true;
181
182        Self::new_with_config_options(Arc::new(options))
183    }
184
185    /// Create a optimizer config with provided [ConfigOptions].
186    pub fn new_with_config_options(options: Arc<ConfigOptions>) -> Self {
187        Self {
188            query_execution_start_time: Some(Utc::now()),
189            alias_generator: Arc::new(AliasGenerator::new()),
190            options,
191        }
192    }
193
194    /// Specify whether to enable the filter_null_keys rule
195    pub fn filter_null_keys(mut self, filter_null_keys: bool) -> Self {
196        Arc::make_mut(&mut self.options)
197            .optimizer
198            .filter_null_join_keys = filter_null_keys;
199        self
200    }
201
202    /// Set the query execution start time
203    pub fn with_query_execution_start_time(
204        mut self,
205        query_execution_start_time: DateTime<Utc>,
206    ) -> Self {
207        self.query_execution_start_time = Some(query_execution_start_time);
208        self
209    }
210
211    /// Clear the query execution start time. When `None`, time-dependent
212    /// functions like `now()` will not be simplified during optimization.
213    pub fn without_query_execution_start_time(mut self) -> Self {
214        self.query_execution_start_time = None;
215        self
216    }
217
218    /// Specify whether the optimizer should skip rules that produce
219    /// errors, or fail the query
220    pub fn with_skip_failing_rules(mut self, b: bool) -> Self {
221        Arc::make_mut(&mut self.options).optimizer.skip_failed_rules = b;
222        self
223    }
224
225    /// Specify how many times to attempt to optimize the plan
226    pub fn with_max_passes(mut self, v: u8) -> Self {
227        Arc::make_mut(&mut self.options).optimizer.max_passes = v as usize;
228        self
229    }
230}
231
232impl Default for OptimizerContext {
233    /// Create optimizer config
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239impl OptimizerConfig for OptimizerContext {
240    fn query_execution_start_time(&self) -> Option<DateTime<Utc>> {
241        self.query_execution_start_time
242    }
243
244    fn alias_generator(&self) -> &Arc<AliasGenerator> {
245        &self.alias_generator
246    }
247
248    fn options(&self) -> Arc<ConfigOptions> {
249        Arc::clone(&self.options)
250    }
251}
252
253/// A rule-based optimizer.
254#[derive(Clone, Debug)]
255pub struct Optimizer {
256    /// All optimizer rules to apply
257    pub rules: Vec<Arc<dyn OptimizerRule + Send + Sync>>,
258}
259
260/// Specifies how recursion for an `OptimizerRule` should be handled.
261///
262/// * `Some(apply_order)`: The Optimizer will recursively apply the rule to the plan.
263/// * `None`: the rule must handle any required recursion itself.
264#[derive(Debug, Clone, Copy, PartialEq)]
265pub enum ApplyOrder {
266    /// Apply the rule to the node before its inputs
267    TopDown,
268    /// Apply the rule to the node after its inputs
269    BottomUp,
270}
271
272impl Default for Optimizer {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl Optimizer {
279    /// Create a new optimizer using the recommended list of rules
280    pub fn new() -> Self {
281        // NOTEs:
282        // - The order of rules in this list is important, as it determines the
283        //   order in which they are applied.
284        // - Adding a new rule here is expensive as it will be applied to all
285        //   queries, and will likely increase the optimization time. Please extend
286        //   existing rules when possible, rather than adding a new rule.
287        //   If you do add a new rule considering having aggressive no-op paths
288        //   (e.g. if the plan doesn't contain any of the nodes you are looking for
289        //    return `Transformed::no`; only works if you control the traversal).
290        let rules: Vec<Arc<dyn OptimizerRule + Sync + Send>> = vec![
291            Arc::new(RewriteSetComparison::new()),
292            Arc::new(OptimizeUnions::new()),
293            Arc::new(UnionsToFilter::new()),
294            Arc::new(SimplifyExpressions::new()),
295            Arc::new(ReplaceDistinctWithAggregate::new()),
296            Arc::new(EliminateJoin::new()),
297            Arc::new(DecorrelatePredicateSubquery::new()),
298            Arc::new(ScalarSubqueryToJoin::new()),
299            Arc::new(DecorrelateLateralJoin::new()),
300            Arc::new(ExtractEquijoinPredicate::new()),
301            Arc::new(EliminateDuplicatedExpr::new()),
302            Arc::new(EliminateFilter::new()),
303            Arc::new(EliminateCrossJoin::new()),
304            Arc::new(EliminateLimit::new()),
305            Arc::new(PropagateEmptyRelation::new()),
306            Arc::new(FilterNullJoinKeys::default()),
307            Arc::new(EliminateOuterJoin::new()),
308            // Filters can't be pushed down past Limits, we should do PushDownFilter after PushDownLimit
309            Arc::new(PushDownLimit::new()),
310            Arc::new(PushDownFilter::new()),
311            Arc::new(SingleDistinctToGroupBy::new()),
312            // The previous optimizations added expressions and projections,
313            // that might benefit from the following rules
314            Arc::new(EliminateGroupByConstant::new()),
315            Arc::new(CommonSubexprEliminate::new()),
316            Arc::new(ExtractLeafExpressions::new()),
317            Arc::new(PushDownLeafProjections::new()),
318            Arc::new(OptimizeProjections::new()),
319        ];
320
321        Self::with_rules(rules)
322    }
323
324    /// Create a new optimizer with the given rules
325    pub fn with_rules(rules: Vec<Arc<dyn OptimizerRule + Send + Sync>>) -> Self {
326        Self { rules }
327    }
328}
329
330/// Recursively rewrites LogicalPlans
331struct Rewriter<'a> {
332    apply_order: ApplyOrder,
333    rule: &'a dyn OptimizerRule,
334    config: &'a dyn OptimizerConfig,
335}
336
337impl<'a> Rewriter<'a> {
338    fn new(
339        apply_order: ApplyOrder,
340        rule: &'a dyn OptimizerRule,
341        config: &'a dyn OptimizerConfig,
342    ) -> Self {
343        Self {
344            apply_order,
345            rule,
346            config,
347        }
348    }
349}
350
351impl TreeNodeRewriter for Rewriter<'_> {
352    type Node = LogicalPlan;
353
354    fn f_down(&mut self, node: LogicalPlan) -> Result<Transformed<LogicalPlan>> {
355        if self.apply_order == ApplyOrder::TopDown {
356            self.rule.rewrite(node, self.config)
357        } else {
358            Ok(Transformed::no(node))
359        }
360    }
361
362    fn f_up(&mut self, node: LogicalPlan) -> Result<Transformed<LogicalPlan>> {
363        if self.apply_order == ApplyOrder::BottomUp {
364            self.rule.rewrite(node, self.config)
365        } else {
366            Ok(Transformed::no(node))
367        }
368    }
369}
370
371/// Applies `f` to each child (input) of `plan` in place, using
372/// [`Arc::make_mut`] for copy-on-write semantics on `Arc<LogicalPlan>`
373/// children. When the `Arc` refcount is 1 (the common case here)
374/// `Arc::make_mut` hands out a `&mut` without cloning; when it is >1 the
375/// inner value is cloned first.
376///
377/// Returns `Ok(true)` if any child was modified by `f`.
378///
379/// This is deliberately private to the optimizer rather than a method on
380/// [`LogicalPlan`]: it is an implementation detail of in-place rewriting, and
381/// the `Arc::make_mut` approach does not generalize to the other tree types
382/// (`Expr` children are `Box`ed; `PhysicalExpr`/`ExecutionPlan` children are
383/// `Arc<dyn _>`, which `Arc::make_mut` cannot handle). If `TreeNode` ever
384/// grows an in-place traversal this logic can move there.
385///
386/// # Error semantics
387///
388/// If `f` returns `Err` for a child, that error is returned immediately;
389/// children visited earlier keep whatever modifications `f` already applied
390/// to them — they are **not** rolled back.
391fn map_children_mut<F: FnMut(&mut LogicalPlan) -> Result<bool>>(
392    plan: &mut LogicalPlan,
393    mut f: F,
394) -> Result<bool> {
395    Ok(match plan {
396        LogicalPlan::Projection(Projection { input, .. })
397        | LogicalPlan::Filter(Filter { input, .. })
398        | LogicalPlan::Repartition(Repartition { input, .. })
399        | LogicalPlan::Window(Window { input, .. })
400        | LogicalPlan::Aggregate(Aggregate { input, .. })
401        | LogicalPlan::Sort(Sort { input, .. })
402        | LogicalPlan::Limit(Limit { input, .. })
403        | LogicalPlan::SubqueryAlias(SubqueryAlias { input, .. })
404        | LogicalPlan::Analyze(Analyze { input, .. })
405        | LogicalPlan::Dml(DmlStatement { input, .. })
406        | LogicalPlan::Copy(CopyTo { input, .. })
407        | LogicalPlan::Unnest(Unnest { input, .. }) => f(Arc::make_mut(input))?,
408        LogicalPlan::Subquery(Subquery { subquery, .. }) => f(Arc::make_mut(subquery))?,
409        LogicalPlan::Join(Join { left, right, .. }) => {
410            let l = f(Arc::make_mut(left))?;
411            let r = f(Arc::make_mut(right))?;
412            l || r
413        }
414        LogicalPlan::Union(Union { inputs, .. }) => {
415            let mut changed = false;
416            for input in inputs {
417                changed |= f(Arc::make_mut(input))?;
418            }
419            changed
420        }
421        LogicalPlan::Distinct(Distinct::All(input)) => f(Arc::make_mut(input))?,
422        LogicalPlan::Distinct(Distinct::On(DistinctOn { input, .. })) => {
423            f(Arc::make_mut(input))?
424        }
425        LogicalPlan::Explain(Explain { plan, .. }) => f(Arc::make_mut(plan))?,
426        LogicalPlan::Ddl(DdlStatement::CreateMemoryTable(CreateMemoryTable {
427            input,
428            ..
429        }))
430        | LogicalPlan::Ddl(DdlStatement::CreateView(CreateView { input, .. })) => {
431            f(Arc::make_mut(input))?
432        }
433        LogicalPlan::RecursiveQuery(RecursiveQuery {
434            static_term,
435            recursive_term,
436            ..
437        }) => {
438            let s = f(Arc::make_mut(static_term))?;
439            let r = f(Arc::make_mut(recursive_term))?;
440            s || r
441        }
442        LogicalPlan::Statement(Statement::Prepare(p)) => f(Arc::make_mut(&mut p.input))?,
443        LogicalPlan::Extension(Extension { node }) => {
444            let inputs = node.inputs();
445            if inputs.is_empty() {
446                false
447            } else {
448                // Extension nodes don't expose mutable children,
449                // fall back to the ownership-based API
450                let mut changed = false;
451                let exprs = node.expressions();
452                let new_inputs: Vec<LogicalPlan> = inputs
453                    .into_iter()
454                    .map(|input| {
455                        let mut plan = input.clone();
456                        if f(&mut plan)? {
457                            changed = true;
458                        }
459                        Ok(plan)
460                    })
461                    .collect::<Result<Vec<_>>>()?;
462                if changed {
463                    *node = node.with_exprs_and_inputs(exprs, new_inputs)?;
464                }
465                changed
466            }
467        }
468        // plans without inputs
469        LogicalPlan::TableScan { .. }
470        | LogicalPlan::EmptyRelation { .. }
471        | LogicalPlan::Values { .. }
472        | LogicalPlan::DescribeTable(_)
473        | LogicalPlan::Ddl(DdlStatement::CreateExternalTable(_))
474        | LogicalPlan::Ddl(DdlStatement::CreateCatalogSchema(_))
475        | LogicalPlan::Ddl(DdlStatement::CreateCatalog(_))
476        | LogicalPlan::Ddl(DdlStatement::CreateIndex(_))
477        | LogicalPlan::Ddl(DdlStatement::DropTable(_))
478        | LogicalPlan::Ddl(DdlStatement::DropView(_))
479        | LogicalPlan::Ddl(DdlStatement::DropCatalogSchema(_))
480        | LogicalPlan::Ddl(DdlStatement::CreateFunction(_))
481        | LogicalPlan::Ddl(DdlStatement::DropFunction(_))
482        | LogicalPlan::Statement(_) => false,
483    })
484}
485
486/// Rewrites a plan tree in place using `Arc::make_mut` for
487/// copy-on-write semantics on `Arc<LogicalPlan>` children.
488///
489/// This avoids the `Arc::unwrap_or_clone` + `Arc::new` cycle that the
490/// ownership-based `TreeNode::rewrite` performs at every child node.
491///
492/// # Error semantics
493///
494/// On `Err`, `*plan` is left in an **unspecified** state and must not be used.
495/// Note this is different than consuming APIs such as [`TreeNode::rewrite`]
496/// where the original plan is freed and no longer available on error
497#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
498fn rewrite_plan_in_place(
499    plan: &mut LogicalPlan,
500    apply_order: ApplyOrder,
501    rule: &dyn OptimizerRule,
502    config: &dyn OptimizerConfig,
503) -> Result<bool> {
504    // f_down phase
505    let mut changed = false;
506    if apply_order == ApplyOrder::TopDown {
507        // `rule.rewrite()` takes the plan by value, so bridge the `&mut` to an
508        // owned value with `std::mem::take`. `LogicalPlan::default()` is a cheap
509        // empty placeholder (shared empty schema, no allocation) and is
510        // overwritten with the rule's output on the next line.
511        let owned = std::mem::take(plan);
512        let result = rule.rewrite(owned, config)?;
513        *plan = result.data;
514        changed |= result.transformed;
515        // Respect TreeNodeRecursion::Stop/Jump from the rule
516        if result.tnr == TreeNodeRecursion::Stop {
517            return Ok(changed);
518        }
519    }
520
521    let mut child_schema_changed = false;
522    let children_changed = map_children_mut(plan, |child| {
523        let old_schema = Arc::clone(child.schema());
524        let child_changed = rewrite_plan_in_place(child, apply_order, rule, config)?;
525        if child_changed && old_schema.as_ref() != child.schema().as_ref() {
526            child_schema_changed = true;
527        }
528        Ok(child_changed)
529    })?;
530    changed |= children_changed;
531
532    if child_schema_changed {
533        // Child rewrites can change their output schemas. Recompute the current
534        // node before later rules use positional requirements from that schema.
535        let owned = std::mem::take(plan);
536        *plan = owned.recompute_schema()?;
537    }
538
539    // f_up phase
540    if apply_order == ApplyOrder::BottomUp {
541        let owned = std::mem::take(plan);
542        let result = rule.rewrite(owned, config)?;
543        *plan = result.data;
544        changed |= result.transformed;
545    }
546
547    Ok(changed)
548}
549
550/// Returns true if the plan contains any subquery expressions
551/// (EXISTS, IN subquery, scalar subquery, set comparison).
552///
553/// Used to determine whether the more expensive `rewrite_with_subqueries`
554/// traversal is needed. When the plan has no subqueries, the cheaper
555/// `rewrite` traversal is sufficient since all plan nodes are reachable
556/// via direct children.
557fn plan_has_subqueries(plan: &LogicalPlan) -> bool {
558    let mut found = false;
559    let _ = plan.apply(|node| {
560        if found {
561            return Ok(TreeNodeRecursion::Stop);
562        }
563        node.apply_expressions(|expr| {
564            if found {
565                return Ok(TreeNodeRecursion::Stop);
566            }
567            expr.apply(|e| {
568                if matches!(
569                    e,
570                    Expr::Exists(_)
571                        | Expr::InSubquery(_)
572                        | Expr::SetComparison(_)
573                        | Expr::ScalarSubquery(_)
574                ) {
575                    found = true;
576                    Ok(TreeNodeRecursion::Stop)
577                } else {
578                    Ok(TreeNodeRecursion::Continue)
579                }
580            })
581        })?;
582        Ok(if found {
583            TreeNodeRecursion::Stop
584        } else {
585            TreeNodeRecursion::Continue
586        })
587    });
588    found
589}
590
591impl Optimizer {
592    /// Optimizes the logical plan by applying optimizer rules, and
593    /// invoking observer function after each call
594    pub fn optimize<F>(
595        &self,
596        plan: LogicalPlan,
597        config: &dyn OptimizerConfig,
598        mut observer: F,
599    ) -> Result<LogicalPlan>
600    where
601        F: FnMut(&LogicalPlan, &dyn OptimizerRule),
602    {
603        // verify LP is valid, before the first LP optimizer pass.
604        plan.check_invariants(InvariantLevel::Executable)
605            .map_err(|e| e.context("Invalid input plan before LP Optimizers"))?;
606
607        let start_time = Instant::now();
608        let options = config.options();
609        let mut new_plan = plan;
610
611        let mut previous_plans = HashSet::with_capacity(16);
612        previous_plans.insert(LogicalPlanSignature::new(&new_plan));
613
614        let starting_schema = Arc::clone(new_plan.schema());
615
616        let mut i = 0;
617        while i < options.optimizer.max_passes {
618            log_plan(&format!("Optimizer input (pass {i})"), &new_plan);
619
620            // Track subquery presence across the pass. Refresh after changed
621            // rules so decorrelation can move later rules onto the in-place
622            // path; that path refreshes parent schemas after child schemas
623            // change.
624            let mut has_subqueries = plan_has_subqueries(&new_plan);
625
626            for rule in &self.rules {
627                // If skipping failed rules, copy plan before attempting to rewrite
628                // as rewriting is destructive
629                let prev_plan = options
630                    .optimizer
631                    .skip_failed_rules
632                    .then(|| new_plan.clone());
633
634                let starting_schema = Arc::clone(new_plan.schema());
635
636                let result = match rule.apply_order() {
637                    // optimizer handles recursion
638                    Some(apply_order) => {
639                        if has_subqueries {
640                            // Plans with subqueries need the full
641                            // rewrite_with_subqueries traversal to
642                            // recurse into subquery plans.
643                            new_plan.rewrite_with_subqueries(
644                                &mut Rewriter::new(
645                                    apply_order,
646                                    rule.as_ref(),
647                                    config,
648                                ),
649                            )
650                        } else {
651                            // No subqueries: use in-place rewriting
652                            // with Arc::make_mut for zero-cost CoW on
653                            // children, avoiding Arc unwrap/rewrap.
654                            //
655                            // On error `new_plan` is left in an unspecified
656                            // state (see `rewrite_plan_in_place`); the result
657                            // handling below discards it, restoring `prev_plan`
658                            // when `skip_failed_rules` is set or propagating
659                            // the error otherwise.
660                            rewrite_plan_in_place(
661                                &mut new_plan,
662                                apply_order,
663                                rule.as_ref(),
664                                config,
665                            )
666                            .map(|transformed| {
667                                Transformed::new_transformed(
668                                    std::mem::take(&mut new_plan),
669                                    transformed,
670                                )
671                            })
672                        }
673                    }
674                    // rule handles recursion itself
675                    None => {
676                        rule.rewrite(new_plan, config)
677                    },
678                }
679                .and_then(|tnr| {
680                    // run checks optimizer invariant checks, per optimizer rule applied
681                    assert_valid_optimization(&tnr.data, &starting_schema)
682                        .map_err(|e| e.context(format!("Check optimizer-specific invariants after optimizer rule: {}", rule.name())))?;
683
684                    // run LP invariant checks only in debug mode for performance reasons
685                    #[cfg(debug_assertions)]
686                    tnr.data.check_invariants(InvariantLevel::Executable)
687                        .map_err(|e| e.context(format!("Invalid (non-executable) plan after Optimizer rule: {}", rule.name())))?;
688
689                    Ok(tnr)
690                });
691
692                // Handle results
693                match (result, prev_plan) {
694                    // OptimizerRule was successful
695                    (
696                        Ok(Transformed {
697                            data, transformed, ..
698                        }),
699                        _,
700                    ) => {
701                        new_plan = data;
702                        observer(&new_plan, rule.as_ref());
703                        if transformed {
704                            has_subqueries = plan_has_subqueries(&new_plan);
705                            log_plan(rule.name(), &new_plan);
706                        } else {
707                            debug!(
708                                "Plan unchanged by optimizer rule '{}' (pass {})",
709                                rule.name(),
710                                i
711                            );
712                        }
713                    }
714                    // OptimizerRule was unsuccessful, but skipped failed rules is on
715                    // so use the previous plan
716                    (Err(e), Some(orig_plan)) => {
717                        // Note to future readers: if you see this warning it signals a
718                        // bug in the DataFusion optimizer. Please consider filing a ticket
719                        // https://github.com/apache/datafusion
720                        warn!(
721                            "Skipping optimizer rule '{}' due to unexpected error: {}",
722                            rule.name(),
723                            e
724                        );
725                        new_plan = orig_plan;
726                    }
727                    // OptimizerRule was unsuccessful, but skipped failed rules is off, return error
728                    (Err(e), None) => {
729                        return Err(e.context(format!(
730                            "Optimizer rule '{}' failed",
731                            rule.name()
732                        )));
733                    }
734                }
735            }
736            log_plan(&format!("Optimized plan (pass {i})"), &new_plan);
737
738            // HashSet::insert returns, whether the value was newly inserted.
739            let plan_is_fresh =
740                previous_plans.insert(LogicalPlanSignature::new(&new_plan));
741            if !plan_is_fresh {
742                // plan did not change, so no need to continue trying to optimize
743                debug!("optimizer pass {i} did not make changes");
744                break;
745            }
746            i += 1;
747        }
748
749        // verify that the optimizer passes only mutated what was permitted.
750        assert_valid_optimization(&new_plan, &starting_schema).map_err(|e| {
751            e.context("Check optimizer-specific invariants after all passes")
752        })?;
753
754        // verify LP is valid, after the last optimizer pass.
755        new_plan
756            .check_invariants(InvariantLevel::Executable)
757            .map_err(|e| {
758                e.context("Invalid (non-executable) plan after LP Optimizers")
759            })?;
760
761        log_plan("Final optimized plan", &new_plan);
762        debug!("Optimizer took {} ms", start_time.elapsed().as_millis());
763        Ok(new_plan)
764    }
765}
766
767/// These are invariants which should hold true before and after [`LogicalPlan`] optimization.
768///
769/// This differs from [`LogicalPlan::check_invariants`], which addresses if a singular
770/// LogicalPlan is valid. Instead, this address if the optimization was valid based upon permitted changes.
771fn assert_valid_optimization(
772    plan: &LogicalPlan,
773    prev_schema: &Arc<DFSchema>,
774) -> Result<()> {
775    // verify invariant: optimizer passes should not change the schema if the schema can't be cast from the previous schema.
776    // Refer to <https://datafusion.apache.org/contributor-guide/specification/invariants.html#logical-schema-is-invariant-under-logical-optimization>
777    assert_expected_schema(prev_schema, plan)?;
778
779    Ok(())
780}
781
782#[cfg(test)]
783mod tests {
784    use std::sync::{Arc, Mutex};
785
786    use datafusion_common::tree_node::Transformed;
787    use datafusion_common::{
788        Column, DFSchema, DFSchemaRef, DataFusionError, Result, assert_contains, plan_err,
789    };
790    use datafusion_expr::logical_plan::EmptyRelation;
791    use datafusion_expr::{
792        Expr, JoinType, LogicalPlan, LogicalPlanBuilder, Projection, col, lit,
793    };
794
795    use crate::optimizer::Optimizer;
796    use crate::test::{test_table_scan, test_table_scan_with_name};
797    use crate::{OptimizerConfig, OptimizerContext, OptimizerRule};
798
799    use super::ApplyOrder;
800
801    #[test]
802    fn skip_failing_rule() {
803        let opt = Optimizer::with_rules(vec![Arc::new(BadRule {})]);
804        let config = OptimizerContext::new().with_skip_failing_rules(true);
805        let plan = LogicalPlan::EmptyRelation(EmptyRelation {
806            produce_one_row: false,
807            schema: Arc::new(DFSchema::empty()),
808        });
809        opt.optimize(plan, &config, &observe).unwrap();
810    }
811
812    #[test]
813    fn no_skip_failing_rule() {
814        let opt = Optimizer::with_rules(vec![Arc::new(BadRule {})]);
815        let config = OptimizerContext::new().with_skip_failing_rules(false);
816        let plan = LogicalPlan::EmptyRelation(EmptyRelation {
817            produce_one_row: false,
818            schema: Arc::new(DFSchema::empty()),
819        });
820        let err = opt.optimize(plan, &config, &observe).unwrap_err();
821        assert_eq!(
822            "Optimizer rule 'bad rule' failed\ncaused by\n\
823            Error during planning: rule failed",
824            err.strip_backtrace()
825        );
826    }
827
828    #[test]
829    fn generate_different_schema() {
830        let opt = Optimizer::with_rules(vec![Arc::new(GetTableScanRule {})]);
831        let config = OptimizerContext::new().with_skip_failing_rules(false);
832        let plan = LogicalPlan::EmptyRelation(EmptyRelation {
833            produce_one_row: false,
834            schema: Arc::new(DFSchema::empty()),
835        });
836        let err = opt.optimize(plan, &config, &observe).unwrap_err();
837
838        // Simplify assert to check the error message contains the expected message
839        assert_contains!(
840            err.strip_backtrace(),
841            "Failed due to a difference in schemas: original schema: DFSchema"
842        );
843    }
844
845    #[test]
846    fn skip_generate_different_schema() {
847        let opt = Optimizer::with_rules(vec![Arc::new(GetTableScanRule {})]);
848        let config = OptimizerContext::new().with_skip_failing_rules(true);
849        let plan = LogicalPlan::EmptyRelation(EmptyRelation {
850            produce_one_row: false,
851            schema: Arc::new(DFSchema::empty()),
852        });
853        opt.optimize(plan, &config, &observe).unwrap();
854    }
855
856    #[test]
857    fn generate_same_schema_different_metadata() -> Result<()> {
858        // if the plan creates more metadata than previously (because
859        // some wrapping functions are removed, etc) do not error
860        let opt = Optimizer::with_rules(vec![Arc::new(GetTableScanRule {})]);
861        let config = OptimizerContext::new().with_skip_failing_rules(false);
862
863        let input = Arc::new(test_table_scan()?);
864        let input_schema = Arc::clone(input.schema());
865
866        let plan = LogicalPlan::Projection(Projection::try_new_with_schema(
867            vec![col("a"), col("b"), col("c")],
868            input,
869            add_metadata_to_fields(input_schema.as_ref()),
870        )?);
871
872        // optimizing should be ok, but the schema will have changed  (no metadata)
873        assert_ne!(plan.schema().as_ref(), input_schema.as_ref());
874        let optimized_plan = opt.optimize(plan, &config, &observe)?;
875        // metadata was removed
876        assert_eq!(optimized_plan.schema().as_ref(), input_schema.as_ref());
877        Ok(())
878    }
879
880    #[test]
881    fn in_place_rewrite_recomputes_parent_schema_when_child_schema_changes() -> Result<()>
882    {
883        let left = LogicalPlanBuilder::from(test_table_scan_with_name("left")?)
884            .project(vec![col("left.a"), col("left.b"), col("left.c")])?
885            .build()?;
886        let right = LogicalPlanBuilder::from(test_table_scan_with_name("right")?)
887            .project(vec![col("right.a"), col("right.b"), col("right.c")])?
888            .build()?;
889        let mut plan = LogicalPlanBuilder::from(left)
890            .join_on(right, JoinType::Inner, [col("left.a").eq(col("right.a"))])?
891            .build()?;
892
893        assert_eq!(plan.schema().fields().len(), 6);
894
895        let changed = super::rewrite_plan_in_place(
896            &mut plan,
897            ApplyOrder::TopDown,
898            &KeepOnlyAProjectionRule {},
899            &OptimizerContext::new(),
900        )?;
901
902        assert!(changed);
903        assert_eq!(plan.schema().fields().len(), 2);
904        assert!(plan.schema().has_column_with_unqualified_name("a"));
905        Ok(())
906    }
907
908    #[test]
909    fn optimizer_detects_plan_equal_to_the_initial() -> Result<()> {
910        // Run a goofy optimizer, which rotates projection columns
911        // [1, 2, 3] -> [2, 3, 1] -> [3, 1, 2] -> [1, 2, 3]
912
913        let opt = Optimizer::with_rules(vec![Arc::new(RotateProjectionRule::new(false))]);
914        let config = OptimizerContext::new().with_max_passes(16);
915
916        let initial_plan = LogicalPlanBuilder::empty(false)
917            .project([lit(1), lit(2), lit(3)])?
918            .project([lit(100)])? // to not trigger changed schema error
919            .build()?;
920
921        let mut plans: Vec<LogicalPlan> = Vec::new();
922        let final_plan =
923            opt.optimize(initial_plan.clone(), &config, |p, _| plans.push(p.clone()))?;
924
925        // initial_plan is not observed, so we have 3 plans
926        assert_eq!(3, plans.len());
927
928        // we got again the initial_plan with [1, 2, 3]
929        assert_eq!(initial_plan, final_plan);
930
931        Ok(())
932    }
933
934    #[test]
935    fn optimizer_detects_plan_equal_to_a_non_initial() -> Result<()> {
936        // Run a goofy optimizer, which reverses and rotates projection columns
937        // [1, 2, 3] -> [3, 2, 1] -> [2, 1, 3] -> [1, 3, 2] -> [3, 2, 1]
938
939        let opt = Optimizer::with_rules(vec![Arc::new(RotateProjectionRule::new(true))]);
940        let config = OptimizerContext::new().with_max_passes(16);
941
942        let initial_plan = LogicalPlanBuilder::empty(false)
943            .project([lit(1), lit(2), lit(3)])?
944            .project([lit(100)])? // to not trigger changed schema error
945            .build()?;
946
947        let mut plans: Vec<LogicalPlan> = Vec::new();
948        let final_plan =
949            opt.optimize(initial_plan, &config, |p, _| plans.push(p.clone()))?;
950
951        // initial_plan is not observed, so we have 4 plans
952        assert_eq!(4, plans.len());
953
954        // we got again the plan with [3, 2, 1]
955        assert_eq!(plans[0], final_plan);
956
957        Ok(())
958    }
959
960    fn add_metadata_to_fields(schema: &DFSchema) -> DFSchemaRef {
961        let new_fields = schema
962            .iter()
963            .enumerate()
964            .map(|(i, (qualifier, field))| {
965                let metadata =
966                    [("key".into(), format!("value {i}"))].into_iter().collect();
967
968                let new_arrow_field = field.as_ref().clone().with_metadata(metadata);
969                (qualifier.cloned(), Arc::new(new_arrow_field))
970            })
971            .collect::<Vec<_>>();
972
973        let new_metadata = schema.metadata().clone();
974        Arc::new(DFSchema::new_with_metadata(new_fields, new_metadata).unwrap())
975    }
976
977    fn observe(_plan: &LogicalPlan, _rule: &dyn OptimizerRule) {}
978
979    #[derive(Default, Debug)]
980    struct BadRule {}
981
982    impl OptimizerRule for BadRule {
983        fn name(&self) -> &str {
984            "bad rule"
985        }
986
987        fn supports_rewrite(&self) -> bool {
988            true
989        }
990
991        fn rewrite(
992            &self,
993            _plan: LogicalPlan,
994            _config: &dyn OptimizerConfig,
995        ) -> Result<Transformed<LogicalPlan>, DataFusionError> {
996            plan_err!("rule failed")
997        }
998    }
999
1000    /// Replaces whatever plan with a single table scan
1001    #[derive(Default, Debug)]
1002    struct GetTableScanRule {}
1003
1004    impl OptimizerRule for GetTableScanRule {
1005        fn name(&self) -> &str {
1006            "get table_scan rule"
1007        }
1008
1009        fn supports_rewrite(&self) -> bool {
1010            true
1011        }
1012
1013        fn rewrite(
1014            &self,
1015            _plan: LogicalPlan,
1016            _config: &dyn OptimizerConfig,
1017        ) -> Result<Transformed<LogicalPlan>> {
1018            let table_scan = test_table_scan()?;
1019            Ok(Transformed::yes(
1020                LogicalPlanBuilder::from(table_scan).build()?,
1021            ))
1022        }
1023    }
1024
1025    #[derive(Default, Debug)]
1026    struct KeepOnlyAProjectionRule {}
1027
1028    impl OptimizerRule for KeepOnlyAProjectionRule {
1029        fn name(&self) -> &str {
1030            "keep_only_a_projection"
1031        }
1032
1033        fn apply_order(&self) -> Option<ApplyOrder> {
1034            Some(ApplyOrder::TopDown)
1035        }
1036
1037        fn supports_rewrite(&self) -> bool {
1038            true
1039        }
1040
1041        fn rewrite(
1042            &self,
1043            plan: LogicalPlan,
1044            _config: &dyn OptimizerConfig,
1045        ) -> Result<Transformed<LogicalPlan>> {
1046            let projection = match plan {
1047                LogicalPlan::Projection(p) => p,
1048                _ => return Ok(Transformed::no(plan)),
1049            };
1050
1051            let expr = Expr::from(Column::from(projection.schema.qualified_field(0)));
1052
1053            Ok(Transformed::yes(LogicalPlan::Projection(
1054                Projection::try_new(vec![expr], Arc::clone(&projection.input))?,
1055            )))
1056        }
1057    }
1058
1059    /// A goofy rule doing rotation of columns in all projections.
1060    ///
1061    /// Useful to test cycle detection.
1062    #[derive(Default, Debug)]
1063    struct RotateProjectionRule {
1064        // reverse exprs instead of rotating on the first pass
1065        reverse_on_first_pass: Mutex<bool>,
1066    }
1067
1068    impl RotateProjectionRule {
1069        fn new(reverse_on_first_pass: bool) -> Self {
1070            Self {
1071                reverse_on_first_pass: Mutex::new(reverse_on_first_pass),
1072            }
1073        }
1074    }
1075
1076    impl OptimizerRule for RotateProjectionRule {
1077        fn name(&self) -> &str {
1078            "rotate_projection"
1079        }
1080
1081        fn apply_order(&self) -> Option<ApplyOrder> {
1082            Some(ApplyOrder::TopDown)
1083        }
1084
1085        fn supports_rewrite(&self) -> bool {
1086            true
1087        }
1088
1089        fn rewrite(
1090            &self,
1091            plan: LogicalPlan,
1092            _config: &dyn OptimizerConfig,
1093        ) -> Result<Transformed<LogicalPlan>> {
1094            let projection = match plan {
1095                LogicalPlan::Projection(p) if p.expr.len() >= 2 => p,
1096                _ => return Ok(Transformed::no(plan)),
1097            };
1098
1099            let mut exprs = projection.expr.clone();
1100
1101            let mut reverse = self.reverse_on_first_pass.lock().unwrap();
1102            if *reverse {
1103                exprs.reverse();
1104                *reverse = false;
1105            } else {
1106                exprs.rotate_left(1);
1107            }
1108
1109            Ok(Transformed::yes(LogicalPlan::Projection(
1110                Projection::try_new(exprs, Arc::clone(&projection.input))?,
1111            )))
1112        }
1113    }
1114}