syedkhalid076 commited on
Commit
c1d32fe
Β·
verified Β·
1 Parent(s): 8906345

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +376 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,378 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import re
3
+ from io import BytesIO
4
+ import base64
5
 
6
+ # Page configuration
7
+ st.set_page_config(
8
+ page_title="DBML Visualizer",
9
+ page_icon="πŸ—„οΈ",
10
+ layout="wide"
11
+ )
12
+
13
+ # Custom CSS for better styling
14
+ st.markdown("""
15
+ <style>
16
+ .main-header {
17
+ font-size: 2.5rem;
18
+ font-weight: bold;
19
+ margin-bottom: 0.5rem;
20
+ color: #1f77b4;
21
+ }
22
+ .sub-header {
23
+ font-size: 1.2rem;
24
+ color: #666;
25
+ margin-bottom: 2rem;
26
+ }
27
+ .stTextArea textarea {
28
+ font-family: 'Courier New', monospace;
29
+ }
30
+ </style>
31
+ """, unsafe_allow_html=True)
32
+
33
+
34
+ class DBMLParser:
35
+ """Parse DBML code into structured data"""
36
+
37
+ def __init__(self, dbml_code):
38
+ self.dbml_code = dbml_code
39
+ self.tables = {}
40
+ self.relationships = []
41
+
42
+ def parse(self):
43
+ """Parse DBML code"""
44
+ self._parse_tables()
45
+ self._parse_relationships()
46
+ return self.tables, self.relationships
47
+
48
+ def _parse_tables(self):
49
+ """Extract tables and columns from DBML"""
50
+ table_pattern = r'Table\s+(\w+)\s*{([^}]*)}'
51
+
52
+ for match in re.finditer(table_pattern, self.dbml_code, re.DOTALL):
53
+ table_name = match.group(1)
54
+ table_body = match.group(2)
55
+
56
+ columns = []
57
+ for line in table_body.strip().split('\n'):
58
+ line = line.strip()
59
+ if not line or line.startswith('//'):
60
+ continue
61
+
62
+ # Parse column definition
63
+ col_match = re.match(r'(\w+)\s+(\w+)(\s+\[[^\]]*\])?', line)
64
+ if col_match:
65
+ col_name = col_match.group(1)
66
+ col_type = col_match.group(2)
67
+ constraints = col_match.group(3) or ''
68
+
69
+ is_pk = 'pk' in constraints.lower() or 'primary key' in constraints.lower()
70
+ is_unique = 'unique' in constraints.lower()
71
+ is_null = 'null' in constraints.lower() and 'not null' not in constraints.lower()
72
+ is_not_null = 'not null' in constraints.lower()
73
+
74
+ columns.append({
75
+ 'name': col_name,
76
+ 'type': col_type,
77
+ 'is_pk': is_pk,
78
+ 'is_unique': is_unique,
79
+ 'is_null': is_null,
80
+ 'is_not_null': is_not_null
81
+ })
82
+
83
+ self.tables[table_name] = columns
84
+
85
+ def _parse_relationships(self):
86
+ """Extract relationships from DBML"""
87
+ # Pattern: Ref: table1.column1 > table2.column2
88
+ ref_pattern = r'Ref:\s*(\w+)\.(\w+)\s*([<>-]+)\s*(\w+)\.(\w+)'
89
+
90
+ for match in re.finditer(ref_pattern, self.dbml_code):
91
+ from_table = match.group(1)
92
+ from_column = match.group(2)
93
+ rel_type = match.group(3)
94
+ to_table = match.group(4)
95
+ to_column = match.group(5)
96
+
97
+ # Determine relationship type
98
+ if '>' in rel_type:
99
+ rel_name = 'one-to-many'
100
+ elif '<' in rel_type:
101
+ rel_name = 'many-to-one'
102
+ from_table, to_table = to_table, from_table
103
+ from_column, to_column = to_column, from_column
104
+ else:
105
+ rel_name = 'one-to-one'
106
+
107
+ self.relationships.append({
108
+ 'from_table': from_table,
109
+ 'from_column': from_column,
110
+ 'to_table': to_table,
111
+ 'to_column': to_column,
112
+ 'type': rel_name
113
+ })
114
+
115
+
116
+ class MermaidGenerator:
117
+ """Generate Mermaid diagram from parsed DBML"""
118
+
119
+ def __init__(self, tables, relationships):
120
+ self.tables = tables
121
+ self.relationships = relationships
122
+
123
+ def generate(self):
124
+ """Generate Mermaid ER diagram"""
125
+ lines = ['erDiagram']
126
+
127
+ # Add tables with columns
128
+ for table_name, columns in self.tables.items():
129
+ for col in columns:
130
+ col_def = f"{col['type']}"
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
+ rel_symbol = '||--o{'
151
+ elif rel['type'] == 'many-to-one':
152
+ rel_symbol = '}o--||'
153
+ else:
154
+ rel_symbol = '||--||'
155
+
156
+ lines.append(f" {rel['from_table']} {rel_symbol} {rel['to_table']} : \"{rel['from_column']}-{rel['to_column']}\"")
157
+
158
+ return '\n'.join(lines)
159
+
160
+
161
+ class PostgreSQLGenerator:
162
+ """Generate PostgreSQL DDL from parsed DBML"""
163
+
164
+ def __init__(self, tables, relationships):
165
+ self.tables = tables
166
+ self.relationships = relationships
167
+
168
+ def generate(self):
169
+ """Generate PostgreSQL CREATE TABLE statements"""
170
+ sql_statements = []
171
+
172
+ # Create tables
173
+ for table_name, columns in self.tables.items():
174
+ lines = [f"CREATE TABLE {table_name} ("]
175
+
176
+ col_defs = []
177
+ pk_columns = []
178
+
179
+ for col in columns:
180
+ col_def = f" {col['name']} {self._map_type(col['type'])}"
181
+
182
+ if col['is_pk']:
183
+ pk_columns.append(col['name'])
184
+
185
+ if col['is_not_null'] and not col['is_pk']:
186
+ col_def += " NOT NULL"
187
+ elif col['is_null']:
188
+ col_def += " NULL"
189
+
190
+ if col['is_unique'] and not col['is_pk']:
191
+ col_def += " UNIQUE"
192
+
193
+ col_defs.append(col_def)
194
+
195
+ # Add primary key constraint
196
+ if pk_columns:
197
+ col_defs.append(f" PRIMARY KEY ({', '.join(pk_columns)})")
198
+
199
+ lines.append(',\n'.join(col_defs))
200
+ lines.append(");")
201
+
202
+ sql_statements.append('\n'.join(lines))
203
+
204
+ # Add foreign key constraints
205
+ fk_statements = []
206
+ for rel in self.relationships:
207
+ fk_name = f"fk_{rel['from_table']}_{rel['to_table']}"
208
+ fk_stmt = f"""
209
+ ALTER TABLE {rel['from_table']}
210
+ ADD CONSTRAINT {fk_name}
211
+ FOREIGN KEY ({rel['from_column']})
212
+ REFERENCES {rel['to_table']}({rel['to_column']});"""
213
+ fk_statements.append(fk_stmt)
214
+
215
+ all_sql = '\n\n'.join(sql_statements)
216
+ if fk_statements:
217
+ all_sql += '\n\n-- Foreign Key Constraints\n' + '\n'.join(fk_statements)
218
+
219
+ return all_sql
220
+
221
+ def _map_type(self, dbml_type):
222
+ """Map DBML types to PostgreSQL types"""
223
+ type_mapping = {
224
+ 'int': 'INTEGER',
225
+ 'integer': 'INTEGER',
226
+ 'bigint': 'BIGINT',
227
+ 'smallint': 'SMALLINT',
228
+ 'varchar': 'VARCHAR(255)',
229
+ 'text': 'TEXT',
230
+ 'bool': 'BOOLEAN',
231
+ 'boolean': 'BOOLEAN',
232
+ 'date': 'DATE',
233
+ 'datetime': 'TIMESTAMP',
234
+ 'timestamp': 'TIMESTAMP',
235
+ 'decimal': 'DECIMAL',
236
+ 'float': 'REAL',
237
+ 'double': 'DOUBLE PRECISION',
238
+ 'uuid': 'UUID',
239
+ 'json': 'JSONB'
240
+ }
241
+
242
+ return type_mapping.get(dbml_type.lower(), dbml_type.upper())
243
+
244
+
245
+ def main():
246
+ st.markdown('<div class="main-header">πŸ—„οΈ DBML Visualizer & SQL Generator</div>', unsafe_allow_html=True)
247
+ st.markdown('<div class="sub-header">Visualize database schemas and generate PostgreSQL code</div>', unsafe_allow_html=True)
248
+
249
+ # Sidebar
250
+ with st.sidebar:
251
+ st.header("πŸ“– About")
252
+ st.write("""
253
+ This tool allows you to:
254
+ - πŸ“Š Visualize DBML schemas
255
+ - πŸ’Ύ Download diagram as image
256
+ - πŸ”§ Generate PostgreSQL DDL
257
+ - πŸ“‹ Copy SQL to clipboard
258
+ """)
259
+
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 {
266
+ id int [pk, increment]
267
+ username varchar [unique, not null]
268
+ email varchar [unique, not null]
269
+ created_at timestamp
270
+ }
271
+
272
+ Table posts {
273
+ id int [pk, increment]
274
+ title varchar [not null]
275
+ content text
276
+ user_id int [not null]
277
+ created_at timestamp
278
+ }
279
+
280
+ Table comments {
281
+ id int [pk, increment]
282
+ post_id int [not null]
283
+ user_id int [not null]
284
+ content text [not null]
285
+ created_at timestamp
286
+ }
287
+
288
+ Ref: posts.user_id > users.id
289
+ Ref: comments.post_id > posts.id
290
+ Ref: comments.user_id > users.id"""
291
+
292
+ # Main content
293
+ col1, col2 = st.columns([1, 1])
294
+
295
+ with col1:
296
+ st.subheader("πŸ“ DBML Input")
297
+
298
+ default_value = example_dbml if st.session_state.get('example_loaded', False) else ""
299
+ dbml_input = st.text_area(
300
+ "Enter your DBML code:",
301
+ value=default_value,
302
+ height=400,
303
+ placeholder="Table users {\n id int [pk]\n name varchar\n}\n\nTable posts {\n id int [pk]\n user_id int\n}\n\nRef: posts.user_id > users.id"
304
+ )
305
+
306
+ if st.button("🎨 Generate Visualization & SQL", type="primary", use_container_width=True):
307
+ if dbml_input.strip():
308
+ try:
309
+ # Parse DBML
310
+ parser = DBMLParser(dbml_input)
311
+ tables, relationships = parser.parse()
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
+ st.success(f"βœ… Successfully parsed {len(tables)} tables and {len(relationships)} relationships!")
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 'mermaid_code' in st.session_state:
339
+ st.subheader("πŸ“Š Database Diagram")
340
+
341
+ # Display Mermaid diagram
342
+ st.markdown(f"""
343
+ ```mermaid
344
+ {st.session_state.mermaid_code}
345
+ ```
346
+ """)
347
+
348
+ # Info about tables
349
+ st.info(f"πŸ“‹ **{len(st.session_state.tables)}** tables, **{len(st.session_state.relationships)}** relationships")
350
+
351
+ else:
352
+ st.info("πŸ‘ˆ Enter DBML code and click 'Generate' to see the visualization")
353
+
354
+ # SQL Output Section
355
+ if 'sql_code' in st.session_state:
356
+ st.markdown("---")
357
+ st.subheader("πŸ”§ Generated PostgreSQL Code")
358
+
359
+ col_sql1, col_sql2 = st.columns([4, 1])
360
+
361
+ with col_sql1:
362
+ st.code(st.session_state.sql_code, language="sql", line_numbers=True)
363
+
364
+ with col_sql2:
365
+ st.download_button(
366
+ label="πŸ“₯ Download SQL",
367
+ data=st.session_state.sql_code,
368
+ file_name="schema.sql",
369
+ mime="text/sql",
370
+ use_container_width=True
371
+ )
372
+
373
+ if st.button("πŸ“‹ Copy to Clipboard", use_container_width=True):
374
+ st.toast("SQL copied to clipboard! (Use Ctrl+C to copy the code block)", icon="βœ…")
375
+
376
+
377
+ if __name__ == "__main__":
378
+ main()