sqlshell 0.1.6__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,5 +1,12 @@
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
@@ -8,493 +15,471 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
8
15
  QLabel, QSplitter, QListWidget, QTableWidget,
9
16
  QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
10
17
  QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
11
- QStyleFactory, QToolBar, QStatusBar)
12
- from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve
13
- from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap
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
14
23
  import numpy as np
15
24
  from datetime import datetime
16
- from sqlshell.sqlshell import create_test_data # Import from the correct location
17
25
 
18
- class SQLSyntaxHighlighter(QSyntaxHighlighter):
19
- def __init__(self, document):
20
- super().__init__(document)
21
- self.highlighting_rules = []
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
22
30
 
23
- # SQL Keywords
24
- keyword_format = QTextCharFormat()
25
- keyword_format.setForeground(QColor("#0000FF")) # Blue
26
- keyword_format.setFontWeight(QFont.Weight.Bold)
27
- keywords = [
28
- "\\bSELECT\\b", "\\bFROM\\b", "\\bWHERE\\b", "\\bAND\\b", "\\bOR\\b",
29
- "\\bINNER\\b", "\\bOUTER\\b", "\\bLEFT\\b", "\\bRIGHT\\b", "\\bJOIN\\b",
30
- "\\bON\\b", "\\bGROUP\\b", "\\bBY\\b", "\\bHAVING\\b", "\\bORDER\\b",
31
- "\\bLIMIT\\b", "\\bOFFSET\\b", "\\bUNION\\b", "\\bEXCEPT\\b", "\\bINTERSECT\\b",
32
- "\\bCREATE\\b", "\\bTABLE\\b", "\\bINDEX\\b", "\\bVIEW\\b", "\\bINSERT\\b",
33
- "\\bINTO\\b", "\\bVALUES\\b", "\\bUPDATE\\b", "\\bSET\\b", "\\bDELETE\\b",
34
- "\\bTRUNCATE\\b", "\\bALTER\\b", "\\bADD\\b", "\\bDROP\\b", "\\bCOLUMN\\b",
35
- "\\bCONSTRAINT\\b", "\\bPRIMARY\\b", "\\bKEY\\b", "\\bFOREIGN\\b", "\\bREFERENCES\\b",
36
- "\\bUNIQUE\\b", "\\bNOT\\b", "\\bNULL\\b", "\\bIS\\b", "\\bDISTINCT\\b",
37
- "\\bCASE\\b", "\\bWHEN\\b", "\\bTHEN\\b", "\\bELSE\\b", "\\bEND\\b",
38
- "\\bAS\\b", "\\bWITH\\b", "\\bBETWEEN\\b", "\\bLIKE\\b", "\\bIN\\b",
39
- "\\bEXISTS\\b", "\\bALL\\b", "\\bANY\\b", "\\bSOME\\b", "\\bDESC\\b", "\\bASC\\b"
40
- ]
41
- for pattern in keywords:
42
- regex = QRegularExpression(pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)
43
- self.highlighting_rules.append((regex, keyword_format))
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")
44
37
 
45
- # Functions
46
- function_format = QTextCharFormat()
47
- function_format.setForeground(QColor("#AA00AA")) # Purple
48
- functions = [
49
- "\\bAVG\\b", "\\bCOUNT\\b", "\\bSUM\\b", "\\bMAX\\b", "\\bMIN\\b",
50
- "\\bCOALESCE\\b", "\\bNVL\\b", "\\bNULLIF\\b", "\\bCAST\\b", "\\bCONVERT\\b",
51
- "\\bLOWER\\b", "\\bUPPER\\b", "\\bTRIM\\b", "\\bLTRIM\\b", "\\bRTRIM\\b",
52
- "\\bLENGTH\\b", "\\bSUBSTRING\\b", "\\bREPLACE\\b", "\\bCONCAT\\b",
53
- "\\bROUND\\b", "\\bFLOOR\\b", "\\bCEIL\\b", "\\bABS\\b", "\\bMOD\\b",
54
- "\\bCURRENT_DATE\\b", "\\bCURRENT_TIME\\b", "\\bCURRENT_TIMESTAMP\\b",
55
- "\\bEXTRACT\\b", "\\bDATE_PART\\b", "\\bTO_CHAR\\b", "\\bTO_DATE\\b"
56
- ]
57
- for pattern in functions:
58
- regex = QRegularExpression(pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)
59
- self.highlighting_rules.append((regex, function_format))
38
+ def set_range(self, min_val, max_val):
39
+ self.min_val = min_val
40
+ self.max_val = max_val
60
41
 
