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