Skip to main content

mz_adapter/
optimize.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//! Optimizer interface to the adapter and coordinator code.
11//!
12//! The goal of this crate is to abstract optimizer specifics behind a
13//! high-level interface that is ready to be consumed by the coordinator code in
14//! a future-proof way (that is, the API is taking the upcoming evolution of
15//! these components into account).
16//!
17//! The contents of this crate should have minimal dependencies to the rest of
18//! the coordinator code so we can pull them out as a separate crate in the
19//! future without too much effort.
20//!
21//! The main type in this module is a very simple [`Optimize`] trait which
22//! allows us to adhere to the following principles:
23//!
24//! - Implementors of this trait are structs that encapsulate all context
25//!   required to optimize a statement of type `T` end-to-end (for example
26//!   [`materialized_view::Optimizer`] for `T` = `MaterializedView`).
27//! - Each struct implements [`Optimize`] once for each optimization stage. The
28//!   `From` type represents the input of the stage and `Self::To` the
29//!   associated stage output. This allows to have more than one entrypoints to
30//!   a pipeline.
31//! - The concrete types used for stage results are opaque structs that are
32//!   specific to the pipeline of that statement type.
33//!   - We use different structs even if two statement types might have
34//!     structurally identical intermediate results. This ensures that client
35//!     code cannot first execute some optimization stages for one type and then
36//!     some stages for a different type.
37//!   - The only way to construct such a struct is by running the [`Optimize`]
38//!     stage that produces it. This ensures that client code cannot interfere
39//!     with the pipeline.
40//!   - In general, the internals of these structs can be accessed only behind a
41//!     shared reference. This ensures that client code can look up information
42//!     from intermediate stages but cannot modify it.
43//!   - Timestamp selection is modeled as a conversion between structs that are
44//!     adjacent in the pipeline using a method called `resolve`.
45//!   - The struct representing the result of the final stage of the
46//!     optimization pipeline can be destructed to access its internals with a
47//!     method called `unapply`.
48//! - The `Send` trait bounds on the `Self` and `From` types ensure that
49//!   [`Optimize`] instances can be passed to different threads (this is
50//!   required of off-thread optimization).
51//!
52//! For details, see the `20230714_optimizer_interface.md` design doc in this
53//! repository.
54
55pub mod copy_to;
56pub mod dataflows;
57pub mod index;
58pub mod materialized_view;
59pub mod metric_sink;
60pub mod peek;
61pub mod pinned;
62pub mod subscribe;
63pub mod view;
64
65use std::fmt::Debug;
66
67use mz_adapter_types::connection::ConnectionId;
68use mz_adapter_types::dyncfgs::PERSIST_FAST_PATH_ORDER;
69use mz_catalog::memory::objects::{CatalogCollectionEntry, CatalogEntry, Index};
70use mz_compute_types::dataflows::MirDataflowDescription;
71use mz_compute_types::dyncfgs::SUBSCRIBE_SNAPSHOT_OPTIMIZATION;
72use mz_controller_types::ClusterId;
73use mz_expr::{EvalError, MirRelationExpr, OptimizedMirRelationExpr, UnmaterializableFunc};
74use mz_ore::stack::RecursionLimitError;
75use mz_repr::adt::timestamp::TimestampError;
76use mz_repr::optimize::{OptimizerFeatureOverrides, OptimizerFeatures, OverrideFrom};
77use mz_repr::{CatalogItemId, GlobalId};
78use mz_sql::names::{FullItemName, QualifiedItemName};
79use mz_sql::plan::{HirRelationExpr, PlanError};
80use mz_sql::session::vars::SystemVars;
81use mz_transform::{MaybeShouldPanic, TransformCtx, TransformError};
82
83// Alias types
84// -----------
85
86pub use mz_compute_types::dataflows::LirDataflowDescription;
87
88// Core API
89// --------
90
91/// A trait that represents an optimization stage.
92///
93/// The trait is implemented by structs that encapsulate the context needed to
94/// run an end-to-end optimization pipeline for a specific statement type
95/// (`Index`, `View`, `MaterializedView`, `Subscribe`, `Select`).
96///
97/// Each implementation represents a concrete optimization stage for a fixed
98/// statement type that consumes an input of type `From` and produces output of
99/// type `Self::To`.
100pub trait Optimize<From> {
101    type To;
102
103    /// Execute the optimization stage, transforming the input plan of type
104    /// `From` to an output plan of type `To`.
105    fn optimize(&mut self, plan: From) -> Result<Self::To, OptimizerError>;
106
107    /// Like [`Self::optimize`], but additionally ensures that panics occurring
108    /// in the [`Self::optimize`] call are caught and demoted to an
109    /// [`OptimizerError::Internal`] error.
110    ///
111    /// Additionally, if the result of the optimization is an error (not a panic) that indicates we
112    /// should panic, then panic.
113    #[mz_ore::instrument(target = "optimizer", level = "debug", name = "optimize")]
114    fn catch_unwind_optimize(&mut self, plan: From) -> Result<Self::To, OptimizerError> {
115        mz_transform::catch_unwind_optimize(|| self.optimize(plan))
116    }
117}
118
119/// Runs the shared one-shot optimization pipeline for a single optimizer.
120///
121/// This factors out the (otherwise duplicated) HIR ⇒ local MIR ⇒ resolve ⇒
122/// global LIR sequence that is common to the `SELECT`/`EXPLAIN` and `COPY TO`
123/// paths. The `resolve` closure attaches the timestamp/session/stats context to
124/// the local plan; it is path-specific only in the concrete plan type it
125/// operates on.
126pub(crate) fn optimize_oneshot<O, LocalPlan, ResolvedPlan, GlobalPlan>(
127    optimizer: &mut O,
128    raw_expr: HirRelationExpr,
129    resolve: impl FnOnce(LocalPlan) -> ResolvedPlan,
130) -> Result<GlobalPlan, OptimizerError>
131where
132    O: Optimize<HirRelationExpr, To = LocalPlan> + Optimize<ResolvedPlan, To = GlobalPlan>,
133{
134    // HIR ⇒ MIR lowering and MIR optimization (local).
135    let local_mir_plan = optimizer.catch_unwind_optimize(raw_expr)?;
136    // Attach resolved context required to continue the pipeline.
137    let resolved_mir_plan = resolve(local_mir_plan);
138    // MIR optimization (global), MIR ⇒ LIR lowering, and LIR optimization (global).
139    optimizer.catch_unwind_optimize(resolved_mir_plan)
140}
141
142// Optimizer configuration
143// -----------------------
144
145/// Feature flags for the optimizer.
146///
147/// To add a new feature flag, do the following steps:
148///
149/// 1. To make the flag available to all stages in our [`Optimize`] pipelines
150///    and allow engineers to set a system-wide override:
151///    1. Add the flag to the `optimizer_feature_flags!(...)` macro call.
152///    2. Add the flag to the `feature_flags!(...)` macro call and extend the
153///       `From<&SystemVars>` implementation for [`OptimizerFeatures`].
154///
155/// 2. To enable `EXPLAIN ... WITH(...)` overrides which will allow engineers to
156///    inspect plan differences before deploying the optimizer changes:
157///    1. Add the flag to the `ExplainPlanOptionName` definition.
158///    2. Add the flag to the `generate_extracted_config!(ExplainPlanOption,
159///       ...)` macro call.
160///    3. Extend the `TryFrom<ExplainPlanOptionExtracted>` implementation for
161///       [`mz_repr::explain::ExplainConfig`].
162///
163/// 3. To enable `CLUSTER ... FEATURES(...)` overrides which will allow
164///    engineers to experiment with runtime differences before deploying the
165///    optimizer changes:
166///    1. Add the flag to the `ClusterFeatureName` definition.
167///    2. Add the flag to the `generate_extracted_config!(ClusterFeature, ...)`
168///       macro call.
169///    3. Extend the `let optimizer_feature_overrides = ...` call in
170///       `plan_create_cluster`.
171#[derive(Clone, Debug)]
172pub struct OptimizerConfig {
173    /// The mode in which the optimizer runs.
174    pub mode: OptimizeMode,
175    /// If the [`GlobalId`] is set the optimizer works in "replan" mode.
176    ///
177    /// This means that it will not consider catalog items (more specifically
178    /// indexes) with [`GlobalId`] greater or equal than the one provided here.
179    pub replan: Option<GlobalId>,
180    /// Show the slow path plan even if a fast path plan was created. Useful for debugging.
181    /// Enforced if `timing` is set.
182    pub no_fast_path: bool,
183    // If set, allow some additional queries down the Persist fast path when we believe
184    // the orderings are compatible.
185    persist_fast_path_order: bool,
186    // Enable calculating with_snapshot metadata for subscribes.
187    subscribe_snapshot_optimization: bool,
188    /// Optimizer feature flags.
189    pub features: OptimizerFeatures,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
193pub enum OptimizeMode {
194    /// A mode where the optimized statement is executed.
195    Execute,
196    /// A mode where the optimized statement is explained.
197    Explain,
198}
199
200impl From<&SystemVars> for OptimizerConfig {
201    fn from(vars: &SystemVars) -> Self {
202        Self {
203            mode: OptimizeMode::Execute,
204            replan: None,
205            no_fast_path: false,
206            persist_fast_path_order: PERSIST_FAST_PATH_ORDER.get(vars.dyncfgs()),
207            subscribe_snapshot_optimization: SUBSCRIBE_SNAPSHOT_OPTIMIZATION.get(vars.dyncfgs()),
208            features: OptimizerFeatures::from(vars),
209        }
210    }
211}
212
213/// Override [`OptimizerConfig::features`] from [`OptimizerFeatureOverrides`].
214impl OverrideFrom<OptimizerFeatureOverrides> for OptimizerConfig {
215    fn override_from(mut self, overrides: &OptimizerFeatureOverrides) -> Self {
216        self.features = self.features.override_from(overrides);
217        self
218    }
219}
220
221/// [`OptimizerConfig`] overrides coming from an [`ExplainContext`].
222impl OverrideFrom<ExplainContext> for OptimizerConfig {
223    fn override_from(mut self, ctx: &ExplainContext) -> Self {
224        let ExplainContext::Plan(ctx) = ctx else {
225            return self; // Return immediately for all other contexts.
226        };
227
228        // Override general parameters.
229        self.mode = OptimizeMode::Explain;
230        self.replan = ctx.replan;
231        self.no_fast_path = ctx.config.no_fast_path;
232
233        // Override feature flags that can be enabled in the EXPLAIN config.
234        self.features = self.features.override_from(&ctx.config.features);
235
236        // Return the final result.
237        self
238    }
239}
240
241impl From<&OptimizerConfig> for mz_sql::plan::HirToMirConfig {
242    fn from(config: &OptimizerConfig) -> Self {
243        Self {
244            enable_new_outer_join_lowering: config.features.enable_new_outer_join_lowering,
245            enable_variadic_left_join_lowering: config.features.enable_variadic_left_join_lowering,
246            enable_cast_elimination: config.features.enable_cast_elimination,
247            enable_simplify_quantified_comparisons: config
248                .features
249                .enable_simplify_quantified_comparisons,
250            enable_fixed_correlated_cte_lowering: config
251                .features
252                .enable_fixed_correlated_cte_lowering,
253            enable_simplify_from_less_existence: config
254                .features
255                .enable_simplify_from_less_existence,
256        }
257    }
258}
259
260// OptimizerCatalog
261// ===============
262
263pub trait OptimizerCatalog: Debug + Send + Sync {
264    fn get_entry(&self, id: &GlobalId) -> CatalogCollectionEntry;
265    fn get_entry_by_item_id(&self, id: &CatalogItemId) -> &CatalogEntry;
266    fn resolve_full_name(
267        &self,
268        name: &QualifiedItemName,
269        conn_id: Option<&ConnectionId>,
270    ) -> FullItemName;
271
272    /// Returns all indexes on the given object and cluster known in the
273    /// catalog.
274    fn get_indexes_on(
275        &self,
276        id: GlobalId,
277        cluster: ClusterId,
278    ) -> Box<dyn Iterator<Item = (GlobalId, &Index)> + '_>;
279}
280
281// OptimizerError
282// ===============
283
284/// Error types that can be generated during optimization.
285#[derive(Debug, thiserror::Error)]
286pub enum OptimizerError {
287    #[error("{0}")]
288    PlanError(#[from] PlanError),
289    #[error("{0}")]
290    RecursionLimitError(#[from] RecursionLimitError),
291    #[error("{0}")]
292    TransformError(#[from] TransformError),
293    #[error("{0}")]
294    EvalError(#[from] EvalError),
295    #[error("cannot materialize call to {0}")]
296    UnmaterializableFunction(UnmaterializableFunc),
297    #[error("cannot call {func} in {context} ")]
298    UncallableFunction {
299        func: UnmaterializableFunc,
300        context: &'static str,
301    },
302    #[error("access to function {0} is restricted")]
303    RestrictedFunction(UnmaterializableFunc),
304    #[error("{0}")]
305    UnsupportedTemporalExpression(String),
306    /// This is a specific kind of internal error. It's distinct from `Internal`, because we want to
307    /// catch it and swallow it in some cases.
308    #[error("internal optimizer error: MfpPlan couldn't be converted into SafeMfpPlan")]
309    InternalUnsafeMfpPlan(String),
310    #[error("internal optimizer error: {0}")]
311    Internal(String),
312}
313
314impl From<String> for OptimizerError {
315    fn from(msg: String) -> Self {
316        Self::Internal(msg)
317    }
318}
319
320impl OptimizerError {
321    pub fn detail(&self) -> Option<String> {
322        match self {
323            Self::UnmaterializableFunction(UnmaterializableFunc::CurrentTimestamp) => {
324                Some("See: https://materialize.com/docs/sql/functions/now_and_mz_now/".into())
325            }
326            Self::RestrictedFunction(_) => Some(
327                "Access to system catalog objects is restricted for this role. \
328                Contact your administrator if you need access."
329                    .into(),
330            ),
331            _ => None,
332        }
333    }
334
335    pub fn hint(&self) -> Option<String> {
336        match self {
337            Self::UnmaterializableFunction(UnmaterializableFunc::CurrentTimestamp) => {
338                Some("In temporal filters `mz_now()` may work instead.".into())
339            }
340            _ => None,
341        }
342    }
343}
344
345impl From<TimestampError> for OptimizerError {
346    fn from(value: TimestampError) -> Self {
347        OptimizerError::EvalError(EvalError::from(value))
348    }
349}
350
351impl From<anyhow::Error> for OptimizerError {
352    fn from(value: anyhow::Error) -> Self {
353        OptimizerError::Internal(value.to_string())
354    }
355}
356
357impl MaybeShouldPanic for OptimizerError {
358    fn should_panic(&self) -> Option<String> {
359        match self {
360            OptimizerError::TransformError(TransformError::CallerShouldPanic(msg)) => {
361                Some(msg.to_string())
362            }
363            _ => None,
364        }
365    }
366}
367
368// Tracing helpers
369// ---------------
370
371#[mz_ore::instrument(target = "optimizer", level = "debug", name = "local")]
372fn optimize_mir_local(
373    expr: MirRelationExpr,
374    ctx: &mut TransformCtx,
375) -> Result<OptimizedMirRelationExpr, OptimizerError> {
376    fail::fail_point!("optimize_mir_local");
377
378    #[allow(deprecated)]
379    let optimizer = mz_transform::Optimizer::logical_optimizer(ctx);
380    let expr = optimizer.optimize(expr, ctx)?;
381
382    // Trace the result of this phase.
383    mz_repr::explain::trace_plan(expr.as_inner());
384
385    Ok::<_, OptimizerError>(expr)
386}
387
388/// This is just a wrapper around [mz_transform::Optimizer::constant_optimizer],
389/// running it, and tracing the result plan.
390#[mz_ore::instrument(target = "optimizer", level = "debug", name = "constant")]
391fn optimize_mir_constant(
392    expr: MirRelationExpr,
393    ctx: &mut TransformCtx,
394    limit: bool,
395) -> Result<MirRelationExpr, OptimizerError> {
396    let optimizer = mz_transform::Optimizer::constant_optimizer(ctx, limit);
397    let expr = optimizer.optimize(expr, ctx)?;
398
399    // Trace the result of this phase.
400    mz_repr::explain::trace_plan(expr.as_inner());
401
402    Ok::<_, OptimizerError>(expr.0)
403}
404
405macro_rules! trace_plan {
406    (at: $span:literal, $plan:expr) => {
407        tracing::debug_span!(target: "optimizer", $span).in_scope(|| {
408            mz_repr::explain::trace_plan($plan);
409        });
410    }
411}
412
413use trace_plan;
414
415use crate::coord::ExplainContext;