Skip to main content

datafusion_physical_plan/
display.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Implementation of physical plan display. See
19//! [`crate::displayable`] for examples of how to format
20
21use std::collections::{BTreeMap, HashMap};
22use std::fmt;
23use std::fmt::Formatter;
24use std::time::Duration;
25
26use arrow::datatypes::SchemaRef;
27
28use datafusion_common::display::{GraphvizBuilder, PlanType, StringifiedPlan};
29use datafusion_expr::display_schema;
30use datafusion_physical_expr::LexOrdering;
31
32use crate::metrics::{MetricCategory, MetricType, MetricValue};
33use crate::render_tree::RenderTree;
34
35use crate::statistics::{StatisticsArgs, StatisticsContext};
36
37use super::{ExecutionPlan, ExecutionPlanVisitor, accept};
38
39/// Options for controlling how each [`ExecutionPlan`] should format itself
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub enum DisplayFormatType {
42    /// Default, compact format. Example: `FilterExec: c12 < 10.0`
43    ///
44    /// This format is designed to provide a detailed textual description
45    /// of all parts of the plan.
46    Default,
47    /// Verbose, showing all available details.
48    ///
49    /// This form is even more detailed than [`Self::Default`]
50    Verbose,
51    /// TreeRender, displayed in the `tree` explain type.
52    ///
53    /// This format is inspired by DuckDB's explain plans. The information
54    /// presented should be "user friendly", and contain only the most relevant
55    /// information for understanding a plan. It should NOT contain the same level
56    /// of detail information as the  [`Self::Default`] format.
57    ///
58    /// In this mode, each line has one of two formats:
59    ///
60    /// 1. A string without a `=`, which is printed in its own line
61    ///
62    /// 2. A string with a `=` that is treated as a `key=value pair`. Everything
63    ///    before the first `=` is treated as the key, and everything after the
64    ///    first `=` is treated as the value.
65    ///
66    /// For example, if the output of `TreeRender` is this:
67    /// ```text
68    /// Parquet
69    /// partition_sizes=[1]
70    /// ```
71    ///
72    /// It is rendered in the center of a box in the following way:
73    ///
74    /// ```text
75    /// ┌───────────────────────────┐
76    /// │       DataSourceExec      │
77    /// │    --------------------   │
78    /// │    partition_sizes: [1]   │
79    /// │          Parquet          │
80    /// └───────────────────────────┘
81    /// ```
82    TreeRender,
83}
84
85/// Wraps an `ExecutionPlan` with various methods for formatting
86///
87///
88/// # Example
89/// ```
90/// # use std::sync::Arc;
91/// # use arrow::datatypes::{Field, Schema, DataType};
92/// # use datafusion_expr::Operator;
93/// # use datafusion_physical_expr::expressions::{binary, col, lit};
94/// # use datafusion_physical_plan::{displayable, ExecutionPlan};
95/// # use datafusion_physical_plan::empty::EmptyExec;
96/// # use datafusion_physical_plan::filter::FilterExec;
97/// # let schema = Schema::new(vec![Field::new("i", DataType::Int32, false)]);
98/// # let plan = EmptyExec::new(Arc::new(schema));
99/// # let i = col("i", &plan.schema()).unwrap();
100/// # let predicate = binary(i, Operator::Eq, lit(1), &plan.schema()).unwrap();
101/// # let plan: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(predicate, Arc::new(plan)).unwrap());
102/// // Get a one line description (Displayable)
103/// let display_plan = displayable(plan.as_ref());
104///
105/// // you can use the returned objects to format plans
106/// // where you can use `Display` such as  format! or println!
107/// assert_eq!(
108///    &format!("The plan is: {}", display_plan.one_line()),
109///   "The plan is: FilterExec: i@0 = 1\n"
110/// );
111/// // You can also print out the plan and its children in indented mode
112/// assert_eq!(display_plan.indent(false).to_string(),
113///   "FilterExec: i@0 = 1\
114///   \n  EmptyExec\
115///   \n"
116/// );
117/// ```
118#[derive(Debug, Clone)]
119pub struct DisplayableExecutionPlan<'a> {
120    inner: &'a dyn ExecutionPlan,
121    /// How to show metrics
122    show_metrics: ShowMetrics,
123    /// If statistics should be displayed
124    show_statistics: bool,
125    /// If schema should be displayed. See [`Self::set_show_schema`]
126    show_schema: bool,
127    /// Which metric categories should be included when rendering
128    metric_types: Vec<MetricType>,
129    /// Optional filter by semantic category (rows / bytes / timing).
130    /// `None` means show all categories; `Some(vec![])` means plan-only.
131    metric_categories: Option<Vec<MetricCategory>>,
132    /// Optional filter by metric names. Only metric names in this list
133    /// will be rendered.
134    metric_names: Option<Vec<String>>,
135    // (TreeRender) Maximum total width of the rendered tree
136    tree_maximum_render_width: usize,
137    /// Optional summary totals (currently only used by `pgjson`) — the total
138    /// row count and wall-clock duration of the `AnalyzeExec` execution.
139    summary: Option<AnalyzeSummary>,
140}
141
142/// Summary information attached to the root of an `EXPLAIN ANALYZE`
143/// pgjson render.
144#[derive(Debug, Clone, Copy)]
145struct AnalyzeSummary {
146    total_rows: Option<usize>,
147    duration: Option<Duration>,
148}
149
150impl<'a> DisplayableExecutionPlan<'a> {
151    fn default_metric_types() -> Vec<MetricType> {
152        vec![MetricType::Summary, MetricType::Dev]
153    }
154
155    /// Create a wrapper around an [`ExecutionPlan`] which can be
156    /// pretty printed in a variety of ways
157    pub fn new(inner: &'a dyn ExecutionPlan) -> Self {
158        Self {
159            inner,
160            show_metrics: ShowMetrics::None,
161            show_statistics: false,
162            show_schema: false,
163            metric_types: Self::default_metric_types(),
164            metric_categories: None,
165            metric_names: None,
166            tree_maximum_render_width: 240,
167            summary: None,
168        }
169    }
170
171    /// Create a wrapper around an [`ExecutionPlan`] which can be
172    /// pretty printed in a variety of ways that also shows aggregated
173    /// metrics
174    pub fn with_metrics(inner: &'a dyn ExecutionPlan) -> Self {
175        Self {
176            inner,
177            show_metrics: ShowMetrics::Aggregated,
178            show_statistics: false,
179            show_schema: false,
180            metric_types: Self::default_metric_types(),
181            metric_categories: None,
182            metric_names: None,
183            tree_maximum_render_width: 240,
184            summary: None,
185        }
186    }
187
188    /// Create a wrapper around an [`ExecutionPlan`] which can be
189    /// pretty printed in a variety of ways that also shows all low
190    /// level metrics
191    pub fn with_full_metrics(inner: &'a dyn ExecutionPlan) -> Self {
192        Self {
193            inner,
194            show_metrics: ShowMetrics::Full,
195            show_statistics: false,
196            show_schema: false,
197            metric_types: Self::default_metric_types(),
198            metric_categories: None,
199            metric_names: None,
200            tree_maximum_render_width: 240,
201            summary: None,
202        }
203    }
204
205    /// Enable display of schema
206    ///
207    /// If true, plans will be displayed with schema information at the end
208    /// of each line. The format is `schema=[[a:Int32;N, b:Int32;N, c:Int32;N]]`
209    pub fn set_show_schema(mut self, show_schema: bool) -> Self {
210        self.show_schema = show_schema;
211        self
212    }
213
214    /// Enable display of statistics
215    pub fn set_show_statistics(mut self, show_statistics: bool) -> Self {
216        self.show_statistics = show_statistics;
217        self
218    }
219
220    /// Specify which metric types should be rendered alongside the plan
221    pub fn set_metric_types(mut self, metric_types: Vec<MetricType>) -> Self {
222        self.metric_types = metric_types;
223        self
224    }
225
226    /// Specify which metric categories to include.
227    ///
228    /// - `None` means show all categories (default).
229    /// - `Some(vec![])` means plan-only — suppress all metrics.
230    /// - `Some(vec![Rows])` means show only row-count metrics (plus
231    ///   uncategorized metrics).
232    ///
233    /// See [`MetricCategory`] for the determinism properties of each
234    /// category.
235    pub fn set_metric_categories(
236        mut self,
237        metric_categories: Option<Vec<MetricCategory>>,
238    ) -> Self {
239        self.metric_categories = metric_categories;
240        self
241    }
242
243    /// Specify which metric names to include.
244    ///
245    /// - An empty vector means plan-only — suppress all metrics.
246    /// - `vec!["metric_1"]` means show only the metric named `metric_1`.
247    ///
248    /// Name filtering is intersected with other types of filters, like metric
249    /// category and metric type.
250    pub fn set_metric_names(mut self, metric_names: Vec<String>) -> Self {
251        self.metric_names = Some(metric_names);
252        self
253    }
254
255    /// Set the maximum render width for the tree format
256    pub fn set_tree_maximum_render_width(mut self, width: usize) -> Self {
257        self.tree_maximum_render_width = width;
258        self
259    }
260
261    /// Attach an `EXPLAIN ANALYZE` summary (total output rows and duration)
262    /// to the rendered output. Currently only used by [`Self::pgjson`], which
263    /// serializes the summary alongside the root plan object.
264    pub fn set_summary(
265        mut self,
266        total_rows: Option<usize>,
267        duration: Option<Duration>,
268    ) -> Self {
269        self.summary = Some(AnalyzeSummary {
270            total_rows,
271            duration,
272        });
273        self
274    }
275
276    /// Return a `format`able structure that produces a single line
277    /// per node.
278    ///
279    /// ```text
280    /// ProjectionExec: expr=[a]
281    ///   CoalesceBatchesExec: target_batch_size=8192
282    ///     FilterExec: a < 5
283    ///       RepartitionExec: partitioning=RoundRobinBatch(16)
284    ///         DataSourceExec: source=...",
285    /// ```
286    pub fn indent(&self, verbose: bool) -> impl fmt::Display + 'a {
287        let format_type = if verbose {
288            DisplayFormatType::Verbose
289        } else {
290            DisplayFormatType::Default
291        };
292        struct Wrapper<'a> {
293            format_type: DisplayFormatType,
294            plan: &'a dyn ExecutionPlan,
295            show_metrics: ShowMetrics,
296            show_statistics: bool,
297            show_schema: bool,
298            metric_types: Vec<MetricType>,
299            metric_categories: Option<Vec<MetricCategory>>,
300            metric_names: Option<Vec<String>>,
301        }
302        impl fmt::Display for Wrapper<'_> {
303            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
304                let mut visitor = IndentVisitor {
305                    t: self.format_type,
306                    f,
307                    indent: 0,
308                    show_metrics: self.show_metrics,
309                    show_statistics: self.show_statistics,
310                    show_schema: self.show_schema,
311                    metric_types: &self.metric_types,
312                    metric_categories: self.metric_categories.as_deref(),
313                    metric_names: self.metric_names.as_deref(),
314                };
315                accept(self.plan, &mut visitor)
316            }
317        }
318        Wrapper {
319            format_type,
320            plan: self.inner,
321            show_metrics: self.show_metrics,
322            show_statistics: self.show_statistics,
323            show_schema: self.show_schema,
324            metric_types: self.metric_types.clone(),
325            metric_categories: self.metric_categories.clone(),
326            metric_names: self.metric_names.clone(),
327        }
328    }
329
330    /// Returns a `format`able structure that produces graphviz format for execution plan, which can
331    /// be directly visualized [here](https://dreampuf.github.io/GraphvizOnline).
332    ///
333    /// An example is
334    /// ```dot
335    /// strict digraph dot_plan {
336    //     0[label="ProjectionExec: expr=[id@0 + 2 as employee.id + Int32(2)]",tooltip=""]
337    //     1[label="EmptyExec",tooltip=""]
338    //     0 -> 1
339    // }
340    /// ```
341    pub fn graphviz(&self) -> impl fmt::Display + 'a {
342        struct Wrapper<'a> {
343            plan: &'a dyn ExecutionPlan,
344            show_metrics: ShowMetrics,
345            show_statistics: bool,
346            metric_types: Vec<MetricType>,
347            metric_categories: Option<Vec<MetricCategory>>,
348            metric_names: Option<Vec<String>>,
349        }
350        impl fmt::Display for Wrapper<'_> {
351            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
352                let t = DisplayFormatType::Default;
353
354                let mut visitor = GraphvizVisitor {
355                    f,
356                    t,
357                    show_metrics: self.show_metrics,
358                    show_statistics: self.show_statistics,
359                    metric_types: &self.metric_types,
360                    metric_categories: self.metric_categories.as_deref(),
361                    metric_names: self.metric_names.as_deref(),
362                    graphviz_builder: GraphvizBuilder::default(),
363                    parents: Vec::new(),
364                };
365
366                visitor.start_graph()?;
367
368                accept(self.plan, &mut visitor)?;
369
370                visitor.end_graph()?;
371                Ok(())
372            }
373        }
374
375        Wrapper {
376            plan: self.inner,
377            show_metrics: self.show_metrics,
378            show_statistics: self.show_statistics,
379            metric_types: self.metric_types.clone(),
380            metric_categories: self.metric_categories.clone(),
381            metric_names: self.metric_names.clone(),
382        }
383    }
384
385    /// Formats the plan using a ASCII art like tree
386    ///
387    /// See [`DisplayFormatType::TreeRender`] for more details.
388    pub fn tree_render(&self) -> impl fmt::Display + 'a {
389        struct Wrapper<'a> {
390            plan: &'a dyn ExecutionPlan,
391            maximum_render_width: usize,
392        }
393        impl fmt::Display for Wrapper<'_> {
394            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
395                let mut visitor = TreeRenderVisitor {
396                    f,
397                    maximum_render_width: self.maximum_render_width,
398                };
399                visitor.visit(self.plan)
400            }
401        }
402        Wrapper {
403            plan: self.inner,
404            maximum_render_width: self.tree_maximum_render_width,
405        }
406    }
407
408    /// Returns a `format`able structure that produces PostgreSQL-style JSON
409    /// output, mirroring the logical-plan pgjson format.
410    ///
411    /// Each node is rendered as a JSON object with:
412    /// - `"Node Type"` — `ExecutionPlan::name()`
413    /// - `"Details"` — the one-line `DisplayAs::Default` rendering
414    /// - `"Output"` — schema column names (when `set_show_schema(true)`)
415    /// - `"Actual Rows"` / `"Actual Total Time"` — PG-canonical metric keys
416    ///   populated from `output_rows` / `elapsed_compute` when available
417    /// - `"Extras"` — remaining metrics keyed by DataFusion metric name
418    /// - `"Plans"` — array of child nodes
419    ///
420    /// When a summary has been set via [`Self::set_summary`], `"Total Rows"`
421    /// and `"Duration"` fields are attached at the root.
422    pub fn pgjson(&self, verbose: bool) -> impl fmt::Display + 'a {
423        struct Wrapper<'a> {
424            plan: &'a dyn ExecutionPlan,
425            verbose: bool,
426            show_metrics: ShowMetrics,
427            show_schema: bool,
428            metric_types: Vec<MetricType>,
429            metric_categories: Option<Vec<MetricCategory>>,
430            metric_names: Option<Vec<String>>,
431            summary: Option<AnalyzeSummary>,
432        }
433        impl fmt::Display for Wrapper<'_> {
434            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
435                let mut visitor = PgJsonExecutionPlanVisitor {
436                    verbose: self.verbose,
437                    show_metrics: self.show_metrics,
438                    show_schema: self.show_schema,
439                    metric_types: &self.metric_types,
440                    metric_categories: self.metric_categories.as_deref(),
441                    metric_names: self.metric_names.as_deref(),
442                    objects: HashMap::new(),
443                    parent_ids: Vec::new(),
444                    next_id: 0,
445                    root: None,
446                };
447                accept(self.plan, &mut visitor).map_err(|_| fmt::Error)?;
448                let root = visitor.root.ok_or(fmt::Error)?;
449                let mut root_entry = serde_json::json!({ "Plan": root });
450                if let Some(summary) = self.summary {
451                    if let Some(total_rows) = summary.total_rows {
452                        root_entry["Total Rows"] = serde_json::Value::from(total_rows);
453                    }
454                    if let Some(duration) = summary.duration {
455                        root_entry["Duration"] =
456                            serde_json::Value::from(format!("{duration:?}"));
457                    }
458                }
459                let doc = serde_json::Value::Array(vec![root_entry]);
460                write!(
461                    f,
462                    "{}",
463                    serde_json::to_string_pretty(&doc).map_err(|_| fmt::Error)?
464                )
465            }
466        }
467
468        Wrapper {
469            plan: self.inner,
470            verbose,
471            show_metrics: self.show_metrics,
472            show_schema: self.show_schema,
473            metric_types: self.metric_types.clone(),
474            metric_categories: self.metric_categories.clone(),
475            metric_names: self.metric_names.clone(),
476            summary: self.summary,
477        }
478    }
479
480    /// Return a single-line summary of the root of the plan
481    /// Example: `ProjectionExec: expr=[a@0 as a]`.
482    pub fn one_line(&self) -> impl fmt::Display + 'a {
483        struct Wrapper<'a> {
484            plan: &'a dyn ExecutionPlan,
485            show_metrics: ShowMetrics,
486            show_statistics: bool,
487            show_schema: bool,
488            metric_types: Vec<MetricType>,
489            metric_categories: Option<Vec<MetricCategory>>,
490            metric_names: Option<Vec<String>>,
491        }
492
493        impl fmt::Display for Wrapper<'_> {
494            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
495                let mut visitor = IndentVisitor {
496                    f,
497                    t: DisplayFormatType::Default,
498                    indent: 0,
499                    show_metrics: self.show_metrics,
500                    show_statistics: self.show_statistics,
501                    show_schema: self.show_schema,
502                    metric_types: &self.metric_types,
503                    metric_categories: self.metric_categories.as_deref(),
504                    metric_names: self.metric_names.as_deref(),
505                };
506                visitor.pre_visit(self.plan)?;
507                Ok(())
508            }
509        }
510
511        Wrapper {
512            plan: self.inner,
513            show_metrics: self.show_metrics,
514            show_statistics: self.show_statistics,
515            show_schema: self.show_schema,
516            metric_types: self.metric_types.clone(),
517            metric_categories: self.metric_categories.clone(),
518            metric_names: self.metric_names.clone(),
519        }
520    }
521
522    #[deprecated(since = "47.0.0", note = "indent() or tree_render() instead")]
523    pub fn to_stringified(
524        &self,
525        verbose: bool,
526        plan_type: PlanType,
527        explain_format: DisplayFormatType,
528    ) -> StringifiedPlan {
529        match (&explain_format, &plan_type) {
530            (DisplayFormatType::TreeRender, PlanType::FinalPhysicalPlan) => {
531                StringifiedPlan::new(plan_type, self.tree_render().to_string())
532            }
533            _ => StringifiedPlan::new(plan_type, self.indent(verbose).to_string()),
534        }
535    }
536}
537
538/// Enum representing the different levels of metrics to display
539#[derive(Debug, Clone, Copy)]
540enum ShowMetrics {
541    /// Do not show any metrics
542    None,
543
544    /// Show aggregated metrics across partition
545    Aggregated,
546
547    /// Show full per-partition metrics
548    Full,
549}
550
551/// Formats plans with a single line per node.
552///
553/// # Example
554///
555/// ```text
556/// ProjectionExec: expr=[column1@0 + 2 as column1 + Int64(2)]
557///   FilterExec: column1@0 = 5
558///     ValuesExec
559/// ```
560struct IndentVisitor<'a, 'b> {
561    /// How to format each node
562    t: DisplayFormatType,
563    /// Write to this formatter
564    f: &'a mut Formatter<'b>,
565    /// Indent size
566    indent: usize,
567    /// How to show metrics
568    show_metrics: ShowMetrics,
569    /// If statistics should be displayed
570    show_statistics: bool,
571    /// If schema should be displayed
572    show_schema: bool,
573    /// Which metric types should be rendered
574    metric_types: &'a [MetricType],
575    /// Optional filter by semantic category (rows / bytes / timing).
576    metric_categories: Option<&'a [MetricCategory]>,
577    /// Optional filter by metric name.
578    metric_names: Option<&'a [String]>,
579}
580
581impl ExecutionPlanVisitor for IndentVisitor<'_, '_> {
582    type Error = fmt::Error;
583    fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
584        write!(self.f, "{:indent$}", "", indent = self.indent * 2)?;
585        plan.fmt_as(self.t, self.f)?;
586        match self.show_metrics {
587            ShowMetrics::None => {}
588            ShowMetrics::Aggregated => {
589                if let Some(metrics) = plan.metrics() {
590                    let mut metrics = metrics
591                        .filter_by_metric_types(self.metric_types)
592                        .aggregate_by_name()
593                        .sorted_for_display()
594                        .timestamps_removed();
595                    if let Some(cats) = self.metric_categories {
596                        metrics = metrics.filter_by_categories(cats);
597                    }
598                    if let Some(names) = self.metric_names {
599                        metrics = metrics.filter_by_names(names);
600                    }
601                    write!(self.f, ", metrics=[{metrics}]")?;
602                } else {
603                    write!(self.f, ", metrics=[]")?;
604                }
605            }
606            ShowMetrics::Full => {
607                if let Some(metrics) = plan.metrics() {
608                    let mut metrics = metrics.filter_by_metric_types(self.metric_types);
609                    if let Some(cats) = self.metric_categories {
610                        metrics = metrics.filter_by_categories(cats);
611                    }
612                    if let Some(names) = self.metric_names {
613                        metrics = metrics.filter_by_names(names);
614                    }
615                    write!(self.f, ", metrics=[{metrics}]")?;
616                } else {
617                    write!(self.f, ", metrics=[]")?;
618                }
619            }
620        }
621        if self.show_statistics {
622            let stats = StatisticsContext::new()
623                .compute(plan, &StatisticsArgs::new())
624                .map_err(|_e| fmt::Error)?;
625            write!(self.f, ", statistics=[{stats}]")?;
626        }
627        if self.show_schema {
628            write!(
629                self.f,
630                ", schema={}",
631                display_schema(plan.schema().as_ref())
632            )?;
633        }
634        writeln!(self.f)?;
635        self.indent += 1;
636        Ok(true)
637    }
638
639    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
640        self.indent -= 1;
641        Ok(true)
642    }
643}
644
645struct GraphvizVisitor<'a, 'b> {
646    f: &'a mut Formatter<'b>,
647    /// How to format each node
648    t: DisplayFormatType,
649    /// How to show metrics
650    show_metrics: ShowMetrics,
651    /// If statistics should be displayed
652    show_statistics: bool,
653    /// Which metric types should be rendered
654    metric_types: &'a [MetricType],
655    /// Optional filter by semantic category
656    metric_categories: Option<&'a [MetricCategory]>,
657    /// Optional filter by metric name.
658    metric_names: Option<&'a [String]>,
659
660    graphviz_builder: GraphvizBuilder,
661    /// Used to record parent node ids when visiting a plan.
662    parents: Vec<usize>,
663}
664
665impl GraphvizVisitor<'_, '_> {
666    fn start_graph(&mut self) -> fmt::Result {
667        self.graphviz_builder.start_graph(self.f)
668    }
669
670    fn end_graph(&mut self) -> fmt::Result {
671        self.graphviz_builder.end_graph(self.f)
672    }
673}
674
675impl ExecutionPlanVisitor for GraphvizVisitor<'_, '_> {
676    type Error = fmt::Error;
677
678    fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
679        let id = self.graphviz_builder.next_id();
680
681        struct Wrapper<'a>(&'a dyn ExecutionPlan, DisplayFormatType);
682
683        impl fmt::Display for Wrapper<'_> {
684            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
685                self.0.fmt_as(self.1, f)
686            }
687        }
688
689        let label = { format!("{}", Wrapper(plan, self.t)) };
690
691        let metrics = match self.show_metrics {
692            ShowMetrics::None => "".to_string(),
693            ShowMetrics::Aggregated => {
694                if let Some(metrics) = plan.metrics() {
695                    let mut metrics = metrics
696                        .filter_by_metric_types(self.metric_types)
697                        .aggregate_by_name()
698                        .sorted_for_display()
699                        .timestamps_removed();
700                    if let Some(cats) = self.metric_categories {
701                        metrics = metrics.filter_by_categories(cats);
702                    }
703                    if let Some(names) = self.metric_names {
704                        metrics = metrics.filter_by_names(names);
705                    }
706                    format!("metrics=[{metrics}]")
707                } else {
708                    "metrics=[]".to_string()
709                }
710            }
711            ShowMetrics::Full => {
712                if let Some(metrics) = plan.metrics() {
713                    let mut metrics = metrics.filter_by_metric_types(self.metric_types);
714                    if let Some(cats) = self.metric_categories {
715                        metrics = metrics.filter_by_categories(cats);
716                    }
717                    if let Some(names) = self.metric_names {
718                        metrics = metrics.filter_by_names(names);
719                    }
720                    format!("metrics=[{metrics}]")
721                } else {
722                    "metrics=[]".to_string()
723                }
724            }
725        };
726
727        let statistics = if self.show_statistics {
728            let stats = StatisticsContext::new()
729                .compute(plan, &StatisticsArgs::new())
730                .map_err(|_e| fmt::Error)?;
731            format!("statistics=[{stats}]")
732        } else {
733            "".to_string()
734        };
735
736        let delimiter = if !metrics.is_empty() && !statistics.is_empty() {
737            ", "
738        } else {
739            ""
740        };
741
742        self.graphviz_builder.add_node(
743            self.f,
744            id,
745            &label,
746            Some(&format!("{metrics}{delimiter}{statistics}")),
747        )?;
748
749        if let Some(parent_node_id) = self.parents.last() {
750            self.graphviz_builder
751                .add_edge(self.f, *parent_node_id, id)?;
752        }
753
754        self.parents.push(id);
755
756        Ok(true)
757    }
758
759    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
760        self.parents.pop();
761        Ok(true)
762    }
763}
764
765/// Formats physical plans into PostgreSQL-style JSON output with live
766/// per-operator metrics.
767///
768/// This visitor mirrors the logical-plan `PgJsonVisitor` in
769/// `datafusion-expr`: during `pre_visit` it assembles a JSON object for the
770/// current node; during `post_visit` it attaches that object into its
771/// parent's `"Plans"` array (or stores it as the root).
772struct PgJsonExecutionPlanVisitor<'a> {
773    verbose: bool,
774    show_metrics: ShowMetrics,
775    show_schema: bool,
776    metric_types: &'a [MetricType],
777    metric_categories: Option<&'a [MetricCategory]>,
778    metric_names: Option<&'a [String]>,
779    objects: HashMap<u32, serde_json::Value>,
780    parent_ids: Vec<u32>,
781    next_id: u32,
782    root: Option<serde_json::Value>,
783}
784
785impl PgJsonExecutionPlanVisitor<'_> {
786    /// Produce the one-line `DisplayAs::Default` rendering of a node.
787    fn one_line_details(plan: &dyn ExecutionPlan) -> String {
788        struct One<'b>(&'b dyn ExecutionPlan);
789        impl fmt::Display for One<'_> {
790            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
791                self.0.fmt_as(DisplayFormatType::Default, f)
792            }
793        }
794        // Some operators include internal newlines; collapse them so the
795        // rendered JSON value stays on a single line.
796        format!("{}", One(plan))
797            .replace('\n', " ")
798            .trim()
799            .to_string()
800    }
801
802    /// Render the given `MetricValue` into the most natural `serde_json::Value`
803    /// we can produce: a number for simple counts/gauges/times, a float-ms for
804    /// `ElapsedCompute`, and a string fallback for anything else.
805    fn metric_value_to_json(value: &MetricValue) -> serde_json::Value {
806        match value {
807            MetricValue::OutputRows(c) => serde_json::Value::from(c.value()),
808            MetricValue::SpillCount(c)
809            | MetricValue::OutputBatches(c)
810            | MetricValue::SpilledRows(c) => serde_json::Value::from(c.value()),
811            MetricValue::SpilledBytes(c) | MetricValue::OutputBytes(c) => {
812                serde_json::Value::from(c.value())
813            }
814            MetricValue::CurrentMemoryUsage(g) => serde_json::Value::from(g.value()),
815            MetricValue::ElapsedCompute(t) => {
816                // Emit as float milliseconds to align with PG's
817                // `"Actual Total Time"` convention. DataFusion tracks compute
818                // time (summed across partitions), not wall time — visualizers
819                // should be read with that caveat in mind.
820                let ms = (t.value() as f64) / 1_000_000.0;
821                serde_json::Value::from(ms)
822            }
823            MetricValue::Count { count, .. } => serde_json::Value::from(count.value()),
824            MetricValue::Gauge { gauge, .. } => serde_json::Value::from(gauge.value()),
825            MetricValue::PeakMemoryUsage { gauge, .. } => {
826                serde_json::Value::from(gauge.value())
827            }
828            MetricValue::Time { time, .. } => {
829                let ms = (time.value() as f64) / 1_000_000.0;
830                serde_json::Value::from(ms)
831            }
832            // Timestamps, PruningMetrics, Ratio, Custom: fall back to Display.
833            other => serde_json::Value::String(format!("{other}")),
834        }
835    }
836
837    /// Populate `"Actual Rows"`, `"Actual Total Time"`, and `"Extras"` for
838    /// the given node from its aggregated `MetricsSet`, honoring the same
839    /// filtering pipeline used by `IndentVisitor`.
840    fn attach_metrics(&self, plan: &dyn ExecutionPlan, object: &mut serde_json::Value) {
841        if matches!(self.show_metrics, ShowMetrics::None) {
842            return;
843        }
844        let Some(metrics) = plan.metrics() else {
845            return;
846        };
847
848        let metrics = match self.show_metrics {
849            ShowMetrics::None => return,
850            ShowMetrics::Aggregated => metrics
851                .filter_by_metric_types(self.metric_types)
852                .aggregate_by_name()
853                .sorted_for_display()
854                .timestamps_removed(),
855            ShowMetrics::Full => metrics.filter_by_metric_types(self.metric_types),
856        };
857        let metrics = if let Some(cats) = self.metric_categories {
858            metrics.filter_by_categories(cats)
859        } else {
860            metrics
861        };
862
863        let metrics = if let Some(names) = self.metric_names {
864            metrics.filter_by_names(names)
865        } else {
866            metrics
867        };
868
869        // Build the Extras bucket, while extracting PG-canonical keys to the
870        // top level.
871        let mut extras = serde_json::Map::new();
872        for metric in metrics.iter() {
873            let value = metric.value();
874            match value {
875                MetricValue::OutputRows(c) => {
876                    object["Actual Rows"] = serde_json::Value::from(c.value());
877                }
878                MetricValue::ElapsedCompute(t) => {
879                    let ms = (t.value() as f64) / 1_000_000.0;
880                    object["Actual Total Time"] = serde_json::Value::from(ms);
881                }
882                _ => {
883                    extras.insert(
884                        value.name().to_string(),
885                        Self::metric_value_to_json(value),
886                    );
887                }
888            }
889        }
890        if !extras.is_empty() {
891            object["Extras"] = serde_json::Value::Object(extras);
892        }
893    }
894}
895
896impl ExecutionPlanVisitor for PgJsonExecutionPlanVisitor<'_> {
897    type Error = fmt::Error;
898
899    fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
900        let id = self.next_id;
901        self.next_id += 1;
902
903        // Build fields in reading order: Node Type, Details, (schema),
904        // (metrics), Plans last — so the JSON output reads top-down like a
905        // PostgreSQL plan.
906        let mut object = serde_json::json!({
907            "Node Type": plan.name(),
908            "Details": Self::one_line_details(plan),
909        });
910
911        if self.show_schema || self.verbose {
912            // Always include output columns when a caller asked for schema;
913            // also include them in verbose mode so the pgjson output mirrors
914            // the extra context shown by indent's verbose flag.
915            let columns: Vec<serde_json::Value> = plan
916                .schema()
917                .fields()
918                .iter()
919                .map(|f| serde_json::Value::String(f.name().to_string()))
920                .collect();
921            object["Output"] = serde_json::Value::Array(columns);
922        }
923
924        self.attach_metrics(plan, &mut object);
925
926        object["Plans"] = serde_json::Value::Array(vec![]);
927
928        self.objects.insert(id, object);
929        self.parent_ids.push(id);
930        Ok(true)
931    }
932
933    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
934        let id = self.parent_ids.pop().ok_or(fmt::Error)?;
935        let current = self.objects.remove(&id).ok_or(fmt::Error)?;
936
937        if let Some(parent_id) = self.parent_ids.last() {
938            let parent = self.objects.get_mut(parent_id).ok_or(fmt::Error)?;
939            let plans = parent
940                .get_mut("Plans")
941                .and_then(|p| p.as_array_mut())
942                .ok_or(fmt::Error)?;
943            plans.push(current);
944        } else {
945            self.root = Some(current);
946        }
947        Ok(true)
948    }
949}
950
951/// This module implements a tree-like art renderer for execution plans,
952/// based on DuckDB's implementation:
953/// <https://github.com/duckdb/duckdb/blob/main/src/include/duckdb/common/tree_renderer/text_tree_renderer.hpp>
954///
955/// The rendered output looks like this:
956/// ```text
957/// ┌───────────────────────────┐
958/// │    CoalesceBatchesExec    │
959/// └─────────────┬─────────────┘
960/// ┌─────────────┴─────────────┐
961/// │        HashJoinExec       ├──────────────┐
962/// └─────────────┬─────────────┘              │
963/// ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
964/// │       DataSourceExec      ││       DataSourceExec      │
965/// └───────────────────────────┘└───────────────────────────┘
966/// ```
967///
968/// The renderer uses a three-layer approach for each node:
969/// 1. Top layer: renders the top borders and connections
970/// 2. Content layer: renders the node content and vertical connections
971/// 3. Bottom layer: renders the bottom borders and connections
972///
973/// Each node is rendered in a box of fixed width (NODE_RENDER_WIDTH).
974struct TreeRenderVisitor<'a, 'b> {
975    /// Write to this formatter
976    f: &'a mut Formatter<'b>,
977    /// Maximum total width of the rendered tree
978    maximum_render_width: usize,
979}
980
981impl TreeRenderVisitor<'_, '_> {
982    // Unicode box-drawing characters for creating borders and connections.
983    const LTCORNER: &'static str = "┌"; // Left top corner
984    const RTCORNER: &'static str = "┐"; // Right top corner
985    const LDCORNER: &'static str = "└"; // Left bottom corner
986    const RDCORNER: &'static str = "┘"; // Right bottom corner
987
988    const TMIDDLE: &'static str = "┬"; // Top T-junction (connects down)
989    const LMIDDLE: &'static str = "├"; // Left T-junction (connects right)
990    const DMIDDLE: &'static str = "┴"; // Bottom T-junction (connects up)
991
992    const VERTICAL: &'static str = "│"; // Vertical line
993    const HORIZONTAL: &'static str = "─"; // Horizontal line
994
995    // TODO: Make these variables configurable.
996    const NODE_RENDER_WIDTH: usize = 29; // Width of each node's box
997    const MAX_EXTRA_LINES: usize = 30; // Maximum number of extra info lines per node
998
999    /// Main entry point for rendering an execution plan as a tree.
1000    /// The rendering process happens in three stages for each level of the tree:
1001    /// 1. Render top borders and connections
1002    /// 2. Render node content and vertical connections
1003    /// 3. Render bottom borders and connections
1004    pub fn visit(&mut self, plan: &dyn ExecutionPlan) -> Result<(), fmt::Error> {
1005        let root = RenderTree::create_tree(plan);
1006
1007        for y in 0..root.height {
1008            // Start by rendering the top layer.
1009            self.render_top_layer(&root, y)?;
1010            // Now we render the content of the boxes
1011            self.render_box_content(&root, y)?;
1012            // Render the bottom layer of each of the boxes
1013            self.render_bottom_layer(&root, y)?;
1014        }
1015
1016        Ok(())
1017    }
1018
1019    /// Renders the top layer of boxes at the given y-level of the tree.
1020    /// This includes:
1021    /// - Top corners (┌─┐) for nodes
1022    /// - Horizontal connections between nodes
1023    /// - Vertical connections to parent nodes
1024    fn render_top_layer(
1025        &mut self,
1026        root: &RenderTree,
1027        y: usize,
1028    ) -> Result<(), fmt::Error> {
1029        for x in 0..root.width {
1030            if self.maximum_render_width > 0
1031                && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width
1032            {
1033                break;
1034            }
1035
1036            if root.has_node(x, y) {
1037                write!(self.f, "{}", Self::LTCORNER)?;
1038                write!(
1039                    self.f,
1040                    "{}",
1041                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
1042                )?;
1043                if y == 0 {
1044                    // top level node: no node above this one
1045                    write!(self.f, "{}", Self::HORIZONTAL)?;
1046                } else {
1047                    // render connection to node above this one
1048                    write!(self.f, "{}", Self::DMIDDLE)?;
1049                }
1050                write!(
1051                    self.f,
1052                    "{}",
1053                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
1054                )?;
1055                write!(self.f, "{}", Self::RTCORNER)?;
1056            } else {
1057                let mut has_adjacent_nodes = false;
1058                for i in 0..(root.width - x) {
1059                    has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y);
1060                }
1061                if !has_adjacent_nodes {
1062                    // There are no nodes to the right side of this position
1063                    // no need to fill the empty space
1064                    continue;
1065                }
1066                // there are nodes next to this, fill the space
1067                write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
1068            }
1069        }
1070        writeln!(self.f)?;
1071
1072        Ok(())
1073    }
1074
1075    /// Renders the content layer of boxes at the given y-level of the tree.
1076    /// This includes:
1077    /// - Node names and extra information
1078    /// - Vertical borders (│) for boxes
1079    /// - Vertical connections between nodes
1080    fn render_box_content(
1081        &mut self,
1082        root: &RenderTree,
1083        y: usize,
1084    ) -> Result<(), fmt::Error> {
1085        let mut extra_info: Vec<Vec<String>> = vec![vec![]; root.width];
1086        let mut extra_height = 0;
1087
1088        for (x, extra_info_item) in extra_info.iter_mut().enumerate().take(root.width) {
1089            if let Some(node) = root.get_node(x, y) {
1090                Self::split_up_extra_info(
1091                    &node.extra_text,
1092                    extra_info_item,
1093                    Self::MAX_EXTRA_LINES,
1094                );
1095                if extra_info_item.len() > extra_height {
1096                    extra_height = extra_info_item.len();
1097                }
1098            }
1099        }
1100
1101        let halfway_point = extra_height.div_ceil(2);
1102
1103        // Render the actual node.
1104        for render_y in 0..=extra_height {
1105            for (x, _) in root.nodes.iter().enumerate().take(root.width) {
1106                if self.maximum_render_width > 0
1107                    && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width
1108                {
1109                    break;
1110                }
1111
1112                let mut has_adjacent_nodes = false;
1113                for i in 0..(root.width - x) {
1114                    has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y);
1115                }
1116
1117                if let Some(node) = root.get_node(x, y) {
1118                    write!(self.f, "{}", Self::VERTICAL)?;
1119
1120                    // Figure out what to render.
1121                    let mut render_text = if render_y == 0 {
1122                        node.name.clone()
1123                    } else if render_y <= extra_info[x].len() {
1124                        extra_info[x][render_y - 1].clone()
1125                    } else {
1126                        String::new()
1127                    };
1128
1129                    render_text = Self::adjust_text_for_rendering(
1130                        &render_text,
1131                        Self::NODE_RENDER_WIDTH - 2,
1132                    );
1133                    write!(self.f, "{render_text}")?;
1134
1135                    if render_y == halfway_point && node.child_positions.len() > 1 {
1136                        write!(self.f, "{}", Self::LMIDDLE)?;
1137                    } else {
1138                        write!(self.f, "{}", Self::VERTICAL)?;
1139                    }
1140                } else if render_y == halfway_point {
1141                    let has_child_to_the_right =
1142                        Self::should_render_whitespace(root, x, y);
1143                    if root.has_node(x, y + 1) {
1144                        // Node right below this one.
1145                        write!(
1146                            self.f,
1147                            "{}",
1148                            Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2)
1149                        )?;
1150                        if has_child_to_the_right {
1151                            write!(self.f, "{}", Self::TMIDDLE)?;
1152                            // Have another child to the right, Keep rendering the line.
1153                            write!(
1154                                self.f,
1155                                "{}",
1156                                Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2)
1157                            )?;
1158                        } else {
1159                            write!(self.f, "{}", Self::RTCORNER)?;
1160                            if has_adjacent_nodes {
1161                                // Only a child below this one: fill the reset with spaces.
1162                                write!(
1163                                    self.f,
1164                                    "{}",
1165                                    " ".repeat(Self::NODE_RENDER_WIDTH / 2)
1166                                )?;
1167                            }
1168                        }
1169                    } else if has_child_to_the_right {
1170                        // Child to the right, but no child right below this one: render a full
1171                        // line.
1172                        write!(
1173                            self.f,
1174                            "{}",
1175                            Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH)
1176                        )?;
1177                    } else if has_adjacent_nodes {
1178                        // Empty spot: render spaces.
1179                        write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
1180                    }
1181                } else if render_y >= halfway_point {
1182                    if root.has_node(x, y + 1) {
1183                        // Have a node below this empty spot: render a vertical line.
1184                        write!(
1185                            self.f,
1186                            "{}{}",
1187                            " ".repeat(Self::NODE_RENDER_WIDTH / 2),
1188                            Self::VERTICAL
1189                        )?;
1190                        if has_adjacent_nodes
1191                            || Self::should_render_whitespace(root, x, y)
1192                        {
1193                            write!(
1194                                self.f,
1195                                "{}",
1196                                " ".repeat(Self::NODE_RENDER_WIDTH / 2)
1197                            )?;
1198                        }
1199                    } else if has_adjacent_nodes
1200                        || Self::should_render_whitespace(root, x, y)
1201                    {
1202                        // Empty spot: render spaces.
1203                        write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
1204                    }
1205                } else if has_adjacent_nodes {
1206                    // Empty spot: render spaces.
1207                    write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
1208                }
1209            }
1210            writeln!(self.f)?;
1211        }
1212
1213        Ok(())
1214    }
1215
1216    /// Renders the bottom layer of boxes at the given y-level of the tree.
1217    /// This includes:
1218    /// - Bottom corners (└─┘) for nodes
1219    /// - Horizontal connections between nodes
1220    /// - Vertical connections to child nodes
1221    fn render_bottom_layer(
1222        &mut self,
1223        root: &RenderTree,
1224        y: usize,
1225    ) -> Result<(), fmt::Error> {
1226        for x in 0..=root.width {
1227            if self.maximum_render_width > 0
1228                && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width
1229            {
1230                break;
1231            }
1232            let mut has_adjacent_nodes = false;
1233            for i in 0..(root.width - x) {
1234                has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y);
1235            }
1236            if root.get_node(x, y).is_some() {
1237                write!(self.f, "{}", Self::LDCORNER)?;
1238                write!(
1239                    self.f,
1240                    "{}",
1241                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
1242                )?;
1243                if root.has_node(x, y + 1) {
1244                    // node below this one: connect to that one
1245                    write!(self.f, "{}", Self::TMIDDLE)?;
1246                } else {
1247                    // no node below this one: end the box
1248                    write!(self.f, "{}", Self::HORIZONTAL)?;
1249                }
1250                write!(
1251                    self.f,
1252                    "{}",
1253                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
1254                )?;
1255                write!(self.f, "{}", Self::RDCORNER)?;
1256            } else if root.has_node(x, y + 1) {
1257                write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH / 2))?;
1258                write!(self.f, "{}", Self::VERTICAL)?;
1259                if has_adjacent_nodes || Self::should_render_whitespace(root, x, y) {
1260                    write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH / 2))?;
1261                }
1262            } else if has_adjacent_nodes || Self::should_render_whitespace(root, x, y) {
1263                write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
1264            }
1265        }
1266        writeln!(self.f)?;
1267
1268        Ok(())
1269    }
1270
1271    fn extra_info_separator() -> String {
1272        "-".repeat(Self::NODE_RENDER_WIDTH - 9)
1273    }
1274
1275    fn remove_padding(s: &str) -> String {
1276        s.trim().to_string()
1277    }
1278
1279    pub fn split_up_extra_info(
1280        extra_info: &HashMap<String, String>,
1281        result: &mut Vec<String>,
1282        max_lines: usize,
1283    ) {
1284        if extra_info.is_empty() {
1285            return;
1286        }
1287
1288        result.push(Self::extra_info_separator());
1289
1290        let mut requires_padding = false;
1291        let mut was_inlined = false;
1292
1293        // use BTreeMap for repeatable key order
1294        let sorted_extra_info: BTreeMap<_, _> = extra_info.iter().collect();
1295        for (key, value) in sorted_extra_info {
1296            let mut str = Self::remove_padding(value);
1297            let mut is_inlined = false;
1298            let available_width = Self::NODE_RENDER_WIDTH - 7;
1299            let total_size = key.len() + str.len() + 2;
1300            let is_multiline = str.contains('\n');
1301
1302            if str.is_empty() {
1303                str = key.to_string();
1304            } else if !is_multiline && total_size < available_width {
1305                str = format!("{key}: {str}");
1306                is_inlined = true;
1307            } else {
1308                str = format!("{key}:\n{str}");
1309            }
1310
1311            if is_inlined && was_inlined {
1312                requires_padding = false;
1313            }
1314
1315            if requires_padding {
1316                result.push(String::new());
1317            }
1318
1319            let mut splits: Vec<String> = str.split('\n').map(String::from).collect();
1320            if splits.len() > max_lines {
1321                let mut truncated_splits = Vec::new();
1322                for split in splits.iter().take(max_lines / 2) {
1323                    truncated_splits.push(split.clone());
1324                }
1325                truncated_splits.push("...".to_string());
1326                for split in splits.iter().skip(splits.len() - max_lines / 2) {
1327                    truncated_splits.push(split.clone());
1328                }
1329                splits = truncated_splits;
1330            }
1331            for split in splits {
1332                Self::split_string_buffer(&split, result);
1333            }
1334            if result.len() > max_lines {
1335                result.truncate(max_lines);
1336                result.push("...".to_string());
1337            }
1338
1339            requires_padding = true;
1340            was_inlined = is_inlined;
1341        }
1342    }
1343
1344    /// Adjusts text to fit within the specified width by:
1345    /// 1. Truncating with ellipsis if too long
1346    /// 2. Center-aligning within the available space if shorter
1347    fn adjust_text_for_rendering(source: &str, max_render_width: usize) -> String {
1348        let render_width = source.chars().count();
1349        if render_width > max_render_width {
1350            let truncated = &source[..max_render_width - 3];
1351            format!("{truncated}...")
1352        } else {
1353            let total_spaces = max_render_width - render_width;
1354            let half_spaces = total_spaces / 2;
1355            let extra_left_space = if total_spaces.is_multiple_of(2) { 0 } else { 1 };
1356            format!(
1357                "{}{}{}",
1358                " ".repeat(half_spaces + extra_left_space),
1359                source,
1360                " ".repeat(half_spaces)
1361            )
1362        }
1363    }
1364
1365    /// Determines if whitespace should be rendered at a given position.
1366    /// This is important for:
1367    /// 1. Maintaining proper spacing between sibling nodes
1368    /// 2. Ensuring correct alignment of connections between parents and children
1369    /// 3. Preserving the tree structure's visual clarity
1370    fn should_render_whitespace(root: &RenderTree, x: usize, y: usize) -> bool {
1371        let mut found_children = 0;
1372
1373        for i in (0..=x).rev() {
1374            let node = root.get_node(i, y);
1375            if root.has_node(i, y + 1) {
1376                found_children += 1;
1377            }
1378            if let Some(node) = node {
1379                if node.child_positions.len() > 1
1380                    && found_children < node.child_positions.len()
1381                {
1382                    return true;
1383                }
1384
1385                return false;
1386            }
1387        }
1388
1389        false
1390    }
1391
1392    fn split_string_buffer(source: &str, result: &mut Vec<String>) {
1393        let mut character_pos = 0;
1394        let mut start_pos = 0;
1395        let mut render_width = 0;
1396        let mut last_possible_split = 0;
1397
1398        let chars: Vec<char> = source.chars().collect();
1399
1400        while character_pos < chars.len() {
1401            // Treating each char as width 1 for simplification
1402            let char_width = 1;
1403
1404            // Does the next character make us exceed the line length?
1405            if render_width + char_width > Self::NODE_RENDER_WIDTH - 2 {
1406                if start_pos + 8 > last_possible_split {
1407                    // The last character we can split on is one of the first 8 characters of the line
1408                    // to not create very small lines we instead split on the current character
1409                    last_possible_split = character_pos;
1410                }
1411
1412                result.push(source[start_pos..last_possible_split].to_string());
1413                render_width = character_pos - last_possible_split;
1414                start_pos = last_possible_split;
1415                character_pos = last_possible_split;
1416            }
1417
1418            // check if we can split on this character
1419            if Self::can_split_on_this_char(chars[character_pos]) {
1420                last_possible_split = character_pos;
1421            }
1422
1423            character_pos += 1;
1424            render_width += char_width;
1425        }
1426
1427        if source.len() > start_pos {
1428            // append the remainder of the input
1429            result.push(source[start_pos..].to_string());
1430        }
1431    }
1432
1433    fn can_split_on_this_char(c: char) -> bool {
1434        (!c.is_ascii_digit() && !c.is_ascii_uppercase() && !c.is_ascii_lowercase())
1435            && c != '_'
1436    }
1437}
1438
1439/// Trait for types which could have additional details when formatted in `Verbose` mode
1440pub trait DisplayAs {
1441    /// Format according to `DisplayFormatType`, used when verbose representation looks
1442    /// different from the default one
1443    ///
1444    /// Should not include a newline
1445    fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result;
1446}
1447
1448/// A new type wrapper to display `T` implementing`DisplayAs` using the `Default` mode
1449pub struct DefaultDisplay<T>(pub T);
1450
1451impl<T: DisplayAs> fmt::Display for DefaultDisplay<T> {
1452    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1453        self.0.fmt_as(DisplayFormatType::Default, f)
1454    }
1455}
1456
1457/// A new type wrapper to display `T` implementing `DisplayAs` using the `Verbose` mode
1458pub struct VerboseDisplay<T>(pub T);
1459
1460impl<T: DisplayAs> fmt::Display for VerboseDisplay<T> {
1461    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1462        self.0.fmt_as(DisplayFormatType::Verbose, f)
1463    }
1464}
1465
1466/// A wrapper to customize partitioned file display
1467#[derive(Debug)]
1468pub struct ProjectSchemaDisplay<'a>(pub &'a SchemaRef);
1469
1470impl fmt::Display for ProjectSchemaDisplay<'_> {
1471    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1472        let parts: Vec<_> = self
1473            .0
1474            .fields()
1475            .iter()
1476            .map(|x| x.name().to_owned())
1477            .collect::<Vec<String>>();
1478        write!(f, "[{}]", parts.join(", "))
1479    }
1480}
1481
1482pub fn display_orderings(f: &mut Formatter, orderings: &[LexOrdering]) -> fmt::Result {
1483    if !orderings.is_empty() {
1484        let start = if orderings.len() == 1 {
1485            ", output_ordering="
1486        } else {
1487            ", output_orderings=["
1488        };
1489        write!(f, "{start}")?;
1490        for (idx, ordering) in orderings.iter().enumerate() {
1491            match idx {
1492                0 => write!(f, "[{ordering}]")?,
1493                _ => write!(f, ", [{ordering}]")?,
1494            }
1495        }
1496        let end = if orderings.len() == 1 { "" } else { "]" };
1497        write!(f, "{end}")?;
1498    }
1499    Ok(())
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504    use std::fmt::Write;
1505    use std::sync::Arc;
1506
1507    use datafusion_common::{
1508        Result, Statistics, internal_datafusion_err, tree_node::TreeNodeRecursion,
1509    };
1510    use datafusion_execution::{SendableRecordBatchStream, TaskContext};
1511    use datafusion_physical_expr::PhysicalExpr;
1512
1513    use crate::statistics::StatisticsArgs;
1514    use crate::{
1515        ChildrenPropertiesMode, DisplayAs, ExecutionPlan, PlanProperties,
1516        ReplaceChildrenOptions,
1517    };
1518
1519    use super::DisplayableExecutionPlan;
1520
1521    #[derive(Debug, Clone, Copy)]
1522    enum TestStatsExecPlan {
1523        Panic,
1524        Error,
1525        Ok,
1526    }
1527
1528    impl DisplayAs for TestStatsExecPlan {
1529        fn fmt_as(
1530            &self,
1531            _t: crate::DisplayFormatType,
1532            f: &mut std::fmt::Formatter,
1533        ) -> std::fmt::Result {
1534            write!(f, "TestStatsExecPlan")
1535        }
1536    }
1537
1538    impl ExecutionPlan for TestStatsExecPlan {
1539        fn name(&self) -> &'static str {
1540            "TestStatsExecPlan"
1541        }
1542
1543        fn properties(&self) -> &Arc<PlanProperties> {
1544            unimplemented!()
1545        }
1546
1547        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1548            vec![]
1549        }
1550
1551        fn replace_children(
1552            self: Arc<Self>,
1553            _: Vec<Arc<dyn ExecutionPlan>>,
1554            _: ReplaceChildrenOptions,
1555        ) -> Result<Arc<dyn ExecutionPlan>> {
1556            unimplemented!()
1557        }
1558
1559        fn apply_expressions(
1560            &self,
1561            _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1562        ) -> Result<TreeNodeRecursion> {
1563            Ok(TreeNodeRecursion::Continue)
1564        }
1565
1566        fn with_new_children(
1567            self: Arc<Self>,
1568            children: Vec<Arc<dyn ExecutionPlan>>,
1569        ) -> Result<Arc<dyn ExecutionPlan>> {
1570            self.replace_children(
1571                children,
1572                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1573            )
1574        }
1575
1576        fn execute(
1577            &self,
1578            _: usize,
1579            _: Arc<TaskContext>,
1580        ) -> Result<SendableRecordBatchStream> {
1581            todo!()
1582        }
1583
1584        fn statistics_from_inputs(
1585            &self,
1586            _input_stats: &[Arc<Statistics>],
1587            args: &StatisticsArgs,
1588        ) -> Result<Arc<Statistics>> {
1589            if args.partition().is_some() {
1590                return Ok(Arc::new(Statistics::new_unknown(self.schema().as_ref())));
1591            }
1592            match self {
1593                Self::Panic => panic!("expected panic"),
1594                Self::Error => Err(internal_datafusion_err!("expected error")),
1595                Self::Ok => Ok(Arc::new(Statistics::new_unknown(self.schema().as_ref()))),
1596            }
1597        }
1598    }
1599
1600    fn test_stats_display(exec: TestStatsExecPlan, show_stats: bool) {
1601        let display =
1602            DisplayableExecutionPlan::new(&exec).set_show_statistics(show_stats);
1603
1604        let mut buf = String::new();
1605        write!(&mut buf, "{}", display.one_line()).unwrap();
1606        let buf = buf.trim();
1607        assert_eq!(buf, "TestStatsExecPlan");
1608    }
1609
1610    #[test]
1611    fn test_display_when_stats_panic_with_no_show_stats() {
1612        test_stats_display(TestStatsExecPlan::Panic, false);
1613    }
1614
1615    #[test]
1616    fn test_display_when_stats_error_with_no_show_stats() {
1617        test_stats_display(TestStatsExecPlan::Error, false);
1618    }
1619
1620    #[test]
1621    fn test_display_when_stats_ok_with_no_show_stats() {
1622        test_stats_display(TestStatsExecPlan::Ok, false);
1623    }
1624
1625    #[test]
1626    #[should_panic(expected = "expected panic")]
1627    fn test_display_when_stats_panic_with_show_stats() {
1628        test_stats_display(TestStatsExecPlan::Panic, true);
1629    }
1630
1631    #[test]
1632    #[should_panic(expected = "Error")] // fmt::Error
1633    fn test_display_when_stats_error_with_show_stats() {
1634        test_stats_display(TestStatsExecPlan::Error, true);
1635    }
1636
1637    #[test]
1638    fn test_display_when_stats_ok_with_show_stats() {
1639        test_stats_display(TestStatsExecPlan::Ok, false);
1640    }
1641
1642    mod pgjson {
1643        use std::sync::Arc;
1644        use std::time::Duration;
1645
1646        use arrow::datatypes::{DataType, Field, Schema};
1647        use insta::assert_snapshot;
1648
1649        use super::super::DisplayableExecutionPlan;
1650        use crate::empty::EmptyExec;
1651        use crate::filter::FilterExec;
1652        use crate::projection::ProjectionExec;
1653        use crate::{ChildrenPropertiesMode, ExecutionPlan, ReplaceChildrenOptions};
1654        use datafusion_physical_expr::expressions::{binary, col, lit};
1655        use datafusion_physical_expr::{Partitioning, PhysicalExpr};
1656
1657        fn sample_plan() -> Arc<dyn ExecutionPlan> {
1658            let schema = Arc::new(Schema::new(vec![
1659                Field::new("a", DataType::Int32, false),
1660                Field::new("b", DataType::Int32, false),
1661            ]));
1662            let empty = Arc::new(EmptyExec::new(Arc::clone(&schema)));
1663            let predicate = binary(
1664                col("a", &schema).unwrap(),
1665                datafusion_expr::Operator::Gt,
1666                lit(5i32),
1667                &schema,
1668            )
1669            .unwrap();
1670            let filter = Arc::new(FilterExec::try_new(predicate, empty).unwrap());
1671            let proj_expr: Vec<(Arc<dyn PhysicalExpr>, String)> =
1672                vec![(col("a", &schema).unwrap(), "a".to_string())];
1673            let _ = Partitioning::UnknownPartitioning(1);
1674            Arc::new(ProjectionExec::try_new(proj_expr, filter).unwrap())
1675        }
1676
1677        #[test]
1678        fn pgjson_renders_plan_without_metrics() {
1679            let plan = sample_plan();
1680            let out = DisplayableExecutionPlan::new(plan.as_ref())
1681                .pgjson(false)
1682                .to_string();
1683            let value: serde_json::Value = serde_json::from_str(&out).unwrap();
1684            // Root is an array with one {"Plan": ...} entry.
1685            let root = value
1686                .as_array()
1687                .expect("root array")
1688                .first()
1689                .expect("root entry")
1690                .get("Plan")
1691                .expect("plan object");
1692            assert_eq!(root["Node Type"].as_str(), Some("ProjectionExec"));
1693            assert!(root.get("Actual Rows").is_none());
1694            assert!(root.get("Extras").is_none());
1695            let plans = root["Plans"].as_array().expect("Plans array");
1696            assert_eq!(plans.len(), 1);
1697            assert_eq!(plans[0]["Node Type"].as_str(), Some("FilterExec"));
1698        }
1699
1700        #[test]
1701        fn pgjson_emits_pg_canonical_metric_keys() {
1702            use crate::metrics::{Count, Metric, MetricValue, MetricsSet, Time};
1703            use crate::{DisplayFormatType, ExecutionPlan, PlanProperties};
1704            use datafusion_common::Result;
1705            use datafusion_execution::{SendableRecordBatchStream, TaskContext};
1706
1707            // Wrap `sample_plan()` with an adapter node that exposes a
1708            // hand-crafted `MetricsSet` so we can assert the PG key mapping
1709            // without running anything.
1710            #[derive(Debug)]
1711            struct WithMetrics {
1712                inner: Arc<dyn ExecutionPlan>,
1713                metrics: MetricsSet,
1714            }
1715            impl crate::DisplayAs for WithMetrics {
1716                fn fmt_as(
1717                    &self,
1718                    _t: DisplayFormatType,
1719                    f: &mut std::fmt::Formatter,
1720                ) -> std::fmt::Result {
1721                    write!(f, "WithMetrics")
1722                }
1723            }
1724            impl ExecutionPlan for WithMetrics {
1725                fn name(&self) -> &'static str {
1726                    "WithMetrics"
1727                }
1728                fn properties(&self) -> &Arc<PlanProperties> {
1729                    self.inner.properties()
1730                }
1731                fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1732                    vec![&self.inner]
1733                }
1734                fn apply_expressions(
1735                    &self,
1736                    _f: &mut dyn FnMut(
1737                        &Arc<dyn PhysicalExpr>,
1738                    ) -> Result<
1739                        datafusion_common::tree_node::TreeNodeRecursion,
1740                    >,
1741                ) -> Result<datafusion_common::tree_node::TreeNodeRecursion>
1742                {
1743                    Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
1744                }
1745
1746                fn replace_children(
1747                    self: Arc<Self>,
1748                    _: Vec<Arc<dyn ExecutionPlan>>,
1749                    _: ReplaceChildrenOptions,
1750                ) -> Result<Arc<dyn ExecutionPlan>> {
1751                    unimplemented!()
1752                }
1753                fn with_new_children(
1754                    self: Arc<Self>,
1755                    children: Vec<Arc<dyn ExecutionPlan>>,
1756                ) -> Result<Arc<dyn ExecutionPlan>> {
1757                    self.replace_children(
1758                        children,
1759                        ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1760                    )
1761                }
1762                fn execute(
1763                    &self,
1764                    _: usize,
1765                    _: Arc<TaskContext>,
1766                ) -> Result<SendableRecordBatchStream> {
1767                    unimplemented!()
1768                }
1769                fn metrics(&self) -> Option<MetricsSet> {
1770                    Some(self.metrics.clone())
1771                }
1772            }
1773
1774            let mut metrics = MetricsSet::new();
1775            let rows = Count::new();
1776            rows.add(42);
1777            metrics.push(Arc::new(Metric::new(MetricValue::OutputRows(rows), None)));
1778            let elapsed = Time::new();
1779            elapsed.add_duration(Duration::from_millis(5));
1780            metrics.push(Arc::new(Metric::new(
1781                MetricValue::ElapsedCompute(elapsed),
1782                None,
1783            )));
1784            let batches = Count::new();
1785            batches.add(7);
1786            metrics.push(Arc::new(Metric::new(
1787                MetricValue::OutputBatches(batches),
1788                None,
1789            )));
1790
1791            let plan: Arc<dyn ExecutionPlan> = Arc::new(WithMetrics {
1792                inner: sample_plan(),
1793                metrics,
1794            });
1795
1796            let out = DisplayableExecutionPlan::with_metrics(plan.as_ref())
1797                .pgjson(false)
1798                .to_string();
1799            let value: serde_json::Value = serde_json::from_str(&out).unwrap();
1800            let root = value[0].get("Plan").expect("plan");
1801            assert_eq!(root["Actual Rows"].as_u64(), Some(42));
1802            assert_eq!(root["Actual Total Time"].as_f64(), Some(5.0));
1803            assert_eq!(root["Extras"]["output_batches"].as_u64(), Some(7));
1804
1805            let metric_names = vec!["output_rows".to_string()];
1806            for rendered in [
1807                DisplayableExecutionPlan::with_metrics(plan.as_ref())
1808                    .set_metric_names(metric_names.clone())
1809                    .indent(false)
1810                    .to_string(),
1811                DisplayableExecutionPlan::with_full_metrics(plan.as_ref())
1812                    .set_metric_names(metric_names.clone())
1813                    .indent(false)
1814                    .to_string(),
1815                DisplayableExecutionPlan::with_metrics(plan.as_ref())
1816                    .set_metric_names(metric_names.clone())
1817                    .graphviz()
1818                    .to_string(),
1819                DisplayableExecutionPlan::with_full_metrics(plan.as_ref())
1820                    .set_metric_names(metric_names.clone())
1821                    .graphviz()
1822                    .to_string(),
1823            ] {
1824                assert!(rendered.contains("output_rows"));
1825                assert!(!rendered.contains("elapsed_compute"));
1826                assert!(!rendered.contains("output_batches"));
1827            }
1828
1829            let out = DisplayableExecutionPlan::with_metrics(plan.as_ref())
1830                .set_metric_names(metric_names)
1831                .pgjson(false)
1832                .to_string();
1833            let value: serde_json::Value = serde_json::from_str(&out).unwrap();
1834            let root = value[0].get("Plan").expect("plan");
1835            assert_eq!(root["Actual Rows"].as_u64(), Some(42));
1836            assert!(root.get("Actual Total Time").is_none());
1837            assert!(root.get("Extras").is_none());
1838        }
1839
1840        #[test]
1841        fn pgjson_includes_summary_when_set() {
1842            let plan = sample_plan();
1843            let out = DisplayableExecutionPlan::with_metrics(plan.as_ref())
1844                .set_summary(Some(42), Some(Duration::from_millis(7)))
1845                .pgjson(false)
1846                .to_string();
1847            let value: serde_json::Value = serde_json::from_str(&out).unwrap();
1848            let entry = &value.as_array().unwrap()[0];
1849            assert_eq!(entry["Total Rows"].as_u64(), Some(42));
1850            assert!(entry["Duration"].is_string());
1851        }
1852
1853        #[test]
1854        fn pgjson_snapshot_of_sample_plan() {
1855            let plan = sample_plan();
1856            let out = DisplayableExecutionPlan::new(plan.as_ref())
1857                .pgjson(false)
1858                .to_string();
1859            // This snapshot assumes `serde_json` is built with the
1860            // `preserve_order` feature (enabled via this crate's dev-deps).
1861            assert_snapshot!(out, @r#"
1862            [
1863              {
1864                "Plan": {
1865                  "Node Type": "ProjectionExec",
1866                  "Details": "ProjectionExec: expr=[a@0 as a]",
1867                  "Plans": [
1868                    {
1869                      "Node Type": "FilterExec",
1870                      "Details": "FilterExec: a@0 > 5",
1871                      "Plans": [
1872                        {
1873                          "Node Type": "EmptyExec",
1874                          "Details": "EmptyExec",
1875                          "Plans": []
1876                        }
1877                      ]
1878                    }
1879                  ]
1880                }
1881              }
1882            ]
1883            "#);
1884        }
1885    }
1886}