tuankg1028 commited on
Commit
3545eca
·
1 Parent(s): 38f1436

Adds database visualization feature

Browse files

Implements database visualization using Mermaid diagrams and text summaries.

Provides a visual representation of the database schema, including tables, columns, and relationships. This allows users to better understand the structure of their database.

Automatically generates visualizations after successful merge operations or SQL execution. Includes a button to manually generate the visualization at any time.

Updates the Gradio UI to display the visualization.

Files changed (2) hide show
  1. schema_sync/app.py +97 -18
  2. schema_sync/db_visualizer.py +153 -0
schema_sync/app.py CHANGED
@@ -5,6 +5,7 @@ import os
5
  from .db_connector import DBConnector
6
  from .schema_inspector import SchemaInspector
7
  from .merge_operations import MergeOperations
 
8
  from .config import get_config
9
 
10
  # Set up logging
@@ -17,10 +18,11 @@ config = get_config()
17
  db = None
18
  inspector = None
19
  merge_ops = None
 
20
 
21
  def connect_to_database(db_url):
22
  """Connect to the database with provided URL"""
23
- global db, inspector, merge_ops
24
 
25
  try:
26
  # Initialize new connection
@@ -32,6 +34,7 @@ def connect_to_database(db_url):
32
 
33
  inspector = SchemaInspector(db)
34
  merge_ops = MergeOperations(db, inspector)
 
35
 
36
  # Test connection by fetching tables
37
  tables = get_db_tables()
@@ -50,10 +53,10 @@ def connect_to_database(db_url):
50
  def handle_merge(action, table, column, from_values, target_value, preview_only=True):
51
  """Handler for Gradio interface"""
52
  if not db:
53
- return "Error: Not connected to database. Please connect first."
54
 
55
  if not table or not column:
56
- return "Error: Table and column must be specified"
57
 
58
  # Parse from_values as comma-separated list
59
  from_values_list = [v.strip() for v in from_values.split(',')]
@@ -61,12 +64,21 @@ def handle_merge(action, table, column, from_values, target_value, preview_only=
61
  if action == "Merge Values":
62
  if preview_only:
63
  result = merge_ops.preview_merge(table, column, from_values_list, target_value)
64
- return result["preview"]
65
  else:
66
  result = merge_ops.run_merge(table, column, from_values_list, target_value)
67
- return result["log"]
 
 
 
 
 
 
 
 
 
68
  else:
69
- return "Action not implemented yet"
70
 
71
  def get_db_tables():
72
  """Get list of tables from the database for dropdown"""
@@ -116,10 +128,10 @@ def execute_sql_file(sql_file):
116
  global db
117
 
118
  if not db:
119
- return "Error: Not connected to database. Please connect first."
120
 
121
  if sql_file is None:
122
- return "Error: No SQL file provided."
123
 
124
  try:
125
  # Read the uploaded file
@@ -127,7 +139,7 @@ def execute_sql_file(sql_file):
127
  sql_content = f.read()
128
 
129
  if not sql_content.strip():
130
- return "Error: SQL file is empty."
131
 
132
  # Split SQL content by semicolons and execute each statement
133
  sql_statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()]
@@ -152,19 +164,52 @@ def execute_sql_file(sql_file):
152
 
153
  session.commit()
154
  results.insert(0, f"Successfully executed {len(sql_statements)} SQL statements.")
155
- return "\n".join(results)
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  except Exception as e:
158
  session.rollback()
159
  error_msg = f"Error executing SQL: {str(e)}"
160
  logger.error(error_msg)
161
- return error_msg
162
  finally:
163
  session.close()
164
 
165
  except Exception as e:
166
  logger.error(f"Error reading SQL file: {str(e)}")
167
- return f"Error reading SQL file: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  def create_ui():
170
  """Create and configure the Gradio UI"""
@@ -172,7 +217,7 @@ def create_ui():
172
  gr.Markdown("# SchemaSync - Database Schema Manipulation Tool")
173
 
174
  with gr.Row():
175
- with gr.Column():
176
  # Database Connection Section
177
  gr.Markdown("## Database Connection")
178
 
@@ -246,13 +291,40 @@ def create_ui():
246
  with gr.Row():
247
  preview_btn = gr.Button("Preview Changes")
248
  run_btn = gr.Button("Run Operation", variant="primary")
 
 
 
 
 
249
 
250
- with gr.Column():
 
 
 
