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