sqlshell 0.1.9__py3-none-any.whl → 0.2.1__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/LICENSE +21 -0
- sqlshell/MANIFEST.in +6 -0
- sqlshell/README.md +59 -0
- sqlshell/__init__.py +3 -1
- sqlshell/context_suggester.py +765 -0
- sqlshell/create_test_data.py +106 -30
- sqlshell/db/database_manager.py +152 -6
- sqlshell/editor.py +68 -11
- sqlshell/main.py +1566 -656
- sqlshell/menus.py +171 -0
- sqlshell/query_tab.py +32 -3
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +275 -0
- sqlshell/table_list.py +907 -0
- sqlshell/utils/__init__.py +8 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_keys.py +356 -0
- sqlshell-0.2.1.dist-info/METADATA +198 -0
- {sqlshell-0.1.9.dist-info → sqlshell-0.2.1.dist-info}/RECORD +22 -12
- {sqlshell-0.1.9.dist-info → sqlshell-0.2.1.dist-info}/WHEEL +1 -1
- sqlshell/setup.py +0 -42
- sqlshell-0.1.9.dist-info/METADATA +0 -122
- {sqlshell-0.1.9.dist-info → sqlshell-0.2.1.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.9.dist-info → sqlshell-0.2.1.dist-info}/top_level.txt +0 -0
sqlshell/main.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import os
|
|
3
3
|
import json
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
4
7
|
|
|
5
8
|
# Ensure proper path setup for resources when running directly
|
|
6
9
|
if __name__ == "__main__":
|
|
@@ -15,7 +18,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
15
18
|
QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
|
|
16
19
|
QStyleFactory, QToolBar, QStatusBar, QLineEdit, QMenu,
|
|
17
20
|
QCheckBox, QWidgetAction, QMenuBar, QInputDialog, QProgressDialog,
|
|
18
|
-
QListWidgetItem)
|
|
21
|
+
QListWidgetItem, QDialog, QGraphicsDropShadowEffect, QTreeWidgetItem)
|
|
19
22
|
from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint, QMimeData
|
|
20
23
|
from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath, QDrag
|
|
21
24
|
import numpy as np
|
|
@@ -28,111 +31,12 @@ from sqlshell.editor import LineNumberArea, SQLEditor
|
|
|
28
31
|
from sqlshell.ui import FilterHeader, BarChartDelegate
|
|
29
32
|
from sqlshell.db import DatabaseManager
|
|
30
33
|
from sqlshell.query_tab import QueryTab
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.parent = parent
|
|
38
|
-
self.setDragEnabled(True)
|
|
39
|
-
self.setDragDropMode(QListWidget.DragDropMode.DragOnly)
|
|
40
|
-
|
|
41
|
-
# Apply custom styling
|
|
42
|
-
self.setStyleSheet("""
|
|
43
|
-
QListWidget {
|
|
44
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
45
|
-
border: none;
|
|
46
|
-
border-radius: 4px;
|
|
47
|
-
color: white;
|
|
48
|
-
}
|
|
49
|
-
QListWidget::item:selected {
|
|
50
|
-
background-color: rgba(255, 255, 255, 0.2);
|
|
51
|
-
}
|
|
52
|
-
QListWidget::item:hover:!selected {
|
|
53
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
54
|
-
}
|
|
55
|
-
""")
|
|
56
|
-
|
|
57
|
-
def startDrag(self, supportedActions):
|
|
58
|
-
"""Override startDrag to customize the drag data."""
|
|
59
|
-
item = self.currentItem()
|
|
60
|
-
if not item:
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
# Extract the table name without the file info in parentheses
|
|
64
|
-
table_name = item.text().split(' (')[0]
|
|
65
|
-
|
|
66
|
-
# Create mime data with the table name
|
|
67
|
-
mime_data = QMimeData()
|
|
68
|
-
mime_data.setText(table_name)
|
|
69
|
-
|
|
70
|
-
# Create drag object
|
|
71
|
-
drag = QDrag(self)
|
|
72
|
-
drag.setMimeData(mime_data)
|
|
73
|
-
|
|
74
|
-
# Create a visually appealing drag pixmap
|
|
75
|
-
font = self.font()
|
|
76
|
-
font.setBold(True)
|
|
77
|
-
metrics = self.fontMetrics()
|
|
78
|
-
text_width = metrics.horizontalAdvance(table_name)
|
|
79
|
-
text_height = metrics.height()
|
|
80
|
-
|
|
81
|
-
# Make the pixmap large enough for the text plus padding and a small icon
|
|
82
|
-
padding = 10
|
|
83
|
-
pixmap = QPixmap(text_width + padding * 2 + 16, text_height + padding)
|
|
84
|
-
pixmap.fill(Qt.GlobalColor.transparent)
|
|
85
|
-
|
|
86
|
-
# Begin painting
|
|
87
|
-
painter = QPainter(pixmap)
|
|
88
|
-
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
89
|
-
|
|
90
|
-
# Draw a nice rounded rectangle background
|
|
91
|
-
bg_color = QColor(44, 62, 80, 220) # Dark blue with transparency
|
|
92
|
-
painter.setBrush(QBrush(bg_color))
|
|
93
|
-
painter.setPen(Qt.PenStyle.NoPen)
|
|
94
|
-
painter.drawRoundedRect(0, 0, pixmap.width(), pixmap.height(), 5, 5)
|
|
95
|
-
|
|
96
|
-
# Draw text
|
|
97
|
-
painter.setPen(Qt.GlobalColor.white)
|
|
98
|
-
painter.setFont(font)
|
|
99
|
-
painter.drawText(int(padding + 16), int(text_height + (padding / 2) - 2), table_name)
|
|
100
|
-
|
|
101
|
-
# Draw a small database icon (simulated)
|
|
102
|
-
icon_x = padding / 2
|
|
103
|
-
icon_y = (pixmap.height() - 12) / 2
|
|
104
|
-
|
|
105
|
-
# Draw a simple database icon as a blue circle with lines
|
|
106
|
-
table_icon_color = QColor("#3498DB")
|
|
107
|
-
painter.setBrush(QBrush(table_icon_color))
|
|
108
|
-
painter.setPen(Qt.GlobalColor.white)
|
|
109
|
-
painter.drawEllipse(int(icon_x), int(icon_y), 12, 12)
|
|
110
|
-
|
|
111
|
-
# Draw "table" lines inside the circle
|
|
112
|
-
painter.setPen(Qt.GlobalColor.white)
|
|
113
|
-
painter.drawLine(int(icon_x + 3), int(icon_y + 4), int(icon_x + 9), int(icon_y + 4))
|
|
114
|
-
painter.drawLine(int(icon_x + 3), int(icon_y + 6), int(icon_x + 9), int(icon_y + 6))
|
|
115
|
-
painter.drawLine(int(icon_x + 3), int(icon_y + 8), int(icon_x + 9), int(icon_y + 8))
|
|
116
|
-
|
|
117
|
-
painter.end()
|
|
118
|
-
|
|
119
|
-
# Set the drag pixmap
|
|
120
|
-
drag.setPixmap(pixmap)
|
|
121
|
-
|
|
122
|
-
# Set hotspot to be at the top-left corner of the text
|
|
123
|
-
drag.setHotSpot(QPoint(padding, pixmap.height() // 2))
|
|
124
|
-
|
|
125
|
-
# Execute drag operation
|
|
126
|
-
result = drag.exec(supportedActions)
|
|
127
|
-
|
|
128
|
-
# Optional: add a highlight effect after dragging
|
|
129
|
-
if result == Qt.DropAction.CopyAction and item:
|
|
130
|
-
# Briefly highlight the dragged item
|
|
131
|
-
orig_bg = item.background()
|
|
132
|
-
item.setBackground(QBrush(QColor(26, 188, 156, 100))) # Light green highlight
|
|
133
|
-
|
|
134
|
-
# Reset after a short delay
|
|
135
|
-
QTimer.singleShot(300, lambda: item.setBackground(orig_bg))
|
|
34
|
+
from sqlshell.styles import (get_application_stylesheet, get_tab_corner_stylesheet,
|
|
35
|
+
get_context_menu_stylesheet,
|
|
36
|
+
get_header_label_stylesheet, get_db_info_label_stylesheet,
|
|
37
|
+
get_tables_header_stylesheet, get_row_count_label_stylesheet)
|
|
38
|
+
from sqlshell.menus import setup_menubar
|
|
39
|
+
from sqlshell.table_list import DraggableTablesList
|
|
136
40
|
|
|
137
41
|
class SQLShell(QMainWindow):
|
|
138
42
|
def __init__(self):
|
|
@@ -145,9 +49,20 @@ class SQLShell(QMainWindow):
|
|
|
145
49
|
self.max_recent_projects = 10 # Maximum number of recent projects to track
|
|
146
50
|
self.tabs = [] # Store list of all tabs
|
|
147
51
|
|
|
52
|
+
# User preferences
|
|
53
|
+
self.auto_load_recent_project = True # Default to auto-loading most recent project
|
|
54
|
+
|
|
55
|
+
# File tracking for quick access
|
|
56
|
+
self.recent_files = [] # Store list of recently opened files
|
|
57
|
+
self.frequent_files = {} # Store file paths with usage counts
|
|
58
|
+
self.max_recent_files = 15 # Maximum number of recent files to track
|
|
59
|
+
|
|
148
60
|
# Load recent projects from settings
|
|
149
61
|
self.load_recent_projects()
|
|
150
62
|
|
|
63
|
+
# Load recent and frequent files from settings
|
|
64
|
+
self.load_recent_files()
|
|
65
|
+
|
|
151
66
|
# Define color scheme
|
|
152
67
|
self.colors = {
|
|
153
68
|
'primary': "#2C3E50", # Dark blue-gray
|
|
@@ -169,199 +84,14 @@ class SQLShell(QMainWindow):
|
|
|
169
84
|
|
|
170
85
|
# Create initial tab
|
|
171
86
|
self.add_tab()
|
|
87
|
+
|
|
88
|
+
# Load most recent project if enabled and available
|
|
89
|
+
if self.auto_load_recent_project:
|
|
90
|
+
self.load_most_recent_project()
|
|
172
91
|
|
|
173
92
|
def apply_stylesheet(self):
|
|
174
93
|
"""Apply custom stylesheet to the application"""
|
|
175
|
-
self.setStyleSheet(
|
|
176
|
-
QMainWindow {{
|
|
177
|
-
background-color: {self.colors['background']};
|
|
178
|
-
}}
|
|
179
|
-
|
|
180
|
-
QWidget {{
|
|
181
|
-
color: {self.colors['text']};
|
|
182
|
-
font-family: 'Segoe UI', 'Arial', sans-serif;
|
|
183
|
-
}}
|
|
184
|
-
|
|
185
|
-
QLabel {{
|
|
186
|
-
font-size: 13px;
|
|
187
|
-
padding: 2px;
|
|
188
|
-
}}
|
|
189
|
-
|
|
190
|
-
QLabel#header_label {{
|
|
191
|
-
font-size: 16px;
|
|
192
|
-
font-weight: bold;
|
|
193
|
-
color: {self.colors['primary']};
|
|
194
|
-
padding: 8px 0;
|
|
195
|
-
}}
|
|
196
|
-
|
|
197
|
-
QPushButton {{
|
|
198
|
-
background-color: {self.colors['secondary']};
|
|
199
|
-
color: white;
|
|
200
|
-
border: none;
|
|
201
|
-
border-radius: 4px;
|
|
202
|
-
padding: 8px 16px;
|
|
203
|
-
font-weight: bold;
|
|
204
|
-
font-size: 13px;
|
|
205
|
-
min-height: 30px;
|
|
206
|
-
}}
|
|
207
|
-
|
|
208
|
-
QPushButton:hover {{
|
|
209
|
-
background-color: #2980B9;
|
|
210
|
-
}}
|
|
211
|
-
|
|
212
|
-
QPushButton:pressed {{
|
|
213
|
-
background-color: #1F618D;
|
|
214
|
-
}}
|
|
215
|
-
|
|
216
|
-
QPushButton#primary_button {{
|
|
217
|
-
background-color: {self.colors['accent']};
|
|
218
|
-
}}
|
|
219
|
-
|
|
220
|
-
QPushButton#primary_button:hover {{
|
|
221
|
-
background-color: #16A085;
|
|
222
|
-
}}
|
|
223
|
-
|
|
224
|
-
QPushButton#primary_button:pressed {{
|
|
225
|
-
background-color: #0E6655;
|
|
226
|
-
}}
|
|
227
|
-
|
|
228
|
-
QPushButton#danger_button {{
|
|
229
|
-
background-color: {self.colors['error']};
|
|
230
|
-
}}
|
|
231
|
-
|
|
232
|
-
QPushButton#danger_button:hover {{
|
|
233
|
-
background-color: #CB4335;
|
|
234
|
-
}}
|
|
235
|
-
|
|
236
|
-
QToolButton {{
|
|
237
|
-
background-color: transparent;
|
|
238
|
-
border: none;
|
|
239
|
-
border-radius: 4px;
|
|
240
|
-
padding: 4px;
|
|
241
|
-
}}
|
|
242
|
-
|
|
243
|
-
QToolButton:hover {{
|
|
244
|
-
background-color: rgba(52, 152, 219, 0.2);
|
|
245
|
-
}}
|
|
246
|
-
|
|
247
|
-
QFrame#sidebar {{
|
|
248
|
-
background-color: {self.colors['primary']};
|
|
249
|
-
border-radius: 0px;
|
|
250
|
-
}}
|
|
251
|
-
|
|
252
|
-
QFrame#content_panel {{
|
|
253
|
-
background-color: white;
|
|
254
|
-
border-radius: 8px;
|
|
255
|
-
border: 1px solid {self.colors['border']};
|
|
256
|
-
}}
|
|
257
|
-
|
|
258
|
-
QListWidget {{
|
|
259
|
-
background-color: white;
|
|
260
|
-
border-radius: 4px;
|
|
261
|
-
border: 1px solid {self.colors['border']};
|
|
262
|
-
padding: 4px;
|
|
263
|
-
outline: none;
|
|
264
|
-
}}
|
|
265
|
-
|
|
266
|
-
QListWidget::item {{
|
|
267
|
-
padding: 8px;
|
|
268
|
-
border-radius: 4px;
|
|
269
|
-
}}
|
|
270
|
-
|
|
271
|
-
QListWidget::item:selected {{
|
|
272
|
-
background-color: {self.colors['secondary']};
|
|
273
|
-
color: white;
|
|
274
|
-
}}
|
|
275
|
-
|
|
276
|
-
QListWidget::item:hover:!selected {{
|
|
277
|
-
background-color: #E3F2FD;
|
|
278
|
-
}}
|
|
279
|
-
|
|
280
|
-
QTableWidget {{
|
|
281
|
-
background-color: white;
|
|
282
|
-
alternate-background-color: #F8F9FA;
|
|
283
|
-
border-radius: 4px;
|
|
284
|
-
border: 1px solid {self.colors['border']};
|
|
285
|
-
gridline-color: #E0E0E0;
|
|
286
|
-
outline: none;
|
|
287
|
-
}}
|
|
288
|
-
|
|
289
|
-
QTableWidget::item {{
|
|
290
|
-
padding: 4px;
|
|
291
|
-
}}
|
|
292
|
-
|
|
293
|
-
QTableWidget::item:selected {{
|
|
294
|
-
background-color: rgba(52, 152, 219, 0.2);
|
|
295
|
-
color: {self.colors['text']};
|
|
296
|
-
}}
|
|
297
|
-
|
|
298
|
-
QHeaderView::section {{
|
|
299
|
-
background-color: {self.colors['primary']};
|
|
300
|
-
color: white;
|
|
301
|
-
padding: 8px;
|
|
302
|
-
border: none;
|
|
303
|
-
font-weight: bold;
|
|
304
|
-
}}
|
|
305
|
-
|
|
306
|
-
QSplitter::handle {{
|
|
307
|
-
background-color: {self.colors['border']};
|
|
308
|
-
}}
|
|
309
|
-
|
|
310
|
-
QStatusBar {{
|
|
311
|
-
background-color: {self.colors['primary']};
|
|
312
|
-
color: white;
|
|
313
|
-
padding: 8px;
|
|
314
|
-
}}
|
|
315
|
-
|
|
316
|
-
QTabWidget::pane {{
|
|
317
|
-
border: 1px solid {self.colors['border']};
|
|
318
|
-
border-radius: 4px;
|
|
319
|
-
top: -1px;
|
|
320
|
-
background-color: white;
|
|
321
|
-
}}
|
|
322
|
-
|
|
323
|
-
QTabBar::tab {{
|
|
324
|
-
background-color: {self.colors['light_bg']};
|
|
325
|
-
color: {self.colors['text']};
|
|
326
|
-
border: 1px solid {self.colors['border']};
|
|
327
|
-
border-bottom: none;
|
|
328
|
-
border-top-left-radius: 4px;
|
|
329
|
-
border-top-right-radius: 4px;
|
|
330
|
-
padding: 8px 12px;
|
|
331
|
-
margin-right: 2px;
|
|
332
|
-
min-width: 80px;
|
|
333
|
-
}}
|
|
334
|
-
|
|
335
|
-
QTabBar::tab:selected {{
|
|
336
|
-
background-color: white;
|
|
337
|
-
border-bottom: 1px solid white;
|
|
338
|
-
}}
|
|
339
|
-
|
|
340
|
-
QTabBar::tab:hover:!selected {{
|
|
341
|
-
background-color: #E3F2FD;
|
|
342
|
-
}}
|
|
343
|
-
|
|
344
|
-
QTabBar::close-button {{
|
|
345
|
-
image: url(close.png);
|
|
346
|
-
subcontrol-position: right;
|
|
347
|
-
}}
|
|
348
|
-
|
|
349
|
-
QTabBar::close-button:hover {{
|
|
350
|
-
background-color: rgba(255, 0, 0, 0.2);
|
|
351
|
-
border-radius: 2px;
|
|
352
|
-
}}
|
|
353
|
-
|
|
354
|
-
QPlainTextEdit, QTextEdit {{
|
|
355
|
-
background-color: white;
|
|
356
|
-
border-radius: 4px;
|
|
357
|
-
border: 1px solid {self.colors['border']};
|
|
358
|
-
padding: 8px;
|
|
359
|
-
selection-background-color: #BBDEFB;
|
|
360
|
-
selection-color: {self.colors['text']};
|
|
361
|
-
font-family: 'Consolas', 'Courier New', monospace;
|
|
362
|
-
font-size: 14px;
|
|
363
|
-
}}
|
|
364
|
-
""")
|
|
94
|
+
self.setStyleSheet(get_application_stylesheet(self.colors))
|
|
365
95
|
|
|
366
96
|
def init_ui(self):
|
|
367
97
|
self.setWindowTitle('SQL Shell')
|
|
@@ -399,78 +129,12 @@ class SQLShell(QMainWindow):
|
|
|
399
129
|
if os.path.exists(main_logo_path):
|
|
400
130
|
self.setWindowIcon(QIcon(main_logo_path))
|
|
401
131
|
|
|
402
|
-
#
|
|
403
|
-
|
|
404
|
-
file_menu = menubar.addMenu('&File')
|
|
405
|
-
|
|
406
|
-
# Project management actions
|
|
407
|
-
new_project_action = file_menu.addAction('New Project')
|
|
408
|
-
new_project_action.setShortcut('Ctrl+N')
|
|
409
|
-
new_project_action.triggered.connect(self.new_project)
|
|
410
|
-
|
|
411
|
-
open_project_action = file_menu.addAction('Open Project...')
|
|
412
|
-
open_project_action.setShortcut('Ctrl+O')
|
|
413
|
-
open_project_action.triggered.connect(self.open_project)
|
|
414
|
-
|
|
415
|
-
# Add Recent Projects submenu
|
|
416
|
-
self.recent_projects_menu = file_menu.addMenu('Recent Projects')
|
|
417
|
-
self.update_recent_projects_menu()
|
|
418
|
-
|
|
419
|
-
save_project_action = file_menu.addAction('Save Project')
|
|
420
|
-
save_project_action.setShortcut('Ctrl+S')
|
|
421
|
-
save_project_action.triggered.connect(self.save_project)
|
|
422
|
-
|
|
423
|
-
save_project_as_action = file_menu.addAction('Save Project As...')
|
|
424
|
-
save_project_as_action.setShortcut('Ctrl+Shift+S')
|
|
425
|
-
save_project_as_action.triggered.connect(self.save_project_as)
|
|
426
|
-
|
|
427
|
-
file_menu.addSeparator()
|
|
428
|
-
|
|
429
|
-
exit_action = file_menu.addAction('Exit')
|
|
430
|
-
exit_action.setShortcut('Ctrl+Q')
|
|
431
|
-
exit_action.triggered.connect(self.close)
|
|
432
|
-
|
|
433
|
-
# Add View menu for window management
|
|
434
|
-
view_menu = menubar.addMenu('&View')
|
|
435
|
-
|
|
436
|
-
# Maximized window option
|
|
437
|
-
maximize_action = view_menu.addAction('Maximize Window')
|
|
438
|
-
maximize_action.setShortcut('F11')
|
|
439
|
-
maximize_action.triggered.connect(self.toggle_maximize_window)
|
|
440
|
-
|
|
441
|
-
# Zoom submenu
|
|
442
|
-
zoom_menu = view_menu.addMenu('Zoom')
|
|
443
|
-
|
|
444
|
-
zoom_in_action = zoom_menu.addAction('Zoom In')
|
|
445
|
-
zoom_in_action.setShortcut('Ctrl++')
|
|
446
|
-
zoom_in_action.triggered.connect(lambda: self.change_zoom(1.1))
|
|
132
|
+
# Setup menus
|
|
133
|
+
setup_menubar(self)
|
|
447
134
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
reset_zoom_action = zoom_menu.addAction('Reset Zoom')
|
|
453
|
-
reset_zoom_action.setShortcut('Ctrl+0')
|
|
454
|
-
reset_zoom_action.triggered.connect(lambda: self.reset_zoom())
|
|
455
|
-
|
|
456
|
-
# Add Tab menu
|
|
457
|
-
tab_menu = menubar.addMenu('&Tab')
|
|
458
|
-
|
|
459
|
-
new_tab_action = tab_menu.addAction('New Tab')
|
|
460
|
-
new_tab_action.setShortcut('Ctrl+T')
|
|
461
|
-
new_tab_action.triggered.connect(self.add_tab)
|
|
462
|
-
|
|
463
|
-
duplicate_tab_action = tab_menu.addAction('Duplicate Current Tab')
|
|
464
|
-
duplicate_tab_action.setShortcut('Ctrl+D')
|
|
465
|
-
duplicate_tab_action.triggered.connect(self.duplicate_current_tab)
|
|
466
|
-
|
|
467
|
-
rename_tab_action = tab_menu.addAction('Rename Current Tab')
|
|
468
|
-
rename_tab_action.setShortcut('Ctrl+R')
|
|
469
|
-
rename_tab_action.triggered.connect(self.rename_current_tab)
|
|
470
|
-
|
|
471
|
-
close_tab_action = tab_menu.addAction('Close Current Tab')
|
|
472
|
-
close_tab_action.setShortcut('Ctrl+W')
|
|
473
|
-
close_tab_action.triggered.connect(self.close_current_tab)
|
|
135
|
+
# Update quick access menu
|
|
136
|
+
if hasattr(self, 'quick_access_menu'):
|
|
137
|
+
self.update_quick_access_menu()
|
|
474
138
|
|
|
475
139
|
# Create custom status bar
|
|
476
140
|
status_bar = QStatusBar()
|
|
@@ -495,53 +159,37 @@ class SQLShell(QMainWindow):
|
|
|
495
159
|
# Database info section
|
|
496
160
|
db_header = QLabel("DATABASE")
|
|
497
161
|
db_header.setObjectName("header_label")
|
|
498
|
-
db_header.setStyleSheet(
|
|
162
|
+
db_header.setStyleSheet(get_header_label_stylesheet())
|
|
499
163
|
left_layout.addWidget(db_header)
|
|
500
164
|
|
|
501
165
|
self.db_info_label = QLabel("No database connected")
|
|
502
|
-
self.db_info_label.setStyleSheet(
|
|
166
|
+
self.db_info_label.setStyleSheet(get_db_info_label_stylesheet())
|
|
503
167
|
left_layout.addWidget(self.db_info_label)
|
|
504
168
|
|
|
505
169
|
# Database action buttons
|
|
506
170
|
db_buttons_layout = QHBoxLayout()
|
|
507
171
|
db_buttons_layout.setSpacing(8)
|
|
508
172
|
|
|
509
|
-
self.
|
|
510
|
-
self.
|
|
511
|
-
self.
|
|
173
|
+
self.load_btn = QPushButton('Load')
|
|
174
|
+
self.load_btn.setIcon(QIcon.fromTheme("document-open"))
|
|
175
|
+
self.load_btn.clicked.connect(self.show_load_dialog)
|
|
512
176
|
|
|
513
|
-
self.
|
|
514
|
-
self.
|
|
177
|
+
self.quick_access_btn = QPushButton('Quick Access')
|
|
178
|
+
self.quick_access_btn.setIcon(QIcon.fromTheme("document-open-recent"))
|
|
179
|
+
self.quick_access_btn.clicked.connect(self.show_quick_access_menu)
|
|
515
180
|
|
|
516
|
-
db_buttons_layout.addWidget(self.
|
|
517
|
-
db_buttons_layout.addWidget(self.
|
|
181
|
+
db_buttons_layout.addWidget(self.load_btn)
|
|
182
|
+
db_buttons_layout.addWidget(self.quick_access_btn)
|
|
518
183
|
left_layout.addLayout(db_buttons_layout)
|
|
519
184
|
|
|
520
185
|
# Tables section
|
|
521
186
|
tables_header = QLabel("TABLES")
|
|
522
187
|
tables_header.setObjectName("header_label")
|
|
523
|
-
tables_header.setStyleSheet(
|
|
188
|
+
tables_header.setStyleSheet(get_tables_header_stylesheet())
|
|
524
189
|
left_layout.addWidget(tables_header)
|
|
525
190
|
|
|
526
|
-
# Table actions
|
|
527
|
-
table_actions_layout = QHBoxLayout()
|
|
528
|
-
table_actions_layout.setSpacing(8)
|
|
529
|
-
|
|
530
|
-
self.browse_btn = QPushButton('Load Files')
|
|
531
|
-
self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
|
|
532
|
-
self.browse_btn.clicked.connect(self.browse_files)
|
|
533
|
-
|
|
534
|
-
self.remove_table_btn = QPushButton('Remove')
|
|
535
|
-
self.remove_table_btn.setObjectName("danger_button")
|
|
536
|
-
self.remove_table_btn.setIcon(QIcon.fromTheme("edit-delete"))
|
|
537
|
-
self.remove_table_btn.clicked.connect(self.remove_selected_table)
|
|
538
|
-
|
|
539
|
-
table_actions_layout.addWidget(self.browse_btn)
|
|
540
|
-
table_actions_layout.addWidget(self.remove_table_btn)
|
|
541
|
-
left_layout.addLayout(table_actions_layout)
|
|
542
|
-
|
|
543
191
|
# Tables list with custom styling
|
|
544
|
-
self.tables_list = DraggableTablesList()
|
|
192
|
+
self.tables_list = DraggableTablesList(self)
|
|
545
193
|
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
546
194
|
self.tables_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
547
195
|
self.tables_list.customContextMenuRequested.connect(self.show_tables_context_menu)
|
|
@@ -581,7 +229,7 @@ class SQLShell(QMainWindow):
|
|
|
581
229
|
main_layout.addWidget(right_panel, 4)
|
|
582
230
|
|
|
583
231
|
# Status bar
|
|
584
|
-
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment | Ctrl+T: New Tab')
|
|
232
|
+
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment | Ctrl+T: New Tab | Ctrl+Shift+O: Quick Access Files')
|
|
585
233
|
|
|
586
234
|
def create_tab_corner_widget(self):
|
|
587
235
|
"""Create a corner widget with a + button to add new tabs"""
|
|
@@ -593,23 +241,7 @@ class SQLShell(QMainWindow):
|
|
|
593
241
|
add_tab_btn = QToolButton()
|
|
594
242
|
add_tab_btn.setText("+")
|
|
595
243
|
add_tab_btn.setToolTip("Add new tab (Ctrl+T)")
|
|
596
|
-
add_tab_btn.setStyleSheet(
|
|
597
|
-
QToolButton {
|
|
598
|
-
background-color: transparent;
|
|
599
|
-
border: none;
|
|
600
|
-
border-radius: 4px;
|
|
601
|
-
padding: 4px;
|
|
602
|
-
font-weight: bold;
|
|
603
|
-
font-size: 16px;
|
|
604
|
-
color: #3498DB;
|
|
605
|
-
}
|
|
606
|
-
QToolButton:hover {
|
|
607
|
-
background-color: rgba(52, 152, 219, 0.2);
|
|
608
|
-
}
|
|
609
|
-
QToolButton:pressed {
|
|
610
|
-
background-color: rgba(52, 152, 219, 0.4);
|
|
611
|
-
}
|
|
612
|
-
""")
|
|
244
|
+
add_tab_btn.setStyleSheet(get_tab_corner_stylesheet())
|
|
613
245
|
add_tab_btn.clicked.connect(self.add_tab)
|
|
614
246
|
|
|
615
247
|
layout.addWidget(add_tab_btn)
|
|
@@ -743,11 +375,16 @@ class SQLShell(QMainWindow):
|
|
|
743
375
|
elif isinstance(value, (float, np.floating)):
|
|
744
376
|
if value.is_integer():
|
|
745
377
|
return str(int(value))
|
|
746
|
-
|
|
378
|
+
# Display full number without scientific notation by using 'f' format
|
|
379
|
+
# Format large numbers with commas for better readability
|
|
380
|
+
if abs(value) >= 1000000:
|
|
381
|
+
return f"{value:,.2f}" # Format with commas and 2 decimal places
|
|
382
|
+
return f"{value:.6f}" # Use fixed-point notation with 6 decimal places
|
|
747
383
|
elif isinstance(value, (pd.Timestamp, datetime)):
|
|
748
384
|
return value.strftime("%Y-%m-%d %H:%M:%S")
|
|
749
385
|
elif isinstance(value, (np.integer, int)):
|
|
750
|
-
|
|
386
|
+
# Format large integers with commas for better readability
|
|
387
|
+
return f"{value:,}"
|
|
751
388
|
elif isinstance(value, bool):
|
|
752
389
|
return str(value)
|
|
753
390
|
elif isinstance(value, (bytes, bytearray)):
|
|
@@ -769,11 +406,14 @@ class SQLShell(QMainWindow):
|
|
|
769
406
|
|
|
770
407
|
for file_name in file_names:
|
|
771
408
|
try:
|
|
409
|
+
# Add to recent files
|
|
410
|
+
self.add_recent_file(file_name)
|
|
411
|
+
|
|
772
412
|
# Use the database manager to load the file
|
|
773
413
|
table_name, df = self.db_manager.load_file(file_name)
|
|
774
414
|
|
|
775
|
-
# Update UI
|
|
776
|
-
self.tables_list.
|
|
415
|
+
# Update UI using new method
|
|
416
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_name))
|
|
777
417
|
self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
|
|
778
418
|
|
|
779
419
|
# Show preview of loaded data
|
|
@@ -798,18 +438,34 @@ class SQLShell(QMainWindow):
|
|
|
798
438
|
|
|
799
439
|
def remove_selected_table(self):
|
|
800
440
|
current_item = self.tables_list.currentItem()
|
|
801
|
-
if current_item:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
441
|
+
if not current_item or self.tables_list.is_folder_item(current_item):
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
table_name = self.tables_list.get_table_name_from_item(current_item)
|
|
445
|
+
if not table_name:
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
if self.db_manager.remove_table(table_name):
|
|
449
|
+
# Remove from tree widget
|
|
450
|
+
parent = current_item.parent()
|
|
451
|
+
if parent:
|
|
452
|
+
parent.removeChild(current_item)
|
|
453
|
+
else:
|
|
454
|
+
index = self.tables_list.indexOfTopLevelItem(current_item)
|
|
455
|
+
if index >= 0:
|
|
456
|
+
self.tables_list.takeTopLevelItem(index)
|
|
457
|
+
|
|
458
|
+
self.statusBar().showMessage(f'Removed table "{table_name}"')
|
|
459
|
+
|
|
460
|
+
# Get the current tab and clear its results table
|
|
461
|
+
current_tab = self.get_current_tab()
|
|
462
|
+
if current_tab:
|
|
463
|
+
current_tab.results_table.setRowCount(0)
|
|
464
|
+
current_tab.results_table.setColumnCount(0)
|
|
465
|
+
current_tab.row_count_label.setText("")
|
|
466
|
+
|
|
467
|
+
# Update completer
|
|
468
|
+
self.update_completer()
|
|
813
469
|
|
|
814
470
|
def open_database(self):
|
|
815
471
|
"""Open a database connection with proper error handling and resource management"""
|
|
@@ -823,19 +479,22 @@ class SQLShell(QMainWindow):
|
|
|
823
479
|
|
|
824
480
|
if filename:
|
|
825
481
|
try:
|
|
482
|
+
# Add to recent files
|
|
483
|
+
self.add_recent_file(filename)
|
|
484
|
+
|
|
826
485
|
# Clear existing database tables from the list widget
|
|
827
|
-
for i in range(self.tables_list.
|
|
828
|
-
item = self.tables_list.
|
|
829
|
-
if item and item.text().endswith('(database)'):
|
|
830
|
-
self.tables_list.
|
|
486
|
+
for i in range(self.tables_list.topLevelItemCount() - 1, -1, -1):
|
|
487
|
+
item = self.tables_list.topLevelItem(i)
|
|
488
|
+
if item and item.text(0).endswith('(database)'):
|
|
489
|
+
self.tables_list.takeTopLevelItem(i)
|
|
831
490
|
|
|
832
491
|
# Use the database manager to open the database
|
|
833
|
-
self.db_manager.open_database(filename)
|
|
492
|
+
self.db_manager.open_database(filename, load_all_tables=True)
|
|
834
493
|
|
|
835
494
|
# Update UI with tables from the database
|
|
836
495
|
for table_name, source in self.db_manager.loaded_tables.items():
|
|
837
496
|
if source == 'database':
|
|
838
|
-
self.tables_list.
|
|
497
|
+
self.tables_list.add_table_item(table_name, "database")
|
|
839
498
|
|
|
840
499
|
# Update the completer with table and column names
|
|
841
500
|
self.update_completer()
|
|
@@ -861,6 +520,12 @@ class SQLShell(QMainWindow):
|
|
|
861
520
|
if self.tab_widget.count() == 0:
|
|
862
521
|
return
|
|
863
522
|
|
|
523
|
+
# Import the suggestion manager
|
|
524
|
+
from sqlshell.suggester_integration import get_suggestion_manager
|
|
525
|
+
|
|
526
|
+
# Get the suggestion manager singleton
|
|
527
|
+
suggestion_mgr = get_suggestion_manager()
|
|
528
|
+
|
|
864
529
|
# Start a background update with a timer
|
|
865
530
|
self.statusBar().showMessage("Updating auto-completion...", 2000)
|
|
866
531
|
|
|
@@ -869,7 +534,34 @@ class SQLShell(QMainWindow):
|
|
|
869
534
|
self.query_history = []
|
|
870
535
|
self.completion_usage = {} # Track usage frequency
|
|
871
536
|
|
|
872
|
-
# Get
|
|
537
|
+
# Get schema information from the database manager
|
|
538
|
+
try:
|
|
539
|
+
# Get table and column information
|
|
540
|
+
tables = set(self.db_manager.loaded_tables.keys())
|
|
541
|
+
table_columns = self.db_manager.table_columns
|
|
542
|
+
|
|
543
|
+
# Get column data types if available
|
|
544
|
+
column_types = {}
|
|
545
|
+
for table, columns in self.db_manager.table_columns.items():
|
|
546
|
+
for col in columns:
|
|
547
|
+
qualified_name = f"{table}.{col}"
|
|
548
|
+
# Try to infer type from sample data
|
|
549
|
+
if hasattr(self.db_manager, 'sample_data') and table in self.db_manager.sample_data:
|
|
550
|
+
sample = self.db_manager.sample_data[table]
|
|
551
|
+
if col in sample.columns:
|
|
552
|
+
# Get data type from pandas
|
|
553
|
+
col_dtype = str(sample[col].dtype)
|
|
554
|
+
column_types[qualified_name] = col_dtype
|
|
555
|
+
# Also store unqualified name
|
|
556
|
+
column_types[col] = col_dtype
|
|
557
|
+
|
|
558
|
+
# Update the suggestion manager with schema information
|
|
559
|
+
suggestion_mgr.update_schema(tables, table_columns, column_types)
|
|
560
|
+
|
|
561
|
+
except Exception as e:
|
|
562
|
+
self.statusBar().showMessage(f"Error getting completions: {str(e)}", 2000)
|
|
563
|
+
|
|
564
|
+
# Get all completion words from basic system (for backward compatibility)
|
|
873
565
|
try:
|
|
874
566
|
completion_words = self.db_manager.get_all_table_columns()
|
|
875
567
|
except Exception as e:
|
|
@@ -886,141 +578,45 @@ class SQLShell(QMainWindow):
|
|
|
886
578
|
)[:100]
|
|
887
579
|
|
|
888
580
|
# Add these to our completion words
|
|
889
|
-
for term,
|
|
581
|
+
for term, count in frequent_terms:
|
|
582
|
+
suggestion_mgr.suggester.usage_counts[term] = count
|
|
890
583
|
if term not in completion_words:
|
|
891
584
|
completion_words.append(term)
|
|
892
585
|
|
|
893
|
-
#
|
|
894
|
-
MAX_COMPLETION_ITEMS = 2000 # Increased from 1000 to accommodate more smart suggestions
|
|
895
|
-
if len(completion_words) > MAX_COMPLETION_ITEMS:
|
|
896
|
-
# Create a more advanced prioritization strategy
|
|
897
|
-
prioritized_words = []
|
|
898
|
-
|
|
899
|
-
# First, include all table names
|
|
900
|
-
tables = list(self.db_manager.loaded_tables.keys())
|
|
901
|
-
prioritized_words.extend(tables)
|
|
902
|
-
|
|
903
|
-
# Then add most common SQL keywords and patterns
|
|
904
|
-
sql_keywords = [w for w in completion_words if w.isupper() and len(w) > 1]
|
|
905
|
-
prioritized_words.extend(sql_keywords[:200]) # Cap at 200 keywords
|
|
906
|
-
|
|
907
|
-
# Add frequently used items
|
|
908
|
-
if hasattr(self, 'completion_usage'):
|
|
909
|
-
frequent_items = [
|
|
910
|
-
item for item, _ in sorted(
|
|
911
|
-
self.completion_usage.items(),
|
|
912
|
-
key=lambda x: x[1],
|
|
913
|
-
reverse=True
|
|
914
|
-
)[:100] # Top 100 most used
|
|
915
|
-
]
|
|
916
|
-
prioritized_words.extend(frequent_items)
|
|
917
|
-
|
|
918
|
-
# Add table.column patterns which are very useful
|
|
919
|
-
qualified_columns = [w for w in completion_words if '.' in w and w.split('.')[0] in tables]
|
|
920
|
-
prioritized_words.extend(qualified_columns[:300]) # Cap at 300 qualified columns
|
|
921
|
-
|
|
922
|
-
# Add common completion patterns
|
|
923
|
-
patterns = [w for w in completion_words if ' ' in w] # Spaces indicate phrases/patterns
|
|
924
|
-
prioritized_words.extend(patterns[:200]) # Cap at 200 patterns
|
|
925
|
-
|
|
926
|
-
# Finally add other columns
|
|
927
|
-
remaining_slots = MAX_COMPLETION_ITEMS - len(prioritized_words)
|
|
928
|
-
remaining_words = [
|
|
929
|
-
w for w in completion_words
|
|
930
|
-
if w not in prioritized_words
|
|
931
|
-
and not w.isupper()
|
|
932
|
-
and '.' not in w
|
|
933
|
-
and ' ' not in w
|
|
934
|
-
]
|
|
935
|
-
prioritized_words.extend(remaining_words[:remaining_slots])
|
|
936
|
-
|
|
937
|
-
# Remove duplicates while preserving order
|
|
938
|
-
seen = set()
|
|
939
|
-
completion_words = []
|
|
940
|
-
for item in prioritized_words:
|
|
941
|
-
if item not in seen:
|
|
942
|
-
seen.add(item)
|
|
943
|
-
completion_words.append(item)
|
|
944
|
-
|
|
945
|
-
# Ensure we don't exceed the maximum
|
|
946
|
-
completion_words = completion_words[:MAX_COMPLETION_ITEMS]
|
|
947
|
-
|
|
948
|
-
# Use a single model for all tabs to save memory and improve performance
|
|
586
|
+
# Create a single shared model for all tabs to save memory
|
|
949
587
|
model = QStringListModel(completion_words)
|
|
950
588
|
|
|
951
589
|
# Keep a reference to the model to prevent garbage collection
|
|
952
590
|
self._current_completer_model = model
|
|
953
591
|
|
|
954
|
-
#
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
592
|
+
# First unregister all existing editors to avoid duplicates
|
|
593
|
+
existing_editors = suggestion_mgr._editors.copy()
|
|
594
|
+
for editor_id in existing_editors:
|
|
595
|
+
suggestion_mgr.unregister_editor(editor_id)
|
|
596
|
+
|
|
597
|
+
# Register editors with the suggestion manager and update their completer models
|
|
598
|
+
for i in range(self.tab_widget.count()):
|
|
599
|
+
tab = self.tab_widget.widget(i)
|
|
600
|
+
if tab and hasattr(tab, 'query_edit'):
|
|
601
|
+
# Register this editor with the suggestion manager using a unique ID
|
|
602
|
+
editor_id = f"tab_{i}_{id(tab.query_edit)}"
|
|
603
|
+
suggestion_mgr.register_editor(tab.query_edit, editor_id)
|
|
604
|
+
|
|
605
|
+
# Update the basic completer model for backward compatibility
|
|
959
606
|
try:
|
|
960
|
-
|
|
607
|
+
tab.query_edit.update_completer_model(model)
|
|
961
608
|
except Exception as e:
|
|
962
|
-
self.statusBar().showMessage(f"Error updating
|
|
963
|
-
|
|
964
|
-
# Only schedule updates for additional tabs if we have more than 3 tabs
|
|
965
|
-
# This reduces overhead for common usage patterns
|
|
966
|
-
if self.tab_widget.count() > 1:
|
|
967
|
-
# Calculate a reasonable maximum delay (ms)
|
|
968
|
-
max_delay = min(500, 50 * self.tab_widget.count())
|
|
969
|
-
|
|
970
|
-
# Store timers to prevent garbage collection
|
|
971
|
-
if not hasattr(self, '_completer_timers'):
|
|
972
|
-
self._completer_timers = []
|
|
973
|
-
|
|
974
|
-
# Clear old timers
|
|
975
|
-
for timer in self._completer_timers:
|
|
976
|
-
if timer.isActive():
|
|
977
|
-
timer.stop()
|
|
978
|
-
self._completer_timers = []
|
|
979
|
-
|
|
980
|
-
# Schedule updates for other tabs with increasing delays
|
|
981
|
-
for i in range(self.tab_widget.count()):
|
|
982
|
-
if i != current_index:
|
|
983
|
-
tab = self.tab_widget.widget(i)
|
|
984
|
-
if tab and not tab.isHidden() and hasattr(tab, 'query_edit'):
|
|
985
|
-
delay = int((i + 1) / self.tab_widget.count() * max_delay)
|
|
986
|
-
|
|
987
|
-
timer = QTimer()
|
|
988
|
-
timer.setSingleShot(True)
|
|
989
|
-
# Store tab and model as local variables for the lambda
|
|
990
|
-
# to avoid closure issues
|
|
991
|
-
tab_ref = tab
|
|
992
|
-
model_ref = model
|
|
993
|
-
timer.timeout.connect(
|
|
994
|
-
lambda t=tab_ref, m=model_ref: self._update_tab_completer(t, m))
|
|
995
|
-
self._completer_timers.append(timer)
|
|
996
|
-
timer.start(delay)
|
|
609
|
+
self.statusBar().showMessage(f"Error updating completer for tab {i}: {str(e)}", 2000)
|
|
997
610
|
|
|
998
611
|
# Process events to keep UI responsive
|
|
999
612
|
QApplication.processEvents()
|
|
1000
613
|
|
|
1001
|
-
# Return True to indicate success
|
|
1002
614
|
return True
|
|
1003
615
|
|
|
1004
616
|
except Exception as e:
|
|
1005
617
|
# Catch any errors to prevent hanging
|
|
1006
618
|
self.statusBar().showMessage(f"Auto-completion update error: {str(e)}", 2000)
|
|
1007
619
|
return False
|
|
1008
|
-
|
|
1009
|
-
def _update_tab_completer(self, tab, model):
|
|
1010
|
-
"""Helper method to update a tab's completer with the given model"""
|
|
1011
|
-
if tab and not tab.isHidden() and hasattr(tab, 'query_edit'): # Only update visible tabs with query editors
|
|
1012
|
-
try:
|
|
1013
|
-
tab.query_edit.update_completer_model(model)
|
|
1014
|
-
QApplication.processEvents() # Keep UI responsive
|
|
1015
|
-
except Exception as e:
|
|
1016
|
-
print(f"Error updating tab completer: {e}")
|
|
1017
|
-
# Try a simpler approach as fallback
|
|
1018
|
-
try:
|
|
1019
|
-
if hasattr(tab.query_edit, 'all_sql_keywords'):
|
|
1020
|
-
fallback_model = QStringListModel(tab.query_edit.all_sql_keywords)
|
|
1021
|
-
tab.query_edit.completer.setModel(fallback_model)
|
|
1022
|
-
except Exception:
|
|
1023
|
-
pass # Last resort: ignore errors to prevent crashes
|
|
1024
620
|
|
|
1025
621
|
def execute_query(self):
|
|
1026
622
|
try:
|
|
@@ -1044,7 +640,16 @@ class SQLShell(QMainWindow):
|
|
|
1044
640
|
self.populate_table(result)
|
|
1045
641
|
self.statusBar().showMessage(f"Query executed successfully. Time: {execution_time:.2f}s. Rows: {len(result)}")
|
|
1046
642
|
|
|
1047
|
-
# Record query
|
|
643
|
+
# Record query for context-aware suggestions
|
|
644
|
+
try:
|
|
645
|
+
from sqlshell.suggester_integration import get_suggestion_manager
|
|
646
|
+
suggestion_mgr = get_suggestion_manager()
|
|
647
|
+
suggestion_mgr.record_query(query)
|
|
648
|
+
except Exception as e:
|
|
649
|
+
# Don't let suggestion errors affect query execution
|
|
650
|
+
print(f"Error recording query for suggestions: {e}")
|
|
651
|
+
|
|
652
|
+
# Record query in history and update completion usage (legacy)
|
|
1048
653
|
self._update_query_history(query)
|
|
1049
654
|
|
|
1050
655
|
except SyntaxError as e:
|
|
@@ -1145,7 +750,7 @@ class SQLShell(QMainWindow):
|
|
|
1145
750
|
|
|
1146
751
|
def show_table_preview(self, item):
|
|
1147
752
|
"""Show a preview of the selected table"""
|
|
1148
|
-
if not item:
|
|
753
|
+
if not item or self.tables_list.is_folder_item(item):
|
|
1149
754
|
return
|
|
1150
755
|
|
|
1151
756
|
# Get the current tab
|
|
@@ -1153,7 +758,15 @@ class SQLShell(QMainWindow):
|
|
|
1153
758
|
if not current_tab:
|
|
1154
759
|
return
|
|
1155
760
|
|
|
1156
|
-
table_name =
|
|
761
|
+
table_name = self.tables_list.get_table_name_from_item(item)
|
|
762
|
+
if not table_name:
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
# Check if this table needs to be reloaded first
|
|
766
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
767
|
+
# Reload the table immediately without asking
|
|
768
|
+
self.reload_selected_table(table_name)
|
|
769
|
+
|
|
1157
770
|
try:
|
|
1158
771
|
# Use the database manager to get a preview of the table
|
|
1159
772
|
preview_df = self.db_manager.get_table_preview(table_name)
|
|
@@ -1189,40 +802,59 @@ class SQLShell(QMainWindow):
|
|
|
1189
802
|
# Show loading indicator
|
|
1190
803
|
self.statusBar().showMessage('Generating test data...')
|
|
1191
804
|
|
|
1192
|
-
# Create
|
|
1193
|
-
|
|
805
|
+
# Create temporary directory for test data
|
|
806
|
+
temp_dir = tempfile.mkdtemp(prefix='sqlshell_test_')
|
|
1194
807
|
|
|
1195
808
|
# Generate test data
|
|
1196
809
|
sales_df = create_test_data.create_sales_data()
|
|
1197
810
|
customer_df = create_test_data.create_customer_data()
|
|
1198
811
|
product_df = create_test_data.create_product_data()
|
|
812
|
+
large_numbers_df = create_test_data.create_large_numbers_data()
|
|
1199
813
|
|
|
1200
|
-
# Save test data
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
814
|
+
# Save test data to temporary directory
|
|
815
|
+
sales_path = os.path.join(temp_dir, 'sample_sales_data.xlsx')
|
|
816
|
+
customer_path = os.path.join(temp_dir, 'customer_data.parquet')
|
|
817
|
+
product_path = os.path.join(temp_dir, 'product_catalog.xlsx')
|
|
818
|
+
large_numbers_path = os.path.join(temp_dir, 'large_numbers.xlsx')
|
|
819
|
+
|
|
820
|
+
sales_df.to_excel(sales_path, index=False)
|
|
821
|
+
customer_df.to_parquet(customer_path, index=False)
|
|
822
|
+
product_df.to_excel(product_path, index=False)
|
|
823
|
+
large_numbers_df.to_excel(large_numbers_path, index=False)
|
|
1204
824
|
|
|
1205
825
|
# Register the tables in the database manager
|
|
1206
|
-
self.db_manager.register_dataframe(sales_df, 'sample_sales_data',
|
|
1207
|
-
self.db_manager.register_dataframe(product_df, 'product_catalog',
|
|
1208
|
-
self.db_manager.register_dataframe(customer_df, 'customer_data',
|
|
826
|
+
self.db_manager.register_dataframe(sales_df, 'sample_sales_data', sales_path)
|
|
827
|
+
self.db_manager.register_dataframe(product_df, 'product_catalog', product_path)
|
|
828
|
+
self.db_manager.register_dataframe(customer_df, 'customer_data', customer_path)
|
|
829
|
+
self.db_manager.register_dataframe(large_numbers_df, 'large_numbers', large_numbers_path)
|
|
1209
830
|
|
|
1210
831
|
# Update UI
|
|
1211
832
|
self.tables_list.clear()
|
|
1212
833
|
for table_name, file_path in self.db_manager.loaded_tables.items():
|
|
1213
|
-
|
|
834
|
+
# Use the new add_table_item method
|
|
835
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_path))
|
|
1214
836
|
|
|
1215
837
|
# Set the sample query in the current tab
|
|
1216
838
|
current_tab = self.get_current_tab()
|
|
1217
839
|
if current_tab:
|
|
1218
840
|
sample_query = """
|
|
841
|
+
-- Example query with tables containing large numbers
|
|
1219
842
|
SELECT
|
|
1220
|
-
|
|
1221
|
-
|
|
843
|
+
ln.ID,
|
|
844
|
+
ln.Category,
|
|
845
|
+
ln.MediumValue,
|
|
846
|
+
ln.LargeValue,
|
|
847
|
+
ln.VeryLargeValue,
|
|
848
|
+
ln.MassiveValue,
|
|
849
|
+
ln.ExponentialValue,
|
|
850
|
+
ln.Revenue,
|
|
851
|
+
ln.Budget
|
|
1222
852
|
FROM
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
853
|
+
large_numbers ln
|
|
854
|
+
WHERE
|
|
855
|
+
ln.LargeValue > 5000000000000
|
|
856
|
+
ORDER BY
|
|
857
|
+
ln.MassiveValue DESC
|
|
1226
858
|
LIMIT 10
|
|
1227
859
|
"""
|
|
1228
860
|
current_tab.set_query_text(sample_query.strip())
|
|
@@ -1233,8 +865,10 @@ LIMIT 10
|
|
|
1233
865
|
# Show success message
|
|
1234
866
|
self.statusBar().showMessage('Test data loaded successfully')
|
|
1235
867
|
|
|
1236
|
-
# Show a preview of the
|
|
1237
|
-
self.
|
|
868
|
+
# Show a preview of the large numbers data
|
|
869
|
+
large_numbers_item = self.tables_list.find_table_item("large_numbers")
|
|
870
|
+
if large_numbers_item:
|
|
871
|
+
self.show_table_preview(large_numbers_item)
|
|
1238
872
|
|
|
1239
873
|
except Exception as e:
|
|
1240
874
|
self.statusBar().showMessage(f'Error loading test data: {str(e)}')
|
|
@@ -1280,8 +914,8 @@ LIMIT 10
|
|
|
1280
914
|
self.db_manager.loaded_tables[table_name] = file_name
|
|
1281
915
|
self.db_manager.table_columns[table_name] = df.columns.tolist()
|
|
1282
916
|
|
|
1283
|
-
# Update UI
|
|
1284
|
-
self.tables_list.
|
|
917
|
+
# Update UI using new method
|
|
918
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_name))
|
|
1285
919
|
self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
|
|
1286
920
|
|
|
1287
921
|
# Update completer with new table and column names
|
|
@@ -1338,8 +972,8 @@ LIMIT 10
|
|
|
1338
972
|
self.db_manager.loaded_tables[table_name] = file_name
|
|
1339
973
|
self.db_manager.table_columns[table_name] = df.columns.tolist()
|
|
1340
974
|
|
|
1341
|
-
# Update UI
|
|
1342
|
-
self.tables_list.
|
|
975
|
+
# Update UI using new method
|
|
976
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_name))
|
|
1343
977
|
self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
|
|
1344
978
|
|
|
1345
979
|
# Update completer with new table and column names
|
|
@@ -1357,7 +991,7 @@ LIMIT 10
|
|
|
1357
991
|
self.statusBar().showMessage('Error exporting data')
|
|
1358
992
|
|
|
1359
993
|
def get_table_data_as_dataframe(self):
|
|
1360
|
-
"""Helper function to convert table widget data to a DataFrame"""
|
|
994
|
+
"""Helper function to convert table widget data to a DataFrame with proper data types"""
|
|
1361
995
|
# Get the current tab
|
|
1362
996
|
current_tab = self.get_current_tab()
|
|
1363
997
|
if not current_tab:
|
|
@@ -1371,7 +1005,81 @@ LIMIT 10
|
|
|
1371
1005
|
item = current_tab.results_table.item(row, column)
|
|
1372
1006
|
row_data.append(item.text() if item else '')
|
|
1373
1007
|
data.append(row_data)
|
|
1374
|
-
|
|
1008
|
+
|
|
1009
|
+
# Create DataFrame from raw string data
|
|
1010
|
+
df_raw = pd.DataFrame(data, columns=headers)
|
|
1011
|
+
|
|
1012
|
+
# Try to use the original dataframe's dtypes if available
|
|
1013
|
+
if hasattr(current_tab, 'current_df') and current_tab.current_df is not None:
|
|
1014
|
+
original_df = current_tab.current_df
|
|
1015
|
+
# Since we might have filtered rows, we can't just return the original DataFrame
|
|
1016
|
+
# But we can use its column types to convert our string data appropriately
|
|
1017
|
+
|
|
1018
|
+
# Create a new DataFrame with appropriate types
|
|
1019
|
+
df_typed = pd.DataFrame()
|
|
1020
|
+
|
|
1021
|
+
for col in df_raw.columns:
|
|
1022
|
+
if col in original_df.columns:
|
|
1023
|
+
# Get the original column type
|
|
1024
|
+
orig_type = original_df[col].dtype
|
|
1025
|
+
|
|
1026
|
+
# Special handling for different data types
|
|
1027
|
+
if pd.api.types.is_numeric_dtype(orig_type):
|
|
1028
|
+
# Handle numeric columns (int or float)
|
|
1029
|
+
try:
|
|
1030
|
+
# First try to convert to numeric type
|
|
1031
|
+
# Remove commas used for thousands separators
|
|
1032
|
+
numeric_col = pd.to_numeric(df_raw[col].str.replace(',', '').replace('NULL', np.nan))
|
|
1033
|
+
df_typed[col] = numeric_col
|
|
1034
|
+
except:
|
|
1035
|
+
# If that fails, keep the original string
|
|
1036
|
+
df_typed[col] = df_raw[col]
|
|
1037
|
+
elif pd.api.types.is_datetime64_dtype(orig_type):
|
|
1038
|
+
# Handle datetime columns
|
|
1039
|
+
try:
|
|
1040
|
+
df_typed[col] = pd.to_datetime(df_raw[col].replace('NULL', np.nan))
|
|
1041
|
+
except:
|
|
1042
|
+
df_typed[col] = df_raw[col]
|
|
1043
|
+
elif pd.api.types.is_bool_dtype(orig_type):
|
|
1044
|
+
# Handle boolean columns
|
|
1045
|
+
try:
|
|
1046
|
+
df_typed[col] = df_raw[col].map({'True': True, 'False': False}).replace('NULL', np.nan)
|
|
1047
|
+
except:
|
|
1048
|
+
df_typed[col] = df_raw[col]
|
|
1049
|
+
else:
|
|
1050
|
+
# For other types, keep as is
|
|
1051
|
+
df_typed[col] = df_raw[col]
|
|
1052
|
+
else:
|
|
1053
|
+
# For columns not in the original dataframe, infer type
|
|
1054
|
+
df_typed[col] = df_raw[col]
|
|
1055
|
+
|
|
1056
|
+
return df_typed
|
|
1057
|
+
|
|
1058
|
+
else:
|
|
1059
|
+
# If we don't have the original dataframe, try to infer types
|
|
1060
|
+
# First replace 'NULL' with actual NaN
|
|
1061
|
+
df_raw.replace('NULL', np.nan, inplace=True)
|
|
1062
|
+
|
|
1063
|
+
# Try to convert each column to numeric if possible
|
|
1064
|
+
for col in df_raw.columns:
|
|
1065
|
+
try:
|
|
1066
|
+
# First try to convert to numeric by removing commas
|
|
1067
|
+
df_raw[col] = pd.to_numeric(df_raw[col].str.replace(',', ''))
|
|
1068
|
+
except:
|
|
1069
|
+
# If that fails, try to convert to datetime
|
|
1070
|
+
try:
|
|
1071
|
+
df_raw[col] = pd.to_datetime(df_raw[col])
|
|
1072
|
+
except:
|
|
1073
|
+
# If both numeric and datetime conversions fail,
|
|
1074
|
+
# try boolean conversion for True/False strings
|
|
1075
|
+
try:
|
|
1076
|
+
if df_raw[col].dropna().isin(['True', 'False']).all():
|
|
1077
|
+
df_raw[col] = df_raw[col].map({'True': True, 'False': False})
|
|
1078
|
+
except:
|
|
1079
|
+
# Otherwise, keep as is
|
|
1080
|
+
pass
|
|
1081
|
+
|
|
1082
|
+
return df_raw
|
|
1375
1083
|
|
|
1376
1084
|
def keyPressEvent(self, event):
|
|
1377
1085
|
"""Handle global keyboard shortcuts"""
|
|
@@ -1400,6 +1108,13 @@ LIMIT 10
|
|
|
1400
1108
|
self.rename_current_tab()
|
|
1401
1109
|
return
|
|
1402
1110
|
|
|
1111
|
+
# Show quick access menu with Ctrl+Shift+O
|
|
1112
|
+
if (event.key() == Qt.Key.Key_O and
|
|
1113
|
+
(event.modifiers() & Qt.KeyboardModifier.ControlModifier) and
|
|
1114
|
+
(event.modifiers() & Qt.KeyboardModifier.ShiftModifier)):
|
|
1115
|
+
self.show_quick_access_menu()
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1403
1118
|
super().keyPressEvent(event)
|
|
1404
1119
|
|
|
1405
1120
|
def closeEvent(self, event):
|
|
@@ -1489,7 +1204,9 @@ LIMIT 10
|
|
|
1489
1204
|
def show_tables_context_menu(self, position):
|
|
1490
1205
|
"""Show context menu for tables list"""
|
|
1491
1206
|
item = self.tables_list.itemAt(position)
|
|
1492
|
-
|
|
1207
|
+
|
|
1208
|
+
# If no item or it's a folder, let the tree widget handle it
|
|
1209
|
+
if not item or self.tables_list.is_folder_item(item):
|
|
1493
1210
|
return
|
|
1494
1211
|
|
|
1495
1212
|
# Get current tab
|
|
@@ -1498,28 +1215,54 @@ LIMIT 10
|
|
|
1498
1215
|
return
|
|
1499
1216
|
|
|
1500
1217
|
# Get table name without the file info in parentheses
|
|
1501
|
-
table_name =
|
|
1218
|
+
table_name = self.tables_list.get_table_name_from_item(item)
|
|
1219
|
+
if not table_name:
|
|
1220
|
+
return
|
|
1502
1221
|
|
|
1503
1222
|
# Create context menu
|
|
1504
1223
|
context_menu = QMenu(self)
|
|
1505
|
-
context_menu.setStyleSheet(
|
|
1506
|
-
QMenu {
|
|
1507
|
-
background-color: white;
|
|
1508
|
-
border: 1px solid #BDC3C7;
|
|
1509
|
-
padding: 5px;
|
|
1510
|
-
}
|
|
1511
|
-
QMenu::item {
|
|
1512
|
-
padding: 5px 20px;
|
|
1513
|
-
}
|
|
1514
|
-
QMenu::item:selected {
|
|
1515
|
-
background-color: #3498DB;
|
|
1516
|
-
color: white;
|
|
1517
|
-
}
|
|
1518
|
-
""")
|
|
1224
|
+
context_menu.setStyleSheet(get_context_menu_stylesheet())
|
|
1519
1225
|
|
|
1520
1226
|
# Add menu actions
|
|
1521
1227
|
select_from_action = context_menu.addAction("Select from")
|
|
1522
1228
|
add_to_editor_action = context_menu.addAction("Just add to editor")
|
|
1229
|
+
|
|
1230
|
+
# Add entropy profiler action
|
|
1231
|
+
context_menu.addSeparator()
|
|
1232
|
+
analyze_entropy_action = context_menu.addAction("Analyze Column Importance")
|
|
1233
|
+
analyze_entropy_action.setIcon(QIcon.fromTheme("system-search"))
|
|
1234
|
+
|
|
1235
|
+
# Add table profiler action
|
|
1236
|
+
profile_table_action = context_menu.addAction("Profile Table Structure")
|
|
1237
|
+
profile_table_action.setIcon(QIcon.fromTheme("edit-find"))
|
|
1238
|
+
|
|
1239
|
+
# Check if table needs reloading and add appropriate action
|
|
1240
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
1241
|
+
reload_action = context_menu.addAction("Reload Table")
|
|
1242
|
+
reload_action.setIcon(QIcon.fromTheme("view-refresh"))
|
|
1243
|
+
else:
|
|
1244
|
+
reload_action = context_menu.addAction("Refresh")
|
|
1245
|
+
reload_action.setIcon(QIcon.fromTheme("view-refresh"))
|
|
1246
|
+
|
|
1247
|
+
# Add move to folder submenu
|
|
1248
|
+
move_menu = context_menu.addMenu("Move to Folder")
|
|
1249
|
+
move_menu.setIcon(QIcon.fromTheme("folder"))
|
|
1250
|
+
|
|
1251
|
+
# Add "New Folder" option to move menu
|
|
1252
|
+
new_folder_action = move_menu.addAction("New Folder...")
|
|
1253
|
+
move_menu.addSeparator()
|
|
1254
|
+
|
|
1255
|
+
# Add folders to the move menu
|
|
1256
|
+
for i in range(self.tables_list.topLevelItemCount()):
|
|
1257
|
+
top_item = self.tables_list.topLevelItem(i)
|
|
1258
|
+
if self.tables_list.is_folder_item(top_item):
|
|
1259
|
+
folder_action = move_menu.addAction(top_item.text(0))
|
|
1260
|
+
folder_action.setData(top_item)
|
|
1261
|
+
|
|
1262
|
+
# Add root option
|
|
1263
|
+
move_menu.addSeparator()
|
|
1264
|
+
root_action = move_menu.addAction("Root (No Folder)")
|
|
1265
|
+
|
|
1523
1266
|
context_menu.addSeparator()
|
|
1524
1267
|
rename_action = context_menu.addAction("Rename table...")
|
|
1525
1268
|
delete_action = context_menu.addAction("Delete table")
|
|
@@ -1529,6 +1272,11 @@ LIMIT 10
|
|
|
1529
1272
|
action = context_menu.exec(self.tables_list.mapToGlobal(position))
|
|
1530
1273
|
|
|
1531
1274
|
if action == select_from_action:
|
|
1275
|
+
# Check if table needs reloading first
|
|
1276
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
1277
|
+
# Reload the table immediately without asking
|
|
1278
|
+
self.reload_selected_table(table_name)
|
|
1279
|
+
|
|
1532
1280
|
# Insert "SELECT * FROM table_name" at cursor position
|
|
1533
1281
|
cursor = current_tab.query_edit.textCursor()
|
|
1534
1282
|
cursor.insertText(f"SELECT * FROM {table_name}")
|
|
@@ -1538,6 +1286,14 @@ LIMIT 10
|
|
|
1538
1286
|
cursor = current_tab.query_edit.textCursor()
|
|
1539
1287
|
cursor.insertText(table_name)
|
|
1540
1288
|
current_tab.query_edit.setFocus()
|
|
1289
|
+
elif action == reload_action:
|
|
1290
|
+
self.reload_selected_table(table_name)
|
|
1291
|
+
elif action == analyze_entropy_action:
|
|
1292
|
+
# Call the entropy analysis method
|
|
1293
|
+
self.analyze_table_entropy(table_name)
|
|
1294
|
+
elif action == profile_table_action:
|
|
1295
|
+
# Call the table profile method
|
|
1296
|
+
self.profile_table_structure(table_name)
|
|
1541
1297
|
elif action == rename_action:
|
|
1542
1298
|
# Show rename dialog
|
|
1543
1299
|
new_name, ok = QInputDialog.getText(
|
|
@@ -1550,8 +1306,8 @@ LIMIT 10
|
|
|
1550
1306
|
if ok and new_name:
|
|
1551
1307
|
if self.rename_table(table_name, new_name):
|
|
1552
1308
|
# Update the item text
|
|
1553
|
-
source = item.text().split(' (')[1][:-1] # Get the source part
|
|
1554
|
-
item.setText(f"{new_name} ({source})")
|
|
1309
|
+
source = item.text(0).split(' (')[1][:-1] # Get the source part
|
|
1310
|
+
item.setText(0, f"{new_name} ({source})")
|
|
1555
1311
|
self.statusBar().showMessage(f'Table renamed to "{new_name}"')
|
|
1556
1312
|
elif action == delete_action:
|
|
1557
1313
|
# Show confirmation dialog
|
|
@@ -1563,17 +1319,91 @@ LIMIT 10
|
|
|
1563
1319
|
)
|
|
1564
1320
|
if reply == QMessageBox.StandardButton.Yes:
|
|
1565
1321
|
self.remove_selected_table()
|
|
1322
|
+
elif action == new_folder_action:
|
|
1323
|
+
# Create a new folder and move the table there
|
|
1324
|
+
folder_name, ok = QInputDialog.getText(
|
|
1325
|
+
self,
|
|
1326
|
+
"New Folder",
|
|
1327
|
+
"Enter folder name:",
|
|
1328
|
+
QLineEdit.EchoMode.Normal
|
|
1329
|
+
)
|
|
1330
|
+
if ok and folder_name:
|
|
1331
|
+
folder = self.tables_list.create_folder(folder_name)
|
|
1332
|
+
self.tables_list.move_item_to_folder(item, folder)
|
|
1333
|
+
self.statusBar().showMessage(f'Moved table "{table_name}" to folder "{folder_name}"')
|
|
1334
|
+
elif action == root_action:
|
|
1335
|
+
# Move table to root (remove from any folder)
|
|
1336
|
+
parent = item.parent()
|
|
1337
|
+
if parent and self.tables_list.is_folder_item(parent):
|
|
1338
|
+
# Create a clone at root level
|
|
1339
|
+
source = item.text(0).split(' (')[1][:-1] # Get the source part
|
|
1340
|
+
needs_reload = table_name in self.tables_list.tables_needing_reload
|
|
1341
|
+
# Remove from current parent
|
|
1342
|
+
parent.removeChild(item)
|
|
1343
|
+
# Add to root
|
|
1344
|
+
self.tables_list.add_table_item(table_name, source, needs_reload)
|
|
1345
|
+
self.statusBar().showMessage(f'Moved table "{table_name}" to root')
|
|
1346
|
+
elif action and action.parent() == move_menu:
|
|
1347
|
+
# Move to selected folder
|
|
1348
|
+
target_folder = action.data()
|
|
1349
|
+
if target_folder:
|
|
1350
|
+
self.tables_list.move_item_to_folder(item, target_folder)
|
|
1351
|
+
self.statusBar().showMessage(f'Moved table "{table_name}" to folder "{target_folder.text(0)}"')
|
|
1566
1352
|
|
|
1567
|
-
def
|
|
1353
|
+
def reload_selected_table(self, table_name=None):
|
|
1354
|
+
"""Reload the data for a table from its source file"""
|
|
1355
|
+
try:
|
|
1356
|
+
# If table_name is not provided, get it from the selected item
|
|
1357
|
+
if not table_name:
|
|
1358
|
+
current_item = self.tables_list.currentItem()
|
|
1359
|
+
if not current_item:
|
|
1360
|
+
return
|
|
1361
|
+
table_name = self.tables_list.get_table_name_from_item(current_item)
|
|
1362
|
+
|
|
1363
|
+
# Show a loading indicator
|
|
1364
|
+
self.statusBar().showMessage(f'Reloading table "{table_name}"...')
|
|
1365
|
+
|
|
1366
|
+
# Use the database manager to reload the table
|
|
1367
|
+
success, message = self.db_manager.reload_table(table_name)
|
|
1368
|
+
|
|
1369
|
+
if success:
|
|
1370
|
+
# Show success message
|
|
1371
|
+
self.statusBar().showMessage(message)
|
|
1372
|
+
|
|
1373
|
+
# Update completer with any new column names
|
|
1374
|
+
self.update_completer()
|
|
1375
|
+
|
|
1376
|
+
# Mark the table as reloaded (remove the reload icon)
|
|
1377
|
+
self.tables_list.mark_table_reloaded(table_name)
|
|
1378
|
+
|
|
1379
|
+
# Show a preview of the reloaded table
|
|
1380
|
+
table_item = self.tables_list.find_table_item(table_name)
|
|
1381
|
+
if table_item:
|
|
1382
|
+
self.show_table_preview(table_item)
|
|
1383
|
+
else:
|
|
1384
|
+
# Show error message
|
|
1385
|
+
QMessageBox.warning(self, "Reload Failed", message)
|
|
1386
|
+
self.statusBar().showMessage(f'Failed to reload table: {message}')
|
|
1387
|
+
|
|
1388
|
+
except Exception as e:
|
|
1389
|
+
QMessageBox.critical(self, "Error",
|
|
1390
|
+
f"Error reloading table:\n\n{str(e)}")
|
|
1391
|
+
self.statusBar().showMessage('Error reloading table')
|
|
1392
|
+
|
|
1393
|
+
def new_project(self, skip_confirmation=False):
|
|
1568
1394
|
"""Create a new project by clearing current state"""
|
|
1569
|
-
if self.db_manager.is_connected():
|
|
1395
|
+
if self.db_manager.is_connected() and not skip_confirmation:
|
|
1570
1396
|
reply = QMessageBox.question(self, 'New Project',
|
|
1571
|
-
|
|
1572
|
-
|
|
1397
|
+
'Are you sure you want to start a new project? All unsaved changes will be lost.',
|
|
1398
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
1573
1399
|
if reply == QMessageBox.StandardButton.Yes:
|
|
1574
1400
|
# Close existing connection
|
|
1575
1401
|
self.db_manager.close_connection()
|
|
1576
1402
|
|
|
1403
|
+
# Clear all database tracking
|
|
1404
|
+
self.db_manager.loaded_tables = {}
|
|
1405
|
+
self.db_manager.table_columns = {}
|
|
1406
|
+
|
|
1577
1407
|
# Reset state
|
|
1578
1408
|
self.tables_list.clear()
|
|
1579
1409
|
|
|
@@ -1594,6 +1424,34 @@ LIMIT 10
|
|
|
1594
1424
|
self.setWindowTitle('SQL Shell')
|
|
1595
1425
|
self.db_info_label.setText("No database connected")
|
|
1596
1426
|
self.statusBar().showMessage('New project created')
|
|
1427
|
+
elif skip_confirmation:
|
|
1428
|
+
# Skip confirmation and just clear everything
|
|
1429
|
+
if self.db_manager.is_connected():
|
|
1430
|
+
self.db_manager.close_connection()
|
|
1431
|
+
|
|
1432
|
+
# Clear all database tracking
|
|
1433
|
+
self.db_manager.loaded_tables = {}
|
|
1434
|
+
self.db_manager.table_columns = {}
|
|
1435
|
+
|
|
1436
|
+
# Reset state
|
|
1437
|
+
self.tables_list.clear()
|
|
1438
|
+
|
|
1439
|
+
# Clear all tabs except one
|
|
1440
|
+
while self.tab_widget.count() > 1:
|
|
1441
|
+
self.close_tab(1) # Always close tab at index 1 to keep at least one tab
|
|
1442
|
+
|
|
1443
|
+
# Clear the remaining tab
|
|
1444
|
+
first_tab = self.get_tab_at_index(0)
|
|
1445
|
+
if first_tab:
|
|
1446
|
+
first_tab.set_query_text("")
|
|
1447
|
+
first_tab.results_table.setRowCount(0)
|
|
1448
|
+
first_tab.results_table.setColumnCount(0)
|
|
1449
|
+
first_tab.row_count_label.setText("")
|
|
1450
|
+
first_tab.results_title.setText("RESULTS")
|
|
1451
|
+
|
|
1452
|
+
self.current_project_file = None
|
|
1453
|
+
self.setWindowTitle('SQL Shell')
|
|
1454
|
+
self.db_info_label.setText("No database connected")
|
|
1597
1455
|
|
|
1598
1456
|
def save_project(self):
|
|
1599
1457
|
"""Save the current project"""
|
|
@@ -1634,6 +1492,7 @@ LIMIT 10
|
|
|
1634
1492
|
|
|
1635
1493
|
project_data = {
|
|
1636
1494
|
'tables': {},
|
|
1495
|
+
'folders': {},
|
|
1637
1496
|
'tabs': tabs_data,
|
|
1638
1497
|
'connection_type': self.db_manager.connection_type,
|
|
1639
1498
|
'database_path': None # Initialize to None
|
|
@@ -1643,8 +1502,51 @@ LIMIT 10
|
|
|
1643
1502
|
if self.db_manager.is_connected() and hasattr(self.db_manager, 'database_path'):
|
|
1644
1503
|
project_data['database_path'] = self.db_manager.database_path
|
|
1645
1504
|
|
|
1646
|
-
#
|
|
1647
|
-
|
|
1505
|
+
# Helper function to recursively save folder structure
|
|
1506
|
+
def save_folder_structure(parent_item, parent_path=""):
|
|
1507
|
+
if parent_item is None:
|
|
1508
|
+
# Handle top-level items
|
|
1509
|
+
for i in range(self.tables_list.topLevelItemCount()):
|
|
1510
|
+
item = self.tables_list.topLevelItem(i)
|
|
1511
|
+
if self.tables_list.is_folder_item(item):
|
|
1512
|
+
# It's a folder - add to folders and process its children
|
|
1513
|
+
folder_name = item.text(0)
|
|
1514
|
+
folder_id = f"folder_{i}"
|
|
1515
|
+
project_data['folders'][folder_id] = {
|
|
1516
|
+
'name': folder_name,
|
|
1517
|
+
'parent': None,
|
|
1518
|
+
'expanded': item.isExpanded()
|
|
1519
|
+
}
|
|
1520
|
+
save_folder_structure(item, folder_id)
|
|
1521
|
+
else:
|
|
1522
|
+
# It's a table - add to tables at root level
|
|
1523
|
+
save_table_item(item)
|
|
1524
|
+
else:
|
|
1525
|
+
# Process children of this folder
|
|
1526
|
+
for i in range(parent_item.childCount()):
|
|
1527
|
+
child = parent_item.child(i)
|
|
1528
|
+
if self.tables_list.is_folder_item(child):
|
|
1529
|
+
# It's a subfolder
|
|
1530
|
+
folder_name = child.text(0)
|
|
1531
|
+
folder_id = f"{parent_path}_sub_{i}"
|
|
1532
|
+
project_data['folders'][folder_id] = {
|
|
1533
|
+
'name': folder_name,
|
|
1534
|
+
'parent': parent_path,
|
|
1535
|
+
'expanded': child.isExpanded()
|
|
1536
|
+
}
|
|
1537
|
+
save_folder_structure(child, folder_id)
|
|
1538
|
+
else:
|
|
1539
|
+
# It's a table in this folder
|
|
1540
|
+
save_table_item(child, parent_path)
|
|
1541
|
+
|
|
1542
|
+
# Helper function to save table item
|
|
1543
|
+
def save_table_item(item, folder_id=None):
|
|
1544
|
+
table_name = self.tables_list.get_table_name_from_item(item)
|
|
1545
|
+
if not table_name or table_name not in self.db_manager.loaded_tables:
|
|
1546
|
+
return
|
|
1547
|
+
|
|
1548
|
+
file_path = self.db_manager.loaded_tables[table_name]
|
|
1549
|
+
|
|
1648
1550
|
# For database tables and query results, store the special identifier
|
|
1649
1551
|
if file_path in ['database', 'query_result']:
|
|
1650
1552
|
source_path = file_path
|
|
@@ -1654,9 +1556,13 @@ LIMIT 10
|
|
|
1654
1556
|
|
|
1655
1557
|
project_data['tables'][table_name] = {
|
|
1656
1558
|
'file_path': source_path,
|
|
1657
|
-
'columns': self.db_manager.table_columns.get(table_name, [])
|
|
1559
|
+
'columns': self.db_manager.table_columns.get(table_name, []),
|
|
1560
|
+
'folder': folder_id
|
|
1658
1561
|
}
|
|
1659
1562
|
|
|
1563
|
+
# Save the folder structure
|
|
1564
|
+
save_folder_structure(None)
|
|
1565
|
+
|
|
1660
1566
|
with open(file_name, 'w') as f:
|
|
1661
1567
|
json.dump(project_data, f, indent=4)
|
|
1662
1568
|
|
|
@@ -1672,6 +1578,20 @@ LIMIT 10
|
|
|
1672
1578
|
def open_project(self, file_name=None):
|
|
1673
1579
|
"""Open a project file"""
|
|
1674
1580
|
if not file_name:
|
|
1581
|
+
# Check for unsaved changes before showing file dialog
|
|
1582
|
+
if self.has_unsaved_changes():
|
|
1583
|
+
reply = QMessageBox.question(self, 'Save Changes',
|
|
1584
|
+
'Do you want to save your changes before opening another project?',
|
|
1585
|
+
QMessageBox.StandardButton.Save |
|
|
1586
|
+
QMessageBox.StandardButton.Discard |
|
|
1587
|
+
QMessageBox.StandardButton.Cancel)
|
|
1588
|
+
|
|
1589
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
1590
|
+
self.save_project()
|
|
1591
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
1592
|
+
return
|
|
1593
|
+
|
|
1594
|
+
# Show file dialog after handling save prompt
|
|
1675
1595
|
file_name, _ = QFileDialog.getOpenFileName(
|
|
1676
1596
|
self,
|
|
1677
1597
|
"Open Project",
|
|
@@ -1697,46 +1617,53 @@ LIMIT 10
|
|
|
1697
1617
|
QApplication.processEvents()
|
|
1698
1618
|
|
|
1699
1619
|
# Start fresh
|
|
1700
|
-
self.new_project()
|
|
1620
|
+
self.new_project(skip_confirmation=True)
|
|
1701
1621
|
progress.setValue(15)
|
|
1702
1622
|
QApplication.processEvents()
|
|
1703
1623
|
|
|
1624
|
+
# Make sure all database tables are cleared from tracking
|
|
1625
|
+
self.db_manager.loaded_tables = {}
|
|
1626
|
+
self.db_manager.table_columns = {}
|
|
1627
|
+
|
|
1704
1628
|
# Check if there's a database path in the project
|
|
1705
1629
|
has_database_path = 'database_path' in project_data and project_data['database_path']
|
|
1706
1630
|
has_database_tables = any(table_info.get('file_path') == 'database'
|
|
1707
1631
|
for table_info in project_data.get('tables', {}).values())
|
|
1708
1632
|
|
|
1709
|
-
#
|
|
1633
|
+
# Connect to database if needed
|
|
1634
|
+
progress.setLabelText("Connecting to database...")
|
|
1710
1635
|
database_tables_loaded = False
|
|
1636
|
+
database_connection_message = None
|
|
1711
1637
|
|
|
1712
|
-
# If the project contains database tables and a database path, try to connect to it
|
|
1713
|
-
progress.setLabelText("Connecting to database...")
|
|
1714
1638
|
if has_database_path and has_database_tables:
|
|
1715
1639
|
database_path = project_data['database_path']
|
|
1716
1640
|
try:
|
|
1717
1641
|
if os.path.exists(database_path):
|
|
1718
1642
|
# Connect to the database
|
|
1719
|
-
self.db_manager.open_database(database_path)
|
|
1643
|
+
self.db_manager.open_database(database_path, load_all_tables=False)
|
|
1720
1644
|
self.db_info_label.setText(self.db_manager.get_connection_info())
|
|
1721
1645
|
self.statusBar().showMessage(f"Connected to database: {database_path}")
|
|
1722
1646
|
|
|
1723
|
-
# Add all database tables to the tables list
|
|
1724
|
-
for table_name, source in self.db_manager.loaded_tables.items():
|
|
1725
|
-
if source == 'database':
|
|
1726
|
-
self.tables_list.addItem(f"{table_name} (database)")
|
|
1727
|
-
|
|
1728
1647
|
# Mark database tables as loaded
|
|
1729
1648
|
database_tables_loaded = True
|
|
1730
1649
|
else:
|
|
1731
1650
|
database_tables_loaded = False
|
|
1732
|
-
|
|
1651
|
+
# Store the message instead of showing immediately
|
|
1652
|
+
database_connection_message = (
|
|
1653
|
+
"Database Not Found",
|
|
1733
1654
|
f"The project's database file was not found at:\n{database_path}\n\n"
|
|
1734
|
-
"Database tables will be shown but not accessible until you reconnect to the database
|
|
1655
|
+
"Database tables will be shown but not accessible until you reconnect to the database.\n\n"
|
|
1656
|
+
"Use the 'Open Database' button to connect to your database file."
|
|
1657
|
+
)
|
|
1735
1658
|
except Exception as e:
|
|
1736
1659
|
database_tables_loaded = False
|
|
1737
|
-
|
|
1660
|
+
# Store the message instead of showing immediately
|
|
1661
|
+
database_connection_message = (
|
|
1662
|
+
"Database Connection Error",
|
|
1738
1663
|
f"Failed to connect to the project's database:\n{str(e)}\n\n"
|
|
1739
|
-
"Database tables will be shown but not accessible until you reconnect to the database
|
|
1664
|
+
"Database tables will be shown but not accessible until you reconnect to the database.\n\n"
|
|
1665
|
+
"Use the 'Open Database' button to connect to your database file."
|
|
1666
|
+
)
|
|
1740
1667
|
else:
|
|
1741
1668
|
# Create connection if needed (we don't have a specific database to connect to)
|
|
1742
1669
|
database_tables_loaded = False
|
|
@@ -1745,11 +1672,52 @@ LIMIT 10
|
|
|
1745
1672
|
self.db_info_label.setText(connection_info)
|
|
1746
1673
|
elif 'connection_type' in project_data and project_data['connection_type'] != self.db_manager.connection_type:
|
|
1747
1674
|
# If connected but with a different database type than what was saved in the project
|
|
1748
|
-
|
|
1675
|
+
# Store the message instead of showing immediately
|
|
1676
|
+
database_connection_message = (
|
|
1677
|
+
"Database Type Mismatch",
|
|
1749
1678
|
f"The project was saved with a {project_data['connection_type']} database, but you're currently using {self.db_manager.connection_type}.\n\n"
|
|
1750
|
-
"Some database-specific features may not work correctly. Consider reconnecting to the correct database type."
|
|
1679
|
+
"Some database-specific features may not work correctly. Consider reconnecting to the correct database type."
|
|
1680
|
+
)
|
|
1751
1681
|
|
|
1752
|
-
progress.setValue(
|
|
1682
|
+
progress.setValue(20)
|
|
1683
|
+
QApplication.processEvents()
|
|
1684
|
+
|
|
1685
|
+
# First, recreate the folder structure
|
|
1686
|
+
folder_items = {} # Store folder items by ID
|
|
1687
|
+
|
|
1688
|
+
# Create folders first
|
|
1689
|
+
if 'folders' in project_data:
|
|
1690
|
+
progress.setLabelText("Creating folders...")
|
|
1691
|
+
# First pass: create top-level folders
|
|
1692
|
+
for folder_id, folder_info in project_data['folders'].items():
|
|
1693
|
+
if folder_info.get('parent') is None:
|
|
1694
|
+
# Create top-level folder
|
|
1695
|
+
folder = self.tables_list.create_folder(folder_info['name'])
|
|
1696
|
+
folder_items[folder_id] = folder
|
|
1697
|
+
# Set expanded state
|
|
1698
|
+
folder.setExpanded(folder_info.get('expanded', True))
|
|
1699
|
+
|
|
1700
|
+
# Second pass: create subfolders
|
|
1701
|
+
for folder_id, folder_info in project_data['folders'].items():
|
|
1702
|
+
parent_id = folder_info.get('parent')
|
|
1703
|
+
if parent_id is not None and parent_id in folder_items:
|
|
1704
|
+
# Create subfolder under parent
|
|
1705
|
+
parent_folder = folder_items[parent_id]
|
|
1706
|
+
subfolder = QTreeWidgetItem(parent_folder)
|
|
1707
|
+
subfolder.setText(0, folder_info['name'])
|
|
1708
|
+
subfolder.setIcon(0, QIcon.fromTheme("folder"))
|
|
1709
|
+
subfolder.setData(0, Qt.ItemDataRole.UserRole, "folder")
|
|
1710
|
+
# Make folder text bold
|
|
1711
|
+
font = subfolder.font(0)
|
|
1712
|
+
font.setBold(True)
|
|
1713
|
+
subfolder.setFont(0, font)
|
|
1714
|
+
# Set folder flags
|
|
1715
|
+
subfolder.setFlags(subfolder.flags() | Qt.ItemFlag.ItemIsDropEnabled)
|
|
1716
|
+
# Set expanded state
|
|
1717
|
+
subfolder.setExpanded(folder_info.get('expanded', True))
|
|
1718
|
+
folder_items[folder_id] = subfolder
|
|
1719
|
+
|
|
1720
|
+
progress.setValue(25)
|
|
1753
1721
|
QApplication.processEvents()
|
|
1754
1722
|
|
|
1755
1723
|
# Calculate progress steps for loading tables
|
|
@@ -1764,59 +1732,113 @@ LIMIT 10
|
|
|
1764
1732
|
if progress.wasCanceled():
|
|
1765
1733
|
break
|
|
1766
1734
|
|
|
1767
|
-
progress.setLabelText(f"
|
|
1735
|
+
progress.setLabelText(f"Processing table: {table_name}")
|
|
1768
1736
|
file_path = table_info['file_path']
|
|
1737
|
+
self.statusBar().showMessage(f"Processing table: {table_name} from {file_path}")
|
|
1738
|
+
|
|
1769
1739
|
try:
|
|
1740
|
+
# Determine folder placement
|
|
1741
|
+
folder_id = table_info.get('folder')
|
|
1742
|
+
parent_folder = folder_items.get(folder_id) if folder_id else None
|
|
1743
|
+
|
|
1770
1744
|
if file_path == 'database':
|
|
1771
|
-
#
|
|
1745
|
+
# Different handling based on whether database connection is active
|
|
1772
1746
|
if database_tables_loaded:
|
|
1773
|
-
|
|
1747
|
+
# Store table info without loading data
|
|
1748
|
+
self.db_manager.loaded_tables[table_name] = 'database'
|
|
1749
|
+
if 'columns' in table_info:
|
|
1750
|
+
self.db_manager.table_columns[table_name] = table_info['columns']
|
|
1751
|
+
|
|
1752
|
+
# Create item without reload icon
|
|
1753
|
+
if parent_folder:
|
|
1754
|
+
# Add to folder
|
|
1755
|
+
item = QTreeWidgetItem(parent_folder)
|
|
1756
|
+
item.setText(0, f"{table_name} (database)")
|
|
1757
|
+
item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
|
|
1758
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
1759
|
+
else:
|
|
1760
|
+
# Add to root
|
|
1761
|
+
self.tables_list.add_table_item(table_name, "database", needs_reload=False)
|
|
1762
|
+
else:
|
|
1763
|
+
# No active database connection, just register the table name
|
|
1764
|
+
self.db_manager.loaded_tables[table_name] = 'database'
|
|
1765
|
+
if 'columns' in table_info:
|
|
1766
|
+
self.db_manager.table_columns[table_name] = table_info['columns']
|
|
1774
1767
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1768
|
+
# Create item with reload icon
|
|
1769
|
+
if parent_folder:
|
|
1770
|
+
# Add to folder
|
|
1771
|
+
item = QTreeWidgetItem(parent_folder)
|
|
1772
|
+
item.setText(0, f"{table_name} (database)")
|
|
1773
|
+
item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
1774
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
1775
|
+
item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
1776
|
+
self.tables_list.tables_needing_reload.add(table_name)
|
|
1777
|
+
else:
|
|
1778
|
+
# Add to root
|
|
1779
|
+
self.tables_list.add_table_item(table_name, "database", needs_reload=True)
|
|
1786
1780
|
elif file_path == 'query_result':
|
|
1787
|
-
# For tables from query results,
|
|
1788
|
-
# For now, just note it as a query result table
|
|
1781
|
+
# For tables from query results, just note it as a query result table
|
|
1789
1782
|
self.db_manager.loaded_tables[table_name] = 'query_result'
|
|
1790
|
-
|
|
1783
|
+
|
|
1784
|
+
# Create item with reload icon
|
|
1785
|
+
if parent_folder:
|
|
1786
|
+
# Add to folder
|
|
1787
|
+
item = QTreeWidgetItem(parent_folder)
|
|
1788
|
+
item.setText(0, f"{table_name} (query result)")
|
|
1789
|
+
item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
1790
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
1791
|
+
item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
1792
|
+
self.tables_list.tables_needing_reload.add(table_name)
|
|
1793
|
+
else:
|
|
1794
|
+
# Add to root
|
|
1795
|
+
self.tables_list.add_table_item(table_name, "query result", needs_reload=True)
|
|
1791
1796
|
elif os.path.exists(file_path):
|
|
1792
|
-
#
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
self.
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1797
|
+
# Register the file as a table source but don't load data yet
|
|
1798
|
+
self.db_manager.loaded_tables[table_name] = file_path
|
|
1799
|
+
if 'columns' in table_info:
|
|
1800
|
+
self.db_manager.table_columns[table_name] = table_info['columns']
|
|
1801
|
+
|
|
1802
|
+
# Create item with reload icon
|
|
1803
|
+
if parent_folder:
|
|
1804
|
+
# Add to folder
|
|
1805
|
+
item = QTreeWidgetItem(parent_folder)
|
|
1806
|
+
item.setText(0, f"{table_name} ({os.path.basename(file_path)})")
|
|
1807
|
+
item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
1808
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
1809
|
+
item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
1810
|
+
self.tables_list.tables_needing_reload.add(table_name)
|
|
1811
|
+
else:
|
|
1812
|
+
# Add to root
|
|
1813
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_path), needs_reload=True)
|
|
1800
1814
|
else:
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1815
|
+
# File doesn't exist, but add to list with warning
|
|
1816
|
+
self.db_manager.loaded_tables[table_name] = file_path
|
|
1817
|
+
if 'columns' in table_info:
|
|
1818
|
+
self.db_manager.table_columns[table_name] = table_info['columns']
|
|
1819
|
+
|
|
1820
|
+
# Create item with reload icon and missing warning
|
|
1821
|
+
if parent_folder:
|
|
1822
|
+
# Add to folder
|
|
1823
|
+
item = QTreeWidgetItem(parent_folder)
|
|
1824
|
+
item.setText(0, f"{table_name} ({os.path.basename(file_path)} (missing))")
|
|
1825
|
+
item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
1826
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
1827
|
+
item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
1828
|
+
self.tables_list.tables_needing_reload.add(table_name)
|
|
1829
|
+
else:
|
|
1830
|
+
# Add to root
|
|
1831
|
+
self.tables_list.add_table_item(table_name, f"{os.path.basename(file_path)} (missing)", needs_reload=True)
|
|
1804
1832
|
|
|
1805
1833
|
except Exception as e:
|
|
1806
1834
|
QMessageBox.warning(self, "Warning",
|
|
1807
|
-
f"Failed to
|
|
1835
|
+
f"Failed to process table {table_name}:\n{str(e)}")
|
|
1808
1836
|
|
|
1809
1837
|
# Update progress for this table
|
|
1810
1838
|
current_progress += table_progress_step
|
|
1811
1839
|
progress.setValue(int(current_progress))
|
|
1812
1840
|
QApplication.processEvents() # Keep UI responsive
|
|
1813
1841
|
|
|
1814
|
-
# If the project had database tables but we couldn't connect automatically, notify the user
|
|
1815
|
-
if has_database_tables and not database_tables_loaded:
|
|
1816
|
-
QMessageBox.information(self, "Database Connection Required",
|
|
1817
|
-
"This project contains database tables. You need to reconnect to the database to use them.\n\n"
|
|
1818
|
-
"Use the 'Open Database' button to connect to your database file.")
|
|
1819
|
-
|
|
1820
1842
|
# Check if the operation was canceled
|
|
1821
1843
|
if progress.wasCanceled():
|
|
1822
1844
|
self.statusBar().showMessage("Project loading was canceled")
|
|
@@ -1919,9 +1941,25 @@ LIMIT 10
|
|
|
1919
1941
|
progress.setValue(100)
|
|
1920
1942
|
QApplication.processEvents()
|
|
1921
1943
|
|
|
1922
|
-
|
|
1944
|
+
# Show message about tables needing reload
|
|
1945
|
+
reload_count = len(self.tables_list.tables_needing_reload)
|
|
1946
|
+
if reload_count > 0:
|
|
1947
|
+
self.statusBar().showMessage(
|
|
1948
|
+
f'Project loaded from {file_name} with {table_count} tables. {reload_count} tables need to be reloaded (click reload icon).'
|
|
1949
|
+
)
|
|
1950
|
+
else:
|
|
1951
|
+
self.statusBar().showMessage(
|
|
1952
|
+
f'Project loaded from {file_name} with {table_count} tables.'
|
|
1953
|
+
)
|
|
1954
|
+
|
|
1955
|
+
# Close progress dialog before showing message boxes
|
|
1923
1956
|
progress.close()
|
|
1924
1957
|
|
|
1958
|
+
# Now show any database connection message we stored earlier
|
|
1959
|
+
if database_connection_message and not database_tables_loaded and has_database_tables:
|
|
1960
|
+
title, message = database_connection_message
|
|
1961
|
+
QMessageBox.warning(self, title, message)
|
|
1962
|
+
|
|
1925
1963
|
except Exception as e:
|
|
1926
1964
|
QMessageBox.critical(self, "Error",
|
|
1927
1965
|
f"Failed to open project:\n\n{str(e)}")
|
|
@@ -1952,6 +1990,10 @@ LIMIT 10
|
|
|
1952
1990
|
settings = json.load(f)
|
|
1953
1991
|
self.recent_projects = settings.get('recent_projects', [])
|
|
1954
1992
|
|
|
1993
|
+
# Load user preferences
|
|
1994
|
+
preferences = settings.get('preferences', {})
|
|
1995
|
+
self.auto_load_recent_project = preferences.get('auto_load_recent_project', True)
|
|
1996
|
+
|
|
1955
1997
|
# Load window settings if available
|
|
1956
1998
|
window_settings = settings.get('window', {})
|
|
1957
1999
|
if window_settings:
|
|
@@ -1969,10 +2011,19 @@ LIMIT 10
|
|
|
1969
2011
|
settings = json.load(f)
|
|
1970
2012
|
settings['recent_projects'] = self.recent_projects
|
|
1971
2013
|
|
|
2014
|
+
# Save user preferences
|
|
2015
|
+
if 'preferences' not in settings:
|
|
2016
|
+
settings['preferences'] = {}
|
|
2017
|
+
settings['preferences']['auto_load_recent_project'] = self.auto_load_recent_project
|
|
2018
|
+
|
|
1972
2019
|
# Save window settings
|
|
1973
2020
|
window_settings = self.save_window_state()
|
|
1974
2021
|
settings['window'] = window_settings
|
|
1975
2022
|
|
|
2023
|
+
# Also save recent and frequent files data
|
|
2024
|
+
settings['recent_files'] = self.recent_files
|
|
2025
|
+
settings['frequent_files'] = self.frequent_files
|
|
2026
|
+
|
|
1976
2027
|
with open(settings_file, 'w') as f:
|
|
1977
2028
|
json.dump(settings, f, indent=4)
|
|
1978
2029
|
except Exception as e:
|
|
@@ -2064,6 +2115,20 @@ LIMIT 10
|
|
|
2064
2115
|
def open_recent_project(self, project_path):
|
|
2065
2116
|
"""Open a project from the recent projects list"""
|
|
2066
2117
|
if os.path.exists(project_path):
|
|
2118
|
+
# Check if current project has unsaved changes before loading the new one
|
|
2119
|
+
if self.has_unsaved_changes():
|
|
2120
|
+
reply = QMessageBox.question(self, 'Save Changes',
|
|
2121
|
+
'Do you want to save your changes before loading another project?',
|
|
2122
|
+
QMessageBox.StandardButton.Save |
|
|
2123
|
+
QMessageBox.StandardButton.Discard |
|
|
2124
|
+
QMessageBox.StandardButton.Cancel)
|
|
2125
|
+
|
|
2126
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
2127
|
+
self.save_project()
|
|
2128
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
2129
|
+
return
|
|
2130
|
+
|
|
2131
|
+
# Now proceed with loading the project
|
|
2067
2132
|
self.current_project_file = project_path
|
|
2068
2133
|
self.open_project(project_path)
|
|
2069
2134
|
else:
|
|
@@ -2079,6 +2144,246 @@ LIMIT 10
|
|
|
2079
2144
|
self.save_recent_projects()
|
|
2080
2145
|
self.update_recent_projects_menu()
|
|
2081
2146
|
|
|
2147
|
+
def load_recent_files(self):
|
|
2148
|
+
"""Load recent and frequent files from settings file"""
|
|
2149
|
+
try:
|
|
2150
|
+
settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
|
|
2151
|
+
if os.path.exists(settings_file):
|
|
2152
|
+
with open(settings_file, 'r') as f:
|
|
2153
|
+
settings = json.load(f)
|
|
2154
|
+
self.recent_files = settings.get('recent_files', [])
|
|
2155
|
+
self.frequent_files = settings.get('frequent_files', {})
|
|
2156
|
+
except Exception:
|
|
2157
|
+
self.recent_files = []
|
|
2158
|
+
self.frequent_files = {}
|
|
2159
|
+
|
|
2160
|
+
def save_recent_files(self):
|
|
2161
|
+
"""Save recent and frequent files to settings file"""
|
|
2162
|
+
try:
|
|
2163
|
+
settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
|
|
2164
|
+
settings = {}
|
|
2165
|
+
if os.path.exists(settings_file):
|
|
2166
|
+
with open(settings_file, 'r') as f:
|
|
2167
|
+
settings = json.load(f)
|
|
2168
|
+
settings['recent_files'] = self.recent_files
|
|
2169
|
+
settings['frequent_files'] = self.frequent_files
|
|
2170
|
+
|
|
2171
|
+
with open(settings_file, 'w') as f:
|
|
2172
|
+
json.dump(settings, f, indent=4)
|
|
2173
|
+
except Exception as e:
|
|
2174
|
+
print(f"Error saving recent files: {e}")
|
|
2175
|
+
|
|
2176
|
+
def add_recent_file(self, file_path):
|
|
2177
|
+
"""Add a file to recent files list and update frequent files count"""
|
|
2178
|
+
file_path = os.path.abspath(file_path)
|
|
2179
|
+
|
|
2180
|
+
# Update recent files
|
|
2181
|
+
if file_path in self.recent_files:
|
|
2182
|
+
self.recent_files.remove(file_path)
|
|
2183
|
+
self.recent_files.insert(0, file_path)
|
|
2184
|
+
self.recent_files = self.recent_files[:self.max_recent_files]
|
|
2185
|
+
|
|
2186
|
+
# Update frequency count
|
|
2187
|
+
if file_path in self.frequent_files:
|
|
2188
|
+
self.frequent_files[file_path] += 1
|
|
2189
|
+
else:
|
|
2190
|
+
self.frequent_files[file_path] = 1
|
|
2191
|
+
|
|
2192
|
+
# Save to settings
|
|
2193
|
+
self.save_recent_files()
|
|
2194
|
+
|
|
2195
|
+
# Update the quick access menu if it exists
|
|
2196
|
+
if hasattr(self, 'quick_access_menu'):
|
|
2197
|
+
self.update_quick_access_menu()
|
|
2198
|
+
|
|
2199
|
+
def get_frequent_files(self, limit=10):
|
|
2200
|
+
"""Get the most frequently used files"""
|
|
2201
|
+
sorted_files = sorted(
|
|
2202
|
+
self.frequent_files.items(),
|
|
2203
|
+
key=lambda item: item[1],
|
|
2204
|
+
reverse=True
|
|
2205
|
+
)
|
|
2206
|
+
return [path for path, count in sorted_files[:limit] if os.path.exists(path)]
|
|
2207
|
+
|
|
2208
|
+
def clear_recent_files(self):
|
|
2209
|
+
"""Clear the list of recent files"""
|
|
2210
|
+
self.recent_files.clear()
|
|
2211
|
+
self.save_recent_files()
|
|
2212
|
+
if hasattr(self, 'quick_access_menu'):
|
|
2213
|
+
self.update_quick_access_menu()
|
|
2214
|
+
|
|
2215
|
+
def clear_frequent_files(self):
|
|
2216
|
+
"""Clear the list of frequent files"""
|
|
2217
|
+
self.frequent_files.clear()
|
|
2218
|
+
self.save_recent_files()
|
|
2219
|
+
if hasattr(self, 'quick_access_menu'):
|
|
2220
|
+
self.update_quick_access_menu()
|
|
2221
|
+
|
|
2222
|
+
def update_quick_access_menu(self):
|
|
2223
|
+
"""Update the quick access menu with recent and frequent files"""
|
|
2224
|
+
if not hasattr(self, 'quick_access_menu'):
|
|
2225
|
+
return
|
|
2226
|
+
|
|
2227
|
+
self.quick_access_menu.clear()
|
|
2228
|
+
|
|
2229
|
+
# Add "Recent Files" section
|
|
2230
|
+
if self.recent_files:
|
|
2231
|
+
recent_section = self.quick_access_menu.addSection("Recent Files")
|
|
2232
|
+
|
|
2233
|
+
for file_path in self.recent_files[:10]: # Show top 10 recent files
|
|
2234
|
+
if os.path.exists(file_path):
|
|
2235
|
+
file_name = os.path.basename(file_path)
|
|
2236
|
+
action = self.quick_access_menu.addAction(file_name)
|
|
2237
|
+
action.setData(file_path)
|
|
2238
|
+
action.setToolTip(file_path)
|
|
2239
|
+
action.triggered.connect(lambda checked, path=file_path: self.quick_open_file(path))
|
|
2240
|
+
|
|
2241
|
+
# Add "Frequently Used Files" section
|
|
2242
|
+
frequent_files = self.get_frequent_files(10) # Get top 10 frequent files
|
|
2243
|
+
if frequent_files:
|
|
2244
|
+
self.quick_access_menu.addSeparator()
|
|
2245
|
+
freq_section = self.quick_access_menu.addSection("Frequently Used Files")
|
|
2246
|
+
|
|
2247
|
+
for file_path in frequent_files:
|
|
2248
|
+
file_name = os.path.basename(file_path)
|
|
2249
|
+
count = self.frequent_files.get(file_path, 0)
|
|
2250
|
+
action = self.quick_access_menu.addAction(f"{file_name} ({count} uses)")
|
|
2251
|
+
action.setData(file_path)
|
|
2252
|
+
action.setToolTip(file_path)
|
|
2253
|
+
action.triggered.connect(lambda checked, path=file_path: self.quick_open_file(path))
|
|
2254
|
+
|
|
2255
|
+
# Add management options if we have any files
|
|
2256
|
+
if self.recent_files or self.frequent_files:
|
|
2257
|
+
self.quick_access_menu.addSeparator()
|
|
2258
|
+
clear_recent = self.quick_access_menu.addAction("Clear Recent Files")
|
|
2259
|
+
clear_recent.triggered.connect(self.clear_recent_files)
|
|
2260
|
+
|
|
2261
|
+
clear_frequent = self.quick_access_menu.addAction("Clear Frequent Files")
|
|
2262
|
+
clear_frequent.triggered.connect(self.clear_frequent_files)
|
|
2263
|
+
else:
|
|
2264
|
+
# No files placeholder
|
|
2265
|
+
no_files = self.quick_access_menu.addAction("No Recent Files")
|
|
2266
|
+
no_files.setEnabled(False)
|
|
2267
|
+
|
|
2268
|
+
def quick_open_file(self, file_path):
|
|
2269
|
+
"""Open a file from the quick access menu"""
|
|
2270
|
+
if not os.path.exists(file_path):
|
|
2271
|
+
QMessageBox.warning(self, "File Not Found",
|
|
2272
|
+
f"The file no longer exists:\n{file_path}")
|
|
2273
|
+
|
|
2274
|
+
# Remove from tracking
|
|
2275
|
+
if file_path in self.recent_files:
|
|
2276
|
+
self.recent_files.remove(file_path)
|
|
2277
|
+
if file_path in self.frequent_files:
|
|
2278
|
+
del self.frequent_files[file_path]
|
|
2279
|
+
self.save_recent_files()
|
|
2280
|
+
self.update_quick_access_menu()
|
|
2281
|
+
return
|
|
2282
|
+
|
|
2283
|
+
try:
|
|
2284
|
+
# Determine file type
|
|
2285
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
|
2286
|
+
|
|
2287
|
+
# Check if this is a Delta table directory
|
|
2288
|
+
is_delta_table = False
|
|
2289
|
+
if os.path.isdir(file_path):
|
|
2290
|
+
delta_path = Path(file_path)
|
|
2291
|
+
delta_log_path = delta_path / '_delta_log'
|
|
2292
|
+
if delta_log_path.exists():
|
|
2293
|
+
is_delta_table = True
|
|
2294
|
+
|
|
2295
|
+
if is_delta_table:
|
|
2296
|
+
# Delta table directory
|
|
2297
|
+
if not self.db_manager.is_connected():
|
|
2298
|
+
# Create a default in-memory DuckDB connection if none exists
|
|
2299
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
2300
|
+
self.db_info_label.setText(connection_info)
|
|
2301
|
+
|
|
2302
|
+
# Use the database manager to load the Delta table
|
|
2303
|
+
table_name, df = self.db_manager.load_file(file_path)
|
|
2304
|
+
|
|
2305
|
+
# Update UI using new method
|
|
2306
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_path))
|
|
2307
|
+
self.statusBar().showMessage(f'Loaded Delta table from {file_path} as "{table_name}"')
|
|
2308
|
+
|
|
2309
|
+
# Show preview of loaded data
|
|
2310
|
+
preview_df = df.head()
|
|
2311
|
+
current_tab = self.get_current_tab()
|
|
2312
|
+
if current_tab:
|
|
2313
|
+
self.populate_table(preview_df)
|
|
2314
|
+
current_tab.results_title.setText(f"PREVIEW: {table_name}")
|
|
2315
|
+
|
|
2316
|
+
# Update completer with new table and column names
|
|
2317
|
+
self.update_completer()
|
|
2318
|
+
elif file_ext in ['.db', '.sqlite', '.sqlite3']:
|
|
2319
|
+
# Database file
|
|
2320
|
+
# Clear existing database tables from the list widget
|
|
2321
|
+
for i in range(self.tables_list.topLevelItemCount() - 1, -1, -1):
|
|
2322
|
+
item = self.tables_list.topLevelItem(i)
|
|
2323
|
+
if item and item.text(0).endswith('(database)'):
|
|
2324
|
+
self.tables_list.takeTopLevelItem(i)
|
|
2325
|
+
|
|
2326
|
+
# Use the database manager to open the database
|
|
2327
|
+
self.db_manager.open_database(file_path)
|
|
2328
|
+
|
|
2329
|
+
# Update UI with tables from the database using new method
|
|
2330
|
+
for table_name, source in self.db_manager.loaded_tables.items():
|
|
2331
|
+
if source == 'database':
|
|
2332
|
+
self.tables_list.add_table_item(table_name, "database")
|
|
2333
|
+
|
|
2334
|
+
# Update the completer with table and column names
|
|
2335
|
+
self.update_completer()
|
|
2336
|
+
|
|
2337
|
+
# Update status bar
|
|
2338
|
+
self.statusBar().showMessage(f"Connected to database: {file_path}")
|
|
2339
|
+
self.db_info_label.setText(self.db_manager.get_connection_info())
|
|
2340
|
+
|
|
2341
|
+
elif file_ext in ['.xlsx', '.xls', '.csv', '.parquet']:
|
|
2342
|
+
# Data file
|
|
2343
|
+
if not self.db_manager.is_connected():
|
|
2344
|
+
# Create a default in-memory DuckDB connection if none exists
|
|
2345
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
2346
|
+
self.db_info_label.setText(connection_info)
|
|
2347
|
+
|
|
2348
|
+
# Use the database manager to load the file
|
|
2349
|
+
table_name, df = self.db_manager.load_file(file_path)
|
|
2350
|
+
|
|
2351
|
+
# Update UI using new method
|
|
2352
|
+
self.tables_list.add_table_item(table_name, os.path.basename(file_path))
|
|
2353
|
+
self.statusBar().showMessage(f'Loaded {file_path} as table "{table_name}"')
|
|
2354
|
+
|
|
2355
|
+
# Show preview of loaded data
|
|
2356
|
+
preview_df = df.head()
|
|
2357
|
+
current_tab = self.get_current_tab()
|
|
2358
|
+
if current_tab:
|
|
2359
|
+
self.populate_table(preview_df)
|
|
2360
|
+
current_tab.results_title.setText(f"PREVIEW: {table_name}")
|
|
2361
|
+
|
|
2362
|
+
# Update completer with new table and column names
|
|
2363
|
+
self.update_completer()
|
|
2364
|
+
else:
|
|
2365
|
+
QMessageBox.warning(self, "Unsupported File Type",
|
|
2366
|
+
f"The file type {file_ext} is not supported.")
|
|
2367
|
+
return
|
|
2368
|
+
|
|
2369
|
+
# Update tracking - increment usage count
|
|
2370
|
+
self.add_recent_file(file_path)
|
|
2371
|
+
|
|
2372
|
+
except Exception as e:
|
|
2373
|
+
QMessageBox.critical(self, "Error",
|
|
2374
|
+
f"Failed to open file:\n\n{str(e)}")
|
|
2375
|
+
self.statusBar().showMessage(f"Error opening file: {os.path.basename(file_path)}")
|
|
2376
|
+
|
|
2377
|
+
def show_quick_access_menu(self):
|
|
2378
|
+
"""Display the quick access menu when the button is clicked"""
|
|
2379
|
+
# First, make sure the menu is up to date
|
|
2380
|
+
self.update_quick_access_menu()
|
|
2381
|
+
|
|
2382
|
+
# Show the menu below the quick access button
|
|
2383
|
+
if hasattr(self, 'quick_access_menu') and hasattr(self, 'quick_access_btn'):
|
|
2384
|
+
self.quick_access_menu.popup(self.quick_access_btn.mapToGlobal(
|
|
2385
|
+
QPoint(0, self.quick_access_btn.height())))
|
|
2386
|
+
|
|
2082
2387
|
def add_tab(self, title="Query 1"):
|
|
2083
2388
|
"""Add a new query tab"""
|
|
2084
2389
|
# Ensure title is a string
|
|
@@ -2123,6 +2428,24 @@ LIMIT 10
|
|
|
2123
2428
|
# Process events to keep UI responsive
|
|
2124
2429
|
QApplication.processEvents()
|
|
2125
2430
|
|
|
2431
|
+
# Update completer for the new tab
|
|
2432
|
+
try:
|
|
2433
|
+
from sqlshell.suggester_integration import get_suggestion_manager
|
|
2434
|
+
|
|
2435
|
+
# Get the suggestion manager singleton
|
|
2436
|
+
suggestion_mgr = get_suggestion_manager()
|
|
2437
|
+
|
|
2438
|
+
# Register the new editor with a unique ID
|
|
2439
|
+
editor_id = f"tab_{index}_{id(tab.query_edit)}"
|
|
2440
|
+
suggestion_mgr.register_editor(tab.query_edit, editor_id)
|
|
2441
|
+
|
|
2442
|
+
# Apply the current completer model if available
|
|
2443
|
+
if hasattr(self, '_current_completer_model'):
|
|
2444
|
+
tab.query_edit.update_completer_model(self._current_completer_model)
|
|
2445
|
+
except Exception as e:
|
|
2446
|
+
# Don't let autocomplete errors affect tab creation
|
|
2447
|
+
print(f"Error setting up autocomplete for new tab: {e}")
|
|
2448
|
+
|
|
2126
2449
|
return tab
|
|
2127
2450
|
|
|
2128
2451
|
def duplicate_current_tab(self):
|
|
@@ -2199,11 +2522,27 @@ LIMIT 10
|
|
|
2199
2522
|
tab.results_table.setColumnCount(0)
|
|
2200
2523
|
return
|
|
2201
2524
|
|
|
2525
|
+
# Get the widget before removing the tab
|
|
2526
|
+
widget = self.tab_widget.widget(index)
|
|
2527
|
+
|
|
2528
|
+
# Unregister the editor from the suggestion manager before closing
|
|
2529
|
+
try:
|
|
2530
|
+
from sqlshell.suggester_integration import get_suggestion_manager
|
|
2531
|
+
suggestion_mgr = get_suggestion_manager()
|
|
2532
|
+
|
|
2533
|
+
# Find and unregister this editor
|
|
2534
|
+
for editor_id in list(suggestion_mgr._editors.keys()):
|
|
2535
|
+
if editor_id.startswith(f"tab_{index}_") or (hasattr(widget, 'query_edit') and
|
|
2536
|
+
str(id(widget.query_edit)) in editor_id):
|
|
2537
|
+
suggestion_mgr.unregister_editor(editor_id)
|
|
2538
|
+
except Exception as e:
|
|
2539
|
+
# Don't let errors affect tab closing
|
|
2540
|
+
print(f"Error unregistering editor from suggestion manager: {e}")
|
|
2541
|
+
|
|
2202
2542
|
# Block signals temporarily to improve performance when removing multiple tabs
|
|
2203
2543
|
was_blocked = self.tab_widget.blockSignals(True)
|
|
2204
2544
|
|
|
2205
2545
|
# Remove the tab
|
|
2206
|
-
widget = self.tab_widget.widget(index)
|
|
2207
2546
|
self.tab_widget.removeTab(index)
|
|
2208
2547
|
|
|
2209
2548
|
# Restore signals
|
|
@@ -2218,6 +2557,36 @@ LIMIT 10
|
|
|
2218
2557
|
|
|
2219
2558
|
# Process events to keep UI responsive
|
|
2220
2559
|
QApplication.processEvents()
|
|
2560
|
+
|
|
2561
|
+
# Update tab indices in the suggestion manager
|
|
2562
|
+
QTimer.singleShot(100, self.update_tab_indices_in_suggestion_manager)
|
|
2563
|
+
|
|
2564
|
+
def update_tab_indices_in_suggestion_manager(self):
|
|
2565
|
+
"""Update tab indices in the suggestion manager after tab removal"""
|
|
2566
|
+
try:
|
|
2567
|
+
from sqlshell.suggester_integration import get_suggestion_manager
|
|
2568
|
+
suggestion_mgr = get_suggestion_manager()
|
|
2569
|
+
|
|
2570
|
+
# Get current editors
|
|
2571
|
+
old_editors = suggestion_mgr._editors.copy()
|
|
2572
|
+
old_completers = suggestion_mgr._completers.copy()
|
|
2573
|
+
|
|
2574
|
+
# Clear current registrations
|
|
2575
|
+
suggestion_mgr._editors.clear()
|
|
2576
|
+
suggestion_mgr._completers.clear()
|
|
2577
|
+
|
|
2578
|
+
# Re-register with updated indices
|
|
2579
|
+
for i in range(self.tab_widget.count()):
|
|
2580
|
+
tab = self.tab_widget.widget(i)
|
|
2581
|
+
if tab and hasattr(tab, 'query_edit'):
|
|
2582
|
+
# Register with new index
|
|
2583
|
+
editor_id = f"tab_{i}_{id(tab.query_edit)}"
|
|
2584
|
+
suggestion_mgr._editors[editor_id] = tab.query_edit
|
|
2585
|
+
if hasattr(tab.query_edit, 'completer') and tab.query_edit.completer:
|
|
2586
|
+
suggestion_mgr._completers[editor_id] = tab.query_edit.completer
|
|
2587
|
+
except Exception as e:
|
|
2588
|
+
# Don't let errors affect application
|
|
2589
|
+
print(f"Error updating tab indices in suggestion manager: {e}")
|
|
2221
2590
|
|
|
2222
2591
|
def close_current_tab(self):
|
|
2223
2592
|
"""Close the current tab"""
|
|
@@ -2317,7 +2686,544 @@ LIMIT 10
|
|
|
2317
2686
|
except Exception as e:
|
|
2318
2687
|
self.statusBar().showMessage(f"Error resetting zoom: {str(e)}", 2000)
|
|
2319
2688
|
|
|
2689
|
+
def load_most_recent_project(self):
|
|
2690
|
+
"""Load the most recent project if available"""
|
|
2691
|
+
if self.recent_projects:
|
|
2692
|
+
most_recent_project = self.recent_projects[0]
|
|
2693
|
+
if os.path.exists(most_recent_project):
|
|
2694
|
+
self.open_project(most_recent_project)
|
|
2695
|
+
self.statusBar().showMessage(f"Auto-loaded most recent project: {os.path.basename(most_recent_project)}")
|
|
2696
|
+
else:
|
|
2697
|
+
# Remove the non-existent project from the list
|
|
2698
|
+
self.recent_projects.remove(most_recent_project)
|
|
2699
|
+
self.save_recent_projects()
|
|
2700
|
+
# Try the next project if available
|
|
2701
|
+
if self.recent_projects:
|
|
2702
|
+
self.load_most_recent_project()
|
|
2703
|
+
|
|
2704
|
+
def load_delta_table(self):
|
|
2705
|
+
"""Load a Delta table from a directory"""
|
|
2706
|
+
if not self.db_manager.is_connected():
|
|
2707
|
+
# Create a default in-memory DuckDB connection if none exists
|
|
2708
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
2709
|
+
self.db_info_label.setText(connection_info)
|
|
2710
|
+
|
|
2711
|
+
# Get directory containing the Delta table
|
|
2712
|
+
delta_dir = QFileDialog.getExistingDirectory(
|
|
2713
|
+
self,
|
|
2714
|
+
"Select Delta Table Directory",
|
|
2715
|
+
"",
|
|
2716
|
+
QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks
|
|
2717
|
+
)
|
|
2718
|
+
|
|
2719
|
+
if not delta_dir:
|
|
2720
|
+
return
|
|
2721
|
+
|
|
2722
|
+
# Check if this is a valid Delta table directory
|
|
2723
|
+
delta_path = Path(delta_dir)
|
|
2724
|
+
delta_log_path = delta_path / '_delta_log'
|
|
2725
|
+
|
|
2726
|
+
if not delta_log_path.exists():
|
|
2727
|
+
# Ask if they want to select a subdirectory
|
|
2728
|
+
subdirs = [d for d in delta_path.iterdir() if d.is_dir() and (d / '_delta_log').exists()]
|
|
2729
|
+
|
|
2730
|
+
if subdirs:
|
|
2731
|
+
# There are subdirectories with Delta tables
|
|
2732
|
+
msg = QMessageBox()
|
|
2733
|
+
msg.setIcon(QMessageBox.Icon.Information)
|
|
2734
|
+
msg.setWindowTitle("Select Subdirectory")
|
|
2735
|
+
msg.setText(f"The selected directory does not contain a Delta table, but it contains {len(subdirs)} subdirectories with Delta tables.")
|
|
2736
|
+
msg.setInformativeText("Would you like to select one of these subdirectories?")
|
|
2737
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
2738
|
+
msg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
|
2739
|
+
|
|
2740
|
+
if msg.exec() == QMessageBox.StandardButton.Yes:
|
|
2741
|
+
# Create a dialog to select a subdirectory
|
|
2742
|
+
subdir_names = [d.name for d in subdirs]
|
|
2743
|
+
subdir, ok = QInputDialog.getItem(
|
|
2744
|
+
self,
|
|
2745
|
+
"Select Delta Subdirectory",
|
|
2746
|
+
"Choose a subdirectory containing a Delta table:",
|
|
2747
|
+
subdir_names,
|
|
2748
|
+
0,
|
|
2749
|
+
False
|
|
2750
|
+
)
|
|
2751
|
+
|
|
2752
|
+
if not ok or not subdir:
|
|
2753
|
+
return
|
|
2754
|
+
|
|
2755
|
+
delta_dir = str(delta_path / subdir)
|
|
2756
|
+
delta_path = Path(delta_dir)
|
|
2757
|
+
else:
|
|
2758
|
+
# Show error and return
|
|
2759
|
+
QMessageBox.critical(self, "Invalid Delta Table",
|
|
2760
|
+
"The selected directory does not contain a Delta table (_delta_log directory not found).")
|
|
2761
|
+
return
|
|
2762
|
+
else:
|
|
2763
|
+
# No Delta tables found
|
|
2764
|
+
QMessageBox.critical(self, "Invalid Delta Table",
|
|
2765
|
+
"The selected directory does not contain a Delta table (_delta_log directory not found).")
|
|
2766
|
+
return
|
|
2767
|
+
|
|
2768
|
+
try:
|
|
2769
|
+
# Add to recent files
|
|
2770
|
+
self.add_recent_file(delta_dir)
|
|
2771
|
+
|
|
2772
|
+
# Use the database manager to load the Delta table
|
|
2773
|
+
import os
|
|
2774
|
+
table_name, df = self.db_manager.load_file(delta_dir)
|
|
2775
|
+
|
|
2776
|
+
# Update UI using new method
|
|
2777
|
+
self.tables_list.add_table_item(table_name, os.path.basename(delta_dir))
|
|
2778
|
+
self.statusBar().showMessage(f'Loaded Delta table from {delta_dir} as "{table_name}"')
|
|
2779
|
+
|
|
2780
|
+
# Show preview of loaded data
|
|
2781
|
+
preview_df = df.head()
|
|
2782
|
+
self.populate_table(preview_df)
|
|
2783
|
+
|
|
2784
|
+
# Update results title to show preview
|
|
2785
|
+
current_tab = self.get_current_tab()
|
|
2786
|
+
if current_tab:
|
|
2787
|
+
current_tab.results_title.setText(f"PREVIEW: {table_name}")
|
|
2788
|
+
|
|
2789
|
+
# Update completer with new table and column names
|
|
2790
|
+
self.update_completer()
|
|
2791
|
+
|
|
2792
|
+
except Exception as e:
|
|
2793
|
+
error_msg = f'Error loading Delta table from {os.path.basename(delta_dir)}: {str(e)}'
|
|
2794
|
+
self.statusBar().showMessage(error_msg)
|
|
2795
|
+
QMessageBox.critical(self, "Error", error_msg)
|
|
2796
|
+
|
|
2797
|
+
current_tab = self.get_current_tab()
|
|
2798
|
+
if current_tab:
|
|
2799
|
+
current_tab.results_table.setRowCount(0)
|
|
2800
|
+
current_tab.results_table.setColumnCount(0)
|
|
2801
|
+
current_tab.row_count_label.setText("")
|
|
2802
|
+
|
|
2803
|
+
def show_load_dialog(self):
|
|
2804
|
+
"""Show a modern dialog with options to load different types of data"""
|
|
2805
|
+
# Create the dialog
|
|
2806
|
+
dialog = QDialog(self)
|
|
2807
|
+
dialog.setWindowTitle("Load Data")
|
|
2808
|
+
dialog.setMinimumWidth(450)
|
|
2809
|
+
dialog.setMinimumHeight(520)
|
|
2810
|
+
|
|
2811
|
+
# Create a layout for the dialog
|
|
2812
|
+
layout = QVBoxLayout(dialog)
|
|
2813
|
+
layout.setSpacing(24)
|
|
2814
|
+
layout.setContentsMargins(30, 30, 30, 30)
|
|
2815
|
+
|
|
2816
|
+
# Header section with title and logo
|
|
2817
|
+
header_layout = QHBoxLayout()
|
|
2818
|
+
|
|
2819
|
+
# Title label with gradient effect
|
|
2820
|
+
title_label = QLabel("Load Data")
|
|
2821
|
+
title_font = QFont()
|
|
2822
|
+
title_font.setPointSize(20)
|
|
2823
|
+
title_font.setBold(True)
|
|
2824
|
+
title_label.setFont(title_font)
|
|
2825
|
+
title_label.setStyleSheet("""
|
|
2826
|
+
font-weight: bold;
|
|
2827
|
+
background: -webkit-linear-gradient(#2C3E50, #3498DB);
|
|
2828
|
+
-webkit-background-clip: text;
|
|
2829
|
+
-webkit-text-fill-color: transparent;
|
|
2830
|
+
""")
|
|
2831
|
+
header_layout.addWidget(title_label, 1)
|
|
2832
|
+
|
|
2833
|
+
# Try to add a small logo image
|
|
2834
|
+
try:
|
|
2835
|
+
icon_path = os.path.join(os.path.dirname(__file__), "resources", "icon.png")
|
|
2836
|
+
if os.path.exists(icon_path):
|
|
2837
|
+
logo_label = QLabel()
|
|
2838
|
+
logo_pixmap = QPixmap(icon_path).scaled(48, 48, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
2839
|
+
logo_label.setPixmap(logo_pixmap)
|
|
2840
|
+
header_layout.addWidget(logo_label)
|
|
2841
|
+
except Exception:
|
|
2842
|
+
pass # Skip logo if any issues
|
|
2843
|
+
|
|
2844
|
+
layout.addLayout(header_layout)
|
|
2845
|
+
|
|
2846
|
+
# Description with clearer styling
|
|
2847
|
+
desc_label = QLabel("Choose a data source to load into SQLShell")
|
|
2848
|
+
desc_label.setStyleSheet("color: #7F8C8D; font-size: 14px; margin: 4px 0 12px 0;")
|
|
2849
|
+
layout.addWidget(desc_label)
|
|
2850
|
+
|
|
2851
|
+
# Add separator line
|
|
2852
|
+
separator = QFrame()
|
|
2853
|
+
separator.setFrameShape(QFrame.Shape.HLine)
|
|
2854
|
+
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
2855
|
+
separator.setStyleSheet("background-color: #E0E0E0; min-height: 1px; max-height: 1px;")
|
|
2856
|
+
layout.addWidget(separator)
|
|
2857
|
+
|
|
2858
|
+
# Create option cards with icons, titles and descriptions
|
|
2859
|
+
options_layout = QVBoxLayout()
|
|
2860
|
+
options_layout.setSpacing(16)
|
|
2861
|
+
options_layout.setContentsMargins(0, 10, 0, 10)
|
|
2862
|
+
|
|
2863
|
+
# Store animation references to prevent garbage collection
|
|
2864
|
+
animations = []
|
|
2865
|
+
|
|
2866
|
+
# Function to create hover animations for cards
|
|
2867
|
+
def create_hover_animations(card):
|
|
2868
|
+
# Store original stylesheet
|
|
2869
|
+
original_style = card.styleSheet()
|
|
2870
|
+
hover_style = """
|
|
2871
|
+
background-color: #F8F9FA;
|
|
2872
|
+
border: 1px solid #3498DB;
|
|
2873
|
+
border-radius: 8px;
|
|
2874
|
+
"""
|
|
2875
|
+
|
|
2876
|
+
# Function to handle enter event with animation
|
|
2877
|
+
def enterEvent(event):
|
|
2878
|
+
# Create and configure animation
|
|
2879
|
+
anim = QPropertyAnimation(card, b"geometry")
|
|
2880
|
+
anim.setDuration(150)
|
|
2881
|
+
current_geo = card.geometry()
|
|
2882
|
+
target_geo = QRect(
|
|
2883
|
+
current_geo.x() - 3, # Slight shift to left for effect
|
|
2884
|
+
current_geo.y(),
|
|
2885
|
+
current_geo.width() + 6, # Slight growth in width
|
|
2886
|
+
current_geo.height()
|
|
2887
|
+
)
|
|
2888
|
+
anim.setStartValue(current_geo)
|
|
2889
|
+
anim.setEndValue(target_geo)
|
|
2890
|
+
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
2891
|
+
|
|
2892
|
+
# Set hover style
|
|
2893
|
+
card.setStyleSheet(hover_style)
|
|
2894
|
+
# Start animation
|
|
2895
|
+
anim.start()
|
|
2896
|
+
# Keep reference to prevent garbage collection
|
|
2897
|
+
animations.append(anim)
|
|
2898
|
+
|
|
2899
|
+
# Call original enter event if it exists
|
|
2900
|
+
original_enter = getattr(card, "_original_enterEvent", None)
|
|
2901
|
+
if original_enter:
|
|
2902
|
+
original_enter(event)
|
|
2903
|
+
|
|
2904
|
+
# Function to handle leave event with animation
|
|
2905
|
+
def leaveEvent(event):
|
|
2906
|
+
# Create and configure animation to return to original state
|
|
2907
|
+
anim = QPropertyAnimation(card, b"geometry")
|
|
2908
|
+
anim.setDuration(200)
|
|
2909
|
+
current_geo = card.geometry()
|
|
2910
|
+
original_geo = QRect(
|
|
2911
|
+
current_geo.x() + 3, # Shift back to original position
|
|
2912
|
+
current_geo.y(),
|
|
2913
|
+
current_geo.width() - 6, # Shrink back to original width
|
|
2914
|
+
current_geo.height()
|
|
2915
|
+
)
|
|
2916
|
+
anim.setStartValue(current_geo)
|
|
2917
|
+
anim.setEndValue(original_geo)
|
|
2918
|
+
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
2919
|
+
|
|
2920
|
+
# Restore original style
|
|
2921
|
+
card.setStyleSheet(original_style)
|
|
2922
|
+
# Start animation
|
|
2923
|
+
anim.start()
|
|
2924
|
+
# Keep reference to prevent garbage collection
|
|
2925
|
+
animations.append(anim)
|
|
2926
|
+
|
|
2927
|
+
# Call original leave event if it exists
|
|
2928
|
+
original_leave = getattr(card, "_original_leaveEvent", None)
|
|
2929
|
+
if original_leave:
|
|
2930
|
+
original_leave(event)
|
|
2931
|
+
|
|
2932
|
+
# Store original event handlers and set new ones
|
|
2933
|
+
card._original_enterEvent = card.enterEvent
|
|
2934
|
+
card._original_leaveEvent = card.leaveEvent
|
|
2935
|
+
card.enterEvent = enterEvent
|
|
2936
|
+
card.leaveEvent = leaveEvent
|
|
2937
|
+
|
|
2938
|
+
return card
|
|
2939
|
+
|
|
2940
|
+
# Function to create styled option buttons with descriptions
|
|
2941
|
+
def create_option_button(title, description, icon_name, option_type, accent_color="#3498DB"):
|
|
2942
|
+
# Create container frame
|
|
2943
|
+
container = QFrame()
|
|
2944
|
+
container.setObjectName("optionCard")
|
|
2945
|
+
container.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
2946
|
+
container.setProperty("optionType", option_type)
|
|
2947
|
+
|
|
2948
|
+
# Set frame style
|
|
2949
|
+
container.setFrameShape(QFrame.Shape.StyledPanel)
|
|
2950
|
+
container.setLineWidth(1)
|
|
2951
|
+
container.setMinimumHeight(90)
|
|
2952
|
+
container.setStyleSheet(f"""
|
|
2953
|
+
background-color: #FFFFFF;
|
|
2954
|
+
border-radius: 10px;
|
|
2955
|
+
border: 1px solid #E0E0E0;
|
|
2956
|
+
""")
|
|
2957
|
+
|
|
2958
|
+
# Create layout for the container
|
|
2959
|
+
card_layout = QHBoxLayout(container)
|
|
2960
|
+
card_layout.setContentsMargins(20, 16, 20, 16)
|
|
2961
|
+
|
|
2962
|
+
# Add icon with colored circle background
|
|
2963
|
+
icon_container = QFrame()
|
|
2964
|
+
icon_container.setFixedSize(QSize(50, 50))
|
|
2965
|
+
icon_container.setStyleSheet(f"""
|
|
2966
|
+
background-color: {accent_color}20; /* 20% opacity */
|
|
2967
|
+
border-radius: 25px;
|
|
2968
|
+
border: none;
|
|
2969
|
+
""")
|
|
2970
|
+
|
|
2971
|
+
icon_layout = QHBoxLayout(icon_container)
|
|
2972
|
+
icon_layout.setContentsMargins(0, 0, 0, 0)
|
|
2973
|
+
|
|
2974
|
+
icon_label = QLabel()
|
|
2975
|
+
icon = QIcon.fromTheme(icon_name)
|
|
2976
|
+
icon_pixmap = icon.pixmap(QSize(24, 24))
|
|
2977
|
+
icon_label.setPixmap(icon_pixmap)
|
|
2978
|
+
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
2979
|
+
icon_layout.addWidget(icon_label)
|
|
2980
|
+
|
|
2981
|
+
card_layout.addWidget(icon_container)
|
|
2982
|
+
|
|
2983
|
+
# Add text section
|
|
2984
|
+
text_layout = QVBoxLayout()
|
|
2985
|
+
text_layout.setSpacing(4)
|
|
2986
|
+
text_layout.setContentsMargins(12, 0, 0, 0)
|
|
2987
|
+
|
|
2988
|
+
# Add title
|
|
2989
|
+
title_label = QLabel(title)
|
|
2990
|
+
title_font = QFont()
|
|
2991
|
+
title_font.setBold(True)
|
|
2992
|
+
title_font.setPointSize(12)
|
|
2993
|
+
title_label.setFont(title_font)
|
|
2994
|
+
text_layout.addWidget(title_label)
|
|
2995
|
+
|
|
2996
|
+
# Add description
|
|
2997
|
+
desc_label = QLabel(description)
|
|
2998
|
+
desc_label.setWordWrap(True)
|
|
2999
|
+
desc_label.setStyleSheet("color: #7F8C8D; font-size: 11px;")
|
|
3000
|
+
text_layout.addWidget(desc_label)
|
|
3001
|
+
|
|
3002
|
+
card_layout.addLayout(text_layout, 1)
|
|
3003
|
+
|
|
3004
|
+
# Add arrow icon to suggest clickable
|
|
3005
|
+
arrow_label = QLabel("→")
|
|
3006
|
+
arrow_label.setStyleSheet(f"color: {accent_color}; font-size: 16px; font-weight: bold;")
|
|
3007
|
+
card_layout.addWidget(arrow_label)
|
|
3008
|
+
|
|
3009
|
+
# Connect click event
|
|
3010
|
+
container.mousePressEvent = lambda e: self.handle_load_option(dialog, option_type)
|
|
3011
|
+
|
|
3012
|
+
# Apply hover animations
|
|
3013
|
+
container = create_hover_animations(container)
|
|
3014
|
+
|
|
3015
|
+
return container
|
|
3016
|
+
|
|
3017
|
+
# Database option
|
|
3018
|
+
db_option = create_option_button(
|
|
3019
|
+
"Database",
|
|
3020
|
+
"Load SQL database files (SQLite, etc.) to query and analyze.",
|
|
3021
|
+
"database",
|
|
3022
|
+
"database",
|
|
3023
|
+
"#2980B9" # Blue accent
|
|
3024
|
+
)
|
|
3025
|
+
options_layout.addWidget(db_option)
|
|
3026
|
+
|
|
3027
|
+
# Files option
|
|
3028
|
+
files_option = create_option_button(
|
|
3029
|
+
"Data Files",
|
|
3030
|
+
"Load Excel, CSV, Parquet and other data file formats.",
|
|
3031
|
+
"document-new",
|
|
3032
|
+
"files",
|
|
3033
|
+
"#27AE60" # Green accent
|
|
3034
|
+
)
|
|
3035
|
+
options_layout.addWidget(files_option)
|
|
3036
|
+
|
|
3037
|
+
# Delta Table option
|
|
3038
|
+
delta_option = create_option_button(
|
|
3039
|
+
"Delta Table",
|
|
3040
|
+
"Load data from Delta Lake format directories.",
|
|
3041
|
+
"folder-open",
|
|
3042
|
+
"delta",
|
|
3043
|
+
"#8E44AD" # Purple accent
|
|
3044
|
+
)
|
|
3045
|
+
options_layout.addWidget(delta_option)
|
|
3046
|
+
|
|
3047
|
+
# Test Data option
|
|
3048
|
+
test_option = create_option_button(
|
|
3049
|
+
"Test Data",
|
|
3050
|
+
"Generate and load sample data for testing and exploration.",
|
|
3051
|
+
"system-run",
|
|
3052
|
+
"test",
|
|
3053
|
+
"#E67E22" # Orange accent
|
|
3054
|
+
)
|
|
3055
|
+
options_layout.addWidget(test_option)
|
|
3056
|
+
|
|
3057
|
+
layout.addLayout(options_layout)
|
|
3058
|
+
|
|
3059
|
+
# Add spacer
|
|
3060
|
+
layout.addStretch()
|
|
3061
|
+
|
|
3062
|
+
# Add separator line before buttons
|
|
3063
|
+
bottom_separator = QFrame()
|
|
3064
|
+
bottom_separator.setFrameShape(QFrame.Shape.HLine)
|
|
3065
|
+
bottom_separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
3066
|
+
bottom_separator.setStyleSheet("background-color: #E0E0E0; min-height: 1px; max-height: 1px;")
|
|
3067
|
+
layout.addWidget(bottom_separator)
|
|
3068
|
+
|
|
3069
|
+
# Add cancel button
|
|
3070
|
+
button_layout = QHBoxLayout()
|
|
3071
|
+
button_layout.setSpacing(12)
|
|
3072
|
+
button_layout.setContentsMargins(0, 16, 0, 0)
|
|
3073
|
+
button_layout.addStretch()
|
|
3074
|
+
|
|
3075
|
+
cancel_btn = QPushButton("Cancel")
|
|
3076
|
+
cancel_btn.setFixedWidth(100)
|
|
3077
|
+
cancel_btn.setStyleSheet("""
|
|
3078
|
+
background-color: #F5F5F5;
|
|
3079
|
+
border: 1px solid #E0E0E0;
|
|
3080
|
+
border-radius: 6px;
|
|
3081
|
+
padding: 8px 16px;
|
|
3082
|
+
color: #7F8C8D;
|
|
3083
|
+
font-weight: bold;
|
|
3084
|
+
""")
|
|
3085
|
+
cancel_btn.clicked.connect(dialog.reject)
|
|
3086
|
+
button_layout.addWidget(cancel_btn)
|
|
3087
|
+
|
|
3088
|
+
layout.addLayout(button_layout)
|
|
3089
|
+
|
|
3090
|
+
# Apply modern drop shadow effect to the dialog
|
|
3091
|
+
try:
|
|
3092
|
+
dialog.setGraphicsEffect(None) # Clear any existing effects
|
|
3093
|
+
shadow = QGraphicsDropShadowEffect(dialog)
|
|
3094
|
+
shadow.setBlurRadius(20)
|
|
3095
|
+
shadow.setColor(QColor(0, 0, 0, 50)) # Semi-transparent black
|
|
3096
|
+
shadow.setOffset(0, 0)
|
|
3097
|
+
dialog.setGraphicsEffect(shadow)
|
|
3098
|
+
except Exception:
|
|
3099
|
+
pass # Skip shadow if there are any issues
|
|
3100
|
+
|
|
3101
|
+
# Add custom styling to make the dialog look modern
|
|
3102
|
+
dialog.setStyleSheet("""
|
|
3103
|
+
QDialog {
|
|
3104
|
+
background-color: #FFFFFF;
|
|
3105
|
+
border-radius: 12px;
|
|
3106
|
+
}
|
|
3107
|
+
QLabel {
|
|
3108
|
+
color: #2C3E50;
|
|
3109
|
+
}
|
|
3110
|
+
""")
|
|
3111
|
+
|
|
3112
|
+
# Store dialog animation references in the instance to prevent garbage collection
|
|
3113
|
+
dialog._animations = animations
|
|
3114
|
+
|
|
3115
|
+
# Center the dialog on the parent window
|
|
3116
|
+
if self.geometry().isValid():
|
|
3117
|
+
dialog.move(
|
|
3118
|
+
self.geometry().center().x() - dialog.width() // 2,
|
|
3119
|
+
self.geometry().center().y() - dialog.height() // 2
|
|
3120
|
+
)
|
|
3121
|
+
|
|
3122
|
+
# Show the dialog
|
|
3123
|
+
dialog.exec()
|
|
3124
|
+
|
|
3125
|
+
def handle_load_option(self, dialog, option):
|
|
3126
|
+
"""Handle the selected load option"""
|
|
3127
|
+
# Close the dialog
|
|
3128
|
+
dialog.accept()
|
|
3129
|
+
|
|
3130
|
+
# Call the appropriate function based on the selected option
|
|
3131
|
+
if option == "database":
|
|
3132
|
+
self.open_database()
|
|
3133
|
+
elif option == "files":
|
|
3134
|
+
self.browse_files()
|
|
3135
|
+
elif option == "delta":
|
|
3136
|
+
self.load_delta_table()
|
|
3137
|
+
elif option == "test":
|
|
3138
|
+
self.load_test_data()
|
|
3139
|
+
|
|
3140
|
+
def analyze_table_entropy(self, table_name):
|
|
3141
|
+
"""Analyze a table with the entropy profiler to identify important columns"""
|
|
3142
|
+
try:
|
|
3143
|
+
# Show a loading indicator
|
|
3144
|
+
self.statusBar().showMessage(f'Analyzing table "{table_name}" columns...')
|
|
3145
|
+
|
|
3146
|
+
# Get the table data
|
|
3147
|
+
if table_name in self.db_manager.loaded_tables:
|
|
3148
|
+
# Check if table needs reloading first
|
|
3149
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
3150
|
+
# Reload the table immediately
|
|
3151
|
+
self.reload_selected_table(table_name)
|
|
3152
|
+
|
|
3153
|
+
# Get the data as a dataframe
|
|
3154
|
+
query = f'SELECT * FROM "{table_name}"'
|
|
3155
|
+
df = self.db_manager.execute_query(query)
|
|
3156
|
+
|
|
3157
|
+
if df is not None and not df.empty:
|
|
3158
|
+
# Import the entropy profiler
|
|
3159
|
+
from sqlshell.utils.profile_entropy import visualize_profile
|
|
3160
|
+
|
|
3161
|
+
# Create and show the visualization
|
|
3162
|
+
self.statusBar().showMessage(f'Generating entropy profile for "{table_name}"...')
|
|
3163
|
+
vis = visualize_profile(df)
|
|
3164
|
+
|
|
3165
|
+
# Store a reference to prevent garbage collection
|
|
3166
|
+
self._entropy_window = vis
|
|
3167
|
+
|
|
3168
|
+
self.statusBar().showMessage(f'Entropy profile generated for "{table_name}"')
|
|
3169
|
+
else:
|
|
3170
|
+
QMessageBox.warning(self, "Empty Table", f"Table '{table_name}' has no data to analyze.")
|
|
3171
|
+
self.statusBar().showMessage(f'Table "{table_name}" is empty - cannot analyze')
|
|
3172
|
+
else:
|
|
3173
|
+
QMessageBox.warning(self, "Table Not Found", f"Table '{table_name}' not found.")
|
|
3174
|
+
self.statusBar().showMessage(f'Table "{table_name}" not found')
|
|
3175
|
+
|
|
3176
|
+
except Exception as e:
|
|
3177
|
+
QMessageBox.critical(self, "Analysis Error", f"Error analyzing table:\n\n{str(e)}")
|
|
3178
|
+
self.statusBar().showMessage(f'Error analyzing table: {str(e)}')
|
|
3179
|
+
|
|
3180
|
+
def profile_table_structure(self, table_name):
|
|
3181
|
+
"""Analyze a table's structure to identify candidate keys and functional dependencies"""
|
|
3182
|
+
try:
|
|
3183
|
+
# Show a loading indicator
|
|
3184
|
+
self.statusBar().showMessage(f'Profiling table structure for "{table_name}"...')
|
|
3185
|
+
|
|
3186
|
+
# Get the table data
|
|
3187
|
+
if table_name in self.db_manager.loaded_tables:
|
|
3188
|
+
# Check if table needs reloading first
|
|
3189
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
3190
|
+
# Reload the table immediately
|
|
3191
|
+
self.reload_selected_table(table_name)
|
|
3192
|
+
|
|
3193
|
+
# Get the data as a dataframe
|
|
3194
|
+
query = f'SELECT * FROM "{table_name}"'
|
|
3195
|
+
df = self.db_manager.execute_query(query)
|
|
3196
|
+
|
|
3197
|
+
if df is not None and not df.empty:
|
|
3198
|
+
# Import the key profiler
|
|
3199
|
+
from sqlshell.utils.profile_keys import visualize_profile
|
|
3200
|
+
|
|
3201
|
+
# Create and show the visualization
|
|
3202
|
+
self.statusBar().showMessage(f'Generating table profile for "{table_name}"...')
|
|
3203
|
+
vis = visualize_profile(df)
|
|
3204
|
+
|
|
3205
|
+
# Store a reference to prevent garbage collection
|
|
3206
|
+
self._keys_profile_window = vis
|
|
3207
|
+
|
|
3208
|
+
self.statusBar().showMessage(f'Table structure profile generated for "{table_name}"')
|
|
3209
|
+
else:
|
|
3210
|
+
QMessageBox.warning(self, "Empty Table", f"Table '{table_name}' has no data to analyze.")
|
|
3211
|
+
self.statusBar().showMessage(f'Table "{table_name}" is empty - cannot analyze')
|
|
3212
|
+
else:
|
|
3213
|
+
QMessageBox.warning(self, "Table Not Found", f"Table '{table_name}' not found.")
|
|
3214
|
+
self.statusBar().showMessage(f'Table "{table_name}" not found')
|
|
3215
|
+
|
|
3216
|
+
except Exception as e:
|
|
3217
|
+
QMessageBox.critical(self, "Profile Error", f"Error profiling table structure:\n\n{str(e)}")
|
|
3218
|
+
self.statusBar().showMessage(f'Error profiling table: {str(e)}')
|
|
3219
|
+
|
|
2320
3220
|
def main():
|
|
3221
|
+
# Parse command line arguments
|
|
3222
|
+
parser = argparse.ArgumentParser(description='SQL Shell - SQL Query Tool')
|
|
3223
|
+
parser.add_argument('--no-auto-load', action='store_true',
|
|
3224
|
+
help='Disable auto-loading the most recent project at startup')
|
|
3225
|
+
args = parser.parse_args()
|
|
3226
|
+
|
|
2321
3227
|
app = QApplication(sys.argv)
|
|
2322
3228
|
app.setStyle(QStyleFactory.create('Fusion'))
|
|
2323
3229
|
|
|
@@ -2358,6 +3264,10 @@ def main():
|
|
|
2358
3264
|
print("Initializing main application...")
|
|
2359
3265
|
window = SQLShell()
|
|
2360
3266
|
|
|
3267
|
+
# Override auto-load setting if command-line argument is provided
|
|
3268
|
+
if args.no_auto_load:
|
|
3269
|
+
window.auto_load_recent_project = False
|
|
3270
|
+
|
|
2361
3271
|
# Define the function to show main window and hide splash
|
|
2362
3272
|
def show_main_window():
|
|
2363
3273
|
# Properly finish the splash screen
|