61
- # Numbers
62
- number_format = QTextCharFormat()
63
- number_format.setForeground(QColor("#009900")) # Green
64
- self.highlighting_rules.append((
65
- QRegularExpression("\\b[0-9]+\\b"),
66
- number_format
67
- ))
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)
68
76
 
69
- # Single-line string literals
70
- string_format = QTextCharFormat()
71
- string_format.setForeground(QColor("#CC6600")) # Orange/Brown
72
- self.highlighting_rules.append((
73
- QRegularExpression("'[^']*'"),
74
- string_format
75
- ))
76
- self.highlighting_rules.append((
77
- QRegularExpression("\"[^\"]*\""),
78
- string_format
79
- ))
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
80
89
 
81
- # Comments
82
- comment_format = QTextCharFormat()
83
- comment_format.setForeground(QColor("#777777")) # Gray
84
- comment_format.setFontItalic(True)
85
- self.highlighting_rules.append((
86
- QRegularExpression("--[^\n]*"),
87
- comment_format
88
- ))
89
-
90
- # Multi-line comments
91
- self.comment_start_expression = QRegularExpression("/\\*")
92
- self.comment_end_expression = QRegularExpression("\\*/")
93
- self.multi_line_comment_format = comment_format
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
94
95
 
95
- def highlightBlock(self, text):
96
- # Apply regular expression highlighting rules
97
- for pattern, format in self.highlighting_rules:
98
- match_iterator = pattern.globalMatch(text)
99
- while match_iterator.hasNext():
100
- match = match_iterator.next()
101
- self.setFormat(match.capturedStart(), match.capturedLength(), format)
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
102
116
 
103
- # Handle multi-line comments
104
- self.setCurrentBlockState(0)
105
-
106
- # If previous block was inside a comment, check if this block continues it
107
- start_index = 0
108
- if self.previousBlockState() != 1:
109
- # Find the start of a comment
110
- start_match = self.comment_start_expression.match(text)
111
- if start_match.hasMatch():
112
- start_index = start_match.capturedStart()
113
- else:
117
+ if not values:
114
118
  return
115
-
116
- while start_index >= 0:
117
- # Find the end of the comment
118
- end_match = self.comment_end_expression.match(text, start_index)
119
-
120
- # If end match found
121
- if end_match.hasMatch():
122
- end_index = end_match.capturedStart()
123
- comment_length = end_index - start_index + end_match.capturedLength()
124
- self.setFormat(start_index, comment_length, self.multi_line_comment_format)
125
-
126
- # Look for next comment
127
- start_match = self.comment_start_expression.match(text, start_index + comment_length)
128
- if start_match.hasMatch():
129
- start_index = start_match.capturedStart()
130
- else:
131
- start_index = -1
132
- else:
133
- # No end found, comment continues to next block
134
- self.setCurrentBlockState(1) # Still inside comment
135
- comment_length = len(text) - start_index
136
- self.setFormat(start_index, comment_length, self.multi_line_comment_format)
137
- start_index = -1
138
119
 
139
- class LineNumberArea(QWidget):
140
- def __init__(self, editor):
141
- super().__init__(editor)
142
- self.editor = editor
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)
143
129
 
144
- def sizeHint(self):
145
- return QSize(self.editor.line_number_area_width(), 0)
130
+ # Update the view
131
+ table.viewport().update()
146
132
 
147
- def paintEvent(self, event):
148
- self.editor.line_number_area_paint_event(event)
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
149
138
 
