Skip to main content

mz_transform/
notice.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//! Notices that the optimizer wants to show to users.
11//!
12//! The top-level notice types are [`RawOptimizerNotice`] (for notices emitted
13//! by optimizer pipelines) and [`OptimizerNotice`] (for notices stored in the
14//! catalog memory). The `adapter` module contains code for converting the
15//! former to the latter.
16//!
17//! The [`RawOptimizerNotice`] type is an enum generated by the
18//! `raw_optimizer_notices` macro. Each notice type lives in its own submodule
19//! and implements the [`OptimizerNoticeApi`] trait.
20//!
21//! To add a new notice do the following:
22//!
23//! 1. Create a new submodule.
24//! 2. Define a struct for the new notice in that submodule.
25//! 3. Implement [`OptimizerNoticeApi`] for that struct.
26//! 4. Re-export the notice type in this module.
27//! 5. Add the notice type to the `raw_optimizer_notices` macro which generates
28//!    the [`RawOptimizerNotice`] enum and other boilerplate code.
29
30// Modules (one for each notice type).
31mod equals_null;
32mod index_already_exists;
33mod index_key_empty;
34mod index_too_wide_for_literal_constraints;
35
36pub use equals_null::EqualsNull;
37pub use index_already_exists::IndexAlreadyExists;
38pub use index_key_empty::IndexKeyEmpty;
39pub use index_too_wide_for_literal_constraints::IndexTooWideForLiteralConstraints;
40
41use std::collections::BTreeSet;
42use std::fmt::{self, Error, Formatter, Write};
43use std::sync::Arc;
44use std::{concat, stringify};
45
46use enum_kinds::EnumKind;
47use mz_repr::GlobalId;
48use mz_repr::explain::ExprHumanizer;
49#[cfg(any(test, feature = "proptest"))]
50use proptest_derive::Arbitrary;
51use serde::{Deserialize, Serialize};
52
53#[derive(
54    Clone,
55    Debug,
56    PartialEq,
57    Eq,
58    PartialOrd,
59    Ord,
60    Hash,
61    Serialize,
62    Deserialize
63)]
64#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
65/// An long lived in-memory representation of a [`RawOptimizerNotice`] that is
66/// meant to be kept as part of the hydrated catalog state.
67pub struct OptimizerNotice {
68    /// An `id` that uniquely identifies this notice in the `mz_notices` relation.
69    pub id: GlobalId,
70    /// The notice kind.
71    pub kind: OptimizerNoticeKind,
72    /// The ID of the catalog item associated with this notice.
73    ///
74    /// This is `None` if the notice is scoped to the entire catalog.
75    pub item_id: Option<GlobalId>,
76    /// A set of ids that need to exist for this notice to be considered valid.
77    /// Removing any of the IDs in this set will result in the notice being
78    /// asynchronously removed from the catalog state.
79    pub dependencies: BTreeSet<GlobalId>,
80    /// A brief description of what concretely went wrong.
81    ///
82    /// Details and context about situations in which this notice kind would be
83    /// emitted should be reserved for the documentation page for this notice
84    /// kind.
85    pub message: String,
86    /// A high-level hint that tells the user what can be improved.
87    pub hint: String,
88    /// A recommended action. This is a more concrete version of the hint.
89    pub action: Action,
90    /// A redacted version of the `message` field.
91    pub message_redacted: Option<String>,
92    /// A redacted version of the `hint` field.
93    pub hint_redacted: Option<String>,
94    /// A redacted version of the `action` field.
95    pub action_redacted: Option<Action>,
96    /// The date at which this notice was last created.
97    pub created_at: u64,
98}
99
100impl OptimizerNotice {
101    /// Turns a vector of notices into a vector of strings that can be used in
102    /// EXPLAIN.
103    ///
104    /// This method should be consistent with [`RawOptimizerNotice::explain`].
105    pub fn explain(
106        notices: &Vec<Arc<Self>>,
107        humanizer: &dyn ExprHumanizer,
108        redacted: bool,
109    ) -> Result<Vec<String>, Error> {
110        let mut notice_strings = Vec::new();
111        for notice in notices {
112            if notice.is_valid(humanizer) {
113                let mut s = String::new();
114                let message = match notice.message_redacted.as_deref() {
115                    Some(message_redacted) if redacted => message_redacted,
116                    _ => notice.message.as_str(),
117                };
118                let hint = match notice.hint_redacted.as_deref() {
119                    Some(hint_redacted) if redacted => hint_redacted,
120                    _ => notice.hint.as_str(),
121                };
122                write!(s, "  - Notice: {}\n", message)?;
123                write!(s, "    Hint: {}", hint)?;
124                notice_strings.push(s);
125            }
126        }
127        Ok(notice_strings)
128    }
129
130    /// Returns `true` iff both the dependencies and the associated item for
131    /// this notice still exist.
132    ///
133    /// This method should be consistent with [`RawOptimizerNotice::is_valid`].
134    fn is_valid(&self, humanizer: &dyn ExprHumanizer) -> bool {
135        // All dependencies exist.
136        self.dependencies.iter().all(|id| humanizer.id_exists(*id))
137    }
138}
139
140#[derive(
141    EnumKind,
142    Clone,
143    Debug,
144    PartialEq,
145    Eq,
146    PartialOrd,
147    Ord,
148    Hash,
149    Serialize,
150    Deserialize
151)]
152#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
153#[enum_kind(ActionKind)]
154/// An action attached to an [`OptimizerNotice`]
155pub enum Action {
156    /// No action.
157    None,
158    /// An action that cannot be defined as a program.
159    PlainText(String),
160    /// One or more SQL statements
161    ///
162    /// The statements should be formatted and fully-qualified names, meaning
163    /// that this field can be rendered in the console with a button that
164    /// executes this as a valid SQL statement.
165    SqlStatements(String),
166}
167
168impl Action {
169    /// Return the kind of this notice.
170    pub fn kind(&self) -> ActionKind {
171        ActionKind::from(self)
172    }
173}
174
175impl ActionKind {
176    /// Return a string representation for this action kind.
177    pub fn as_str(&self) -> &'static str {
178        match self {
179            Self::None => "",
180            Self::PlainText => "plain_text",
181            Self::SqlStatements => "sql_statements",
182        }
183    }
184}
185
186/// An API structs [`RawOptimizerNotice`] wrapped by structs
187pub trait OptimizerNoticeApi: Sized {
188    /// See [`OptimizerNoticeApi::dependencies`].
189    fn dependencies(&self) -> BTreeSet<GlobalId>;
190
191    /// Format the text for the optionally redacted [`OptimizerNotice::message`]
192    /// value for this notice.
193    fn fmt_message(
194        &self,
195        f: &mut Formatter<'_>,
196        humanizer: &dyn ExprHumanizer,
197        redacted: bool,
198    ) -> fmt::Result;
199
200    /// Format the text for the optionally redacted [`OptimizerNotice::hint`]
201    /// value for this notice.
202    fn fmt_hint(
203        &self,
204        f: &mut Formatter<'_>,
205        humanizer: &dyn ExprHumanizer,
206        redacted: bool,
207    ) -> fmt::Result;
208
209    /// Format the text for the optionally redacted [`OptimizerNotice::action`]
210    /// value for this notice.
211    fn fmt_action(
212        &self,
213        f: &mut Formatter<'_>,
214        humanizer: &dyn ExprHumanizer,
215        redacted: bool,
216    ) -> fmt::Result;
217
218    /// The kind of action suggested by this notice.
219    fn action_kind(&self, humanizer: &dyn ExprHumanizer) -> ActionKind;
220
221    /// Return a thunk that will render the optionally redacted
222    /// [`OptimizerNotice::message`] value for this notice.
223    fn message<'a>(
224        &'a self,
225        humanizer: &'a dyn ExprHumanizer,
226        redacted: bool,
227    ) -> HumanizedMessage<'a, Self> {
228        HumanizedMessage {
229            notice: self,
230            humanizer,
231            redacted,
232        }
233    }
234
235    /// Return a thunk that will render the optionally redacted
236    /// [`OptimizerNotice::hint`] value for
237    /// this notice.
238    fn hint<'a>(
239        &'a self,
240        humanizer: &'a dyn ExprHumanizer,
241        redacted: bool,
242    ) -> HumanizedHint<'a, Self> {
243        HumanizedHint {
244            notice: self,
245            humanizer,
246            redacted,
247        }
248    }
249
250    /// Return a thunk that will render the optionally redacted
251    /// [`OptimizerNotice::action`] value for this notice.
252    fn action<'a>(
253        &'a self,
254        humanizer: &'a dyn ExprHumanizer,
255        redacted: bool,
256    ) -> HumanizedAction<'a, Self> {
257        HumanizedAction {
258            notice: self,
259            humanizer,
260            redacted,
261        }
262    }
263}
264
265/// A wrapper for the [`OptimizerNoticeApi::fmt_message`] that implements
266/// [`fmt::Display`].
267#[allow(missing_debug_implementations)]
268pub struct HumanizedMessage<'a, T> {
269    notice: &'a T,
270    humanizer: &'a dyn ExprHumanizer,
271    redacted: bool,
272}
273impl<'a, T: OptimizerNoticeApi> fmt::Display for HumanizedMessage<'a, T> {
274    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
275        self.notice.fmt_message(f, self.humanizer, self.redacted)
276    }
277}
278
279/// A wrapper for the [`OptimizerNoticeApi::fmt_hint`] that implements [`fmt::Display`].
280#[allow(missing_debug_implementations)]
281pub struct HumanizedHint<'a, T> {
282    notice: &'a T,
283    humanizer: &'a dyn ExprHumanizer,
284    redacted: bool,
285}
286
287impl<'a, T: OptimizerNoticeApi> fmt::Display for HumanizedHint<'a, T> {
288    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
289        self.notice.fmt_hint(f, self.humanizer, self.redacted)
290    }
291}
292
293/// A wrapper for the [`OptimizerNoticeApi::fmt_action`] that implements
294/// [`fmt::Display`].
295#[allow(missing_debug_implementations)]
296pub struct HumanizedAction<'a, T> {
297    notice: &'a T,
298    humanizer: &'a dyn ExprHumanizer,
299    redacted: bool,
300}
301
302impl<'a, T: OptimizerNoticeApi> fmt::Display for HumanizedAction<'a, T> {
303    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
304        self.notice.fmt_action(f, self.humanizer, self.redacted)
305    }
306}
307
308macro_rules! raw_optimizer_notices {
309    ($($ty:ident => $name:literal,)+) => {
310        paste::paste!{
311            /// Notices that the optimizer wants to show to users.
312            #[derive(EnumKind, Clone, Debug, Eq, PartialEq, Hash)]
313            #[cfg_attr(
314                any(test, feature = "proptest"),
315                enum_kind(
316                    OptimizerNoticeKind,
317                    derive(PartialOrd, Ord, Hash, Serialize, Deserialize, Arbitrary)
318                )
319            )]
320            #[cfg_attr(
321                not(any(test, feature = "proptest")),
322                enum_kind(
323                    OptimizerNoticeKind,
324                    derive(PartialOrd, Ord, Hash, Serialize, Deserialize)
325                )
326            )]
327            pub enum RawOptimizerNotice {
328                $(
329                    #[doc = concat!("See [`", stringify!($ty), "`].")]
330                    $ty($ty),
331                )+
332            }
333
334            impl OptimizerNoticeApi for RawOptimizerNotice {
335                fn dependencies(&self) -> BTreeSet<GlobalId> {
336                    match self {
337                        $(Self::$ty(notice) => notice.dependencies(),)+
338                    }
339                }
340
341                fn fmt_message(
342                    &self,
343                    f: &mut Formatter<'_>,
344                    humanizer: &dyn ExprHumanizer,
345                    redacted: bool,
346                ) -> fmt::Result {
347                    match self {
348                        $(Self::$ty(notice) => notice.fmt_message(f, humanizer, redacted),)+
349                    }
350                }
351
352                fn fmt_hint(
353                    &self,
354                    f: &mut Formatter<'_>,
355                    humanizer: &dyn ExprHumanizer,
356                    redacted: bool,
357                ) -> fmt::Result {
358                    match self {
359                        $(Self::$ty(notice) => notice.fmt_hint(f, humanizer, redacted),)+
360                    }
361                }
362
363                fn fmt_action(
364                    &self,
365                    f: &mut Formatter<'_>,
366                    humanizer: &dyn ExprHumanizer,
367                    redacted: bool,
368                ) -> fmt::Result {
369                    match self {
370                        $(Self::$ty(notice) => notice.fmt_action(f, humanizer, redacted),)+
371                    }
372                }
373
374                fn action_kind(&self, humanizer: &dyn ExprHumanizer) -> ActionKind {
375                    match self {
376                        $(Self::$ty(notice) => notice.action_kind(humanizer),)+
377                    }
378                }
379            }
380
381            impl OptimizerNoticeKind {
382                /// Return a string representation for this optimizer notice
383                /// kind.
384                pub fn as_str(&self) -> &'static str {
385                    match self {
386                        $(Self::$ty => $name,)+
387                    }
388                }
389
390                /// A notice name, which will be applied as the label on the
391                /// metric that is counting notices labelled by notice kind.
392                pub fn metric_label(&self) -> &str {
393                    match self {
394                        $(Self::$ty => stringify!($ty),)+
395                    }
396                }
397            }
398
399            $(
400                impl From<$ty> for RawOptimizerNotice {
401                    fn from(value: $ty) -> Self {
402                        RawOptimizerNotice::$ty(value)
403                    }
404                }
405            )+
406        }
407    };
408}
409
410raw_optimizer_notices![
411    EqualsNull => "Comparison with NULL",
412    IndexAlreadyExists => "An identical index already exists",
413    IndexTooWideForLiteralConstraints => "Index too wide for literal constraints",
414    IndexKeyEmpty => "Empty index key",
415];
416
417impl RawOptimizerNotice {
418    /// Turns a vector of notices into a vector of strings that can be used in
419    /// EXPLAIN.
420    ///
421    /// This method should be consistent with [`OptimizerNotice::explain`].
422    pub fn explain(
423        notices: &Vec<RawOptimizerNotice>,
424        humanizer: &dyn ExprHumanizer,
425        redacted: bool,
426    ) -> Result<Vec<String>, Error> {
427        let mut notice_strings = Vec::new();
428        for notice in notices {
429            if notice.is_valid(humanizer) {
430                let mut s = String::new();
431                write!(s, "  - Notice: {}\n", notice.message(humanizer, redacted))?;
432                write!(s, "    Hint: {}", notice.hint(humanizer, redacted))?;
433                notice_strings.push(s);
434            }
435        }
436        Ok(notice_strings)
437    }
438
439    /// Returns `true` iff all dependencies for this notice still exist.
440    ///
441    /// This method should be consistent with [`OptimizerNotice::is_valid`].
442    fn is_valid(&self, humanizer: &dyn ExprHumanizer) -> bool {
443        self.dependencies()
444            .iter()
445            .all(|id| humanizer.id_exists(*id))
446    }
447
448    /// A notice name, which will be applied as the label on the metric that is
449    /// counting notices labelled by notice type.
450    pub fn metric_label(&self) -> &str {
451        OptimizerNoticeKind::from(self).as_str()
452    }
453}