sql_splitter/graph/format/
mermaid.rs

1//! Mermaid erDiagram format output.
2
3use crate::graph::view::GraphView;
4
5/// Generate Mermaid erDiagram from a graph view
6pub fn to_mermaid(view: &GraphView) -> String {
7    let mut output = String::new();
8
9    // Use erDiagram for proper ERD visualization
10    output.push_str("erDiagram\n");
11
12    // Generate entity definitions with attributes
13    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    // Generate relationships
47    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
62/// Escape a string for use as a Mermaid entity ID
63fn escape_mermaid_id(s: &str) -> String {
64    // Mermaid IDs should be alphanumeric with underscores
65    s.chars()
66        .map(|c| {
67            if c.is_alphanumeric() || c == '_' {
68                c
69            } else {
70                '_'
71            }
72        })
73        .collect()
74}
75
76/// Escape a type string for Mermaid (no spaces, special chars)
77fn escape_mermaid_type(s: &str) -> String {
78    // Remove parentheses content for cleaner display
79    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}