Skip to content

Commit

Permalink
feat(frontend): output explain result as graphviz dot format (#19446)
Browse files Browse the repository at this point in the history
  • Loading branch information
lyang24 authored Nov 21, 2024
1 parent 075c90d commit a771dab
Show file tree
Hide file tree
Showing 9 changed files with 591 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
- name: test dot output format (logical)
sql: |
CREATE TABLE t (v1 int);
explain (logical, format dot) SELECT approx_percentile(0.5) WITHIN GROUP (order by v1) from t;
expected_outputs:
- explain_output
- name: test dot output format (batch)
sql: |
CREATE TABLE t (v1 int);
explain (physical, format dot) SELECT approx_percentile(0.5) WITHIN GROUP (order by v1) from t;
expected_outputs:
- explain_output
- name: test dot output format (stream)
sql: |
CREATE TABLE t (v1 int);
explain (physical, format dot) create materialized view m1 as SELECT approx_percentile(0.5) WITHIN GROUP (order by v1) from t;
expected_outputs:
- explain_output
- name: test long dot output format (stream)
sql: |
create table t1(a int, b int);
create table t2(c int primary key, d int);
explain (physical, format dot) create materialized view m1 as SELECT
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col1,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col2,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col3,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col4,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col5,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col6,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col7,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col8,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col9,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col10,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col11,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col12,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col13,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col14,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col15,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col16,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col17,
COALESCE((SELECT b FROM t2 WHERE t1.a = t2.c), 0) col18
from t1;
expected_outputs:
- explain_output

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/frontend/src/handler/explain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ async fn do_handle_explain(
ExplainFormat::Json => blocks.push(plan.explain_to_json()),
ExplainFormat::Xml => blocks.push(plan.explain_to_xml()),
ExplainFormat::Yaml => blocks.push(plan.explain_to_yaml()),
ExplainFormat::Dot => blocks.push(plan.explain_to_dot()),
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/src/optimizer/logical_optimization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,9 @@ impl LogicalOptimizer {
ExplainFormat::Yaml => {
ctx.store_logical(plan.explain_to_yaml());
}
ExplainFormat::Dot => {
ctx.store_logical(plan.explain_to_dot());
}
}
}

Expand Down Expand Up @@ -819,6 +822,9 @@ impl LogicalOptimizer {
ExplainFormat::Yaml => {
ctx.store_logical(plan.explain_to_yaml());
}
ExplainFormat::Dot => {
ctx.store_logical(plan.explain_to_dot());
}
}
}

