This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new ee42e8e6c3 feat: Add graphviz display format for execution plan. 
(#6726)
ee42e8e6c3 is described below

commit ee42e8e6c3d6c6c23bdce8f6c367020b7873bff2
Author: Renjie Liu <[email protected]>
AuthorDate: Fri Jul 7 21:02:16 2023 +0800

    feat: Add graphviz display format for execution plan. (#6726)
    
    * Implement graphviz format for execution plan
    
    * Update cargo.lock
    
    * fix ci
    
    * fix test
    
    * Fix comment
    
    * Resolve conflicts with main
---
 datafusion-cli/Cargo.lock                          |  32 +++---
 datafusion/common/src/display/graphviz.rs          | 105 +++++++++++++++++
 .../common/src/{display.rs => display/mod.rs}      |   3 +
 datafusion/core/src/physical_plan/display.rs       | 127 +++++++++++++++++++++
 datafusion/core/src/physical_plan/streaming.rs     |   3 +-
 datafusion/core/src/physical_planner.rs            |  30 +++++
 datafusion/core/tests/sql/explain_analyze.rs       |  16 ++-
 datafusion/expr/src/logical_plan/display.rs        |  58 +++-------
 datafusion/expr/src/logical_plan/plan.rs           |  72 +++++++-----
 9 files changed, 350 insertions(+), 96 deletions(-)

diff --git a/datafusion-cli/Cargo.lock b/datafusion-cli/Cargo.lock
index 9265f54e8a..149c6e2c5b 100644
--- a/datafusion-cli/Cargo.lock
+++ b/datafusion-cli/Cargo.lock
@@ -710,9 +710,9 @@ dependencies = [
 
 [[package]]
 name = "blake3"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888"
+checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5"
 dependencies = [
  "arrayref",
  "arrayvec",
@@ -947,9 +947,9 @@ dependencies = [
 
 [[package]]
 name = "constant_time_eq"
-version = "0.2.6"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
+checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
 
 [[package]]
 name = "core-foundation"
@@ -2779,9 +2779,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-webpki"
-version = "0.101.0"
+version = "0.101.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "89efed4bd0af2a8de0feb22ba38030244c93db56112b8aa67d27022286852b1c"
+checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
 dependencies = [
  "ring",
  "untrusted",
@@ -2893,18 +2893,18 @@ checksum = 
"63134939175b3131fe4d2c131b103fd42f25ccca89423d43b5e4f267920ccf03"
 
 [[package]]
 name = "serde"
-version = "1.0.166"
+version = "1.0.167"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
+checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.166"
+version = "1.0.167"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
+checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2962,9 +2962,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
 
 [[package]]
 name = "snafu"
@@ -3155,18 +3155,18 @@ checksum = 
"222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
 
 [[package]]
 name = "thiserror"
-version = "1.0.41"
+version = "1.0.43"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802"
+checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.41"
+version = "1.0.43"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59"
+checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/datafusion/common/src/display/graphviz.rs 
b/datafusion/common/src/display/graphviz.rs
new file mode 100644
index 0000000000..f84490cd3e
--- /dev/null
+++ b/datafusion/common/src/display/graphviz.rs
@@ -0,0 +1,105 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Logic related to creating DOT language graphs.
+
+use std::fmt;
+
+#[derive(Default)]
+pub struct GraphvizBuilder {
+    id_gen: usize,
+}
+
+impl GraphvizBuilder {
+    // Generate next id in graphviz.
+    pub fn next_id(&mut self) -> usize {
+        self.id_gen += 1;
+        self.id_gen
+    }
+
+    // Write out the start of whole graph.
+    pub fn start_graph(&mut self, f: &mut fmt::Formatter) -> fmt::Result {
+        writeln!(
+            f,
+            r#"
+// Begin DataFusion GraphViz Plan,
+// display it online here: https://dreampuf.github.io/GraphvizOnline
+"#
+        )?;
+        writeln!(f, "digraph {{")
+    }
+
+    pub fn end_graph(&mut self, f: &mut fmt::Formatter) -> fmt::Result {
+        writeln!(f, "}}")?;
+        writeln!(f, "// End DataFusion GraphViz Plan")
+    }
+
+    // write out the start of the subgraph cluster
+    pub fn start_cluster(&mut self, f: &mut fmt::Formatter, title: &str) -> 
fmt::Result {
+        writeln!(f, "  subgraph cluster_{}", self.next_id())?;
+        writeln!(f, "  {{")?;
+        writeln!(f, "    graph[label={}]", Self::quoted(title))
+    }
+
+    // write out the end of the subgraph cluster
+    pub fn end_cluster(&mut self, f: &mut fmt::Formatter) -> fmt::Result {
+        writeln!(f, "  }}")
+    }
+
+    /// makes a quoted string suitable for inclusion in a graphviz chart
+    pub fn quoted(label: &str) -> String {
+        let label = label.replace('"', "_");
+        format!("\"{label}\"")
+    }
+
+    pub fn add_node(
+        &self,
+        f: &mut fmt::Formatter,
+        id: usize,
+        label: &str,
+        tooltip: Option<&str>,
+    ) -> fmt::Result {
+        if let Some(tooltip) = tooltip {
+            writeln!(
+                f,
+                "    {}[shape=box label={}, tooltip={}]",
+                id,
+                GraphvizBuilder::quoted(label),
+                GraphvizBuilder::quoted(tooltip),
+            )
+        } else {
+            writeln!(
+                f,
+                "    {}[shape=box label={}]",
+                id,
+                GraphvizBuilder::quoted(label),
+            )
+        }
+    }
+
+    pub fn add_edge(
+        &self,
+        f: &mut fmt::Formatter,
+        from_id: usize,
+        to_id: usize,
+    ) -> fmt::Result {
+        writeln!(
+            f,
+            "    {from_id} -> {to_id} [arrowhead=none, arrowtail=normal, 
dir=back]"
+        )
+    }
+}
diff --git a/datafusion/common/src/display.rs 
b/datafusion/common/src/display/mod.rs
similarity index 99%
rename from datafusion/common/src/display.rs
rename to datafusion/common/src/display/mod.rs
index 79de9bc031..766b37ce28 100644
--- a/datafusion/common/src/display.rs
+++ b/datafusion/common/src/display/mod.rs
@@ -17,6 +17,9 @@
 
 //! Types for plan display
 
+mod graphviz;
+pub use graphviz::*;
+
 use std::{
     fmt::{self, Display, Formatter},
     sync::Arc,
diff --git a/datafusion/core/src/physical_plan/display.rs 
b/datafusion/core/src/physical_plan/display.rs
index 674cbcf247..63b4f0c28c 100644
--- a/datafusion/core/src/physical_plan/display.rs
+++ b/datafusion/core/src/physical_plan/display.rs
@@ -20,10 +20,12 @@
 //! format
 
 use std::fmt;
+use std::fmt::Formatter;
 
 use datafusion_common::display::StringifiedPlan;
 
 use super::{accept, ExecutionPlan, ExecutionPlanVisitor};
+use datafusion_common::display::GraphvizBuilder;
 
 /// Options for controlling how each [`ExecutionPlan`] should format itself
 #[derive(Debug, Clone, Copy)]
@@ -110,6 +112,49 @@ impl<'a> DisplayableExecutionPlan<'a> {
         }
     }
 
+    /// Returns a `format`able structure that produces graphviz format for 
execution plan, which can
+    /// be directly visualized 
[here](https://dreampuf.github.io/GraphvizOnline).
+    ///
+    /// An example is
+    /// ```dot
+    /// strict digraph dot_plan {
+    //     0[label="ProjectionExec: expr=[id@0 + 2 as employee.id + 
Int32(2)]",tooltip=""]
+    //     1[label="EmptyExec: produce_one_row=false",tooltip=""]
+    //     0 -> 1
+    // }
+    /// ```
+    pub fn graphviz(&self) -> impl fmt::Display + 'a {
+        struct Wrapper<'a> {
+            plan: &'a dyn ExecutionPlan,
+            show_metrics: ShowMetrics,
+        }
+        impl<'a> fmt::Display for Wrapper<'a> {
+            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                let t = DisplayFormatType::Default;
+
+                let mut visitor = GraphvizVisitor {
+                    f,
+                    t,
+                    show_metrics: self.show_metrics,
+                    graphviz_builder: GraphvizBuilder::default(),
+                    parents: Vec::new(),
+                };
+
+                visitor.start_graph()?;
+
+                accept(self.plan, &mut visitor)?;
+
+                visitor.end_graph()?;
+                Ok(())
+            }
+        }
+
+        Wrapper {
+            plan: self.inner,
+            show_metrics: self.show_metrics,
+        }
+    }
+
     /// Return a single-line summary of the root of the plan
     /// Example: `ProjectionExec: expr=[a@0 as a]`.
     pub fn one_line(&self) -> impl fmt::Display + 'a {
@@ -209,6 +254,88 @@ impl<'a, 'b> ExecutionPlanVisitor for IndentVisitor<'a, 
'b> {
     }
 }
 
