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/__init__.py +4 -2
- sqlshell/create_test_data.py +50 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/editor.py +355 -0
- sqlshell/main.py +1510 -180
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/create_icon.py +53 -0
- sqlshell/resources/create_splash.py +66 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/splash_screen.py +177 -0
- sqlshell/sqlshell/create_test_data.py +4 -23
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/syntax_highlighter.py +123 -0
- sqlshell-0.1.8.dist-info/METADATA +120 -0
- sqlshell-0.1.8.dist-info/RECORD +21 -0
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.8.dist-info}/WHEEL +1 -1
- sqlshell-0.1.5.dist-info/METADATA +0 -92
- sqlshell-0.1.5.dist-info/RECORD +0 -11
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.8.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.8.dist-info}/top_level.txt +0 -0
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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.
|
|
20
|
-
self.
|
|
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 =
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
#
|
|
181
|
-
|
|
952
|
+
# Add rows for this chunk
|
|
953
|
+
self.results_table.setRowCount(chunk_end)
|
|
182
954
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
+
|
|
1143
|
+
# Update completer
|
|
1144
|
+
self.update_completer()
|
|
282
1145
|
|
|
283
1146
|
def open_database(self):
|
|
284
|
-
"""Open a database
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
self.
|
|
307
|
-
|
|
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
|
-
|
|
316
|
-
|
|
1158
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
1159
|
+
self,
|
|
1160
|
+
"Open Database",
|
|
1161
|
+
"",
|
|
1162
|
+
"All Database Files (*.db *.sqlite *.sqlite3);;All Files (*)"
|
|
1163
|
+
)
|
|
317
1164
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
self.
|
|
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.
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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__':
|