use std::collections::BTreeSet;
use std::fmt::Write as _;
use std::path::PathBuf;
use anyhow::Result;
use pgevolve_core::parse::parse_directory;
use pgevolve_core::plan::edges::{DepEdge, DepSource, NodeId};
use crate::cli::GraphFormat;
use crate::config::PgevolveConfig;
pub fn run(
config: &PgevolveConfig,
format: GraphFormat,
out: Option<PathBuf>,
plan: Option<&PathBuf>,
) -> Result<i32> {
if plan.is_some() {
anyhow::bail!("--plan rendering not yet implemented (v0.2 sub-spec landing)");
}
let catalog = parse_directory(&config.project.schema_dir, &[])?;
let graph = pgevolve_core::plan::edges::build_create_graph(&catalog);
let edges: Vec<DepEdge> = graph.dep_edges().collect();
let rendered = match format {
GraphFormat::Dot => render_dot(&edges),
GraphFormat::Mermaid => render_mermaid(&edges),
};
if let Some(path) = out {
std::fs::write(&path, &rendered)?;
eprintln!("wrote {} bytes to {}", rendered.len(), path.display());
} else {
print!("{rendered}");
}
Ok(0)
}
pub fn render_dot(edges: &[DepEdge]) -> String {
let mut out = String::from(
"digraph pgevolve_deps {\n rankdir=LR;\n node [shape=box, fontname=Helvetica];\n",
);
let mut nodes: BTreeSet<String> = BTreeSet::default();
for e in edges {
nodes.insert(node_label(&e.from));
nodes.insert(node_label(&e.to));
}
for n in &nodes {
let _ = writeln!(out, " \"{n}\";");
}
let mut sorted = edges.to_vec();
sorted.sort();
for e in sorted {
let style = match e.source {
DepSource::Structural => "solid",
DepSource::AstExtracted => "dashed",
DepSource::AstDeclared => "dotted",
};
let _ = writeln!(
out,
" \"{}\" -> \"{}\" [style={style}];",
node_label(&e.from),
node_label(&e.to)
);
}
out.push_str("}\n");
out
}
fn render_mermaid(edges: &[DepEdge]) -> String {
let mut out = String::from("graph LR\n");
let mut sorted = edges.to_vec();
sorted.sort();
for e in sorted {
let arrow = match e.source {
DepSource::Structural => "-->",
DepSource::AstExtracted => "-.->",
DepSource::AstDeclared => "==>",
};
let _ = writeln!(
out,
" {} {arrow} {}",
mermaid_safe(&node_label(&e.from)),
mermaid_safe(&node_label(&e.to))
);
}
out
}
fn node_label(n: &NodeId) -> String {
match n {
NodeId::Schema(s) => format!("schema:{}", s.as_str()),
NodeId::Table(q) => format!("table:{q}"),
NodeId::Index(q) => format!("index:{q}"),
NodeId::Sequence(q) => format!("sequence:{q}"),
NodeId::Constraint { table, name } => format!("constraint:{table}.{}", name.as_str()),
NodeId::View(q) => format!("view:{q}"),
NodeId::Mv(q) => format!("mv:{q}"),
NodeId::Type(q) => format!("type:{q}"),
NodeId::Function(q, args) => format!(
"function:{}({})",
q,
args.types
.iter()
.map(pgevolve_core::ir::column_type::ColumnType::render_sql)
.collect::<Vec<_>>()
.join(",")
),
NodeId::Procedure(q) => format!("procedure:{q}"),
NodeId::Extension(n) => format!("extension:{}", n.as_str()),
NodeId::Trigger(q) => format!("trigger:{q}"),
NodeId::Publication(n) => format!("publication:{}", n.as_str()),
NodeId::Subscription(n) => format!("subscription:{}", n.as_str()),
NodeId::Statistic(q) => format!("statistic:{q}"),
NodeId::Collation(q) => format!("collation:{q}"),
NodeId::EventTrigger(n) => format!("event_trigger:{}", n.as_str()),
NodeId::Aggregate(q, args) => format!(
"aggregate:{}({})",
q,
args.types
.iter()
.map(pgevolve_core::ir::column_type::ColumnType::render_sql)
.collect::<Vec<_>>()
.join(",")
),
NodeId::Cast(src, tgt) => format!("cast:{src}_as_{tgt}"),
NodeId::TsDictionary(q) => format!("ts_dictionary:{}", q.render_sql()),
NodeId::TsConfiguration(q) => format!("ts_configuration:{}", q.render_sql()),
}
}
fn mermaid_safe(label: &str) -> String {
let id = label.replace(['.', ':'], "_");
format!("{id}[\"{label}\"]")
}