251
  output = gr.TextArea(
252
  label="Operation Log",
253
  placeholder="Operation results will appear here",
254
- lines=20
255
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  # Connect buttons to handlers
258
  connect_btn.click(
@@ -270,19 +342,26 @@ def create_ui():
270
  execute_sql_btn.click(
271
  fn=execute_sql_file,
272
  inputs=sql_file,
273
- outputs=output
274
  )
275
 
276
  preview_btn.click(
277
  fn=handle_merge,
278
  inputs=[action, table, column, from_values, target_value, preview_checkbox],
279
- outputs=output
280
  )
281
 
282
  run_btn.click(
283
  fn=handle_merge,
284
  inputs=[action, table, column, from_values, target_value, gr.Checkbox(value=False, visible=False)],
285
- outputs=output
 
 
 
 
 
 
 
286
  )
287
 
288
  return app
 
5
  from .db_connector import DBConnector
6
  from .schema_inspector import SchemaInspector
7
  from .merge_operations import MergeOperations
8
+ from .db_visualizer import DatabaseVisualizer
9
  from .config import get_config
10
 
11
  # Set up logging
 
18
  db = None
19
  inspector = None
20
  merge_ops = None
21
+ visualizer = None
22
 
23
  def connect_to_database(db_url):
24
  """Connect to the database with provided URL"""
25
+ global db, inspector, merge_ops, visualizer
26
 
27
  try:
28
  # Initialize new connection
 
34
 
35
  inspector = SchemaInspector(db)
36
  merge_ops = MergeOperations(db, inspector)
37
+ visualizer = DatabaseVisualizer(db, inspector)
38
 
39
  # Test connection by fetching tables
40
  tables = get_db_tables()
 
53
  def handle_merge(action, table, column, from_values, target_value, preview_only=True):
54
  """Handler for Gradio interface"""
55
  if not db:
56
+ return "Error: Not connected to database. Please connect first.", "", ""
57
 
58
  if not table or not column:
59
+ return "Error: Table and column must be specified", "", ""
60
 
61
  # Parse from_values as comma-separated list
62
  from_values_list = [v.strip() for v in from_values.split(',')]
 
64
  if action == "Merge Values":
65
  if preview_only:
66
  result = merge_ops.preview_merge(table, column, from_values_list, target_value)
67
+ return result["preview"], "", ""
68
  else:
69
  result = merge_ops.run_merge(table, column, from_values_list, target_value)
70
+ # Auto-generate visualization after successful operation
71
+ if result.get("success", False) and visualizer:
72
+ try:
73
+ text_summary = visualizer.generate_table_summary()
74
+ mermaid_diagram = visualizer.generate_mermaid_diagram()
75
+ return result["log"], text_summary, mermaid_diagram
76
+ except Exception as e:
77
+ logger.error(f"Error generating visualization after merge: {str(e)}")
78
+ return result["log"], "Error generating visualization", ""
79
+ return result["log"], "", ""
80
  else:
81
+ return "Action not implemented yet", "", ""
82
 
83
  def get_db_tables():
84
  """Get list of tables from the database for dropdown"""
 
128
  global db
129
 
130
  if not db:
131
+ return "Error: Not connected to database. Please connect first.", "", ""
132
 
133
  if sql_file is None:
134
+ return "Error: No SQL file provided.", "", ""
135
 
136
  try:
137
  # Read the uploaded file
 
139
  sql_content = f.read()
140
 
141
  if not sql_content.strip():
142
+ return "Error: SQL file is empty.", "", ""
143
 
144
  # Split SQL content by semicolons and execute each statement
145
  sql_statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()]
 
164
 
165
  session.commit()
166
  results.insert(0, f"Successfully executed {len(sql_statements)} SQL statements.")
167
+ operation_log = "\n".join(results)
168
+
169
+ # Auto-generate visualization after successful SQL execution
170
+ if visualizer:
171
+ try:
172
+ text_summary = visualizer.generate_table_summary()
173
+ mermaid_diagram = visualizer.generate_mermaid_diagram()
174
+ return operation_log, text_summary, mermaid_diagram
175
+ except Exception as e:
176
+ logger.error(f"Error generating visualization after SQL execution: {str(e)}")
177
+ return operation_log, "Error generating visualization", ""
178
+
179
+ return operation_log, "", ""
180
 
181
  except Exception as e:
182
  session.rollback()
183
  error_msg = f"Error executing SQL: {str(e)}"
184
  logger.error(error_msg)
185
+ return error_msg, "", ""
186
  finally:
187
  session.close()
188
 
189
  except Exception as e:
190
  logger.error(f"Error reading SQL file: {str(e)}")
191
+ return f"Error reading SQL file: {str(e)}", "", ""
192
+
193
+ def generate_database_visualization():
194
+ """Generate database visualization"""
195
+ if not visualizer:
196
+ return "Error: Not connected to database. Please connect first.", ""
197
+
198
+ try:
199
+ # Generate both Mermaid diagram and text summary
200
+ mermaid_diagram = visualizer.generate_mermaid_diagram()
201
+ text_summary = visualizer.generate_table_summary()
202
+
203
+ return text_summary, mermaid_diagram
204
+
205
+ except Exception as e:
206
+ error_msg = f"Error generating visualization: {str(e)}"
207
+ logger.error(error_msg)
208
+ return error_msg, ""
209
+
210
+ def refresh_visualization():
211
+ """Refresh the database visualization"""
212
+ return generate_database_visualization()
213
 
214
  def create_ui():
215
  """Create and configure the Gradio UI"""
 
217
  gr.Markdown("# SchemaSync - Database Schema Manipulation Tool")
218
 
219
  with gr.Row():
220
+ with gr.Column(scale=1):
221
  # Database Connection Section
222
  gr.Markdown("## Database Connection")
223
 
 
291
  with gr.Row():
292
  preview_btn = gr.Button("Preview Changes")
293
  run_btn = gr.Button("Run Operation", variant="primary")
294
+
295
+ # Database Visualization Section
296
+ gr.Markdown("## Database Visualization")
297
+
298
+ visualize_btn = gr.Button("Generate Visualization", variant="secondary")
299
 
300
+ with gr.Column(scale=2):
301
+ # Operation Results
302
+ gr.Markdown("## Operation Results")
303
+
304
  output = gr.TextArea(
305
  label="Operation Log",
306
  placeholder="Operation results will appear here",
307
+ lines=15
308
  )
309
+
310
+ # Visualization Results
311
+ gr.Markdown("## Database Schema")
312
+
313
+ with gr.Tabs():
314
+ with gr.TabItem("Schema Summary"):
315
+ schema_summary = gr.Markdown(
316
+ value="Connect to a database and run operations or click 'Generate Visualization' to see the schema structure."
317
+ )
318
+
319
+ with gr.TabItem("ER Diagram"):
320
+ gr.Markdown("Copy the code below and paste it into [Mermaid Live Editor](https://mermaid.live) to view the interactive diagram.")
321
+
322
+ mermaid_code = gr.Code(
323
+ label="Mermaid Diagram Code",
324
+ language="markdown",
325
+ lines=15,
326
+ value="Connect to database and run operations to see diagram code here."
327
+ )
328
 
329
  # Connect buttons to handlers
330
  connect_btn.click(
 
342
  execute_sql_btn.click(
343
  fn=execute_sql_file,
344
  inputs=sql_file,
345
+ outputs=[output, schema_summary, mermaid_code]
346
  )
347
 
348
  preview_btn.click(
349
  fn=handle_merge,
350
  inputs=[action, table, column, from_values, target_value, preview_checkbox],
351
+ outputs=[output, schema_summary, mermaid_code]
352
  )
353
 
354
  run_btn.click(
355
  fn=handle_merge,
356
  inputs=[action, table, column, from_values, target_value, gr.Checkbox(value=False, visible=False)],
357
+ outputs=[output, schema_summary, mermaid_code]
358
+ )
359
+
360
+ # Manual visualization generation
361
+ visualize_btn.click(
362
+ fn=generate_database_visualization,
363
+ inputs=None,
364
+ outputs=[schema_summary, mermaid_code]
365
  )
366
 
367
  return app
schema_sync/db_visualizer.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import json
3
+ from sqlalchemy import text
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class DatabaseVisualizer:
8
+ def __init__(self, db_connector, schema_inspector):
9
+ self.db = db_connector
10
+ self.inspector = schema_inspector
11
+
12
+ def get_database_schema(self):
13
+ """Get complete database schema with tables, columns, and relationships"""
14
+ try:
15
+ # Get all tables
16
+ tables_query = """
17
+ SELECT table_name
18
+ FROM information_schema.tables
19
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
20
+ ORDER BY table_name
21
+ """
22
+ tables_result = self.db.execute_query(tables_query)
23
+ tables = [row[0] for row in tables_result]
24
+
25
+ schema_data = {
26
+ "tables": {},
27
+ "relationships": []
28
+ }
29
+
30
+ # Get detailed info for each table
31
+ for table in tables:
32
+ # Get columns
33
+ columns = self.inspector.get_column_info(table)
34
+
35
+ # Get foreign keys
36
+ foreign_keys = self.inspector.get_foreign_keys(table)
37
+
38
+ # Get primary key
39
+ pk_query = f"""
40
+ SELECT column_name
41
+ FROM information_schema.key_column_usage
42
+ WHERE table_name = '{table}'
43
+ AND constraint_name IN (
44
+ SELECT constraint_name
45
+ FROM information_schema.table_constraints
46
+ WHERE table_name = '{table}'
47
+ AND constraint_type = 'PRIMARY KEY'
48
+ )
49
+ """
50
+ pk_result = self.db.execute_query(pk_query)
51
+ primary_keys = [row[0] for row in pk_result]
52
+
53
+ # Store table info
54
+ schema_data["tables"][table] = {
55
+ "columns": columns,
56
+ "primary_keys": primary_keys,
57
+ "foreign_keys": foreign_keys
58
+ }
59
+
60
+ # Add relationships
61
+ for fk in foreign_keys:
62
+ relationship = {
63
+ "from_table": table,
64
+ "from_column": fk["column_name"],
65
+ "to_table": fk["foreign_table_name"],
66
+ "to_column": fk["foreign_column_name"]
67
+ }
68
+ schema_data["relationships"].append(relationship)
69
+
70
+ return schema_data
71
+
72
+ except Exception as e:
73
+ logger.error(f"Error getting database schema: {str(e)}")
74
+ return {"tables": {}, "relationships": []}
75
+
76
+ def generate_mermaid_diagram(self):
77
+ """Generate a Mermaid ER diagram of the database"""
78
+ schema = self.get_database_schema()
79
+
80
+ if not schema["tables"]:
81
+ return "No tables found in database"
82
+
83
+ mermaid_lines = ["erDiagram"]
84
+
85
+ # Add tables and columns
86
+ for table_name, table_info in schema["tables"].items():
87
+ mermaid_lines.append(f" {table_name} {{")
88
+
89
+ for col in table_info["columns"]:
90
+ col_name = col["column_name"]
91
+ data_type = col["data_type"]
92
+ is_pk = col_name in table_info["primary_keys"]
93
+ is_fk = any(fk["column_name"] == col_name for fk in table_info["foreign_keys"])
94
+
95
+ # Add type indicators
96
+ type_indicator = ""
97
+ if is_pk:
98
+ type_indicator = " PK"
99
+ elif is_fk:
100
+ type_indicator = " FK"
101
+
102
+ mermaid_lines.append(f" {data_type} {col_name}{type_indicator}")
103
+
104
+ mermaid_lines.append(" }")
105
+
106
+ # Add relationships
107
+ for rel in schema["relationships"]:
108
+ # Mermaid relationship syntax: TableA ||--o{ TableB : relationship
109
+ mermaid_lines.append(
110
+ f" {rel['to_table']} ||--o{{ {rel['from_table']} : \"{rel['from_column']} -> {rel['to_column']}\""
111
+ )
112
+
113
+ return "\n".join(mermaid_lines)
114
+
115
+ def generate_table_summary(self):
116
+ """Generate a text summary of database structure"""
117
+ schema = self.get_database_schema()
118
+
119
+ if not schema["tables"]:
120
+ return "No tables found in database"
121
+
122
+ summary_lines = ["# Database Schema Summary\n"]
123
+
124
+ # Table overview
125
+ summary_lines.append(f"**Total Tables:** {len(schema['tables'])}")
126
+ summary_lines.append(f"**Total Relationships:** {len(schema['relationships'])}\n")
127
+
128
+ # Detailed table info
129
+ summary_lines.append("## Tables\n")
130
+
131
+ for table_name, table_info in schema["tables"].items():
132
+ summary_lines.append(f"### {table_name}")
133
+ summary_lines.append(f"- **Columns:** {len(table_info['columns'])}")
134
+ summary_lines.append(f"- **Primary Keys:** {', '.join(table_info['primary_keys']) if table_info['primary_keys'] else 'None'}")
135
+ summary_lines.append(f"- **Foreign Keys:** {len(table_info['foreign_keys'])}")
136
+
137
+ # Column details
138
+ summary_lines.append("\n**Columns:**")
139
+ for col in table_info["columns"]:
140
+ nullable = "NULL" if col["is_nullable"] == "YES" else "NOT NULL"
141
+ summary_lines.append(f"- `{col['column_name']}` ({col['data_type']}) {nullable}")
142
+
143
+ summary_lines.append("")
144
+
145
+ # Relationships summary
146
+ if schema["relationships"]:
147
+ summary_lines.append("## Relationships\n")
148
+ for rel in schema["relationships"]:
149
+ summary_lines.append(
150
+ f"- `{rel['from_table']}.{rel['from_column']}` → `{rel['to_table']}.{rel['to_column']}`"
151
+ )
152
+
153
+ return "\n".join(summary_lines)