mz_transform/
lib.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Transformations for relation expressions.
11//!
12//! This crate contains traits, types, and methods suitable for transforming
13//! `MirRelationExpr` types in ways that preserve semantics and improve performance.
14//! The core trait is `Transform`, and many implementors of this trait can be
15//! boxed and iterated over. Some common transformation patterns are wrapped
16//! as `Transform` implementors themselves.
17//!
18//! The crate also contains the beginnings of whole-dataflow optimization,
19//! which uses the same analyses but spanning multiple dataflow elements.
20
21#![warn(missing_docs)]
22#![warn(missing_debug_implementations)]
23
24use std::collections::BTreeMap;
25use std::error::Error;
26use std::panic::AssertUnwindSafe;
27use std::sync::Arc;
28use std::{fmt, iter};
29
30use mz_expr::{MirRelationExpr, MirScalarExpr};
31use mz_ore::id_gen::IdGen;
32use mz_ore::soft_panic_or_log;
33use mz_ore::stack::RecursionLimitError;
34use mz_repr::GlobalId;
35use mz_repr::optimize::OptimizerFeatures;
36use mz_sql::optimizer_metrics::OptimizerMetrics;
37use tracing::error;
38
39use crate::canonicalize_mfp::CanonicalizeMfp;
40use crate::collect_notices::CollectNotices;
41use crate::column_knowledge::ColumnKnowledge;
42use crate::dataflow::DataflowMetainfo;
43use crate::demand::Demand;
44use crate::equivalence_propagation::EquivalencePropagation;
45use crate::fold_constants::FoldConstants;
46use crate::join_implementation::JoinImplementation;
47use crate::literal_constraints::LiteralConstraints;
48use crate::literal_lifting::LiteralLifting;
49use crate::movement::ProjectionPushdown;
50use crate::non_null_requirements::NonNullRequirements;
51use crate::normalize_lets::NormalizeLets;
52use crate::normalize_ops::NormalizeOps;
53use crate::predicate_pushdown::PredicatePushdown;
54use crate::reduce_elision::ReduceElision;
55use crate::reduce_reduction::ReduceReduction;
56use crate::reduction_pushdown::ReductionPushdown;
57use crate::redundant_join::RedundantJoin;
58use crate::reprtypecheck::{SharedContext as ReprSharedContext, Typecheck as ReprTypecheck};
59use crate::semijoin_idempotence::SemijoinIdempotence;
60use crate::threshold_elision::ThresholdElision;
61use crate::union_cancel::UnionBranchCancellation;
62use crate::will_distinct::WillDistinct;
63
64pub use dataflow::optimize_dataflow;
65
66pub mod analysis;
67pub mod canonicalization;
68pub mod canonicalize_mfp;
69pub mod collect_notices;
70pub mod column_knowledge;
71pub mod compound;
72pub mod cse;
73pub mod dataflow;
74pub mod demand;
75pub mod equivalence_propagation;
76pub mod fold_constants;
77pub mod fusion;
78pub mod join_implementation;
79pub mod literal_constraints;
80pub mod literal_lifting;
81pub mod monotonic;
82pub mod movement;
83pub mod non_null_requirements;
84pub mod normalize_lets;
85pub mod normalize_ops;
86pub mod notice;
87pub mod ordering;
88pub mod predicate_pushdown;
89pub mod reduce_elision;
90pub mod reduce_reduction;
91pub mod reduction_pushdown;
92pub mod redundant_join;
93pub mod reprtypecheck;
94pub mod semijoin_idempotence;
95pub mod threshold_elision;
96pub mod union_cancel;
97pub mod will_distinct;
98
99/// Compute the conjunction of a variadic number of expressions.
100#[macro_export]
101macro_rules! all {
102    ($x:expr) => ($x);
103    ($($x:expr,)+) => ( $($x)&&+ )
104}
105
106/// Compute the disjunction of a variadic number of expressions.
107#[macro_export]
108macro_rules! any {
109    ($x:expr) => ($x);
110    ($($x:expr,)+) => ( $($x)||+ )
111}
112
113/// Arguments that get threaded through all transforms, plus a `DataflowMetainfo` that can be
114/// manipulated by the transforms.
115#[derive(Debug)]
116pub struct TransformCtx<'a> {
117    /// The global ID for this query (if it exists).
118    pub global_id: Option<GlobalId>,
119    /// The indexes accessible.
120    pub indexes: &'a dyn IndexOracle,
121    /// Statistical estimates.
122    pub stats: &'a dyn StatisticsOracle,
123    /// Features passed to the enclosing `Optimizer`.
124    pub features: &'a OptimizerFeatures,
125    /// Representation typechecking context.
126    pub repr_typecheck_ctx: &'a ReprSharedContext,
127    /// Transforms can use this field to communicate information outside the result plans.
128    pub df_meta: &'a mut DataflowMetainfo,
129    /// Metrics for the optimizer.
130    pub metrics: Option<&'a mut OptimizerMetrics>,
131    /// The last hash of the query, if known.
132    pub last_hash: BTreeMap<GlobalId, u64>,
133}
134
135const FOLD_CONSTANTS_LIMIT: usize = 10000;
136
137impl<'a> TransformCtx<'a> {
138    /// Generates a [`TransformCtx`] instance for the local MIR optimization
139    /// stage.
140    ///
141    /// Used to call [`Optimizer::optimize`] on a
142    /// [`Optimizer::logical_optimizer`] in order to transform a stand-alone
143    /// [`MirRelationExpr`].
144    pub fn local(
145        features: &'a OptimizerFeatures,
146        repr_typecheck_ctx: &'a ReprSharedContext,
147        df_meta: &'a mut DataflowMetainfo,
148        metrics: Option<&'a mut OptimizerMetrics>,
149        global_id: Option<GlobalId>,
150    ) -> Self {
151        Self {
152            indexes: &EmptyIndexOracle,
153            stats: &EmptyStatisticsOracle,
154            global_id,
155            features,
156            repr_typecheck_ctx,
157            df_meta,
158            metrics,
159            last_hash: Default::default(),
160        }
161    }
162
163    /// Generates a [`TransformCtx`] instance for the global MIR optimization
164    /// stage.
165    ///
166    /// Used to call [`optimize_dataflow`].
167    pub fn global(
168        indexes: &'a dyn IndexOracle,
169        stats: &'a dyn StatisticsOracle,
170        features: &'a OptimizerFeatures,
171        repr_typecheck_ctx: &'a ReprSharedContext,
172        df_meta: &'a mut DataflowMetainfo,
173        metrics: Option<&'a mut OptimizerMetrics>,
174    ) -> Self {
175        Self {
176            indexes,
177            stats,
178            global_id: None,
179            features,
180            df_meta,
181            repr_typecheck_ctx,
182            metrics,
183            last_hash: Default::default(),
184        }
185    }
186
187    fn repr_typecheck(&self) -> ReprSharedContext {
188        Arc::clone(self.repr_typecheck_ctx)
189    }
190
191    /// Lets self know the id of the object that is being optimized.
192    pub fn set_global_id(&mut self, global_id: GlobalId) {
193        self.global_id = Some(global_id);
194    }
195
196    fn reset_global_id(&mut self) {
197        self.global_id = None;
198    }
199
200    /// Updates `last_hash` with the hash of the given MIR plan for the id `self.global_id`.
201    /// Returns the hash.
202    fn update_last_hash(&mut self, plan: &MirRelationExpr) -> u64 {
203        let hash = plan.hash_to_u64();
204        if let Some(id) = self.global_id {
205            self.last_hash.insert(id, hash);
206        }
207        hash
208    }
209}
210
211/// Types capable of transforming relation expressions.
212pub trait Transform: fmt::Debug {
213    /// Transforms a relation into a functionally equivalent relation.
214    ///
215    /// This is a wrapper around `actually_perform_transform` that also
216    /// measures the time taken and updates the optimizer metrics.
217    fn transform(
218        &self,
219        relation: &mut MirRelationExpr,
220        args: &mut TransformCtx,
221    ) -> Result<(), TransformError> {
222        let hash_before = args
223            .global_id
224            .and_then(|id| args.last_hash.get(&id).copied())
225            .unwrap_or_else(|| relation.hash_to_u64());
226
227        mz_ore::soft_assert_eq_no_log!(hash_before, relation.hash_to_u64(), "cached hash clash");
228        // actually run the transform, recording the time taken
229        let start = std::time::Instant::now();
230        let res = self.actually_perform_transform(relation, args);
231        let duration = start.elapsed();
232
233        let hash_after = args.update_last_hash(relation);
234        if let Some(metrics) = &mut args.metrics {
235            let transform_name = self.name();
236            metrics.observe_transform_time(transform_name, duration);
237            metrics.inc_transform(hash_before != hash_after, transform_name);
238        }
239
240        res
241    }
242
243    /// Transform a relation into a functionally equivalent relation.
244    ///
245    /// You transform should implement this method, but users should call
246    /// `transform` instead.
247    fn actually_perform_transform(
248        &self,
249        relation: &mut MirRelationExpr,
250        ctx: &mut TransformCtx,
251    ) -> Result<(), TransformError>;
252
253    /// A string describing the transform.
254    ///
255    /// This is useful mainly when iterating through many `Box<Transform>`
256    /// and one wants to judge progress before some defect occurs.
257    fn debug(&self) -> String {
258        format!("{:?}", self)
259    }
260
261    /// A short string naming the transform, as it will be reported in metrics.
262    fn name(&self) -> &'static str;
263}
264
265/// Errors that can occur during a transformation.
266#[derive(Debug, Clone)]
267pub enum TransformError {
268    /// An unstructured error.
269    Internal(String),
270    /// A reference to an apparently unbound identifier.
271    IdentifierMissing(mz_expr::LocalId),
272    /// Notify the caller to panic with the given message.
273    ///
274    /// This is used to bypass catch_unwind-wrapped calls of the optimizer and
275    /// support `SELECT mz_unsafe.mz_panic(<literal>)` statements as a mechanism to kill
276    /// environmentd in various tests.
277    CallerShouldPanic(String),
278}
279
280impl fmt::Display for TransformError {
281    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282        match self {
283            TransformError::Internal(msg) => write!(f, "internal transform error: {}", msg),
284            TransformError::IdentifierMissing(i) => {
285                write!(f, "apparently unbound identifier: {:?}", i)
286            }
287            TransformError::CallerShouldPanic(msg) => {
288                write!(f, "caller should panic with message: {}", msg)
289            }
290        }
291    }
292}
293
294impl Error for TransformError {}
295
296impl From<RecursionLimitError> for TransformError {
297    fn from(error: RecursionLimitError) -> Self {
298        TransformError::Internal(error.to_string())
299    }
300}
301
302/// Implemented by error types that sometimes want to indicate that an error should cause a panic
303/// even in a `catch_unwind` context. Useful for implementing `mz_unsafe.mz_panic('forced panic')`.
304pub trait MaybeShouldPanic {
305    /// Whether the error means that we want a panic. If yes, then returns the error msg.
306    fn should_panic(&self) -> Option<String>;
307}
308
309impl MaybeShouldPanic for TransformError {
310    fn should_panic(&self) -> Option<String> {
311        match self {
312            TransformError::CallerShouldPanic(msg) => Some(msg.to_string()),
313            _ => None,
314        }
315    }
316}
317
318/// Catch panics in the given optimization, and demote them to [`TransformError::Internal`] error.
319///
320/// Additionally, if the result of the optimization is an error (not a panic) that indicates we
321/// should panic, then panic.
322pub fn catch_unwind_optimize<Opt, To, E>(optimization: Opt) -> Result<To, E>
323where
324    Opt: FnOnce() -> Result<To, E>,
325    E: From<TransformError> + MaybeShouldPanic,
326{
327    match mz_ore::panic::catch_unwind_str(AssertUnwindSafe(optimization)) {
328        Ok(Err(e)) if e.should_panic().is_some() => {
329            // Promote a `CallerShouldPanic` error from the result to a proper panic. This is
330            // needed in order to ensure that `mz_unsafe.mz_panic('forced panic')` calls still
331            // panic the caller.
332            panic!("{}", e.should_panic().expect("checked above"));
333        }
334        Ok(result) => result.map_err(|e| e),
335        Err(panic) => {
336            // A panic during optimization is always a bug; log an error so we learn about it.
337            // TODO(teskje): collect and log a backtrace from the panic site
338            tracing::error!("caught a panic during query optimization: {panic}");
339
340            let msg = format!("unexpected panic during query optimization: {panic}");
341            Err(TransformError::Internal(msg).into())
342        }
343    }
344}
345
346/// A trait for a type that can answer questions about what indexes exist.
347pub trait IndexOracle: fmt::Debug {
348    /// Returns an iterator over the indexes that exist on the identified
349    /// collection.
350    ///
351    /// Each index is described by the list of key expressions. If no indexes
352    /// exist for the identified collection, or if the identified collection
353    /// is unknown, the returned iterator will be empty.
354    ///
355    // NOTE(benesch): The allocation here is unfortunate, but on the other hand
356    // you need only allocate when you actually look for an index. Can we do
357    // better somehow? Making the entire optimizer generic over this iterator
358    // type doesn't presently seem worthwhile.
359    fn indexes_on(
360        &self,
361        id: GlobalId,
362    ) -> Box<dyn Iterator<Item = (GlobalId, &[MirScalarExpr])> + '_>;
363}
364
365/// An [`IndexOracle`] that knows about no indexes.
366#[derive(Debug)]
367pub struct EmptyIndexOracle;
368
369impl IndexOracle for EmptyIndexOracle {
370    fn indexes_on(
371        &self,
372        _id: GlobalId,
373    ) -> Box<dyn Iterator<Item = (GlobalId, &[MirScalarExpr])> + '_> {
374        Box::new(iter::empty())
375    }
376}
377
378/// A trait for a type that can estimate statistics about a given `GlobalId`
379pub trait StatisticsOracle: fmt::Debug + Send {
380    /// Returns a cardinality estimate for the given identifier
381    ///
382    /// Returning `None` means "no estimate"; returning `Some(0)` means estimating that the shard backing `id` is empty
383    fn cardinality_estimate(&self, id: GlobalId) -> Option<usize>;
384
385    /// Returns a map from identifiers to sizes
386    fn as_map(&self) -> BTreeMap<GlobalId, usize>;
387}
388
389/// A [`StatisticsOracle`] that knows nothing and can give no estimates.
390#[derive(Debug)]
391pub struct EmptyStatisticsOracle;
392
393impl StatisticsOracle for EmptyStatisticsOracle {
394    fn cardinality_estimate(&self, _: GlobalId) -> Option<usize> {
395        None
396    }
397
398    fn as_map(&self) -> BTreeMap<GlobalId, usize> {
399        BTreeMap::new()
400    }
401}
402
403/// A sequence of transformations iterated some number of times.
404#[derive(Debug)]
405pub struct Fixpoint {
406    name: &'static str,
407    transforms: Vec<Box<dyn Transform>>,
408    limit: usize,
409}
410
411impl Fixpoint {
412    /// Run a single iteration of the [`Fixpoint`] transform by iterating
413    /// through all transforms.
414    #[mz_ore::instrument(
415        target = "optimizer",
416        level = "debug",
417        fields(path.segment = iter_name)
418    )]
419    fn apply_transforms(
420        &self,
421        relation: &mut MirRelationExpr,
422        ctx: &mut TransformCtx,
423        iter_name: String,
424    ) -> Result<(), TransformError> {
425        for transform in self.transforms.iter() {
426            transform.transform(relation, ctx)?;
427        }
428        mz_repr::explain::trace_plan(relation);
429        Ok(())
430    }
431}
432
433impl Transform for Fixpoint {
434    fn name(&self) -> &'static str {
435        self.name
436    }
437
438    #[mz_ore::instrument(
439        target = "optimizer",
440        level = "debug",
441        fields(path.segment = self.name)
442    )]
443    fn actually_perform_transform(
444        &self,
445        relation: &mut MirRelationExpr,
446        ctx: &mut TransformCtx,
447    ) -> Result<(), TransformError> {
448        // The number of iterations for a relation to settle depends on the
449        // number of nodes in the relation. Instead of picking an arbitrary
450        // hard limit on the number of iterations, we use a soft limit and
451        // check whether the relation has become simpler after reaching it.
452        // If so, we perform another pass of transforms. Otherwise, there is
453        // a bug somewhere that prevents the relation from settling on a
454        // stable shape.
455        let mut iter_no = 0;
456        let mut seen = BTreeMap::new();
457        seen.insert(relation.hash_to_u64(), iter_no);
458        let original = relation.clone();
459        loop {
460            let prev_size = relation.size();
461            for i in iter_no..iter_no + self.limit {
462                let prev = relation.clone();
463                self.apply_transforms(relation, ctx, format!("{i:04}"))?;
464                if *relation == prev {
465                    if prev_size > 100000 {
466                        tracing::warn!(%prev_size, "Very big MIR plan");
467                    }
468                    mz_repr::explain::trace_plan(relation);
469                    return Ok(());
470                }
471                let seen_i = seen.insert(relation.hash_to_u64(), i);
472                if let Some(seen_i) = seen_i {
473                    // Let's see whether this is just a hash collision, or a real loop: Run the
474                    // whole thing from the beginning up until `seen_i`, and compare all the plans
475                    // to the current plan from the outer `for`.
476                    // (It would not be enough to compare only the plan at `seen_i`, because
477                    // then we could miss a real loop if there is also a hash collision somewhere
478                    // in the middle of the loop, because then we'd compare the last plan of the
479                    // loop not with its actual match, but with the colliding plan.)
480                    let mut again = original.clone();
481                    // The `+2` is because:
482                    // - one `+1` is to finally get to the plan at `seen_i`,
483                    // - another `+1` is because we are comparing to `relation` only _before_
484                    //   calling `apply_transforms`.
485                    for j in 0..(seen_i + 2) {
486                        if again == *relation {
487                            // We really got into an infinite loop (e.g., we are oscillating between
488                            // two plans). This is not catastrophic, because we can just say we are
489                            // done now, but it would be great to eventually find a way to prevent
490                            // these loops from happening in the first place. We have several
491                            // relevant issues, see
492                            // https://github.com/MaterializeInc/database-issues/issues/8197#issuecomment-2200172227
493                            mz_repr::explain::trace_plan(relation);
494                            error!(
495                                "Fixpoint `{}` detected a loop of length {} after {} iterations",
496                                self.name,
497                                i - seen_i,
498                                i
499                            );
500                            return Ok(());
501                        }
502                        ctx.update_last_hash(&again);
503                        self.apply_transforms(
504                            &mut again,
505                            ctx,
506                            format!("collision detection {j:04}"),
507                        )?;
508                    }
509                    // If we got here, then this was just a hash collision! Just continue as if
510                    // nothing happened.
511                }
512            }
513            let current_size = relation.size();
514
515            iter_no += self.limit;
516
517            if current_size < prev_size {
518                tracing::warn!(
519                    "Fixpoint {} ran for {} iterations \
520                     without reaching a fixpoint but reduced the relation size; \
521                     current_size ({}) < prev_size ({}); \
522                     continuing for {} more iterations",
523                    self.name,
524                    iter_no,
525                    current_size,
526                    prev_size,
527                    self.limit
528                );
529            } else {
530                // We failed to reach a fixed point, or find a sufficiently short cycle.
531                // This is not catastrophic, because we can just say we are done now,
532                // but it would be great to eventually find a way to prevent these loops from
533                // happening in the first place. We have several relevant issues, see
534                // https://github.com/MaterializeInc/database-issues/issues/8197#issuecomment-2200172227
535                mz_repr::explain::trace_plan(relation);
536                soft_panic_or_log!(
537                    "Fixpoint {} failed to reach a fixed point, or cycle of length at most {}",
538                    self.name,
539                    self.limit,
540                );
541                return Ok(());
542            }
543        }
544    }
545}
546
547/// Convenience macro for guarding transforms behind a feature flag.
548///
549/// If you have a code block like
550///
551/// ```ignore
552/// vec![
553///     Box::new(Foo::default()),
554///     Box::new(Bar::default()),
555///     Box::new(Baz::default()),
556/// ]
557/// ```
558///
559/// and you want to guard `Bar` behind a feature flag `enable_bar`, you can
560/// write
561///
562/// ```ignore
563/// transforms![
564///     Box::new(Foo::default()),
565///     Box::new(Bar::default()); if ctx.features.enable_bar,
566///     Box::new(Baz::default()),
567/// ]
568/// ```
569///
570/// as a shorthand and in order to minimize your code diff.
571#[allow(unused_macros)]
572macro_rules! transforms {
573    // Internal rule. Matches lines with a guard: `$transform; if $cond`.
574    (@op fill $buf:ident with $transform:expr; if $cond:expr, $($transforms:tt)*) => {
575        if $cond {
576            $buf.push($transform);
577        }
578        transforms!(@op fill $buf with $($transforms)*);
579    };
580    // Internal rule. Matches lines without a guard: `$transform`.
581    (@op fill $buf:ident with $transform:expr, $($transforms:tt)*) => {
582        $buf.push($transform);
583        transforms!(@op fill $buf with $($transforms)*);
584    };
585    // Internal rule: matches the empty $transforms TokenTree (terminal case).
586    (@op fill $buf:ident with) => {
587        // do nothing
588    };
589    ($($transforms:tt)*) => {{
590        #[allow(clippy::vec_init_then_push)]
591        {
592            let mut __buf = Vec::<Box<dyn Transform>>::new();
593            transforms!(@op fill __buf with $($transforms)*);
594            __buf
595        }
596    }};
597}
598
599/// A sequence of transformations that simplify the `MirRelationExpr`
600#[derive(Debug)]
601pub struct FuseAndCollapse {
602    transforms: Vec<Box<dyn Transform>>,
603}
604
605impl Default for FuseAndCollapse {
606    fn default() -> Self {
607        Self {
608            // TODO: The relative orders of the transforms have not been
609            // determined except where there are comments.
610            // TODO (database-issues#2036): All the transforms here except for `ProjectionLifting`
611            //  and `RedundantJoin` can be implemented as free functions.
612            transforms: vec![
613                Box::new(canonicalization::ProjectionExtraction),
614                Box::new(movement::ProjectionLifting::default()),
615                Box::new(fusion::Fusion),
616                Box::new(canonicalization::FlatMapElimination),
617                Box::new(fusion::join::Join),
618                Box::new(NormalizeLets::new(false)),
619                Box::new(fusion::reduce::Reduce),
620                Box::new(WillDistinct),
621                Box::new(compound::UnionNegateFusion),
622                // This goes after union fusion so we can cancel out
623                // more branches at a time.
624                Box::new(UnionBranchCancellation),
625                // This should run before redundant join to ensure that key info
626                // is correct.
627                Box::new(NormalizeLets::new(false)),
628                // Removes redundant inputs from joins.
629                // Note that this eliminates one redundant input per join,
630                // so it is necessary to run this section in a loop.
631                Box::new(RedundantJoin::default()),
632                // As a final logical action, convert any constant expression to a constant.
633                // Some optimizations fight against this, and we want to be sure to end as a
634                // `MirRelationExpr::Constant` if that is the case, so that subsequent use can
635                // clearly see this.
636                Box::new(fold_constants_fixpoint(true)),
637            ],
638        }
639    }
640}
641
642impl Transform for FuseAndCollapse {
643    fn name(&self) -> &'static str {
644        "FuseAndCollapse"
645    }
646
647    #[mz_ore::instrument(
648        target = "optimizer",
649        level = "debug",
650        fields(path.segment = "fuse_and_collapse")
651    )]
652    fn actually_perform_transform(
653        &self,
654        relation: &mut MirRelationExpr,
655        ctx: &mut TransformCtx,
656    ) -> Result<(), TransformError> {
657        for transform in self.transforms.iter() {
658            transform.transform(relation, ctx)?;
659        }
660        mz_repr::explain::trace_plan(&*relation);
661        Ok(())
662    }
663}
664
665/// Run the [`FuseAndCollapse`] transforms in a fixpoint.
666pub fn fuse_and_collapse_fixpoint() -> Fixpoint {
667    Fixpoint {
668        name: "fuse_and_collapse_fixpoint",
669        limit: 100,
670        transforms: FuseAndCollapse::default().transforms,
671    }
672}
673
674/// Does constant folding to a fixpoint: An expression all of whose leaves are constants, of size
675/// small enough to be inlined and folded should reach a single `MirRelationExpr::Constant`.
676///
677/// If `limit` is false, it does constant folding even on large constants.
678///
679/// This needs to call `FoldConstants` together with `NormalizeLets` in a fixpoint loop, because
680/// currently `FoldConstants` doesn't inline CTEs, so these two need to alternate until fixpoint.
681///
682/// Also note that `FoldConstants` can break the normalized form by removing all references to a
683/// Let.
684///
685/// We also call `ReduceScalars`, because that does constant folding inside scalar expressions.
686pub fn fold_constants_fixpoint(limit: bool) -> Fixpoint {
687    Fixpoint {
688        name: "fold_constants_fixpoint",
689        limit: 100,
690        transforms: vec![
691            Box::new(FoldConstants {
692                limit: if limit {
693                    Some(FOLD_CONSTANTS_LIMIT)
694                } else {
695                    None
696                },
697            }),
698            Box::new(canonicalization::ReduceScalars),
699            Box::new(NormalizeLets::new(false)),
700        ],
701    }
702}
703
704/// Construct a normalizing transform that runs transforms that normalize the
705/// structure of the tree until a fixpoint.
706///
707/// Care needs to be taken to ensure that the fixpoint converges for every
708/// possible input tree. If this is not the case, there are two possibilities:
709/// 1. The rewrite loop runs enters an oscillating cycle.
710/// 2. The expression grows without bound.
711pub fn normalize() -> Fixpoint {
712    Fixpoint {
713        name: "normalize",
714        limit: 100,
715        transforms: vec![Box::new(NormalizeLets::new(false)), Box::new(NormalizeOps)],
716    }
717}
718
719/// A naive optimizer for relation expressions.
720///
721/// The optimizer currently applies only peep-hole optimizations, from a limited
722/// set that were sufficient to get some of TPC-H up and working. It is worth a
723/// review at some point to improve the quality, coverage, and architecture of
724/// the optimizations.
725#[derive(Debug)]
726pub struct Optimizer {
727    /// A logical name identifying this optimizer instance.
728    pub name: &'static str,
729    /// The list of transforms to apply to an input relation.
730    pub transforms: Vec<Box<dyn Transform>>,
731}
732
733impl Optimizer {
734    /// Builds a logical optimizer that only performs logical transformations.
735    #[deprecated = "Create an Optimize instance and call `optimize` instead."]
736    pub fn logical_optimizer(ctx: &mut TransformCtx) -> Self {
737        let transforms: Vec<Box<dyn Transform>> = transforms![
738            // 0. `Transform`s that don't actually change the plan.
739            Box::new(ReprTypecheck::new(ctx.repr_typecheck()).strict_join_equivalences()),
740            Box::new(CollectNotices),
741            // 1. Structure-agnostic cleanup
742            Box::new(normalize()),
743            Box::new(NonNullRequirements::default()),
744            // 2. Collapse constants, joins, unions, and lets as much as possible.
745            // TODO: lift filters/maps to maximize ability to collapse
746            // things down?
747            Box::new(fuse_and_collapse_fixpoint()),
748            // 3. Needs to happen before LiteralLifting, EquivalencePropagation
749            // make (literal) filters look more complicated than what the NonNegative Analysis can
750            // recognize.
751            Box::new(ThresholdElision),
752            // 4. Move predicate information up and down the tree.
753            //    This also fixes the shape of joins in the plan.
754            Box::new(Fixpoint {
755                name: "fixpoint_logical_01",
756                limit: 100,
757                transforms: vec![
758                    // Predicate pushdown sets the equivalence classes of joins.
759                    Box::new(PredicatePushdown::default()),
760                    Box::new(EquivalencePropagation::default()),
761                    // Lifts the information `col1 = col2`
762                    Box::new(Demand::default()),
763                    Box::new(FuseAndCollapse::default()),
764                ],
765            }),
766            // 5. Reduce/Join simplifications.
767            Box::new(Fixpoint {
768                name: "fixpoint_logical_02",
769                limit: 100,
770                transforms: vec![
771                    Box::new(SemijoinIdempotence::default()),
772                    // Pushes aggregations down
773                    Box::new(ReductionPushdown),
774                    // Replaces reduces with maps when the group keys are
775                    // unique with maps
776                    Box::new(ReduceElision),
777                    // Rips complex reduces apart.
778                    Box::new(ReduceReduction),
779                    // Converts `Cross Join {Constant(Literal) + Input}` to
780                    // `Map {Cross Join (Input, Constant()), Literal}`.
781                    // Join fusion will clean this up to `Map{Input, Literal}`
782                    Box::new(LiteralLifting::default()),
783                    // Identifies common relation subexpressions.
784                    Box::new(cse::relation_cse::RelationCSE::new(false)),
785                    Box::new(FuseAndCollapse::default()),
786                ],
787            }),
788            Box::new(
789                ReprTypecheck::new(ctx.repr_typecheck())
790                    .disallow_new_globals()
791                    .strict_join_equivalences()
792            ),
793        ];
794        Self {
795            name: "logical",
796            transforms,
797        }
798    }
799
800    /// Builds a physical optimizer.
801    ///
802    /// Performs logical transformations followed by all physical ones.
803    /// This is meant to be used for optimizing each view within a dataflow
804    /// once view inlining has already happened, right before dataflow
805    /// rendering.
806    pub fn physical_optimizer(ctx: &mut TransformCtx) -> Self {
807        // Implementation transformations
808        let transforms: Vec<Box<dyn Transform>> = transforms![
809            Box::new(ReprTypecheck::new(ctx.repr_typecheck()).disallow_new_globals().strict_join_equivalences()),
810            // Considerations for the relationship between JoinImplementation and other transforms:
811            // - there should be a run of LiteralConstraints before JoinImplementation lifts away
812            //   the Filters from the Gets;
813            // - there should be no RelationCSE between this LiteralConstraints and
814            //   JoinImplementation, because that could move an IndexedFilter behind a Get.
815            // - The last RelationCSE before JoinImplementation should be with inline_mfp = true.
816            // - Currently, JoinImplementation can't be before LiteralLifting because the latter
817            //   sometimes creates `Unimplemented` joins (despite LiteralLifting already having been
818            //   run in the logical optimizer).
819            // - Not running EquivalencePropagation in the same fixpoint loop with JoinImplementation
820            //   is slightly hurting our plans. However, I'd say we should fix these problems by
821            //   making EquivalencePropagation (and/or JoinImplementation) smarter (database-issues#5289), rather than
822            //   having them in the same fixpoint loop. If they would be in the same fixpoint loop,
823            //   then we either run the risk of EquivalencePropagation invalidating a join plan (database-issues#5260),
824            //   or we would have to run JoinImplementation an unbounded number of times, which is
825            //   also not good database-issues#4639.
826            //   (The same is true for FoldConstants, Demand, and LiteralLifting to a lesser
827            //   extent.)
828            //
829            // Also note that FoldConstants and LiteralLifting are not confluent. They can
830            // oscillate between e.g.:
831            //         Constant
832            //           - (4)
833            // and
834            //         Map (4)
835            //           Constant
836            //             - ()
837            Box::new(Fixpoint {
838                name: "fixpoint_physical_01",
839                limit: 100,
840                transforms: vec![
841                    Box::new(EquivalencePropagation::default()),
842                    Box::new(fold_constants_fixpoint(true)),
843                    Box::new(Demand::default()),
844                    // Demand might have introduced dummies, so let's also do a ProjectionPushdown.
845                    Box::new(ProjectionPushdown::default()),
846                    Box::new(LiteralLifting::default()),
847                ],
848            }),
849            Box::new(LiteralConstraints),
850            Box::new(Fixpoint {
851                name: "fixpoint_join_impl",
852                limit: 100,
853                transforms: vec![Box::new(JoinImplementation::default())],
854            }),
855            Box::new(CanonicalizeMfp),
856            // Identifies common relation subexpressions.
857            Box::new(cse::relation_cse::RelationCSE::new(false)),
858            // `RelationCSE` can create new points of interest for `ProjectionPushdown`: If an MFP
859            // is cut in half by `RelationCSE`, then we'd like to push projections behind the new
860            // Get as much as possible. This is because a fork in the plan involves copying the
861            // data. (But we need `ProjectionPushdown` to skip joins, because it can't deal with
862            // filled in JoinImplementations.)
863            Box::new(ProjectionPushdown::skip_joins()); if ctx.features.enable_projection_pushdown_after_relation_cse,
864            // Plans look nicer if we tidy MFPs again after ProjectionPushdown.
865            Box::new(CanonicalizeMfp); if ctx.features.enable_projection_pushdown_after_relation_cse,
866            // Do a last run of constant folding. Importantly, this also runs `NormalizeLets`!
867            // We need `NormalizeLets` at the end of the MIR pipeline for various reasons:
868            // - The rendering expects some invariants about Let/LetRecs.
869            // - `CollectIndexRequests` needs a normalized plan.
870            //   https://github.com/MaterializeInc/database-issues/issues/6371
871            Box::new(fold_constants_fixpoint(true)),
872            Box::new(ReprTypecheck::new(ctx.repr_typecheck()).disallow_new_globals().disallow_dummy().strict_join_equivalences()),
873        ];
874        Self {
875            name: "physical",
876            transforms,
877        }
878    }
879
880    /// Contains the logical optimizations that should run after cross-view
881    /// transformations run.
882    ///
883    /// Set `allow_new_globals` when you will use these as the first passes.
884    /// The first instance of the typechecker in an optimizer pipeline should
885    /// allow new globals (or it will crash when it encounters them).
886    pub fn logical_cleanup_pass(ctx: &mut TransformCtx, allow_new_globals: bool) -> Self {
887        let mut repr_typechecker =
888            ReprTypecheck::new(ctx.repr_typecheck()).strict_join_equivalences();
889        if !allow_new_globals {
890            repr_typechecker = repr_typechecker.disallow_new_globals();
891        }
892
893        let transforms: Vec<Box<dyn Transform>> = transforms![
894            Box::new(repr_typechecker),
895            // Delete unnecessary maps.
896            Box::new(fusion::Fusion),
897            Box::new(Fixpoint {
898                name: "fixpoint_logical_cleanup_pass_01",
899                limit: 100,
900                transforms: vec![
901                    Box::new(CanonicalizeMfp),
902                    // Remove threshold operators which have no effect.
903                    Box::new(ThresholdElision),
904                    // Projection pushdown may unblock fusing joins and unions.
905                    Box::new(fusion::join::Join),
906                    // Predicate pushdown required to tidy after join fusion.
907                    Box::new(PredicatePushdown::default()),
908                    Box::new(RedundantJoin::default()),
909                    // Redundant join produces projects that need to be fused.
910                    Box::new(fusion::Fusion),
911                    Box::new(compound::UnionNegateFusion),
912                    // This goes after union fusion so we can cancel out
913                    // more branches at a time.
914                    Box::new(UnionBranchCancellation),
915                    // The last RelationCSE before JoinImplementation should be with
916                    // inline_mfp = true.
917                    Box::new(cse::relation_cse::RelationCSE::new(true)),
918                    Box::new(fold_constants_fixpoint(true)),
919                ],
920            }),
921            Box::new(
922                ReprTypecheck::new(ctx.repr_typecheck())
923                    .disallow_new_globals()
924                    .strict_join_equivalences()
925            ),
926        ];
927        Self {
928            name: "logical_cleanup",
929            transforms,
930        }
931    }
932
933    /// Builds a tiny optimizer, which is only suitable for optimizing fast-path queries.
934    pub fn fast_path_optimizer(_ctx: &mut TransformCtx) -> Self {
935        let transforms: Vec<Box<dyn Transform>> = vec![
936            Box::new(canonicalization::ReduceScalars),
937            Box::new(LiteralConstraints),
938            Box::new(CanonicalizeMfp),
939            // We might have arrived at a constant, e.g., due to contradicting literal constraints.
940            Box::new(Fixpoint {
941                name: "fast_path_fold_constants_fixpoint",
942                limit: 100,
943                transforms: vec![
944                    Box::new(FoldConstants {
945                        limit: Some(FOLD_CONSTANTS_LIMIT),
946                    }),
947                    Box::new(canonicalization::ReduceScalars),
948                ],
949            }),
950        ];
951        Self {
952            name: "fast_path_optimizer",
953            transforms,
954        }
955    }
956
957    /// Builds a tiny optimizer, which just folds constants. For more details, see
958    /// [fold_constants_fixpoint].
959    pub fn constant_optimizer(_ctx: &mut TransformCtx, limit: bool) -> Self {
960        Self {
961            name: "fast_path_optimizer",
962            transforms: vec![Box::new(fold_constants_fixpoint(limit))],
963        }
964    }
965
966    /// Optimizes the supplied relation expression.
967    ///
968    /// These optimizations are performed with no information about available arrangements,
969    /// which makes them suitable for pre-optimization before dataflow deployment.
970    #[mz_ore::instrument(
971        target = "optimizer",
972        level = "debug",
973        fields(path.segment = self.name)
974    )]
975    pub fn optimize(
976        &self,
977        mut relation: MirRelationExpr,
978        ctx: &mut TransformCtx,
979    ) -> Result<mz_expr::OptimizedMirRelationExpr, TransformError> {
980        let transform_result = self.transform(&mut relation, ctx);
981
982        match transform_result {
983            Ok(_) => {
984                mz_repr::explain::trace_plan(&relation);
985                Ok(mz_expr::OptimizedMirRelationExpr(relation))
986            }
987            Err(e) => {
988                // Without this, the dropping of `relation` (which happens automatically when
989                // returning from this function) might run into a stack overflow, see
990                // https://github.com/MaterializeInc/database-issues/issues/4043
991                relation.destroy_carefully();
992                error!("Optimizer::optimize(): {}", e);
993                Err(e)
994            }
995        }
996    }
997
998    /// Optimizes the supplied relation expression in place, using available arrangements.
999    ///
1000    /// This method should only be called with non-empty `indexes` when optimizing a dataflow,
1001    /// as the optimizations may lock in the use of arrangements that may cease to exist.
1002    fn transform(
1003        &self,
1004        relation: &mut MirRelationExpr,
1005        args: &mut TransformCtx,
1006    ) -> Result<(), TransformError> {
1007        args.update_last_hash(relation);
1008
1009        for transform in self.transforms.iter() {
1010            transform.transform(relation, args)?;
1011        }
1012
1013        Ok(())
1014    }
1015}