Skip to main content

mz_compute_types/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 LIR structures.
11//!
12//! The format adheres to the following conventions:
13//! 1. In general, every line that starts with an uppercase character
14//!    corresponds to a [`LirRelationExpr`] variant.
15//! 2. Whenever the variant has an attached `~Plan`, the printed name is
16//!    `$V::$P` where `$V` identifies the variant and `$P` the plan.
17//! 3. The fields of a `~Plan` struct attached to a [`LirRelationExpr`] are rendered as if
18//!    they were part of the variant themself.
19//! 4. Non-recursive parameters of each sub-plan are written as `$key=$val`
20//!    pairs on the same line or as lowercase `$key` fields on indented lines.
21//! 5. A single non-recursive parameter can be written just as `$val`.
22
23use std::fmt;
24use std::ops::Deref;
25
26use itertools::Itertools;
27use mz_expr::Id;
28use mz_expr::MfpPlan;
29use mz_expr::explain::{HumanizedExplain, HumanizerMode, fmt_text_constant_rows};
30use mz_ore::soft_assert_or_log;
31use mz_ore::str::{IndentLike, StrExt, separated};
32use mz_repr::explain::text::DisplayText;
33use mz_repr::explain::{
34    CompactScalarSeq, CompactScalars, ExplainConfig, ExprHumanizer, Indices, PlanRenderingContext,
35};
36
37use crate::plan::join::delta_join::{DeltaPathPlan, DeltaStagePlan};
38use crate::plan::join::linear_join::LinearStagePlan;
39use crate::plan::join::{DeltaJoinPlan, JoinClosure, LinearJoinPlan};
40use crate::plan::reduce::{
41    AccumulablePlan, BasicPlan, BucketedPlan, HierarchicalPlan, MonotonicPlan, SingleBasicPlan,
42};
43use crate::plan::scalar::LirScalarExpr;
44use crate::plan::threshold::ThresholdPlan;
45use crate::plan::{
46    ArrangementStrategy, AvailableCollections, LirId, LirRelationExpr, LirRelationNode,
47};
48
49impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for LirRelationExpr {
50    fn fmt_text(
51        &self,
52        f: &mut fmt::Formatter<'_>,
53        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
54    ) -> fmt::Result {
55        if ctx.config.verbose_syntax {
56            self.fmt_verbose_text(f, ctx)
57        } else {
58            self.fmt_default_text(f, ctx)
59        }
60    }
61}
62
63impl LirRelationExpr {
64    // NOTE: This code needs to be kept in sync with the `Display` instance for
65    // `RenderPlan:ExprHumanizer`.
66    //
67    // This code determines what you see in `EXPLAIN`; that other code
68    // determine what you see when you run `mz_lir_mapping`.
69    fn fmt_default_text(
70        &self,
71        f: &mut fmt::Formatter<'_>,
72        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
73    ) -> fmt::Result {
74        use LirRelationNode::*;
75
76        let mode = HumanizedExplain::new(ctx.config.redacted);
77        let annotations = PlanAnnotations::new(ctx.config.clone(), self);
78
79        match &self.node {
80            Constant { rows } => {
81                write!(f, "{}→Constant ", ctx.indent)?;
82
83                match rows {
84                    Ok(rows) => write!(
85                        f,
86                        "({} row{})",
87                        rows.len(),
88                        if rows.len() == 1 { "" } else { "s" }
89                    )?,
90                    Err(err) => {
91                        if mode.redacted() {
92                            write!(f, "(error: █)")?;
93                        } else {
94                            write!(f, "(error: {})", err.to_string().quoted(),)?;
95                        }
96                    }
97                }
98
99                writeln!(f, "{annotations}")?;
100            }
101            Get { id, keys, plan } => {
102                ctx.indent.set(); // mark the current indent level
103
104                // Resolve the id as a string.
105                let id = match id {
106                    Id::Local(id) => id.to_string(),
107                    Id::Global(id) => ctx
108                        .humanizer
109                        .humanize_id(*id)
110                        .unwrap_or_else(|| id.to_string()),
111                };
112                // Render plan-specific fields.
113                use crate::plan::GetPlan;
114                match plan {
115                    GetPlan::PassArrangements => {
116                        if keys.raw && keys.arranged.is_empty() {
117                            writeln!(f, "{}→Stream {id}{annotations}", ctx.indent)?;
118                        } else {
119                            // we're not reporting on whether or not `raw` is set
120                            // we're not reporting on how many arrangements there are
121                            writeln!(f, "{}→Arranged {id}{annotations}", ctx.indent)?;
122                        }
123                    }
124                    GetPlan::Arrangement(key, Some(val), mfp) => {
125                        if !mfp.is_identity() {
126                            writeln!(f, "{}→Fused with Child Map/Filter/Project", ctx.indent)?;
127                            ctx.indent += 1;
128                            fmt_mfp_default_text(mfp, &mode, f, ctx)?;
129                            ctx.indent += 1;
130                        }
131
132                        writeln!(f, "{}→Index Lookup on {id}{annotations}", ctx.indent)?;
133                        ctx.indent += 1;
134                        let key = CompactScalars(mode.seq(key, None));
135                        write!(f, "{}Key: ({key}) ", ctx.indent)?;
136                        let val = mode.expr(val, None);
137                        writeln!(f, "Value: {val}")?;
138                    }
139                    GetPlan::Arrangement(key, None, mfp) => {
140                        if !mfp.is_identity() {
141                            writeln!(f, "{}→Fused with Child Map/Filter/Project", ctx.indent)?;
142                            ctx.indent += 1;
143                            fmt_mfp_default_text(mfp, &mode, f, ctx)?;
144                            ctx.indent += 1;
145                        }
146
147                        writeln!(f, "{}→Arranged {id}{annotations}", ctx.indent)?;
148                        ctx.indent += 1;
149                        let key = CompactScalars(mode.seq(key, None));
150                        writeln!(f, "{}Key: ({key})", ctx.indent)?;
151                    }
152                    GetPlan::Collection(mfp) => {
153                        if !mfp.is_identity() {
154                            writeln!(f, "{}→Fused with Child Map/Filter/Project", ctx.indent)?;
155                            ctx.indent += 1;
156                            fmt_mfp_default_text(mfp, &mode, f, ctx)?;
157                            ctx.indent += 1;
158                        }
159
160                        writeln!(f, "{}→Read {id}{annotations}", ctx.indent)?;
161                    }
162                }
163                ctx.indent.reset(); // reset the original indent level
164            }
165            Let { id, value, body } => {
166                let mut bindings = vec![(id, value.as_ref())];
167                let mut head = body.as_ref();
168
169                // Render Let-blocks nested in the body an outer Let-block in one step
170                // with a flattened list of bindings
171                while let Let { id, value, body } = &head.node {
172                    bindings.push((id, value.as_ref()));
173                    head = body.as_ref();
174                }
175
176                writeln!(f, "{}→With", ctx.indent)?;
177                ctx.indented(|ctx| {
178                    for (id, value) in bindings.iter() {
179                        writeln!(f, "{}cte {} =", ctx.indent, *id)?;
180                        ctx.indented(|ctx| value.fmt_text(f, ctx))?;
181                    }
182                    Ok(())
183                })?;
184                writeln!(f, "{}→Return{annotations}", ctx.indent)?;
185                ctx.indented(|ctx| head.fmt_text(f, ctx))?;
186            }
187            LetRec {
188                ids,
189                values,
190                limits,
191                body,
192            } => {
193                let head = body.as_ref();
194
195                writeln!(f, "{}→With Mutually Recursive", ctx.indent)?;
196                ctx.indented(|ctx| {
197                    let bindings = ids.iter().zip_eq(values).zip_eq(limits);
198                    for ((id, value), limit) in bindings {
199                        if let Some(limit) = limit {
200                            writeln!(f, "{}cte {} {} =", ctx.indent, limit, *id)?;
201                        } else {
202                            writeln!(f, "{}cte {} =", ctx.indent, *id)?;
203                        }
204                        ctx.indented(|ctx| value.fmt_text(f, ctx))?;
205                    }
206                    Ok(())
207                })?;
208                writeln!(f, "{}→Return{annotations}", ctx.indent)?;
209                ctx.indented(|ctx| head.fmt_text(f, ctx))?;
210            }
211            Mfp {
212                input,
213                mfp,
214                input_key_val: _,
215            } => {
216                writeln!(f, "{}→Map/Filter/Project{annotations}", ctx.indent)?;
217                ctx.indent.set();
218
219                ctx.indent += 1;
220                fmt_mfp_default_text(mfp, &mode, f, ctx)?;
221
222                // one more nesting level if we showed anything for the MFP
223                if !mfp.is_identity() {
224                    ctx.indent += 1;
225                }
226                input.fmt_text(f, ctx)?;
227                ctx.indent.reset();
228            }
229            FlatMap {
230                input_key: _,
231                input,
232                exprs,
233                func,
234                mfp_after,
235            } => {
236                ctx.indent.set();
237                if !mfp_after.is_identity() {
238                    writeln!(f, "{}→Fused with Child Map/Filter/Project", ctx.indent)?;
239                    ctx.indent += 1;
240                    fmt_mfp_default_text(mfp_after, &mode, f, ctx)?;
241                    ctx.indent += 1;
242                }
243
244                let exprs = mode.seq(exprs, None);
245                let exprs = CompactScalars(exprs);
246                writeln!(
247                    f,
248                    "{}→Table Function {func}({exprs}){annotations}",
249                    ctx.indent
250                )?;
251                ctx.indent += 1;
252
253                input.fmt_text(f, ctx)?;
254
255                ctx.indent.reset();
256            }
257            Join { inputs, plan } => {
258                use crate::plan::join::JoinPlan;
259                match plan {
260                    JoinPlan::Linear(plan) => {
261                        let label = if plan.has_cross_stage() {
262                            "→Differential Cross Join"
263                        } else {
264                            "→Differential Join"
265                        };
266                        write!(f, "{}{label} ", ctx.indent)?;
267                        fmt_join_chain(
268                            f,
269                            ctx.humanizer,
270                            &mode,
271                            inputs,
272                            plan.source_relation,
273                            plan.source_key.as_ref(),
274                            plan.stage_plans
275                                .iter()
276                                .map(|s| (s.lookup_relation, &s.lookup_key)),
277                        )?;
278                        writeln!(f, "{annotations}")?;
279                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
280                    }
281                    JoinPlan::Delta(plan) => {
282                        let label = if plan.has_cross_stage() {
283                            "→Delta Cross Join"
284                        } else {
285                            "→Delta Join"
286                        };
287                        write!(f, "{}{label}", ctx.indent)?;
288                        for dpp in &plan.path_plans {
289                            write!(f, " [")?;
290                            fmt_join_chain(
291                                f,
292                                ctx.humanizer,
293                                &mode,
294                                inputs,
295                                dpp.source_relation,
296                                Some(&dpp.source_key),
297                                dpp.stage_plans
298                                    .iter()
299                                    .map(|s| (s.lookup_relation, &s.lookup_key)),
300                            )?;
301                            write!(f, "]")?;
302                        }
303                        writeln!(f, "{annotations}")?;
304                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
305                    }
306                }
307
308                ctx.indented(|ctx| {
309                    for input in inputs {
310                        input.fmt_text(f, ctx)?;
311                    }
312                    Ok(())
313                })?;
314            }
315            Reduce {
316                input_key: _,
317                input,
318                key_val_plan,
319                plan,
320                mfp_after,
321                temporal_bucketing_strategy,
322            } => {
323                ctx.indent.set();
324                if !mfp_after.is_identity() {
325                    writeln!(f, "{}→Fused with Child Map/Filter/Project", ctx.indent)?;
326                    ctx.indent += 1;
327                    mode.expr(mfp_after.deref(), None)
328                        .fmt_default_text(f, ctx)?;
329                    ctx.indent += 1;
330                }
331
332                let temporally_bucketed = matches!(
333                    temporal_bucketing_strategy,
334                    ArrangementStrategy::TemporalBucketing
335                );
336
337                use crate::plan::reduce::ReducePlan;
338                match plan {
339                    ReducePlan::Distinct => {
340                        write!(f, "{}→", ctx.indent)?;
341                        if temporally_bucketed {
342                            write!(f, "Temporally-Bucketed ")?;
343                        }
344                        writeln!(f, "Distinct GroupAggregate{annotations}")?;
345                    }
346                    ReducePlan::Accumulable(plan) => {
347                        write!(f, "{}→", ctx.indent)?;
348                        if temporally_bucketed {
349                            write!(f, "Temporally-Bucketed ")?;
350                        }
351                        writeln!(f, "Accumulable GroupAggregate{annotations}")?;
352                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
353                    }
354                    ReducePlan::Hierarchical(
355                        plan @ HierarchicalPlan::Bucketed(BucketedPlan { buckets, .. }),
356                    ) => {
357                        write!(f, "{}→", ctx.indent)?;
358                        if temporally_bucketed {
359                            write!(f, "Temporally-Bucketed ")?;
360                        }
361                        write!(f, "Bucketed Hierarchical GroupAggregate (buckets:")?;
362                        for bucket in buckets {
363                            write!(f, " {bucket}")?;
364                        }
365                        writeln!(f, "){annotations}")?;
366                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
367                    }
368                    ReducePlan::Hierarchical(
369                        plan @ HierarchicalPlan::Monotonic(MonotonicPlan {
370                            must_consolidate, ..
371                        }),
372                    ) => {
373                        write!(f, "{}→", ctx.indent)?;
374                        if temporally_bucketed {
375                            write!(f, "Temporally-Bucketed ")?;
376                        }
377                        if *must_consolidate {
378                            write!(f, "Consolidating ")?;
379                        }
380                        writeln!(f, "Monotonic GroupAggregate{annotations}",)?;
381                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
382                    }
383                    ReducePlan::Basic(plan) => {
384                        ctx.indent.set();
385                        if let BasicPlan::Single(SingleBasicPlan {
386                            fused_unnest_list, ..
387                        }) = &plan
388                        {
389                            if *fused_unnest_list {
390                                writeln!(
391                                    f,
392                                    "{}→Fused with Child Table Function unnest_list",
393                                    ctx.indent
394                                )?;
395                                ctx.indent += 1;
396                            }
397                        }
398                        write!(f, "{}→", ctx.indent)?;
399                        if temporally_bucketed {
400                            write!(f, "Temporally-Bucketed ")?;
401                        }
402                        writeln!(f, "Non-incremental GroupAggregate{annotations}")?;
403                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
404                        ctx.indent.reset();
405                    }
406                }
407
408                ctx.indented(|ctx| {
409                    let kvp = key_val_plan.key_plan.deref();
410                    if !kvp.is_identity() {
411                        writeln!(f, "{}Key:", ctx.indent)?;
412                        ctx.indented(|ctx| {
413                            let key_plan = mode.expr(kvp, None);
414                            key_plan.fmt_default_text(f, ctx)
415                        })?;
416                    }
417
418                    input.fmt_text(f, ctx)
419                })?;
420
421                ctx.indent.reset();
422            }
423            TopK {
424                input,
425                top_k_plan,
426                temporal_bucketing_strategy,
427            } => {
428                let temporally_bucketed = matches!(
429                    temporal_bucketing_strategy,
430                    ArrangementStrategy::TemporalBucketing
431                );
432                use crate::plan::top_k::TopKPlan;
433                match top_k_plan {
434                    TopKPlan::MonotonicTop1(plan) => {
435                        write!(f, "{}→", ctx.indent)?;
436                        if temporally_bucketed {
437                            write!(f, "Temporally-Bucketed ")?;
438                        }
439                        if plan.must_consolidate {
440                            write!(f, "Consolidating ")?;
441                        }
442                        writeln!(f, "Monotonic Top1{annotations}")?;
443
444                        ctx.indented(|ctx| {
445                            if plan.group_key.len() > 0 {
446                                let group_by = CompactScalars(mode.seq(&plan.group_key, None));
447                                writeln!(f, "{}Group By {group_by}", ctx.indent)?;
448                            }
449                            if plan.order_key.len() > 0 {
450                                let order_by = separated(", ", mode.seq(&plan.order_key, None));
451                                writeln!(f, "{}Order By {order_by}", ctx.indent)?;
452                            }
453                            Ok(())
454                        })?;
455                    }
456                    TopKPlan::MonotonicTopK(plan) => {
457                        write!(f, "{}→", ctx.indent)?;
458                        if temporally_bucketed {
459                            write!(f, "Temporally-Bucketed ")?;
460                        }
461                        if plan.must_consolidate {
462                            write!(f, "Consolidating ")?;
463                        }
464                        writeln!(f, "Monotonic TopK{annotations}")?;
465
466                        ctx.indented(|ctx| {
467                            if plan.group_key.len() > 0 {
468                                let group_by = CompactScalars(mode.seq(&plan.group_key, None));
469                                writeln!(f, "{}Group By {group_by}", ctx.indent)?;
470                            }
471                            if plan.order_key.len() > 0 {
472                                let order_by = separated(", ", mode.seq(&plan.order_key, None));
473                                writeln!(f, "{}Order By {order_by}", ctx.indent)?;
474                            }
475                            if let Some(limit) = &plan.limit {
476                                let limit = mode.expr(limit, None);
477                                writeln!(f, "{}Limit {limit}", ctx.indent)?;
478                            }
479                            Ok(())
480                        })?;
481                    }
482                    TopKPlan::Basic(plan) => {
483                        write!(f, "{}→", ctx.indent)?;
484                        if temporally_bucketed {
485                            write!(f, "Temporally-Bucketed ")?;
486                        }
487                        writeln!(f, "Non-monotonic TopK{annotations}")?;
488
489                        ctx.indented(|ctx| {
490                            if plan.group_key.len() > 0 {
491                                let group_by = CompactScalars(mode.seq(&plan.group_key, None));
492                                writeln!(f, "{}Group By {group_by}", ctx.indent)?;
493                            }
494                            if plan.order_key.len() > 0 {
495                                let order_by = separated(", ", mode.seq(&plan.order_key, None));
496                                writeln!(f, "{}Order By {order_by}", ctx.indent)?;
497                            }
498                            if let Some(limit) = &plan.limit {
499                                let limit = mode.expr(limit, None);
500                                writeln!(f, "{}Limit {limit}", ctx.indent)?;
501                            }
502                            if plan.offset != 0 {
503                                let offset = plan.offset;
504                                writeln!(f, "{}Offset {offset}", ctx.indent)?;
505                            }
506                            Ok(())
507                        })?;
508                    }
509                }
510
511                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
512            }
513            Negate { input } => {
514                writeln!(f, "{}→Negate Diffs{annotations}", ctx.indent)?;
515
516                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
517            }
518            Threshold {
519                input,
520                threshold_plan,
521            } => {
522                match threshold_plan {
523                    ThresholdPlan::Basic(plan) => {
524                        write!(f, "{}→Threshold Diffs ", ctx.indent)?;
525                        let ensure_arrangement = Arrangement::from(&plan.ensure_arrangement);
526                        ensure_arrangement.fmt_text(f, ctx)?;
527                        writeln!(f, "{annotations}")?;
528                    }
529                };
530
531                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
532            }
533            Union {
534                inputs,
535                consolidate_output,
536                temporal_bucketing_strategies,
537            } => {
538                let any_temporally_bucketed = temporal_bucketing_strategies
539                    .iter()
540                    .any(|s| matches!(s, ArrangementStrategy::TemporalBucketing));
541                write!(f, "{}→", ctx.indent)?;
542                if any_temporally_bucketed {
543                    write!(f, "Temporally-Bucketed ")?;
544                }
545                if *consolidate_output {
546                    write!(f, "Consolidating ")?;
547                }
548                writeln!(f, "Union{annotations}")?;
549
550                ctx.indented(|ctx| {
551                    for input in inputs.iter() {
552                        input.fmt_text(f, ctx)?;
553                    }
554                    Ok(())
555                })?;
556            }
557            ArrangeBy {
558                input_key: _,
559                input,
560                input_mfp,
561                forms,
562                strategy: _,
563            } => {
564                ctx.indent.set();
565                if forms.raw && forms.arranged.is_empty() {
566                    soft_assert_or_log!(forms.raw, "raw stream with no arrangements");
567                    writeln!(f, "{}→Unarranged Raw Stream{annotations}", ctx.indent)?;
568                } else {
569                    write!(f, "{}→Arrange", ctx.indent)?;
570
571                    if !forms.arranged.is_empty() {
572                        let mode = HumanizedExplain::new(ctx.config.redacted);
573                        for (key, _, _) in &forms.arranged {
574                            if !key.is_empty() {
575                                let key = mode.seq(key, None);
576                                let key = CompactScalars(key);
577                                write!(f, " ({key})")?;
578                            } else {
579                                write!(f, " (empty key)")?;
580                            }
581                        }
582                    }
583                    writeln!(f, "{annotations}")?;
584                }
585
586                if !input_mfp.is_identity() {
587                    ctx.indent += 1;
588                    writeln!(f, "{}→Fused with Parent Map/Filter/Project", ctx.indent)?;
589                    ctx.indented(|ctx| fmt_mfp_default_text(input_mfp, &mode, f, ctx))?;
590                }
591
592                ctx.indent += 1;
593                input.fmt_text(f, ctx)?;
594                ctx.indent.reset();
595            }
596        }
597
598        Ok(())
599    }
600
601    fn fmt_verbose_text(
602        &self,
603        f: &mut fmt::Formatter<'_>,
604        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
605    ) -> fmt::Result {
606        use LirRelationNode::*;
607
608        let mode = HumanizedExplain::new(ctx.config.redacted);
609        let annotations = PlanAnnotations::new(ctx.config.clone(), self);
610
611        match &self.node {
612            Constant { rows } => match rows {
613                Ok(rows) => {
614                    if !rows.is_empty() {
615                        writeln!(f, "{}Constant{}", ctx.indent, annotations)?;
616                        ctx.indented(|ctx| {
617                            fmt_text_constant_rows(
618                                f,
619                                rows.iter().map(|(data, _, diff)| (data, diff)),
620                                &mut ctx.indent,
621                                ctx.config.redacted,
622                            )
623                        })?;
624                    } else {
625                        writeln!(f, "{}Constant <empty>{}", ctx.indent, annotations)?;
626                    }
627                }
628                Err(err) => {
629                    if mode.redacted() {
630                        writeln!(f, "{}Error █{}", ctx.indent, annotations)?;
631                    } else {
632                        {
633                            writeln!(
634                                f,
635                                "{}Error {}{}",
636                                ctx.indent,
637                                err.to_string().quoted(),
638                                annotations
639                            )?;
640                        }
641                    }
642                }
643            },
644
645            Get { id, keys, plan } => {
646                ctx.indent.set(); // mark the current indent level
647
648                // Resolve the id as a string.
649                let id = match id {
650                    Id::Local(id) => id.to_string(),
651                    Id::Global(id) => ctx
652                        .humanizer
653                        .humanize_id(*id)
654                        .unwrap_or_else(|| id.to_string()),
655                };
656                // Render plan-specific fields.
657                use crate::plan::GetPlan;
658                match plan {
659                    GetPlan::PassArrangements => {
660                        writeln!(
661                            f,
662                            "{}Get::PassArrangements {}{}",
663                            ctx.indent, id, annotations
664                        )?;
665                        ctx.indent += 1;
666                    }
667                    GetPlan::Arrangement(key, val, mfp) => {
668                        writeln!(f, "{}Get::Arrangement {}{}", ctx.indent, id, annotations)?;
669                        ctx.indent += 1;
670                        fmt_mfp_verbose_text(mfp, &mode, f, ctx)?;
671                        {
672                            let key = mode.seq(key, None);
673                            let key = CompactScalars(key);
674                            writeln!(f, "{}key={}", ctx.indent, key)?;
675                        }
676                        if let Some(val) = val {
677                            let val = mode.expr(val, None);
678                            writeln!(f, "{}val={}", ctx.indent, val)?;
679                        }
680                    }
681                    GetPlan::Collection(mfp) => {
682                        writeln!(f, "{}Get::Collection {}{}", ctx.indent, id, annotations)?;
683                        ctx.indent += 1;
684                        fmt_mfp_verbose_text(mfp, &mode, f, ctx)?;
685                    }
686                }
687
688                // Render plan-agnostic fields (common for all plans for this variant).
689                keys.fmt_text(f, ctx)?;
690
691                ctx.indent.reset(); // reset the original indent level
692            }
693            Let { id, value, body } => {
694                let mut bindings = vec![(id, value.as_ref())];
695                let mut head = body.as_ref();
696
697                // Render Let-blocks nested in the body an outer Let-block in one step
698                // with a flattened list of bindings
699                while let Let { id, value, body } = &head.node {
700                    bindings.push((id, value.as_ref()));
701                    head = body.as_ref();
702                }
703
704                writeln!(f, "{}With", ctx.indent)?;
705                ctx.indented(|ctx| {
706                    for (id, value) in bindings.iter() {
707                        writeln!(f, "{}cte {} =", ctx.indent, *id)?;
708                        ctx.indented(|ctx| value.fmt_text(f, ctx))?;
709                    }
710                    Ok(())
711                })?;
712                writeln!(f, "{}Return{}", ctx.indent, annotations)?;
713                ctx.indented(|ctx| head.fmt_text(f, ctx))?;
714            }
715            LetRec {
716                ids,
717                values,
718                limits,
719                body,
720            } => {
721                let head = body.as_ref();
722
723                writeln!(f, "{}With Mutually Recursive", ctx.indent)?;
724                ctx.indented(|ctx| {
725                    let bindings = ids.iter().zip_eq(values).zip_eq(limits);
726                    for ((id, value), limit) in bindings {
727                        if let Some(limit) = limit {
728                            writeln!(f, "{}cte {} {} =", ctx.indent, limit, *id)?;
729                        } else {
730                            writeln!(f, "{}cte {} =", ctx.indent, *id)?;
731                        }
732                        ctx.indented(|ctx| value.fmt_text(f, ctx))?;
733                    }
734                    Ok(())
735                })?;
736                writeln!(f, "{}Return{}", ctx.indent, annotations)?;
737                ctx.indented(|ctx| head.fmt_text(f, ctx))?;
738            }
739            Mfp {
740                input,
741                mfp,
742                input_key_val,
743            } => {
744                writeln!(f, "{}Mfp{}", ctx.indent, annotations)?;
745                ctx.indented(|ctx| {
746                    fmt_mfp_verbose_text(mfp, &mode, f, ctx)?;
747                    if let Some((key, val)) = input_key_val {
748                        {
749                            let key = mode.seq(key, None);
750                            let key = CompactScalars(key);
751                            writeln!(f, "{}input_key={}", ctx.indent, key)?;
752                        }
753                        if let Some(val) = val {
754                            let val = mode.expr(val, None);
755                            writeln!(f, "{}input_val={}", ctx.indent, val)?;
756                        }
757                    }
758                    input.fmt_text(f, ctx)
759                })?;
760            }
761            FlatMap {
762                input_key,
763                input,
764                exprs,
765                func,
766                mfp_after,
767            } => {
768                let exprs = mode.seq(exprs, None);
769                let exprs = CompactScalars(exprs);
770                writeln!(
771                    f,
772                    "{}FlatMap {}({}){}",
773                    ctx.indent, func, exprs, annotations
774                )?;
775                ctx.indented(|ctx| {
776                    if let Some(key) = input_key {
777                        let key = mode.seq(key, None);
778                        let key = CompactScalars(key);
779                        writeln!(f, "{}input_key={}", ctx.indent, key)?;
780                    }
781                    if !mfp_after.is_identity() {
782                        writeln!(f, "{}mfp_after", ctx.indent)?;
783                        ctx.indented(|ctx| fmt_mfp_verbose_text(mfp_after, &mode, f, ctx))?;
784                    }
785                    input.fmt_text(f, ctx)
786                })?;
787            }
788            Join { inputs, plan } => {
789                use crate::plan::join::JoinPlan;
790                match plan {
791                    JoinPlan::Linear(plan) => {
792                        writeln!(f, "{}Join::Linear{}", ctx.indent, annotations)?;
793                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
794                    }
795                    JoinPlan::Delta(plan) => {
796                        writeln!(f, "{}Join::Delta{}", ctx.indent, annotations)?;
797                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
798                    }
799                }
800                ctx.indented(|ctx| {
801                    for input in inputs {
802                        input.fmt_text(f, ctx)?;
803                    }
804                    Ok(())
805                })?;
806            }
807            Reduce {
808                input_key,
809                input,
810                key_val_plan,
811                plan,
812                mfp_after,
813                temporal_bucketing_strategy,
814            } => {
815                use crate::plan::reduce::ReducePlan;
816                match plan {
817                    ReducePlan::Distinct => {
818                        writeln!(f, "{}Reduce::Distinct{}", ctx.indent, annotations)?;
819                    }
820                    ReducePlan::Accumulable(plan) => {
821                        writeln!(f, "{}Reduce::Accumulable{}", ctx.indent, annotations)?;
822                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
823                    }
824                    ReducePlan::Hierarchical(plan) => {
825                        writeln!(f, "{}Reduce::Hierarchical{}", ctx.indent, annotations)?;
826                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
827                    }
828                    ReducePlan::Basic(plan) => {
829                        writeln!(f, "{}Reduce::Basic{}", ctx.indent, annotations)?;
830                        ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
831                    }
832                }
833                ctx.indented(|ctx| {
834                    if let Some(key) = input_key {
835                        let key = mode.seq(key, None);
836                        let key = CompactScalars(key);
837                        writeln!(f, "{}input_key={}", ctx.indent, key)?;
838                    }
839                    if !matches!(temporal_bucketing_strategy, ArrangementStrategy::Direct) {
840                        writeln!(
841                            f,
842                            "{}temporal_bucketing_strategy={}",
843                            ctx.indent, temporal_bucketing_strategy
844                        )?;
845                    }
846                    if key_val_plan.key_plan.deref().is_identity() {
847                        writeln!(f, "{}key_plan=id", ctx.indent)?;
848                    } else {
849                        writeln!(f, "{}key_plan", ctx.indent)?;
850                        ctx.indented(|ctx| {
851                            let key_plan = mode.expr(key_val_plan.key_plan.deref(), None);
852                            key_plan.fmt_text(f, ctx)
853                        })?;
854                    }
855                    if key_val_plan.val_plan.deref().is_identity() {
856                        writeln!(f, "{}val_plan=id", ctx.indent)?;
857                    } else {
858                        writeln!(f, "{}val_plan", ctx.indent)?;
859                        ctx.indented(|ctx| {
860                            let val_plan = mode.expr(key_val_plan.val_plan.deref(), None);
861                            val_plan.fmt_text(f, ctx)
862                        })?;
863                    }
864                    if !mfp_after.is_identity() {
865                        writeln!(f, "{}mfp_after", ctx.indent)?;
866                        ctx.indented(|ctx| mode.expr(mfp_after.deref(), None).fmt_text(f, ctx))?;
867                    }
868
869                    input.fmt_text(f, ctx)
870                })?;
871            }
872            TopK {
873                input,
874                top_k_plan,
875                temporal_bucketing_strategy,
876            } => {
877                use crate::plan::top_k::TopKPlan;
878                match top_k_plan {
879                    TopKPlan::MonotonicTop1(plan) => {
880                        write!(f, "{}TopK::MonotonicTop1", ctx.indent)?;
881                        if plan.group_key.len() > 0 {
882                            let group_by = mode.seq(&plan.group_key, None);
883                            let group_by = CompactScalars(group_by);
884                            write!(f, " group_by=[{}]", group_by)?;
885                        }
886                        if plan.order_key.len() > 0 {
887                            let order_by = mode.seq(&plan.order_key, None);
888                            let order_by = separated(", ", order_by);
889                            write!(f, " order_by=[{}]", order_by)?;
890                        }
891                        if plan.must_consolidate {
892                            write!(f, " must_consolidate")?;
893                        }
894                    }
895                    TopKPlan::MonotonicTopK(plan) => {
896                        write!(f, "{}TopK::MonotonicTopK", ctx.indent)?;
897                        if plan.group_key.len() > 0 {
898                            let group_by = mode.seq(&plan.group_key, None);
899                            let group_by = CompactScalars(group_by);
900                            write!(f, " group_by=[{}]", group_by)?;
901                        }
902                        if plan.order_key.len() > 0 {
903                            let order_by = mode.seq(&plan.order_key, None);
904                            let order_by = separated(", ", order_by);
905                            write!(f, " order_by=[{}]", order_by)?;
906                        }
907                        if let Some(limit) = &plan.limit {
908                            let limit = mode.expr(limit, None);
909                            write!(f, " limit={}", limit)?;
910                        }
911                        if plan.must_consolidate {
912                            write!(f, " must_consolidate")?;
913                        }
914                    }
915                    TopKPlan::Basic(plan) => {
916                        write!(f, "{}TopK::Basic", ctx.indent)?;
917                        if plan.group_key.len() > 0 {
918                            let group_by = mode.seq(&plan.group_key, None);
919                            let group_by = CompactScalars(group_by);
920                            write!(f, " group_by=[{}]", group_by)?;
921                        }
922                        if plan.order_key.len() > 0 {
923                            let order_by = mode.seq(&plan.order_key, None);
924                            let order_by = separated(", ", order_by);
925                            write!(f, " order_by=[{}]", order_by)?;
926                        }
927                        if let Some(limit) = &plan.limit {
928                            let limit = mode.expr(limit, None);
929                            write!(f, " limit={}", limit)?;
930                        }
931                        if &plan.offset > &0 {
932                            write!(f, " offset={}", plan.offset)?;
933                        }
934                    }
935                }
936                writeln!(f, "{}", annotations)?;
937                ctx.indented(|ctx| {
938                    if !matches!(temporal_bucketing_strategy, ArrangementStrategy::Direct) {
939                        writeln!(
940                            f,
941                            "{}temporal_bucketing_strategy={}",
942                            ctx.indent, temporal_bucketing_strategy
943                        )?;
944                    }
945                    input.fmt_text(f, ctx)
946                })?;
947            }
948            Negate { input } => {
949                writeln!(f, "{}Negate{}", ctx.indent, annotations)?;
950                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
951            }
952            Threshold {
953                input,
954                threshold_plan,
955            } => {
956                use crate::plan::threshold::ThresholdPlan;
957                match threshold_plan {
958                    ThresholdPlan::Basic(plan) => {
959                        let ensure_arrangement = Arrangement::from(&plan.ensure_arrangement);
960                        write!(f, "{}Threshold::Basic", ctx.indent)?;
961                        write!(f, " ensure_arrangement=")?;
962                        ensure_arrangement.fmt_text(f, ctx)?;
963                        writeln!(f, "{}", annotations)?;
964                    }
965                };
966                ctx.indented(|ctx| input.fmt_text(f, ctx))?;
967            }
968            Union {
969                inputs,
970                consolidate_output,
971                temporal_bucketing_strategies,
972            } => {
973                if *consolidate_output {
974                    writeln!(
975                        f,
976                        "{}Union consolidate_output={}{}",
977                        ctx.indent, consolidate_output, annotations
978                    )?;
979                } else {
980                    writeln!(f, "{}Union{}", ctx.indent, annotations)?;
981                }
982                ctx.indented(|ctx| {
983                    if temporal_bucketing_strategies
984                        .iter()
985                        .any(|s| !matches!(s, ArrangementStrategy::Direct))
986                    {
987                        let strategies = temporal_bucketing_strategies
988                            .iter()
989                            .map(|s| format!("{}", s))
990                            .collect::<Vec<_>>()
991                            .join(", ");
992                        writeln!(
993                            f,
994                            "{}temporal_bucketing_strategies=[{}]",
995                            ctx.indent, strategies
996                        )?;
997                    }
998                    for input in inputs.iter() {
999                        input.fmt_text(f, ctx)?;
1000                    }
1001                    Ok(())
1002                })?;
1003            }
1004            ArrangeBy {
1005                input_key,
1006                input,
1007                input_mfp,
1008                forms,
1009                strategy,
1010            } => {
1011                writeln!(f, "{}ArrangeBy{}", ctx.indent, annotations)?;
1012                ctx.indented(|ctx| {
1013                    if let Some(key) = input_key {
1014                        let key = mode.seq(key, None);
1015                        let key = CompactScalars(key);
1016                        writeln!(f, "{}input_key=[{}]", ctx.indent, key)?;
1017                    }
1018                    if !matches!(strategy, ArrangementStrategy::Direct) {
1019                        writeln!(f, "{}strategy={:?}", ctx.indent, strategy)?;
1020                    }
1021                    fmt_mfp_verbose_text(input_mfp, &mode, f, ctx)?;
1022                    forms.fmt_text(f, ctx)?;
1023                    // Render input
1024                    input.fmt_text(f, ctx)
1025                })?;
1026            }
1027        }
1028
1029        Ok(())
1030    }
1031}
1032
1033/// Format the temporal bounds of an `MfpPlan` as `mz_now()` inequalities.
1034///
1035/// Renders as `TemporalFilter: <lowers> <= mz_now() < <uppers>`, omitting
1036/// the `<lowers> <=` or `< <uppers>` parts when the corresponding bound
1037/// list is empty.
1038///
1039/// Format an `MfpPlan` using concise (default) syntax: project/filter/map + temporal bounds.
1040fn fmt_mfp_default_text(
1041    mfp_plan: &MfpPlan<LirScalarExpr>,
1042    mode: &HumanizedExplain,
1043    f: &mut fmt::Formatter<'_>,
1044    ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1045) -> fmt::Result {
1046    mode.expr(mfp_plan.safe_mfp().deref(), None)
1047        .fmt_default_text(f, ctx)?;
1048    fmt_temporal_bounds(mfp_plan, mode, f, ctx)
1049}
1050
1051/// Format an `MfpPlan` using verbose syntax: project/filter/map + temporal bounds.
1052fn fmt_mfp_verbose_text(
1053    mfp_plan: &MfpPlan<LirScalarExpr>,
1054    mode: &HumanizedExplain,
1055    f: &mut fmt::Formatter<'_>,
1056    ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1057) -> fmt::Result {
1058    mode.expr(mfp_plan.safe_mfp().deref(), None)
1059        .fmt_text(f, ctx)?;
1060    fmt_temporal_bounds(mfp_plan, mode, f, ctx)
1061}
1062
1063/// Render temporal bounds as `TemporalFilter: <lowers> <= mz_now() < <uppers>`.
1064///
1065/// TODO(mgree): It is possible to recover equalities (`mz_now() = expr`) and
1066/// other, finer-grained relationships from the bound expressions, but for now
1067/// we just display `<=` and `<`.
1068#[allow(clippy::needless_pass_by_ref_mut)]
1069fn fmt_temporal_bounds(
1070    mfp_plan: &MfpPlan<LirScalarExpr>,
1071    mode: &HumanizedExplain,
1072    f: &mut fmt::Formatter<'_>,
1073    ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1074) -> fmt::Result {
1075    let (_, lower_bounds, upper_bounds) = mfp_plan.as_parts();
1076
1077    if lower_bounds.is_empty() && upper_bounds.is_empty() {
1078        return Ok(());
1079    }
1080
1081    write!(f, "{}TemporalFilter: ", ctx.indent)?;
1082    if !lower_bounds.is_empty() {
1083        let lowers = lower_bounds.iter().map(|b| mode.expr(b, None));
1084        write!(f, "{} <= ", separated(", ", lowers))?;
1085    }
1086    write!(f, "mz_now()")?;
1087    if !upper_bounds.is_empty() {
1088        let uppers = upper_bounds.iter().map(|b| mode.expr(b, None));
1089        write!(f, " < {}", separated(", ", uppers))?;
1090    }
1091    writeln!(f)?;
1092
1093    Ok(())
1094}
1095
1096impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for AvailableCollections {
1097    fn fmt_text(
1098        &self,
1099        f: &mut fmt::Formatter<'_>,
1100        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1101    ) -> fmt::Result {
1102        if ctx.config.verbose_syntax {
1103            self.fmt_verbose_text(f, ctx)
1104        } else {
1105            self.fmt_default_text(f, ctx)
1106        }
1107    }
1108}
1109impl AvailableCollections {
1110    fn fmt_default_text(
1111        &self,
1112        f: &mut fmt::Formatter<'_>,
1113        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1114    ) -> fmt::Result {
1115        let plural = if self.arranged.len() == 1 { "" } else { "s" };
1116        write!(
1117            f,
1118            "{}Keys: {} arrangement{plural} available",
1119            ctx.indent,
1120            self.arranged.len()
1121        )?;
1122
1123        if self.raw {
1124            writeln!(f, ", plus raw stream")?;
1125        } else {
1126            writeln!(f, ", no raw stream")?;
1127        }
1128
1129        ctx.indented(|ctx| {
1130            for (i, arrangement) in self.arranged.iter().enumerate() {
1131                let arrangement = Arrangement::from(arrangement);
1132                write!(f, "{}Arrangement {i}: ", ctx.indent)?;
1133                arrangement.fmt_text(f, ctx)?;
1134                writeln!(f, "")?;
1135            }
1136            Ok(())
1137        })?;
1138
1139        Ok(())
1140    }
1141
1142    fn fmt_verbose_text(
1143        &self,
1144        f: &mut fmt::Formatter<'_>,
1145        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1146    ) -> fmt::Result {
1147        // raw field
1148        let raw = &self.raw;
1149        writeln!(f, "{}raw={}", ctx.indent, raw)?;
1150        // arranged field
1151        for (i, arrangement) in self.arranged.iter().enumerate() {
1152            let arrangement = Arrangement::from(arrangement);
1153            write!(f, "{}arrangements[{}]=", ctx.indent, i)?;
1154            arrangement.fmt_text(f, ctx)?;
1155            writeln!(f, "")?;
1156        }
1157        Ok(())
1158    }
1159}
1160
1161/// Format a join implementation chain like `%0:t[#0{a}] » %1:u[#0{c}] » %2[×]`.
1162///
1163/// Each position is rendered as `%pos:name` when an underlying [`LirRelationNode::Get`] can be
1164/// dug out of the corresponding input plan (see [`humanize_input_name`]),
1165/// otherwise just `%pos`. `[×]` (U+00D7) marks a cross product (empty lookup
1166/// key). A `None` `source_key` renders the source position with no bracketed
1167/// suffix.
1168fn fmt_join_chain<'a, I>(
1169    f: &mut fmt::Formatter<'_>,
1170    humanizer: &dyn ExprHumanizer,
1171    mode: &HumanizedExplain,
1172    inputs: &[LirRelationExpr],
1173    source_relation: usize,
1174    source_key: Option<&'a Vec<LirScalarExpr>>,
1175    stages: I,
1176) -> fmt::Result
1177where
1178    I: IntoIterator<Item = (usize, &'a Vec<LirScalarExpr>)>,
1179{
1180    write!(
1181        f,
1182        "{}",
1183        humanize_input_name(humanizer, &inputs[source_relation], source_relation)
1184    )?;
1185    if let Some(key) = source_key {
1186        fmt_join_key_brackets(f, mode, key)?;
1187    }
1188    for (lookup_relation, lookup_key) in stages {
1189        write!(
1190            f,
1191            " » {}",
1192            humanize_input_name(humanizer, &inputs[lookup_relation], lookup_relation)
1193        )?;
1194        fmt_join_key_brackets(f, mode, lookup_key)?;
1195    }
1196    Ok(())
1197}
1198
1199/// Render `[k1, k2, …]` for a non-empty join key, or `[×]` for a cross product.
1200fn fmt_join_key_brackets(
1201    f: &mut fmt::Formatter<'_>,
1202    mode: &HumanizedExplain,
1203    key: &Vec<LirScalarExpr>,
1204) -> fmt::Result {
1205    if key.is_empty() {
1206        write!(f, "[×]")
1207    } else {
1208        let key = CompactScalars(mode.seq(key, None));
1209        write!(f, "[{key}]")
1210    }
1211}
1212
1213/// Render a join input as `%pos:name` if we can dig a `Get` out of `plan`,
1214/// otherwise just `%pos`. Mirrors `dig_name_from_expr` in
1215/// `src/expr/src/explain/text.rs` for the MIR `EXPLAIN OPTIMIZED PLAN` output.
1216fn humanize_input_name(
1217    humanizer: &dyn ExprHumanizer,
1218    plan: &LirRelationExpr,
1219    pos: usize,
1220) -> String {
1221    fn dig(humanizer: &dyn ExprHumanizer, plan: &LirRelationExpr) -> Option<String> {
1222        use crate::plan::LirRelationNode::*;
1223        match &plan.node {
1224            Get { id, .. } => match id {
1225                Id::Local(lid) => Some(lid.to_string()),
1226                Id::Global(gid) => Some(
1227                    humanizer
1228                        .humanize_id_unqualified(*gid)
1229                        .unwrap_or_else(|| gid.to_string()),
1230                ),
1231            },
1232            // Transparent wrappers: keep digging.
1233            ArrangeBy { input, .. } => dig(humanizer, input),
1234            Mfp { input, .. } => dig(humanizer, input),
1235            _ => None,
1236        }
1237    }
1238    match dig(humanizer, plan) {
1239        Some(name) => format!("%{pos}:{name}"),
1240        None => format!("%{pos}"),
1241    }
1242}
1243
1244impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for LinearJoinPlan {
1245    fn fmt_text(
1246        &self,
1247        f: &mut fmt::Formatter<'_>,
1248        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1249    ) -> fmt::Result {
1250        if ctx.config.verbose_syntax {
1251            self.fmt_verbose_text(f, ctx)
1252        } else {
1253            self.fmt_default_text(f, ctx)
1254        }
1255    }
1256}
1257impl LinearJoinPlan {
1258    /// True iff at least one stage is a cross product (empty lookup key).
1259    fn has_cross_stage(&self) -> bool {
1260        self.stage_plans.iter().any(|s| s.lookup_key.is_empty())
1261    }
1262
1263    #[allow(clippy::needless_pass_by_ref_mut)]
1264    fn fmt_default_text(
1265        &self,
1266        f: &mut fmt::Formatter<'_>,
1267        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1268    ) -> fmt::Result {
1269        // Per-stage closures (natural 0..N order). The header chain already
1270        // shows each stage's position + key, so we only emit a block when
1271        // there's a non-identity closure to attribute to that stage.
1272        for stage in self.stage_plans.iter() {
1273            if stage.closure.maps_or_filters() {
1274                writeln!(f, "{}after %{}:", ctx.indent, stage.lookup_relation)?;
1275                ctx.indented(|ctx| stage.closure.fmt_default_text(f, ctx))?;
1276            }
1277        }
1278        if let Some(final_closure) = &self.final_closure {
1279            if final_closure.maps_or_filters() {
1280                writeln!(f, "{}Final closure:", ctx.indent)?;
1281                ctx.indented(|ctx| final_closure.fmt_default_text(f, ctx))?;
1282            }
1283        }
1284        Ok(())
1285    }
1286
1287    fn fmt_verbose_text(
1288        &self,
1289        f: &mut fmt::Formatter<'_>,
1290        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1291    ) -> fmt::Result {
1292        let mode = HumanizedExplain::new(ctx.config.redacted);
1293        let plan = self;
1294        if let Some(closure) = plan.final_closure.as_ref() {
1295            if !closure.is_identity() {
1296                writeln!(f, "{}final_closure", ctx.indent)?;
1297                ctx.indented(|ctx| closure.fmt_text(f, ctx))?;
1298            }
1299        }
1300        for (i, plan) in plan.stage_plans.iter().enumerate() {
1301            writeln!(f, "{}linear_stage[{}]", ctx.indent, i)?;
1302            ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
1303        }
1304        if let Some(closure) = plan.initial_closure.as_ref() {
1305            if !closure.is_identity() {
1306                writeln!(f, "{}initial_closure", ctx.indent)?;
1307                ctx.indented(|ctx| closure.fmt_text(f, ctx))?;
1308            }
1309        }
1310        match &plan.source_key {
1311            Some(source_key) => {
1312                let source_key = mode.seq(source_key, None);
1313                let source_key = CompactScalars(source_key);
1314                writeln!(
1315                    f,
1316                    "{}source={{ relation={}, key=[{}] }}",
1317                    ctx.indent, &plan.source_relation, source_key
1318                )?
1319            }
1320            None => writeln!(
1321                f,
1322                "{}source={{ relation={}, key=[] }}",
1323                ctx.indent, &plan.source_relation
1324            )?,
1325        };
1326        Ok(())
1327    }
1328}
1329
1330impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for LinearStagePlan {
1331    fn fmt_text(
1332        &self,
1333        f: &mut fmt::Formatter<'_>,
1334        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1335    ) -> fmt::Result {
1336        if ctx.config.verbose_syntax {
1337            self.fmt_verbose_text(f, ctx)
1338        } else {
1339            self.fmt_default_text(f, ctx)
1340        }
1341    }
1342}
1343impl LinearStagePlan {
1344    #[allow(clippy::needless_pass_by_ref_mut)]
1345    fn fmt_default_text(
1346        &self,
1347        f: &mut fmt::Formatter<'_>,
1348        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1349    ) -> fmt::Result {
1350        // NB this code path should not be live, as fmt_default_text for
1351        // `LinearJoinPlan` prints out each stage already
1352        let lookup_relation = &self.lookup_relation;
1353        if !self.lookup_key.is_empty() {
1354            let lookup_key = CompactScalarSeq(&self.lookup_key);
1355            writeln!(
1356                f,
1357                "{}Lookup key {lookup_key} in %{lookup_relation}",
1358                ctx.indent
1359            )
1360        } else {
1361            writeln!(f, "{}Lookup in %{lookup_relation}", ctx.indent)
1362        }
1363    }
1364
1365    fn fmt_verbose_text(
1366        &self,
1367        f: &mut fmt::Formatter<'_>,
1368        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1369    ) -> fmt::Result {
1370        let mode = HumanizedExplain::new(ctx.config.redacted);
1371
1372        let plan = self;
1373        if !plan.closure.is_identity() {
1374            writeln!(f, "{}closure", ctx.indent)?;
1375            ctx.indented(|ctx| plan.closure.fmt_text(f, ctx))?;
1376        }
1377        {
1378            let lookup_relation = &plan.lookup_relation;
1379            let lookup_key = CompactScalarSeq(&plan.lookup_key);
1380            writeln!(
1381                f,
1382                "{}lookup={{ relation={}, key=[{}] }}",
1383                ctx.indent, lookup_relation, lookup_key
1384            )?;
1385        }
1386        {
1387            let stream_key = mode.seq(&plan.stream_key, None);
1388            let stream_key = CompactScalars(stream_key);
1389            let stream_thinning = Indices(&plan.stream_thinning);
1390            writeln!(
1391                f,
1392                "{}stream={{ key=[{}], thinning=({}) }}",
1393                ctx.indent, stream_key, stream_thinning
1394            )?;
1395        }
1396        Ok(())
1397    }
1398}
1399
1400impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for DeltaJoinPlan {
1401    fn fmt_text(
1402        &self,
1403        f: &mut fmt::Formatter<'_>,
1404        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1405    ) -> fmt::Result {
1406        if ctx.config.verbose_syntax {
1407            self.fmt_verbose_text(f, ctx)
1408        } else {
1409            self.fmt_default_text(f, ctx)
1410        }
1411    }
1412}
1413impl DeltaJoinPlan {
1414    /// True iff any stage in any path is a cross product.
1415    fn has_cross_stage(&self) -> bool {
1416        self.path_plans
1417            .iter()
1418            .any(|p| p.stage_plans.iter().any(|s| s.lookup_key.is_empty()))
1419    }
1420
1421    fn fmt_default_text(
1422        &self,
1423        f: &mut fmt::Formatter<'_>,
1424        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1425    ) -> fmt::Result {
1426        // The header chain already shows each path's positions + keys, so we
1427        // only print a `path %src:` block when at least one of its stage
1428        // closures or its final closure is non-identity.
1429        for plan in self.path_plans.iter() {
1430            let has_stage_closure = plan.stage_plans.iter().any(|s| s.closure.maps_or_filters());
1431            let has_final_closure = plan
1432                .final_closure
1433                .as_ref()
1434                .is_some_and(|c| c.maps_or_filters());
1435            if !has_stage_closure && !has_final_closure {
1436                continue;
1437            }
1438            writeln!(f, "{}path %{}:", ctx.indent, plan.source_relation)?;
1439            ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
1440        }
1441        Ok(())
1442    }
1443
1444    fn fmt_verbose_text(
1445        &self,
1446        f: &mut fmt::Formatter<'_>,
1447        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1448    ) -> fmt::Result {
1449        for (i, plan) in self.path_plans.iter().enumerate() {
1450            writeln!(f, "{}plan_path[{}]", ctx.indent, i)?;
1451            ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
1452        }
1453        Ok(())
1454    }
1455}
1456
1457impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for DeltaPathPlan {
1458    fn fmt_text(
1459        &self,
1460        f: &mut fmt::Formatter<'_>,
1461        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1462    ) -> fmt::Result {
1463        if ctx.config.verbose_syntax {
1464            self.fmt_verbose_text(f, ctx)
1465        } else {
1466            self.fmt_default_text(f, ctx)
1467        }
1468    }
1469}
1470
1471impl DeltaPathPlan {
1472    #[allow(clippy::needless_pass_by_ref_mut)]
1473    fn fmt_default_text(
1474        &self,
1475        f: &mut fmt::Formatter<'_>,
1476        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1477    ) -> fmt::Result {
1478        for stage in self.stage_plans.iter() {
1479            if stage.closure.maps_or_filters() {
1480                writeln!(f, "{}after %{}:", ctx.indent, stage.lookup_relation)?;
1481                ctx.indented(|ctx| stage.closure.fmt_default_text(f, ctx))?;
1482            }
1483        }
1484        if let Some(final_closure) = &self.final_closure {
1485            if final_closure.maps_or_filters() {
1486                writeln!(f, "{}Final closure:", ctx.indent)?;
1487                ctx.indented(|ctx| final_closure.fmt_default_text(f, ctx))?;
1488            }
1489        }
1490        Ok(())
1491    }
1492
1493    fn fmt_verbose_text(
1494        &self,
1495        f: &mut fmt::Formatter<'_>,
1496        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1497    ) -> fmt::Result {
1498        let mode = HumanizedExplain::new(ctx.config.redacted);
1499        let plan = self;
1500        if let Some(closure) = plan.final_closure.as_ref() {
1501            if !closure.is_identity() {
1502                writeln!(f, "{}final_closure", ctx.indent)?;
1503                ctx.indented(|ctx| closure.fmt_text(f, ctx))?;
1504            }
1505        }
1506        for (i, plan) in plan.stage_plans.iter().enumerate().rev() {
1507            writeln!(f, "{}delta_stage[{}]", ctx.indent, i)?;
1508            ctx.indented(|ctx| plan.fmt_text(f, ctx))?;
1509        }
1510        if !plan.initial_closure.is_identity() {
1511            writeln!(f, "{}initial_closure", ctx.indent)?;
1512            ctx.indented(|ctx| plan.initial_closure.fmt_text(f, ctx))?;
1513        }
1514        {
1515            let source_relation = &plan.source_relation;
1516            let source_key = mode.seq(&plan.source_key, None);
1517            let source_key = CompactScalars(source_key);
1518            writeln!(
1519                f,
1520                "{}source={{ relation={}, key=[{}] }}",
1521                ctx.indent, source_relation, source_key
1522            )?;
1523        }
1524        Ok(())
1525    }
1526}
1527
1528impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for DeltaStagePlan {
1529    fn fmt_text(
1530        &self,
1531        f: &mut fmt::Formatter<'_>,
1532        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1533    ) -> fmt::Result {
1534        if ctx.config.verbose_syntax {
1535            self.fmt_verbose_text(f, ctx)
1536        } else {
1537            self.fmt_default_text(f, ctx)
1538        }
1539    }
1540}
1541impl DeltaStagePlan {
1542    #[allow(clippy::needless_pass_by_ref_mut)]
1543    fn fmt_default_text(
1544        &self,
1545        f: &mut fmt::Formatter<'_>,
1546        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1547    ) -> fmt::Result {
1548        // NB this code path should not be live, as fmt_default_text for
1549        // `DeltaPathPlan` prints out each stage already
1550        let lookup_relation = &self.lookup_relation;
1551        let lookup_key = CompactScalarSeq(&self.lookup_key);
1552        writeln!(
1553            f,
1554            "{}Lookup key {lookup_key} in %{lookup_relation}",
1555            ctx.indent
1556        )
1557    }
1558
1559    fn fmt_verbose_text(
1560        &self,
1561        f: &mut fmt::Formatter<'_>,
1562        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1563    ) -> fmt::Result {
1564        let mode = HumanizedExplain::new(ctx.config.redacted);
1565        let plan = self;
1566        if !plan.closure.is_identity() {
1567            writeln!(f, "{}closure", ctx.indent)?;
1568            ctx.indented(|ctx| plan.closure.fmt_text(f, ctx))?;
1569        }
1570        {
1571            let lookup_relation = &plan.lookup_relation;
1572            let lookup_key = mode.seq(&plan.lookup_key, None);
1573            let lookup_key = CompactScalars(lookup_key);
1574            writeln!(
1575                f,
1576                "{}lookup={{ relation={}, key=[{}] }}",
1577                ctx.indent, lookup_relation, lookup_key
1578            )?;
1579        }
1580        {
1581            let stream_key = mode.seq(&plan.stream_key, None);
1582            let stream_key = CompactScalars(stream_key);
1583            let stream_thinning = mode.seq(&plan.stream_thinning, None);
1584            let stream_thinning = CompactScalars(stream_thinning);
1585            writeln!(
1586                f,
1587                "{}stream={{ key=[{}], thinning=({}) }}",
1588                ctx.indent, stream_key, stream_thinning
1589            )?;
1590        }
1591        Ok(())
1592    }
1593}
1594
1595impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for JoinClosure {
1596    fn fmt_text(
1597        &self,
1598        f: &mut fmt::Formatter<'_>,
1599        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1600    ) -> fmt::Result {
1601        if ctx.config.verbose_syntax {
1602            self.fmt_verbose_text(f, ctx)
1603        } else {
1604            self.fmt_default_text(f, ctx)
1605        }
1606    }
1607}
1608impl JoinClosure {
1609    fn fmt_default_text(
1610        &self,
1611        f: &mut fmt::Formatter<'_>,
1612        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1613    ) -> fmt::Result {
1614        let mode = HumanizedExplain::new(ctx.config.redacted);
1615        if !self.before.is_identity() {
1616            mode.expr(self.before.deref(), None)
1617                .fmt_default_text(f, ctx)?;
1618        }
1619        if !self.ready_equivalences.is_empty() {
1620            let equivalences = separated(
1621                " AND ",
1622                self.ready_equivalences
1623                    .iter()
1624                    .map(|equivalence| separated(" = ", mode.seq(equivalence, None))),
1625            );
1626            writeln!(f, "{}Equivalences: {equivalences}", ctx.indent)?;
1627        }
1628        Ok(())
1629    }
1630
1631    fn fmt_verbose_text(
1632        &self,
1633        f: &mut fmt::Formatter<'_>,
1634        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1635    ) -> fmt::Result {
1636        let mode = HumanizedExplain::new(ctx.config.redacted);
1637        mode.expr(self.before.deref(), None).fmt_text(f, ctx)?;
1638        if !self.ready_equivalences.is_empty() {
1639            let equivalences = separated(
1640                " AND ",
1641                self.ready_equivalences
1642                    .iter()
1643                    .map(|equivalence| separated(" = ", mode.seq(equivalence, None))),
1644            );
1645            writeln!(f, "{}ready_equivalences={}", ctx.indent, equivalences)?;
1646        }
1647        Ok(())
1648    }
1649}
1650
1651impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for AccumulablePlan {
1652    fn fmt_text(
1653        &self,
1654        f: &mut fmt::Formatter<'_>,
1655        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1656    ) -> fmt::Result {
1657        if ctx.config.verbose_syntax {
1658            self.fmt_verbose_text(f, ctx)
1659        } else {
1660            self.fmt_default_text(f, ctx)
1661        }
1662    }
1663}
1664impl AccumulablePlan {
1665    #[allow(clippy::needless_pass_by_ref_mut)]
1666    fn fmt_default_text(
1667        &self,
1668        f: &mut fmt::Formatter<'_>,
1669        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1670    ) -> fmt::Result {
1671        let mode = HumanizedExplain::new(ctx.config.redacted);
1672
1673        if !self.simple_aggrs.is_empty() {
1674            let simple_aggrs = self
1675                .simple_aggrs
1676                .iter()
1677                .map(|(_i_datum, agg)| mode.expr(agg, None));
1678            let simple_aggrs = separated(", ", simple_aggrs);
1679            writeln!(f, "{}Simple aggregates: {simple_aggrs}", ctx.indent)?;
1680        }
1681
1682        if !self.distinct_aggrs.is_empty() {
1683            let distinct_aggrs = self
1684                .distinct_aggrs
1685                .iter()
1686                .map(|(_i_datum, agg)| mode.expr(agg, None));
1687            let distinct_aggrs = separated(", ", distinct_aggrs);
1688            writeln!(f, "{}Distinct aggregates: {distinct_aggrs}", ctx.indent)?;
1689        }
1690        Ok(())
1691    }
1692
1693    #[allow(clippy::needless_pass_by_ref_mut)]
1694    fn fmt_verbose_text(
1695        &self,
1696        f: &mut fmt::Formatter<'_>,
1697        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1698    ) -> fmt::Result {
1699        let mode = HumanizedExplain::new(ctx.config.redacted);
1700        // full_aggrs (skipped because they are repeated in simple_aggrs ∪ distinct_aggrs)
1701        // for (i, aggr) in self.full_aggrs.iter().enumerate() {
1702        //     write!(f, "{}full_aggrs[{}]=", ctx.indent, i)?;
1703        //     aggr.fmt_text(f, &mut ())?;
1704        //     writeln!(f)?;
1705        // }
1706        // simple_aggrs
1707        for (i, (i_datum, agg)) in self.simple_aggrs.iter().enumerate() {
1708            let agg = mode.expr(agg, None);
1709            write!(f, "{}simple_aggrs[{}]=", ctx.indent, i)?;
1710            writeln!(f, "({}, {})", i_datum, agg)?;
1711        }
1712        // distinct_aggrs
1713        for (i, (i_datum, agg)) in self.distinct_aggrs.iter().enumerate() {
1714            let agg = mode.expr(agg, None);
1715            write!(f, "{}distinct_aggrs[{}]=", ctx.indent, i)?;
1716            writeln!(f, "({}, {})", i_datum, agg)?;
1717        }
1718        Ok(())
1719    }
1720}
1721
1722impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for HierarchicalPlan {
1723    fn fmt_text(
1724        &self,
1725        f: &mut fmt::Formatter<'_>,
1726        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1727    ) -> fmt::Result {
1728        if ctx.config.verbose_syntax {
1729            self.fmt_verbose_text(f, ctx)
1730        } else {
1731            self.fmt_default_text(f, ctx)
1732        }
1733    }
1734}
1735impl HierarchicalPlan {
1736    #[allow(clippy::needless_pass_by_ref_mut)]
1737    fn fmt_default_text(
1738        &self,
1739        f: &mut fmt::Formatter<'_>,
1740        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1741    ) -> fmt::Result {
1742        let mode = HumanizedExplain::new(ctx.config.redacted);
1743        let aggr_funcs = mode.seq(self.aggr_funcs(), None);
1744        let aggr_funcs = separated(", ", aggr_funcs);
1745        writeln!(f, "{}Aggregations: {aggr_funcs}", ctx.indent)
1746    }
1747
1748    #[allow(clippy::needless_pass_by_ref_mut)]
1749    fn fmt_verbose_text(
1750        &self,
1751        f: &mut fmt::Formatter<'_>,
1752        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1753    ) -> fmt::Result {
1754        let mode = HumanizedExplain::new(ctx.config.redacted);
1755        match self {
1756            HierarchicalPlan::Monotonic(plan) => {
1757                let aggr_funcs = mode.seq(&plan.aggr_funcs, None);
1758                let aggr_funcs = separated(", ", aggr_funcs);
1759                writeln!(f, "{}aggr_funcs=[{}]", ctx.indent, aggr_funcs)?;
1760                writeln!(f, "{}monotonic", ctx.indent)?;
1761                if plan.must_consolidate {
1762                    writeln!(f, "{}must_consolidate", ctx.indent)?;
1763                }
1764            }
1765            HierarchicalPlan::Bucketed(plan) => {
1766                let aggr_funcs = mode.seq(&plan.aggr_funcs, None);
1767                let aggr_funcs = separated(", ", aggr_funcs);
1768                writeln!(f, "{}aggr_funcs=[{}]", ctx.indent, aggr_funcs)?;
1769                let buckets = separated(", ", &plan.buckets);
1770                writeln!(f, "{}buckets=[{}]", ctx.indent, buckets)?;
1771            }
1772        }
1773        Ok(())
1774    }
1775}
1776
1777impl DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for BasicPlan {
1778    fn fmt_text(
1779        &self,
1780        f: &mut fmt::Formatter<'_>,
1781        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1782    ) -> fmt::Result {
1783        if ctx.config.verbose_syntax {
1784            self.fmt_verbose_text(f, ctx)
1785        } else {
1786            self.fmt_default_text(f, ctx)
1787        }
1788    }
1789}
1790impl BasicPlan {
1791    #[allow(clippy::needless_pass_by_ref_mut)]
1792    fn fmt_default_text(
1793        &self,
1794        f: &mut fmt::Formatter<'_>,
1795        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1796    ) -> fmt::Result {
1797        let mode = HumanizedExplain::new(ctx.config.redacted);
1798        match self {
1799            BasicPlan::Single(SingleBasicPlan {
1800                expr,
1801                fused_unnest_list: _,
1802            }) => {
1803                let agg = mode.expr(expr, None);
1804                writeln!(f, "{}Aggregation: {agg}", ctx.indent)?;
1805            }
1806            BasicPlan::Multiple(aggs) => {
1807                let mode = HumanizedExplain::new(ctx.config.redacted);
1808                write!(f, "{}Aggregations:", ctx.indent)?;
1809
1810                for agg in aggs.iter() {
1811                    let agg = mode.expr(agg, None);
1812                    write!(f, " {agg}")?;
1813                }
1814                writeln!(f)?;
1815            }
1816        }
1817        Ok(())
1818    }
1819
1820    #[allow(clippy::needless_pass_by_ref_mut)]
1821    fn fmt_verbose_text(
1822        &self,
1823        f: &mut fmt::Formatter<'_>,
1824        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1825    ) -> fmt::Result {
1826        let mode = HumanizedExplain::new(ctx.config.redacted);
1827        match self {
1828            BasicPlan::Single(SingleBasicPlan {
1829                expr,
1830                fused_unnest_list,
1831            }) => {
1832                let agg = mode.expr(expr, None);
1833                let fused_unnest_list = if *fused_unnest_list {
1834                    ", fused_unnest_list=true"
1835                } else {
1836                    ""
1837                };
1838                writeln!(f, "{}aggr=({}{})", ctx.indent, agg, fused_unnest_list)?;
1839            }
1840            BasicPlan::Multiple(aggs) => {
1841                for (i, agg) in aggs.iter().enumerate() {
1842                    let agg = mode.expr(agg, None);
1843                    writeln!(f, "{}aggrs[{}]={}", ctx.indent, i, agg)?;
1844                }
1845            }
1846        }
1847        Ok(())
1848    }
1849}
1850
1851/// Helper struct for rendering an arrangement.
1852struct Arrangement<'a> {
1853    key: &'a Vec<LirScalarExpr>,
1854    permutation: Permutation<'a>,
1855    thinning: &'a Vec<usize>,
1856}
1857
1858impl<'a> From<&'a (Vec<LirScalarExpr>, Vec<usize>, Vec<usize>)> for Arrangement<'a> {
1859    fn from(
1860        (key, permutation, thinning): &'a (Vec<LirScalarExpr>, Vec<usize>, Vec<usize>),
1861    ) -> Self {
1862        Arrangement {
1863            key,
1864            permutation: Permutation(permutation),
1865            thinning,
1866        }
1867    }
1868}
1869
1870impl<'a> DisplayText<PlanRenderingContext<'_, LirRelationExpr>> for Arrangement<'a> {
1871    fn fmt_text(
1872        &self,
1873        f: &mut fmt::Formatter<'_>,
1874        ctx: &mut PlanRenderingContext<'_, LirRelationExpr>,
1875    ) -> fmt::Result {
1876        if ctx.config.verbose_syntax {
1877            self.fmt_verbose_text(f, ctx)
1878        } else {
1879            self.fmt_default_text(f, ctx)
1880        }
1881    }
1882}
1883
1884impl<'a> Arrangement<'a> {
1885    #[allow(clippy::needless_pass_by_ref_mut)]
1886    fn fmt_default_text(
1887        &self,
1888        f: &mut fmt::Formatter<'_>,
1889        ctx: &PlanRenderingContext<'_, LirRelationExpr>,
1890    ) -> fmt::Result {
1891        let mode = HumanizedExplain::new(ctx.config.redacted);
1892        if !self.key.is_empty() {
1893            let key = mode.seq(self.key, None);
1894            let key = CompactScalars(key);
1895            write!(f, "{key}")
1896        } else {
1897            write!(f, "(empty key)")
1898        }
1899    }
1900
1901    #[allow(clippy::needless_pass_by_ref_mut)]
1902    fn fmt_verbose_text(
1903        &self,
1904        f: &mut fmt::Formatter<'_>,
1905        ctx: &PlanRenderingContext<'_, LirRelationExpr>,
1906    ) -> fmt::Result {
1907        let mode = HumanizedExplain::new(ctx.config.redacted);
1908        // prepare key
1909        let key = mode.seq(self.key, None);
1910        let key = CompactScalars(key);
1911        // prepare perumation map
1912        let permutation = &self.permutation;
1913        // prepare thinning
1914        let thinning = Indices(self.thinning);
1915        // write the arrangement spec
1916        write!(
1917            f,
1918            "{{ key=[{}], permutation={}, thinning=({}) }}",
1919            key, permutation, thinning
1920        )
1921    }
1922}
1923
1924/// Helper struct for rendering a permutation.
1925struct Permutation<'a>(&'a Vec<usize>);
1926
1927impl<'a> fmt::Display for Permutation<'a> {
1928    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1929        let mut pairs = vec![];
1930        for (x, y) in self.0.iter().enumerate().filter(|(x, y)| x != *y) {
1931            pairs.push(format!("#{}: #{}", x, y));
1932        }
1933
1934        if pairs.len() > 0 {
1935            write!(f, "{{{}}}", separated(", ", pairs))
1936        } else {
1937            write!(f, "id")
1938        }
1939    }
1940}
1941
1942/// Annotations for physical plans.
1943struct PlanAnnotations {
1944    config: ExplainConfig,
1945    node_id: LirId,
1946}
1947
1948// The current implementation deviates from the `AnnotatedPlan` used in `Mir~`-based plans. This is
1949// fine, since at the moment the only attribute we are going to explain is the `node_id`, which at
1950// the moment is kept inline with the `LirRelationExpr` variants. If at some point in the future we want to
1951// start deriving and printing attributes that are derived ad-hoc, however, we might want to adopt
1952// `AnnotatedPlan` here as well.
1953impl PlanAnnotations {
1954    fn new(config: ExplainConfig, plan: &LirRelationExpr) -> Self {
1955        let node_id = plan.lir_id;
1956        Self { config, node_id }
1957    }
1958}
1959
1960impl fmt::Display for PlanAnnotations {
1961    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1962        if self.config.node_ids {
1963            f.debug_struct(" //")
1964                .field("node_id", &self.node_id)
1965                .finish()
1966        } else {
1967            // No physical plan annotations enabled.
1968            Ok(())
1969        }
1970    }
1971}