File size: 14,378 Bytes
4beb1ef
 
38f1436
 
4beb1ef
 
 
3545eca
4beb1ef
 
 
 
 
 
 
f11a1eb
4beb1ef
f11a1eb
 
 
3545eca
f11a1eb
 
 
3545eca
f11a1eb
 
 
 
38f1436
 
 
 
 
f11a1eb
 
3545eca
f11a1eb
 
 
 
 
 
 
38f1436
 
 
 
f11a1eb
 
 
4beb1ef
 
 
f11a1eb
3545eca
f11a1eb
4beb1ef
3545eca
4beb1ef
 
 
 
 
 
 
3545eca
4beb1ef
 
3545eca
 
 
 
 
 
 
 
 
 
4beb1ef
3545eca
4beb1ef
 
 
f11a1eb
 
 
 
4beb1ef
 
 
 
 
 
f11a1eb
4beb1ef
 
 
 
 
 
 
 
 
f11a1eb
 
 
4beb1ef
f11a1eb
4beb1ef
 
f11a1eb
 
4beb1ef
 
f11a1eb
 
 
 
 
 
 
 
 
4beb1ef
38f1436
 
 
 
 
3545eca
38f1436
 
3545eca
38f1436
 
 
 
 
 
 
3545eca
38f1436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3545eca
 
 
 
 
 
 
 
 
 
 
 
 
38f1436
 
 
 
 
3545eca
38f1436
 
 
 
 
3545eca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38f1436
4beb1ef
 
 
 
 
 
3545eca
f11a1eb
 
 
 
 
 
38f1436
f11a1eb
 
 
 
 
 
 
 
 
 
 
 
38f1436
 
 
 
 
 
 
 
 
 
 
f11a1eb
 
 
4beb1ef
38f1436
4beb1ef
 
 
 
 
f11a1eb
4beb1ef
f11a1eb
 
4beb1ef
 
 
 
f11a1eb
 
4beb1ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3545eca
 
 
 
 
4beb1ef
3545eca
 
 
 
4beb1ef
 
 
3545eca
4beb1ef
3545eca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4beb1ef
 
f11a1eb
 
 
 
 
 
 
 
 
 
 
 
38f1436
 
 
3545eca
38f1436
 
4beb1ef
 
 
3545eca
4beb1ef
 
 
 
 
3545eca
 
 
 
 
 
 
 
4beb1ef
 
 
 
 
 
 
 
 
 
f11a1eb
 
4beb1ef
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import gradio as gr
import logging
import tempfile
import os
from .db_connector import DBConnector
from .schema_inspector import SchemaInspector
from .merge_operations import MergeOperations
from .db_visualizer import DatabaseVisualizer
from .config import get_config

