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