Spaces:
Sleeping
Sleeping
Added Visualization
Browse files- src/streamlit_app.py +167 -59
src/streamlit_app.py
CHANGED
|
@@ -2,6 +2,7 @@ import streamlit as st
|
|
| 2 |
import re
|
| 3 |
from io import BytesIO
|
| 4 |
import base64
|
|
|
|
| 5 |
|
| 6 |
# Page configuration
|
| 7 |
st.set_page_config(
|
|
@@ -113,49 +114,122 @@ class DBMLParser:
|
|
| 113 |
})
|
| 114 |
|
| 115 |
|
| 116 |
-
class
|
| 117 |
-
"""Generate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
def __init__(self, tables, relationships):
|
| 120 |
self.tables = tables
|
| 121 |
self.relationships = relationships
|
| 122 |
|
| 123 |
def generate(self):
|
| 124 |
-
"""Generate
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
# Add tables
|
| 128 |
for table_name, columns in self.tables.items():
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
# Add constraints
|
| 133 |
-
constraints = []
|
| 134 |
-
if col['is_pk']:
|
| 135 |
-
constraints.append('PK')
|
| 136 |
-
if col['is_unique']:
|
| 137 |
-
constraints.append('UK')
|
| 138 |
-
if col['is_not_null']:
|
| 139 |
-
constraints.append('NOT NULL')
|
| 140 |
-
|
| 141 |
-
constraint_str = f" \"{','.join(constraints)}\"" if constraints else ""
|
| 142 |
-
|
| 143 |
-
lines.append(f" {table_name} {{")
|
| 144 |
-
lines.append(f" {col_def} {col['name']}{constraint_str}")
|
| 145 |
-
lines.append(f" }}")
|
| 146 |
|
| 147 |
-
# Add relationships
|
| 148 |
for rel in self.relationships:
|
| 149 |
if rel['type'] == 'one-to-many':
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
elif rel['type'] == 'many-to-one':
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
else:
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
return
|
| 159 |
|
| 160 |
|
| 161 |
class PostgreSQLGenerator:
|
|
@@ -252,7 +326,7 @@ def main():
|
|
| 252 |
st.write("""
|
| 253 |
This tool allows you to:
|
| 254 |
- π Visualize DBML schemas
|
| 255 |
-
- πΎ Download diagram as
|
| 256 |
- π§ Generate PostgreSQL DDL
|
| 257 |
- π Copy SQL to clipboard
|
| 258 |
""")
|
|
@@ -260,6 +334,10 @@ def main():
|
|
| 260 |
st.header("π‘ Example DBML")
|
| 261 |
if st.button("Load Example"):
|
| 262 |
st.session_state.example_loaded = True
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
# Example DBML
|
| 265 |
example_dbml = """Table users {
|
|
@@ -306,44 +384,74 @@ Ref: comments.user_id > users.id"""
|
|
| 306 |
if st.button("π¨ Generate Visualization & SQL", type="primary", use_container_width=True):
|
| 307 |
if dbml_input.strip():
|
| 308 |
try:
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
if not tables:
|
| 314 |
-
st.error("β No tables found in DBML code. Please check your syntax.")
|
| 315 |
-
else:
|
| 316 |
-
# Generate Mermaid diagram
|
| 317 |
-
mermaid_gen = MermaidGenerator(tables, relationships)
|
| 318 |
-
mermaid_code = mermaid_gen.generate()
|
| 319 |
-
|
| 320 |
-
# Generate PostgreSQL
|
| 321 |
-
sql_gen = PostgreSQLGenerator(tables, relationships)
|
| 322 |
-
sql_code = sql_gen.generate()
|
| 323 |
-
|
| 324 |
-
# Store in session state
|
| 325 |
-
st.session_state.mermaid_code = mermaid_code
|
| 326 |
-
st.session_state.sql_code = sql_code
|
| 327 |
-
st.session_state.tables = tables
|
| 328 |
-
st.session_state.relationships = relationships
|
| 329 |
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
except Exception as e:
|
| 333 |
st.error(f"β Error parsing DBML: {str(e)}")
|
|
|
|
|
|
|
| 334 |
else:
|
| 335 |
st.warning("β οΈ Please enter some DBML code first.")
|
| 336 |
|
| 337 |
with col2:
|
| 338 |
-
if '
|
| 339 |
st.subheader("π Database Diagram")
|
| 340 |
|
| 341 |
-
# Display
|
| 342 |
-
st.
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
# Info about tables
|
| 349 |
st.info(f"π **{len(st.session_state.tables)}** tables, **{len(st.session_state.relationships)}** relationships")
|
|
@@ -370,8 +478,8 @@ Ref: comments.user_id > users.id"""
|
|
| 370 |
use_container_width=True
|
| 371 |
)
|
| 372 |
|
| 373 |
-
if st.button("π Copy
|
| 374 |
-
st.toast("
|
| 375 |
|
| 376 |
|
| 377 |
if __name__ == "__main__":
|
|
|
|
| 2 |
import re
|
| 3 |
from io import BytesIO
|
| 4 |
import base64
|
| 5 |
+
import graphviz
|
| 6 |
|
| 7 |
# Page configuration
|
| 8 |
st.set_page_config(
|
|
|
|
| 114 |
})
|
| 115 |
|
| 116 |
|
| 117 |
+
class GraphvizGenerator:
|
| 118 |
+
"""Generate Graphviz diagram from parsed DBML"""
|
| 119 |
+
|
| 120 |
+
# Color scheme
|
| 121 |
+
COLORS = {
|
| 122 |
+
'table_bg': '#E8F4F8',
|
| 123 |
+
'table_border': '#2E86AB',
|
| 124 |
+
'pk_bg': '#FFE5B4',
|
| 125 |
+
'pk_text': '#8B4513',
|
| 126 |
+
'header_bg': '#2E86AB',
|
| 127 |
+
'header_text': '#FFFFFF',
|
| 128 |
+
'text': '#333333',
|
| 129 |
+
'arrow': '#555555'
|
| 130 |
+
}
|
| 131 |
|
| 132 |
def __init__(self, tables, relationships):
|
| 133 |
self.tables = tables
|
| 134 |
self.relationships = relationships
|
| 135 |
|
| 136 |
def generate(self):
|
| 137 |
+
"""Generate Graphviz diagram"""
|
| 138 |
+
dot = graphviz.Digraph(comment='Database Schema')
|
| 139 |
+
dot.attr(rankdir='LR', bgcolor='white', splines='ortho', nodesep='1', ranksep='1.5')
|
| 140 |
+
dot.attr('node', shape='plaintext')
|
| 141 |
+
dot.attr('edge', color=self.COLORS['arrow'], penwidth='2')
|
| 142 |
|
| 143 |
+
# Add tables
|
| 144 |
for table_name, columns in self.tables.items():
|
| 145 |
+
html_label = self._create_table_html(table_name, columns)
|
| 146 |
+
dot.node(table_name, label=f'<{html_label}>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
# Add relationships with proper arrows
|
| 149 |
for rel in self.relationships:
|
| 150 |
if rel['type'] == 'one-to-many':
|
| 151 |
+
# One to many: arrow from parent to child
|
| 152 |
+
dot.edge(
|
| 153 |
+
rel['to_table'],
|
| 154 |
+
rel['from_table'],
|
| 155 |
+
label=f" {rel['to_column']} β {rel['from_column']} ",
|
| 156 |
+
arrowhead='crow',
|
| 157 |
+
fontsize='10',
|
| 158 |
+
fontcolor='#666666'
|
| 159 |
+
)
|
| 160 |
elif rel['type'] == 'many-to-one':
|
| 161 |
+
# Many to one: arrow from child to parent
|
| 162 |
+
dot.edge(
|
| 163 |
+
rel['from_table'],
|
| 164 |
+
rel['to_table'],
|
| 165 |
+
label=f" {rel['from_column']} β {rel['to_column']} ",
|
| 166 |
+
arrowhead='normal',
|
| 167 |
+
fontsize='10',
|
| 168 |
+
fontcolor='#666666'
|
| 169 |
+
)
|
| 170 |
else:
|
| 171 |
+
# One to one
|
| 172 |
+
dot.edge(
|
| 173 |
+
rel['from_table'],
|
| 174 |
+
rel['to_table'],
|
| 175 |
+
label=f" {rel['from_column']} β {rel['to_column']} ",
|
| 176 |
+
arrowhead='none',
|
| 177 |
+
dir='both',
|
| 178 |
+
fontsize='10',
|
| 179 |
+
fontcolor='#666666'
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
return dot
|
| 183 |
+
|
| 184 |
+
def _create_table_html(self, table_name, columns):
|
| 185 |
+
"""Create HTML table for Graphviz node"""
|
| 186 |
+
html = f'''<
|
| 187 |
+
<TABLE BORDER="2" CELLBORDER="1" CELLSPACING="0" CELLPADDING="8" BGCOLOR="{self.COLORS['table_bg']}" COLOR="{self.COLORS['table_border']}">
|
| 188 |
+
<TR>
|
| 189 |
+
<TD COLSPAN="2" BGCOLOR="{self.COLORS['header_bg']}" ALIGN="CENTER">
|
| 190 |
+
<FONT COLOR="{self.COLORS['header_text']}" POINT-SIZE="14"><B>{table_name}</B></FONT>
|
| 191 |
+
</TD>
|
| 192 |
+
</TR>
|
| 193 |
+
'''
|
| 194 |
+
|
| 195 |
+
for col in columns:
|
| 196 |
+
# Determine background color
|
| 197 |
+
bg_color = self.COLORS['pk_bg'] if col['is_pk'] else self.COLORS['table_bg']
|
| 198 |
+
|
| 199 |
+
# Build column name with constraints
|
| 200 |
+
col_name = col['name']
|
| 201 |
+
if col['is_pk']:
|
| 202 |
+
col_name = f"π {col_name}"
|
| 203 |
+
|
| 204 |
+
# Build type with constraints
|
| 205 |
+
type_str = col['type']
|
| 206 |
+
constraints = []
|
| 207 |
+
if col['is_pk']:
|
| 208 |
+
constraints.append('PK')
|
| 209 |
+
if col['is_unique']:
|
| 210 |
+
constraints.append('UQ')
|
| 211 |
+
if col['is_not_null']:
|
| 212 |
+
constraints.append('NOT NULL')
|
| 213 |
|
| 214 |
+
if constraints:
|
| 215 |
+
type_str += f" [{', '.join(constraints)}]"
|
| 216 |
+
|
| 217 |
+
html += f'''
|
| 218 |
+
<TR>
|
| 219 |
+
<TD ALIGN="LEFT" BGCOLOR="{bg_color}">
|
| 220 |
+
<FONT COLOR="{self.COLORS['text']}" POINT-SIZE="11"><B>{col_name}</B></FONT>
|
| 221 |
+
</TD>
|
| 222 |
+
<TD ALIGN="LEFT" BGCOLOR="{bg_color}">
|
| 223 |
+
<FONT COLOR="{self.COLORS['text']}" POINT-SIZE="10">{type_str}</FONT>
|
| 224 |
+
</TD>
|
| 225 |
+
</TR>
|
| 226 |
+
'''
|
| 227 |
+
|
| 228 |
+
html += '''
|
| 229 |
+
</TABLE>
|
| 230 |
+
>'''
|
| 231 |
|
| 232 |
+
return html
|
| 233 |
|
| 234 |
|
| 235 |
class PostgreSQLGenerator:
|
|
|
|
| 326 |
st.write("""
|
| 327 |
This tool allows you to:
|
| 328 |
- π Visualize DBML schemas
|
| 329 |
+
- πΎ Download diagram as PNG/SVG
|
| 330 |
- π§ Generate PostgreSQL DDL
|
| 331 |
- π Copy SQL to clipboard
|
| 332 |
""")
|
|
|
|
| 334 |
st.header("π‘ Example DBML")
|
| 335 |
if st.button("Load Example"):
|
| 336 |
st.session_state.example_loaded = True
|
| 337 |
+
|
| 338 |
+
st.header("βοΈ Diagram Settings")
|
| 339 |
+
image_format = st.selectbox("Download Format", ["PNG", "SVG"], index=0)
|
| 340 |
+
st.session_state.image_format = image_format.lower()
|
| 341 |
|
| 342 |
# Example DBML
|
| 343 |
example_dbml = """Table users {
|
|
|
|
| 384 |
if st.button("π¨ Generate Visualization & SQL", type="primary", use_container_width=True):
|
| 385 |
if dbml_input.strip():
|
| 386 |
try:
|
| 387 |
+
with st.spinner("Parsing DBML and generating visualization..."):
|
| 388 |
+
# Parse DBML
|
| 389 |
+
parser = DBMLParser(dbml_input)
|
| 390 |
+
tables, relationships = parser.parse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
+
if not tables:
|
| 393 |
+
st.error("β No tables found in DBML code. Please check your syntax.")
|
| 394 |
+
else:
|
| 395 |
+
# Generate Graphviz diagram
|
| 396 |
+
graph_gen = GraphvizGenerator(tables, relationships)
|
| 397 |
+
dot = graph_gen.generate()
|
| 398 |
+
|
| 399 |
+
# Generate PostgreSQL
|
| 400 |
+
sql_gen = PostgreSQLGenerator(tables, relationships)
|
| 401 |
+
sql_code = sql_gen.generate()
|
| 402 |
+
|
| 403 |
+
# Store in session state
|
| 404 |
+
st.session_state.dot = dot
|
| 405 |
+
st.session_state.sql_code = sql_code
|
| 406 |
+
st.session_state.tables = tables
|
| 407 |
+
st.session_state.relationships = relationships
|
| 408 |
+
|
| 409 |
+
st.success(f"β
Successfully parsed {len(tables)} tables and {len(relationships)} relationships!")
|
| 410 |
|
| 411 |
except Exception as e:
|
| 412 |
st.error(f"β Error parsing DBML: {str(e)}")
|
| 413 |
+
import traceback
|
| 414 |
+
st.code(traceback.format_exc())
|
| 415 |
else:
|
| 416 |
st.warning("β οΈ Please enter some DBML code first.")
|
| 417 |
|
| 418 |
with col2:
|
| 419 |
+
if 'dot' in st.session_state:
|
| 420 |
st.subheader("π Database Diagram")
|
| 421 |
|
| 422 |
+
# Display the diagram
|
| 423 |
+
st.graphviz_chart(st.session_state.dot)
|
| 424 |
+
|
| 425 |
+
# Download buttons
|
| 426 |
+
col_btn1, col_btn2 = st.columns(2)
|
| 427 |
+
|
| 428 |
+
with col_btn1:
|
| 429 |
+
# PNG download
|
| 430 |
+
try:
|
| 431 |
+
png_data = st.session_state.dot.pipe(format='png')
|
| 432 |
+
st.download_button(
|
| 433 |
+
label="π₯ Download PNG",
|
| 434 |
+
data=png_data,
|
| 435 |
+
file_name="database_schema.png",
|
| 436 |
+
mime="image/png",
|
| 437 |
+
use_container_width=True
|
| 438 |
+
)
|
| 439 |
+
except:
|
| 440 |
+
st.warning("PNG export requires Graphviz installation")
|
| 441 |
+
|
| 442 |
+
with col_btn2:
|
| 443 |
+
# SVG download
|
| 444 |
+
try:
|
| 445 |
+
svg_data = st.session_state.dot.pipe(format='svg')
|
| 446 |
+
st.download_button(
|
| 447 |
+
label="π₯ Download SVG",
|
| 448 |
+
data=svg_data,
|
| 449 |
+
file_name="database_schema.svg",
|
| 450 |
+
mime="image/svg+xml",
|
| 451 |
+
use_container_width=True
|
| 452 |
+
)
|
| 453 |
+
except:
|
| 454 |
+
st.warning("SVG export requires Graphviz installation")
|
| 455 |
|
| 456 |
# Info about tables
|
| 457 |
st.info(f"π **{len(st.session_state.tables)}** tables, **{len(st.session_state.relationships)}** relationships")
|
|
|
|
| 478 |
use_container_width=True
|
| 479 |
)
|
| 480 |
|
| 481 |
+
if st.button("π Copy SQL", use_container_width=True):
|
| 482 |
+
st.toast("Copy the SQL code from the code block above!", icon="π")
|
| 483 |
|
| 484 |
|
| 485 |
if __name__ == "__main__":
|