+struct GraphvizVisitor<'a, 'b> {
+    f: &'a mut Formatter<'b>,
+    /// How to format each node
+    t: DisplayFormatType,
+    /// How to show metrics
+    show_metrics: ShowMetrics,
+    graphviz_builder: GraphvizBuilder,
+    /// Used to record parent node ids when visiting a plan.
+    parents: Vec<usize>,
+}
+
+impl GraphvizVisitor<'_, '_> {
+    fn start_graph(&mut self) -> fmt::Result {
+        self.graphviz_builder.start_graph(self.f)
+    }
+
+    fn end_graph(&mut self) -> fmt::Result {
+        self.graphviz_builder.end_graph(self.f)
+    }
+}
+
+impl ExecutionPlanVisitor for GraphvizVisitor<'_, '_> {
+    type Error = fmt::Error;
+
+    fn pre_visit(
+        &mut self,
+        plan: &dyn ExecutionPlan,
+    ) -> datafusion_common::Result<bool, Self::Error> {
+        let id = self.graphviz_builder.next_id();
+
+        struct Wrapper<'a>(&'a dyn ExecutionPlan, DisplayFormatType);
+
+        impl<'a> std::fmt::Display for Wrapper<'a> {
+            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+                self.0.fmt_as(self.1, f)
+            }
+        }
+
+        let label = { format!("{}", Wrapper(plan, self.t)) };
+
+        let metrics = match self.show_metrics {
+            ShowMetrics::None => "".to_string(),
+            ShowMetrics::Aggregated => {
+                if let Some(metrics) = plan.metrics() {
+                    let metrics = metrics
+                        .aggregate_by_name()
+                        .sorted_for_display()
+                        .timestamps_removed();
+
+                    format!("metrics=[{metrics}]")
+                } else {
+                    "metrics=[]".to_string()
+                }
+            }
+            ShowMetrics::Full => {
+                if let Some(metrics) = plan.metrics() {
+                    format!("metrics=[{metrics}]")
+                } else {
+                    "metrics=[]".to_string()
+                }
+            }
+        };
+
+        self.graphviz_builder
+            .add_node(self.f, id, &label, Some(&metrics))?;
+
+        if let Some(parent_node_id) = self.parents.last() {
+            self.graphviz_builder
+                .add_edge(self.f, *parent_node_id, id)?;
+        }
+
+        self.parents.push(id);
+
+        Ok(true)
+    }
+
+    fn post_visit(&mut self, _plan: &dyn ExecutionPlan) -> Result<bool, 
Self::Error> {
+        self.parents.pop();
+        Ok(true)
+    }
+}
+
 /// Trait for types which could have additional details when formatted in 
