Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): output explain result as graphviz dot format #19446

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading