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