sql_splitter/graph/format/
mermaid.rs1use crate::graph::view::GraphView;
4
5pub fn to_mermaid(view: &GraphView) -> String {
7 let mut output = String::new();
8
9 output.push_str("erDiagram\n");
11
12 for table in view.sorted_tables() {
14 let safe_name = escape_mermaid_id(&table.name);
15 output.push_str(&format!(" {} {{\n", safe_name));
16
17 for col in &table.columns {
18 let key_marker = if col.is_primary_key {
19 "PK"
20 } else if col.is_foreign_key {
21 "FK"
22 } else {
23 ""
24 };
25
26 let col_type = escape_mermaid_type(&col.col_type);
27 let col_name = escape_mermaid_id(&col.name);
28
29 if key_marker.is_empty() {
30 output.push_str(&format!(" {} {}\n", col_type, col_name));
31 } else {
32 output.push_str(&format!(
33 " {} {} {}\n",
34 col_type, col_name, key_marker
35 ));
36 }
37 }
38
39 output.push_str(" }\n");
40 }
41
42 if !view.edges.is_empty() {
43 output.push('\n');
44 }
45
46 for edge in &view.edges {
48 let from = escape_mermaid_id(&edge.from_table);
49 let to = escape_mermaid_id(&edge.to_table);
50 let cardinality = edge.cardinality.as_mermaid();
51 let label = edge.from_column.clone();
52
53 output.push_str(&format!(
54 " {} {} {} : \"{}\"\n",
55 from, cardinality, to, label
56 ));
57 }
58
59 output
60}
61
62fn escape_mermaid_id(s: &str) -> String {
64 s.chars()
66 .map(|c| {
67 if c.is_alphanumeric() || c == '_' {
68 c
69 } else {
70 '_'
71 }
72 })
73 .collect()
74}
75
76fn escape_mermaid_type(s: &str) -> String {
78 let base = if let Some(paren_pos) = s.find('(') {
80 &s[..paren_pos]
81 } else {
82 s
83 };
84 base.chars()
85 .map(|c| {
86 if c.is_alphanumeric() || c == '_' {
87 c
88 } else {
89 '_'
90 }
91 })
92 .collect()
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::graph::view::{Cardinality, ColumnInfo, EdgeInfo, TableInfo};
99 use ahash::AHashMap;
100
101 fn create_test_view() -> GraphView {
102 let mut tables = AHashMap::new();
103
104 tables.insert(
105 "users".to_string(),
106 TableInfo {
107 name: "users".to_string(),
108 columns: vec![
109 ColumnInfo {
110 name: "id".to_string(),
111 col_type: "INT".to_string(),
112 is_primary_key: true,
113 is_foreign_key: false,
114 is_nullable: false,
115 references_table: None,
116 references_column: None,
117 },
118 ColumnInfo {
119 name: "email".to_string(),
120 col_type: "VARCHAR(255)".to_string(),
121 is_primary_key: false,
122 is_foreign_key: false,
123 is_nullable: true,
124 references_table: None,
125 references_column: None,
126 },
127 ],
128 },
129 );
130
131 tables.insert(
132 "orders".to_string(),
133 TableInfo {
134 name: "orders".to_string(),
135 columns: vec![
136 ColumnInfo {
137 name: "id".to_string(),
138 col_type: "INT".to_string(),
139 is_primary_key: true,
140 is_foreign_key: false,
141 is_nullable: false,
142 references_table: None,
143 references_column: None,
144 },
145 ColumnInfo {
146 name: "user_id".to_string(),
147 col_type: "INT".to_string(),
148 is_primary_key: false,
149 is_foreign_key: true,
150 is_nullable: false,
151 references_table: Some("users".to_string()),
152 references_column: Some("id".to_string()),
153 },
154 ],
155 },
156 );
157
158 let edges = vec![EdgeInfo {
159 from_table: "orders".to_string(),
160 from_column: "user_id".to_string(),
161 to_table: "users".to_string(),
162 to_column: "id".to_string(),
163 cardinality: Cardinality::ManyToOne,
164 }];
165
166 GraphView { tables, edges }
167 }
168
169 #[test]
170 fn test_mermaid_er_diagram() {
171 let view = create_test_view();
172 let output = to_mermaid(&view);
173
174 assert!(output.contains("erDiagram"));
175 assert!(output.contains("users {"));
176 assert!(output.contains("orders {"));
177 }
178
179 #[test]
180 fn test_mermaid_columns() {
181 let view = create_test_view();
182 let output = to_mermaid(&view);
183
184 assert!(output.contains("INT id PK"));
185 assert!(output.contains("INT user_id FK"));
186 assert!(output.contains("VARCHAR email"));
187 }
188
189 #[test]
190 fn test_mermaid_relationships() {
191 let view = create_test_view();
192 let output = to_mermaid(&view);
193
194 assert!(output.contains("}o--||"));
195 assert!(output.contains(": \"user_id\""));
196 }
197}