`Verbose` mode
 pub trait DisplayAs {
     /// Format according to `DisplayFormatType`, used when verbose 
representation looks
diff --git a/datafusion/core/src/physical_plan/streaming.rs 
b/datafusion/core/src/physical_plan/streaming.rs
index 97b244d1ac..f06f61ca1e 100644
--- a/datafusion/core/src/physical_plan/streaming.rs
+++ b/datafusion/core/src/physical_plan/streaming.rs
@@ -28,12 +28,11 @@ use datafusion_common::{DataFusionError, Result, 
Statistics};
 use datafusion_physical_expr::{LexOrdering, PhysicalSortExpr};
 use log::debug;
 
-use crate::datasource::physical_plan::{OutputOrderingDisplay, 
ProjectSchemaDisplay};
 use crate::physical_plan::stream::RecordBatchStreamAdapter;
 use crate::physical_plan::{ExecutionPlan, Partitioning, 
SendableRecordBatchStream};
 use datafusion_execution::TaskContext;
 
-use super::{DisplayAs, DisplayFormatType};
+use super::DisplayAs;
 
 /// A partition that can be converted into a [`SendableRecordBatchStream`]
 pub trait PartitionStream: Send + Sync {
diff --git a/datafusion/core/src/physical_planner.rs 
b/datafusion/core/src/physical_planner.rs
index 92c7604c37..de34353d59 100644
--- a/datafusion/core/src/physical_planner.rs
+++ b/datafusion/core/src/physical_planner.rs
@@ -2610,4 +2610,34 @@ mod tests {
             ctx.read_csv(path, options).await?.into_optimized_plan()?,
         ))
     }