Expand Down
82 changes: 79 additions & 3 deletions src/frontend/src/optimizer/plan_node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
//! - all field should be valued in construction, so the properties' derivation should be finished
//! in the `new()` function.
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::ops::Deref;
Expand All @@ -37,6 +38,8 @@ use dyn_clone::DynClone;
use fixedbitset::FixedBitSet;
use itertools::Itertools;
use paste::paste;
use petgraph::dot::{Config, Dot};
use petgraph::graph::{Graph, NodeIndex};
use pretty_xmlish::{Pretty, PrettyConfig};
use risingwave_common::catalog::Schema;
use risingwave_common::util::recursive::{self, Recurse};
Expand Down Expand Up @@ -642,6 +645,9 @@ pub trait Explain {
/// Write explain the whole plan tree.
fn explain<'a>(&self) -> Pretty<'a>;

/// Write explain the whole plan tree with node id.
fn explain_with_id<'a>(&self) -> Pretty<'a>;

/// Explain the plan node and return a string.
fn explain_to_string(&self) -> String;

Expand All @@ -653,6 +659,9 @@ pub trait Explain {

/// Explain the plan node and return a yaml string.
fn explain_to_yaml(&self) -> String;

/// Explain the plan node and return a dot format string.
fn explain_to_dot(&self) -> String;
}

impl Explain for PlanRef {
Expand All @@ -666,6 +675,21 @@ impl Explain for PlanRef {
Pretty::Record(node)
}

/// Write explain the whole plan tree with node id.
fn explain_with_id<'a>(&self) -> Pretty<'a> {
let node_id = self.id();
let mut node = self.distill();
// NOTE(kwannoel): Can lead to poor performance if plan is very large,
// but we want to show the id first.
node.fields
.insert(0, ("id".into(), Pretty::display(&node_id.0)));
let inputs = self.inputs();
for input in inputs.iter().peekable() {
node.children.push(input.explain_with_id());
}
Pretty::Record(node)
}

/// Explain the plan node and return a string.
fn explain_to_string(&self) -> String {
let plan = reorganize_elements_id(self.clone());
Expand All @@ -680,22 +704,74 @@ impl Explain for PlanRef {
fn explain_to_json(&self) -> String {
let plan = reorganize_elements_id(self.clone());
let explain_ir = plan.explain();
serde_json::to_string_pretty(&PrettySerde(explain_ir))
serde_json::to_string_pretty(&PrettySerde(explain_ir, true))
.expect("failed to serialize plan to json")
}

/// Explain the plan node and return a xml string.
fn explain_to_xml(&self) -> String {
let plan = reorganize_elements_id(self.clone());
let explain_ir = plan.explain();
quick_xml::se::to_string(&PrettySerde(explain_ir)).expect("failed to serialize plan to xml")
quick_xml::se::to_string(&PrettySerde(explain_ir, true))
.expect("failed to serialize plan to xml")
}

/// Explain the plan node and return a yaml string.
fn explain_to_yaml(&self) -> String {
let plan = reorganize_elements_id(self.clone());
let explain_ir = plan.explain();
serde_yaml::to_string(&PrettySerde(explain_ir)).expect("failed to serialize plan to yaml")
serde_yaml::to_string(&PrettySerde(explain_ir, true))
.expect("failed to serialize plan to yaml")
}

/// Explain the plan node and return a dot format string.
fn explain_to_dot(&self) -> String {
let plan = reorganize_elements_id(self.clone());
let explain_ir = plan.explain_with_id();
let mut graph = Graph::<String, String>::new();
let mut nodes = HashMap::new();
build_graph_from_pretty(&explain_ir, &mut graph, &mut nodes, None);
let dot = Dot::with_config(&graph, &[Config::EdgeNoLabel]);
dot.to_string()
}
}

fn build_graph_from_pretty(
pretty: &Pretty<'_>,
graph: &mut Graph<String, String>,
nodes: &mut HashMap<String, NodeIndex>,
parent_label: Option<&str>,
) {
if let Pretty::Record(r) = pretty {
let mut label = String::new();
label.push_str(&r.name);
for (k, v) in &r.fields {
label.push('\n');
label.push_str(k);
label.push_str(": ");
label.push_str(
&serde_json::to_string(&PrettySerde(v.clone(), false))
.expect("failed to serialize plan to dot"),
);
}
// output alignment.
if !r.fields.is_empty() {
label.push('\n');
}

let current_node = *nodes
.entry(label.clone())
.or_insert_with(|| graph.add_node(label.clone()));

if let Some(parent_label) = parent_label {
if let Some(&parent_node) = nodes.get(parent_label) {
graph.add_edge(parent_node, current_node, "contains".to_string());
}
}

for child in &r.children {
build_graph_from_pretty(child, graph, nodes, Some(&label));
}
}
}

Expand Down
30 changes: 17 additions & 13 deletions src/frontend/src/utils/pretty_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ use pretty_xmlish::Pretty;
use serde::ser::{SerializeSeq, SerializeStruct};
use serde::{Serialize, Serializer};

pub struct PrettySerde<'a>(pub Pretty<'a>);
// Second anymous field is include_children.
// If true the children information will be serialized.
pub struct PrettySerde<'a>(pub Pretty<'a>, pub bool);

impl Serialize for PrettySerde<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
Expand All @@ -46,31 +48,33 @@ impl Serialize for PrettySerde<'_> {
&node
.fields
.iter()
.map(|(k, v)| (k.as_ref(), PrettySerde(v.clone())))
.map(|(k, v)| (k.as_ref(), PrettySerde(v.clone(), self.1)))
.collect::<BTreeMap<_, _>>(),
)?;
state.serialize_field(
"children",
&node
.children
.iter()
.map(|c| PrettySerde(c.clone()))
.collect::<Vec<_>>(),
)?;
if self.1 {
state.serialize_field(
"children",
&node
.children
.iter()
.map(|c| PrettySerde(c.clone(), self.1))
.collect::<Vec<_>>(),
)?;
}
state.end()
}

Pretty::Array(elements) => {
let mut seq = serializer.serialize_seq(Some(elements.len()))?;
for element in elements {
seq.serialize_element(&PrettySerde((*element).clone()))?;
seq.serialize_element(&PrettySerde((*element).clone(), self.1))?;
}
seq.end()
}

Pretty::Linearized(inner, size) => {
let mut state = serializer.serialize_struct("Linearized", 2)?;
state.serialize_field("inner", &PrettySerde((**inner).clone()))?;
state.serialize_field("inner", &PrettySerde((**inner).clone(), self.1))?;
state.serialize_field("size", size)?;
state.end()
}
Expand All @@ -94,7 +98,7 @@ mod tests {
#[test]
fn test_pretty_serde() {
let pretty = Pretty::childless_record("root", vec![("a", Pretty::Text("1".into()))]);
let pretty_serde = PrettySerde(pretty);
let pretty_serde = PrettySerde(pretty, true);
let serialized = serde_json::to_string(&pretty_serde).unwrap();
check(
serialized,
Expand Down
2 changes: 2 additions & 0 deletions src/sqlparser/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ pub enum ExplainFormat {
Json,
Xml,
Yaml,
Dot,
}

impl fmt::Display for ExplainFormat {
Expand All @@ -1150,6 +1151,7 @@ impl fmt::Display for ExplainFormat {
ExplainFormat::Json => f.write_str("JSON"),
ExplainFormat::Xml => f.write_str("XML"),
ExplainFormat::Yaml => f.write_str("YAML"),
ExplainFormat::Dot => f.write_str("DOT"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/sqlparser/src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ define_keywords!(
DISTRIBUTED,
DISTSQL,
DO,
DOT,
DOUBLE,
DROP,
DYNAMIC,
Expand Down
2 changes: 2 additions & 0 deletions src/sqlparser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4039,11 +4039,13 @@ impl Parser<'_> {
Keyword::JSON,
Keyword::XML,
Keyword::YAML,
Keyword::DOT,
])? {
Keyword::TEXT => ExplainFormat::Text,
Keyword::JSON => ExplainFormat::Json,
Keyword::XML => ExplainFormat::Xml,
Keyword::YAML => ExplainFormat::Yaml,
Keyword::DOT => ExplainFormat::Dot,
_ => unreachable!("{}", keyword),
}
}
Expand Down

0 comments on commit a771dab

Please sign in to comment.