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::{GlobalId, ReprColumnType, ReprScalarType, SqlColumnType, SqlScalarType};
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 `SqlRelationType` 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: &SqlScalarType, postgres_compat: bool) -> String;
446
447    /// Returns a human-readable name for the specified scalar type.
448    /// Calls `humanize_scalar_type` with the `SqlScalarType` representation of the specified type.
449    fn humanize_scalar_type_repr(&self, typ: &ReprScalarType, postgres_compat: bool) -> String {
450        self.humanize_scalar_type(&SqlScalarType::from_repr(typ), postgres_compat)
451    }
452
453    /// Returns a human-readable name for the specified column type.
454    /// Used in, e.g., EXPLAIN and error msgs, in which case exact Postgres compatibility is less
455    /// important than showing as much detail as possible. Also used in `pg_typeof`, where Postgres
456    /// compatibility is more important.
457    fn humanize_column_type(&self, typ: &SqlColumnType, postgres_compat: bool) -> String {
458        format!(
459            "{}{}",
460            self.humanize_scalar_type(&typ.scalar_type, postgres_compat),
461            if typ.nullable { "?" } else { "" }
462        )
463    }
464
465    /// Returns a human-readable name for the specified column type.
466    /// Calls `humanize_column_type` with the `SqlColumnType` representation of the specified type.
467    fn humanize_column_type_repr(&self, typ: &ReprColumnType, postgres_compat: bool) -> String {
468        self.humanize_column_type(&SqlColumnType::from_repr(typ), postgres_compat)
469    }
470
471    /// Returns a vector of column names for the relation identified by `id`.
472    fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>>;
473
474    /// Returns the `#column` name for the relation identified by `id`.
475    fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String>;
476
477    /// Returns whether the specified id exists.
478    fn id_exists(&self, id: GlobalId) -> bool;
479}
480
481/// An [`ExprHumanizer`] that extends the `inner` instance with shadow items
482/// that are reported as present, even though they might not exist in `inner`.
483#[derive(Debug)]
484pub struct ExprHumanizerExt<'a> {
485    /// A map of custom items that might not exist in the backing `inner`
486    /// humanizer, but are reported as present by this humanizer instance.
487    items: BTreeMap<GlobalId, TransientItem>,
488    /// The inner humanizer used to resolve queries for [GlobalId] values not
489    /// present in the `items` map.
490    inner: &'a dyn ExprHumanizer,
491}
492
493impl<'a> ExprHumanizerExt<'a> {
494    pub fn new(items: BTreeMap<GlobalId, TransientItem>, inner: &'a dyn ExprHumanizer) -> Self {
495        Self { items, inner }
496    }
497}
498
499impl<'a> ExprHumanizer for ExprHumanizerExt<'a> {
500    fn humanize_id(&self, id: GlobalId) -> Option<String> {
501        match self.items.get(&id) {
502            Some(item) => item
503                .humanized_id_parts
504                .as_ref()
505                .map(|parts| parts.join(".")),
506            None => self.inner.humanize_id(id),
507        }
508    }
509
510    fn humanize_id_unqualified(&self, id: GlobalId) -> Option<String> {
511        match self.items.get(&id) {
512            Some(item) => item
513                .humanized_id_parts
514                .as_ref()
515                .and_then(|parts| parts.last().cloned()),
516            None => self.inner.humanize_id_unqualified(id),
517        }
518    }
519
520    fn humanize_id_parts(&self, id: GlobalId) -> Option<Vec<String>> {
521        match self.items.get(&id) {
522            Some(item) => item.humanized_id_parts.clone(),
523            None => self.inner.humanize_id_parts(id),
524        }
525    }
526
527    fn humanize_scalar_type(&self, ty: &SqlScalarType, postgres_compat: bool) -> String {
528        self.inner.humanize_scalar_type(ty, postgres_compat)
529    }
530
531    fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>> {
532        match self.items.get(&id) {
533            Some(item) => item.column_names.clone(),
534            None => self.inner.column_names_for_id(id),
535        }
536    }
537
538    fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String> {
539        match self.items.get(&id) {
540            Some(item) => match &item.column_names {
541                Some(column_names) => Some(column_names[column].clone()),
542                None => None,
543            },
544            None => self.inner.humanize_column(id, column),
545        }
546    }
547
548    fn id_exists(&self, id: GlobalId) -> bool {
549        self.items.contains_key(&id) || self.inner.id_exists(id)
550    }
551}
552
553/// A description of a catalog item that does not exist, but can be reported as
554/// present in the catalog by a [`ExprHumanizerExt`] instance that has it in its
555/// `items` list.
556#[derive(Debug)]
557pub struct TransientItem {
558    humanized_id_parts: Option<Vec<String>>,
559    column_names: Option<Vec<String>>,
560}
561
562impl TransientItem {
563    pub fn new(humanized_id_parts: Option<Vec<String>>, column_names: Option<Vec<String>>) -> Self {
564        Self {
565            humanized_id_parts,
566            column_names,
567        }
568    }
569}
570
571/// A bare-minimum implementation of [`ExprHumanizer`].
572///
573/// The `DummyHumanizer` does a poor job of humanizing expressions. It is
574/// intended for use in contexts where polish is not required, like in tests or
575/// while debugging.
576#[derive(Debug)]
577pub struct DummyHumanizer;
578
579impl ExprHumanizer for DummyHumanizer {
580    fn humanize_id(&self, _: GlobalId) -> Option<String> {
581        // Returning `None` allows the caller to fall back to displaying the
582        // ID, if they so desire.
583        None
584    }
585
586    fn humanize_id_unqualified(&self, _id: GlobalId) -> Option<String> {
587        None
588    }
589
590    fn humanize_id_parts(&self, _id: GlobalId) -> Option<Vec<String>> {
591        None
592    }
593
594    fn humanize_scalar_type(&self, ty: &SqlScalarType, _postgres_compat: bool) -> String {
595        // The debug implementation is better than nothing.
596        format!("{:?}", ty)
597    }
598
599    fn column_names_for_id(&self, _id: GlobalId) -> Option<Vec<String>> {
600        None
601    }
602
603    fn humanize_column(&self, _id: GlobalId, _column: usize) -> Option<String> {
604        None
605    }
606
607    fn id_exists(&self, _id: GlobalId) -> bool {
608        false
609    }
610}
611
612/// Pretty-prints a list of indices.
613#[derive(Debug)]
614pub struct Indices<'a>(pub &'a [usize]);
615
616/// Pretty-prints a list of scalar expressions that may have runs of column
617/// indices as a comma-separated list interleaved with interval expressions.
618///
619/// Interval expressions are used only for runs of three or more elements.
620#[derive(Debug)]
621pub struct CompactScalarSeq<'a, T: ScalarOps>(pub &'a [T]); // TODO(cloud#8196) remove this
622
623/// Pretty-prints a list of scalar expressions that may have runs of column
624/// indices as a comma-separated list interleaved with interval expressions.
625///
626/// Interval expressions are used only for runs of three or more elements.
627#[derive(Debug)]
628pub struct CompactScalars<T, I>(pub I)
629where
630    T: ScalarOps,
631    I: Iterator<Item = T> + Clone;
632
633pub trait ScalarOps {
634    fn match_col_ref(&self) -> Option<usize>;
635
636    fn references(&self, col_ref: usize) -> bool;
637}
638
639/// A somewhat ad-hoc way to keep carry a plan with a set
640/// of analyses derived for each node in that plan.
641#[allow(missing_debug_implementations)]
642pub struct AnnotatedPlan<'a, T> {
643    pub plan: &'a T,
644    pub annotations: BTreeMap<&'a T, Analyses>,
645}
646
647/// A container for derived analyses.
648#[derive(Clone, Default, Debug)]
649pub struct Analyses {
650    pub non_negative: Option<bool>,
651    pub subtree_size: Option<usize>,
652    pub arity: Option<usize>,
653    pub types: Option<Option<Vec<SqlColumnType>>>,
654    pub keys: Option<Vec<Vec<usize>>>,
655    pub cardinality: Option<String>,
656    pub column_names: Option<Vec<String>>,
657    pub equivalences: Option<String>,
658}
659
660#[derive(Debug, Clone)]
661pub struct HumanizedAnalyses<'a> {
662    analyses: &'a Analyses,
663    humanizer: &'a dyn ExprHumanizer,
664    config: &'a ExplainConfig,
665}
666
667impl<'a> HumanizedAnalyses<'a> {
668    pub fn new<T>(analyses: &'a Analyses, ctx: &PlanRenderingContext<'a, T>) -> Self {
669        Self {
670            analyses,
671            humanizer: ctx.humanizer,
672            config: ctx.config,
673        }
674    }
675}
676
677impl<'a> Display for HumanizedAnalyses<'a> {
678    // Analysis rendering is guarded by the ExplainConfig flag for each
679    // Analysis. This is needed because we might have derived Analysis that
680    // are not explicitly requested (such as column_names), in which case we
681    // don't want to display them.
682    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
683        let mut builder = f.debug_struct("//");
684
685        if self.config.subtree_size {
686            let subtree_size = self.analyses.subtree_size.expect("subtree_size");
687            builder.field("subtree_size", &subtree_size);
688        }
689
690        if self.config.non_negative {
691            let non_negative = self.analyses.non_negative.expect("non_negative");
692            builder.field("non_negative", &non_negative);
693        }
694
695        if self.config.arity {
696            let arity = self.analyses.arity.expect("arity");
697            builder.field("arity", &arity);
698        }
699
700        if self.config.types {
701            let types = match self.analyses.types.as_ref().expect("types") {
702                Some(types) => {
703                    let types = types
704                        .into_iter()
705                        .map(|c| self.humanizer.humanize_column_type(c, false))
706                        .collect::<Vec<_>>();
707
708                    bracketed("(", ")", separated(", ", types)).to_string()
709                }
710                None => "(<error>)".to_string(),
711            };
712            builder.field("types", &types);
713        }
714
715        if self.config.keys {
716            let keys = self
717                .analyses
718                .keys
719                .as_ref()
720                .expect("keys")
721                .into_iter()
722                .map(|key| bracketed("[", "]", separated(", ", key)).to_string());
723            let keys = bracketed("(", ")", separated(", ", keys)).to_string();
724            builder.field("keys", &keys);
725        }
726
727        if self.config.cardinality {
728            let cardinality = self.analyses.cardinality.as_ref().expect("cardinality");
729            builder.field("cardinality", cardinality);
730        }
731
732        if self.config.column_names {
733            let column_names = self.analyses.column_names.as_ref().expect("column_names");
734            let column_names = column_names.into_iter().enumerate().map(|(i, c)| {
735                if c.is_empty() {
736                    Cow::Owned(format!("#{i}"))
737                } else {
738                    Cow::Borrowed(c)
739                }
740            });
741            let column_names = bracketed("(", ")", separated(", ", column_names)).to_string();
742            builder.field("column_names", &column_names);
743        }
744
745        if self.config.equivalences {
746            let equivs = self.analyses.equivalences.as_ref().expect("equivalences");
747            builder.field("equivs", equivs);
748        }
749
750        builder.finish()
751    }
752}
753
754/// A set of indexes that are used in the explained plan.
755///
756/// Each element consists of the following components:
757/// 1. The id of the index.
758/// 2. A vector of [IndexUsageType] denoting how the index is used in the plan.
759///
760/// Using a `BTreeSet` here ensures a deterministic iteration order, which in turn ensures that
761/// the corresponding EXPLAIN output is deterministic as well.
762#[derive(Clone, Debug, Default)]
763pub struct UsedIndexes(BTreeSet<(GlobalId, Vec<IndexUsageType>)>);
764
765impl UsedIndexes {
766    pub fn new(values: BTreeSet<(GlobalId, Vec<IndexUsageType>)>) -> UsedIndexes {
767        UsedIndexes(values)
768    }
769
770    pub fn is_empty(&self) -> bool {
771        self.0.is_empty()
772    }
773}
774
775#[derive(Debug, Clone, Arbitrary, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)]
776pub enum IndexUsageType {
777    /// Read the entire index.
778    FullScan,
779    /// Differential join. The work is proportional to the number of matches.
780    DifferentialJoin,
781    /// Delta join
782    DeltaJoin(DeltaJoinIndexUsageType),
783    /// `IndexedFilter`, e.g., something like `WHERE x = 42` with an index on `x`.
784    /// This also stores the id of the index that we want to do the lookup from. (This id is already
785    /// chosen by `LiteralConstraints`, and then `IndexUsageType::Lookup` communicates this inside
786    /// `CollectIndexRequests` from the `IndexedFilter` to the `Get`.)
787    Lookup(GlobalId),
788    /// This is a rare case that happens when the user creates an index that is identical to an
789    /// existing one (i.e., on the same object, and with the same keys). We'll re-use the
790    /// arrangement of the existing index. The plan is an `ArrangeBy` + `Get`, where the `ArrangeBy`
791    /// is requesting the same key as an already existing index. (`export_index` is what inserts
792    /// this `ArrangeBy`.)
793    PlanRootNoArrangement,
794    /// The index is used for directly writing to a sink. Can happen with a SUBSCRIBE to an indexed
795    /// view.
796    SinkExport,
797    /// The index is used for creating a new index. Note that either a `FullScan` or a
798    /// `PlanRootNoArrangement` usage will always accompany an `IndexExport` usage.
799    IndexExport,
800    /// When a fast path peek has a LIMIT, but no ORDER BY, then we read from the index only as many
801    /// records (approximately), as the OFFSET + LIMIT needs.
802    /// Note: When a fast path peek does a lookup and also has a limit, the usage type will be
803    /// `Lookup`. However, the smart limiting logic will still apply.
804    FastPathLimit,
805    /// We saw a dangling `ArrangeBy`, i.e., where we have no idea what the arrangement will be used
806    /// for. This is an internal error. Can be a bug either in `CollectIndexRequests`, or some
807    /// other transform that messed up the plan. It's also possible that somebody is trying to add
808    /// an `ArrangeBy` marking for some operator other than a `Join`. (Which is fine, but please
809    /// update `CollectIndexRequests`.)
810    DanglingArrangeBy,
811    /// Internal error in `CollectIndexRequests` or a failed attempt to look up
812    /// an index in `DataflowMetainfo::used_indexes`.
813    Unknown,
814}
815
816/// In a snapshot, one arrangement of the first input is scanned, all the other arrangements (of the
817/// first input, and of all other inputs) only get lookups.
818/// When later input batches are arriving, all inputs are fully read.
819#[derive(Debug, Clone, Arbitrary, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)]
820pub enum DeltaJoinIndexUsageType {
821    Unknown,
822    Lookup,
823    FirstInputFullScan,
824}
825
826impl std::fmt::Display for IndexUsageType {
827    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
828        write!(
829            f,
830            "{}",
831            match self {
832                IndexUsageType::FullScan => "*** full scan ***",
833                IndexUsageType::Lookup(_idx_id) => "lookup",
834                IndexUsageType::DifferentialJoin => "differential join",
835                IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::FirstInputFullScan) =>
836                    "delta join 1st input (full scan)",
837                // Technically, this is a lookup only for a snapshot. For later update batches, all
838                // records are read. However, I wrote lookup here, because in most cases the
839                // lookup/scan distinction matters only for a snapshot. This is because for arriving
840                // update records, something in the system will always do work proportional to the
841                // number of records anyway. In other words, something is always scanning new
842                // updates, but we can avoid scanning records again and again in snapshots.
843                IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Lookup) => "delta join lookup",
844                IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown) =>
845                    "*** INTERNAL ERROR (unknown delta join usage) ***",
846                IndexUsageType::PlanRootNoArrangement => "plan root (no new arrangement)",
847                IndexUsageType::SinkExport => "sink export",
848                IndexUsageType::IndexExport => "index export",
849                IndexUsageType::FastPathLimit => "fast path limit",
850                IndexUsageType::DanglingArrangeBy => "*** INTERNAL ERROR (dangling ArrangeBy) ***",
851                IndexUsageType::Unknown => "*** INTERNAL ERROR (unknown usage) ***",
852            }
853        )
854    }
855}
856
857impl IndexUsageType {
858    pub fn display_vec<'a, I>(usage_types: I) -> impl Display + Sized + 'a
859    where
860        I: IntoIterator<Item = &'a IndexUsageType>,
861    {
862        separated(", ", usage_types.into_iter().sorted().dedup())
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use mz_ore::assert_ok;
869
870    use super::*;
871
872    struct Environment {
873        name: String,
874    }
875
876    impl Default for Environment {
877        fn default() -> Self {
878            Environment {
879                name: "test env".to_string(),
880            }
881        }
882    }
883
884    struct Frontiers<T> {
885        since: T,
886        upper: T,
887    }
888
889    impl<T> Frontiers<T> {
890        fn new(since: T, upper: T) -> Self {
891            Self { since, upper }
892        }
893    }
894
895    struct ExplainContext<'a> {
896        env: &'a mut Environment,
897        config: &'a ExplainConfig,
898        frontiers: Frontiers<u64>,
899    }
900
901    /// A test IR that should be the subject of explanations.
902    struct TestExpr {
903        lhs: i32,
904        rhs: i32,
905    }
906
907    struct TestExplanation<'a> {
908        expr: &'a TestExpr,
909        context: &'a ExplainContext<'a>,
910    }
911
912    impl<'a> DisplayText for TestExplanation<'a> {
913        fn fmt_text(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
914            let lhs = &self.expr.lhs;
915            let rhs = &self.expr.rhs;
916            writeln!(f, "expr = {lhs} + {rhs}")?;
917
918            if self.context.config.timing {
919                let since = &self.context.frontiers.since;
920                let upper = &self.context.frontiers.upper;
921                writeln!(f, "at t ∊ [{since}, {upper})")?;
922            }
923
924            let name = &self.context.env.name;
925            writeln!(f, "env = {name}")?;
926
927            Ok(())
928        }
929    }
930
931    impl<'a> Explain<'a> for TestExpr {
932        type Context = ExplainContext<'a>;
933        type Text = TestExplanation<'a>;
934        type Json = UnsupportedFormat;
935        type Dot = UnsupportedFormat;
936
937        fn explain_text(
938            &'a mut self,
939            context: &'a Self::Context,
940        ) -> Result<Self::Text, ExplainError> {
941            Ok(TestExplanation {
942                expr: self,
943                context,
944            })
945        }
946    }
947
948    fn do_explain(
949        env: &mut Environment,
950        frontiers: Frontiers<u64>,
951    ) -> Result<String, ExplainError> {
952        let mut expr = TestExpr { lhs: 1, rhs: 2 };
953
954        let format = ExplainFormat::Text;
955        let config = &ExplainConfig {
956            redacted: false,
957            arity: false,
958            cardinality: false,
959            column_names: false,
960            filter_pushdown: false,
961            humanized_exprs: false,
962            join_impls: false,
963            keys: false,
964            linear_chains: false,
965            no_fast_path: false,
966            no_notices: false,
967            node_ids: false,
968            non_negative: false,
969            raw_plans: false,
970            raw_syntax: false,
971            verbose_syntax: true,
972            subtree_size: false,
973            equivalences: false,
974            timing: true,
975            types: false,
976            features: Default::default(),
977        };
978        let context = ExplainContext {
979            env,
980            config,
981            frontiers,
982        };
983
984        expr.explain(&format, &context)
985    }
986
987    #[mz_ore::test]
988    fn test_mutable_context() {
989        let mut env = Environment::default();
990        let frontiers = Frontiers::<u64>::new(3, 7);
991
992        let act = do_explain(&mut env, frontiers);
993        let exp = "expr = 1 + 2\nat t ∊ [3, 7)\nenv = test env\n".to_string();
994
995        assert_ok!(act);
996        assert_eq!(act.unwrap(), exp);
997    }
998}