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