150
- class SQLEditor(QPlainTextEdit):
151
- def __init__(self, parent=None):
152
- super().__init__(parent)
153
- self.line_number_area = LineNumberArea(self)
154
-
155
- # Set monospaced font
156
- font = QFont("Consolas", 12) # Increased font size for better readability
157
- font.setFixedPitch(True)
158
- self.setFont(font)
159
-
160
- # Connect signals
161
- self.blockCountChanged.connect(self.update_line_number_area_width)
162
- self.updateRequest.connect(self.update_line_number_area)
163
-
164
- # Initialize
165
- self.update_line_number_area_width(0)
166
-
167
- # Set tab width to 4 spaces
168
- self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
169
-
170
- # Set placeholder text
171
- self.setPlaceholderText("Enter your SQL query here...")
172
-
173
- # Initialize completer
174
- self.completer = None
175
-
176
- # SQL Keywords for autocomplete
177
- self.sql_keywords = [
178
- "SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
179
- "ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
180
- "CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
181
- "TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
182
- "UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
183
- "AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
184
- "AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
185
- ]
186
-
187
- # Initialize with SQL keywords
188
- self.set_completer(QCompleter(self.sql_keywords))
189
-
190
- # Set modern selection color
191
- self.selection_color = QColor("#3498DB")
192
- self.selection_color.setAlpha(50) # Make it semi-transparent
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
+ """)
193
155
 
194
- def set_completer(self, completer):
195
- """Set the completer for the editor"""
196
- if self.completer:
197
- self.completer.disconnect(self)
198
-
199
- self.completer = completer
200
-
201
- if not self.completer:
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:
202
184
  return
203
-
204
- self.completer.setWidget(self)
205
- self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
206
- self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
207
- self.completer.activated.connect(self.insert_completion)
208
-
209
- def update_completer_model(self, words):
210
- """Update the completer model with new words"""
211
- if not self.completer:
185
+
186
+ table = self.parent()
187
+ if not table:
212
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
+ )
213
221
 
214
- # Combine SQL keywords with table/column names
215
- all_words = self.sql_keywords + words
216
-
217
- # Create a model with all words
218
- model = QStringListModel()
219
- model.setStringList(all_words)
220
-
221
- # Set the model to the completer
222
- self.completer.setModel(model)
223
-
224
- def text_under_cursor(self):
225
- """Get the text under the cursor for completion"""
226
- tc = self.textCursor()
227
- tc.select(QTextCursor.SelectionType.WordUnderCursor)
228
- return tc.selectedText()
229
-
230
- def insert_completion(self, completion):
231
- """Insert the completion text"""
232
- if self.completer.widget() != self:
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):
233
284
  return
234
285
 
235
- tc = self.textCursor()
236
- extra = len(completion) - len(self.completer.completionPrefix())
237
- tc.movePosition(QTextCursor.MoveOperation.Left)
238
- tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
239
- tc.insertText(completion[-extra:] + " ")
240
- self.setTextCursor(tc)
286
+ table = self.parent()
287
+ unique_values = set()
241
288
 
242
- def complete(self):
243
- """Show completion popup"""
244
- prefix = self.text_under_cursor()
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())
245
294
 
246
- if not prefix or len(prefix) < 2: # Only show completions for words with at least 2 characters
247
- if self.completer.popup().isVisible():
248
- self.completer.popup().hide()
249
- return
250
-
251
- self.completer.setCompletionPrefix(prefix)
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
+ """)
252
317
 
253
- # If no completions, hide popup
254
- if self.completer.completionCount() == 0:
255
- self.completer.popup().hide()
256
- return
257
-
258
- # Get popup and position it under the current text
259
- popup = self.completer.popup()
260
- popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
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
+ """)
261
390
 
262
- # Calculate position for the popup
263
- cr = self.cursorRect()
264
- cr.setWidth(self.completer.popup().sizeHintForColumn(0) +
265
- self.completer.popup().verticalScrollBar().sizeHint().width())
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
+ """)
266
404
 
267
- # Show the popup
268
- self.completer.complete(cr)
269
-
270
- def keyPressEvent(self, event):
271
- # Handle completer popup navigation
272
- if self.completer and self.completer.popup().isVisible():
273
- # Handle navigation keys for the popup
274
- if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Tab,
275
- Qt.Key.Key_Escape, Qt.Key.Key_Up, Qt.Key.Key_Down]:
276
- event.ignore()
277
- return
405
+ button_widget = QWidget(menu)
406
+ button_layout = QHBoxLayout(button_widget)
407
+ button_layout.addWidget(apply_button)
408
+ button_layout.addWidget(clear_button)
278
409
 
