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