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