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