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;
24
25use arrow::datatypes::SchemaRef;
26
27use datafusion_common::display::{GraphvizBuilder, PlanType, StringifiedPlan};
28use datafusion_expr::display_schema;
29use datafusion_physical_expr::LexOrdering;
30
31use crate::metrics::MetricType;
32use crate::render_tree::RenderTree;
33
34use super::{accept, ExecutionPlan, ExecutionPlanVisitor};
35
36/// Options for controlling how each [`ExecutionPlan`] should format itself
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum DisplayFormatType {
39    /// Default, compact format. Example: `FilterExec: c12 < 10.0`
40    ///
41    /// This format is designed to provide a detailed textual description
42    /// of all parts of the plan.
43    Default,
44    /// Verbose, showing all available details.
45    ///
46    /// This form is even more detailed than [`Self::Default`]
47    Verbose,
48    /// TreeRender, displayed in the `tree` explain type.
49    ///
50    /// This format is inspired by DuckDB's explain plans. The information
51    /// presented should be "user friendly", and contain only the most relevant
52    /// information for understanding a plan. It should NOT contain the same level
53    /// of detail information as the  [`Self::Default`] format.
54    ///
55    /// In this mode, each line has one of two formats:
56    ///
57    /// 1. A string without a `=`, which is printed in its own line
58    ///
59    /// 2. A string with a `=` that is treated as a `key=value pair`. Everything
60    ///    before the first `=` is treated as the key, and everything after the
61    ///    first `=` is treated as the value.
62    ///
63    /// For example, if the output of `TreeRender` is this:
64    /// ```text
65    /// Parquet
66    /// partition_sizes=[1]
67    /// ```
68    ///
69    /// It is rendered in the center of a box in the following way:
70    ///
71    /// ```text
72    /// ┌───────────────────────────┐
73    /// │       DataSourceExec      │
74    /// │    --------------------   │
75    /// │    partition_sizes: [1]   │
76    /// │          Parquet          │
77    /// └───────────────────────────┘
78    ///  ```
79    TreeRender,
80}
81
82/// Wraps an `ExecutionPlan` with various methods for formatting
83///
84///
85/// # Example
86/// ```
87/// # use std::sync::Arc;
88/// # use arrow::datatypes::{Field, Schema, DataType};
89/// # use datafusion_expr::Operator;
90/// # use datafusion_physical_expr::expressions::{binary, col, lit};
91/// # use datafusion_physical_plan::{displayable, ExecutionPlan};
92/// # use datafusion_physical_plan::empty::EmptyExec;
93/// # use datafusion_physical_plan::filter::FilterExec;
94/// # let schema = Schema::new(vec![Field::new("i", DataType::Int32, false)]);
95/// # let plan = EmptyExec::new(Arc::new(schema));
96/// # let i = col("i", &plan.schema()).unwrap();
97/// # let predicate = binary(i, Operator::Eq, lit(1), &plan.schema()).unwrap();
98/// # let plan: Arc<dyn ExecutionPlan> = Arc::new(FilterExec::try_new(predicate, Arc::new(plan)).unwrap());
99/// // Get a one line description (Displayable)
100/// let display_plan = displayable(plan.as_ref());
101///
102/// // you can use the returned objects to format plans
103/// // where you can use `Display` such as  format! or println!
104/// assert_eq!(
105///    &format!("The plan is: {}", display_plan.one_line()),
106///   "The plan is: FilterExec: i@0 = 1\n"
107/// );
108/// // You can also print out the plan and its children in indented mode
109/// assert_eq!(display_plan.indent(false).to_string(),
110///   "FilterExec: i@0 = 1\
111///   \n  EmptyExec\
112///   \n"
113/// );
114/// ```
115#[derive(Debug, Clone)]
116pub struct DisplayableExecutionPlan<'a> {
117    inner: &'a dyn ExecutionPlan,
118    /// How to show metrics
119    show_metrics: ShowMetrics,
120    /// If statistics should be displayed
121    show_statistics: bool,
122    /// If schema should be displayed. See [`Self::set_show_schema`]
123    show_schema: bool,
124    /// Which metric categories should be included when rendering
125    metric_types: Vec<MetricType>,
126    // (TreeRender) Maximum total width of the rendered tree
127    tree_maximum_render_width: usize,
128}
129
130impl<'a> DisplayableExecutionPlan<'a> {
131    fn default_metric_types() -> Vec<MetricType> {
132        vec![MetricType::SUMMARY, MetricType::DEV]
133    }
134
135    /// Create a wrapper around an [`ExecutionPlan`] which can be
136    /// pretty printed in a variety of ways
137    pub fn new(inner: &'a dyn ExecutionPlan) -> Self {
138        Self {
139            inner,
140            show_metrics: ShowMetrics::None,
141            show_statistics: false,
142            show_schema: false,
143            metric_types: Self::default_metric_types(),
144            tree_maximum_render_width: 240,
145        }
146    }
147
148    /// Create a wrapper around an [`ExecutionPlan`] which can be
149    /// pretty printed in a variety of ways that also shows aggregated
150    /// metrics
151    pub fn with_metrics(inner: &'a dyn ExecutionPlan) -> Self {
152        Self {
153            inner,
154            show_metrics: ShowMetrics::Aggregated,
155            show_statistics: false,
156            show_schema: false,
157            metric_types: Self::default_metric_types(),
158            tree_maximum_render_width: 240,
159        }
160    }
161
162    /// Create a wrapper around an [`ExecutionPlan`] which can be
163    /// pretty printed in a variety of ways that also shows all low
164    /// level metrics
165    pub fn with_full_metrics(inner: &'a dyn ExecutionPlan) -> Self {
166        Self {
167            inner,
168            show_metrics: ShowMetrics::Full,
169            show_statistics: false,
170            show_schema: false,
171            metric_types: Self::default_metric_types(),
172            tree_maximum_render_width: 240,
173        }
174    }
175
176    /// Enable display of schema
177    ///
178    /// If true, plans will be displayed with schema information at the end
179    /// of each line. The format is `schema=[[a:Int32;N, b:Int32;N, c:Int32;N]]`
180    pub fn set_show_schema(mut self, show_schema: bool) -> Self {
181        self.show_schema = show_schema;
182        self
183    }
184
185    /// Enable display of statistics
186    pub fn set_show_statistics(mut self, show_statistics: bool) -> Self {
187        self.show_statistics = show_statistics;
188        self
189    }
190
191    /// Specify which metric types should be rendered alongside the plan
192    pub fn set_metric_types(mut self, metric_types: Vec<MetricType>) -> Self {
193        self.metric_types = metric_types;
194        self
195    }
196
197    /// Set the maximum render width for the tree format
198    pub fn set_tree_maximum_render_width(mut self, width: usize) -> Self {
199        self.tree_maximum_render_width = width;
200        self
201    }
202
203    /// Return a `format`able structure that produces a single line
204    /// per node.
205    ///
206    /// ```text
207    /// ProjectionExec: expr=[a]
208    ///   CoalesceBatchesExec: target_batch_size=8192
209    ///     FilterExec: a < 5
210    ///       RepartitionExec: partitioning=RoundRobinBatch(16)
211    ///         DataSourceExec: source=...",
212    /// ```
213    pub fn indent(&self, verbose: bool) -> impl fmt::Display + 'a {
214        let format_type = if verbose {
215            DisplayFormatType::Verbose
216        } else {
217            DisplayFormatType::Default
218        };
219        struct Wrapper<'a> {
220            format_type: DisplayFormatType,
221            plan: &'a dyn ExecutionPlan,
222            show_metrics: ShowMetrics,
223            show_statistics: bool,
224            show_schema: bool,
225            metric_types: Vec<MetricType>,
226        }
227        impl fmt::Display for Wrapper<'_> {
228            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
229                let mut visitor = IndentVisitor {
230                    t: self.format_type,
231                    f,
232                    indent: 0,
233                    show_metrics: self.show_metrics,
234                    show_statistics: self.show_statistics,
235                    show_schema: self.show_schema,
236                    metric_types: &self.metric_types,
237                };
238                accept(self.plan, &mut visitor)
239            }
240        }
241        Wrapper {
242            format_type,
243            plan: self.inner,
244            show_metrics: self.show_metrics,
245            show_statistics: self.show_statistics,
246            show_schema: self.show_schema,
247            metric_types: self.metric_types.clone(),
248        }
249    }
250
251    /// Returns a `format`able structure that produces graphviz format for execution plan, which can
252    /// be directly visualized [here](https://dreampuf.github.io/GraphvizOnline).
253    ///
254    /// An example is
255    /// ```dot
256    /// strict digraph dot_plan {
257    //     0[label="ProjectionExec: expr=[id@0 + 2 as employee.id + Int32(2)]",tooltip=""]
258    //     1[label="EmptyExec",tooltip=""]
259    //     0 -> 1
260    // }
261    /// ```
262    pub fn graphviz(&self) -> impl fmt::Display + 'a {
263        struct Wrapper<'a> {
264            plan: &'a dyn ExecutionPlan,
265            show_metrics: ShowMetrics,
266            show_statistics: bool,
267            metric_types: Vec<MetricType>,
268        }
269        impl fmt::Display for Wrapper<'_> {
270            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
271                let t = DisplayFormatType::Default;
272
273                let mut visitor = GraphvizVisitor {
274                    f,
275                    t,
276                    show_metrics: self.show_metrics,
277                    show_statistics: self.show_statistics,
278                    metric_types: &self.metric_types,
279                    graphviz_builder: GraphvizBuilder::default(),
280                    parents: Vec::new(),
281                };
282
283                visitor.start_graph()?;
284
285                accept(self.plan, &mut visitor)?;
286
287                visitor.end_graph()?;
288                Ok(())
289            }
290        }
291
292        Wrapper {
293            plan: self.inner,
294            show_metrics: self.show_metrics,
295            show_statistics: self.show_statistics,
296            metric_types: self.metric_types.clone(),
297        }
298    }
299
300    /// Formats the plan using a ASCII art like tree
301    ///
302    /// See [`DisplayFormatType::TreeRender`] for more details.
303    pub fn tree_render(&self) -> impl fmt::Display + 'a {
304        struct Wrapper<'a> {
305            plan: &'a dyn ExecutionPlan,
306            maximum_render_width: usize,
307        }
308        impl fmt::Display for Wrapper<'_> {
309            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
310                let mut visitor = TreeRenderVisitor {
311                    f,
312                    maximum_render_width: self.maximum_render_width,
313                };
314                visitor.visit(self.plan)
315            }
316        }
317        Wrapper {
318            plan: self.inner,
319            maximum_render_width: self.tree_maximum_render_width,
320        }
321    }
322
323    /// Return a single-line summary of the root of the plan
324    /// Example: `ProjectionExec: expr=[a@0 as a]`.
325    pub fn one_line(&self) -> impl fmt::Display + 'a {
326        struct Wrapper<'a> {
327            plan: &'a dyn ExecutionPlan,
328            show_metrics: ShowMetrics,
329            show_statistics: bool,
330            show_schema: bool,
331            metric_types: Vec<MetricType>,
332        }
333
334        impl fmt::Display for Wrapper<'_> {
335            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
336                let mut visitor = IndentVisitor {
337                    f,
338                    t: DisplayFormatType::Default,
339                    indent: 0,
340                    show_metrics: self.show_metrics,
341                    show_statistics: self.show_statistics,
342                    show_schema: self.show_schema,
343                    metric_types: &self.metric_types,
344                };
345                visitor.pre_visit(self.plan)?;
346                Ok(())
347            }
348        }
349
350        Wrapper {
351            plan: self.inner,
352            show_metrics: self.show_metrics,
353            show_statistics: self.show_statistics,
354            show_schema: self.show_schema,
355            metric_types: self.metric_types.clone(),
356        }
357    }
358
359    #[deprecated(since = "47.0.0", note = "indent() or tree_render() instead")]
360    pub fn to_stringified(
361        &self,
362        verbose: bool,
363        plan_type: PlanType,
364        explain_format: DisplayFormatType,
365    ) -> StringifiedPlan {
366        match (&explain_format, &plan_type) {
367            (DisplayFormatType::TreeRender, PlanType::FinalPhysicalPlan) => {
368                StringifiedPlan::new(plan_type, self.tree_render().to_string())
369            }
370            _ => StringifiedPlan::new(plan_type, self.indent(verbose).to_string()),
371        }
372    }
373}
374
375/// Enum representing the different levels of metrics to display
376#[derive(Debug, Clone, Copy)]
377enum ShowMetrics {
378    /// Do not show any metrics
379    None,
380
381    /// Show aggregated metrics across partition
382    Aggregated,
383
384    /// Show full per-partition metrics
385    Full,
386}
387
388/// Formats plans with a single line per node.
389///
390/// # Example
391///
392/// ```text
393/// ProjectionExec: expr=[column1@0 + 2 as column1 + Int64(2)]
394///   FilterExec: column1@0 = 5
395///     ValuesExec
396/// ```
397struct IndentVisitor<'a, 'b> {
398    /// How to format each node
399    t: DisplayFormatType,
400    /// Write to this formatter
401    f: &'a mut Formatter<'b>,
402    /// Indent size
403    indent: usize,
404    /// How to show metrics
405    show_metrics: ShowMetrics,
406    /// If statistics should be displayed
407    show_statistics: bool,
408    /// If schema should be displayed
409    show_schema: bool,
410    /// Which metric types should be rendered
411    metric_types: &'a [MetricType],
412}
413
414impl ExecutionPlanVisitor for IndentVisitor<'_, '_> {
415    type Error = fmt::Error;
416    fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
417        write!(self.f, "{:indent$}", "", indent = self.indent * 2)?;
418        plan.fmt_as(self.t, self.f)?;
419        match self.show_metrics {
420            ShowMetrics::None => {}
421            ShowMetrics::Aggregated => {
422                if let Some(metrics) = plan.metrics() {
423                    let metrics = metrics
424                        .filter_by_metric_types(self.metric_types)
425                        .aggregate_by_name()
426                        .sorted_for_display()
427                        .timestamps_removed();
428
429                    write!(self.f, ", metrics=[{metrics}]")?;
430                } else {
431                    write!(self.f, ", metrics=[]")?;
432                }
433            }
434            ShowMetrics::Full => {
435                if let Some(metrics) = plan.metrics() {
436                    let metrics = metrics.filter_by_metric_types(self.metric_types);
437                    write!(self.f, ", metrics=[{metrics}]")?;
438                } else {
439                    write!(self.f, ", metrics=[]")?;
440                }
441            }
442        }
443        if self.show_statistics {
444            let stats = plan.partition_statistics(None).map_err(|_e| fmt::Error)?;
445            write!(self.f, ", statistics=[{stats}]")?;
446        }
447        if self.show_schema {
448            write!(
449                self.f,
450                ", schema={}",
451                display_schema(plan.schema().as_ref())
452            )?;
453        }
454        writeln!(self.f)?;
455        self.indent += 1;
456        Ok(true)
457    }
458
459    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
460        self.indent -= 1;
461        Ok(true)
462    }
463}
464
465struct GraphvizVisitor<'a, 'b> {
466    f: &'a mut Formatter<'b>,
467    /// How to format each node
468    t: DisplayFormatType,
469    /// How to show metrics
470    show_metrics: ShowMetrics,
471    /// If statistics should be displayed
472    show_statistics: bool,
473    /// Which metric types should be rendered
474    metric_types: &'a [MetricType],
475
476    graphviz_builder: GraphvizBuilder,
477    /// Used to record parent node ids when visiting a plan.
478    parents: Vec<usize>,
479}
480
481impl GraphvizVisitor<'_, '_> {
482    fn start_graph(&mut self) -> fmt::Result {
483        self.graphviz_builder.start_graph(self.f)
484    }
485
486    fn end_graph(&mut self) -> fmt::Result {
487        self.graphviz_builder.end_graph(self.f)
488    }
489}
490
491impl ExecutionPlanVisitor for GraphvizVisitor<'_, '_> {
492    type Error = fmt::Error;
493
494    fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
495        let id = self.graphviz_builder.next_id();
496
497        struct Wrapper<'a>(&'a dyn ExecutionPlan, DisplayFormatType);
498
499        impl fmt::Display for Wrapper<'_> {
500            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
501                self.0.fmt_as(self.1, f)
502            }
503        }
504
505        let label = { format!("{}", Wrapper(plan, self.t)) };
506
507        let metrics = match self.show_metrics {
508            ShowMetrics::None => "".to_string(),
509            ShowMetrics::Aggregated => {
510                if let Some(metrics) = plan.metrics() {
511                    let metrics = metrics
512                        .filter_by_metric_types(self.metric_types)
513                        .aggregate_by_name()
514                        .sorted_for_display()
515                        .timestamps_removed();
516
517                    format!("metrics=[{metrics}]")
518                } else {
519                    "metrics=[]".to_string()
520                }
521            }
522            ShowMetrics::Full => {
523                if let Some(metrics) = plan.metrics() {
524                    let metrics = metrics.filter_by_metric_types(self.metric_types);
525                    format!("metrics=[{metrics}]")
526                } else {
527                    "metrics=[]".to_string()
528                }
529            }
530        };
531
532        let statistics = if self.show_statistics {
533            let stats = plan.partition_statistics(None).map_err(|_e| fmt::Error)?;
534            format!("statistics=[{stats}]")
535        } else {
536            "".to_string()
537        };
538
539        let delimiter = if !metrics.is_empty() && !statistics.is_empty() {
540            ", "
541        } else {
542            ""
543        };
544
545        self.graphviz_builder.add_node(
546            self.f,
547            id,
548            &label,
549            Some(&format!("{metrics}{delimiter}{statistics}")),
550        )?;
551
552        if let Some(parent_node_id) = self.parents.last() {
553            self.graphviz_builder
554                .add_edge(self.f, *parent_node_id, id)?;
555        }
556
557        self.parents.push(id);
558
559        Ok(true)
560    }
561
562    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
563        self.parents.pop();
564        Ok(true)
565    }
566}
567
568/// This module implements a tree-like art renderer for execution plans,
569/// based on DuckDB's implementation:
570/// <https://github.com/duckdb/duckdb/blob/main/src/include/duckdb/common/tree_renderer/text_tree_renderer.hpp>
571///
572/// The rendered output looks like this:
573/// ```text
574/// ┌───────────────────────────┐
575/// │    CoalesceBatchesExec    │
576/// └─────────────┬─────────────┘
577/// ┌─────────────┴─────────────┐
578/// │        HashJoinExec       ├──────────────┐
579/// └─────────────┬─────────────┘              │
580/// ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
581/// │       DataSourceExec      ││       DataSourceExec      │
582/// └───────────────────────────┘└───────────────────────────┘
583/// ```
584///
585/// The renderer uses a three-layer approach for each node:
586/// 1. Top layer: renders the top borders and connections
587/// 2. Content layer: renders the node content and vertical connections
588/// 3. Bottom layer: renders the bottom borders and connections
589///
590/// Each node is rendered in a box of fixed width (NODE_RENDER_WIDTH).
591struct TreeRenderVisitor<'a, 'b> {
592    /// Write to this formatter
593    f: &'a mut Formatter<'b>,
594    /// Maximum total width of the rendered tree
595    maximum_render_width: usize,
596}
597
598impl TreeRenderVisitor<'_, '_> {
599    // Unicode box-drawing characters for creating borders and connections.
600    const LTCORNER: &'static str = "┌"; // Left top corner
601    const RTCORNER: &'static str = "┐"; // Right top corner
602    const LDCORNER: &'static str = "└"; // Left bottom corner
603    const RDCORNER: &'static str = "┘"; // Right bottom corner
604
605    const TMIDDLE: &'static str = "┬"; // Top T-junction (connects down)
606    const LMIDDLE: &'static str = "├"; // Left T-junction (connects right)
607    const DMIDDLE: &'static str = "┴"; // Bottom T-junction (connects up)
608
609    const VERTICAL: &'static str = "│"; // Vertical line
610    const HORIZONTAL: &'static str = "─"; // Horizontal line
611
612    // TODO: Make these variables configurable.
613    const NODE_RENDER_WIDTH: usize = 29; // Width of each node's box
614    const MAX_EXTRA_LINES: usize = 30; // Maximum number of extra info lines per node
615
616    /// Main entry point for rendering an execution plan as a tree.
617    /// The rendering process happens in three stages for each level of the tree:
618    /// 1. Render top borders and connections
619    /// 2. Render node content and vertical connections
620    /// 3. Render bottom borders and connections
621    pub fn visit(&mut self, plan: &dyn ExecutionPlan) -> Result<(), fmt::Error> {
622        let root = RenderTree::create_tree(plan);
623
624        for y in 0..root.height {
625            // Start by rendering the top layer.
626            self.render_top_layer(&root, y)?;
627            // Now we render the content of the boxes
628            self.render_box_content(&root, y)?;
629            // Render the bottom layer of each of the boxes
630            self.render_bottom_layer(&root, y)?;
631        }
632
633        Ok(())
634    }
635
636    /// Renders the top layer of boxes at the given y-level of the tree.
637    /// This includes:
638    /// - Top corners (┌─┐) for nodes
639    /// - Horizontal connections between nodes
640    /// - Vertical connections to parent nodes
641    fn render_top_layer(
642        &mut self,
643        root: &RenderTree,
644        y: usize,
645    ) -> Result<(), fmt::Error> {
646        for x in 0..root.width {
647            if self.maximum_render_width > 0
648                && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width
649            {
650                break;
651            }
652
653            if root.has_node(x, y) {
654                write!(self.f, "{}", Self::LTCORNER)?;
655                write!(
656                    self.f,
657                    "{}",
658                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
659                )?;
660                if y == 0 {
661                    // top level node: no node above this one
662                    write!(self.f, "{}", Self::HORIZONTAL)?;
663                } else {
664                    // render connection to node above this one
665                    write!(self.f, "{}", Self::DMIDDLE)?;
666                }
667                write!(
668                    self.f,
669                    "{}",
670                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
671                )?;
672                write!(self.f, "{}", Self::RTCORNER)?;
673            } else {
674                let mut has_adjacent_nodes = false;
675                for i in 0..(root.width - x) {
676                    has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y);
677                }
678                if !has_adjacent_nodes {
679                    // There are no nodes to the right side of this position
680                    // no need to fill the empty space
681                    continue;
682                }
683                // there are nodes next to this, fill the space
684                write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH))?;
685            }
686        }
687        writeln!(self.f)?;
688
689        Ok(())
690    }
691
692    /// Renders the content layer of boxes at the given y-level of the tree.
693    /// This includes:
694    /// - Node names and extra information
695    /// - Vertical borders (│) for boxes
696    /// - Vertical connections between nodes
697    fn render_box_content(
698        &mut self,
699        root: &RenderTree,
700        y: usize,
701    ) -> Result<(), fmt::Error> {
702        let mut extra_info: Vec<Vec<String>> = vec![vec![]; root.width];
703        let mut extra_height = 0;
704
705        for (x, extra_info_item) in extra_info.iter_mut().enumerate().take(root.width) {
706            if let Some(node) = root.get_node(x, y) {
707                Self::split_up_extra_info(
708                    &node.extra_text,
709                    extra_info_item,
710                    Self::MAX_EXTRA_LINES,
711                );
712                if extra_info_item.len() > extra_height {
713                    extra_height = extra_info_item.len();
714                }
715            }
716        }
717
718        let halfway_point = extra_height.div_ceil(2);
719
720        // Render the actual node.
721        for render_y in 0..=extra_height {
722            for (x, _) in root.nodes.iter().enumerate().take(root.width) {
723                if self.maximum_render_width > 0
724                    && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width
725                {
726                    break;
727                }
728
729                let mut has_adjacent_nodes = false;
730                for i in 0..(root.width - x) {
731                    has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y);
732                }
733
734                if let Some(node) = root.get_node(x, y) {
735                    write!(self.f, "{}", Self::VERTICAL)?;
736
737                    // Rigure out what to render.
738                    let mut render_text = String::new();
739                    if render_y == 0 {
740                        render_text = node.name.clone();
741                    } else if render_y <= extra_info[x].len() {
742                        render_text = extra_info[x][render_y - 1].clone();
743                    }
744
745                    render_text = Self::adjust_text_for_rendering(
746                        &render_text,
747                        Self::NODE_RENDER_WIDTH - 2,
748                    );
749                    write!(self.f, "{render_text}")?;
750
751                    if render_y == halfway_point && node.child_positions.len() > 1 {
752                        write!(self.f, "{}", Self::LMIDDLE)?;
753                    } else {
754                        write!(self.f, "{}", Self::VERTICAL)?;
755                    }
756                } else if render_y == halfway_point {
757                    let has_child_to_the_right =
758                        Self::should_render_whitespace(root, x, y);
759                    if root.has_node(x, y + 1) {
760                        // Node right below this one.
761                        write!(
762                            self.f,
763                            "{}",
764                            Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2)
765                        )?;
766                        if has_child_to_the_right {
767                            write!(self.f, "{}", Self::TMIDDLE)?;
768                            // Have another child to the right, Keep rendering the line.
769                            write!(
770                                self.f,
771                                "{}",
772                                Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2)
773                            )?;
774                        } else {
775                            write!(self.f, "{}", Self::RTCORNER)?;
776                            if has_adjacent_nodes {
777                                // Only a child below this one: fill the reset with spaces.
778                                write!(
779                                    self.f,
780                                    "{}",
781                                    " ".repeat(Self::NODE_RENDER_WIDTH / 2)
782                                )?;
783                            }
784                        }
785                    } else if has_child_to_the_right {
786                        // Child to the right, but no child right below this one: render a full
787                        // line.
788                        write!(
789                            self.f,
790                            "{}",
791                            Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH)
792                        )?;
793                    } else if has_adjacent_nodes {
794                        // Empty spot: render spaces.
795                        write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
796                    }
797                } else if render_y >= halfway_point {
798                    if root.has_node(x, y + 1) {
799                        // Have a node below this empty spot: render a vertical line.
800                        write!(
801                            self.f,
802                            "{}{}",
803                            " ".repeat(Self::NODE_RENDER_WIDTH / 2),
804                            Self::VERTICAL
805                        )?;
806                        if has_adjacent_nodes
807                            || Self::should_render_whitespace(root, x, y)
808                        {
809                            write!(
810                                self.f,
811                                "{}",
812                                " ".repeat(Self::NODE_RENDER_WIDTH / 2)
813                            )?;
814                        }
815                    } else if has_adjacent_nodes
816                        || Self::should_render_whitespace(root, x, y)
817                    {
818                        // Empty spot: render spaces.
819                        write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
820                    }
821                } else if has_adjacent_nodes {
822                    // Empty spot: render spaces.
823                    write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?;
824                }
825            }
826            writeln!(self.f)?;
827        }
828
829        Ok(())
830    }
831
832    /// Renders the bottom layer of boxes at the given y-level of the tree.
833    /// This includes:
834    /// - Bottom corners (└─┘) for nodes
835    /// - Horizontal connections between nodes
836    /// - Vertical connections to child nodes
837    fn render_bottom_layer(
838        &mut self,
839        root: &RenderTree,
840        y: usize,
841    ) -> Result<(), fmt::Error> {
842        for x in 0..=root.width {
843            if self.maximum_render_width > 0
844                && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width
845            {
846                break;
847            }
848            let mut has_adjacent_nodes = false;
849            for i in 0..(root.width - x) {
850                has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y);
851            }
852            if root.get_node(x, y).is_some() {
853                write!(self.f, "{}", Self::LDCORNER)?;
854                write!(
855                    self.f,
856                    "{}",
857                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
858                )?;
859                if root.has_node(x, y + 1) {
860                    // node below this one: connect to that one
861                    write!(self.f, "{}", Self::TMIDDLE)?;
862                } else {
863                    // no node below this one: end the box
864                    write!(self.f, "{}", Self::HORIZONTAL)?;
865                }
866                write!(
867                    self.f,
868                    "{}",
869                    Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1)
870                )?;
871                write!(self.f, "{}", Self::RDCORNER)?;
872            } else if root.has_node(x, y + 1) {
873                write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH / 2))?;
874                write!(self.f, "{}", Self::VERTICAL)?;
875                if has_adjacent_nodes || Self::should_render_whitespace(root, x, y) {
876                    write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH / 2))?;
877                }
878            } else if has_adjacent_nodes || Self::should_render_whitespace(root, x, y) {
879                write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH))?;
880            }
881        }
882        writeln!(self.f)?;
883
884        Ok(())
885    }
886
887    fn extra_info_separator() -> String {
888        "-".repeat(Self::NODE_RENDER_WIDTH - 9)
889    }
890
891    fn remove_padding(s: &str) -> String {
892        s.trim().to_string()
893    }
894
895    pub fn split_up_extra_info(
896        extra_info: &HashMap<String, String>,
897        result: &mut Vec<String>,
898        max_lines: usize,
899    ) {
900        if extra_info.is_empty() {
901            return;
902        }
903
904        result.push(Self::extra_info_separator());
905
906        let mut requires_padding = false;
907        let mut was_inlined = false;
908
909        // use BTreeMap for repeatable key order
910        let sorted_extra_info: BTreeMap<_, _> = extra_info.iter().collect();
911        for (key, value) in sorted_extra_info {
912            let mut str = Self::remove_padding(value);
913            let mut is_inlined = false;
914            let available_width = Self::NODE_RENDER_WIDTH - 7;
915            let total_size = key.len() + str.len() + 2;
916            let is_multiline = str.contains('\n');
917
918            if str.is_empty() {
919                str = key.to_string();
920            } else if !is_multiline && total_size < available_width {
921                str = format!("{key}: {str}");
922                is_inlined = true;
923            } else {
924                str = format!("{key}:\n{str}");
925            }
926
927            if is_inlined && was_inlined {
928                requires_padding = false;
929            }
930
931            if requires_padding {
932                result.push(String::new());
933            }
934
935            let mut splits: Vec<String> = str.split('\n').map(String::from).collect();
936            if splits.len() > max_lines {
937                let mut truncated_splits = Vec::new();
938                for split in splits.iter().take(max_lines / 2) {
939                    truncated_splits.push(split.clone());
940                }
941                truncated_splits.push("...".to_string());
942                for split in splits.iter().skip(splits.len() - max_lines / 2) {
943                    truncated_splits.push(split.clone());
944                }
945                splits = truncated_splits;
946            }
947            for split in splits {
948                Self::split_string_buffer(&split, result);
949            }
950            if result.len() > max_lines {
951                result.truncate(max_lines);
952                result.push("...".to_string());
953            }
954
955            requires_padding = true;
956            was_inlined = is_inlined;
957        }
958    }
959
960    /// Adjusts text to fit within the specified width by:
961    /// 1. Truncating with ellipsis if too long
962    /// 2. Center-aligning within the available space if shorter
963    fn adjust_text_for_rendering(source: &str, max_render_width: usize) -> String {
964        let render_width = source.chars().count();
965        if render_width > max_render_width {
966            let truncated = &source[..max_render_width - 3];
967            format!("{truncated}...")
968        } else {
969            let total_spaces = max_render_width - render_width;
970            let half_spaces = total_spaces / 2;
971            let extra_left_space = if total_spaces.is_multiple_of(2) { 0 } else { 1 };
972            format!(
973                "{}{}{}",
974                " ".repeat(half_spaces + extra_left_space),
975                source,
976                " ".repeat(half_spaces)
977            )
978        }
979    }
980
981    /// Determines if whitespace should be rendered at a given position.
982    /// This is important for:
983    /// 1. Maintaining proper spacing between sibling nodes
984    /// 2. Ensuring correct alignment of connections between parents and children
985    /// 3. Preserving the tree structure's visual clarity
986    fn should_render_whitespace(root: &RenderTree, x: usize, y: usize) -> bool {
987        let mut found_children = 0;
988
989        for i in (0..=x).rev() {
990            let node = root.get_node(i, y);
991            if root.has_node(i, y + 1) {
992                found_children += 1;
993            }
994            if let Some(node) = node {
995                if node.child_positions.len() > 1
996                    && found_children < node.child_positions.len()
997                {
998                    return true;
999                }
1000
1001                return false;
1002            }
1003        }
1004
1005        false
1006    }
1007
1008    fn split_string_buffer(source: &str, result: &mut Vec<String>) {
1009        let mut character_pos = 0;
1010        let mut start_pos = 0;
1011        let mut render_width = 0;
1012        let mut last_possible_split = 0;
1013
1014        let chars: Vec<char> = source.chars().collect();
1015
1016        while character_pos < chars.len() {
1017            // Treating each char as width 1 for simplification
1018            let char_width = 1;
1019
1020            // Does the next character make us exceed the line length?
1021            if render_width + char_width > Self::NODE_RENDER_WIDTH - 2 {
1022                if start_pos + 8 > last_possible_split {
1023                    // The last character we can split on is one of the first 8 characters of the line
1024                    // to not create very small lines we instead split on the current character
1025                    last_possible_split = character_pos;
1026                }
1027
1028                result.push(source[start_pos..last_possible_split].to_string());
1029                render_width = character_pos - last_possible_split;
1030                start_pos = last_possible_split;
1031                character_pos = last_possible_split;
1032            }
1033
1034            // check if we can split on this character
1035            if Self::can_split_on_this_char(chars[character_pos]) {
1036                last_possible_split = character_pos;
1037            }
1038
1039            character_pos += 1;
1040            render_width += char_width;
1041        }
1042
1043        if source.len() > start_pos {
1044            // append the remainder of the input
1045            result.push(source[start_pos..].to_string());
1046        }
1047    }
1048
1049    fn can_split_on_this_char(c: char) -> bool {
1050        (!c.is_ascii_digit() && !c.is_ascii_uppercase() && !c.is_ascii_lowercase())
1051            && c != '_'
1052    }
1053}
1054
1055/// Trait for types which could have additional details when formatted in `Verbose` mode
1056pub trait DisplayAs {
1057    /// Format according to `DisplayFormatType`, used when verbose representation looks
1058    /// different from the default one
1059    ///
1060    /// Should not include a newline
1061    fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result;
1062}
1063
1064/// A new type wrapper to display `T` implementing`DisplayAs` using the `Default` mode
1065pub struct DefaultDisplay<T>(pub T);
1066
1067impl<T: DisplayAs> fmt::Display for DefaultDisplay<T> {
1068    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1069        self.0.fmt_as(DisplayFormatType::Default, f)
1070    }
1071}
1072
1073/// A new type wrapper to display `T` implementing `DisplayAs` using the `Verbose` mode
1074pub struct VerboseDisplay<T>(pub T);
1075
1076impl<T: DisplayAs> fmt::Display for VerboseDisplay<T> {
1077    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1078        self.0.fmt_as(DisplayFormatType::Verbose, f)
1079    }
1080}
1081
1082/// A wrapper to customize partitioned file display
1083#[derive(Debug)]
1084pub struct ProjectSchemaDisplay<'a>(pub &'a SchemaRef);
1085
1086impl fmt::Display for ProjectSchemaDisplay<'_> {
1087    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1088        let parts: Vec<_> = self
1089            .0
1090            .fields()
1091            .iter()
1092            .map(|x| x.name().to_owned())
1093            .collect::<Vec<String>>();
1094        write!(f, "[{}]", parts.join(", "))
1095    }
1096}
1097
1098pub fn display_orderings(f: &mut Formatter, orderings: &[LexOrdering]) -> fmt::Result {
1099    if !orderings.is_empty() {
1100        let start = if orderings.len() == 1 {
1101            ", output_ordering="
1102        } else {
1103            ", output_orderings=["
1104        };
1105        write!(f, "{start}")?;
1106        for (idx, ordering) in orderings.iter().enumerate() {
1107            match idx {
1108                0 => write!(f, "[{ordering}]")?,
1109                _ => write!(f, ", [{ordering}]")?,
1110            }
1111        }
1112        let end = if orderings.len() == 1 { "" } else { "]" };
1113        write!(f, "{end}")?;
1114    }
1115    Ok(())
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120    use std::fmt::Write;
1121    use std::sync::Arc;
1122
1123    use datafusion_common::{internal_datafusion_err, Result, Statistics};
1124    use datafusion_execution::{SendableRecordBatchStream, TaskContext};
1125
1126    use crate::{DisplayAs, ExecutionPlan, PlanProperties};
1127
1128    use super::DisplayableExecutionPlan;
1129
1130    #[derive(Debug, Clone, Copy)]
1131    enum TestStatsExecPlan {
1132        Panic,
1133        Error,
1134        Ok,
1135    }
1136
1137    impl DisplayAs for TestStatsExecPlan {
1138        fn fmt_as(
1139            &self,
1140            _t: crate::DisplayFormatType,
1141            f: &mut std::fmt::Formatter,
1142        ) -> std::fmt::Result {
1143            write!(f, "TestStatsExecPlan")
1144        }
1145    }
1146
1147    impl ExecutionPlan for TestStatsExecPlan {
1148        fn name(&self) -> &'static str {
1149            "TestStatsExecPlan"
1150        }
1151
1152        fn as_any(&self) -> &dyn std::any::Any {
1153            self
1154        }
1155
1156        fn properties(&self) -> &PlanProperties {
1157            unimplemented!()
1158        }
1159
1160        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1161            vec![]
1162        }
1163
1164        fn with_new_children(
1165            self: Arc<Self>,
1166            _: Vec<Arc<dyn ExecutionPlan>>,
1167        ) -> Result<Arc<dyn ExecutionPlan>> {
1168            unimplemented!()
1169        }
1170
1171        fn execute(
1172            &self,
1173            _: usize,
1174            _: Arc<TaskContext>,
1175        ) -> Result<SendableRecordBatchStream> {
1176            todo!()
1177        }
1178
1179        fn statistics(&self) -> Result<Statistics> {
1180            self.partition_statistics(None)
1181        }
1182
1183        fn partition_statistics(&self, partition: Option<usize>) -> Result<Statistics> {
1184            if partition.is_some() {
1185                return Ok(Statistics::new_unknown(self.schema().as_ref()));
1186            }
1187            match self {
1188                Self::Panic => panic!("expected panic"),
1189                Self::Error => Err(internal_datafusion_err!("expected error")),
1190                Self::Ok => Ok(Statistics::new_unknown(self.schema().as_ref())),
1191            }
1192        }
1193    }
1194
1195    fn test_stats_display(exec: TestStatsExecPlan, show_stats: bool) {
1196        let display =
1197            DisplayableExecutionPlan::new(&exec).set_show_statistics(show_stats);
1198
1199        let mut buf = String::new();
1200        write!(&mut buf, "{}", display.one_line()).unwrap();
1201        let buf = buf.trim();
1202        assert_eq!(buf, "TestStatsExecPlan");
1203    }
1204
1205    #[test]
1206    fn test_display_when_stats_panic_with_no_show_stats() {
1207        test_stats_display(TestStatsExecPlan::Panic, false);
1208    }
1209
1210    #[test]
1211    fn test_display_when_stats_error_with_no_show_stats() {
1212        test_stats_display(TestStatsExecPlan::Error, false);
1213    }
1214
1215    #[test]
1216    fn test_display_when_stats_ok_with_no_show_stats() {
1217        test_stats_display(TestStatsExecPlan::Ok, false);
1218    }
1219
1220    #[test]
1221    #[should_panic(expected = "expected panic")]
1222    fn test_display_when_stats_panic_with_show_stats() {
1223        test_stats_display(TestStatsExecPlan::Panic, true);
1224    }
1225
1226    #[test]
1227    #[should_panic(expected = "Error")] // fmt::Error
1228    fn test_display_when_stats_error_with_show_stats() {
1229        test_stats_display(TestStatsExecPlan::Error, true);
1230    }
1231
1232    #[test]
1233    fn test_display_when_stats_ok_with_show_stats() {
1234        test_stats_display(TestStatsExecPlan::Ok, false);
1235    }
1236}