mz_sql/plan/explain/
text.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//! `EXPLAIN ... AS TEXT` support for HIR structures.
11//!
12//! The format adheres to the following conventions:
13//! 1. In general, every line corresponds to an [`HirRelationExpr`] node in the
14//!    plan.
15//! 2. Non-recursive parameters of each sub-plan are written as `$key=$val`
16//!    pairs on the same line.
17//! 3. A single non-recursive parameter can be written just as `$val`.
18//! 4. Exceptions in (1) can be made when virtual syntax is requested (done by
19//!    default, can be turned off with `WITH(raw_syntax)`).
20
21use itertools::Itertools;
22use std::fmt;
23
24use mz_expr::explain::{HumanizedExplain, HumanizerMode, fmt_text_constant_rows};
25use mz_expr::virtual_syntax::{AlgExcept, Except};
26use mz_expr::{Id, WindowFrame};
27use mz_ore::str::{IndentLike, separated};
28use mz_repr::Diff;
29use mz_repr::explain::text::DisplayText;
30use mz_repr::explain::{CompactScalarSeq, Indices, PlanRenderingContext};
31
32use crate::plan::{AggregateExpr, Hir, HirRelationExpr, HirScalarExpr, JoinKind, WindowExprType};
33
34impl DisplayText<PlanRenderingContext<'_, HirRelationExpr>> for HirRelationExpr {
35    fn fmt_text(
36        &self,
37        f: &mut fmt::Formatter<'_>,
38        ctx: &mut PlanRenderingContext<'_, HirRelationExpr>,
39    ) -> fmt::Result {
40        if ctx.config.raw_syntax {
41            self.fmt_raw_syntax(f, ctx)
42        } else {
43            self.fmt_virtual_syntax(f, ctx)
44        }
45    }
46}
47
48impl HirRelationExpr {
49    fn fmt_virtual_syntax(
50        &self,
51        f: &mut fmt::Formatter<'_>,
52        ctx: &mut PlanRenderingContext<'_, HirRelationExpr>,
53    ) -> fmt::Result {
54        if let Some(Except { all, lhs, rhs }) = Hir::un_except(self) {
55            if all {
56                writeln!(f, "{}ExceptAll", ctx.indent)?;
57            } else {
58                writeln!(f, "{}Except", ctx.indent)?;
59            }
60            ctx.indented(|ctx| {
61                lhs.fmt_text(f, ctx)?;
62                rhs.fmt_text(f, ctx)?;
63                Ok(())
64            })?;
65        } else {
66            // fallback to raw syntax formatting as a last resort
67            self.fmt_raw_syntax(f, ctx)?;
68        }
69
70        Ok(())
71    }
72
73    fn fmt_raw_syntax(
74        &self,
75        f: &mut fmt::Formatter<'_>,
76        ctx: &mut PlanRenderingContext<'_, HirRelationExpr>,
77    ) -> fmt::Result {
78        use HirRelationExpr::*;
79
80        let mode = HumanizedExplain::new(ctx.config.redacted);
81
82        match &self {
83            Constant { rows, .. } => {
84                if !rows.is_empty() {
85                    writeln!(f, "{}Constant", ctx.indent)?;
86                    ctx.indented(|ctx| {
87                        fmt_text_constant_rows(
88                            f,
89                            rows.iter().map(|row| (row, &Diff::ONE)),
90                            &mut ctx.indent,
91                            ctx.config.redacted,
92                        )
93                    })?;
94                } else {
95                    writeln!(f, "{}Constant <empty>", ctx.indent)?;
96                }
97            }
98            Let {
99                name,
100                id,
101                value,
102                body,
103            } => {
104                let mut bindings = vec![(id, name, value.as_ref())];
105                let mut head = body.as_ref();
106
107                // Render Let-blocks nested in the body an outer Let-block in one step
108                // with a flattened list of bindings
109                while let Let {
110                    name,
111                    id,
112                    value,
113                    body,
114                } = head
115                {
116                    bindings.push((id, name, value.as_ref()));
117                    head = body.as_ref();
118                }
119
120                writeln!(f, "{}With", ctx.indent)?;
121                ctx.indented(|ctx| {
122                    for (id, name, value) in bindings.iter() {
123                        // TODO: print the name and not the id
124                        writeln!(f, "{}cte [{} as {}] =", ctx.indent, *id, *name)?;
125                        ctx.indented(|ctx| value.fmt_text(f, ctx))?;
126                    }
127                    Ok(())
128                })?;
129                writeln!(f, "{}Return", ctx.indent)?;
130                ctx.indented(|ctx| head.fmt_text(f, ctx))?;
131            }
132            LetRec {
133                limit,
134                bindings,
135                body,
136            } => {
137                write!(f, "{}With Mutually Recursive", ctx.indent)?;
138                if let Some(limit) = limit {
139                    write!(f, " {}", limit)?;
140                }
141                writeln!(f)?;
142                ctx.indented(|ctx| {
143                    for (name, id, value, _type) in bindings.iter() {
144                        // TODO: print the name and not the id
145                        writeln!(f, "{}cte [{} as {}] =", ctx.indent, *id, *name)?;
146                        ctx.indented(|ctx| value.fmt_text(f, ctx))?;
147                    }
148                    Ok(())
149                })?;
150                writeln!(f, "{}Return", ctx.indent)?;
151                ctx.indented(|ctx| body.fmt_text(f, ctx))?;
152            }
153            Get { id, .. } => match id {
154                Id::Local(id) => {
155                    // TODO: resolve local id to the human-readable name from the context
156                    writeln!(f, "{}Get {}", ctx.indent, id)?;
157                }
158                Id::Global(id) => {
159                    let humanized_id = ctx
160                        .humanizer
161                        .humanize_id(*id)
162                        .unwrap_or_else(|| id.to_string());
163                    writeln!(f, "{}Get {}", ctx.indent, humanized_id)?;
164                }
165            },
166            Project { outputs, input } => {
167                let outputs = Indices(outputs);
168                writeln!(f, "{}Project ({})", ctx.indent, outputs)?;
169                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
170            }
171            Map { scalars, input } => {
172                let scalars = CompactScalarSeq(scalars);
173                writeln!(f, "{}Map ({})", ctx.indent, scalars)?;
174                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
175            }
176            CallTable { func, exprs } => {
177                let exprs = CompactScalarSeq(exprs);
178                writeln!(f, "{}CallTable {}({})", ctx.indent, func, exprs)?;
179            }
180            Filter { predicates, input } => {
181                let predicates = separated(" AND ", predicates);
182                writeln!(f, "{}Filter {}", ctx.indent, predicates)?;
183                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
184            }
185            Join {
186                left,
187                right,
188                on,
189                kind,
190            } => {
191                if on.is_literal_true() && kind == &JoinKind::Inner {
192                    write!(f, "{}CrossJoin", ctx.indent)?;
193                } else {
194                    write!(f, "{}{}Join {}", ctx.indent, kind, on)?;
195                }
196                writeln!(f)?;
197                ctx.indented(|ctx| {
198                    left.fmt_text(f, ctx)?;
199                    right.fmt_text(f, ctx)?;
200                    Ok(())
201                })?;
202            }
203            Reduce {
204                group_key,
205                aggregates,
206                expected_group_size,
207                input,
208            } => {
209                write!(f, "{}Reduce", ctx.indent)?;
210                if group_key.len() > 0 {
211                    let group_key = Indices(group_key);
212                    write!(f, " group_by=[{}]", group_key)?;
213                }
214                if aggregates.len() > 0 {
215                    let aggregates = separated(", ", aggregates);
216                    write!(f, " aggregates=[{}]", aggregates)?;
217                }
218                if let Some(expected_group_size) = expected_group_size {
219                    write!(f, " exp_group_size={}", expected_group_size)?;
220                }
221                writeln!(f)?;
222                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
223            }
224            Distinct { input } => {
225                writeln!(f, "{}Distinct", ctx.indent)?;
226                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
227            }
228            TopK {
229                group_key,
230                order_key,
231                limit,
232                offset,
233                input,
234                expected_group_size,
235            } => {
236                write!(f, "{}TopK", ctx.indent)?;
237                if group_key.len() > 0 {
238                    let group_by = Indices(group_key);
239                    write!(f, " group_by=[{}]", group_by)?;
240                }
241                if order_key.len() > 0 {
242                    let order_by = mode.seq(order_key, None);
243                    let order_by = separated(", ", order_by);
244                    write!(f, " order_by=[{}]", order_by)?;
245                }
246                if let Some(limit) = limit {
247                    write!(f, " limit={}", limit)?;
248                }
249                if offset > &0 {
250                    write!(f, " offset={}", offset)?
251                }
252                if let Some(expected_group_size) = expected_group_size {
253                    write!(f, " exp_group_size={}", expected_group_size)?;
254                }
255                writeln!(f)?;
256                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
257            }
258            Negate { input } => {
259                writeln!(f, "{}Negate", ctx.indent)?;
260                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
261            }
262            Threshold { input } => {
263                writeln!(f, "{}Threshold", ctx.indent)?;
264                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
265            }
266            Union { base, inputs } => {
267                writeln!(f, "{}Union", ctx.indent)?;
268                ctx.indented(|ctx| {
269                    base.fmt_text(f, ctx)?;
270                    for input in inputs.iter() {
271                        input.fmt_text(f, ctx)?;
272                    }
273                    Ok(())
274                })?;
275            }
276        }
277
278        Ok(())
279    }
280}
281
282impl fmt::Display for HirScalarExpr {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        use HirRelationExpr::Get;
285        use HirScalarExpr::*;
286        match self {
287            Column(i) => write!(
288                f,
289                "#{}{}",
290                (0..i.level).map(|_| '^').collect::<String>(),
291                i.column
292            ),
293            Parameter(i) => write!(f, "${}", i),
294            Literal(row, _) => write!(f, "{}", row.unpack_first()),
295            CallUnmaterializable(func) => write!(f, "{}()", func),
296            CallUnary { func, expr } => {
297                if let mz_expr::UnaryFunc::Not(_) = *func {
298                    if let CallUnary { func, expr } = expr.as_ref() {
299                        if let Some(is) = func.is() {
300                            return write!(f, "({}) IS NOT {}", expr, is);
301                        }
302                    }
303                }
304                if let Some(is) = func.is() {
305                    write!(f, "({}) IS {}", expr, is)
306                } else {
307                    write!(f, "{}({})", func, expr)
308                }
309            }
310            CallBinary { func, expr1, expr2 } => {
311                if func.is_infix_op() {
312                    write!(f, "({} {} {})", expr1, func, expr2)
313                } else {
314                    write!(f, "{}({}, {})", func, expr1, expr2)
315                }
316            }
317            CallVariadic { func, exprs } => {
318                use mz_expr::VariadicFunc::*;
319                match func {
320                    ArrayCreate { .. } => {
321                        let exprs = separated(", ", exprs);
322                        write!(f, "array[{}]", exprs)
323                    }
324                    ListCreate { .. } => {
325                        let exprs = separated(", ", exprs);
326                        write!(f, "list[{}]", exprs)
327                    }
328                    RecordCreate { .. } => {
329                        let exprs = separated(", ", exprs);
330                        write!(f, "row({})", exprs)
331                    }
332                    func if func.is_infix_op() && exprs.len() > 1 => {
333                        let func = format!(" {} ", func);
334                        let exprs = separated(&func, exprs);
335                        write!(f, "({})", exprs)
336                    }
337                    func => {
338                        let exprs = separated(", ", exprs);
339                        write!(f, "{}({})", func, exprs)
340                    }
341                }
342            }
343            If { cond, then, els } => {
344                write!(f, "case when {} then {} else {} end", cond, then, els)
345            }
346            Windowing(expr) => {
347                // First, print
348                // - the window function name
349                // - the arguments.
350                // Also, dig out some info from the `func`.
351                let (column_orders, ignore_nulls, window_frame) = match &expr.func {
352                    WindowExprType::Scalar(scalar_window_expr) => {
353                        write!(f, "{}()", scalar_window_expr.func)?;
354                        (&scalar_window_expr.order_by, false, None)
355                    }
356                    WindowExprType::Value(value_window_expr) => {
357                        write!(f, "{}({})", value_window_expr.func, value_window_expr.args)?;
358                        (
359                            &value_window_expr.order_by,
360                            value_window_expr.ignore_nulls,
361                            Some(&value_window_expr.window_frame),
362                        )
363                    }
364                    WindowExprType::Aggregate(aggregate_window_expr) => {
365                        write!(f, "{}", aggregate_window_expr.aggregate_expr)?;
366                        (
367                            &aggregate_window_expr.order_by,
368                            false,
369                            Some(&aggregate_window_expr.window_frame),
370                        )
371                    }
372                };
373
374                // Reconstruct the ORDER BY (see comment on `WindowExpr.order_by`).
375                // We assume that the `column_order.column`s refer to each of the expressions in
376                // `expr.order_by` in order. This is a consequence of how `plan_function_order_by`
377                // works.
378                assert!(
379                    column_orders
380                        .iter()
381                        .enumerate()
382                        .all(|(i, column_order)| i == column_order.column)
383                );
384                let order_by = column_orders
385                    .iter()
386                    .zip_eq(expr.order_by.iter())
387                    .map(|(column_order, expr)| {
388                        ColumnOrderWithExpr {
389                            expr: expr.clone(),
390                            desc: column_order.desc,
391                            nulls_last: column_order.nulls_last,
392                        }
393                        // (We can ignore column_order.column because of the above assert.)
394                    })
395                    .collect_vec();
396
397                // Print IGNORE NULLS if present.
398                if ignore_nulls {
399                    write!(f, " ignore nulls")?;
400                }
401
402                // Print the OVER clause.
403                // This is close to the SQL syntax, but we are adding some [] to make it easier to
404                // read.
405                write!(f, " over (")?;
406                if !expr.partition_by.is_empty() {
407                    write!(
408                        f,
409                        "partition by [{}] ",
410                        separated(", ", expr.partition_by.iter())
411                    )?;
412                }
413                write!(f, "order by [{}]", separated(", ", order_by.iter()))?;
414                if let Some(window_frame) = window_frame {
415                    if *window_frame != WindowFrame::default() {
416                        write!(f, " {}", window_frame)?;
417                    }
418                }
419                write!(f, ")")?;
420
421                Ok(())
422            }
423            Exists(expr) => match expr.as_ref() {
424                Get { id, .. } => write!(f, "exists(Get {})", id), // TODO: optional humanizer
425                _ => write!(f, "exists(???)"),
426            },
427            Select(expr) => match expr.as_ref() {
428                Get { id, .. } => write!(f, "select(Get {})", id), // TODO: optional humanizer
429                _ => write!(f, "select(???)"),
430            },
431        }
432    }
433}
434
435/// This is like ColumnOrder, but contains a HirScalarExpr instead of just a column reference by
436/// index. (This is only used in EXPLAIN, when reconstructing an ORDER BY inside an OVER clause.)
437struct ColumnOrderWithExpr {
438    /// The scalar expression.
439    pub expr: HirScalarExpr,
440    /// Whether to sort in descending order.
441    pub desc: bool,
442    /// Whether to sort nulls last.
443    pub nulls_last: bool,
444}
445
446impl fmt::Display for ColumnOrderWithExpr {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        // If you modify this, then please also attend to Display for ColumnOrder!
449        write!(
450            f,
451            "{} {} {}",
452            self.expr,
453            if self.desc { "desc" } else { "asc" },
454            if self.nulls_last {
455                "nulls_last"
456            } else {
457                "nulls_first"
458            },
459        )
460    }
461}
462
463impl fmt::Display for AggregateExpr {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        if self.is_count_asterisk() {
466            return write!(f, "count(*)");
467        }
468
469        // TODO(cloud#8196)
470        let mode = HumanizedExplain::new(false);
471        let func = self.func.clone().into_expr();
472        let func = mode.expr(&func, None);
473        let distinct = if self.distinct { "distinct " } else { "" };
474
475        write!(f, "{}({}", func, distinct)?;
476        self.expr.fmt(f)?;
477        write!(f, ")")
478    }
479}