Skip to main content

plexus_devtools/
viz.rs

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}