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