# Set up logging
logging.basicConfig(level=logging.INFO, 
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Initialize config but don't connect to DB yet
config = get_config()
db = None
inspector = None
merge_ops = None
visualizer = None

def connect_to_database(db_url):
    """Connect to the database with provided URL"""
    global db, inspector, merge_ops, visualizer
    
    try:
        # Initialize new connection
        db = DBConnector(db_url)
        success = db.connect()
        
        if not success:
            return "Failed to connect to database. Check the connection string and ensure the required database driver is installed.", gr.Dropdown(choices=[])
            
        inspector = SchemaInspector(db)
        merge_ops = MergeOperations(db, inspector)
        visualizer = DatabaseVisualizer(db, inspector)
        
        # Test connection by fetching tables
        tables = get_db_tables()
        if tables:
            return f"Successfully connected to database. Found {len(tables)} tables.", gr.Dropdown(choices=tables)
        else:
            return "Connected to database but found no tables.", gr.Dropdown(choices=[])
    except ImportError as e:
        error_msg = f"Database driver error: {str(e)}"
        logger.error(error_msg)
        return error_msg, gr.Dropdown(choices=[])
    except Exception as e:
        logger.error(f"Database connection error: {str(e)}")
        return f"Error connecting to database: {str(e)}", gr.Dropdown(choices=[])

def handle_merge(action, table, column, from_values, target_value, preview_only=True):
    """Handler for Gradio interface"""
    if not db:
        return "Error: Not connected to database. Please connect first.", "", ""
    
    if not table or not column:
        return "Error: Table and column must be specified", "", ""
    
    # Parse from_values as comma-separated list
    from_values_list = [v.strip() for v in from_values.split(',')]
    
    if action == "Merge Values":
        if preview_only:
            result = merge_ops.preview_merge(table, column, from_values_list, target_value)
            return result["preview"], "", ""
        else:
            result = merge_ops.run_merge(table, column, from_values_list, target_value)
            # Auto-generate visualization after successful operation
            if result.get("success", False) and visualizer:
                try:
                    text_summary = visualizer.generate_table_summary()
                    mermaid_diagram = visualizer.generate_mermaid_diagram()
                    return result["log"], text_summary, mermaid_diagram
                except Exception as e:
                    logger.error(f"Error generating visualization after merge: {str(e)}")
                    return result["log"], "Error generating visualization", ""
            return result["log"], "", ""
    else:
        return "Action not implemented yet", "", ""

def get_db_tables():
    """Get list of tables from the database for dropdown"""
    if not db:
        logger.warning("Attempted to fetch tables but database not connected")
        return []
        
    logger.info("Fetching tables from the database...")
    try:
        # Try a much simpler query first - just get tables from public schema
        query = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'"
        result = db.execute_query(query)
        tables = [row[0] for row in result]

        logger.info(f"Found {len(tables)} tables in the database")
        return tables
    except Exception as e:
        logger.error(f"Error getting tables: {str(e)}")
        # Return an empty list as last resort
        return []

def get_columns(table):
    """Get columns for selected table"""
    if not db or not inspector:
        return gr.Dropdown(choices=[])
        
    if not table:
        return gr.Dropdown(choices=[])
    try:
        columns = inspector.get_column_info(table)
        logger.info(f"Columns for {table}: {columns}")
        return gr.Dropdown(choices=[col['column_name'] for col in columns])
    except Exception as e:
        logger.error(f"Error getting columns: {str(e)}")
        return gr.Dropdown(choices=[])

def refresh_tables():
    """Refresh the list of tables"""
    if not db:
        return "Not connected to database", gr.Dropdown(choices=[])
    
    tables = get_db_tables()
    return f"Refreshed tables. Found {len(tables)} tables.", gr.Dropdown(choices=tables)

def execute_sql_file(sql_file):
    """Execute SQL commands from uploaded file"""
    global db
    
    if not db:
        return "Error: Not connected to database. Please connect first.", "", ""
    
    if sql_file is None:
        return "Error: No SQL file provided.", "", ""
    
    try:
        # Read the uploaded file
        with open(sql_file.name, 'r', encoding='utf-8') as f:
            sql_content = f.read()
        
        if not sql_content.strip():
            return "Error: SQL file is empty.", "", ""
        
        # Split SQL content by semicolons and execute each statement
        sql_statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()]
        
        results = []
        session = db.get_session()
        
        try:
            for i, statement in enumerate(sql_statements, 1):
                logger.info(f"Executing SQL statement {i}: {statement[:100]}...")
                result = session.execute(statement)
                
                # Try to fetch results if it's a SELECT statement
                if statement.strip().upper().startswith('SELECT'):
                    rows = result.fetchall()
                    results.append(f"Statement {i}: Returned {len(rows)} rows")
                    if len(rows) <= 10:  # Show first 10 rows for small results
                        for row in rows:
                            results.append(f"  {row}")
                else:
                    results.append(f"Statement {i}: Executed successfully (affected rows: {result.rowcount})")
            
            session.commit()
            results.insert(0, f"Successfully executed {len(sql_statements)} SQL statements.")
            operation_log = "\n".join(results)
            
            # Auto-generate visualization after successful SQL execution
            if visualizer:
                try:
                    text_summary = visualizer.generate_table_summary()
                    mermaid_diagram = visualizer.generate_mermaid_diagram()
                    return operation_log, text_summary, mermaid_diagram
                except Exception as e:
                    logger.error(f"Error generating visualization after SQL execution: {str(e)}")
                    return operation_log, "Error generating visualization", ""
            
            return operation_log, "", ""
            
        except Exception as e:
            session.rollback()
            error_msg = f"Error executing SQL: {str(e)}"
            logger.error(error_msg)
            return error_msg, "", ""
        finally:
            session.close()
            
    except Exception as e:
        logger.error(f"Error reading SQL file: {str(e)}")
        return f"Error reading SQL file: {str(e)}", "", ""

def generate_database_visualization():
    """Generate database visualization"""
    if not visualizer:
        return "Error: Not connected to database. Please connect first.", ""
    
    try:
        # Generate both Mermaid diagram and text summary
        mermaid_diagram = visualizer.generate_mermaid_diagram()
        text_summary = visualizer.generate_table_summary()
        
        return text_summary, mermaid_diagram
        
    except Exception as e:
        error_msg = f"Error generating visualization: {str(e)}"
        logger.error(error_msg)
        return error_msg, ""

