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