Skip to main content

mz_expr/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 structures defined in this crate.
11
12use std::collections::BTreeMap;
13use std::fmt::{self, Display as _};
14use std::sync::Arc;
15
16use itertools::Itertools;
17use mz_ore::soft_assert_eq_or_log;
18use mz_ore::str::{Indent, IndentLike, StrExt, closure_to_display, separated};
19use mz_ore::treat_as_equal::TreatAsEqual;
20use mz_repr::explain::text::DisplayText;
21use mz_repr::explain::{
22    CompactScalars, ExprHumanizer, HumanizedAnalyses, IndexUsageType, Indices,
23    PlanRenderingContext, RenderingContext, ScalarOps,
24};
25use mz_repr::{Datum, Diff, GlobalId, Row, StableRow, UNKNOWN_COLUMN_NAME};
26use mz_sql_parser::ast::Ident;
27
28use crate::explain::{ExplainMultiPlan, ExplainSinglePlan, SourceOperators};
29use crate::{
30    AccessStrategy, AggregateExpr, EvalError, Id, JoinImplementation, JoinInputCharacteristics,
31    LocalId, MapFilterProject, MirRelationExpr, MirScalarExpr, OptimizableExpr, RowSetFinishing,
32};
33
34impl<'a, T: 'a> DisplayText for ExplainSinglePlan<'a, T>
35where
36    T: DisplayText<PlanRenderingContext<'a, T>> + Ord,
37{
38    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
39        let mut ctx = PlanRenderingContext::new(
40            Indent::default(),
41            self.context.humanizer,
42            self.plan.annotations.clone(),
43            self.context.config,
44            self.context
45                .used_indexes
46                .ambiguous_ids(self.context.humanizer),
47        );
48
49        let mode = HumanizedExplain::new(self.context.config.redacted);
50
51        if let Some(finishing) = &self.context.finishing {
52            if ctx.config.humanized_exprs {
53                let analyses = ctx.annotations.get(&self.plan.plan);
54                let cols = analyses
55                    .map(|analyses| analyses.column_names.clone())
56                    .flatten();
57                mode.expr(finishing, cols.as_ref()).fmt_text(f, &mut ctx)?;
58            } else {
59                mode.expr(finishing, None).fmt_text(f, &mut ctx)?;
60            }
61            ctx.indented(|ctx| self.plan.plan.fmt_text(f, ctx))?;
62        } else {
63            self.plan.plan.fmt_text(f, &mut ctx)?;
64        }
65
66        if !self.context.used_indexes.is_empty() {
67            writeln!(f)?;
68            self.context.used_indexes.fmt_text(f, &mut ctx)?;
69        }
70
71        if let Some(target_cluster) = self.context.target_cluster {
72            writeln!(f)?;
73            writeln!(f, "Target cluster: {}", target_cluster)?;
74        }
75
76        if !self.context.optimizer_notices.is_empty() {
77            writeln!(f)?;
78            writeln!(f, "Notices:")?;
79            for notice in self.context.optimizer_notices.iter() {
80                writeln!(f, "{}", notice)?;
81            }
82        }
83
84        if self.context.config.timing {
85            writeln!(f)?;
86            writeln!(f, "Optimization time: {:?}", self.context.duration)?;
87        }
88
89        Ok(())
90    }
91}
92
93impl<'a, T: 'a, O> DisplayText for ExplainMultiPlan<'a, T, O>
94where
95    T: DisplayText<PlanRenderingContext<'a, T>> + Ord,
96    O: SourceOperators,
97{
98    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
99        let mut ctx = RenderingContext::new(Indent::default(), self.context.humanizer);
100
101        let mode = HumanizedExplain::new(self.context.config.redacted);
102
103        // Render plans.
104        for (no, (id, plan)) in self.plans.iter().enumerate() {
105            let mut ctx = PlanRenderingContext::new(
106                ctx.indent.clone(),
107                ctx.humanizer,
108                plan.annotations.clone(),
109                self.context.config,
110                self.context
111                    .used_indexes
112                    .ambiguous_ids(self.context.humanizer),
113            );
114
115            if no > 0 {
116                writeln!(f)?;
117            }
118
119            writeln!(f, "{}{}:", ctx.indent, id)?;
120            ctx.indented(|ctx| {
121                match &self.context.finishing {
122                    // If present, a RowSetFinishing always applies to the first rendered plan.
123                    Some(finishing) if no == 0 => {
124                        if ctx.config.humanized_exprs {
125                            let analyses = ctx.annotations.get(plan.plan);
126                            let cols = analyses
127                                .map(|analyses| analyses.column_names.clone())
128                                .flatten();
129                            mode.expr(finishing, cols.as_ref()).fmt_text(f, ctx)?;
130                        } else {
131                            mode.expr(finishing, None).fmt_text(f, ctx)?;
132                        };
133                        ctx.indented(|ctx| plan.plan.fmt_text(f, ctx))?;
134                    }
135                    // All other plans are rendered without a RowSetFinishing.
136                    _ => {
137                        plan.plan.fmt_text(f, ctx)?;
138                    }
139                }
140                Ok(())
141            })?;
142        }
143
144        if self.sources.iter().any(|src| !src.is_identity()) {
145            // Render one blank line between the plans and sources, but only if
146            // there were plans rendered above.
147            if !self.plans.is_empty() {
148                writeln!(f)?;
149            }
150            for src in self.sources.iter().filter(|src| !src.is_identity()) {
151                if self.context.config.humanized_exprs {
152                    let mut cols = ctx.humanizer.column_names_for_id(src.id);
153                    // The column names of the source needs to be extended with
154                    // anonymous columns for each source expression before we can
155                    // pass it to the ExplainSource rendering code.
156                    if let Some(cols) = cols.as_mut() {
157                        let anonymous = std::iter::repeat(String::new());
158                        cols.extend(
159                            anonymous.take(src.op.as_ref().map_or(0, |op| op.appended_columns())),
160                        )
161                    };
162                    // Render source with humanized expressions.
163                    mode.expr(src, cols.as_ref()).fmt_text(f, &mut ctx)?;
164                } else {
165                    // Render source without humanized expressions.
166                    mode.expr(src, None).fmt_text(f, &mut ctx)?;
167                }
168            }
169        }
170
171        if !self.context.used_indexes.is_empty() {
172            writeln!(f)?;
173            self.context.used_indexes.fmt_text(f, &mut ctx)?;
174        }
175
176        if let Some(target_cluster) = self.context.target_cluster {
177            writeln!(f)?;
178            writeln!(f, "Target cluster: {}", target_cluster)?;
179        }
180
181        if !(self.context.config.no_notices || self.context.optimizer_notices.is_empty()) {
182            writeln!(f)?;
183            writeln!(f, "Notices:")?;
184            for notice in self.context.optimizer_notices.iter() {
185                writeln!(f, "{}", notice)?;
186            }
187        }
188
189        if self.context.config.timing {
190            writeln!(f)?;
191            writeln!(f, "Optimization time: {:?}", self.context.duration)?;
192        }
193
194        Ok(())
195    }
196}
197
198impl<'a, C, M> DisplayText<C> for HumanizedExpr<'a, RowSetFinishing, M>
199where
200    C: AsMut<Indent>,
201    M: HumanizerMode,
202{
203    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut C) -> fmt::Result {
204        write!(f, "{}Finish", ctx.as_mut())?;
205        // order by
206        if !self.expr.order_by.is_empty() {
207            let order_by = self.expr.order_by.iter().map(|e| self.child(e));
208            write!(f, " order_by=[{}]", separated(", ", order_by))?;
209        }
210        // limit
211        if let Some(limit) = self.expr.limit {
212            write!(f, " limit={}", limit)?;
213        }
214        // offset
215        if self.expr.offset > 0 {
216            write!(f, " offset={}", self.expr.offset)?;
217        }
218        // project
219        {
220            let project = Indices(&self.expr.project);
221            write!(f, " output=[{}]", project)?;
222        }
223        writeln!(f)
224    }
225}
226
227impl<'a, C, M, E> DisplayText<C> for HumanizedExpr<'a, MapFilterProject<E>, M>
228where
229    C: AsMut<Indent>,
230    M: HumanizerMode,
231    E: OptimizableExpr + HumanizeDisplay,
232{
233    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut C) -> fmt::Result {
234        let (scalars, predicates, outputs, input_arity) = (
235            &self.expr.expressions,
236            &self.expr.predicates,
237            &self.expr.projection,
238            &self.expr.input_arity,
239        );
240
241        // render `project` field iff not the identity projection
242        if &outputs.len() != input_arity || outputs.iter().enumerate().any(|(i, p)| i != *p) {
243            let outputs = Indices(outputs);
244            writeln!(f, "{}project=({})", ctx.as_mut(), outputs)?;
245        }
246        // render `filter` field iff predicates are present
247        if !predicates.is_empty() {
248            let predicates = predicates.iter().map(|(_, p)| self.child(p));
249            let predicates = separated(" AND ", predicates);
250            writeln!(f, "{}filter=({})", ctx.as_mut(), predicates)?;
251        }
252        // render `map` field iff scalars are present
253        if !scalars.is_empty() {
254            let scalars = scalars.iter().map(|s| self.child(s));
255            let scalars = separated(", ", scalars);
256            writeln!(f, "{}map=({})", ctx.as_mut(), scalars)?;
257        }
258
259        Ok(())
260    }
261}
262
263impl<'a, M: HumanizerMode, E: OptimizableExpr + HumanizeDisplay>
264    HumanizedExpr<'a, MapFilterProject<E>, M>
265{
266    /// Render an MFP using the default (concise) syntax.
267    pub fn fmt_default_text<T>(
268        &self,
269        f: &mut fmt::Formatter<'_>,
270        ctx: &mut PlanRenderingContext<'_, T>,
271    ) -> fmt::Result {
272        if self.expr.projection.len() != self.expr.input_arity
273            || self
274                .expr
275                .projection
276                .iter()
277                .enumerate()
278                .any(|(i, p)| i != *p)
279        {
280            if self.expr.projection.len() == 0 {
281                writeln!(f, "{}Project: ()", ctx.indent)?;
282            } else {
283                let outputs = Indices(&self.expr.projection);
284                writeln!(f, "{}Project: {outputs}", ctx.indent)?;
285            }
286        }
287
288        // render `filter` field iff predicates are present
289        if !self.expr.predicates.is_empty() {
290            let predicates = self.expr.predicates.iter().map(|(_, p)| self.child(p));
291            let predicates = separated(" AND ", predicates);
292            writeln!(f, "{}Filter: {predicates}", ctx.indent)?;
293        }
294        // render `map` field iff scalars are present
295        if !self.expr.expressions.is_empty() {
296            let scalars = self.expr.expressions.iter().map(|s| self.child(s));
297            let scalars = separated(", ", scalars);
298            writeln!(f, "{}Map: {scalars}", ctx.indent)?;
299        }
300
301        Ok(())
302    }
303}
304
305/// `EXPLAIN ... AS TEXT` support for [`MirRelationExpr`].
306///
307/// The format adheres to the following conventions:
308/// 1. In general, every line corresponds to an [`MirRelationExpr`] node in the
309///    plan.
310/// 2. Non-recursive parameters of each sub-plan are written as `$key=$val`
311///    pairs on the same line.
312/// 3. A single non-recursive parameter can be written just as `$val`.
313/// 4. Exceptions in (1) can be made when virtual syntax is requested (done by
314///    default, can be turned off with `WITH(raw_syntax)`).
315/// 5. Exceptions in (2) can be made when join implementations are rendered
316///    explicitly `WITH(join_impls)`.
317impl DisplayText<PlanRenderingContext<'_, MirRelationExpr>> for MirRelationExpr {
318    fn fmt_text(
319        &self,
320        f: &mut fmt::Formatter<'_>,
321        ctx: &mut PlanRenderingContext<'_, MirRelationExpr>,
322    ) -> fmt::Result {
323        if ctx.config.verbose_syntax {
324            self.fmt_verbose_syntax(f, ctx)
325        } else {
326            self.fmt_default_syntax(f, ctx)
327        }
328    }
329}
330
331impl MirRelationExpr {
332    fn fmt_default_syntax(
333        &self,
334        f: &mut fmt::Formatter<'_>,
335        ctx: &mut PlanRenderingContext<'_, MirRelationExpr>,
336    ) -> fmt::Result {
337        // TODO(mgree) MIR does not support a different default syntax (yet!)
338        self.fmt_verbose_syntax(f, ctx)
339    }
340
341    fn fmt_verbose_syntax(
342        &self,
343        f: &mut fmt::Formatter<'_>,
344        ctx: &mut PlanRenderingContext<'_, MirRelationExpr>,
345    ) -> fmt::Result {
346        use MirRelationExpr::*;
347
348        let mode = HumanizedExplain::new(ctx.config.redacted);
349
350        match &self {
351            Constant { rows, typ: _ } => match rows {
352                Ok(rows) => {
353                    if !rows.is_empty() {
354                        write!(f, "{}Constant", ctx.indent)?;
355                        self.fmt_analyses(f, ctx)?;
356                        ctx.indented(|ctx| {
357                            fmt_text_constant_rows(
358                                f,
359                                rows.iter().map(|(x, y)| (x, y)),
360                                &mut ctx.indent,
361                                mode.redacted(),
362                            )
363                        })?;
364                    } else {
365                        write!(f, "{}Constant <empty>", ctx.indent)?;
366                        self.fmt_analyses(f, ctx)?;
367                    }
368                }
369                Err(err) => {
370                    if mode.redacted() {
371                        writeln!(f, "{}Error █", ctx.indent)?;
372                    } else {
373                        writeln!(f, "{}Error {}", ctx.indent, err.to_string().escaped())?;
374                    }
375                }
376            },
377            Let { id, value, body } => {
378                let mut bindings = vec![(id, value.as_ref())];
379                let mut head = body.as_ref();
380
381                // Render Let-blocks nested in the body an outer Let-block in one step
382                // with a flattened list of bindings
383                while let Let { id, value, body } = head {
384                    bindings.push((id, value.as_ref()));
385                    head = body.as_ref();
386                }
387
388                if ctx.config.linear_chains {
389                    writeln!(f, "{}With", ctx.indent)?;
390                    ctx.indented(|ctx| {
391                        for (id, value) in bindings.iter() {
392                            writeln!(f, "{}cte {} =", ctx.indent, *id)?;
393                            ctx.indented(|ctx| value.fmt_text(f, ctx))?;
394                        }
395                        Ok(())
396                    })?;
397                    write!(f, "{}Return", ctx.indent)?;
398                    self.fmt_analyses(f, ctx)?;
399                    ctx.indented(|ctx| head.fmt_text(f, ctx))?;
400                } else {
401                    writeln!(f, "{}With", ctx.indent)?;
402                    ctx.indented(|ctx| {
403                        for (id, value) in bindings.iter() {
404                            writeln!(f, "{}cte {} =", ctx.indent, *id)?;
405                            ctx.indented(|ctx| value.fmt_text(f, ctx))?;
406                        }
407                        Ok(())
408                    })?;
409                    write!(f, "{}Return", ctx.indent)?;
410                    self.fmt_analyses(f, ctx)?;
411                    ctx.indented(|ctx| head.fmt_text(f, ctx))?;
412                }
413            }
414            LetRec {
415                ids,
416                values,
417                limits,
418                body,
419            } => {
420                assert_eq!(ids.len(), values.len());
421                assert_eq!(ids.len(), limits.len());
422                let head = body.as_ref();
423
424                // Determine whether all `limits` are the same.
425                // If all of them are the same, then we print it on top of the block (or not print
426                // it at all if it's None). If there are differences, then we print them on the
427                // ctes.
428                let all_limits_same = limits
429                    .iter()
430                    .reduce(|first, i| if i == first { first } else { &None })
431                    .unwrap_or(&None);
432
433                if ctx.config.linear_chains {
434                    unreachable!(); // We exclude this case in `as_explain_single_plan`.
435                } else {
436                    write!(f, "{}With Mutually Recursive", ctx.indent)?;
437                    if let Some(limit) = all_limits_same {
438                        write!(f, " {}", limit)?;
439                    }
440                    writeln!(f)?;
441                    ctx.indented(|ctx| {
442                        let bindings = ids.iter().zip_eq(values).zip_eq(limits);
443                        for ((id, value), limit) in bindings {
444                            write!(f, "{}cte", ctx.indent)?;
445                            if all_limits_same.is_none() {
446                                if let Some(limit) = limit {
447                                    write!(f, " {}", limit)?;
448                                }
449                            }
450                            writeln!(f, " {} =", id)?;
451                            ctx.indented(|ctx| value.fmt_text(f, ctx))?;
452                        }
453                        Ok(())
454                    })?;
455                    write!(f, "{}Return", ctx.indent)?;
456                    self.fmt_analyses(f, ctx)?;
457                    ctx.indented(|ctx| head.fmt_text(f, ctx))?;
458                }
459            }
460            Get {
461                id,
462                access_strategy: persist_or_index,
463                ..
464            } => {
465                match id {
466                    Id::Local(id) => {
467                        assert!(matches!(persist_or_index, AccessStrategy::UnknownOrLocal));
468                        write!(f, "{}Get {}", ctx.indent, id)?;
469                    }
470                    Id::Global(id) => {
471                        let humanize = |id: &GlobalId| {
472                            ctx.humanizer
473                                .humanize_id(*id)
474                                .unwrap_or_else(|| id.to_string())
475                        };
476                        let humanize_unqualified = |id: &GlobalId| {
477                            ctx.humanize_id_maybe_unqualified(*id)
478                                .unwrap_or_else(|| id.to_string())
479                        };
480                        let humanize_unqualified_maybe_deleted = |id: &GlobalId| {
481                            ctx.humanize_id_maybe_unqualified(*id)
482                                .unwrap_or_else(|| "[DELETED INDEX]".to_owned())
483                        };
484                        match persist_or_index {
485                            AccessStrategy::UnknownOrLocal => {
486                                write!(f, "{}Get {}", ctx.indent, humanize(id))?;
487                            }
488                            AccessStrategy::Persist => {
489                                write!(f, "{}ReadStorage {}", ctx.indent, humanize(id))?;
490                            }
491                            AccessStrategy::SameDataflow => {
492                                write!(
493                                    f,
494                                    "{}ReadGlobalFromSameDataflow {}",
495                                    ctx.indent,
496                                    humanize(id)
497                                )?;
498                            }
499                            AccessStrategy::Index(index_accesses) => {
500                                let mut grouped_index_accesses = BTreeMap::new();
501                                for (idx_id, usage_type) in index_accesses {
502                                    grouped_index_accesses
503                                        .entry(idx_id)
504                                        .or_insert(Vec::new())
505                                        .push(usage_type.clone());
506                                }
507                                write!(
508                                    f,
509                                    "{}ReadIndex on={} {}",
510                                    ctx.indent,
511                                    humanize_unqualified(id),
512                                    separated(
513                                        " ",
514                                        grouped_index_accesses.iter().map(
515                                            |(idx_id, usage_types)| {
516                                                closure_to_display(move |f| {
517                                                    write!(
518                                                        f,
519                                                        "{}=[{}]",
520                                                        humanize_unqualified_maybe_deleted(idx_id),
521                                                        IndexUsageType::display_vec(usage_types)
522                                                    )
523                                                })
524                                            }
525                                        )
526                                    ),
527                                )?;
528                            }
529                        }
530                    }
531                }
532                self.fmt_analyses(f, ctx)?;
533            }
534            Project { outputs, input } => {
535                FmtNode {
536                    fmt_root: |f, ctx| {
537                        let outputs = mode.seq(outputs, input.column_names(ctx));
538                        let outputs = CompactScalars(outputs);
539                        write!(f, "{}Project ({})", ctx.indent, outputs)?;
540                        self.fmt_analyses(f, ctx)
541                    },
542                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
543                }
544                .render(f, ctx)?;
545            }
546            Map { scalars, input } => {
547                FmtNode {
548                    fmt_root: |f, ctx: &mut PlanRenderingContext<'_, MirRelationExpr>| {
549                        // Here, it's better to refer to `self.column_names(ctx)` rather than
550                        // `input.column_names(ctx)`, because then we also get humanization for refs
551                        // to cols introduced earlier by the same `Map`.
552                        let scalars = mode.seq(scalars, self.column_names(ctx));
553                        let scalars = CompactScalars(scalars);
554                        write!(f, "{}Map ({})", ctx.indent, scalars)?;
555                        self.fmt_analyses(f, ctx)
556                    },
557                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
558                }
559                .render(f, ctx)?;
560            }
561            FlatMap { input, func, exprs } => {
562                FmtNode {
563                    fmt_root: |f, ctx: &mut PlanRenderingContext<'_, MirRelationExpr>| {
564                        let exprs = mode.seq(exprs, input.column_names(ctx));
565                        let exprs = CompactScalars(exprs);
566                        write!(f, "{}FlatMap {}({})", ctx.indent, func, exprs)?;
567                        self.fmt_analyses(f, ctx)
568                    },
569                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
570                }
571                .render(f, ctx)?;
572            }
573            Filter { predicates, input } => {
574                FmtNode {
575                    fmt_root: |f, ctx: &mut PlanRenderingContext<'_, MirRelationExpr>| {
576                        if predicates.is_empty() {
577                            write!(f, "{}Filter", ctx.indent)?;
578                        } else {
579                            let cols = input.column_names(ctx);
580                            let predicates = mode.seq(predicates, cols);
581                            let predicates = separated(" AND ", predicates);
582                            write!(f, "{}Filter {}", ctx.indent, predicates)?;
583                        }
584                        self.fmt_analyses(f, ctx)
585                    },
586                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
587                }
588                .render(f, ctx)?;
589            }
590            Join {
591                inputs,
592                equivalences,
593                implementation:
594                    implementation @ (JoinImplementation::Differential(..)
595                    | JoinImplementation::DeltaQuery(..)
596                    | JoinImplementation::Unimplemented),
597            } => {
598                let has_equivalences = !equivalences.is_empty();
599
600                if has_equivalences {
601                    let cols = self.column_names(ctx);
602                    let equivalences = separated(
603                        " AND ",
604                        equivalences.iter().map(|equivalence| {
605                            let equivalence = mode.seq(equivalence, cols);
606                            separated(" = ", equivalence)
607                        }),
608                    );
609                    write!(f, "{}Join on=({})", ctx.indent, equivalences)?;
610                } else {
611                    write!(f, "{}CrossJoin", ctx.indent)?;
612                }
613                if let Some(name) = implementation.name() {
614                    write!(f, " type={}", name)?;
615                }
616
617                self.fmt_analyses(f, ctx)?;
618
619                if ctx.config.join_impls {
620                    let input_name = &|pos: usize| -> String {
621                        // Dig out a Get (or IndexedFilter), and return the name of its Id.
622                        fn dig_name_from_expr(
623                            h: &dyn ExprHumanizer,
624                            e: &MirRelationExpr,
625                        ) -> Option<String> {
626                            let global_id_name = |id: &GlobalId| -> String {
627                                h.humanize_id_unqualified(*id)
628                                    .unwrap_or_else(|| id.to_string())
629                            };
630                            let (_mfp, e) = MapFilterProject::extract_from_expression(e);
631                            match e {
632                                Get { id, .. } => match id {
633                                    Id::Local(lid) => Some(lid.to_string()),
634                                    Id::Global(gid) => Some(global_id_name(gid)),
635                                },
636                                ArrangeBy { input, .. } => dig_name_from_expr(h, input),
637                                Join {
638                                    implementation: JoinImplementation::IndexedFilter(gid, ..),
639                                    ..
640                                } => Some(global_id_name(gid)),
641                                _ => None,
642                            }
643                        }
644                        match dig_name_from_expr(ctx.humanizer, &inputs[pos]) {
645                            Some(str) => format!("%{}:{}", pos, str),
646                            None => format!("%{}", pos),
647                        }
648                    };
649                    let join_key_to_string = |key: &Vec<MirScalarExpr>| -> String {
650                        if key.is_empty() {
651                            "×".to_owned()
652                        } else {
653                            CompactScalars(mode.seq(key, None)).to_string()
654                        }
655                    };
656                    let join_order = |start_idx: usize,
657                                      start_key: &Option<Vec<MirScalarExpr>>,
658                                      start_characteristics: &Option<JoinInputCharacteristics>,
659                                      tail: &Vec<(
660                        usize,
661                        Vec<MirScalarExpr>,
662                        Option<JoinInputCharacteristics>,
663                    )>|
664                     -> String {
665                        format!(
666                            "{}{}{} » {}",
667                            input_name(start_idx),
668                            match start_key {
669                                None => "".to_owned(),
670                                Some(key) => format!("[{}]", join_key_to_string(key)),
671                            },
672                            start_characteristics
673                                .as_ref()
674                                .map(|c| c.explain())
675                                .unwrap_or_else(|| "".to_string()),
676                            separated(
677                                " » ",
678                                tail.iter().map(|(pos, key, characteristics)| {
679                                    format!(
680                                        "{}[{}]{}",
681                                        input_name(*pos),
682                                        join_key_to_string(key),
683                                        characteristics
684                                            .as_ref()
685                                            .map(|c| c.explain())
686                                            .unwrap_or_else(|| "".to_string())
687                                    )
688                                })
689                            ),
690                        )
691                    };
692                    ctx.indented(|ctx| {
693                        match implementation {
694                            JoinImplementation::Differential(
695                                (start_idx, start_key, start_characteristics),
696                                tail,
697                            ) => {
698                                soft_assert_eq_or_log!(inputs.len(), tail.len() + 1);
699
700                                writeln!(f, "{}implementation", ctx.indent)?;
701                                ctx.indented(|ctx| {
702                                    writeln!(
703                                        f,
704                                        "{}{}",
705                                        ctx.indent,
706                                        join_order(
707                                            *start_idx,
708                                            start_key,
709                                            start_characteristics,
710                                            tail
711                                        )
712                                    )
713                                })?;
714                            }
715                            JoinImplementation::DeltaQuery(half_join_chains) => {
716                                soft_assert_eq_or_log!(inputs.len(), half_join_chains.len());
717
718                                writeln!(f, "{}implementation", ctx.indent)?;
719                                ctx.indented(|ctx| {
720                                    for (pos, chain) in half_join_chains.iter().enumerate() {
721                                        writeln!(
722                                            f,
723                                            "{}{}",
724                                            ctx.indent,
725                                            join_order(pos, &None, &None, chain)
726                                        )?;
727                                    }
728                                    Ok(())
729                                })?;
730                            }
731                            JoinImplementation::IndexedFilter(_, _, _, _) => {
732                                unreachable!() // because above we matched the other implementations
733                            }
734                            JoinImplementation::Unimplemented => {}
735                        }
736                        Ok(())
737                    })?;
738                }
739
740                ctx.indented(|ctx| {
741                    for input in inputs {
742                        input.fmt_text(f, ctx)?;
743                    }
744                    Ok(())
745                })?;
746            }
747            Join {
748                implementation:
749                    JoinImplementation::IndexedFilter(coll_id, idx_id, _key, literal_constraints),
750                inputs,
751                ..
752            } => {
753                let cse_id = match inputs.get(1).unwrap() {
754                    // If the constant input is actually a Get, then let `fmt_indexed_filter` know.
755                    Get { id, .. } => {
756                        if let Id::Local(local_id) = id {
757                            Some(local_id)
758                        } else {
759                            unreachable!()
760                        }
761                    }
762                    _ => None,
763                };
764                Self::fmt_indexed_filter(
765                    f,
766                    ctx,
767                    coll_id,
768                    idx_id,
769                    Some(literal_constraints.clone()),
770                    cse_id,
771                )?;
772                self.fmt_analyses(f, ctx)?;
773            }
774            Reduce {
775                group_key,
776                aggregates,
777                expected_group_size,
778                monotonic,
779                input,
780            } => {
781                FmtNode {
782                    fmt_root: |f, ctx: &mut PlanRenderingContext<'_, MirRelationExpr>| {
783                        if aggregates.len() == 0 && !ctx.config.raw_syntax {
784                            write!(f, "{}Distinct", ctx.indent)?;
785
786                            let group_key = mode.seq(group_key, input.column_names(ctx));
787                            let group_key = CompactScalars(group_key);
788                            write!(f, " project=[{}]", group_key)?;
789                        } else {
790                            write!(f, "{}Reduce", ctx.indent)?;
791
792                            if group_key.len() > 0 {
793                                let group_key = mode.seq(group_key, input.column_names(ctx));
794                                let group_key = CompactScalars(group_key);
795                                write!(f, " group_by=[{}]", group_key)?;
796                            }
797                        }
798                        if aggregates.len() > 0 {
799                            let cols = input.column_names(ctx);
800                            let aggregates = mode.seq(aggregates, cols);
801                            write!(f, " aggregates=[{}]", separated(", ", aggregates))?;
802                        }
803                        if *monotonic {
804                            write!(f, " monotonic")?;
805                        }
806                        if let Some(expected_group_size) = expected_group_size {
807                            write!(f, " exp_group_size={}", expected_group_size)?;
808                        }
809                        self.fmt_analyses(f, ctx)
810                    },
811                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
812                }
813                .render(f, ctx)?;
814            }
815            TopK {
816                group_key,
817                order_key,
818                limit,
819                offset,
820                monotonic,
821                input,
822                expected_group_size,
823            } => {
824                FmtNode {
825                    fmt_root: |f, ctx: &mut PlanRenderingContext<'_, MirRelationExpr>| {
826                        write!(f, "{}TopK", ctx.indent)?;
827                        let cols = input.column_names(ctx);
828                        if group_key.len() > 0 {
829                            if cols.is_some() {
830                                let group_by = mode.seq(group_key, cols);
831                                write!(f, " group_by=[{}]", separated(", ", group_by))?;
832                            } else {
833                                let group_by = Indices(group_key);
834                                write!(f, " group_by=[{}]", group_by)?;
835                            }
836                        }
837                        if order_key.len() > 0 {
838                            let order_by = mode.seq(order_key, cols);
839                            write!(f, " order_by=[{}]", separated(", ", order_by))?;
840                        }
841                        if let Some(limit) = limit {
842                            let limit = mode.expr(limit, cols);
843                            write!(f, " limit={}", limit)?;
844                        }
845                        if offset > &0 {
846                            write!(f, " offset={}", offset)?
847                        }
848                        if *monotonic {
849                            write!(f, " monotonic")?;
850                        }
851                        if let Some(expected_group_size) = expected_group_size {
852                            write!(f, " exp_group_size={}", expected_group_size)?;
853                        }
854                        self.fmt_analyses(f, ctx)
855                    },
856                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
857                }
858                .render(f, ctx)?;
859            }
860            Negate { input } => {
861                FmtNode {
862                    fmt_root: |f, ctx| {
863                        write!(f, "{}Negate", ctx.indent)?;
864                        self.fmt_analyses(f, ctx)
865                    },
866                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
867                }
868                .render(f, ctx)?;
869            }
870            Threshold { input } => {
871                FmtNode {
872                    fmt_root: |f, ctx| {
873                        write!(f, "{}Threshold", ctx.indent)?;
874                        self.fmt_analyses(f, ctx)
875                    },
876                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
877                }
878                .render(f, ctx)?;
879            }
880            Union { base, inputs } => {
881                write!(f, "{}Union", ctx.indent)?;
882                self.fmt_analyses(f, ctx)?;
883                ctx.indented(|ctx| {
884                    base.fmt_text(f, ctx)?;
885                    for input in inputs.iter() {
886                        input.fmt_text(f, ctx)?;
887                    }
888                    Ok(())
889                })?;
890            }
891            ArrangeBy { input, keys } => {
892                FmtNode {
893                    fmt_root: |f, ctx: &mut PlanRenderingContext<'_, MirRelationExpr>| {
894                        write!(f, "{}ArrangeBy", ctx.indent)?;
895
896                        let keys = keys.iter().map(|key| {
897                            let key = mode.seq(key, input.column_names(ctx));
898                            CompactScalars(key)
899                        });
900                        let keys = separated("], [", keys);
901                        write!(f, " keys=[[{}]]", keys)?;
902
903                        self.fmt_analyses(f, ctx)
904                    },
905                    fmt_children: |f, ctx| input.fmt_text(f, ctx),
906                }
907                .render(f, ctx)?;
908            }
909        }
910
911        Ok(())
912    }
913
914    fn fmt_analyses(
915        &self,
916        f: &mut fmt::Formatter<'_>,
917        ctx: &PlanRenderingContext<'_, MirRelationExpr>,
918    ) -> fmt::Result {
919        if ctx.config.requires_analyses() {
920            if let Some(analyses) = ctx.annotations.get(self) {
921                writeln!(f, " {}", HumanizedAnalyses::new(analyses, ctx))
922            } else {
923                writeln!(f, " // error: no analyses for subtree in map")
924            }
925        } else {
926            writeln!(f)
927        }
928    }
929
930    fn column_names<'a>(
931        &'a self,
932        ctx: &'a PlanRenderingContext<'_, MirRelationExpr>,
933    ) -> Option<&'a Vec<String>> {
934        if !ctx.config.humanized_exprs {
935            None
936        } else if let Some(analyses) = ctx.annotations.get(self) {
937            analyses.column_names.as_ref()
938        } else {
939            None
940        }
941    }
942
943    pub fn fmt_indexed_filter<'a, T>(
944        f: &mut fmt::Formatter<'_>,
945        ctx: &mut PlanRenderingContext<'a, T>,
946        coll_id: &GlobalId, // The id of the collection that the index is on
947        idx_id: &GlobalId,  // The id of the index
948        constants: Option<Vec<Row>>, // The values that we are looking up
949        cse_id: Option<&LocalId>, // Sometimes, RelationCSE pulls out the const input
950    ) -> fmt::Result {
951        let mode = HumanizedExplain::new(ctx.config.redacted);
952
953        let humanized_coll = ctx
954            .humanizer
955            .humanize_id(*coll_id)
956            .unwrap_or_else(|| coll_id.to_string());
957        let humanized_index = ctx
958            .humanize_id_maybe_unqualified(*idx_id)
959            .unwrap_or_else(|| "[DELETED INDEX]".to_owned());
960        if let Some(constants) = constants {
961            write!(
962                f,
963                "{}ReadIndex on={} {}=[{} ",
964                ctx.as_mut(),
965                humanized_coll,
966                humanized_index,
967                IndexUsageType::Lookup(*idx_id),
968            )?;
969            if let Some(cse_id) = cse_id {
970                // If we were to simply print `constants` here, then the EXPLAIN output would look
971                // weird: It would look like as if there was a dangling cte, because we (probably)
972                // wouldn't be printing any Get that refers to that cte.
973                write!(f, "values=<Get {}>]", cse_id)?;
974            } else {
975                if constants.len() == 1 {
976                    let value = mode.expr(&constants[0], None);
977                    write!(f, "value={}]", value)?;
978                } else {
979                    let values = mode.seq(&constants, None);
980                    write!(f, "values=[{}]]", separated("; ", values))?;
981                }
982            }
983        } else {
984            // Can't happen in dataflow, only in fast path.
985            write!(
986                f,
987                "{}ReadIndex on={} {}=[{}]",
988                ctx.indent,
989                humanized_coll,
990                humanized_index,
991                IndexUsageType::FullScan
992            )?;
993        }
994        Ok(())
995    }
996}
997
998/// A helper struct that abstracts over the formatting behavior of a
999/// single-input node.
1000///
1001/// If [`mz_repr::explain::ExplainConfig::linear_chains`] is set, this will
1002/// render children before parents using the same indentation level, and if not
1003/// the children will be rendered indented after their parent.
1004struct FmtNode<F, G>
1005where
1006    F: FnOnce(
1007        &mut fmt::Formatter<'_>,
1008        &mut PlanRenderingContext<'_, MirRelationExpr>,
1009    ) -> fmt::Result,
1010    G: FnOnce(
1011        &mut fmt::Formatter<'_>,
1012        &mut PlanRenderingContext<'_, MirRelationExpr>,
1013    ) -> fmt::Result,
1014{
1015    fmt_root: F,
1016    fmt_children: G,
1017}
1018
1019impl<F, G> FmtNode<F, G>
1020where
1021    F: FnOnce(
1022        &mut fmt::Formatter<'_>,
1023        &mut PlanRenderingContext<'_, MirRelationExpr>,
1024    ) -> fmt::Result,
1025    G: FnOnce(
1026        &mut fmt::Formatter<'_>,
1027        &mut PlanRenderingContext<'_, MirRelationExpr>,
1028    ) -> fmt::Result,
1029{
1030    fn render(
1031        self,
1032        f: &mut fmt::Formatter<'_>,
1033        ctx: &mut PlanRenderingContext<'_, MirRelationExpr>,
1034    ) -> fmt::Result {
1035        let FmtNode {
1036            fmt_root,
1037            fmt_children,
1038        } = self;
1039        if ctx.config.linear_chains {
1040            // Render children before parents
1041            fmt_children(f, ctx)?;
1042            fmt_root(f, ctx)?;
1043        } else {
1044            // Render children indented after parent
1045            fmt_root(f, ctx)?;
1046            // cannot use ctx.indented() here
1047            *ctx.as_mut() += 1;
1048            fmt_children(f, ctx)?;
1049            *ctx.as_mut() -= 1;
1050        }
1051        Ok(())
1052    }
1053}
1054
1055impl MirScalarExpr {
1056    pub fn format(&self, f: &mut fmt::Formatter<'_>, cols: Option<&Vec<String>>) -> fmt::Result {
1057        let mode = HumanizedExplain::default();
1058        fmt::Display::fmt(&mode.expr(self, cols), f)
1059    }
1060}
1061
1062impl fmt::Display for MirScalarExpr {
1063    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1064        self.format(f, None)
1065    }
1066}
1067
1068/// A helper struct for wrapping expressions whose text output is modulated by
1069/// the presence of some local (inferred schema) or global (required redaction)
1070/// context.
1071#[derive(Debug, Clone)]
1072pub struct HumanizedExpr<'a, T, M = HumanizedExplain> {
1073    /// The expression to be humanized.
1074    pub expr: &'a T,
1075    /// An optional vector of inferred column names to be used when rendering
1076    /// column references in `expr`.
1077    pub cols: Option<&'a Vec<String>>,
1078    /// The rendering mode to use. See [`HumanizerMode`] for details.
1079    pub mode: M,
1080}
1081
1082impl<'a, T, M: HumanizerMode> HumanizedExpr<'a, T, M> {
1083    /// Wrap the given child `expr` into a [`HumanizedExpr`] using the same
1084    /// `cols` and `mode` as `self`.
1085    pub fn child<U>(&self, expr: &'a U) -> HumanizedExpr<'a, U, M> {
1086        HumanizedExpr {
1087            expr,
1088            cols: self.cols,
1089            mode: self.mode.clone(),
1090        }
1091    }
1092}
1093
1094/// A trait that abstracts the various ways in which we can humanize
1095/// expressions.
1096///
1097/// Currently, the degrees of freedom are:
1098/// - Humanizing for an `EXPLAIN` output vs humanizing for a notice output. This
1099///   is currently handled by the two different implementations of this trait -
1100///   [`HumanizedExplain`] vs [`HumanizedNotice`].
1101/// - Humanizing with redacted or non-redacted literals. This is currently
1102///   covered by the [`HumanizerMode::redacted`] method which is used by the
1103///   default implementation of [`HumanizerMode::humanize_datum`].
1104pub trait HumanizerMode: Sized + Clone {
1105    /// Default implementation of a default constructor.
1106    ///
1107    /// This will produce a [`HumanizerMode`] instance that redacts output in
1108    /// production deployments, but not in debug builds and in CI.
1109    fn default() -> Self {
1110        let redacted = !mz_ore::assert::soft_assertions_enabled();
1111        Self::new(redacted)
1112    }
1113
1114    /// Create a new instance of the optimizer mode with literal redaction
1115    /// determined by the `redacted` parameter value.
1116    fn new(redacted: bool) -> Self;
1117
1118    /// Factory method that wraps the given `expr` and `cols` into a
1119    /// [`HumanizedExpr`] with the current `mode`.
1120    fn expr<'a, T>(
1121        &self,
1122        expr: &'a T,
1123        cols: Option<&'a Vec<String>>,
1124    ) -> HumanizedExpr<'a, T, Self> {
1125        HumanizedExpr {
1126            expr,
1127            cols,
1128            mode: self.clone(),
1129        }
1130    }
1131
1132    /// Return `true` iff literal redaction is enabled for this mode.
1133    fn redacted(&self) -> bool;
1134
1135    /// Render reference to column `col` which resolves to the given `ident`.
1136    fn humanize_ident(col: usize, ident: Ident, f: &mut fmt::Formatter<'_>) -> fmt::Result;
1137
1138    /// Render a literal datum.
1139    ///
1140    /// The default implementation prints a redacted symbol (█) if redaction is
1141    /// enabled.
1142    fn humanize_datum(&self, datum: Datum<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1143        if self.redacted() {
1144            write!(f, "█")
1145        } else {
1146            write!(f, "{}", datum)
1147        }
1148    }
1149
1150    /// Wrap the given `exprs` (with `cols`) into [`HumanizedExpr`] with the current `mode`.
1151    fn seq<'i, T>(
1152        &self,
1153        exprs: &'i [T],
1154        cols: Option<&'i Vec<String>>,
1155    ) -> impl Iterator<Item = HumanizedExpr<'i, T, Self>> + Clone {
1156        exprs.iter().map(move |expr| self.expr(expr, cols))
1157    }
1158}
1159
1160/// A [`HumanizerMode`] that is ambiguous but allows us to print valid SQL
1161/// statements, so it should be used in optimizer notices.
1162///
1163/// The inner parameter is `true` iff literals should be redacted.
1164#[derive(Debug, Clone)]
1165pub struct HumanizedNotice(bool);
1166
1167impl HumanizerMode for HumanizedNotice {
1168    fn new(redacted: bool) -> Self {
1169        Self(redacted)
1170    }
1171
1172    fn redacted(&self) -> bool {
1173        self.0
1174    }
1175
1176    /// Write `ident`.
1177    fn humanize_ident(_col: usize, ident: Ident, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1178        write!(f, "{ident}")
1179    }
1180}
1181
1182/// A [`HumanizerMode`] that is unambiguous and should be used in `EXPLAIN` output.
1183///
1184/// The inner parameter is `true` iff literals should be redacted.
1185#[derive(Debug, Clone)]
1186pub struct HumanizedExplain(bool);
1187
1188impl HumanizerMode for HumanizedExplain {
1189    fn new(redacted: bool) -> Self {
1190        Self(redacted)
1191    }
1192
1193    fn redacted(&self) -> bool {
1194        self.0
1195    }
1196
1197    /// Write `#c{ident}`.
1198    fn humanize_ident(col: usize, ident: Ident, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1199        if ident.as_str() == UNKNOWN_COLUMN_NAME {
1200            write!(f, "#{col}")
1201        } else if ident.has_only_bare_chars() {
1202            // The leading `#c` already disambiguates the column, and EXPLAIN
1203            // output is never reparsed, so a keyword-named column (`any`,
1204            // `all`, …) needs no quoting here — unlike in SQL display. Print
1205            // the name bare for legibility; only fall back to the quoting
1206            // `Ident` display for names with awkward characters (whitespace,
1207            // braces, quotes) that would otherwise muddle the `{…}` annotation.
1208            write!(f, "#{col}{{{}}}", ident.as_str())
1209        } else {
1210            write!(f, "#{col}{{{ident}}}")
1211        }
1212    }
1213}
1214
1215// A usize that directly represents a column reference.
1216impl<'a, M> fmt::Display for HumanizedExpr<'a, usize, M>
1217where
1218    M: HumanizerMode,
1219{
1220    #[inline]
1221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1222        match self.cols {
1223            // We have a name inferred for the column indexed by `self.expr`. Write `ident`.
1224            Some(cols) if cols.len() > *self.expr && !cols[*self.expr].is_empty() => {
1225                // Note: using unchecked here is okay since we're directly
1226                // converting to a string afterwards.
1227                let ident = Ident::new_unchecked(cols[*self.expr].clone()); // TODO: try to avoid the `.clone()` here.
1228                M::humanize_ident(*self.expr, ident, f)
1229            }
1230            // We don't have name inferred for this column.
1231            _ => {
1232                // Write `#c`.
1233                write!(f, "#{}", self.expr)
1234            }
1235        }
1236    }
1237}
1238
1239// A column reference with a stored name.
1240// We defer to the inferred name, if available.
1241impl<'a, M> fmt::Display for HumanizedExpr<'a, (&usize, &Arc<str>), M>
1242where
1243    M: HumanizerMode,
1244{
1245    #[inline]
1246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247        match self.cols {
1248            // We have a name inferred for the column indexed by `self.expr`. Write `ident`.
1249            Some(cols) if cols.len() > *self.expr.0 && !cols[*self.expr.0].is_empty() => {
1250                // Note: using unchecked here is okay since we're directly
1251                // converting to a string afterwards.
1252                let ident = Ident::new_unchecked(cols[*self.expr.0].clone()); // TODO: try to avoid the `.clone()` here.
1253                M::humanize_ident(*self.expr.0, ident, f)
1254            }
1255            // We don't have name inferred for this column.
1256            _ => M::humanize_ident(*self.expr.0, Ident::new_unchecked(self.expr.1.as_ref()), f),
1257        }
1258    }
1259}
1260
1261impl<'a, T, M> ScalarOps for HumanizedExpr<'a, T, M>
1262where
1263    T: ScalarOps,
1264{
1265    fn match_col_ref(&self) -> Option<usize> {
1266        self.expr.match_col_ref()
1267    }
1268
1269    fn references(&self, col_ref: usize) -> bool {
1270        self.expr.references(col_ref)
1271    }
1272}
1273
1274pub trait HumanizeDisplay: Sized {
1275    fn humanize<'a, M: HumanizerMode>(
1276        e: &HumanizedExpr<'a, Self, M>,
1277        f: &mut fmt::Formatter<'_>,
1278    ) -> fmt::Result;
1279}
1280
1281impl HumanizeDisplay for MirScalarExpr {
1282    fn humanize<'a, M: HumanizerMode>(
1283        e: &HumanizedExpr<'a, MirScalarExpr, M>,
1284        f: &mut fmt::Formatter<'_>,
1285    ) -> fmt::Result {
1286        use MirScalarExpr::*;
1287
1288        match e.expr {
1289            Column(i, TreatAsEqual(None)) => {
1290                // Delegate to the `HumanizedExpr<'a, _>` implementation (plain column reference).
1291                e.child(i).fmt(f)
1292            }
1293            Column(i, TreatAsEqual(Some(name))) => {
1294                // Delegate to the `HumanizedExpr<'a, _>` implementation (with stored name information)
1295                e.child(&(i, name)).fmt(f)
1296            }
1297            Literal(row, _) => {
1298                // Delegate to the `HumanizedExpr<'a, _>` implementation.
1299                e.child(row).fmt(f)
1300            }
1301            CallUnmaterializable(func) => write!(f, "{}()", func),
1302            CallUnary { func, expr } => {
1303                if let crate::UnaryFunc::Not(_) = *func {
1304                    if let CallUnary { func, expr } = expr.as_ref() {
1305                        if let Some(is) = func.is() {
1306                            let expr = e.child::<MirScalarExpr>(&*expr);
1307                            return write!(f, "({}) IS NOT {}", expr, is);
1308                        }
1309                    }
1310                }
1311                if let Some(is) = func.is() {
1312                    let expr = e.child::<MirScalarExpr>(&*expr);
1313                    write!(f, "({}) IS {}", expr, is)
1314                } else {
1315                    let expr = e.child::<MirScalarExpr>(&*expr);
1316                    write!(f, "{}({})", func, expr)
1317                }
1318            }
1319            CallBinary { func, expr1, expr2 } => {
1320                let expr1 = e.child::<MirScalarExpr>(&*expr1);
1321                let expr2 = e.child::<MirScalarExpr>(&*expr2);
1322                if func.is_infix_op() {
1323                    write!(f, "({} {} {})", expr1, func, expr2)
1324                } else {
1325                    write!(f, "{}({}, {})", func, expr1, expr2)
1326                }
1327            }
1328            CallVariadic { func, exprs } => {
1329                use crate::VariadicFunc::*;
1330                match func {
1331                    CaseLiteral(cl) => {
1332                        let input = e.child::<MirScalarExpr>(&exprs[0]);
1333                        write!(f, "case_lookup {}", input)?;
1334                        for entry in &cl.lookup {
1335                            let result = e.child::<MirScalarExpr>(&exprs[entry.expr_index]);
1336                            write!(f, " when ")?;
1337                            e.mode.humanize_datum(entry.literal.unpack_first(), f)?;
1338                            write!(f, " then {}", result)?;
1339                        }
1340                        let els = e.child::<MirScalarExpr>(exprs.last().unwrap());
1341                        write!(f, " else {} end", els)
1342                    }
1343                    ArrayCreate(..) => {
1344                        let exprs = exprs.iter().map(|expr| e.child(expr));
1345                        let exprs = separated(", ", exprs);
1346                        write!(f, "array[{}]", exprs)
1347                    }
1348                    ListCreate(..) => {
1349                        let exprs = exprs.iter().map(|expr| e.child(expr));
1350                        let exprs = separated(", ", exprs);
1351                        write!(f, "list[{}]", exprs)
1352                    }
1353                    RecordCreate(..) => {
1354                        let exprs = exprs.iter().map(|expr| e.child(expr));
1355                        let exprs = separated(", ", exprs);
1356                        write!(f, "row({})", exprs)
1357                    }
1358                    func if func.is_infix_op() && exprs.len() > 1 => {
1359                        let exprs = exprs.iter().map(|expr| e.child(expr));
1360                        let func = format!(" {} ", func);
1361                        let exprs = separated(&func, exprs);
1362                        write!(f, "({})", exprs)
1363                    }
1364                    func => {
1365                        let exprs = exprs.iter().map(|expr| e.child(expr));
1366                        let exprs = separated(", ", exprs);
1367                        write!(f, "{}({})", func, exprs)
1368                    }
1369                }
1370            }
1371            If { cond, then, els } => {
1372                let cond = e.child::<MirScalarExpr>(&*cond);
1373                let then = e.child::<MirScalarExpr>(&*then);
1374                let els = e.child::<MirScalarExpr>(&*els);
1375                write!(f, "case when {} then {} else {} end", cond, then, els)
1376            }
1377        }
1378    }
1379}
1380
1381impl<'a, T, M> fmt::Display for HumanizedExpr<'a, T, M>
1382where
1383    T: HumanizeDisplay,
1384    M: HumanizerMode,
1385{
1386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1387        T::humanize(self, f)
1388    }
1389}
1390
1391impl<'a, M> fmt::Display for HumanizedExpr<'a, AggregateExpr, M>
1392where
1393    M: HumanizerMode,
1394{
1395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1396        if self.expr.is_count_asterisk() {
1397            return write!(f, "count(*)");
1398        }
1399
1400        write!(
1401            f,
1402            "{}({}",
1403            self.child(&self.expr.func),
1404            if self.expr.distinct { "distinct " } else { "" }
1405        )?;
1406
1407        self.child(&self.expr.expr).fmt(f)?;
1408        write!(f, ")")
1409    }
1410}
1411
1412/// Render a literal value represented as a single-element [`Row`] or an
1413/// [`EvalError`].
1414///
1415/// The default implementation calls [`HumanizerMode::humanize_datum`] for
1416/// the former and handles the error case (including redaction) directly for
1417/// the latter.
1418impl<'a, M> fmt::Display for HumanizedExpr<'a, Result<Row, EvalError>, M>
1419where
1420    M: HumanizerMode,
1421{
1422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1423        match self.expr {
1424            Ok(row) => self.mode.humanize_datum(row.unpack_first(), f),
1425            Err(err) => {
1426                if self.mode.redacted() {
1427                    write!(f, "error(█)")
1428                } else {
1429                    write!(f, "error({})", err.to_string().escaped())
1430                }
1431            }
1432        }
1433    }
1434}
1435
1436/// Delegates to the [`Row`] rendering of the wrapped row.
1437impl HumanizeDisplay for StableRow {
1438    fn humanize<'a, M: HumanizerMode>(
1439        e: &HumanizedExpr<'a, Self, M>,
1440        f: &mut fmt::Formatter<'_>,
1441    ) -> fmt::Result {
1442        e.child::<Row>(&e.expr.0).fmt(f)
1443    }
1444}
1445
1446/// Like the impl for `Result<Row, EvalError>`, for the stable-serialization
1447/// row wrapper used by LIR literals.
1448impl<'a, M> fmt::Display for HumanizedExpr<'a, Result<StableRow, EvalError>, M>
1449where
1450    M: HumanizerMode,
1451{
1452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1453        match self.expr {
1454            Ok(row) => self.mode.humanize_datum(row.unpack_first(), f),
1455            Err(err) => {
1456                if self.mode.redacted() {
1457                    write!(f, "error(█)")
1458                } else {
1459                    write!(f, "error({})", err.to_string().escaped())
1460                }
1461            }
1462        }
1463    }
1464}
1465
1466impl<'a, M> fmt::Display for HumanizedExpr<'a, Datum<'a>, M>
1467where
1468    M: HumanizerMode,
1469{
1470    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1471        self.mode.humanize_datum(*self.expr, f)
1472    }
1473}
1474
1475impl<'a, M> fmt::Display for HumanizedExpr<'a, Row, M>
1476where
1477    M: HumanizerMode,
1478{
1479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1480        f.write_str("(")?;
1481        for (i, d) in self.expr.iter().enumerate() {
1482            if i > 0 {
1483                write!(f, ", {}", self.child(&d))?;
1484            } else {
1485                write!(f, "{}", self.child(&d))?;
1486            }
1487        }
1488        f.write_str(")")?;
1489        Ok(())
1490    }
1491}
1492
1493pub fn fmt_text_constant_rows<'a, I>(
1494    f: &mut fmt::Formatter<'_>,
1495    mut rows: I,
1496    ctx: &mut Indent,
1497    redacted: bool,
1498) -> fmt::Result
1499where
1500    I: Iterator<Item = (&'a Row, &'a Diff)>,
1501{
1502    let mut row_count = Diff::ZERO;
1503    let mut first_rows = Vec::with_capacity(20);
1504    for _ in 0..20 {
1505        if let Some((row, diff)) = rows.next() {
1506            row_count += diff.abs();
1507            first_rows.push((row, diff));
1508        }
1509    }
1510    let rest_of_row_count = rows.map(|(_, diff)| diff.abs()).sum::<Diff>();
1511    if !rest_of_row_count.is_zero() {
1512        writeln!(
1513            f,
1514            "{}total_rows (diffs absed): {}",
1515            ctx,
1516            row_count + rest_of_row_count
1517        )?;
1518        writeln!(f, "{}first_rows:", ctx)?;
1519        ctx.indented(move |ctx| write_first_rows(f, &first_rows, ctx, redacted))?;
1520    } else {
1521        write_first_rows(f, &first_rows, ctx, redacted)?;
1522    }
1523    Ok(())
1524}
1525
1526fn write_first_rows(
1527    f: &mut fmt::Formatter<'_>,
1528    first_rows: &Vec<(&Row, &Diff)>,
1529    ctx: &Indent,
1530    redacted: bool,
1531) -> fmt::Result {
1532    let mode = HumanizedExplain::new(redacted);
1533    for (row, diff) in first_rows {
1534        let row = mode.expr(*row, None);
1535        if **diff == Diff::ONE {
1536            writeln!(f, "{}- {}", ctx, row)?;
1537        } else {
1538            writeln!(f, "{}- ({} x {})", ctx, row, diff)?;
1539        }
1540    }
1541    Ok(())
1542}