1use plexus_serde::{Op, Plan};
2use std::process::{Command, Stdio};
3
4use crate::explain::{op_inputs, op_kind};
5
6pub fn plan_to_dot(plan: &Plan) -> String {
7 let mut out = String::new();
8 out.push_str("digraph plexus_plan {\n");
9 out.push_str(" rankdir=LR;\n");
10 out.push_str(" node [shape=box, fontname=\"Menlo\"];\n");
11
12 for (idx, op) in plan.ops.iter().enumerate() {
13 let label = format!("{}: {}", idx, op_label(op));
14 out.push_str(&format!(" op_{idx} [label=\"{}\"];\n", escape_dot(&label)));
15 }
16
17 for (idx, op) in plan.ops.iter().enumerate() {
18 for input in op_inputs(op) {
19 out.push_str(&format!(" op_{input} -> op_{idx};\n"));
20 }
21 }
22
23 out.push_str(&format!(
24 " root [shape=oval, label=\"root: {}\"];\n",
25 plan.root_op
26 ));
27 out.push_str(&format!(" op_{} -> root;\n", plan.root_op));
28 out.push_str("}\n");
29 out
30}
31
32pub fn render_dot_to_svg(dot: &[u8]) -> Result<Vec<u8>, String> {
33 let mut child = Command::new("dot")
34 .arg("-Tsvg")
35 .stdin(Stdio::piped())
36 .stdout(Stdio::piped())
37 .stderr(Stdio::piped())
38 .spawn()
39 .map_err(|e| format!("failed to run 'dot -Tsvg': {e}"))?;
40
41 use std::io::Write;
42 child
43 .stdin
44 .as_mut()
45 .ok_or_else(|| "failed to open dot stdin".to_string())?
46 .write_all(dot)
47 .map_err(|e| format!("failed writing DOT to graphviz stdin: {e}"))?;
48
49 let output = child
50 .wait_with_output()
51 .map_err(|e| format!("failed waiting for graphviz: {e}"))?;
52 if !output.status.success() {
53 return Err(format!(
54 "graphviz failed (status {}): {}",
55 output
56 .status
57 .code()
58 .map_or_else(|| "signal".to_string(), |c| c.to_string()),
59 String::from_utf8_lossy(&output.stderr)
60 ));
61 }
62 Ok(output.stdout)
63}
64
65fn op_label(op: &Op) -> String {
66 op_kind(op).as_str().to_string()
67}
68
69fn escape_dot(input: &str) -> String {
70 input.replace('\\', "\\\\").replace('"', "\\\"")
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use plexus_serde::{current_plan_version, ColDef, ColKind, Op, Plan};
77
78 #[test]
79 fn emits_dot_edges() {
80 let plan = Plan {
81 version: current_plan_version("test"),
82 ops: vec![
83 Op::ScanNodes {
84 labels: Vec::new(),
85 schema: vec![ColDef {
86 name: "n".to_string(),
87 kind: ColKind::Node,
88 logical_type: plexus_serde::LogicalType::Unknown,
89 }],
90 must_labels: Vec::new(),
91 forbidden_labels: Vec::new(),
92 est_rows: 1,
93 selectivity: 1.0,
94 graph_ref: None,
95 },
96 Op::Return { input: 0 },
97 ],
98 root_op: 1,
99 };
100
101 let dot = plan_to_dot(&plan);
102 assert!(dot.contains("op_0 -> op_1"));
103 assert!(dot.contains("op_1 -> root"));
104 }
105}