279
- # Handle special key combinations
280
- if event.key() == Qt.Key.Key_Tab:
281
- # Insert 4 spaces instead of a tab character
282
- self.insertPlainText(" ")
283
- return
284
-
285
- # Auto-indentation for new lines
286
- if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
287
- cursor = self.textCursor()
288
- block = cursor.block()
289
- text = block.text()
290
-
291
- # Get the indentation of the current line
292
- indentation = ""
293
- for char in text:
294
- if char.isspace():
295
- indentation += char
296
- else:
297
- break
298
-
299
- # Check if line ends with an opening bracket or keywords that should increase indentation
300
- increase_indent = ""
301
- if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
302
- ["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
303
- increase_indent = " "
304
-
305
- # Insert new line with proper indentation
306
- super().keyPressEvent(event)
307
- self.insertPlainText(indentation + increase_indent)
308
- return
309
-
310
- # Handle keyboard shortcuts
311
- if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
312
- if event.key() == Qt.Key.Key_Space:
313
- # Show completion popup
314
- self.complete()
315
- return
316
- elif event.key() == Qt.Key.Key_K:
317
- # Comment/uncomment the selected lines
318
- self.toggle_comment()
319
- return
320
-
321
- # For normal key presses
322
- super().keyPressEvent(event)
410
+ button_action = QWidgetAction(menu)
411
+ button_action.setDefaultWidget(button_widget)
412
+ menu.addAction(button_action)
323
413
 
324
- # Check for autocomplete after typing
325
- if event.text() and not event.text().isspace():
326
- self.complete()
327
-
328
- def paintEvent(self, event):
329
- # Call the parent's paintEvent first
330
- super().paintEvent(event)
331
-
332
- # Get the current cursor
333
- cursor = self.textCursor()
334
-
335
- # If there's a selection, paint custom highlight
336
- if cursor.hasSelection():
337
- # Create a painter for this widget
338
- painter = QPainter(self.viewport())
339
-
340
- # Get the selection start and end positions
341
- start = cursor.selectionStart()
342
- end = cursor.selectionEnd()
343
-
344
- # Create temporary cursor to get the rectangles
345
- temp_cursor = QTextCursor(cursor)
346
-
347
- # Move to start and get the starting position
348
- temp_cursor.setPosition(start)
349
- start_pos = self.cursorRect(temp_cursor)
350
-
351
- # Move to end and get the ending position
352
- temp_cursor.setPosition(end)
353
- end_pos = self.cursorRect(temp_cursor)
354
-
355
- # Set the highlight color with transparency
356
- painter.setBrush(QBrush(self.selection_color))
357
- painter.setPen(Qt.PenStyle.NoPen)
358
-
359
- # Draw the highlight rectangle
360
- if start_pos.top() == end_pos.top():
361
- # Single line selection
362
- painter.drawRect(QRect(start_pos.left(), start_pos.top(),
363
- end_pos.right() - start_pos.left(), start_pos.height()))
364
- else:
365
- # Multi-line selection
366
- # First line
367
- painter.drawRect(QRect(start_pos.left(), start_pos.top(),
368
- self.viewport().width() - start_pos.left(), start_pos.height()))
369
-
370
- # Middle lines (if any)
371
- if end_pos.top() > start_pos.top() + start_pos.height():
372
- painter.drawRect(QRect(0, start_pos.top() + start_pos.height(),
373
- self.viewport().width(),
374
- end_pos.top() - (start_pos.top() + start_pos.height())))
375
-
376
- # Last line
377
- painter.drawRect(QRect(0, end_pos.top(), end_pos.right(), end_pos.height()))
378
-
379
- painter.end()
380
-
381
- def focusInEvent(self, event):
382
- super().focusInEvent(event)
383
- # Show temporary hint in status bar when editor gets focus
384
- if hasattr(self.parent(), 'statusBar'):
385
- self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
386
-
387
- def toggle_comment(self):
388
- cursor = self.textCursor()
389
- if cursor.hasSelection():
390
- # Get the selected text
391
- start = cursor.selectionStart()
392
- end = cursor.selectionEnd()
393
-
394
- # Remember the selection
395
- cursor.setPosition(start)
396
- start_block = cursor.blockNumber()
397
- cursor.setPosition(end)
398
- end_block = cursor.blockNumber()
399
-
400
- # Process each line in the selection
401
- cursor.setPosition(start)
402
- cursor.beginEditBlock()
403
-
404
- for _ in range(start_block, end_block + 1):
405
- # Move to start of line
406
- cursor.movePosition(cursor.MoveOperation.StartOfLine)
407
-
408
- # Check if the line is already commented
409
- line_text = cursor.block().text().lstrip()
410
- if line_text.startswith('--'):
411
- # Remove comment
412
- pos = cursor.block().text().find('--')
413
- cursor.setPosition(cursor.block().position() + pos)
414
- cursor.deleteChar()
415
- cursor.deleteChar()
416
- else:
417
- # Add comment
418
- cursor.insertText('--')
419
-
420
- # Move to next line if not at the end
421
- if not cursor.atEnd():
422
- cursor.movePosition(cursor.MoveOperation.NextBlock)
414
+ def apply_filter():
415
+ # Get selected values
416
+ selected_values = {value for value, checkbox in value_checkboxes.items()
417
+ if checkbox.isChecked()}
423
418
 
424
- cursor.endEditBlock()
425
- else:
426
- # Comment/uncomment current line
427
- cursor.movePosition(cursor.MoveOperation.StartOfLine)
428
- cursor.movePosition(cursor.MoveOperation.EndOfLine, cursor.MoveMode.KeepAnchor)
429
- line_text = cursor.selectedText().lstrip()
430
-
431
- cursor.movePosition(cursor.MoveOperation.StartOfLine)
432
- if line_text.startswith('--'):
433
- # Remove comment
434
- pos = cursor.block().text().find('--')
435
- cursor.setPosition(cursor.block().position() + pos)
436
- cursor.deleteChar()
437
- cursor.deleteChar()
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
438
422
  else:
439
- # Add comment
440
- cursor.insertText('--')
441
-
442
- def line_number_area_width(self):
443
- digits = 1
444
- max_num = max(1, self.blockCount())
445
- while max_num >= 10:
446
- max_num //= 10
447
- digits += 1
448
-
449
- space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
450
- return space
451
-
452
- def update_line_number_area_width(self, _):
453
- self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
454
-
455
- def update_line_number_area(self, rect, dy):
456
- if dy:
457
- self.line_number_area.scroll(0, dy)
458
- else:
459
- self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
460
-
461
- if rect.contains(self.viewport().rect()):
462
- self.update_line_number_area_width(0)
463
-
464
- def resizeEvent(self, event):
465
- super().resizeEvent(event)
466
- cr = self.contentsRect()
467
- self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
468
-
469
- def line_number_area_paint_event(self, event):
470
- painter = QPainter(self.line_number_area)
471
- painter.fillRect(event.rect(), QColor("#f0f0f0")) # Light gray background
472
-
473
- block = self.firstVisibleBlock()
474
- block_number = block.blockNumber()
475
- top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
476
- bottom = top + round(self.blockBoundingRect(block).height())
477
-
478
- while block.isValid() and top <= event.rect().bottom():
479
- if block.isVisible() and bottom >= event.rect().top():
480
- number = str(block_number + 1)
481
- painter.setPen(QColor("#808080")) # Gray text
482
- painter.drawText(0, top, self.line_number_area.width() - 5,
483
- self.fontMetrics().height(),
484
- Qt.AlignmentFlag.AlignRight, number)
485
-
486
- block = block.next()
487
- top = bottom
488
- bottom = top + round(self.blockBoundingRect(block).height())
489
- block_number += 1
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}")
490
472
 
