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