mz_repr/
explain.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//! A set of traits for modeling things that can be explained by a
11//! SQL `EXPLAIN` statement.
12//!
13//! The main trait in this module is [`Explain`].
14//!
15//! An explainable subject `S` implements [`Explain`], and as part of that:
16//!
17//! 1. Fixes the *context type* required for the explanation.
18//!    in [`Explain::Context`].
19//! 2. Fixes the *explanation type* for each [`ExplainFormat`]
20//!    in [`Explain::Text`], [`Explain::Json`], ....
21//! 3. Provides *an explanation type constructor* for each supported
22//!    [`ExplainFormat`] from references to `S`, [`ExplainConfig` ],
23//!    and the current [`Explain::Context`] in
24//!    [`Explain::explain_text`], [`Explain::explain_json`], ....
25//!
26//! The same *explanation type* can be shared by more than one
27//! [`ExplainFormat`].
28//!
29//! Use [`UnsupportedFormat`] and the default `explain_$format`
30//! constructor for [`Explain`] to indicate that the implementation does
31//! not support this `$format`.
32
33use itertools::Itertools;
34use proptest_derive::Arbitrary;
35use serde::{Deserialize, Serialize};
36use std::borrow::Cow;
37use std::collections::{BTreeMap, BTreeSet};
38use std::fmt;
39use std::fmt::{Display, Formatter};
40
41use mz_ore::stack::RecursionLimitError;
42use mz_ore::str::{Indent, bracketed, separated};
43
44use crate::explain::dot::{DisplayDot, dot_string};
45use crate::explain::json::{DisplayJson, json_string};
46use crate::explain::text::{DisplayText, text_string};
47use crate::optimize::OptimizerFeatureOverrides;
48use crate::{ColumnType, GlobalId, ScalarType};
49
50pub mod dot;
51pub mod json;
52pub mod text;
53#[cfg(feature = "tracing")]
54pub mod tracing;
55
56#[cfg(feature = "tracing")]
57pub use crate::explain::tracing::trace_plan;
58
59/// Possible output formats for an explanation.
60#[derive(Debug, Clone, Copy, Eq, PartialEq)]
61pub enum ExplainFormat {
62    Text,
63    Json,
64    Dot,
65}
66
67impl fmt::Display for ExplainFormat {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            ExplainFormat::Text => f.write_str("TEXT"),
71            ExplainFormat::Json => f.write_str("JSON"),
72            ExplainFormat::Dot => f.write_str("DOT"),
73        }
74    }
75}
76
77/// A zero-variant enum to be used as the explanation type in the
78/// [`Explain`] implementation for all formats that are not supported
79/// for `Self`.
80#[allow(missing_debug_implementations)]
81pub enum UnsupportedFormat {}
82
83/// The type of errors that may occur when an [`Explain::explain`]
84/// call goes wrong.
85#[derive(Debug)]
86pub enum ExplainError {
87    UnsupportedFormat(ExplainFormat),
88    FormatError(fmt::Error),
89    AnyhowError(anyhow::Error),
90    RecursionLimitError(RecursionLimitError),
91    SerdeJsonError(serde_json::Error),
92    LinearChainsPlusRecursive,
93    UnknownError(String),
94}
95
96impl fmt::Display for ExplainError {
97    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98        write!(f, "error while rendering explain output: ")?;
99        match self {
100            ExplainError::UnsupportedFormat(format) => {
101                write!(f, "{} format is not supported", format)
102            }
103            ExplainError::FormatError(error) => {
104                write!(f, "{}", error)
105            }
106            ExplainError::AnyhowError(error) => {
107                write!(f, "{}", error)
108            }
109            ExplainError::RecursionLimitError(error) => {
110                write!(f, "{}", error)
111            }
112            ExplainError::SerdeJsonError(error) => {
113                write!(f, "{}", error)
114            }
115            ExplainError::LinearChainsPlusRecursive => {
116                write!(
117                    f,
118                    "The linear_chains option is not supported with WITH MUTUALLY RECURSIVE."
119                )
120            }
121            ExplainError::UnknownError(error) => {
122                write!(f, "{}", error)
123            }
124        }
125    }
126}
127
128impl From<fmt::Error> for ExplainError {
129    fn from(error: fmt::Error) -> Self {
130        ExplainError::FormatError(error)
131    }
132}
133
134impl From<anyhow::Error> for ExplainError {
135    fn from(error: anyhow::Error) -> Self {
136        ExplainError::AnyhowError(error)
137    }
138}
139
140impl From<RecursionLimitError> for ExplainError {
141    fn from(error: RecursionLimitError) -> Self {
142        ExplainError::RecursionLimitError(error)
143    }
144}
145
146impl From<serde_json::Error> for ExplainError {
147    fn from(error: serde_json::Error) -> Self {
148        ExplainError::SerdeJsonError(error)
149    }
150}
151
152/// A set of options for controlling the output of [`Explain`] implementations.
153#[derive(Clone, Debug)]
154pub struct ExplainConfig {
155    // Analyses:
156    // (These are shown only if the Analysis is supported by the backing IR.)
157    /// Show the `SubtreeSize` Analysis in the explanation.
158    pub subtree_size: bool,
159    /// Show the number of columns, i.e., the `Arity` Analysis.
160    pub arity: bool,
161    /// Show the types, i.e., the `RelationType` Analysis.
162    pub types: bool,
163    /// Show the sets of unique keys, i.e., the `UniqueKeys` Analysis.
164    pub keys: bool,
165    /// Show the `NonNegative` Analysis.
166    pub non_negative: bool,
167    /// Show the `Cardinality` Analysis.
168    pub cardinality: bool,
169    /// Show the `ColumnNames` Analysis.
170    pub column_names: bool,
171    /// Show the `Equivalences` Analysis.
172    pub equivalences: bool,
173    // TODO: add an option to show the `Monotonic` Analysis. This is non-trivial, because this
174    // Analysis needs the set of monotonic GlobalIds, which are cumbersome to pass around.
175
176    // Other display options:
177    /// Render implemented MIR `Join` nodes in a way which reflects the implementation.
178    pub join_impls: bool,
179    /// Use inferred column names when rendering scalar and aggregate expressions.
180    pub humanized_exprs: bool,
181    /// Restrict output trees to linear chains. Ignored if `raw_plans` is set.
182    pub linear_chains: bool,
183    /// Show the slow path plan even if a fast path plan was created. Useful for debugging.
184    /// Enforced if `timing` is set.
185    pub no_fast_path: bool,
186    /// Don't print optimizer hints.
187    pub no_notices: bool,
188    /// Show node IDs in physical plans.
189    pub node_ids: bool,
190    /// Don't normalize plans before explaining them.
191    pub raw_plans: bool,
192    /// Disable virtual syntax in the explanation.
193    pub raw_syntax: bool,
194    /// Use verbose syntax in the explanation.
195    pub verbose_syntax: bool,
196    /// Anonymize literals in the plan.
197    pub redacted: bool,
198    /// Print optimization timings.
199    pub timing: bool,
200    /// Show MFP pushdown information.
201    pub filter_pushdown: bool,
202
203    /// Optimizer feature flags.
204    pub features: OptimizerFeatureOverrides,
205}
206
207impl Default for ExplainConfig {
208    fn default() -> Self {
209        Self {
210            // Don't redact in debug builds and in CI.
211            redacted: !mz_ore::assert::soft_assertions_enabled(),
212            arity: false,
213            cardinality: false,
214            column_names: false,
215            filter_pushdown: false,
216            humanized_exprs: false,
217            join_impls: true,
218            keys: false,
219            linear_chains: false,
220            no_fast_path: true,
221            no_notices: false,
222            node_ids: false,
223            non_negative: false,
224            raw_plans: true,
225            raw_syntax: false,
226            verbose_syntax: false,
227            subtree_size: false,
228            timing: false,
229            types: false,
230            equivalences: false,
231            features: Default::default(),
232        }
233    }
234}
235
236impl ExplainConfig {
237    pub fn requires_analyses(&self) -> bool {
238        self.subtree_size
239            || self.non_negative
240            || self.arity
241            || self.types
242            || self.keys
243            || self.cardinality
244            || self.column_names
245            || self.equivalences
246    }
247}
248
249/// The type of object to be explained
250#[derive(Clone, Debug)]
251pub enum Explainee {
252    /// An existing materialized view.
253    MaterializedView(GlobalId),
254    /// An existing index.
255    Index(GlobalId),
256    /// An object that will be served using a dataflow.
257    ///
258    /// This variant is deprecated and will be removed in database-issues#5301.
259    Dataflow(GlobalId),
260    /// The object to be explained is a one-off query and may or may not be
261    /// served using a dataflow.
262    Select,
263}
264
265/// A trait that provides a unified interface for objects that
266/// can be explained.
267///
268/// All possible subjects of the various forms of an `EXPLAIN`
269/// SQL statement should implement this trait.
270pub trait Explain<'a>: 'a {
271    /// The type of the immutable context in which
272    /// the explanation will be rendered.
273    type Context;
274
275    /// The explanation type produced by a successful
276    /// [`Explain::explain_text`] call.
277    type Text: DisplayText;
278
279    /// The explanation type produced by a successful
280    /// [`Explain::explain_json`] call.
281    type Json: DisplayJson;
282
283    /// The explanation type produced by a successful
284    /// [`Explain::explain_json`] call.
285    type Dot: DisplayDot;
286
287    /// Explain an instance of [`Self`] within the given
288    /// [`Explain::Context`].
289    ///
290    /// Implementors should never have the need to not rely on
291    /// this default implementation.
292    ///
293    /// # Errors
294    ///
295    /// If the given `format` is not supported, the implementation
296    /// should return an [`ExplainError::UnsupportedFormat`].
297    ///
298    /// If an [`ExplainConfig`] parameter cannot be honored, the
299    /// implementation should silently ignore this parameter and
300    /// proceed without returning a [`Result::Err`].
301    fn explain(
302        &'a mut self,
303        format: &'a ExplainFormat,
304        context: &'a Self::Context,
305    ) -> Result<String, ExplainError> {
306        match format {
307            ExplainFormat::Text => self.explain_text(context).map(|e| text_string(&e)),
308            ExplainFormat::Json => self.explain_json(context).map(|e| json_string(&e)),
309            ExplainFormat::Dot => self.explain_dot(context).map(|e| dot_string(&e)),
310        }
311    }
312
313    /// Construct a [`Result::Ok`] of the [`Explain::Text`] format
314    /// from the config and the context.
315    ///
316    /// # Errors
317    ///
318    /// If the [`ExplainFormat::Text`] is not supported, the implementation
319    /// should return an [`ExplainError::UnsupportedFormat`].
320    ///
321    /// If an [`ExplainConfig`] parameter cannot be honored, the
322    /// implementation should silently ignore this parameter and
323    /// proceed without returning a [`Result::Err`].
324    #[allow(unused_variables)]
325    fn explain_text(&'a mut self, context: &'a Self::Context) -> Result<Self::Text, ExplainError> {
326        Err(ExplainError::UnsupportedFormat(ExplainFormat::Text))
327    }
328
329    /// Construct a [`Result::Ok`] of the [`Explain::Json`] format
330    /// from the config and the context.
331    ///
332    /// # Errors
333    ///
334    /// If the [`ExplainFormat::Json`] is not supported, the implementation
335    /// should return an [`ExplainError::UnsupportedFormat`].
336    ///
337    /// If an [`ExplainConfig`] parameter cannot be honored, the
338    /// implementation should silently ignore this parameter and
339    /// proceed without returning a [`Result::Err`].
340    #[allow(unused_variables)]
341    fn explain_json(&'a mut self, context: &'a Self::Context) -> Result<Self::Json, ExplainError> {
342        Err(ExplainError::UnsupportedFormat(ExplainFormat::Json))
343    }
344
345    /// Construct a [`Result::Ok`] of the [`Explain::Dot`] format
346    /// from the config and the context.
347    ///
348    /// # Errors
349    ///
350    /// If the [`ExplainFormat::Dot`] is not supported, the implementation
351    /// should return an [`ExplainError::UnsupportedFormat`].
352    ///
353    /// If an [`ExplainConfig`] parameter cannot be honored, the
354    /// implementation should silently ignore this parameter and
355    /// proceed without returning a [`Result::Err`].
356    #[allow(unused_variables)]
357    fn explain_dot(&'a mut self, context: &'a Self::Context) -> Result<Self::Dot, ExplainError> {
358        Err(ExplainError::UnsupportedFormat(ExplainFormat::Dot))
359    }
360}
361
362/// A helper struct which will most commonly be used as the generic
363/// rendering context type `C` for various `Explain$Format`
364/// implementations.
365#[derive(Debug)]
366pub struct RenderingContext<'a> {
367    pub indent: Indent,
368    pub humanizer: &'a dyn ExprHumanizer,
369}
370
371impl<'a> RenderingContext<'a> {
372    pub fn new(indent: Indent, humanizer: &'a dyn ExprHumanizer) -> RenderingContext<'a> {
373        RenderingContext { indent, humanizer }
374    }
375}
376
377impl<'a> AsMut<Indent> for RenderingContext<'a> {
378    fn as_mut(&mut self) -> &mut Indent {
379        &mut self.indent
380    }
381}
382
383impl<'a> AsRef<&'a dyn ExprHumanizer> for RenderingContext<'a> {
384    fn as_ref(&self) -> &&'a dyn ExprHumanizer {
385        &self.humanizer
386    }
387}
388
389#[allow(missing_debug_implementations)]
390pub struct PlanRenderingContext<'a, T> {
391    pub indent: Indent,
392    pub humanizer: &'a dyn ExprHumanizer,
393    pub annotations: BTreeMap<&'a T, Analyses>,
394    pub config: &'a ExplainConfig,
395}
396
397impl<'a, T> PlanRenderingContext<'a, T> {
398    pub fn new(
399        indent: Indent,
400        humanizer: &'a dyn ExprHumanizer,
401        annotations: BTreeMap<&'a T, Analyses>,
402        config: &'a ExplainConfig,
403    ) -> PlanRenderingContext<'a, T> {
404        PlanRenderingContext {
405            indent,
406            humanizer,
407            annotations,
408            config,
409        }
410    }
411}
412
413impl<'a, T> AsMut<Indent> for PlanRenderingContext<'a, T> {
414    fn as_mut(&mut self) -> &mut Indent {
415        &mut self.indent
416    }
417}
418
419impl<'a, T> AsRef<&'a dyn ExprHumanizer> for PlanRenderingContext<'a, T> {
420    fn as_ref(&self) -> &&'a dyn ExprHumanizer {
421        &self.humanizer
422    }
423}
424
425/// A trait for humanizing components of an expression.
426///
427/// This will be most often used as part of the rendering context
428/// type for various `Display$Format` implementation.
429pub trait ExprHumanizer: fmt::Debug {
430    /// Attempts to return a human-readable string for the relation
431    /// identified by `id`.
432    fn humanize_id(&self, id: GlobalId) -> Option<String>;
433
434    /// Same as above, but without qualifications, e.g., only `foo` for `materialize.public.foo`.
435    fn humanize_id_unqualified(&self, id: GlobalId) -> Option<String>;
436
437    /// Like [`Self::humanize_id`], but returns the constituent parts of the
438    /// name as individual elements.
439    fn humanize_id_parts(&self, id: GlobalId) -> Option<Vec<String>>;
440
441    /// Returns a human-readable name for the specified scalar type.
442    /// Used in, e.g., EXPLAIN and error msgs, in which case exact Postgres compatibility is less
443    /// important than showing as much detail as possible. Also used in `pg_typeof`, where Postgres
444    /// compatibility is more important.
445    fn humanize_scalar_type(&self, ty: &ScalarType, postgres_compat: bool) -> String;
446
447    /// Returns a human-readable name for the specified column type.
448    /// Used in, e.g., EXPLAIN and error msgs, in which case exact Postgres compatibility is less
449    /// important than showing as much detail as possible. Also used in `pg_typeof`, where Postgres
450    /// compatibility is more important.
451    fn humanize_column_type(&self, typ: &ColumnType, postgres_compat: bool) -> String {
452        format!(
453            "{}{}",
454            self.humanize_scalar_type(&typ.scalar_type, postgres_compat),
455            if typ.nullable { "?" } else { "" }
456        )
457    }
458
459    /// Returns a vector of column names for the relation identified by `id`.
460    fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>>;
461
462    /// Returns the `#column` name for the relation identified by `id`.
463    fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String>;
464
465    /// Returns whether the specified id exists.
466    fn id_exists(&self, id: GlobalId) -> bool;
467}
468
469/// An [`ExprHumanizer`] that extends the `inner` instance with shadow items
470/// that are reported as present, even though they might not exist in `inner`.
471#[derive(Debug)]
472pub struct ExprHumanizerExt<'a> {
473    /// A map of custom items that might not exist in the backing `inner`
474    /// humanizer, but are reported as present by this humanizer instance.
475    items: BTreeMap<GlobalId, TransientItem>,
476    /// The inner humanizer used to resolve queries for [GlobalId] values not
477    /// present in the `items` map.
478    inner: &'a dyn ExprHumanizer,
479}
480
481impl<'a> ExprHumanizerExt<'a> {
482    pub fn new(items: BTreeMap<GlobalId, TransientItem>, inner: &'a dyn ExprHumanizer) -> Self {
483        Self { items, inner }
484    }
485}
486
487impl<'a> ExprHumanizer for ExprHumanizerExt<'a> {
488    fn humanize_id(&self, id: GlobalId) -> Option<String> {
489        match self.items.get(&id) {
490            Some(item) => item
491                .humanized_id_parts
492                .as_ref()
493                .map(|parts| parts.join(".")),
494            None => self.inner.humanize_id(id),
495        }
496    }
497
498    fn humanize_id_unqualified(&self, id: GlobalId) -> Option<String> {
499        match self.items.get(&id) {
500            Some(item) => item
501                .humanized_id_parts
502                .as_ref()
503                .and_then(|parts| parts.last().cloned()),
504            None => self.inner.humanize_id_unqualified(id),
505        }
506    }
507
508    fn humanize_id_parts(&self, id: GlobalId) -> Option<Vec<String>> {
509        match self.items.get(&id) {
510            Some(item) => item.humanized_id_parts.clone(),
511            None => self.inner.humanize_id_parts(id),
512        }
513    }
514
515    fn humanize_scalar_type(&self, ty: &ScalarType, postgres_compat: bool) -> String {
516        self.inner.humanize_scalar_type(ty, postgres_compat)
517    }
518
519    fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>> {
520        match self.items.get(&id) {
521            Some(item) => item.column_names.clone(),
522            None => self.inner.column_names_for_id(id),
523        }
524    }
525
526    fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String> {
527        match self.items.get(&id) {
528            Some(item) => match &item.column_names {
529                Some(column_names) => Some(column_names[column].clone()),
530                None => None,
531            },
532            None => self.inner.humanize_column(id, column),
533        }
534    }
535
536    fn id_exists(&self, id: GlobalId) -> bool {
537        self.items.contains_key(&id) || self.inner.id_exists(id)
538    }
539}
540
541/// A description of a catalog item that does not exist, but can be reported as
542/// present in the catalog by a [`ExprHumanizerExt`] instance that has it in its
543/// `items` list.
544#[derive(Debug)]
545pub struct TransientItem {
546    humanized_id_parts: Option<Vec<String>>,
547    column_names: Option<Vec<String>>,
548}
549
550impl TransientItem {
551    pub fn new(humanized_id_parts: Option<Vec<String>>, column_names: Option<Vec<String>>) -> Self {
552        Self {
553            humanized_id_parts,
554            column_names,
555        }
556    }
557}
558
559/// A bare-minimum implementation of [`ExprHumanizer`].
560///
561/// The `DummyHumanizer` does a poor job of humanizing expressions. It is
562/// intended for use in contexts where polish is not required, like in tests or
563/// while debugging.
564#[derive(Debug)]
565pub struct DummyHumanizer;
566
567impl ExprHumanizer for DummyHumanizer {
568    fn humanize_id(&self, _: GlobalId) -> Option<String> {
569        // Returning `None` allows the caller to fall back to displaying the
570        // ID, if they so desire.
571        None
572    }
573
574    fn humanize_id_unqualified(&self, _id: GlobalId) -> Option<String> {
575        None
576    }
577
578    fn humanize_id_parts(&self, _id: GlobalId) -> Option<Vec<String>> {
579        None
580    }
581
582    fn humanize_scalar_type(&self, ty: &ScalarType, _postgres_compat: bool) -> String {
583        // The debug implementation is better than nothing.
584        format!("{:?}", ty)
585    }
586
587    fn column_names_for_id(&self, _id: GlobalId) -> Option<Vec<String>> {
588        None
589    }
590
591    fn humanize_column(&self, _id: GlobalId, _column: usize) -> Option<String> {
592        None
593    }
594
595    fn id_exists(&self, _id: GlobalId) -> bool {
596        false
597    }
598}
599
600/// Pretty-prints a list of indices.
601#[derive(Debug)]
602pub struct Indices<'a>(pub &'a [usize]);
603
604/// Pretty-prints a list of scalar expressions that may have runs of column
605/// indices as a comma-separated list interleaved with interval expressions.
606///
607/// Interval expressions are used only for runs of three or more elements.
608#[derive(Debug)]
609pub struct CompactScalarSeq<'a, T: ScalarOps>(pub &'a [T]); // TODO(cloud#8196) remove this
610
611/// Pretty-prints a list of scalar expressions that may have runs of column
612/// indices as a comma-separated list interleaved with interval expressions.
613///
614/// Interval expressions are used only for runs of three or more elements.
615#[derive(Debug)]
616pub struct CompactScalars<T, I>(pub I)
617where
618    T: ScalarOps,
619    I: Iterator<Item = T> + Clone;
620
621pub trait ScalarOps {
622    fn match_col_ref(&self) -> Option<usize>;
623
624    fn references(&self, col_ref: usize) -> bool;
625}
626
627/// A somewhat ad-hoc way to keep carry a plan with a set
628/// of analyses derived for each node in that plan.
629#[allow(missing_debug_implementations)]
630pub struct AnnotatedPlan<'a, T> {
631    pub plan: &'a T,
632    pub annotations: BTreeMap<&'a T, Analyses>,
633}
634
635/// A container for derived analyses.
636#[derive(Clone, Default, Debug)]
637pub struct Analyses {
638    pub non_negative: Option<bool>,
639    pub subtree_size: Option<usize>,
640    pub arity: Option<usize>,
641    pub types: Option<Option<Vec<ColumnType>>>,
642    pub keys: Option<Vec<Vec<usize>>>,
643    pub cardinality: Option<String>,
644    pub column_names: Option<Vec<String>>,
645    pub equivalences: Option<String>,
646}
647
648#[derive(Debug, Clone)]
649pub struct HumanizedAnalyses<'a> {
650    analyses: &'a Analyses,
651    humanizer: &'a dyn ExprHumanizer,
652    config: &'a ExplainConfig,
653}
654
655impl<'a> HumanizedAnalyses<'a> {
656    pub fn new<T>(analyses: &'a Analyses, ctx: &PlanRenderingContext<'a, T>) -> Self {
657        Self {
658            analyses,
659            humanizer: ctx.humanizer,
660            config: ctx.config,
661        }
662    }
663}
664
665impl<'a> Display for HumanizedAnalyses<'a> {
666    // Analysis rendering is guarded by the ExplainConfig flag for each
667    // Analysis. This is needed because we might have derived Analysis that
668    // are not explicitly requested (such as column_names), in which case we
669    // don't want to display them.
670    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
671        let mut builder = f.debug_struct("//");
672
673        if self.config.subtree_size {
674            let subtree_size = self.analyses.subtree_size.expect("subtree_size");
675            builder.field("subtree_size", &subtree_size);
676        }
677
678        if self.config.non_negative {
679            let non_negative = self.analyses.non_negative.expect("non_negative");
680            builder.field("non_negative", &non_negative);
681        }
682
683        if self.config.arity {
684            let arity = self.analyses.arity.expect("arity");
685            builder.field("arity", &arity);
686        }
687
688        if self.config.types {
689            let types = match self.analyses.types.as_ref().expect("types") {
690                Some(types) => {
691                    let types = types
692                        .into_iter()
693                        .map(|c| self.humanizer.humanize_column_type(c, false))
694                        .collect::<Vec<_>>();
695
696                    bracketed("(", ")", separated(", ", types)).to_string()
697                }
698                None => "(<error>)".to_string(),
699            };
700            builder.field("types", &types);
701        }
702
703        if self.config.keys {
704            let keys = self
705                .analyses
706                .keys
707                .as_ref()
708                .expect("keys")
709                .into_iter()
710                .map(|key| bracketed("[", "]", separated(", ", key)).to_string());
711            let keys = bracketed("(", ")", separated(", ", keys)).to_string();
712            builder.field("keys", &keys);
713        }
714
715        if self.config.cardinality {
716            let cardinality = self.analyses.cardinality.as_ref().expect("cardinality");
717            builder.field("cardinality", cardinality);
718        }
719
720        if self.config.column_names {
721            let column_names = self.analyses.column_names.as_ref().expect("column_names");
722            let column_names = column_names.into_iter().enumerate().map(|(i, c)| {
723                if c.is_empty() {
724                    Cow::Owned(format!("#{i}"))
725                } else {
726                    Cow::Borrowed(c)
727                }
728            });
729            let column_names = bracketed("(", ")", separated(", ", column_names)).to_string();
730            builder.field("column_names", &column_names);
731        }
732
733        if self.config.equivalences {
734            let equivs = self.analyses.equivalences.as_ref().expect("equivalences");
735            builder.field("equivs", equivs);
736        }
737
738        builder.finish()
739    }
740}
741
742/// A set of indexes that are used in the explained plan.
743///
744/// Each element consists of the following components:
745/// 1. The id of the index.
746/// 2. A vector of [IndexUsageType] denoting how the index is used in the plan.
747///
748/// Using a `BTreeSet` here ensures a deterministic iteration order, which in turn ensures that
749/// the corresponding EXPLAIN output is deterministic as well.
750#[derive(Clone, Debug, Default)]
751pub struct UsedIndexes(BTreeSet<(GlobalId, Vec<IndexUsageType>)>);
752
753impl UsedIndexes {
754    pub fn new(values: BTreeSet<(GlobalId, Vec<IndexUsageType>)>) -> UsedIndexes {
755        UsedIndexes(values)
756    }
757
758    pub fn is_empty(&self) -> bool {
759        self.0.is_empty()
760    }
761}
762
763#[derive(Debug, Clone, Arbitrary, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)]
764pub enum IndexUsageType {
765    /// Read the entire index.
766    FullScan,
767    /// Differential join. The work is proportional to the number of matches.
768    DifferentialJoin,
769    /// Delta join
770    DeltaJoin(DeltaJoinIndexUsageType),
771    /// `IndexedFilter`, e.g., something like `WHERE x = 42` with an index on `x`.
772    /// This also stores the id of the index that we want to do the lookup from. (This id is already
773    /// chosen by `LiteralConstraints`, and then `IndexUsageType::Lookup` communicates this inside
774    /// `CollectIndexRequests` from the `IndexedFilter` to the `Get`.)
775    Lookup(GlobalId),
776    /// This is a rare case that happens when the user creates an index that is identical to an
777    /// existing one (i.e., on the same object, and with the same keys). We'll re-use the
778    /// arrangement of the existing index. The plan is an `ArrangeBy` + `Get`, where the `ArrangeBy`
779    /// is requesting the same key as an already existing index. (`export_index` is what inserts
780    /// this `ArrangeBy`.)
781    PlanRootNoArrangement,
782    /// The index is used for directly writing to a sink. Can happen with a SUBSCRIBE to an indexed
783    /// view.
784    SinkExport,
785    /// The index is used for creating a new index. Note that either a `FullScan` or a
786    /// `PlanRootNoArrangement` usage will always accompany an `IndexExport` usage.
787    IndexExport,
788    /// When a fast path peek has a LIMIT, but no ORDER BY, then we read from the index only as many
789    /// records (approximately), as the OFFSET + LIMIT needs.
790    /// Note: When a fast path peek does a lookup and also has a limit, the usage type will be
791    /// `Lookup`. However, the smart limiting logic will still apply.
792    FastPathLimit,
793    /// We saw a dangling `ArrangeBy`, i.e., where we have no idea what the arrangement will be used
794    /// for. This is an internal error. Can be a bug either in `CollectIndexRequests`, or some
795    /// other transform that messed up the plan. It's also possible that somebody is trying to add
796    /// an `ArrangeBy` marking for some operator other than a `Join`. (Which is fine, but please
797    /// update `CollectIndexRequests`.)
798    DanglingArrangeBy,
799    /// Internal error in `CollectIndexRequests` or a failed attempt to look up
800    /// an index in `DataflowMetainfo::used_indexes`.
801    Unknown,
802}
803
804/// In a snapshot, one arrangement of the first input is scanned, all the other arrangements (of the
805/// first input, and of all other inputs) only get lookups.
806/// When later input batches are arriving, all inputs are fully read.
807#[derive(Debug, Clone, Arbitrary, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)]
808pub enum DeltaJoinIndexUsageType {
809    Unknown,
810    Lookup,
811    FirstInputFullScan,
812}
813
814impl std::fmt::Display for IndexUsageType {
815    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
816        write!(
817            f,
818            "{}",
819            match self {
820                IndexUsageType::FullScan => "*** full scan ***",
821                IndexUsageType::Lookup(_idx_id) => "lookup",
822                IndexUsageType::DifferentialJoin => "differential join",
823                IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::FirstInputFullScan) =>
824                    "delta join 1st input (full scan)",
825                // Technically, this is a lookup only for a snapshot. For later update batches, all
826                // records are read. However, I wrote lookup here, because in most cases the
827                // lookup/scan distinction matters only for a snapshot. This is because for arriving
828                // update records, something in the system will always do work proportional to the
829                // number of records anyway. In other words, something is always scanning new
830                // updates, but we can avoid scanning records again and again in snapshots.
831                IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Lookup) => "delta join lookup",
832                IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown) =>
833                    "*** INTERNAL ERROR (unknown delta join usage) ***",
834                IndexUsageType::PlanRootNoArrangement => "plan root (no new arrangement)",
835                IndexUsageType::SinkExport => "sink export",
836                IndexUsageType::IndexExport => "index export",
837                IndexUsageType::FastPathLimit => "fast path limit",
838                IndexUsageType::DanglingArrangeBy => "*** INTERNAL ERROR (dangling ArrangeBy) ***",
839                IndexUsageType::Unknown => "*** INTERNAL ERROR (unknown usage) ***",
840            }
841        )
842    }
843}
844
845impl IndexUsageType {
846    pub fn display_vec<'a, I>(usage_types: I) -> impl Display + Sized + 'a
847    where
848        I: IntoIterator<Item = &'a IndexUsageType>,
849    {
850        separated(", ", usage_types.into_iter().sorted().dedup())
851    }
852}
853
854#[cfg(test)]
855mod tests {
856    use mz_ore::assert_ok;
857
858    use super::*;
859
860    struct Environment {
861        name: String,
862    }
863
864    impl Default for Environment {
865        fn default() -> Self {
866            Environment {
867                name: "test env".to_string(),
868            }
869        }
870    }
871
872    struct Frontiers<T> {
873        since: T,
874        upper: T,
875    }
876
877    impl<T> Frontiers<T> {
878        fn new(since: T, upper: T) -> Self {
879            Self { since, upper }
880        }
881    }
882
883    struct ExplainContext<'a> {
884        env: &'a mut Environment,
885        config: &'a ExplainConfig,
886        frontiers: Frontiers<u64>,
887    }
888
889    /// A test IR that should be the subject of explanations.
890    struct TestExpr {
891        lhs: i32,
892        rhs: i32,
893    }
894
895    struct TestExplanation<'a> {
896        expr: &'a TestExpr,
897        context: &'a ExplainContext<'a>,
898    }
899
900    impl<'a> DisplayText for TestExplanation<'a> {
901        fn fmt_text(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
902            let lhs = &self.expr.lhs;
903            let rhs = &self.expr.rhs;
904            writeln!(f, "expr = {lhs} + {rhs}")?;
905
906            if self.context.config.timing {
907                let since = &self.context.frontiers.since;
908                let upper = &self.context.frontiers.upper;
909                writeln!(f, "at t ∊ [{since}, {upper})")?;
910            }
911
912            let name = &self.context.env.name;
913            writeln!(f, "env = {name}")?;
914
915            Ok(())
916        }
917    }
918
919    impl<'a> Explain<'a> for TestExpr {
920        type Context = ExplainContext<'a>;
921        type Text = TestExplanation<'a>;
922        type Json = UnsupportedFormat;
923        type Dot = UnsupportedFormat;
924
925        fn explain_text(
926            &'a mut self,
927            context: &'a Self::Context,
928        ) -> Result<Self::Text, ExplainError> {
929            Ok(TestExplanation {
930                expr: self,
931                context,
932            })
933        }
934    }
935
936    fn do_explain(
937        env: &mut Environment,
938        frontiers: Frontiers<u64>,
939    ) -> Result<String, ExplainError> {
940        let mut expr = TestExpr { lhs: 1, rhs: 2 };
941
942        let format = ExplainFormat::Text;
943        let config = &ExplainConfig {
944            redacted: false,
945            arity: false,
946            cardinality: false,
947            column_names: false,
948            filter_pushdown: false,
949            humanized_exprs: false,
950            join_impls: false,
951            keys: false,
952            linear_chains: false,
953            no_fast_path: false,
954            no_notices: false,
955            node_ids: false,
956            non_negative: false,
957            raw_plans: false,
958            raw_syntax: false,
959            verbose_syntax: true,
960            subtree_size: false,
961            equivalences: false,
962            timing: true,
963            types: false,
964            features: Default::default(),
965        };
966        let context = ExplainContext {
967            env,
968            config,
969            frontiers,
970        };
971
972        expr.explain(&format, &context)
973    }
974
975    #[mz_ore::test]
976    fn test_mutable_context() {
977        let mut env = Environment::default();
978        let frontiers = Frontiers::<u64>::new(3, 7);
979
980        let act = do_explain(&mut env, frontiers);
981        let exp = "expr = 1 + 2\nat t ∊ [3, 7)\nenv = test env\n".to_string();
982
983        assert_ok!(act);
984        assert_eq!(act.unwrap(), exp);
985    }
986}