491
473
  class SQLShell(QMainWindow):
492
474
  def __init__(self):
493
475
  super().__init__()
494
- self.current_db_type = 'duckdb' # Default to DuckDB
495
- self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
476
+ self.conn = None
477
+ self.current_connection_type = None
496
478
  self.loaded_tables = {} # Keep track of loaded tables
497
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
498
483
 
499
484
  # Define color scheme
500
485
  self.colors = {
@@ -674,6 +659,33 @@ class SQLShell(QMainWindow):
674
659
  self.setWindowTitle('SQL Shell')
675
660
  self.setGeometry(100, 100, 1400, 800)
676
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
+
677
689
  # Create custom status bar
678
690
  status_bar = QStatusBar()
679
691
  self.setStatusBar(status_bar)
@@ -759,6 +771,8 @@ class SQLShell(QMainWindow):
759
771
  }
760
772
  """)
761
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)
762
776
  left_layout.addWidget(self.tables_list)
763
777
 
764
778
  # Add spacer at the bottom
@@ -856,8 +870,14 @@ class SQLShell(QMainWindow):
856
870
  self.results_table = QTableWidget()
857
871
  self.results_table.setSortingEnabled(True)
858
872
  self.results_table.setAlternatingRowColors(True)
859
- self.results_table.horizontalHeader().setStretchLastSection(True)
860
- 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
+
861
881
  self.results_table.verticalHeader().setVisible(False)
862
882
  self.results_table.setShowGrid(True)
863
883
  self.results_table.setGridStyle(Qt.PenStyle.SolidLine)
@@ -890,83 +910,141 @@ class SQLShell(QMainWindow):
890
910
  "Ctrl+Space: Show autocomplete"
891
911
  )
892
912
 
893
- def format_value(self, value):
894
- """Format values for display"""
895
- if pd.isna(value):
896
- return 'NULL'
897
- elif isinstance(value, (int, np.integer)):
898
- return f"{value:,}"
899
- elif isinstance(value, (float, np.floating)):
900
- return f"{value:,.2f}"
901
- elif isinstance(value, (datetime, pd.Timestamp)):
902
- return value.strftime('%Y-%m-%d %H:%M:%S')
903
- return str(value)
904
-
905
913
  def populate_table(self, df):
906
- """Populate the table widget with DataFrame content"""
907
- 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()
908
928
  self.results_table.setRowCount(0)
909
929
  self.results_table.setColumnCount(0)
910
- self.row_count_label.setText("No results")
911
- return
912
-
913
- # Set dimensions
914
- self.results_table.setRowCount(len(df))
915
- self.results_table.setColumnCount(len(df.columns))
916
-
917
- # Set headers
918
- self.results_table.setHorizontalHeaderLabels(df.columns)
919
-
920
- # Populate data
921
- for i, (_, row) in enumerate(df.iterrows()):
922
- for j, value in enumerate(row):
923
- formatted_value = self.format_value(value)
924
- item = QTableWidgetItem(formatted_value)
925
-
926
- # Set alignment based on data type
927
- if isinstance(value, (int, float, np.integer, np.floating)):
928
- item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
929
- else:
930
- item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
930
+
931
+ if df.empty:
932
+ self.statusBar().showMessage("Query returned no results")
933
+ return
931
934
 
932
- # Make cells read-only
933
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
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]
934
951
 
935
- # Apply special styling for NULL values
936
- if pd.isna(value):
937
- item.setForeground(QColor(self.colors['text_light']))
938
- item.setBackground(QColor("#F8F9FA"))
952
+ # Add rows for this chunk
953
+ self.results_table.setRowCount(chunk_end)
939
954
 
940
- self.results_table.setItem(i, j, item)
941
-
942
- # Auto-adjust column widths while ensuring minimum and maximum sizes
943
- self.results_table.resizeColumnsToContents()
944
- for i in range(len(df.columns)):
945
- width = self.results_table.columnWidth(i)
946
- self.results_table.setColumnWidth(i, min(max(width, 80), 300))
947
-
948
- # Update row count
949
- row_text = "row" if len(df) == 1 else "rows"
950
- self.row_count_label.setText(f"{len(df):,} {row_text}")
951
-
952
- # Apply zebra striping
953
- for i in range(len(df)):
954
- if i % 2 == 0:
955
- for j in range(len(df.columns)):
956
- item = self.results_table.item(i, j)
957
- if item and not pd.isna(df.iloc[i, j]):
958
- item.setBackground(QColor("#FFFFFF"))
959
- else:
960
- for j in range(len(df.columns)):
961
- item = self.results_table.item(i, j)
962
- if item and not pd.isna(df.iloc[i, j]):
963
- item.setBackground(QColor("#F8F9FA"))
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)
964
1042
 
965
1043
  def browse_files(self):
966
1044
  if not self.conn:
967
1045
  # Create a default in-memory DuckDB connection if none exists
968
1046
  self.conn = duckdb.connect(':memory:')
969
- self.current_db_type = 'duckdb'
1047
+ self.current_connection_type = 'duckdb'
970
1048
  self.db_info_label.setText("Connected to: in-memory DuckDB")
971
1049
 
972
1050
  file_names, _ = QFileDialog.getOpenFileNames(
@@ -999,7 +1077,7 @@ class SQLShell(QMainWindow):
999
1077
  counter += 1
1000
1078
 
1001
1079
  # Handle table creation based on database type
1002
- if self.current_db_type == 'sqlite':
1080
+ if self.current_connection_type == 'sqlite':
1003
1081
  # For SQLite, create a table from the DataFrame
1004
1082
  df.to_sql(table_name, self.conn, index=False, if_exists='replace')
1005
1083
  else:
@@ -1066,48 +1144,41 @@ class SQLShell(QMainWindow):
1066
1144
  self.update_completer()
1067
1145
 
1068
1146
  def open_database(self):
1069
- """Open a database file (DuckDB or SQLite)"""
1070
- file_name, _ = QFileDialog.getOpenFileName(
1071
- self,
1072
- "Open Database File",
1073
- "",
1074
- "Database Files (*.db);;All Files (*)"
1075
- )
1076
-
1077
- if not file_name:
1078
- return
1079
-
1147
+ """Open a database connection with proper error handling and resource management"""
1080
1148
  try:
1081
- # Try to detect database type
1082
- is_sqlite = self.is_sqlite_db(file_name)
1083
-
1084
- # Close existing connection if any
1085
1149
  if self.conn:
1086
- self.conn.close()
1087
-
1088
- # Connect to the database
1089
- if is_sqlite:
1090
- self.conn = sqlite3.connect(file_name)
1091
- self.current_db_type = 'sqlite'
1092
- else:
1093
- self.conn = duckdb.connect(file_name)
1094
- self.current_db_type = 'duckdb'
1095
-
1096
- # Clear existing tables
1097
- self.loaded_tables.clear()
1098
- self.tables_list.clear()
1099
-
1100
- # Load tables
1101
- self.load_database_tables()
1102
-
1103
- # Update UI
1104
- db_type = "SQLite" if is_sqlite else "DuckDB"
1105
- self.db_info_label.setText(f"Connected to: {os.path.basename(file_name)} ({db_type})")
1106
- self.statusBar().showMessage(f'Successfully opened {db_type} database: {file_name}')
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
1157
+
1158
+ filename, _ = QFileDialog.getOpenFileName(
1159
+ self,
1160
+ "Open Database",
1161
+ "",
1162
+ "All Database Files (*.db *.sqlite *.sqlite3);;All Files (*)"
1163
+ )
1107
1164
 
1108
- except Exception as e:
1109
- QMessageBox.critical(self, "Error", f"Failed to open database: {str(e)}")
1110
- 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
1111
1182
 
1112
1183
  def is_sqlite_db(self, filename):
1113
1184
  """Check if the file is a SQLite database"""
@@ -1121,7 +1192,7 @@ class SQLShell(QMainWindow):
1121
1192
  def load_database_tables(self):
1122
1193
  """Load all tables from the current database"""
1123
1194
  try:
1124
- if self.current_db_type == 'sqlite':
1195
+ if self.current_connection_type == 'sqlite':
1125
1196
  query = "SELECT name FROM sqlite_master WHERE type='table'"
1126
1197
  cursor = self.conn.cursor()
1127
1198
  tables = cursor.execute(query).fetchall()
@@ -1170,60 +1241,44 @@ class SQLShell(QMainWindow):
1170
1241
  self.query_edit.update_completer_model(completion_words)
1171
1242
 
1172
1243
  def execute_query(self):
1173
- query = self.query_edit.toPlainText().strip()
1174
- if not query:
1175
- return
1176
-
1177
- if not self.conn:
1178
- QMessageBox.warning(self, "No Connection", "No database connection available. Creating an in-memory DuckDB database.")
1179
- self.conn = duckdb.connect(':memory:')
1180
- self.current_db_type = 'duckdb'
1181
- self.db_info_label.setText("Connected to: in-memory DuckDB")
1182
-
1183
- # Show loading indicator in status bar
1184
- self.statusBar().showMessage('Executing query...')
1185
-
1186
1244
  try:
1187
- # Reset results title
1188
- results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
1189
- if results_title:
1190
- results_title.setText("RESULTS")
1191
-
1192
- if self.current_db_type == 'sqlite':
1193
- # Execute SQLite query and convert to DataFrame
1194
- result = pd.read_sql_query(query, self.conn)
1195
- else:
1196
- # Execute DuckDB query
1197
- result = self.conn.execute(query).fetchdf()
1198
-
1199
- self.populate_table(result)
1200
-
1201
- # Show success message with query stats
1202
- execution_time = datetime.now().strftime("%H:%M:%S")
1203
- row_count = len(result)
1204
- self.statusBar().showMessage(f'Query executed successfully at {execution_time} - {row_count:,} rows returned')
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()
1205
1251
 
1206
- except Exception as e:
1207
- self.results_table.setRowCount(0)
1208
- self.results_table.setColumnCount(0)
1209
- self.row_count_label.setText("Error")
1210
- self.statusBar().showMessage('Error executing query')
1211
-
1212
- # Show error message with modern styling
1213
- error_msg = str(e)
1214
- if "not found" in error_msg.lower():
1215
- error_msg += "\nMake sure the table name is correct and the table is loaded."
1216
- elif "syntax error" in error_msg.lower():
1217
- error_msg += "\nPlease check your SQL syntax."
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)
1218
1257
 
1219
- error_box = QMessageBox(self)
1220
- error_box.setIcon(QMessageBox.Icon.Critical)
1221
- error_box.setWindowTitle("Query Error")
1222
- error_box.setText("Error executing query")
1223
- error_box.setInformativeText(error_msg)
1224
- error_box.setDetailedText(f"Query:\n{query}")
1225
- error_box.setStandardButtons(QMessageBox.StandardButton.Ok)
1226
- error_box.exec()
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")
1277
+
1278
+ except Exception as e:
1279
+ QMessageBox.critical(self, "Unexpected Error",
1280
+ f"An unexpected error occurred:\n\n{str(e)}")
1281
+ self.statusBar().showMessage("Query execution failed")
1227
1282
 
1228
1283
  def clear_query(self):
1229
1284
  """Clear the query editor with animation"""
@@ -1243,7 +1298,7 @@ class SQLShell(QMainWindow):
1243
1298
  if item:
1244
1299
  table_name = item.text().split(' (')[0]
1245
1300
  try:
1246
- if self.current_db_type == 'sqlite':
1301
+ if self.current_connection_type == 'sqlite':
1247
1302
  preview_df = pd.read_sql_query(f'SELECT * FROM "{table_name}" LIMIT 5', self.conn)
1248
1303
  else:
1249
1304
  preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
@@ -1273,6 +1328,12 @@ class SQLShell(QMainWindow):
1273
1328
  def load_test_data(self):
1274
1329
  """Generate and load test data"""
1275
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
+
1276
1337
  # Show loading indicator
1277
1338
  self.statusBar().showMessage('Generating test data...')
1278
1339
 
@@ -1312,19 +1373,12 @@ class SQLShell(QMainWindow):
1312
1373
  # Set the sample query
1313
1374
  sample_query = """
