sqlshell 0.1.5__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlshell might be problematic. Click here for more details.

sqlshell/main.py CHANGED
@@ -1,125 +1,888 @@
1
1
  import sys
2
2
  import os
3
+ import json
4
+
5
+ # Ensure proper path setup for resources when running directly
6
+ if __name__ == "__main__":
7
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8
+ sys.path.insert(0, project_root)
9
+
3
10
  import duckdb
4
11
  import sqlite3
5
12
  import pandas as pd
6
13
  from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
7
14
  QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
8
15
  QLabel, QSplitter, QListWidget, QTableWidget,
9
- QTableWidgetItem, QHeaderView, QMessageBox)
10
- from PyQt6.QtCore import Qt, QAbstractTableModel
11
- from PyQt6.QtGui import QFont, QColor
16
+ QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
17
+ QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
18
+ QStyleFactory, QToolBar, QStatusBar, QLineEdit, QMenu,
19
+ QCheckBox, QWidgetAction, QMenuBar, QInputDialog,
20
+ QStyledItemDelegate)
21
+ from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint
22
+ from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath
12
23
  import numpy as np
13
24
  from datetime import datetime
14
- from sqlshell.sqlshell import create_test_data # Import from the correct location
25
+
26
+ from sqlshell import create_test_data
27
+ from sqlshell.splash_screen import AnimatedSplashScreen
28
+ from sqlshell.syntax_highlighter import SQLSyntaxHighlighter
29
+ from sqlshell.editor import LineNumberArea, SQLEditor
30
+
31
+ class BarChartDelegate(QStyledItemDelegate):
32
+ def __init__(self, parent=None):
33
+ super().__init__(parent)
34
+ self.min_val = 0
35
+ self.max_val = 1
36
+ self.bar_color = QColor("#3498DB")
37
+
38
+ def set_range(self, min_val, max_val):
39
+ self.min_val = min_val
40
+ self.max_val = max_val
41
+
42
+ def paint(self, painter, option, index):
43
+ # Draw the default background
44
+ super().paint(painter, option, index)
45
+
46
+ try:
47
+ text = index.data()
48
+ value = float(text.replace(',', ''))
49
+
50
+ # Calculate normalized value
51
+ range_val = self.max_val - self.min_val if self.max_val != self.min_val else 1
52
+ normalized = (value - self.min_val) / range_val
53
+
54
+ # Define bar dimensions
55
+ bar_height = 16
56
+ max_bar_width = 100
57
+ bar_width = max(5, int(max_bar_width * normalized))
58
+
59
+ # Calculate positions
60
+ text_width = option.fontMetrics.horizontalAdvance(text) + 10
61
+ bar_x = option.rect.left() + text_width + 10
62
+ bar_y = option.rect.center().y() - bar_height // 2
63
+
64
+ # Draw the bar
65
+ bar_rect = QRect(bar_x, bar_y, bar_width, bar_height)
66
+ painter.fillRect(bar_rect, self.bar_color)
67
+
68
+ # Draw the text
69
+ text_rect = QRect(option.rect.left() + 4, option.rect.top(),
70
+ text_width, option.rect.height())
71
+ painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
72
+
73
+ except (ValueError, AttributeError):
74
+ # If not a number, just draw the text
75
+ super().paint(painter, option, index)
76
+
77
+ class FilterHeader(QHeaderView):
78
+ def __init__(self, parent=None):
79
+ super().__init__(Qt.Orientation.Horizontal, parent)
80
+ self.filter_buttons = []
81
+ self.active_filters = {} # Track active filters for each column
82
+ self.columns_with_bars = set() # Track which columns show bar charts
83
+ self.bar_delegates = {} # Store delegates for columns with bars
84
+ self.setSectionsClickable(True)
85
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
86
+ self.customContextMenuRequested.connect(self.show_header_context_menu)
87
+ self.main_window = None # Store reference to main window
88
+ self.filter_icon_color = QColor("#3498DB") # Bright blue color for filter icon
89
+
90
+ def toggle_bar_chart(self, column_index):
91
+ """Toggle bar chart visualization for a column"""
92
+ table = self.parent()
93
+ if not table:
94
+ return
95
+
96
+ if column_index in self.columns_with_bars:
97
+ # Remove bars
98
+ self.columns_with_bars.remove(column_index)
99
+ if column_index in self.bar_delegates:
100
+ table.setItemDelegateForColumn(column_index, None)
101
+ del self.bar_delegates[column_index]
102
+ else:
103
+ # Add bars
104
+ self.columns_with_bars.add(column_index)
105
+
106
+ # Get all values for normalization
107
+ values = []
108
+ for row in range(table.rowCount()):
109
+ item = table.item(row, column_index)
110
+ if item:
111
+ try:
112
+ value = float(item.text().replace(',', ''))
113
+ values.append(value)
114
+ except ValueError:
115
+ continue
116
+
117
+ if not values:
118
+ return
119
+
120
+ # Calculate min and max for normalization
121
+ min_val = min(values)
122
+ max_val = max(values)
123
+
124
+ # Create and set up delegate
125
+ delegate = BarChartDelegate(table)
126
+ delegate.set_range(min_val, max_val)
127
+ self.bar_delegates[column_index] = delegate
128
+ table.setItemDelegateForColumn(column_index, delegate)
129
+
130
+ # Update the view
131
+ table.viewport().update()
132
+
133
+ def show_header_context_menu(self, pos):
134
+ """Show context menu for header section"""
135
+ logical_index = self.logicalIndexAt(pos)
136
+ if logical_index < 0:
137
+ return
138
+
139
+ # Create context menu
140
+ context_menu = QMenu(self)
141
+ context_menu.setStyleSheet("""
142
+ QMenu {
143
+ background-color: white;
144
+ border: 1px solid #BDC3C7;
145
+ padding: 5px;
146
+ }
147
+ QMenu::item {
148
+ padding: 5px 20px;
149
+ }
150
+ QMenu::item:selected {
151
+ background-color: #3498DB;
152
+ color: white;
153
+ }
154
+ """)
155
+
156
+ # Add sort actions
157
+ sort_asc_action = context_menu.addAction("Sort Ascending")
158
+ sort_desc_action = context_menu.addAction("Sort Descending")
159
+ context_menu.addSeparator()
160
+ filter_action = context_menu.addAction("Filter...")
161
+
162
+ # Add bar chart action if column is numeric
163
+ table = self.parent()
164
+ if table and table.rowCount() > 0:
165
+ try:
166
+ # Check if column contains numeric values
167
+ sample_value = table.item(0, logical_index).text()
168
+ float(sample_value.replace(',', '')) # Try converting to float
169
+
170
+ context_menu.addSeparator()
171
+ toggle_bar_action = context_menu.addAction(
172
+ "Remove Bar Chart" if logical_index in self.columns_with_bars
173
+ else "Add Bar Chart"
174
+ )
175
+ except (ValueError, AttributeError):
176
+ toggle_bar_action = None
177
+ else:
178
+ toggle_bar_action = None
179
+
180
+ # Show menu and get selected action
181
+ action = context_menu.exec(self.mapToGlobal(pos))
182
+
183
+ if not action:
184
+ return
185
+
186
+ table = self.parent()
187
+ if not table:
188
+ return
189
+
190
+ if action == sort_asc_action:
191
+ table.sortItems(logical_index, Qt.SortOrder.AscendingOrder)
192
+ elif action == sort_desc_action:
193
+ table.sortItems(logical_index, Qt.SortOrder.DescendingOrder)
194
+ elif action == filter_action:
195
+ self.show_filter_menu(logical_index)
196
+ elif action == toggle_bar_action:
197
+ self.toggle_bar_chart(logical_index)
198
+
199
+ def set_main_window(self, window):
200
+ """Set the reference to the main window"""
201
+ self.main_window = window
202
+
203
+ def paintSection(self, painter, rect, logical_index):
204
+ """Override paint section to add filter indicator"""
205
+ super().paintSection(painter, rect, logical_index)
206
+
207
+ if logical_index in self.active_filters:
208
+ # Draw background highlight for filtered columns
209
+ highlight_color = QColor(52, 152, 219, 30) # Light blue background
210
+ painter.fillRect(rect, highlight_color)
211
+
212
+ # Make icon larger and more visible
213
+ icon_size = min(rect.height() - 8, 24) # Larger icon, but not too large
214
+ margin = 6
215
+ icon_rect = QRect(
216
+ rect.right() - icon_size - margin,
217
+ rect.top() + (rect.height() - icon_size) // 2,
218
+ icon_size,
219
+ icon_size
220
+ )
221
+
222
+ # Draw filter icon with improved visibility
223
+ painter.save()
224
+
225
+ # Set up the pen for better visibility
226
+ pen = painter.pen()
227
+ pen.setWidth(3) # Thicker lines
228
+ pen.setColor(self.filter_icon_color)
229
+ painter.setPen(pen)
230
+
231
+ # Calculate points for larger funnel shape
232
+ points = [
233
+ QPoint(icon_rect.left(), icon_rect.top()),
234
+ QPoint(icon_rect.right(), icon_rect.top()),
235
+ QPoint(icon_rect.center().x() + icon_size//3, icon_rect.center().y()),
236
+ QPoint(icon_rect.center().x() + icon_size//3, icon_rect.bottom()),
237
+ QPoint(icon_rect.center().x() - icon_size//3, icon_rect.bottom()),
238
+ QPoint(icon_rect.center().x() - icon_size//3, icon_rect.center().y()),
239
+ QPoint(icon_rect.left(), icon_rect.top())
240
+ ]
241
+
242
+ # Create and fill path
243
+ path = QPainterPath()
244
+ path.moveTo(float(points[0].x()), float(points[0].y()))
245
+ for point in points[1:]:
246
+ path.lineTo(float(point.x()), float(point.y()))
247
+
248
+ # Fill with semi-transparent blue
249
+ painter.fillPath(path, QBrush(QColor(52, 152, 219, 120))) # More opaque fill
250
+
251
+ # Draw outline
252
+ painter.drawPolyline(QPolygon(points))
253
+
254
+ # If multiple values are filtered, add a number
255
+ if len(self.active_filters[logical_index]) > 1:
256
+ # Draw number with better visibility
257
+ number_rect = QRect(icon_rect.left(), icon_rect.top(),
258
+ icon_rect.width(), icon_rect.height())
259
+ painter.setFont(QFont("Arial", icon_size//2, QFont.Weight.Bold))
260
+
261
+ # Draw text shadow for better contrast
262
+ painter.setPen(QColor("white"))
263
+ painter.drawText(number_rect.adjusted(1, 1, 1, 1),
264
+ Qt.AlignmentFlag.AlignCenter,
265
+ str(len(self.active_filters[logical_index])))
266
+
267
+ # Draw main text
268
+ painter.setPen(self.filter_icon_color)
269
+ painter.drawText(number_rect, Qt.AlignmentFlag.AlignCenter,
270
+ str(len(self.active_filters[logical_index])))
271
+
272
+ painter.restore()
273
+
274
+ # Draw a more visible indicator at the bottom of the header section
275
+ painter.save()
276
+ indicator_height = 3 # Thicker indicator line
277
+ indicator_rect = QRect(rect.left(), rect.bottom() - indicator_height,
278
+ rect.width(), indicator_height)
279
+ painter.fillRect(indicator_rect, self.filter_icon_color)
280
+ painter.restore()
281
+
282
+ def show_filter_menu(self, logical_index):
283
+ if not self.parent() or not isinstance(self.parent(), QTableWidget):
284
+ return
285
+
286
+ table = self.parent()
287
+ unique_values = set()
288
+
289
+ # Collect unique values from the column
290
+ for row in range(table.rowCount()):
291
+ item = table.item(row, logical_index)
292
+ if item and not table.isRowHidden(row):
293
+ unique_values.add(item.text())
294
+
295
+ # Create and show the filter menu
296
+ menu = QMenu(self)
297
+ menu.setStyleSheet("""
298
+ QMenu {
299
+ background-color: white;
300
+ border: 1px solid #BDC3C7;
301
+ padding: 5px;
302
+ }
303
+ QMenu::item {
304
+ padding: 5px 20px;
305
+ }
306
+ QMenu::item:selected {
307
+ background-color: #3498DB;
308
+ color: white;
309
+ }
310
+ QCheckBox {
311
+ padding: 5px;
312
+ }
313
+ QScrollArea {
314
+ border: none;
315
+ }
316
+ """)
317
+
318
+ # Add search box at the top
319
+ search_widget = QWidget(menu)
320
+ search_layout = QVBoxLayout(search_widget)
321
+ search_edit = QLineEdit(search_widget)
322
+ search_edit.setPlaceholderText("Search values...")
323
+ search_layout.addWidget(search_edit)
324
+
325
+ # Add action for search widget
326
+ search_action = QWidgetAction(menu)
327
+ search_action.setDefaultWidget(search_widget)
328
+ menu.addAction(search_action)
329
+ menu.addSeparator()
330
+
331
+ # Add "Select All" checkbox
332
+ select_all = QCheckBox("Select All", menu)
333
+ select_all.setChecked(True)
334
+ select_all_action = QWidgetAction(menu)
335
+ select_all_action.setDefaultWidget(select_all)
336
+ menu.addAction(select_all_action)
337
+ menu.addSeparator()
338
+
339
+ # Create scrollable area for checkboxes
340
+ scroll_widget = QWidget(menu)
341
+ scroll_layout = QVBoxLayout(scroll_widget)
342
+ scroll_layout.setSpacing(2)
343
+ scroll_layout.setContentsMargins(0, 0, 0, 0)
344
+
345
+ # Add checkboxes for unique values
346
+ value_checkboxes = {}
347
+ for value in sorted(unique_values):
348
+ checkbox = QCheckBox(str(value), scroll_widget)
349
+ # Set checked state based on active filters
350
+ checkbox.setChecked(logical_index not in self.active_filters or
351
+ value in self.active_filters[logical_index])
352
+ value_checkboxes[value] = checkbox
353
+ scroll_layout.addWidget(checkbox)
354
+
355
+ # Add scrollable area to menu
356
+ scroll_action = QWidgetAction(menu)
357
+ scroll_action.setDefaultWidget(scroll_widget)
358
+ menu.addAction(scroll_action)
359
+
360
+ # Connect search box to filter checkboxes
361
+ def filter_checkboxes(text):
362
+ for value, checkbox in value_checkboxes.items():
363
+ checkbox.setVisible(text.lower() in str(value).lower())
364
+
365
+ search_edit.textChanged.connect(filter_checkboxes)
366
+
367
+ # Connect select all to other checkboxes
368
+ def toggle_all(state):
369
+ for checkbox in value_checkboxes.values():
370
+ if not checkbox.isHidden(): # Only toggle visible checkboxes
371
+ checkbox.setChecked(state)
372
+
373
+ select_all.stateChanged.connect(toggle_all)
374
+
375
+ # Add Apply and Clear buttons
376
+ menu.addSeparator()
377
+ apply_button = QPushButton("Apply Filter", menu)
378
+ apply_button.setStyleSheet("""
379
+ QPushButton {
380
+ background-color: #2ECC71;
381
+ color: white;
382
+ border: none;
383
+ padding: 5px 15px;
384
+ border-radius: 3px;
385
+ }
386
+ QPushButton:hover {
387
+ background-color: #27AE60;
388
+ }
389
+ """)
390
+
391
+ clear_button = QPushButton("Clear Filter", menu)
392
+ clear_button.setStyleSheet("""
393
+ QPushButton {
394
+ background-color: #E74C3C;
395
+ color: white;
396
+ border: none;
397
+ padding: 5px 15px;
398
+ border-radius: 3px;
399
+ }
400
+ QPushButton:hover {
401
+ background-color: #C0392B;
402
+ }
403
+ """)
404
+
405
+ button_widget = QWidget(menu)
406
+ button_layout = QHBoxLayout(button_widget)
407
+ button_layout.addWidget(apply_button)
408
+ button_layout.addWidget(clear_button)
409
+
410
+ button_action = QWidgetAction(menu)
411
+ button_action.setDefaultWidget(button_widget)
412
+ menu.addAction(button_action)
413
+
414
+ def apply_filter():
415
+ # Get selected values
416
+ selected_values = {value for value, checkbox in value_checkboxes.items()
417
+ if checkbox.isChecked()}
418
+
419
+ if len(selected_values) < len(unique_values):
420
+ # Store active filter only if not all values are selected
421
+ self.active_filters[logical_index] = selected_values
422
+ else:
423
+ # Remove filter if all values are selected
424
+ self.active_filters.pop(logical_index, None)
425
+
426
+ # Apply all active filters
427
+ self.apply_all_filters(table)
428
+
429
+ menu.close()
430
+ self.updateSection(logical_index) # Redraw section to show/hide filter icon
431
+
432
+ def clear_filter():
433
+ # Remove filter for this column
434
+ if logical_index in self.active_filters:
435
+ del self.active_filters[logical_index]
436
+
437
+ # Apply remaining filters
438
+ self.apply_all_filters(table)
439
+
440
+ menu.close()
441
+ self.updateSection(logical_index) # Redraw section to hide filter icon
442
+
443
+ apply_button.clicked.connect(apply_filter)
444
+ clear_button.clicked.connect(clear_filter)
445
+
446
+ # Show menu under the header section
447
+ header_pos = self.mapToGlobal(self.geometry().bottomLeft())
448
+ header_pos.setX(header_pos.x() + self.sectionPosition(logical_index))
449
+ menu.exec(header_pos)
450
+
451
+ def apply_all_filters(self, table):
452
+ """Apply all active filters to the table"""
453
+ # Show all rows first
454
+ for row in range(table.rowCount()):
455
+ table.setRowHidden(row, False)
456
+
457
+ # Apply each active filter
458
+ for col_idx, allowed_values in self.active_filters.items():
459
+ for row in range(table.rowCount()):
460
+ item = table.item(row, col_idx)
461
+ if item and not table.isRowHidden(row):
462
+ table.setRowHidden(row, item.text() not in allowed_values)
463
+
464
+ # Update status bar with visible row count
465
+ if self.main_window:
466
+ visible_rows = sum(1 for row in range(table.rowCount())
467
+ if not table.isRowHidden(row))
468
+ total_filters = len(self.active_filters)
469
+ filter_text = f" ({total_filters} filter{'s' if total_filters != 1 else ''} active)" if total_filters > 0 else ""
470
+ self.main_window.statusBar().showMessage(
471
+ f"Showing {visible_rows:,} rows{filter_text}")
15
472
 
16
473
  class SQLShell(QMainWindow):
17
474
  def __init__(self):
18
475
  super().__init__()
19
- self.current_db_type = 'duckdb' # Default to DuckDB
20
- self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
476
+ self.conn = None
477
+ self.current_connection_type = None
21
478
  self.loaded_tables = {} # Keep track of loaded tables
479
+ self.table_columns = {} # Keep track of table columns
480
+ self.current_df = None # Store the current DataFrame for filtering
481
+ self.filter_widgets = [] # Store filter line edits
482
+ self.current_project_file = None # Store the current project file path
483
+
484
+ # Define color scheme
485
+ self.colors = {
486
+ 'primary': "#2C3E50", # Dark blue-gray
487
+ 'secondary': "#3498DB", # Bright blue
488
+ 'accent': "#1ABC9C", # Teal
489
+ 'background': "#ECF0F1", # Light gray
490
+ 'text': "#2C3E50", # Dark blue-gray
491
+ 'text_light': "#7F8C8D", # Medium gray
492
+ 'success': "#2ECC71", # Green
493
+ 'warning': "#F39C12", # Orange
494
+ 'error': "#E74C3C", # Red
495
+ 'dark_bg': "#34495E", # Darker blue-gray
496
+ 'light_bg': "#F5F5F5", # Very light gray
497
+ 'border': "#BDC3C7" # Light gray border
498
+ }
499
+
22
500
  self.init_ui()
501
+ self.apply_stylesheet()
502
+
503
+ def apply_stylesheet(self):
504
+ """Apply custom stylesheet to the application"""
505
+ self.setStyleSheet(f"""
506
+ QMainWindow {{
507
+ background-color: {self.colors['background']};
508
+ }}
509
+
510
+ QWidget {{
511
+ color: {self.colors['text']};
512
+ font-family: 'Segoe UI', 'Arial', sans-serif;
513
+ }}
514
+
515
+ QLabel {{
516
+ font-size: 13px;
517
+ padding: 2px;
518
+ }}
519
+
520
+ QLabel#header_label {{
521
+ font-size: 16px;
522
+ font-weight: bold;
523
+ color: {self.colors['primary']};
524
+ padding: 8px 0;
525
+ }}
526
+
527
+ QPushButton {{
528
+ background-color: {self.colors['secondary']};
529
+ color: white;
530
+ border: none;
531
+ border-radius: 4px;
532
+ padding: 8px 16px;
533
+ font-weight: bold;
534
+ font-size: 13px;
535
+ min-height: 30px;
536
+ }}
537
+
538
+ QPushButton:hover {{
539
+ background-color: #2980B9;
540
+ }}
541
+
542
+ QPushButton:pressed {{
543
+ background-color: #1F618D;
544
+ }}
545
+
546
+ QPushButton#primary_button {{
547
+ background-color: {self.colors['accent']};
548
+ }}
549
+
550
+ QPushButton#primary_button:hover {{
551
+ background-color: #16A085;
552
+ }}
553
+
554
+ QPushButton#primary_button:pressed {{
555
+ background-color: #0E6655;
556
+ }}
557
+
558
+ QPushButton#danger_button {{
559
+ background-color: {self.colors['error']};
560
+ }}
561
+
562
+ QPushButton#danger_button:hover {{
563
+ background-color: #CB4335;
564
+ }}
565
+
566
+ QToolButton {{
567
+ background-color: transparent;
568
+ border: none;
569
+ border-radius: 4px;
570
+ padding: 4px;
571
+ }}
572
+
573
+ QToolButton:hover {{
574
+ background-color: rgba(52, 152, 219, 0.2);
575
+ }}
576
+
577
+ QFrame#sidebar {{
578
+ background-color: {self.colors['primary']};
579
+ border-radius: 0px;
580
+ }}
581
+
582
+ QFrame#content_panel {{
583
+ background-color: white;
584
+ border-radius: 8px;
585
+ border: 1px solid {self.colors['border']};
586
+ }}
587
+
588
+ QListWidget {{
589
+ background-color: white;
590
+ border-radius: 4px;
591
+ border: 1px solid {self.colors['border']};
592
+ padding: 4px;
593
+ outline: none;
594
+ }}
595
+
596
+ QListWidget::item {{
597
+ padding: 8px;
598
+ border-radius: 4px;
599
+ }}
600
+
601
+ QListWidget::item:selected {{
602
+ background-color: {self.colors['secondary']};
603
+ color: white;
604
+ }}
605
+
606
+ QListWidget::item:hover:!selected {{
607
+ background-color: #E3F2FD;
608
+ }}
609
+
610
+ QTableWidget {{
611
+ background-color: white;
612
+ alternate-background-color: #F8F9FA;
613
+ border-radius: 4px;
614
+ border: 1px solid {self.colors['border']};
615
+ gridline-color: #E0E0E0;
616
+ outline: none;
617
+ }}
618
+
619
+ QTableWidget::item {{
620
+ padding: 4px;
621
+ }}
622
+
623
+ QTableWidget::item:selected {{
624
+ background-color: rgba(52, 152, 219, 0.2);
625
+ color: {self.colors['text']};
626
+ }}
627
+
628
+ QHeaderView::section {{
629
+ background-color: {self.colors['primary']};
630
+ color: white;
631
+ padding: 8px;
632
+ border: none;
633
+ font-weight: bold;
634
+ }}
635
+
636
+ QSplitter::handle {{
637
+ background-color: {self.colors['border']};
638
+ }}
639
+
640
+ QStatusBar {{
641
+ background-color: {self.colors['primary']};
642
+ color: white;
643
+ padding: 8px;
644
+ }}
645
+
646
+ QPlainTextEdit, QTextEdit {{
647
+ background-color: white;
648
+ border-radius: 4px;
649
+ border: 1px solid {self.colors['border']};
650
+ padding: 8px;
651
+ selection-background-color: #BBDEFB;
652
+ selection-color: {self.colors['text']};
653
+ font-family: 'Consolas', 'Courier New', monospace;
654
+ font-size: 14px;
655
+ }}
656
+ """)
23
657
 
24
658
  def init_ui(self):
25
659
  self.setWindowTitle('SQL Shell')
26
660
  self.setGeometry(100, 100, 1400, 800)
27
-
661
+
662
+ # Create menu bar
663
+ menubar = self.menuBar()
664
+ file_menu = menubar.addMenu('&File')
665
+
666
+ # Project management actions
667
+ new_project_action = file_menu.addAction('New Project')
668
+ new_project_action.setShortcut('Ctrl+N')
669
+ new_project_action.triggered.connect(self.new_project)
670
+
671
+ open_project_action = file_menu.addAction('Open Project...')
672
+ open_project_action.setShortcut('Ctrl+O')
673
+ open_project_action.triggered.connect(self.open_project)
674
+
675
+ save_project_action = file_menu.addAction('Save Project')
676
+ save_project_action.setShortcut('Ctrl+S')
677
+ save_project_action.triggered.connect(self.save_project)
678
+
679
+ save_project_as_action = file_menu.addAction('Save Project As...')
680
+ save_project_as_action.setShortcut('Ctrl+Shift+S')
681
+ save_project_as_action.triggered.connect(self.save_project_as)
682
+
683
+ file_menu.addSeparator()
684
+
685
+ exit_action = file_menu.addAction('Exit')
686
+ exit_action.setShortcut('Ctrl+Q')
687
+ exit_action.triggered.connect(self.close)
688
+
689
+ # Create custom status bar
690
+ status_bar = QStatusBar()
691
+ self.setStatusBar(status_bar)
692
+
28
693
  # Create central widget and layout
29
694
  central_widget = QWidget()
30
695
  self.setCentralWidget(central_widget)
31
696
  main_layout = QHBoxLayout(central_widget)
697
+ main_layout.setContentsMargins(0, 0, 0, 0)
698
+ main_layout.setSpacing(0)
32
699
 
33
700
  # Left panel for table list
34
- left_panel = QWidget()
701
+ left_panel = QFrame()
702
+ left_panel.setObjectName("sidebar")
703
+ left_panel.setMinimumWidth(300)
704
+ left_panel.setMaximumWidth(400)
35
705
  left_layout = QVBoxLayout(left_panel)
706
+ left_layout.setContentsMargins(16, 16, 16, 16)
707
+ left_layout.setSpacing(12)
708
+
709
+ # Database info section
710
+ db_header = QLabel("DATABASE")
711
+ db_header.setObjectName("header_label")
712
+ db_header.setStyleSheet("color: white;")
713
+ left_layout.addWidget(db_header)
36
714
 
37
- # Database info label
38
715
  self.db_info_label = QLabel("No database connected")
716
+ self.db_info_label.setStyleSheet("color: white; background-color: rgba(255, 255, 255, 0.1); padding: 8px; border-radius: 4px;")
39
717
  left_layout.addWidget(self.db_info_label)
40
718
 
41
- tables_label = QLabel("Tables:")
42
- left_layout.addWidget(tables_label)
43
-
44
- self.tables_list = QListWidget()
45
- self.tables_list.itemClicked.connect(self.show_table_preview)
46
- left_layout.addWidget(self.tables_list)
719
+ # Database action buttons
720
+ db_buttons_layout = QHBoxLayout()
721
+ db_buttons_layout.setSpacing(8)
47
722
 
48
- # Buttons for table management
49
- table_buttons_layout = QHBoxLayout()
50
723
  self.open_db_btn = QPushButton('Open Database')
724
+ self.open_db_btn.setIcon(QIcon.fromTheme("document-open"))
51
725
  self.open_db_btn.clicked.connect(self.open_database)
726
+
727
+ self.test_btn = QPushButton('Load Test Data')
728
+ self.test_btn.clicked.connect(self.load_test_data)
729
+
730
+ db_buttons_layout.addWidget(self.open_db_btn)
731
+ db_buttons_layout.addWidget(self.test_btn)
732
+ left_layout.addLayout(db_buttons_layout)
733
+
734
+ # Tables section
735
+ tables_header = QLabel("TABLES")
736
+ tables_header.setObjectName("header_label")
737
+ tables_header.setStyleSheet("color: white; margin-top: 16px;")
738
+ left_layout.addWidget(tables_header)
739
+
740
+ # Table actions
741
+ table_actions_layout = QHBoxLayout()
742
+ table_actions_layout.setSpacing(8)
743
+
52
744
  self.browse_btn = QPushButton('Load Files')
745
+ self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
53
746
  self.browse_btn.clicked.connect(self.browse_files)
54
- self.remove_table_btn = QPushButton('Remove Selected')
747
+
748
+ self.remove_table_btn = QPushButton('Remove')
749
+ self.remove_table_btn.setObjectName("danger_button")
750
+ self.remove_table_btn.setIcon(QIcon.fromTheme("edit-delete"))
55
751
  self.remove_table_btn.clicked.connect(self.remove_selected_table)
56
- self.test_btn = QPushButton('Test')
57
- self.test_btn.clicked.connect(self.load_test_data)
58
752
 
59
- table_buttons_layout.addWidget(self.open_db_btn)
60
- table_buttons_layout.addWidget(self.browse_btn)
61
- table_buttons_layout.addWidget(self.remove_table_btn)
62
- table_buttons_layout.addWidget(self.test_btn)
63
- left_layout.addLayout(table_buttons_layout)
64
-
753
+ table_actions_layout.addWidget(self.browse_btn)
754
+ table_actions_layout.addWidget(self.remove_table_btn)
755
+ left_layout.addLayout(table_actions_layout)
756
+
757
+ # Tables list with custom styling
758
+ self.tables_list = QListWidget()
759
+ self.tables_list.setStyleSheet("""
760
+ QListWidget {
761
+ background-color: rgba(255, 255, 255, 0.1);
762
+ border: none;
763
+ border-radius: 4px;
764
+ color: white;
765
+ }
766
+ QListWidget::item:selected {
767
+ background-color: rgba(255, 255, 255, 0.2);
768
+ }
769
+ QListWidget::item:hover:!selected {
770
+ background-color: rgba(255, 255, 255, 0.1);
771
+ }
772
+ """)
773
+ self.tables_list.itemClicked.connect(self.show_table_preview)
774
+ self.tables_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
775
+ self.tables_list.customContextMenuRequested.connect(self.show_tables_context_menu)
776
+ left_layout.addWidget(self.tables_list)
777
+
778
+ # Add spacer at the bottom
779
+ left_layout.addStretch()
780
+
65
781
  # Right panel for query and results
66
- right_panel = QWidget()
782
+ right_panel = QFrame()
783
+ right_panel.setObjectName("content_panel")
67
784
  right_layout = QVBoxLayout(right_panel)
785
+ right_layout.setContentsMargins(16, 16, 16, 16)
786
+ right_layout.setSpacing(16)
787
+
788
+ # Query section header
789
+ query_header = QLabel("SQL QUERY")
790
+ query_header.setObjectName("header_label")
791
+ right_layout.addWidget(query_header)
68
792
 
69
793
  # Create splitter for query and results
70
794
  splitter = QSplitter(Qt.Orientation.Vertical)
795
+ splitter.setHandleWidth(8)
796
+ splitter.setChildrenCollapsible(False)
71
797
 
72
798
  # Top part - Query section
73
- query_widget = QWidget()
799
+ query_widget = QFrame()
800
+ query_widget.setObjectName("content_panel")
74
801
  query_layout = QVBoxLayout(query_widget)
802
+ query_layout.setContentsMargins(16, 16, 16, 16)
803
+ query_layout.setSpacing(12)
804
+
805
+ # Query input
806
+ self.query_edit = SQLEditor()
807
+ # Apply syntax highlighting to the query editor
808
+ self.sql_highlighter = SQLSyntaxHighlighter(self.query_edit.document())
809
+ query_layout.addWidget(self.query_edit)
75
810
 
76
811
  # Button row
77
812
  button_layout = QHBoxLayout()
78
- self.execute_btn = QPushButton('Execute (Ctrl+Enter)')
813
+ button_layout.setSpacing(8)
814
+
815
+ self.execute_btn = QPushButton('Execute Query')
816
+ self.execute_btn.setObjectName("primary_button")
817
+ self.execute_btn.setIcon(QIcon.fromTheme("media-playback-start"))
79
818
  self.execute_btn.clicked.connect(self.execute_query)
819
+ self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
820
+
80
821
  self.clear_btn = QPushButton('Clear')
822
+ self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
81
823
  self.clear_btn.clicked.connect(self.clear_query)
82
824
 
83
- # Add export buttons
84
- self.export_excel_btn = QPushButton('Export to Excel')
85
- self.export_excel_btn.clicked.connect(self.export_to_excel)
86
- self.export_parquet_btn = QPushButton('Export to Parquet')
87
- self.export_parquet_btn.clicked.connect(self.export_to_parquet)
88
-
89
825
  button_layout.addWidget(self.execute_btn)
90
826
  button_layout.addWidget(self.clear_btn)
91
- button_layout.addWidget(self.export_excel_btn)
92
- button_layout.addWidget(self.export_parquet_btn)
93
827
  button_layout.addStretch()
94
828
 
95
829
  query_layout.addLayout(button_layout)
96
-
97
- # Query input
98
- self.query_edit = QTextEdit()
99
- self.query_edit.setPlaceholderText("Enter your SQL query here...")
100
- query_layout.addWidget(self.query_edit)
101
-
830
+
102
831
  # Bottom part - Results section
103
- results_widget = QWidget()
832
+ results_widget = QFrame()
833
+ results_widget.setObjectName("content_panel")
104
834
  results_layout = QVBoxLayout(results_widget)
835
+ results_layout.setContentsMargins(16, 16, 16, 16)
836
+ results_layout.setSpacing(12)
837
+
838
+ # Results header with row count and export options
839
+ results_header_layout = QHBoxLayout()
840
+
841
+ results_title = QLabel("RESULTS")
842
+ results_title.setObjectName("header_label")
105
843
 
106
- # Results header with row count
107
- results_header = QWidget()
108
- results_header_layout = QHBoxLayout(results_header)
109
- self.results_label = QLabel("Results:")
110
844
  self.row_count_label = QLabel("")
111
- results_header_layout.addWidget(self.results_label)
845
+ self.row_count_label.setStyleSheet(f"color: {self.colors['text_light']}; font-style: italic;")
846
+
847
+ results_header_layout.addWidget(results_title)
112
848
  results_header_layout.addWidget(self.row_count_label)
113
849
  results_header_layout.addStretch()
114
- results_layout.addWidget(results_header)
115
850
 
116
- # Table widget for results
851
+ # Export buttons
852
+ export_layout = QHBoxLayout()
853
+ export_layout.setSpacing(8)
854
+
855
+ self.export_excel_btn = QPushButton('Export to Excel')
856
+ self.export_excel_btn.setIcon(QIcon.fromTheme("x-office-spreadsheet"))
857
+ self.export_excel_btn.clicked.connect(self.export_to_excel)
858
+
859
+ self.export_parquet_btn = QPushButton('Export to Parquet')
860
+ self.export_parquet_btn.setIcon(QIcon.fromTheme("document-save"))
861
+ self.export_parquet_btn.clicked.connect(self.export_to_parquet)
862
+
863
+ export_layout.addWidget(self.export_excel_btn)
864
+ export_layout.addWidget(self.export_parquet_btn)
865
+
866
+ results_header_layout.addLayout(export_layout)
867
+ results_layout.addLayout(results_header_layout)
868
+
869
+ # Table widget for results with modern styling
117
870
  self.results_table = QTableWidget()
118
871
  self.results_table.setSortingEnabled(True)
119
872
  self.results_table.setAlternatingRowColors(True)
120
- self.results_table.horizontalHeader().setStretchLastSection(True)
121
- self.results_table.horizontalHeader().setSectionsMovable(True)
873
+
874
+ # Set custom header for filtering
875
+ header = FilterHeader(self.results_table)
876
+ header.set_main_window(self) # Set reference to main window
877
+ self.results_table.setHorizontalHeader(header)
878
+ header.setStretchLastSection(True)
879
+ header.setSectionsMovable(True)
880
+
122
881
  self.results_table.verticalHeader().setVisible(False)
882
+ self.results_table.setShowGrid(True)
883
+ self.results_table.setGridStyle(Qt.PenStyle.SolidLine)
884
+ self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
885
+
123
886
  results_layout.addWidget(self.results_table)
124
887
 
125
888
  # Add widgets to splitter
@@ -136,67 +899,152 @@ class SQLShell(QMainWindow):
136
899
  main_layout.addWidget(right_panel, 4)
137
900
 
138
901
  # Status bar
139
- self.statusBar().showMessage('Ready')
140
-
141
- def format_value(self, value):
142
- """Format values for display"""
143
- if pd.isna(value):
144
- return 'NULL'
145
- elif isinstance(value, (int, np.integer)):
146
- return f"{value:,}"
147
- elif isinstance(value, (float, np.floating)):
148
- return f"{value:,.2f}"
149
- elif isinstance(value, (datetime, pd.Timestamp)):
150
- return value.strftime('%Y-%m-%d %H:%M:%S')
151
- return str(value)
902
+ self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment')
903
+
904
+ # Show keyboard shortcuts in a tooltip for the query editor
905
+ self.query_edit.setToolTip(
906
+ "Keyboard Shortcuts:\n"
907
+ "Ctrl+Enter: Execute Query\n"
908
+ "Ctrl+K: Toggle Comment\n"
909
+ "Tab: Insert 4 spaces\n"
910
+ "Ctrl+Space: Show autocomplete"
911
+ )
152
912
 
153
913
  def populate_table(self, df):
154
- """Populate the table widget with DataFrame content"""
155
- if len(df) == 0:
914
+ """Populate the results table with DataFrame data using memory-efficient chunking"""
915
+ try:
916
+ # Store the current DataFrame for filtering
917
+ self.current_df = df.copy()
918
+
919
+ # Remember which columns had bar charts
920
+ header = self.results_table.horizontalHeader()
921
+ if isinstance(header, FilterHeader):
922
+ columns_with_bars = header.columns_with_bars.copy()
923
+ else:
924
+ columns_with_bars = set()
925
+
926
+ # Clear existing data
927
+ self.results_table.clearContents()
156
928
  self.results_table.setRowCount(0)
157
929
  self.results_table.setColumnCount(0)
158
- self.row_count_label.setText("No results")
159
- return
160
-
161
- # Set dimensions
162
- self.results_table.setRowCount(len(df))
163
- self.results_table.setColumnCount(len(df.columns))
164
-
165
- # Set headers
166
- self.results_table.setHorizontalHeaderLabels(df.columns)
167
-
168
- # Populate data
169
- for i, (_, row) in enumerate(df.iterrows()):
170
- for j, value in enumerate(row):
171
- formatted_value = self.format_value(value)
172
- item = QTableWidgetItem(formatted_value)
930
+
931
+ if df.empty:
932
+ self.statusBar().showMessage("Query returned no results")
933
+ return
173
934
 
174
- # Set alignment based on data type
175
- if isinstance(value, (int, float, np.integer, np.floating)):
176
- item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
177
- else:
178
- item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
935
+ # Set up the table dimensions
936
+ row_count = len(df)
937
+ col_count = len(df.columns)
938
+ self.results_table.setColumnCount(col_count)
939
+
940
+ # Set column headers
941
+ headers = [str(col) for col in df.columns]
942
+ self.results_table.setHorizontalHeaderLabels(headers)
943
+
944
+ # Calculate chunk size (adjust based on available memory)
945
+ CHUNK_SIZE = 1000
946
+
947
+ # Process data in chunks to avoid memory issues with large datasets
948
+ for chunk_start in range(0, row_count, CHUNK_SIZE):
949
+ chunk_end = min(chunk_start + CHUNK_SIZE, row_count)
950
+ chunk = df.iloc[chunk_start:chunk_end]
179
951
 
180
- # Make cells read-only
181
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
952
+ # Add rows for this chunk
953
+ self.results_table.setRowCount(chunk_end)
182
954
 
183
- self.results_table.setItem(i, j, item)
184
-
185
- # Auto-adjust column widths while ensuring minimum and maximum sizes
186
- self.results_table.resizeColumnsToContents()
187
- for i in range(len(df.columns)):
188
- width = self.results_table.columnWidth(i)
189
- self.results_table.setColumnWidth(i, min(max(width, 50), 300))
190
-
191
- # Update row count
192
- row_text = "row" if len(df) == 1 else "rows"
193
- self.row_count_label.setText(f"{len(df):,} {row_text}")
955
+ for row_idx, (_, row_data) in enumerate(chunk.iterrows(), start=chunk_start):
956
+ for col_idx, value in enumerate(row_data):
957
+ formatted_value = self.format_value(value)
958
+ item = QTableWidgetItem(formatted_value)
959
+ self.results_table.setItem(row_idx, col_idx, item)
960
+
961
+ # Process events to keep UI responsive
962
+ QApplication.processEvents()
963
+
964
+ # Optimize column widths
965
+ self.results_table.resizeColumnsToContents()
966
+
967
+ # Restore bar charts for columns that previously had them
968
+ header = self.results_table.horizontalHeader()
969
+ if isinstance(header, FilterHeader):
970
+ for col_idx in columns_with_bars:
971
+ if col_idx < col_count: # Only if column still exists
972
+ header.toggle_bar_chart(col_idx)
973
+
974
+ # Update status
975
+ memory_usage = df.memory_usage(deep=True).sum() / (1024 * 1024) # Convert to MB
976
+ self.statusBar().showMessage(
977
+ f"Loaded {row_count:,} rows, {col_count} columns. Memory usage: {memory_usage:.1f} MB"
978
+ )
979
+
980
+ except Exception as e:
981
+ QMessageBox.critical(self, "Error",
982
+ f"Failed to populate results table:\n\n{str(e)}")
983
+ self.statusBar().showMessage("Failed to display results")
984
+
985
+ def apply_filters(self):
986
+ """Apply filters to the table based on filter inputs"""
987
+ if self.current_df is None or not self.filter_widgets:
988
+ return
989
+
990
+ try:
991
+ # Start with the original DataFrame
992
+ filtered_df = self.current_df.copy()
993
+
994
+ # Apply each non-empty filter
995
+ for col_idx, filter_widget in enumerate(self.filter_widgets):
996
+ filter_text = filter_widget.text().strip()
997
+ if filter_text:
998
+ col_name = self.current_df.columns[col_idx]
999
+ # Convert column to string for filtering
1000
+ filtered_df[col_name] = filtered_df[col_name].astype(str)
1001
+ filtered_df = filtered_df[filtered_df[col_name].str.contains(filter_text, case=False, na=False)]
1002
+
1003
+ # Update table with filtered data
1004
+ row_count = len(filtered_df)
1005
+ for row_idx in range(row_count):
1006
+ for col_idx, value in enumerate(filtered_df.iloc[row_idx]):
1007
+ formatted_value = self.format_value(value)
1008
+ item = QTableWidgetItem(formatted_value)
1009
+ self.results_table.setItem(row_idx, col_idx, item)
1010
+
1011
+ # Hide rows that don't match filter
1012
+ for row_idx in range(row_count + 1, self.results_table.rowCount()):
1013
+ self.results_table.hideRow(row_idx)
1014
+
1015
+ # Show all filtered rows
1016
+ for row_idx in range(1, row_count + 1):
1017
+ self.results_table.showRow(row_idx)
1018
+
1019
+ # Update status
1020
+ self.statusBar().showMessage(f"Showing {row_count:,} rows after filtering")
1021
+
1022
+ except Exception as e:
1023
+ self.statusBar().showMessage(f"Error applying filters: {str(e)}")
1024
+
1025
+ def format_value(self, value):
1026
+ """Format cell values efficiently"""
1027
+ if pd.isna(value):
1028
+ return "NULL"
1029
+ elif isinstance(value, (float, np.floating)):
1030
+ if value.is_integer():
1031
+ return str(int(value))
1032
+ return f"{value:.6g}" # Use general format with up to 6 significant digits
1033
+ elif isinstance(value, (pd.Timestamp, datetime)):
1034
+ return value.strftime("%Y-%m-%d %H:%M:%S")
1035
+ elif isinstance(value, (np.integer, int)):
1036
+ return str(value)
1037
+ elif isinstance(value, bool):
1038
+ return str(value)
1039
+ elif isinstance(value, (bytes, bytearray)):
1040
+ return value.hex()
1041
+ return str(value)
194
1042
 
195
1043
  def browse_files(self):
196
1044
  if not self.conn:
197
1045
  # Create a default in-memory DuckDB connection if none exists
198
1046
  self.conn = duckdb.connect(':memory:')
199
- self.current_db_type = 'duckdb'
1047
+ self.current_connection_type = 'duckdb'
200
1048
  self.db_info_label.setText("Connected to: in-memory DuckDB")
201
1049
 
202
1050
  file_names, _ = QFileDialog.getOpenFileNames(
@@ -229,7 +1077,7 @@ class SQLShell(QMainWindow):
229
1077
  counter += 1
230
1078
 
231
1079
  # Handle table creation based on database type
232
- if self.current_db_type == 'sqlite':
1080
+ if self.current_connection_type == 'sqlite':
233
1081
  # For SQLite, create a table from the DataFrame
234
1082
  df.to_sql(table_name, self.conn, index=False, if_exists='replace')
235
1083
  else:
@@ -238,6 +1086,9 @@ class SQLShell(QMainWindow):
238
1086
 
239
1087
  self.loaded_tables[table_name] = file_name
240
1088
 
1089
+ # Store column names
1090
+ self.table_columns[table_name] = df.columns.tolist()
1091
+
241
1092
  # Update UI
242
1093
  self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
243
1094
  self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
@@ -245,14 +1096,22 @@ class SQLShell(QMainWindow):
245
1096
  # Show preview of loaded data
246
1097
  preview_df = df.head()
247
1098
  self.populate_table(preview_df)
248
- self.results_label.setText(f"Preview of {table_name}:")
1099
+
1100
+ # Update results title to show preview
1101
+ results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
1102
+ if results_title and results_title.text() == "RESULTS":
1103
+ results_title.setText(f"PREVIEW: {table_name}")
1104
+
1105
+ # Update completer with new table and column names
1106
+ self.update_completer()
249
1107
 
250
1108
  except Exception as e:
251
- self.statusBar().showMessage(f'Error loading file: {str(e)}')
1109
+ error_msg = f'Error loading file {os.path.basename(file_name)}: {str(e)}'
1110
+ self.statusBar().showMessage(error_msg)
1111
+ QMessageBox.critical(self, "Error", error_msg)
252
1112
  self.results_table.setRowCount(0)
253
1113
  self.results_table.setColumnCount(0)
254
1114
  self.row_count_label.setText("")
255
- self.results_label.setText(f"Error loading file: {str(e)}")
256
1115
 
257
1116
  def sanitize_table_name(self, name):
258
1117
  # Replace invalid characters with underscores
@@ -272,57 +1131,54 @@ class SQLShell(QMainWindow):
272
1131
  self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
273
1132
  # Remove from our tracking
274
1133
  del self.loaded_tables[table_name]
1134
+ if table_name in self.table_columns:
1135
+ del self.table_columns[table_name]
275
1136
  # Remove from list widget
276
1137
  self.tables_list.takeItem(self.tables_list.row(current_item))
277
1138
  self.statusBar().showMessage(f'Removed table "{table_name}"')
278
1139
  self.results_table.setRowCount(0)
279
1140
  self.results_table.setColumnCount(0)
280
1141
  self.row_count_label.setText("")
281
- self.results_label.setText(f"Removed table: {table_name}")
1142
+
1143
+ # Update completer
1144
+ self.update_completer()
282
1145
 
283
1146
  def open_database(self):
284
- """Open a database file (DuckDB or SQLite)"""
285
- file_name, _ = QFileDialog.getOpenFileName(
286
- self,
287
- "Open Database File",
288
- "",
289
- "Database Files (*.db);;All Files (*)"
290
- )
291
-
292
- if not file_name:
293
- return
294
-
1147
+ """Open a database connection with proper error handling and resource management"""
295
1148
  try:
296
- # Try to detect database type
297
- is_sqlite = self.is_sqlite_db(file_name)
298
-
299
- # Close existing connection if any
300
1149
  if self.conn:
301
- self.conn.close()
302
-
303
- # Connect to the database
304
- if is_sqlite:
305
- self.conn = sqlite3.connect(file_name)
306
- self.current_db_type = 'sqlite'
307
- else:
308
- self.conn = duckdb.connect(file_name)
309
- self.current_db_type = 'duckdb'
310
-
311
- # Clear existing tables
312
- self.loaded_tables.clear()
313
- self.tables_list.clear()
1150
+ # Close existing connection before opening new one
1151
+ if self.current_connection_type == "duckdb":
1152
+ self.conn.close()
1153
+ else: # sqlite
1154
+ self.conn.close()
1155
+ self.conn = None
1156
+ self.current_connection_type = None
314
1157
 
315
- # Load tables
316
- self.load_database_tables()
1158
+ filename, _ = QFileDialog.getOpenFileName(
1159
+ self,
1160
+ "Open Database",
1161
+ "",
1162
+ "All Database Files (*.db *.sqlite *.sqlite3);;All Files (*)"
1163
+ )
317
1164
 
318
- # Update UI
319
- db_type = "SQLite" if is_sqlite else "DuckDB"
320
- self.db_info_label.setText(f"Connected to: {os.path.basename(file_name)} ({db_type})")
321
- self.statusBar().showMessage(f'Successfully opened {db_type} database: {file_name}')
322
-
323
- except Exception as e:
324
- QMessageBox.critical(self, "Error", f"Failed to open database: {str(e)}")
325
- self.statusBar().showMessage('Error opening database')
1165
+ if filename:
1166
+ if self.is_sqlite_db(filename):
1167
+ self.conn = sqlite3.connect(filename)
1168
+ self.current_connection_type = "sqlite"
1169
+ else:
1170
+ self.conn = duckdb.connect(filename)
1171
+ self.current_connection_type = "duckdb"
1172
+
1173
+ self.load_database_tables()
1174
+ self.statusBar().showMessage(f"Connected to database: {filename}")
1175
+
1176
+ except (sqlite3.Error, duckdb.Error) as e:
1177
+ QMessageBox.critical(self, "Database Connection Error",
1178
+ f"Failed to open database:\n\n{str(e)}")
1179
+ self.statusBar().showMessage("Failed to open database")
1180
+ self.conn = None
1181
+ self.current_connection_type = None
326
1182
 
327
1183
  def is_sqlite_db(self, filename):
328
1184
  """Check if the file is a SQLite database"""
@@ -336,77 +1192,151 @@ class SQLShell(QMainWindow):
336
1192
  def load_database_tables(self):
337
1193
  """Load all tables from the current database"""
338
1194
  try:
339
- if self.current_db_type == 'sqlite':
1195
+ if self.current_connection_type == 'sqlite':
340
1196
  query = "SELECT name FROM sqlite_master WHERE type='table'"
341
1197
  cursor = self.conn.cursor()
342
1198
  tables = cursor.execute(query).fetchall()
343
1199
  for (table_name,) in tables:
344
1200
  self.loaded_tables[table_name] = 'database'
345
1201
  self.tables_list.addItem(f"{table_name} (database)")
1202
+
1203
+ # Get column names for each table
1204
+ try:
1205
+ column_query = f"PRAGMA table_info({table_name})"
1206
+ columns = cursor.execute(column_query).fetchall()
1207
+ self.table_columns[table_name] = [col[1] for col in columns] # Column name is at index 1
1208
+ except Exception:
1209
+ self.table_columns[table_name] = []
346
1210
  else: # duckdb
347
1211
  query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
348
1212
  result = self.conn.execute(query).fetchdf()
349
1213
  for table_name in result['table_name']:
350
1214
  self.loaded_tables[table_name] = 'database'
351
1215
  self.tables_list.addItem(f"{table_name} (database)")
1216
+
1217
+ # Get column names for each table
1218
+ try:
1219
+ column_query = f"SELECT column_name FROM information_schema.columns WHERE table_name='{table_name}' AND table_schema='main'"
1220
+ columns = self.conn.execute(column_query).fetchdf()
1221
+ self.table_columns[table_name] = columns['column_name'].tolist()
1222
+ except Exception:
1223
+ self.table_columns[table_name] = []
1224
+
1225
+ # Update the completer with table and column names
1226
+ self.update_completer()
352
1227
  except Exception as e:
353
1228
  self.statusBar().showMessage(f'Error loading tables: {str(e)}')
354
1229
 
355
- def execute_query(self):
356
- query = self.query_edit.toPlainText().strip()
357
- if not query:
358
- return
1230
+ def update_completer(self):
1231
+ """Update the completer with table and column names"""
1232
+ # Collect all table names and column names
1233
+ completion_words = list(self.loaded_tables.keys())
359
1234
 
1235
+ # Add column names with table name prefix (for joins)
1236
+ for table, columns in self.table_columns.items():
1237
+ completion_words.extend(columns)
1238
+ completion_words.extend([f"{table}.{col}" for col in columns])
1239
+
1240
+ # Update the completer in the query editor
1241
+ self.query_edit.update_completer_model(completion_words)
1242
+
1243
+ def execute_query(self):
360
1244
  try:
361
- if self.current_db_type == 'sqlite':
362
- # Execute SQLite query and convert to DataFrame
363
- result = pd.read_sql_query(query, self.conn)
364
- else:
365
- # Execute DuckDB query
366
- result = self.conn.execute(query).fetchdf()
1245
+ query = self.query_edit.toPlainText().strip()
1246
+ if not query:
1247
+ QMessageBox.warning(self, "Empty Query", "Please enter a SQL query to execute.")
1248
+ return
1249
+
1250
+ start_time = datetime.now()
1251
+
1252
+ try:
1253
+ if self.current_connection_type == "duckdb":
1254
+ result = self.conn.execute(query).fetchdf()
1255
+ else: # sqlite
1256
+ result = pd.read_sql_query(query, self.conn)
1257
+
1258
+ execution_time = (datetime.now() - start_time).total_seconds()
1259
+ self.populate_table(result)
1260
+ self.statusBar().showMessage(f"Query executed successfully. Time: {execution_time:.2f}s. Rows: {len(result)}")
1261
+
1262
+ except (duckdb.Error, sqlite3.Error) as e:
1263
+ error_msg = str(e)
1264
+ if "syntax error" in error_msg.lower():
1265
+ QMessageBox.critical(self, "SQL Syntax Error",
1266
+ f"There is a syntax error in your query:\n\n{error_msg}")
1267
+ elif "no such table" in error_msg.lower():
1268
+ QMessageBox.critical(self, "Table Not Found",
1269
+ f"The referenced table does not exist:\n\n{error_msg}")
1270
+ elif "no such column" in error_msg.lower():
1271
+ QMessageBox.critical(self, "Column Not Found",
1272
+ f"The referenced column does not exist:\n\n{error_msg}")
1273
+ else:
1274
+ QMessageBox.critical(self, "Database Error",
1275
+ f"An error occurred while executing the query:\n\n{error_msg}")
1276
+ self.statusBar().showMessage("Query execution failed")
367
1277
 
368
- self.populate_table(result)
369
- self.results_label.setText("Query Results:")
370
- self.statusBar().showMessage('Query executed successfully')
371
1278
  except Exception as e:
372
- self.results_table.setRowCount(0)
373
- self.results_table.setColumnCount(0)
374
- self.row_count_label.setText("")
375
- self.results_label.setText(f"Error executing query: {str(e)}")
376
- self.statusBar().showMessage('Error executing query')
1279
+ QMessageBox.critical(self, "Unexpected Error",
1280
+ f"An unexpected error occurred:\n\n{str(e)}")
1281
+ self.statusBar().showMessage("Query execution failed")
377
1282
 
378
1283
  def clear_query(self):
1284
+ """Clear the query editor with animation"""
1285
+ # Save current text for animation
1286
+ current_text = self.query_edit.toPlainText()
1287
+ if not current_text:
1288
+ return
1289
+
1290
+ # Clear the editor
379
1291
  self.query_edit.clear()
1292
+
1293
+ # Show success message
1294
+ self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
380
1295
 
381
1296
  def show_table_preview(self, item):
382
1297
  """Show a preview of the selected table"""
383
1298
  if item:
384
1299
  table_name = item.text().split(' (')[0]
385
1300
  try:
386
- if self.current_db_type == 'sqlite':
1301
+ if self.current_connection_type == 'sqlite':
387
1302
  preview_df = pd.read_sql_query(f'SELECT * FROM "{table_name}" LIMIT 5', self.conn)
388
1303
  else:
389
1304
  preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
390
1305
 
391
1306
  self.populate_table(preview_df)
392
- self.results_label.setText(f"Preview of {table_name}:")
393
1307
  self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
1308
+
1309
+ # Update the results title to show which table is being previewed
1310
+ results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
1311
+ if results_title and results_title.text() == "RESULTS":
1312
+ results_title.setText(f"PREVIEW: {table_name}")
1313
+
394
1314
  except Exception as e:
395
1315
  self.results_table.setRowCount(0)
396
1316
  self.results_table.setColumnCount(0)
397
1317
  self.row_count_label.setText("")
398
- self.results_label.setText(f"Error showing preview: {str(e)}")
399
1318
  self.statusBar().showMessage('Error showing table preview')
400
-
401
- def keyPressEvent(self, event):
402
- if event.key() == Qt.Key.Key_Return and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
403
- self.execute_query()
404
- else:
405
- super().keyPressEvent(event)
1319
+
1320
+ # Show error message with modern styling
1321
+ QMessageBox.critical(
1322
+ self,
1323
+ "Error",
1324
+ f"Error showing preview: {str(e)}",
1325
+ QMessageBox.StandardButton.Ok
1326
+ )
406
1327
 
407
1328
  def load_test_data(self):
408
1329
  """Generate and load test data"""
409
1330
  try:
1331
+ # Ensure we have a DuckDB connection
1332
+ if not self.conn or self.current_connection_type != 'duckdb':
1333
+ self.conn = duckdb.connect(':memory:')
1334
+ self.current_connection_type = 'duckdb'
1335
+ self.db_info_label.setText("Connected to: in-memory DuckDB")
1336
+
1337
+ # Show loading indicator
1338
+ self.statusBar().showMessage('Generating test data...')
1339
+
410
1340
  # Create test data directory if it doesn't exist
411
1341
  os.makedirs('test_data', exist_ok=True)
412
1342
 
@@ -430,18 +1360,41 @@ class SQLShell(QMainWindow):
430
1360
  self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
431
1361
  self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
432
1362
 
1363
+ # Store column names
1364
+ self.table_columns['sample_sales_data'] = sales_df.columns.tolist()
1365
+ self.table_columns['product_catalog'] = product_df.columns.tolist()
1366
+ self.table_columns['customer_data'] = customer_df.columns.tolist()
1367
+
433
1368
  # Update UI
434
1369
  self.tables_list.clear()
435
1370
  for table_name, file_path in self.loaded_tables.items():
436
1371
  self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
437
1372
 
438
1373
  # Set the sample query
439
- self.query_edit.setText("select * from sample_sales_data cd inner join product_catalog pc on pc.productid = cd.productid limit 3")
1374
+ sample_query = """
1375
+ SELECT
1376
+ DISTINCT
1377
+ c.customername
1378
+ FROM
1379
+ sample_sales_data s
1380
+ INNER JOIN customer_data c ON c.customerid = s.customerid
1381
+ INNER JOIN product_catalog p ON p.productid = s.productid
1382
+ LIMIT 10
1383
+ """
1384
+ self.query_edit.setPlainText(sample_query.strip())
1385
+
1386
+ # Update completer
1387
+ self.update_completer()
440
1388
 
1389
+ # Show success message
441
1390
  self.statusBar().showMessage('Test data loaded successfully')
442
1391
 
1392
+ # Show a preview of the sales data
1393
+ self.show_table_preview(self.tables_list.item(0))
1394
+
443
1395
  except Exception as e:
444
1396
  self.statusBar().showMessage(f'Error loading test data: {str(e)}')
1397
+ QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
445
1398
 
446
1399
  def export_to_excel(self):
447
1400
  if self.results_table.rowCount() == 0:
@@ -453,12 +1406,48 @@ class SQLShell(QMainWindow):
453
1406
  return
454
1407
 
455
1408
  try:
1409
+ # Show loading indicator
1410
+ self.statusBar().showMessage('Exporting data to Excel...')
1411
+
456
1412
  # Convert table data to DataFrame
457
1413
  df = self.get_table_data_as_dataframe()
458
1414
  df.to_excel(file_name, index=False)
459
- self.statusBar().showMessage(f'Data exported to {file_name}')
1415
+
1416
+ # Generate table name from file name
1417
+ base_name = os.path.splitext(os.path.basename(file_name))[0]
1418
+ table_name = self.sanitize_table_name(base_name)
1419
+
1420
+ # Ensure unique table name
1421
+ original_name = table_name
1422
+ counter = 1
1423
+ while table_name in self.loaded_tables:
1424
+ table_name = f"{original_name}_{counter}"
1425
+ counter += 1
1426
+
1427
+ # Register the table in DuckDB
1428
+ self.conn.register(table_name, df)
1429
+
1430
+ # Update tracking
1431
+ self.loaded_tables[table_name] = file_name
1432
+ self.table_columns[table_name] = df.columns.tolist()
1433
+
1434
+ # Update UI
1435
+ self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
1436
+ self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
1437
+
1438
+ # Update completer with new table and column names
1439
+ self.update_completer()
1440
+
1441
+ # Show success message
1442
+ QMessageBox.information(
1443
+ self,
1444
+ "Export Successful",
1445
+ f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
1446
+ QMessageBox.StandardButton.Ok
1447
+ )
460
1448
  except Exception as e:
461
1449
  QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
1450
+ self.statusBar().showMessage('Error exporting data')
462
1451
 
463
1452
  def export_to_parquet(self):
464
1453
  if self.results_table.rowCount() == 0:
@@ -470,12 +1459,48 @@ class SQLShell(QMainWindow):
470
1459
  return
471
1460
 
472
1461
  try:
1462
+ # Show loading indicator
1463
+ self.statusBar().showMessage('Exporting data to Parquet...')
1464
+
473
1465
  # Convert table data to DataFrame
474
1466
  df = self.get_table_data_as_dataframe()
475
1467
  df.to_parquet(file_name, index=False)
476
- self.statusBar().showMessage(f'Data exported to {file_name}')
1468
+
1469
+ # Generate table name from file name
1470
+ base_name = os.path.splitext(os.path.basename(file_name))[0]
1471
+ table_name = self.sanitize_table_name(base_name)
1472
+
1473
+ # Ensure unique table name
1474
+ original_name = table_name
1475
+ counter = 1
1476
+ while table_name in self.loaded_tables:
1477
+ table_name = f"{original_name}_{counter}"
1478
+ counter += 1
1479
+
1480
+ # Register the table in DuckDB
1481
+ self.conn.register(table_name, df)
1482
+
1483
+ # Update tracking
1484
+ self.loaded_tables[table_name] = file_name
1485
+ self.table_columns[table_name] = df.columns.tolist()
1486
+
1487
+ # Update UI
1488
+ self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
1489
+ self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
1490
+
1491
+ # Update completer with new table and column names
1492
+ self.update_completer()
1493
+
1494
+ # Show success message
1495
+ QMessageBox.information(
1496
+ self,
1497
+ "Export Successful",
1498
+ f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
1499
+ QMessageBox.StandardButton.Ok
1500
+ )
477
1501
  except Exception as e:
478
1502
  QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
1503
+ self.statusBar().showMessage('Error exporting data')
479
1504
 
480
1505
  def get_table_data_as_dataframe(self):
481
1506
  """Helper function to convert table widget data to a DataFrame"""
@@ -489,14 +1514,319 @@ class SQLShell(QMainWindow):
489
1514
  data.append(row_data)
490
1515
  return pd.DataFrame(data, columns=headers)
491
1516
 
1517
+ def keyPressEvent(self, event):
1518
+ """Handle global keyboard shortcuts"""
1519
+ # Execute query with Ctrl+Enter or Cmd+Enter (for Mac)
1520
+ if event.key() == Qt.Key.Key_Return and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
1521
+ self.execute_btn.click() # Simply click the button instead of animating
1522
+ return
1523
+
1524
+ # Clear query with Ctrl+L
1525
+ if event.key() == Qt.Key.Key_L and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
1526
+ self.clear_btn.click() # Simply click the button instead of animating
1527
+ return
1528
+
1529
+ super().keyPressEvent(event)
1530
+
1531
+ def closeEvent(self, event):
1532
+ """Ensure proper cleanup of database connections when closing the application"""
1533
+ try:
1534
+ # Check for unsaved changes
1535
+ if self.has_unsaved_changes():
1536
+ reply = QMessageBox.question(self, 'Save Changes',
1537
+ 'Do you want to save your changes before closing?',
1538
+ QMessageBox.StandardButton.Save |
1539
+ QMessageBox.StandardButton.Discard |
1540
+ QMessageBox.StandardButton.Cancel)
1541
+
1542
+ if reply == QMessageBox.StandardButton.Save:
1543
+ self.save_project()
1544
+ elif reply == QMessageBox.StandardButton.Cancel:
1545
+ event.ignore()
1546
+ return
1547
+
1548
+ # Close database connections
1549
+ if self.conn:
1550
+ if self.current_connection_type == "duckdb":
1551
+ self.conn.close()
1552
+ else: # sqlite
1553
+ self.conn.close()
1554
+ event.accept()
1555
+ except Exception as e:
1556
+ QMessageBox.warning(self, "Cleanup Warning",
1557
+ f"Warning: Could not properly close database connection:\n{str(e)}")
1558
+ event.accept()
1559
+
1560
+ def has_unsaved_changes(self):
1561
+ """Check if there are unsaved changes in the project"""
1562
+ if not self.current_project_file:
1563
+ return bool(self.loaded_tables or self.query_edit.toPlainText().strip())
1564
+
1565
+ try:
1566
+ # Load the last saved state
1567
+ with open(self.current_project_file, 'r') as f:
1568
+ saved_data = json.load(f)
1569
+
1570
+ # Compare current state with saved state
1571
+ current_data = {
1572
+ 'tables': {
1573
+ name: {
1574
+ 'file_path': path,
1575
+ 'columns': self.table_columns.get(name, [])
1576
+ }
1577
+ for name, path in self.loaded_tables.items()
1578
+ },
1579
+ 'query': self.query_edit.toPlainText(),
1580
+ 'connection_type': self.current_connection_type
1581
+ }
1582
+
1583
+ return current_data != saved_data
1584
+
1585
+ except Exception:
1586
+ # If there's any error reading the saved file, assume there are unsaved changes
1587
+ return True
1588
+
1589
+ def show_tables_context_menu(self, position):
1590
+ """Show context menu for tables list"""
1591
+ item = self.tables_list.itemAt(position)
1592
+ if not item:
1593
+ return
1594
+
1595
+ # Get table name without the file info in parentheses
1596
+ table_name = item.text().split(' (')[0]
1597
+
1598
+ # Create context menu
1599
+ context_menu = QMenu(self)
1600
+ context_menu.setStyleSheet("""
1601
+ QMenu {
1602
+ background-color: white;
1603
+ border: 1px solid #BDC3C7;
1604
+ padding: 5px;
1605
+ }
1606
+ QMenu::item {
1607
+ padding: 5px 20px;
1608
+ }
1609
+ QMenu::item:selected {
1610
+ background-color: #3498DB;
1611
+ color: white;
1612
+ }
1613
+ """)
1614
+
1615
+ # Add menu actions
1616
+ select_from_action = context_menu.addAction("Select from")
1617
+ add_to_editor_action = context_menu.addAction("Just add to editor")
1618
+
1619
+ # Show menu and get selected action
1620
+ action = context_menu.exec(self.tables_list.mapToGlobal(position))
1621
+
1622
+ if action == select_from_action:
1623
+ # Insert "SELECT * FROM table_name" at cursor position
1624
+ cursor = self.query_edit.textCursor()
1625
+ cursor.insertText(f"SELECT * FROM {table_name}")
1626
+ self.query_edit.setFocus()
1627
+ elif action == add_to_editor_action:
1628
+ # Just insert the table name at cursor position
1629
+ cursor = self.query_edit.textCursor()
1630
+ cursor.insertText(table_name)
1631
+ self.query_edit.setFocus()
1632
+
1633
+ def new_project(self):
1634
+ """Create a new project by clearing current state"""
1635
+ if self.conn:
1636
+ reply = QMessageBox.question(self, 'New Project',
1637
+ 'Are you sure you want to start a new project? All unsaved changes will be lost.',
1638
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
1639
+ if reply == QMessageBox.StandardButton.Yes:
1640
+ # Close existing connection
1641
+ if self.current_connection_type == "duckdb":
1642
+ self.conn.close()
1643
+ else: # sqlite
1644
+ self.conn.close()
1645
+
1646
+ # Reset state
1647
+ self.conn = None
1648
+ self.current_connection_type = None
1649
+ self.loaded_tables.clear()
1650
+ self.table_columns.clear()
1651
+ self.tables_list.clear()
1652
+ self.query_edit.clear()
1653
+ self.results_table.setRowCount(0)
1654
+ self.results_table.setColumnCount(0)
1655
+ self.current_project_file = None
1656
+ self.setWindowTitle('SQL Shell')
1657
+ self.statusBar().showMessage('New project created')
1658
+
1659
+ def save_project(self):
1660
+ """Save the current project"""
1661
+ if not self.current_project_file:
1662
+ self.save_project_as()
1663
+ return
1664
+
1665
+ self.save_project_to_file(self.current_project_file)
1666
+
1667
+ def save_project_as(self):
1668
+ """Save the current project to a new file"""
1669
+ file_name, _ = QFileDialog.getSaveFileName(
1670
+ self,
1671
+ "Save Project",
1672
+ "",
1673
+ "SQL Shell Project (*.sqls);;All Files (*)"
1674
+ )
1675
+
1676
+ if file_name:
1677
+ if not file_name.endswith('.sqls'):
1678
+ file_name += '.sqls'
1679
+ self.save_project_to_file(file_name)
1680
+ self.current_project_file = file_name
1681
+ self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
1682
+
1683
+ def save_project_to_file(self, file_name):
1684
+ """Save project data to a file"""
1685
+ try:
1686
+ project_data = {
1687
+ 'tables': {},
1688
+ 'query': self.query_edit.toPlainText(),
1689
+ 'connection_type': self.current_connection_type
1690
+ }
1691
+
1692
+ # Save table information
1693
+ for table_name, file_path in self.loaded_tables.items():
1694
+ # For database tables and query results, store the special identifier
1695
+ if file_path in ['database', 'query_result']:
1696
+ source_path = file_path
1697
+ else:
1698
+ # For file-based tables, store the absolute path
1699
+ source_path = os.path.abspath(file_path)
1700
+
1701
+ project_data['tables'][table_name] = {
1702
+ 'file_path': source_path,
1703
+ 'columns': self.table_columns.get(table_name, [])
1704
+ }
1705
+
1706
+ with open(file_name, 'w') as f:
1707
+ json.dump(project_data, f, indent=4)
1708
+
1709
+ self.statusBar().showMessage(f'Project saved to {file_name}')
1710
+
1711
+ except Exception as e:
1712
+ QMessageBox.critical(self, "Error",
1713
+ f"Failed to save project:\n\n{str(e)}")
1714
+
1715
+ def open_project(self):
1716
+ """Open a project file"""
1717
+ file_name, _ = QFileDialog.getOpenFileName(
1718
+ self,
1719
+ "Open Project",
1720
+ "",
1721
+ "SQL Shell Project (*.sqls);;All Files (*)"
1722
+ )
1723
+
1724
+ if file_name:
1725
+ try:
1726
+ with open(file_name, 'r') as f:
1727
+ project_data = json.load(f)
1728
+
1729
+ # Start fresh
1730
+ self.new_project()
1731
+
1732
+ # Create connection if needed
1733
+ if not self.conn:
1734
+ self.conn = duckdb.connect(':memory:')
1735
+ self.current_connection_type = 'duckdb'
1736
+ self.db_info_label.setText("Connected to: in-memory DuckDB")
1737
+
1738
+ # Load tables
1739
+ for table_name, table_info in project_data['tables'].items():
1740
+ file_path = table_info['file_path']
1741
+ try:
1742
+ if file_path == 'database':
1743
+ # For tables from database, we need to recreate them from their data
1744
+ # Execute a SELECT to get the data and recreate the table
1745
+ query = f"SELECT * FROM {table_name}"
1746
+ df = pd.read_sql_query(query, self.conn)
1747
+ self.conn.register(table_name, df)
1748
+ self.loaded_tables[table_name] = 'database'
1749
+ self.tables_list.addItem(f"{table_name} (database)")
1750
+ elif file_path == 'query_result':
1751
+ # For tables from query results, we'll need to re-run the query
1752
+ # For now, just note it as a query result table
1753
+ self.loaded_tables[table_name] = 'query_result'
1754
+ self.tables_list.addItem(f"{table_name} (query result)")
1755
+ elif os.path.exists(file_path):
1756
+ # Load the file based on its extension
1757
+ if file_path.endswith(('.xlsx', '.xls')):
1758
+ df = pd.read_excel(file_path)
1759
+ elif file_path.endswith('.csv'):
1760
+ df = pd.read_csv(file_path)
1761
+ elif file_path.endswith('.parquet'):
1762
+ df = pd.read_parquet(file_path)
1763
+ else:
1764
+ continue
1765
+
1766
+ # Register the table
1767
+ self.conn.register(table_name, df)
1768
+ self.loaded_tables[table_name] = file_path
1769
+ self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
1770
+ else:
1771
+ QMessageBox.warning(self, "Warning",
1772
+ f"Could not find file for table {table_name}: {file_path}")
1773
+ continue
1774
+
1775
+ # Store the columns
1776
+ self.table_columns[table_name] = table_info['columns']
1777
+
1778
+ except Exception as e:
1779
+ QMessageBox.warning(self, "Warning",
1780
+ f"Failed to load table {table_name}:\n{str(e)}")
1781
+
1782
+ # Restore query
1783
+ if 'query' in project_data:
1784
+ self.query_edit.setPlainText(project_data['query'])
1785
+
1786
+ # Update UI
1787
+ self.current_project_file = file_name
1788
+ self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
1789
+ self.statusBar().showMessage(f'Project loaded from {file_name}')
1790
+ self.update_completer()
1791
+
1792
+ except Exception as e:
1793
+ QMessageBox.critical(self, "Error",
1794
+ f"Failed to open project:\n\n{str(e)}")
1795
+
492
1796
  def main():
493
1797
  app = QApplication(sys.argv)
1798
+ app.setStyle(QStyleFactory.create('Fusion'))
1799
+
1800
+ # Ensure we have a valid working directory with pool.db
1801
+ package_dir = os.path.dirname(os.path.abspath(__file__))
1802
+ working_dir = os.getcwd()
1803
+
1804
+ # If pool.db doesn't exist in current directory, copy it from package
1805
+ if not os.path.exists(os.path.join(working_dir, 'pool.db')):
1806
+ import shutil
1807
+ package_db = os.path.join(package_dir, 'pool.db')
1808
+ if os.path.exists(package_db):
1809
+ shutil.copy2(package_db, working_dir)
1810
+ else:
1811
+ package_db = os.path.join(os.path.dirname(package_dir), 'pool.db')
1812
+ if os.path.exists(package_db):
1813
+ shutil.copy2(package_db, working_dir)
1814
+
1815
+ # Show splash screen
1816
+ splash = AnimatedSplashScreen()
1817
+ splash.show()
1818
+
1819
+ # Create and show main window after delay
1820
+ timer = QTimer()
1821
+ window = SQLShell()
1822
+ timer.timeout.connect(lambda: show_main_window())
1823
+ timer.start(2000) # 2 second delay
494
1824
 
495
- # Set application style
496
- app.setStyle('Fusion')
1825
+ def show_main_window():
1826
+ window.show()
1827
+ splash.finish(window)
1828
+ timer.stop()
497
1829
 
498
- sql_shell = SQLShell()
499
- sql_shell.show()
500
1830
  sys.exit(app.exec())
501
1831
 
502
1832
  if __name__ == '__main__':