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}