def refresh_visualization():
    """Refresh the database visualization"""
    return generate_database_visualization()

def create_ui():
    """Create and configure the Gradio UI"""
    with gr.Blocks(title="SchemaSync") as app:
        gr.Markdown("# SchemaSync - Database Schema Manipulation Tool")
        
        with gr.Row():
            with gr.Column(scale=1):
                # Database Connection Section
                gr.Markdown("## Database Connection")
                
                db_url = gr.Textbox(
                    label="Database URL",
                    placeholder="postgresql://username:password@localhost:5432/database",
                    value="",
                    type="password"
                )
                
                with gr.Row():
                    connect_btn = gr.Button("Connect to Database")
                    refresh_btn = gr.Button("Refresh Tables")
                
                connection_status = gr.Textbox(
                    label="Connection Status",
                    interactive=False
                )
                
                # SQL Import Section
                gr.Markdown("## SQL File Import")
                
                sql_file = gr.File(
                    label="Upload SQL File",
                    file_types=[".sql", ".txt"],
                    file_count="single"
                )
                
                execute_sql_btn = gr.Button("Execute SQL File")
                
                # Operations Section
                gr.Markdown("## Schema Operations")
                
                action = gr.Dropdown(
                    choices=["Merge Values"],
                    label="Action",
                    value="Merge Values"
                )
                
                table = gr.Dropdown(
                    choices=[],
                    label="Table",
                    interactive=True,
                    allow_custom_value=True
                )
                
                column = gr.Dropdown(
                    label="Column",
                    interactive=True,
                    allow_custom_value=True
                )
                
                # Update column dropdown when table changes
                table.change(fn=get_columns, inputs=table, outputs=column)
                
                from_values = gr.Textbox(
                    label="From Values (comma-separated)",
                    placeholder="value1, value2, value3"
                )
                
                target_value = gr.Textbox(
                    label="Target Value",
                    placeholder="target_value"
                )
                
                preview_checkbox = gr.Checkbox(
                    label="Preview Only (no changes will be made)",
                    value=True
                )
                
                with gr.Row():
                    preview_btn = gr.Button("Preview Changes")
                    run_btn = gr.Button("Run Operation", variant="primary")
                
                # Database Visualization Section
                gr.Markdown("## Database Visualization")
                
                visualize_btn = gr.Button("Generate Visualization", variant="secondary")
            
            with gr.Column(scale=2):
                # Operation Results
                gr.Markdown("## Operation Results")
                
                output = gr.TextArea(
                    label="Operation Log",
                    placeholder="Operation results will appear here",
                    lines=15
                )
                
                # Visualization Results
                gr.Markdown("## Database Schema")
                
                with gr.Tabs():
                    with gr.TabItem("Schema Summary"):
                        schema_summary = gr.Markdown(
                            value="Connect to a database and run operations or click 'Generate Visualization' to see the schema structure."
                        )
                    
                    with gr.TabItem("ER Diagram"):
                        gr.Markdown("Copy the code below and paste it into [Mermaid Live Editor](https://mermaid.live) to view the interactive diagram.")
                        
                        mermaid_code = gr.Code(
                            label="Mermaid Diagram Code",
                            language="markdown",
                            lines=15,
                            value="Connect to database and run operations to see diagram code here."
                        )
        
        # Connect buttons to handlers
        connect_btn.click(
            fn=connect_to_database,
            inputs=db_url,
            outputs=[connection_status, table]
        )
        
        refresh_btn.click(
            fn=refresh_tables,
            inputs=None,
            outputs=[connection_status, table]
        )
        
        execute_sql_btn.click(
            fn=execute_sql_file,
            inputs=sql_file,
            outputs=[output, schema_summary, mermaid_code]
        )
        
        preview_btn.click(
            fn=handle_merge,
            inputs=[action, table, column, from_values, target_value, preview_checkbox],
            outputs=[output, schema_summary, mermaid_code]
        )
        
        run_btn.click(
            fn=handle_merge,
            inputs=[action, table, column, from_values, target_value, gr.Checkbox(value=False, visible=False)],
            outputs=[output, schema_summary, mermaid_code]
        )
        
        # Manual visualization generation
        visualize_btn.click(
            fn=generate_database_visualization,
            inputs=None,
            outputs=[schema_summary, mermaid_code]
        )
        
    return app

def main():
    """Main entry point for the application"""
    app = create_ui()
    app.launch(
        server_name=config.get('HOST', '0.0.0.0'),
        server_port=int(config.get('PORT', 7860)),
        share=False,
        debug=True,
    )
    # Don't return anything from this function