1314
1375
  SELECT
1315
- s.orderid,
1316
- s.orderdate,
1317
- c.customername,
1318
- p.productname,
1319
- s.quantity,
1320
- s.unitprice,
1321
- (s.quantity * s.unitprice) AS total_amount
1376
+ DISTINCT
1377
+ c.customername
1322
1378
  FROM
1323
1379
  sample_sales_data s
1324
1380
  INNER JOIN customer_data c ON c.customerid = s.customerid
1325
1381
  INNER JOIN product_catalog p ON p.productid = s.productid
1326
- ORDER BY
1327
- s.orderdate DESC
1328
1382
  LIMIT 10
1329
1383
  """
1330
1384
  self.query_edit.setPlainText(sample_query.strip())
@@ -1359,13 +1413,36 @@ LIMIT 10
1359
1413
  df = self.get_table_data_as_dataframe()
1360
1414
  df.to_excel(file_name, index=False)
1361
1415
 
1362
- self.statusBar().showMessage(f'Data exported to {file_name}')
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()
1363
1440
 
1364
1441
  # Show success message
1365
1442
  QMessageBox.information(
1366
1443
  self,
1367
1444
  "Export Successful",
1368
- f"Data has been exported to:\n{file_name}",
1445
+ f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
1369
1446
  QMessageBox.StandardButton.Ok
1370
1447
  )
1371
1448
  except Exception as e:
@@ -1389,13 +1466,36 @@ LIMIT 10
1389
1466
  df = self.get_table_data_as_dataframe()
1390
1467
  df.to_parquet(file_name, index=False)
1391
1468
 
1392
- self.statusBar().showMessage(f'Data exported to {file_name}')
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()
1393
1493
 
1394
1494
  # Show success message
1395
1495
  QMessageBox.information(
1396
1496
  self,
1397
1497
  "Export Successful",
1398
- f"Data has been exported to:\n{file_name}",
1498
+ f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
1399
1499
  QMessageBox.StandardButton.Ok
1400
1500
  )
1401
1501
  except Exception as e:
@@ -1428,45 +1528,305 @@ LIMIT 10
1428
1528
 
1429
1529
  super().keyPressEvent(event)
1430
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
+
1431
1796
  def main():
1432
1797
  app = QApplication(sys.argv)
1798
+ app.setStyle(QStyleFactory.create('Fusion'))
1433
1799
 
1434
- # Set application style
1435
- app.setStyle('Fusion')
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()
1436
1803
 
1437
- # Apply custom palette for the entire application
1438
- palette = QPalette()
1439
- palette.setColor(QPalette.ColorRole.Window, QColor("#ECF0F1"))
1440
- palette.setColor(QPalette.ColorRole.WindowText, QColor("#2C3E50"))
1441
- palette.setColor(QPalette.ColorRole.Base, QColor("#FFFFFF"))
1442
- palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#F5F5F5"))
1443
- palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#2C3E50"))
1444
- palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#FFFFFF"))
1445
- palette.setColor(QPalette.ColorRole.Text, QColor("#2C3E50"))
1446
- palette.setColor(QPalette.ColorRole.Button, QColor("#3498DB"))
1447
- palette.setColor(QPalette.ColorRole.ButtonText, QColor("#FFFFFF"))
1448
- palette.setColor(QPalette.ColorRole.BrightText, QColor("#FFFFFF"))
1449
- palette.setColor(QPalette.ColorRole.Link, QColor("#3498DB"))
1450
- palette.setColor(QPalette.ColorRole.Highlight, QColor("#3498DB"))
1451
- palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
1452
- app.setPalette(palette)
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)
1453
1814
 
1454
- # Set default font
1455
- default_font = QFont("Segoe UI", 10)
1456
- app.setFont(default_font)
1815
+ # Show splash screen
1816
+ splash = AnimatedSplashScreen()
1817
+ splash.show()
1457
1818
 
1458
- # Create and show the application
1459
- sql_shell = SQLShell()
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
1460
1824
 
1461
- # Set application icon (if available)
1462
- try:
1463
- app_icon = QIcon("sqlshell/resources/icon.png")
1464
- sql_shell.setWindowIcon(app_icon)
1465
- except:
1466
- # If icon not found, continue without it
1467
- pass
1825
+ def show_main_window():
1826
+ window.show()
1827
+ splash.finish(window)
1828
+ timer.stop()
1468
1829
 
1469
- sql_shell.show()
1470
1830
  sys.exit(app.exec())
1471
1831
 
1472
1832
  if __name__ == '__main__':