Skip to main content

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