Skip to main content

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