+
+    #[tokio::test]
+    async fn test_display_plan_in_graphviz_format() {
+        let schema = Schema::new(vec![Field::new("id", DataType::Int32, 
false)]);
+
+        let logical_plan = scan_empty(Some("employee"), &schema, None)
+            .unwrap()
+            .project(vec![col("id") + lit(2)])
+            .unwrap()
+            .build()
+            .unwrap();
+
+        let plan = plan(&logical_plan).await.unwrap();
+
+        let expected_graph = r###"
+// Begin DataFusion GraphViz Plan,
+// display it online here: https://dreampuf.github.io/GraphvizOnline
+
+digraph {
+    1[shape=box label="ProjectionExec: expr=[id@0 + 2 as employee.id + 
Int32(2)]", tooltip=""]
+    2[shape=box label="EmptyExec: produce_one_row=false", tooltip=""]
+    1 -> 2 [arrowhead=none, arrowtail=normal, dir=back]
+}
+// End DataFusion GraphViz Plan
+"###;
+
+        let generated_graph = format!("{}", displayable(&*plan).graphviz());
+
+        assert_eq!(expected_graph, generated_graph);
+    }
 }
diff --git a/datafusion/core/tests/sql/explain_analyze.rs 
b/datafusion/core/tests/sql/explain_analyze.rs
index e0130cb09c..9125dc0ba7 100644
--- a/datafusion/core/tests/sql/explain_analyze.rs
+++ b/datafusion/core/tests/sql/explain_analyze.rs
@@ -210,7 +210,9 @@ async fn csv_explain_plans() {
     //
     // verify the grahviz format of the plan
     let expected = vec![
-        "// Begin DataFusion GraphViz Plan (see https://graphviz.org)",
+        "// Begin DataFusion GraphViz Plan,",
+        "// display it online here: https://dreampuf.github.io/GraphvizOnline";,
+        "",
         "digraph {",
         "  subgraph cluster_1",
         "  {",
@@ -282,7 +284,9 @@ async fn csv_explain_plans() {
     //
     // verify the grahviz format of the plan
     let expected = vec![
-        "// Begin DataFusion GraphViz Plan (see https://graphviz.org)",
+        "// Begin DataFusion GraphViz Plan,",
+        "// display it online here: https://dreampuf.github.io/GraphvizOnline";,
+        "",
         "digraph {",
         "  subgraph cluster_1",
         "  {",
@@ -427,7 +431,9 @@ async fn csv_explain_verbose_plans() {
     //
     // verify the grahviz format of the plan
     let expected = vec![
-        "// Begin DataFusion GraphViz Plan (see https://graphviz.org)",
+        "// Begin DataFusion GraphViz Plan,",
+        "// display it online here: https://dreampuf.github.io/GraphvizOnline";,
+        "",
         "digraph {",
         "  subgraph cluster_1",
         "  {",
@@ -499,7 +505,9 @@ async fn csv_explain_verbose_plans() {
     //
     // verify the grahviz format of the plan
     let expected = vec![
-        "// Begin DataFusion GraphViz Plan (see https://graphviz.org)",
+        "// Begin DataFusion GraphViz Plan,",
+        "// display it online here: https://dreampuf.github.io/GraphvizOnline";,
+        "",
         "digraph {",
         "  subgraph cluster_1",
         "  {",
diff --git a/datafusion/expr/src/logical_plan/display.rs 
b/datafusion/expr/src/logical_plan/display.rs
index c82689b2cc..112dbf74db 100644
--- a/datafusion/expr/src/logical_plan/display.rs
+++ b/datafusion/expr/src/logical_plan/display.rs
@@ -18,6 +18,7 @@
 
 use crate::LogicalPlan;
 use arrow::datatypes::Schema;
+use datafusion_common::display::GraphvizBuilder;
 use datafusion_common::tree_node::{TreeNodeVisitor, VisitRecursion};
 use datafusion_common::DataFusionError;
 use std::fmt;
@@ -123,37 +124,6 @@ pub fn display_schema(schema: &Schema) -> impl 
fmt::Display + '_ {
     Wrapper(schema)
 }
 
-/// Logic related to creating DOT language graphs.
-#[derive(Default)]
-struct GraphvizBuilder {
-    id_gen: usize,
-}
-
-impl GraphvizBuilder {
-    fn next_id(&mut self) -> usize {
-        self.id_gen += 1;
-        self.id_gen
-    }
-
-    // write out the start of the subgraph cluster
-    fn start_cluster(&mut self, f: &mut fmt::Formatter, title: &str) -> 
fmt::Result {
-        writeln!(f, "  subgraph cluster_{}", self.next_id())?;
-        writeln!(f, "  {{")?;
-        writeln!(f, "    graph[label={}]", Self::quoted(title))
-    }
-
-    // write out the end of the subgraph cluster
-    fn end_cluster(&mut self, f: &mut fmt::Formatter) -> fmt::Result {
-        writeln!(f, "  }}")
-    }
-
-    /// makes a quoted string suitable for inclusion in a graphviz chart
-    fn quoted(label: &str) -> String {
-        let label = label.replace('"', "_");
-        format!("\"{label}\"")
-    }
-}
-
 /// Formats plans for graphical display using the `DOT` language. This
 /// format can be visualized using software from
 /// [`graphviz`](https://graphviz.org/)
@@ -190,6 +160,14 @@ impl<'a, 'b> GraphvizVisitor<'a, 'b> {
     pub fn post_visit_plan(&mut self) -> fmt::Result {
         self.graphviz_builder.end_cluster(self.f)
     }
+
+    pub fn start_graph(&mut self) -> fmt::Result {
+        self.graphviz_builder.start_graph(self.f)
+    }
+
+    pub fn end_graph(&mut self) -> fmt::Result {
+        self.graphviz_builder.end_graph(self.f)
+    }
 }
 
 impl<'a, 'b> TreeNodeVisitor for GraphvizVisitor<'a, 'b> {
@@ -213,22 +191,16 @@ impl<'a, 'b> TreeNodeVisitor for GraphvizVisitor<'a, 'b> {
             format!("{}", plan.display())
         };
 
-        writeln!(
-            self.f,
-            "    {}[shape=box label={}]",
-            id,
-            GraphvizBuilder::quoted(&label)
-        )
-        .map_err(|_e| DataFusionError::Internal("Fail to 
format".to_string()))?;
+        self.graphviz_builder
+            .add_node(self.f, id, &label, None)
+            .map_err(|_e| DataFusionError::Internal("Fail to 
format".to_string()))?;
 
         // Create an edge to our parent node, if any
         //  parent_id -> id
         if let Some(parent_id) = self.parent_ids.last() {
-            writeln!(
-                self.f,
-                "    {parent_id} -> {id} [arrowhead=none, arrowtail=normal, 
dir=back]"
-            )
-            .map_err(|_e| DataFusionError::Internal("Fail to 
format".to_string()))?;
+            self.graphviz_builder
+                .add_edge(self.f, *parent_id, id)
+                .map_err(|_e| DataFusionError::Internal("Fail to 
format".to_string()))?;
         }
 
         self.parent_ids.push(id);
diff --git a/datafusion/expr/src/logical_plan/plan.rs 
b/datafusion/expr/src/logical_plan/plan.rs
index e058708701..d9bb255733 100644
--- a/datafusion/expr/src/logical_plan/plan.rs
+++ b/datafusion/expr/src/logical_plan/plan.rs
@@ -898,14 +898,10 @@ impl LogicalPlan {
         struct Wrapper<'a>(&'a LogicalPlan);
         impl<'a> Display for Wrapper<'a> {
             fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-                writeln!(
-                    f,
-                    "// Begin DataFusion GraphViz Plan (see 
https://graphviz.org)"
-                )?;
-                writeln!(f, "digraph {{")?;
-
                 let mut visitor = GraphvizVisitor::new(f);
 
+                visitor.start_graph()?;
+
                 visitor.pre_visit_plan("LogicalPlan")?;
                 self.0.visit(&mut visitor).map_err(|_| fmt::Error)?;
                 visitor.post_visit_plan()?;
@@ -915,8 +911,7 @@ impl LogicalPlan {
                 self.0.visit(&mut visitor).map_err(|_| fmt::Error)?;
                 visitor.post_visit_plan()?;
 
-                writeln!(f, "}}")?;
-                writeln!(f, "// End DataFusion GraphViz Plan")?;
+                visitor.end_graph()?;
                 Ok(())
             }
         }
@@ -1850,31 +1845,46 @@ mod tests {
     fn test_display_graphviz() -> Result<()> {
         let plan = display_plan()?;
 
+        let expected_graphviz = r###"
+// Begin DataFusion GraphViz Plan,
+// display it online here: https://dreampuf.github.io/GraphvizOnline
+
+digraph {
+  subgraph cluster_1
+  {
+    graph[label="LogicalPlan"]
+    2[shape=box label="Projection: employee_csv.id"]
+    3[shape=box label="Filter: employee_csv.state IN (<subquery>)"]
+    2 -> 3 [arrowhead=none, arrowtail=normal, dir=back]
+    4[shape=box label="Subquery:"]
+    3 -> 4 [arrowhead=none, arrowtail=normal, dir=back]
+    5[shape=box label="TableScan: employee_csv projection=[state]"]
+    4 -> 5 [arrowhead=none, arrowtail=normal, dir=back]
+    6[shape=box label="TableScan: employee_csv projection=[id, state]"]
+    3 -> 6 [arrowhead=none, arrowtail=normal, dir=back]
+  }
+  subgraph cluster_7
+  {
+    graph[label="Detailed LogicalPlan"]
+    8[shape=box label="Projection: employee_csv.id\nSchema: [id:Int32]"]
+    9[shape=box label="Filter: employee_csv.state IN (<subquery>)\nSchema: 
[id:Int32, state:Utf8]"]
+    8 -> 9 [arrowhead=none, arrowtail=normal, dir=back]
+    10[shape=box label="Subquery:\nSchema: [state:Utf8]"]
+    9 -> 10 [arrowhead=none, arrowtail=normal, dir=back]
+    11[shape=box label="TableScan: employee_csv projection=[state]\nSchema: 
[state:Utf8]"]
+    10 -> 11 [arrowhead=none, arrowtail=normal, dir=back]
+    12[shape=box label="TableScan: employee_csv projection=[id, 
state]\nSchema: [id:Int32, state:Utf8]"]
+    9 -> 12 [arrowhead=none, arrowtail=normal, dir=back]
+  }
+}
+// End DataFusion GraphViz Plan
+"###;
+
         // just test for a few key lines in the output rather than the
         // whole thing to make test mainteance easier.
         let graphviz = format!("{}", plan.display_graphviz());
 
-        assert!(
-            graphviz.contains(
-                r#"// Begin DataFusion GraphViz Plan (see 
https://graphviz.org)"#
-            ),
-            "\n{}",
-            plan.display_graphviz()
-        );
-        assert!(
-            graphviz.contains(
-                r#"[shape=box label="TableScan: employee_csv projection=[id, 
state]"]"#
-            ),
-            "\n{}",
-            plan.display_graphviz()
-        );
-        assert!(graphviz.contains(r#"[shape=box label="TableScan: employee_csv 
projection=[id, state]\nSchema: [id:Int32, state:Utf8]"]"#),
-                "\n{}", plan.display_graphviz());
-        assert!(
-            graphviz.contains(r#"// End DataFusion GraphViz Plan"#),
-            "\n{}",
-            plan.display_graphviz()
-        );
+        assert_eq!(expected_graphviz, graphviz);
         Ok(())
     }
 
@@ -1895,7 +1905,7 @@ mod tests {
                 _ => {
                     return Err(DataFusionError::NotImplemented(
                         "unknown plan type".to_string(),
-                    ))
+                    ));
                 }
             };
 
@@ -1911,7 +1921,7 @@ mod tests {
                 _ => {
                     return Err(DataFusionError::NotImplemented(
                         "unknown plan type".to_string(),
-                    ))
+                    ));
                 }
             };
 

Reply via email to