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