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