sqlshell 0.1.6__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlshell might be problematic. Click here for more details.
- sqlshell/__init__.py +4 -2
- sqlshell/create_test_data.py +50 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/db/__init__.py +5 -0
- sqlshell/db/database_manager.py +691 -0
- sqlshell/editor.py +856 -0
- sqlshell/main.py +1904 -961
- sqlshell/query_tab.py +172 -0
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/create_icon.py +131 -0
- sqlshell/resources/create_splash.py +96 -0
- sqlshell/resources/icon.png +0 -0
- sqlshell/resources/logo_large.png +0 -0
- sqlshell/resources/logo_medium.png +0 -0
- sqlshell/resources/logo_small.png +0 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/setup.py +1 -1
- sqlshell/splash_screen.py +405 -0
- sqlshell/sqlshell/create_test_data.py +4 -23
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/syntax_highlighter.py +123 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +403 -0
- sqlshell-0.1.9.dist-info/METADATA +122 -0
- sqlshell-0.1.9.dist-info/RECORD +31 -0
- {sqlshell-0.1.6.dist-info → sqlshell-0.1.9.dist-info}/WHEEL +1 -1
- sqlshell-0.1.6.dist-info/METADATA +0 -92
- sqlshell-0.1.6.dist-info/RECORD +0 -11
- {sqlshell-0.1.6.dist-info → sqlshell-0.1.9.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.6.dist-info → sqlshell-0.1.9.dist-info}/top_level.txt +0 -0
sqlshell/main.py
CHANGED
|
@@ -1,500 +1,152 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import os
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
# Ensure proper path setup for resources when running directly
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
+
sys.path.insert(0, project_root)
|
|
9
|
+
|
|
5
10
|
import pandas as pd
|
|
6
11
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
7
12
|
QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
|
|
8
13
|
QLabel, QSplitter, QListWidget, QTableWidget,
|
|
9
14
|
QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
|
|
10
15
|
QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
|
|
11
|
-
QStyleFactory, QToolBar, QStatusBar
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
QStyleFactory, QToolBar, QStatusBar, QLineEdit, QMenu,
|
|
17
|
+
QCheckBox, QWidgetAction, QMenuBar, QInputDialog, QProgressDialog,
|
|
18
|
+
QListWidgetItem)
|
|
19
|
+
from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint, QMimeData
|
|
20
|
+
from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath, QDrag
|
|
14
21
|
import numpy as np
|
|
15
22
|
from datetime import datetime
|
|
16
|
-
from sqlshell.sqlshell import create_test_data # Import from the correct location
|
|
17
|
-
|
|
18
|
-
class SQLSyntaxHighlighter(QSyntaxHighlighter):
|
|
19
|
-
def __init__(self, document):
|
|
20
|
-
super().__init__(document)
|
|
21
|
-
self.highlighting_rules = []
|
|
22
|
-
|
|
23
|
-
# SQL Keywords
|
|
24
|
-
keyword_format = QTextCharFormat()
|
|
25
|
-
keyword_format.setForeground(QColor("#0000FF")) # Blue
|
|
26
|
-
keyword_format.setFontWeight(QFont.Weight.Bold)
|
|
27
|
-
keywords = [
|
|
28
|
-
"\\bSELECT\\b", "\\bFROM\\b", "\\bWHERE\\b", "\\bAND\\b", "\\bOR\\b",
|
|
29
|
-
"\\bINNER\\b", "\\bOUTER\\b", "\\bLEFT\\b", "\\bRIGHT\\b", "\\bJOIN\\b",
|
|
30
|
-
"\\bON\\b", "\\bGROUP\\b", "\\bBY\\b", "\\bHAVING\\b", "\\bORDER\\b",
|
|
31
|
-
"\\bLIMIT\\b", "\\bOFFSET\\b", "\\bUNION\\b", "\\bEXCEPT\\b", "\\bINTERSECT\\b",
|
|
32
|
-
"\\bCREATE\\b", "\\bTABLE\\b", "\\bINDEX\\b", "\\bVIEW\\b", "\\bINSERT\\b",
|
|
33
|
-
"\\bINTO\\b", "\\bVALUES\\b", "\\bUPDATE\\b", "\\bSET\\b", "\\bDELETE\\b",
|
|
34
|
-
"\\bTRUNCATE\\b", "\\bALTER\\b", "\\bADD\\b", "\\bDROP\\b", "\\bCOLUMN\\b",
|
|
35
|
-
"\\bCONSTRAINT\\b", "\\bPRIMARY\\b", "\\bKEY\\b", "\\bFOREIGN\\b", "\\bREFERENCES\\b",
|
|
36
|
-
"\\bUNIQUE\\b", "\\bNOT\\b", "\\bNULL\\b", "\\bIS\\b", "\\bDISTINCT\\b",
|
|
37
|
-
"\\bCASE\\b", "\\bWHEN\\b", "\\bTHEN\\b", "\\bELSE\\b", "\\bEND\\b",
|
|
38
|
-
"\\bAS\\b", "\\bWITH\\b", "\\bBETWEEN\\b", "\\bLIKE\\b", "\\bIN\\b",
|
|
39
|
-
"\\bEXISTS\\b", "\\bALL\\b", "\\bANY\\b", "\\bSOME\\b", "\\bDESC\\b", "\\bASC\\b"
|
|
40
|
-
]
|
|
41
|
-
for pattern in keywords:
|
|
42
|
-
regex = QRegularExpression(pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)
|
|
43
|
-
self.highlighting_rules.append((regex, keyword_format))
|
|
44
|
-
|
|
45
|
-
# Functions
|
|
46
|
-
function_format = QTextCharFormat()
|
|
47
|
-
function_format.setForeground(QColor("#AA00AA")) # Purple
|
|
48
|
-
functions = [
|
|
49
|
-
"\\bAVG\\b", "\\bCOUNT\\b", "\\bSUM\\b", "\\bMAX\\b", "\\bMIN\\b",
|
|
50
|
-
"\\bCOALESCE\\b", "\\bNVL\\b", "\\bNULLIF\\b", "\\bCAST\\b", "\\bCONVERT\\b",
|
|
51
|
-
"\\bLOWER\\b", "\\bUPPER\\b", "\\bTRIM\\b", "\\bLTRIM\\b", "\\bRTRIM\\b",
|
|
52
|
-
"\\bLENGTH\\b", "\\bSUBSTRING\\b", "\\bREPLACE\\b", "\\bCONCAT\\b",
|
|
53
|
-
"\\bROUND\\b", "\\bFLOOR\\b", "\\bCEIL\\b", "\\bABS\\b", "\\bMOD\\b",
|
|
54
|
-
"\\bCURRENT_DATE\\b", "\\bCURRENT_TIME\\b", "\\bCURRENT_TIMESTAMP\\b",
|
|
55
|
-
"\\bEXTRACT\\b", "\\bDATE_PART\\b", "\\bTO_CHAR\\b", "\\bTO_DATE\\b"
|
|
56
|
-
]
|
|
57
|
-
for pattern in functions:
|
|
58
|
-
regex = QRegularExpression(pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)
|
|
59
|
-
self.highlighting_rules.append((regex, function_format))
|
|
60
|
-
|
|
61
|
-
# Numbers
|
|
62
|
-
number_format = QTextCharFormat()
|
|
63
|
-
number_format.setForeground(QColor("#009900")) # Green
|
|
64
|
-
self.highlighting_rules.append((
|
|
65
|
-
QRegularExpression("\\b[0-9]+\\b"),
|
|
66
|
-
number_format
|
|
67
|
-
))
|
|
68
|
-
|
|
69
|
-
# Single-line string literals
|
|
70
|
-
string_format = QTextCharFormat()
|
|
71
|
-
string_format.setForeground(QColor("#CC6600")) # Orange/Brown
|
|
72
|
-
self.highlighting_rules.append((
|
|
73
|
-
QRegularExpression("'[^']*'"),
|
|
74
|
-
string_format
|
|
75
|
-
))
|
|
76
|
-
self.highlighting_rules.append((
|
|
77
|
-
QRegularExpression("\"[^\"]*\""),
|
|
78
|
-
string_format
|
|
79
|
-
))
|
|
80
|
-
|
|
81
|
-
# Comments
|
|
82
|
-
comment_format = QTextCharFormat()
|
|
83
|
-
comment_format.setForeground(QColor("#777777")) # Gray
|
|
84
|
-
comment_format.setFontItalic(True)
|
|
85
|
-
self.highlighting_rules.append((
|
|
86
|
-
QRegularExpression("--[^\n]*"),
|
|
87
|
-
comment_format
|
|
88
|
-
))
|
|
89
|
-
|
|
90
|
-
# Multi-line comments
|
|
91
|
-
self.comment_start_expression = QRegularExpression("/\\*")
|
|
92
|
-
self.comment_end_expression = QRegularExpression("\\*/")
|
|
93
|
-
self.multi_line_comment_format = comment_format
|
|
94
|
-
|
|
95
|
-
def highlightBlock(self, text):
|
|
96
|
-
# Apply regular expression highlighting rules
|
|
97
|
-
for pattern, format in self.highlighting_rules:
|
|
98
|
-
match_iterator = pattern.globalMatch(text)
|
|
99
|
-
while match_iterator.hasNext():
|
|
100
|
-
match = match_iterator.next()
|
|
101
|
-
self.setFormat(match.capturedStart(), match.capturedLength(), format)
|
|
102
|
-
|
|
103
|
-
# Handle multi-line comments
|
|
104
|
-
self.setCurrentBlockState(0)
|
|
105
|
-
|
|
106
|
-
# If previous block was inside a comment, check if this block continues it
|
|
107
|
-
start_index = 0
|
|
108
|
-
if self.previousBlockState() != 1:
|
|
109
|
-
# Find the start of a comment
|
|
110
|
-
start_match = self.comment_start_expression.match(text)
|
|
111
|
-
if start_match.hasMatch():
|
|
112
|
-
start_index = start_match.capturedStart()
|
|
113
|
-
else:
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
while start_index >= 0:
|
|
117
|
-
# Find the end of the comment
|
|
118
|
-
end_match = self.comment_end_expression.match(text, start_index)
|
|
119
|
-
|
|
120
|
-
# If end match found
|
|
121
|
-
if end_match.hasMatch():
|
|
122
|
-
end_index = end_match.capturedStart()
|
|
123
|
-
comment_length = end_index - start_index + end_match.capturedLength()
|
|
124
|
-
self.setFormat(start_index, comment_length, self.multi_line_comment_format)
|
|
125
|
-
|
|
126
|
-
# Look for next comment
|
|
127
|
-
start_match = self.comment_start_expression.match(text, start_index + comment_length)
|
|
128
|
-
if start_match.hasMatch():
|
|
129
|
-
start_index = start_match.capturedStart()
|
|
130
|
-
else:
|
|
131
|
-
start_index = -1
|
|
132
|
-
else:
|
|
133
|
-
# No end found, comment continues to next block
|
|
134
|
-
self.setCurrentBlockState(1) # Still inside comment
|
|
135
|
-
comment_length = len(text) - start_index
|
|
136
|
-
self.setFormat(start_index, comment_length, self.multi_line_comment_format)
|
|
137
|
-
start_index = -1
|
|
138
|
-
|
|
139
|
-
class LineNumberArea(QWidget):
|
|
140
|
-
def __init__(self, editor):
|
|
141
|
-
super().__init__(editor)
|
|
142
|
-
self.editor = editor
|
|
143
23
|
|
|
144
|
-
|
|
145
|
-
|
|
24
|
+
from sqlshell import create_test_data
|
|
25
|
+
from sqlshell.splash_screen import AnimatedSplashScreen
|
|
26
|
+
from sqlshell.syntax_highlighter import SQLSyntaxHighlighter
|
|
27
|
+
from sqlshell.editor import LineNumberArea, SQLEditor
|
|
28
|
+
from sqlshell.ui import FilterHeader, BarChartDelegate
|
|
29
|
+
from sqlshell.db import DatabaseManager
|
|
30
|
+
from sqlshell.query_tab import QueryTab
|
|
146
31
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
class SQLEditor(QPlainTextEdit):
|
|
32
|
+
class DraggableTablesList(QListWidget):
|
|
33
|
+
"""Custom QListWidget that provides better drag functionality for table names."""
|
|
34
|
+
|
|
151
35
|
def __init__(self, parent=None):
|
|
152
36
|
super().__init__(parent)
|
|
153
|
-
self.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
font = QFont("Consolas", 12) # Increased font size for better readability
|
|
157
|
-
font.setFixedPitch(True)
|
|
158
|
-
self.setFont(font)
|
|
159
|
-
|
|
160
|
-
# Connect signals
|
|
161
|
-
self.blockCountChanged.connect(self.update_line_number_area_width)
|
|
162
|
-
self.updateRequest.connect(self.update_line_number_area)
|
|
163
|
-
|
|
164
|
-
# Initialize
|
|
165
|
-
self.update_line_number_area_width(0)
|
|
166
|
-
|
|
167
|
-
# Set tab width to 4 spaces
|
|
168
|
-
self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
|
|
169
|
-
|
|
170
|
-
# Set placeholder text
|
|
171
|
-
self.setPlaceholderText("Enter your SQL query here...")
|
|
172
|
-
|
|
173
|
-
# Initialize completer
|
|
174
|
-
self.completer = None
|
|
175
|
-
|
|
176
|
-
# SQL Keywords for autocomplete
|
|
177
|
-
self.sql_keywords = [
|
|
178
|
-
"SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
|
|
179
|
-
"ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
|
|
180
|
-
"CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
|
|
181
|
-
"TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
|
|
182
|
-
"UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
|
|
183
|
-
"AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
|
|
184
|
-
"AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
|
|
185
|
-
]
|
|
186
|
-
|
|
187
|
-
# Initialize with SQL keywords
|
|
188
|
-
self.set_completer(QCompleter(self.sql_keywords))
|
|
189
|
-
|
|
190
|
-
# Set modern selection color
|
|
191
|
-
self.selection_color = QColor("#3498DB")
|
|
192
|
-
self.selection_color.setAlpha(50) # Make it semi-transparent
|
|
193
|
-
|
|
194
|
-
def set_completer(self, completer):
|
|
195
|
-
"""Set the completer for the editor"""
|
|
196
|
-
if self.completer:
|
|
197
|
-
self.completer.disconnect(self)
|
|
198
|
-
|
|
199
|
-
self.completer = completer
|
|
200
|
-
|
|
201
|
-
if not self.completer:
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
self.completer.setWidget(self)
|
|
205
|
-
self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
|
206
|
-
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
|
207
|
-
self.completer.activated.connect(self.insert_completion)
|
|
208
|
-
|
|
209
|
-
def update_completer_model(self, words):
|
|
210
|
-
"""Update the completer model with new words"""
|
|
211
|
-
if not self.completer:
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
# Combine SQL keywords with table/column names
|
|
215
|
-
all_words = self.sql_keywords + words
|
|
216
|
-
|
|
217
|
-
# Create a model with all words
|
|
218
|
-
model = QStringListModel()
|
|
219
|
-
model.setStringList(all_words)
|
|
220
|
-
|
|
221
|
-
# Set the model to the completer
|
|
222
|
-
self.completer.setModel(model)
|
|
223
|
-
|
|
224
|
-
def text_under_cursor(self):
|
|
225
|
-
"""Get the text under the cursor for completion"""
|
|
226
|
-
tc = self.textCursor()
|
|
227
|
-
tc.select(QTextCursor.SelectionType.WordUnderCursor)
|
|
228
|
-
return tc.selectedText()
|
|
229
|
-
|
|
230
|
-
def insert_completion(self, completion):
|
|
231
|
-
"""Insert the completion text"""
|
|
232
|
-
if self.completer.widget() != self:
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
tc = self.textCursor()
|
|
236
|
-
extra = len(completion) - len(self.completer.completionPrefix())
|
|
237
|
-
tc.movePosition(QTextCursor.MoveOperation.Left)
|
|
238
|
-
tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
|
|
239
|
-
tc.insertText(completion[-extra:] + " ")
|
|
240
|
-
self.setTextCursor(tc)
|
|
241
|
-
|
|
242
|
-
def complete(self):
|
|
243
|
-
"""Show completion popup"""
|
|
244
|
-
prefix = self.text_under_cursor()
|
|
37
|
+
self.parent = parent
|
|
38
|
+
self.setDragEnabled(True)
|
|
39
|
+
self.setDragDropMode(QListWidget.DragDropMode.DragOnly)
|
|
245
40
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# Calculate position for the popup
|
|
263
|
-
cr = self.cursorRect()
|
|
264
|
-
cr.setWidth(self.completer.popup().sizeHintForColumn(0) +
|
|
265
|
-
self.completer.popup().verticalScrollBar().sizeHint().width())
|
|
266
|
-
|
|
267
|
-
# Show the popup
|
|
268
|
-
self.completer.complete(cr)
|
|
269
|
-
|
|
270
|
-
def keyPressEvent(self, event):
|
|
271
|
-
# Handle completer popup navigation
|
|
272
|
-
if self.completer and self.completer.popup().isVisible():
|
|
273
|
-
# Handle navigation keys for the popup
|
|
274
|
-
if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Tab,
|
|
275
|
-
Qt.Key.Key_Escape, Qt.Key.Key_Up, Qt.Key.Key_Down]:
|
|
276
|
-
event.ignore()
|
|
277
|
-
return
|
|
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
|
+
""")
|
|
278
56
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
# Auto-indentation for new lines
|
|
286
|
-
if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
|
|
287
|
-
cursor = self.textCursor()
|
|
288
|
-
block = cursor.block()
|
|
289
|
-
text = block.text()
|
|
290
|
-
|
|
291
|
-
# Get the indentation of the current line
|
|
292
|
-
indentation = ""
|
|
293
|
-
for char in text:
|
|
294
|
-
if char.isspace():
|
|
295
|
-
indentation += char
|
|
296
|
-
else:
|
|
297
|
-
break
|
|
298
|
-
|
|
299
|
-
# Check if line ends with an opening bracket or keywords that should increase indentation
|
|
300
|
-
increase_indent = ""
|
|
301
|
-
if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
|
|
302
|
-
["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
|
|
303
|
-
increase_indent = " "
|
|
304
|
-
|
|
305
|
-
# Insert new line with proper indentation
|
|
306
|
-
super().keyPressEvent(event)
|
|
307
|
-
self.insertPlainText(indentation + increase_indent)
|
|
57
|
+
def startDrag(self, supportedActions):
|
|
58
|
+
"""Override startDrag to customize the drag data."""
|
|
59
|
+
item = self.currentItem()
|
|
60
|
+
if not item:
|
|
308
61
|
return
|
|
309
62
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
# Show temporary hint in status bar when editor gets focus
|
|
384
|
-
if hasattr(self.parent(), 'statusBar'):
|
|
385
|
-
self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
|
|
386
|
-
|
|
387
|
-
def toggle_comment(self):
|
|
388
|
-
cursor = self.textCursor()
|
|
389
|
-
if cursor.hasSelection():
|
|
390
|
-
# Get the selected text
|
|
391
|
-
start = cursor.selectionStart()
|
|
392
|
-
end = cursor.selectionEnd()
|
|
393
|
-
|
|
394
|
-
# Remember the selection
|
|
395
|
-
cursor.setPosition(start)
|
|
396
|
-
start_block = cursor.blockNumber()
|
|
397
|
-
cursor.setPosition(end)
|
|
398
|
-
end_block = cursor.blockNumber()
|
|
399
|
-
|
|
400
|
-
# Process each line in the selection
|
|
401
|
-
cursor.setPosition(start)
|
|
402
|
-
cursor.beginEditBlock()
|
|
403
|
-
|
|
404
|
-
for _ in range(start_block, end_block + 1):
|
|
405
|
-
# Move to start of line
|
|
406
|
-
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
407
|
-
|
|
408
|
-
# Check if the line is already commented
|
|
409
|
-
line_text = cursor.block().text().lstrip()
|
|
410
|
-
if line_text.startswith('--'):
|
|
411
|
-
# Remove comment
|
|
412
|
-
pos = cursor.block().text().find('--')
|
|
413
|
-
cursor.setPosition(cursor.block().position() + pos)
|
|
414
|
-
cursor.deleteChar()
|
|
415
|
-
cursor.deleteChar()
|
|
416
|
-
else:
|
|
417
|
-
# Add comment
|
|
418
|
-
cursor.insertText('--')
|
|
419
|
-
|
|
420
|
-
# Move to next line if not at the end
|
|
421
|
-
if not cursor.atEnd():
|
|
422
|
-
cursor.movePosition(cursor.MoveOperation.NextBlock)
|
|
423
|
-
|
|
424
|
-
cursor.endEditBlock()
|
|
425
|
-
else:
|
|
426
|
-
# Comment/uncomment current line
|
|
427
|
-
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
428
|
-
cursor.movePosition(cursor.MoveOperation.EndOfLine, cursor.MoveMode.KeepAnchor)
|
|
429
|
-
line_text = cursor.selectedText().lstrip()
|
|
430
|
-
|
|
431
|
-
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
432
|
-
if line_text.startswith('--'):
|
|
433
|
-
# Remove comment
|
|
434
|
-
pos = cursor.block().text().find('--')
|
|
435
|
-
cursor.setPosition(cursor.block().position() + pos)
|
|
436
|
-
cursor.deleteChar()
|
|
437
|
-
cursor.deleteChar()
|
|
438
|
-
else:
|
|
439
|
-
# Add comment
|
|
440
|
-
cursor.insertText('--')
|
|
441
|
-
|
|
442
|
-
def line_number_area_width(self):
|
|
443
|
-
digits = 1
|
|
444
|
-
max_num = max(1, self.blockCount())
|
|
445
|
-
while max_num >= 10:
|
|
446
|
-
max_num //= 10
|
|
447
|
-
digits += 1
|
|
448
|
-
|
|
449
|
-
space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
|
|
450
|
-
return space
|
|
451
|
-
|
|
452
|
-
def update_line_number_area_width(self, _):
|
|
453
|
-
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
|
|
454
|
-
|
|
455
|
-
def update_line_number_area(self, rect, dy):
|
|
456
|
-
if dy:
|
|
457
|
-
self.line_number_area.scroll(0, dy)
|
|
458
|
-
else:
|
|
459
|
-
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
|
|
460
|
-
|
|
461
|
-
if rect.contains(self.viewport().rect()):
|
|
462
|
-
self.update_line_number_area_width(0)
|
|
463
|
-
|
|
464
|
-
def resizeEvent(self, event):
|
|
465
|
-
super().resizeEvent(event)
|
|
466
|
-
cr = self.contentsRect()
|
|
467
|
-
self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
|
|
468
|
-
|
|
469
|
-
def line_number_area_paint_event(self, event):
|
|
470
|
-
painter = QPainter(self.line_number_area)
|
|
471
|
-
painter.fillRect(event.rect(), QColor("#f0f0f0")) # Light gray background
|
|
472
|
-
|
|
473
|
-
block = self.firstVisibleBlock()
|
|
474
|
-
block_number = block.blockNumber()
|
|
475
|
-
top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
|
|
476
|
-
bottom = top + round(self.blockBoundingRect(block).height())
|
|
477
|
-
|
|
478
|
-
while block.isValid() and top <= event.rect().bottom():
|
|
479
|
-
if block.isVisible() and bottom >= event.rect().top():
|
|
480
|
-
number = str(block_number + 1)
|
|
481
|
-
painter.setPen(QColor("#808080")) # Gray text
|
|
482
|
-
painter.drawText(0, top, self.line_number_area.width() - 5,
|
|
483
|
-
self.fontMetrics().height(),
|
|
484
|
-
Qt.AlignmentFlag.AlignRight, number)
|
|
485
|
-
|
|
486
|
-
block = block.next()
|
|
487
|
-
top = bottom
|
|
488
|
-
bottom = top + round(self.blockBoundingRect(block).height())
|
|
489
|
-
block_number += 1
|
|
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))
|
|
490
136
|
|
|
491
137
|
class SQLShell(QMainWindow):
|
|
492
138
|
def __init__(self):
|
|
493
139
|
super().__init__()
|
|
494
|
-
self.
|
|
495
|
-
self.
|
|
496
|
-
self.
|
|
497
|
-
self.
|
|
140
|
+
self.db_manager = DatabaseManager()
|
|
141
|
+
self.current_df = None # Store the current DataFrame for filtering
|
|
142
|
+
self.filter_widgets = [] # Store filter line edits
|
|
143
|
+
self.current_project_file = None # Store the current project file path
|
|
144
|
+
self.recent_projects = [] # Store list of recent projects
|
|
145
|
+
self.max_recent_projects = 10 # Maximum number of recent projects to track
|
|
146
|
+
self.tabs = [] # Store list of all tabs
|
|
147
|
+
|
|
148
|
+
# Load recent projects from settings
|
|
149
|
+
self.load_recent_projects()
|
|
498
150
|
|
|
499
151
|
# Define color scheme
|
|
500
152
|
self.colors = {
|
|
@@ -514,6 +166,9 @@ class SQLShell(QMainWindow):
|
|
|
514
166
|
|
|
515
167
|
self.init_ui()
|
|
516
168
|
self.apply_stylesheet()
|
|
169
|
+
|
|
170
|
+
# Create initial tab
|
|
171
|
+
self.add_tab()
|
|
517
172
|
|
|
518
173
|
def apply_stylesheet(self):
|
|
519
174
|
"""Apply custom stylesheet to the application"""
|
|
@@ -658,6 +313,44 @@ class SQLShell(QMainWindow):
|
|
|
658
313
|
padding: 8px;
|
|
659
314
|
}}
|
|
660
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
|
+
|
|
661
354
|
QPlainTextEdit, QTextEdit {{
|
|
662
355
|
background-color: white;
|
|
663
356
|
border-radius: 4px;
|
|
@@ -672,7 +365,112 @@ class SQLShell(QMainWindow):
|
|
|
672
365
|
|
|
673
366
|
def init_ui(self):
|
|
674
367
|
self.setWindowTitle('SQL Shell')
|
|
675
|
-
|
|
368
|
+
|
|
369
|
+
# Get screen geometry for smart sizing
|
|
370
|
+
screen = QApplication.primaryScreen()
|
|
371
|
+
screen_geometry = screen.availableGeometry()
|
|
372
|
+
screen_width = screen_geometry.width()
|
|
373
|
+
screen_height = screen_geometry.height()
|
|
374
|
+
|
|
375
|
+
# Calculate adaptive window size based on screen size
|
|
376
|
+
# Use 85% of screen size for larger screens, fixed size for smaller screens
|
|
377
|
+
if screen_width >= 1920 and screen_height >= 1080: # Larger screens
|
|
378
|
+
window_width = int(screen_width * 0.85)
|
|
379
|
+
window_height = int(screen_height * 0.85)
|
|
380
|
+
self.setGeometry(
|
|
381
|
+
(screen_width - window_width) // 2, # Center horizontally
|
|
382
|
+
(screen_height - window_height) // 2, # Center vertically
|
|
383
|
+
window_width,
|
|
384
|
+
window_height
|
|
385
|
+
)
|
|
386
|
+
else: # Default for smaller screens
|
|
387
|
+
self.setGeometry(100, 100, 1400, 800)
|
|
388
|
+
|
|
389
|
+
# Remember if the window was maximized
|
|
390
|
+
self.was_maximized = False
|
|
391
|
+
|
|
392
|
+
# Set application icon
|
|
393
|
+
icon_path = os.path.join(os.path.dirname(__file__), "resources", "icon.png")
|
|
394
|
+
if os.path.exists(icon_path):
|
|
395
|
+
self.setWindowIcon(QIcon(icon_path))
|
|
396
|
+
else:
|
|
397
|
+
# Fallback to the main logo if the icon isn't found
|
|
398
|
+
main_logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "sqlshell_logo.png")
|
|
399
|
+
if os.path.exists(main_logo_path):
|
|
400
|
+
self.setWindowIcon(QIcon(main_logo_path))
|
|
401
|
+
|
|
402
|
+
# Create menu bar
|
|
403
|
+
menubar = self.menuBar()
|
|
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))
|
|
447
|
+
|
|
448
|
+
zoom_out_action = zoom_menu.addAction('Zoom Out')
|
|
449
|
+
zoom_out_action.setShortcut('Ctrl+-')
|
|
450
|
+
zoom_out_action.triggered.connect(lambda: self.change_zoom(0.9))
|
|
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)
|
|
676
474
|
|
|
677
475
|
# Create custom status bar
|
|
678
476
|
status_bar = QStatusBar()
|
|
@@ -743,28 +541,16 @@ class SQLShell(QMainWindow):
|
|
|
743
541
|
left_layout.addLayout(table_actions_layout)
|
|
744
542
|
|
|
745
543
|
# Tables list with custom styling
|
|
746
|
-
self.tables_list =
|
|
747
|
-
self.tables_list.setStyleSheet("""
|
|
748
|
-
QListWidget {
|
|
749
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
750
|
-
border: none;
|
|
751
|
-
border-radius: 4px;
|
|
752
|
-
color: white;
|
|
753
|
-
}
|
|
754
|
-
QListWidget::item:selected {
|
|
755
|
-
background-color: rgba(255, 255, 255, 0.2);
|
|
756
|
-
}
|
|
757
|
-
QListWidget::item:hover:!selected {
|
|
758
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
759
|
-
}
|
|
760
|
-
""")
|
|
544
|
+
self.tables_list = DraggableTablesList()
|
|
761
545
|
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
546
|
+
self.tables_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
547
|
+
self.tables_list.customContextMenuRequested.connect(self.show_tables_context_menu)
|
|
762
548
|
left_layout.addWidget(self.tables_list)
|
|
763
549
|
|
|
764
550
|
# Add spacer at the bottom
|
|
765
551
|
left_layout.addStretch()
|
|
766
552
|
|
|
767
|
-
# Right panel for query and results
|
|
553
|
+
# Right panel for query tabs and results
|
|
768
554
|
right_panel = QFrame()
|
|
769
555
|
right_panel.setObjectName("content_panel")
|
|
770
556
|
right_layout = QVBoxLayout(right_panel)
|
|
@@ -776,198 +562,203 @@ class SQLShell(QMainWindow):
|
|
|
776
562
|
query_header.setObjectName("header_label")
|
|
777
563
|
right_layout.addWidget(query_header)
|
|
778
564
|
|
|
779
|
-
# Create
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
query_layout = QVBoxLayout(query_widget)
|
|
788
|
-
query_layout.setContentsMargins(16, 16, 16, 16)
|
|
789
|
-
query_layout.setSpacing(12)
|
|
790
|
-
|
|
791
|
-
# Query input
|
|
792
|
-
self.query_edit = SQLEditor()
|
|
793
|
-
# Apply syntax highlighting to the query editor
|
|
794
|
-
self.sql_highlighter = SQLSyntaxHighlighter(self.query_edit.document())
|
|
795
|
-
query_layout.addWidget(self.query_edit)
|
|
796
|
-
|
|
797
|
-
# Button row
|
|
798
|
-
button_layout = QHBoxLayout()
|
|
799
|
-
button_layout.setSpacing(8)
|
|
800
|
-
|
|
801
|
-
self.execute_btn = QPushButton('Execute Query')
|
|
802
|
-
self.execute_btn.setObjectName("primary_button")
|
|
803
|
-
self.execute_btn.setIcon(QIcon.fromTheme("media-playback-start"))
|
|
804
|
-
self.execute_btn.clicked.connect(self.execute_query)
|
|
805
|
-
self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
|
|
806
|
-
|
|
807
|
-
self.clear_btn = QPushButton('Clear')
|
|
808
|
-
self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
|
|
809
|
-
self.clear_btn.clicked.connect(self.clear_query)
|
|
810
|
-
|
|
811
|
-
button_layout.addWidget(self.execute_btn)
|
|
812
|
-
button_layout.addWidget(self.clear_btn)
|
|
813
|
-
button_layout.addStretch()
|
|
814
|
-
|
|
815
|
-
query_layout.addLayout(button_layout)
|
|
816
|
-
|
|
817
|
-
# Bottom part - Results section
|
|
818
|
-
results_widget = QFrame()
|
|
819
|
-
results_widget.setObjectName("content_panel")
|
|
820
|
-
results_layout = QVBoxLayout(results_widget)
|
|
821
|
-
results_layout.setContentsMargins(16, 16, 16, 16)
|
|
822
|
-
results_layout.setSpacing(12)
|
|
823
|
-
|
|
824
|
-
# Results header with row count and export options
|
|
825
|
-
results_header_layout = QHBoxLayout()
|
|
826
|
-
|
|
827
|
-
results_title = QLabel("RESULTS")
|
|
828
|
-
results_title.setObjectName("header_label")
|
|
829
|
-
|
|
830
|
-
self.row_count_label = QLabel("")
|
|
831
|
-
self.row_count_label.setStyleSheet(f"color: {self.colors['text_light']}; font-style: italic;")
|
|
832
|
-
|
|
833
|
-
results_header_layout.addWidget(results_title)
|
|
834
|
-
results_header_layout.addWidget(self.row_count_label)
|
|
835
|
-
results_header_layout.addStretch()
|
|
836
|
-
|
|
837
|
-
# Export buttons
|
|
838
|
-
export_layout = QHBoxLayout()
|
|
839
|
-
export_layout.setSpacing(8)
|
|
840
|
-
|
|
841
|
-
self.export_excel_btn = QPushButton('Export to Excel')
|
|
842
|
-
self.export_excel_btn.setIcon(QIcon.fromTheme("x-office-spreadsheet"))
|
|
843
|
-
self.export_excel_btn.clicked.connect(self.export_to_excel)
|
|
844
|
-
|
|
845
|
-
self.export_parquet_btn = QPushButton('Export to Parquet')
|
|
846
|
-
self.export_parquet_btn.setIcon(QIcon.fromTheme("document-save"))
|
|
847
|
-
self.export_parquet_btn.clicked.connect(self.export_to_parquet)
|
|
848
|
-
|
|
849
|
-
export_layout.addWidget(self.export_excel_btn)
|
|
850
|
-
export_layout.addWidget(self.export_parquet_btn)
|
|
851
|
-
|
|
852
|
-
results_header_layout.addLayout(export_layout)
|
|
853
|
-
results_layout.addLayout(results_header_layout)
|
|
854
|
-
|
|
855
|
-
# Table widget for results with modern styling
|
|
856
|
-
self.results_table = QTableWidget()
|
|
857
|
-
self.results_table.setSortingEnabled(True)
|
|
858
|
-
self.results_table.setAlternatingRowColors(True)
|
|
859
|
-
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
860
|
-
self.results_table.horizontalHeader().setSectionsMovable(True)
|
|
861
|
-
self.results_table.verticalHeader().setVisible(False)
|
|
862
|
-
self.results_table.setShowGrid(True)
|
|
863
|
-
self.results_table.setGridStyle(Qt.PenStyle.SolidLine)
|
|
864
|
-
self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
|
865
|
-
|
|
866
|
-
results_layout.addWidget(self.results_table)
|
|
867
|
-
|
|
868
|
-
# Add widgets to splitter
|
|
869
|
-
splitter.addWidget(query_widget)
|
|
870
|
-
splitter.addWidget(results_widget)
|
|
565
|
+
# Create tab widget for multiple queries
|
|
566
|
+
self.tab_widget = QTabWidget()
|
|
567
|
+
self.tab_widget.setTabsClosable(True)
|
|
568
|
+
self.tab_widget.setMovable(True)
|
|
569
|
+
self.tab_widget.tabCloseRequested.connect(self.close_tab)
|
|
570
|
+
|
|
571
|
+
# Connect double-click signal for direct tab renaming
|
|
572
|
+
self.tab_widget.tabBarDoubleClicked.connect(self.handle_tab_double_click)
|
|
871
573
|
|
|
872
|
-
#
|
|
873
|
-
|
|
574
|
+
# Add a "+" button to the tab bar
|
|
575
|
+
self.tab_widget.setCornerWidget(self.create_tab_corner_widget())
|
|
874
576
|
|
|
875
|
-
right_layout.addWidget(
|
|
577
|
+
right_layout.addWidget(self.tab_widget)
|
|
876
578
|
|
|
877
579
|
# Add panels to main layout
|
|
878
580
|
main_layout.addWidget(left_panel, 1)
|
|
879
581
|
main_layout.addWidget(right_panel, 4)
|
|
880
582
|
|
|
881
583
|
# Status bar
|
|
882
|
-
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment')
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
)
|
|
584
|
+
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment | Ctrl+T: New Tab')
|
|
585
|
+
|
|
586
|
+
def create_tab_corner_widget(self):
|
|
587
|
+
"""Create a corner widget with a + button to add new tabs"""
|
|
588
|
+
corner_widget = QWidget()
|
|
589
|
+
layout = QHBoxLayout(corner_widget)
|
|
590
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
591
|
+
layout.setSpacing(0)
|
|
592
|
+
|
|
593
|
+
add_tab_btn = QToolButton()
|
|
594
|
+
add_tab_btn.setText("+")
|
|
595
|
+
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
|
+
""")
|
|
613
|
+
add_tab_btn.clicked.connect(self.add_tab)
|
|
614
|
+
|
|
615
|
+
layout.addWidget(add_tab_btn)
|
|
616
|
+
return corner_widget
|
|
617
|
+
|
|
618
|
+
def populate_table(self, df):
|
|
619
|
+
"""Populate the results table with DataFrame data using memory-efficient chunking"""
|
|
620
|
+
try:
|
|
621
|
+
# Get the current tab
|
|
622
|
+
current_tab = self.get_current_tab()
|
|
623
|
+
if not current_tab:
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
# Store the current DataFrame for filtering
|
|
627
|
+
current_tab.current_df = df.copy()
|
|
628
|
+
self.current_df = df.copy() # Keep this for compatibility with existing code
|
|
629
|
+
|
|
630
|
+
# Remember which columns had bar charts
|
|
631
|
+
header = current_tab.results_table.horizontalHeader()
|
|
632
|
+
if isinstance(header, FilterHeader):
|
|
633
|
+
columns_with_bars = header.columns_with_bars.copy()
|
|
634
|
+
else:
|
|
635
|
+
columns_with_bars = set()
|
|
636
|
+
|
|
637
|
+
# Clear existing data
|
|
638
|
+
current_tab.results_table.clearContents()
|
|
639
|
+
current_tab.results_table.setRowCount(0)
|
|
640
|
+
current_tab.results_table.setColumnCount(0)
|
|
641
|
+
|
|
642
|
+
if df.empty:
|
|
643
|
+
self.statusBar().showMessage("Query returned no results")
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
# Set up the table dimensions
|
|
647
|
+
row_count = len(df)
|
|
648
|
+
col_count = len(df.columns)
|
|
649
|
+
current_tab.results_table.setColumnCount(col_count)
|
|
650
|
+
|
|
651
|
+
# Set column headers
|
|
652
|
+
headers = [str(col) for col in df.columns]
|
|
653
|
+
current_tab.results_table.setHorizontalHeaderLabels(headers)
|
|
654
|
+
|
|
655
|
+
# Calculate chunk size (adjust based on available memory)
|
|
656
|
+
CHUNK_SIZE = 1000
|
|
657
|
+
|
|
658
|
+
# Process data in chunks to avoid memory issues with large datasets
|
|
659
|
+
for chunk_start in range(0, row_count, CHUNK_SIZE):
|
|
660
|
+
chunk_end = min(chunk_start + CHUNK_SIZE, row_count)
|
|
661
|
+
chunk = df.iloc[chunk_start:chunk_end]
|
|
662
|
+
|
|
663
|
+
# Add rows for this chunk
|
|
664
|
+
current_tab.results_table.setRowCount(chunk_end)
|
|
665
|
+
|
|
666
|
+
for row_idx, (_, row_data) in enumerate(chunk.iterrows(), start=chunk_start):
|
|
667
|
+
for col_idx, value in enumerate(row_data):
|
|
668
|
+
formatted_value = self.format_value(value)
|
|
669
|
+
item = QTableWidgetItem(formatted_value)
|
|
670
|
+
current_tab.results_table.setItem(row_idx, col_idx, item)
|
|
671
|
+
|
|
672
|
+
# Process events to keep UI responsive
|
|
673
|
+
QApplication.processEvents()
|
|
674
|
+
|
|
675
|
+
# Optimize column widths
|
|
676
|
+
current_tab.results_table.resizeColumnsToContents()
|
|
677
|
+
|
|
678
|
+
# Restore bar charts for columns that previously had them
|
|
679
|
+
header = current_tab.results_table.horizontalHeader()
|
|
680
|
+
if isinstance(header, FilterHeader):
|
|
681
|
+
for col_idx in columns_with_bars:
|
|
682
|
+
if col_idx < col_count: # Only if column still exists
|
|
683
|
+
header.toggle_bar_chart(col_idx)
|
|
684
|
+
|
|
685
|
+
# Update row count label
|
|
686
|
+
current_tab.row_count_label.setText(f"{row_count:,} rows")
|
|
687
|
+
|
|
688
|
+
# Update status
|
|
689
|
+
memory_usage = df.memory_usage(deep=True).sum() / (1024 * 1024) # Convert to MB
|
|
690
|
+
self.statusBar().showMessage(
|
|
691
|
+
f"Loaded {row_count:,} rows, {col_count} columns. Memory usage: {memory_usage:.1f} MB"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
except Exception as e:
|
|
695
|
+
QMessageBox.critical(self, "Error",
|
|
696
|
+
f"Failed to populate results table:\n\n{str(e)}")
|
|
697
|
+
self.statusBar().showMessage("Failed to display results")
|
|
698
|
+
|
|
699
|
+
def apply_filters(self):
|
|
700
|
+
"""Apply filters to the table based on filter inputs"""
|
|
701
|
+
if self.current_df is None or not self.filter_widgets:
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
# Start with the original DataFrame
|
|
706
|
+
filtered_df = self.current_df.copy()
|
|
707
|
+
|
|
708
|
+
# Apply each non-empty filter
|
|
709
|
+
for col_idx, filter_widget in enumerate(self.filter_widgets):
|
|
710
|
+
filter_text = filter_widget.text().strip()
|
|
711
|
+
if filter_text:
|
|
712
|
+
col_name = self.current_df.columns[col_idx]
|
|
713
|
+
# Convert column to string for filtering
|
|
714
|
+
filtered_df[col_name] = filtered_df[col_name].astype(str)
|
|
715
|
+
filtered_df = filtered_df[filtered_df[col_name].str.contains(filter_text, case=False, na=False)]
|
|
716
|
+
|
|
717
|
+
# Update table with filtered data
|
|
718
|
+
row_count = len(filtered_df)
|
|
719
|
+
for row_idx in range(row_count):
|
|
720
|
+
for col_idx, value in enumerate(filtered_df.iloc[row_idx]):
|
|
721
|
+
formatted_value = self.format_value(value)
|
|
722
|
+
item = QTableWidgetItem(formatted_value)
|
|
723
|
+
self.results_table.setItem(row_idx, col_idx, item)
|
|
724
|
+
|
|
725
|
+
# Hide rows that don't match filter
|
|
726
|
+
for row_idx in range(row_count + 1, self.results_table.rowCount()):
|
|
727
|
+
self.results_table.hideRow(row_idx)
|
|
728
|
+
|
|
729
|
+
# Show all filtered rows
|
|
730
|
+
for row_idx in range(1, row_count + 1):
|
|
731
|
+
self.results_table.showRow(row_idx)
|
|
732
|
+
|
|
733
|
+
# Update status
|
|
734
|
+
self.statusBar().showMessage(f"Showing {row_count:,} rows after filtering")
|
|
735
|
+
|
|
736
|
+
except Exception as e:
|
|
737
|
+
self.statusBar().showMessage(f"Error applying filters: {str(e)}")
|
|
892
738
|
|
|
893
739
|
def format_value(self, value):
|
|
894
|
-
"""Format values
|
|
740
|
+
"""Format cell values efficiently"""
|
|
895
741
|
if pd.isna(value):
|
|
896
|
-
return
|
|
897
|
-
elif isinstance(value, (int, np.integer)):
|
|
898
|
-
return f"{value:,}"
|
|
742
|
+
return "NULL"
|
|
899
743
|
elif isinstance(value, (float, np.floating)):
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
return value
|
|
744
|
+
if value.is_integer():
|
|
745
|
+
return str(int(value))
|
|
746
|
+
return f"{value:.6g}" # Use general format with up to 6 significant digits
|
|
747
|
+
elif isinstance(value, (pd.Timestamp, datetime)):
|
|
748
|
+
return value.strftime("%Y-%m-%d %H:%M:%S")
|
|
749
|
+
elif isinstance(value, (np.integer, int)):
|
|
750
|
+
return str(value)
|
|
751
|
+
elif isinstance(value, bool):
|
|
752
|
+
return str(value)
|
|
753
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
754
|
+
return value.hex()
|
|
903
755
|
return str(value)
|
|
904
756
|
|
|
905
|
-
def populate_table(self, df):
|
|
906
|
-
"""Populate the table widget with DataFrame content"""
|
|
907
|
-
if len(df) == 0:
|
|
908
|
-
self.results_table.setRowCount(0)
|
|
909
|
-
self.results_table.setColumnCount(0)
|
|
910
|
-
self.row_count_label.setText("No results")
|
|
911
|
-
return
|
|
912
|
-
|
|
913
|
-
# Set dimensions
|
|
914
|
-
self.results_table.setRowCount(len(df))
|
|
915
|
-
self.results_table.setColumnCount(len(df.columns))
|
|
916
|
-
|
|
917
|
-
# Set headers
|
|
918
|
-
self.results_table.setHorizontalHeaderLabels(df.columns)
|
|
919
|
-
|
|
920
|
-
# Populate data
|
|
921
|
-
for i, (_, row) in enumerate(df.iterrows()):
|
|
922
|
-
for j, value in enumerate(row):
|
|
923
|
-
formatted_value = self.format_value(value)
|
|
924
|
-
item = QTableWidgetItem(formatted_value)
|
|
925
|
-
|
|
926
|
-
# Set alignment based on data type
|
|
927
|
-
if isinstance(value, (int, float, np.integer, np.floating)):
|
|
928
|
-
item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
929
|
-
else:
|
|
930
|
-
item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
931
|
-
|
|
932
|
-
# Make cells read-only
|
|
933
|
-
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
934
|
-
|
|
935
|
-
# Apply special styling for NULL values
|
|
936
|
-
if pd.isna(value):
|
|
937
|
-
item.setForeground(QColor(self.colors['text_light']))
|
|
938
|
-
item.setBackground(QColor("#F8F9FA"))
|
|
939
|
-
|
|
940
|
-
self.results_table.setItem(i, j, item)
|
|
941
|
-
|
|
942
|
-
# Auto-adjust column widths while ensuring minimum and maximum sizes
|
|
943
|
-
self.results_table.resizeColumnsToContents()
|
|
944
|
-
for i in range(len(df.columns)):
|
|
945
|
-
width = self.results_table.columnWidth(i)
|
|
946
|
-
self.results_table.setColumnWidth(i, min(max(width, 80), 300))
|
|
947
|
-
|
|
948
|
-
# Update row count
|
|
949
|
-
row_text = "row" if len(df) == 1 else "rows"
|
|
950
|
-
self.row_count_label.setText(f"{len(df):,} {row_text}")
|
|
951
|
-
|
|
952
|
-
# Apply zebra striping
|
|
953
|
-
for i in range(len(df)):
|
|
954
|
-
if i % 2 == 0:
|
|
955
|
-
for j in range(len(df.columns)):
|
|
956
|
-
item = self.results_table.item(i, j)
|
|
957
|
-
if item and not pd.isna(df.iloc[i, j]):
|
|
958
|
-
item.setBackground(QColor("#FFFFFF"))
|
|
959
|
-
else:
|
|
960
|
-
for j in range(len(df.columns)):
|
|
961
|
-
item = self.results_table.item(i, j)
|
|
962
|
-
if item and not pd.isna(df.iloc[i, j]):
|
|
963
|
-
item.setBackground(QColor("#F8F9FA"))
|
|
964
|
-
|
|
965
757
|
def browse_files(self):
|
|
966
|
-
if not self.
|
|
758
|
+
if not self.db_manager.is_connected():
|
|
967
759
|
# Create a default in-memory DuckDB connection if none exists
|
|
968
|
-
|
|
969
|
-
self.
|
|
970
|
-
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
760
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
761
|
+
self.db_info_label.setText(connection_info)
|
|
971
762
|
|
|
972
763
|
file_names, _ = QFileDialog.getOpenFileNames(
|
|
973
764
|
self,
|
|
@@ -978,38 +769,8 @@ class SQLShell(QMainWindow):
|
|
|
978
769
|
|
|
979
770
|
for file_name in file_names:
|
|
980
771
|
try:
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
elif file_name.endswith('.csv'):
|
|
984
|
-
df = pd.read_csv(file_name)
|
|
985
|
-
elif file_name.endswith('.parquet'):
|
|
986
|
-
df = pd.read_parquet(file_name)
|
|
987
|
-
else:
|
|
988
|
-
raise ValueError("Unsupported file format")
|
|
989
|
-
|
|
990
|
-
# Generate table name from file name
|
|
991
|
-
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
992
|
-
table_name = self.sanitize_table_name(base_name)
|
|
993
|
-
|
|
994
|
-
# Ensure unique table name
|
|
995
|
-
original_name = table_name
|
|
996
|
-
counter = 1
|
|
997
|
-
while table_name in self.loaded_tables:
|
|
998
|
-
table_name = f"{original_name}_{counter}"
|
|
999
|
-
counter += 1
|
|
1000
|
-
|
|
1001
|
-
# Handle table creation based on database type
|
|
1002
|
-
if self.current_db_type == 'sqlite':
|
|
1003
|
-
# For SQLite, create a table from the DataFrame
|
|
1004
|
-
df.to_sql(table_name, self.conn, index=False, if_exists='replace')
|
|
1005
|
-
else:
|
|
1006
|
-
# For DuckDB, register the DataFrame as a view
|
|
1007
|
-
self.conn.register(table_name, df)
|
|
1008
|
-
|
|
1009
|
-
self.loaded_tables[table_name] = file_name
|
|
1010
|
-
|
|
1011
|
-
# Store column names
|
|
1012
|
-
self.table_columns[table_name] = df.columns.tolist()
|
|
772
|
+
# Use the database manager to load the file
|
|
773
|
+
table_name, df = self.db_manager.load_file(file_name)
|
|
1013
774
|
|
|
1014
775
|
# Update UI
|
|
1015
776
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
@@ -1035,26 +796,11 @@ class SQLShell(QMainWindow):
|
|
|
1035
796
|
self.results_table.setColumnCount(0)
|
|
1036
797
|
self.row_count_label.setText("")
|
|
1037
798
|
|
|
1038
|
-
def sanitize_table_name(self, name):
|
|
1039
|
-
# Replace invalid characters with underscores
|
|
1040
|
-
import re
|
|
1041
|
-
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
1042
|
-
# Ensure it starts with a letter
|
|
1043
|
-
if not name[0].isalpha():
|
|
1044
|
-
name = 'table_' + name
|
|
1045
|
-
return name.lower()
|
|
1046
|
-
|
|
1047
799
|
def remove_selected_table(self):
|
|
1048
800
|
current_item = self.tables_list.currentItem()
|
|
1049
801
|
if current_item:
|
|
1050
802
|
table_name = current_item.text().split(' (')[0]
|
|
1051
|
-
if
|
|
1052
|
-
# Remove from DuckDB
|
|
1053
|
-
self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
|
|
1054
|
-
# Remove from our tracking
|
|
1055
|
-
del self.loaded_tables[table_name]
|
|
1056
|
-
if table_name in self.table_columns:
|
|
1057
|
-
del self.table_columns[table_name]
|
|
803
|
+
if self.db_manager.remove_table(table_name):
|
|
1058
804
|
# Remove from list widget
|
|
1059
805
|
self.tables_list.takeItem(self.tables_list.row(current_item))
|
|
1060
806
|
self.statusBar().showMessage(f'Removed table "{table_name}"')
|
|
@@ -1066,213 +812,380 @@ class SQLShell(QMainWindow):
|
|
|
1066
812
|
self.update_completer()
|
|
1067
813
|
|
|
1068
814
|
def open_database(self):
|
|
1069
|
-
"""Open a database
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if not file_name:
|
|
1078
|
-
return
|
|
815
|
+
"""Open a database connection with proper error handling and resource management"""
|
|
816
|
+
try:
|
|
817
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
818
|
+
self,
|
|
819
|
+
"Open Database",
|
|
820
|
+
"",
|
|
821
|
+
"All Database Files (*.db *.sqlite *.sqlite3);;All Files (*)"
|
|
822
|
+
)
|
|
1079
823
|
|
|
824
|
+
if filename:
|
|
825
|
+
try:
|
|
826
|
+
# Clear existing database tables from the list widget
|
|
827
|
+
for i in range(self.tables_list.count() - 1, -1, -1):
|
|
828
|
+
item = self.tables_list.item(i)
|
|
829
|
+
if item and item.text().endswith('(database)'):
|
|
830
|
+
self.tables_list.takeItem(i)
|
|
831
|
+
|
|
832
|
+
# Use the database manager to open the database
|
|
833
|
+
self.db_manager.open_database(filename)
|
|
834
|
+
|
|
835
|
+
# Update UI with tables from the database
|
|
836
|
+
for table_name, source in self.db_manager.loaded_tables.items():
|
|
837
|
+
if source == 'database':
|
|
838
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
839
|
+
|
|
840
|
+
# Update the completer with table and column names
|
|
841
|
+
self.update_completer()
|
|
842
|
+
|
|
843
|
+
# Update status bar
|
|
844
|
+
self.statusBar().showMessage(f"Connected to database: {filename}")
|
|
845
|
+
self.db_info_label.setText(self.db_manager.get_connection_info())
|
|
846
|
+
|
|
847
|
+
except Exception as e:
|
|
848
|
+
QMessageBox.critical(self, "Database Connection Error",
|
|
849
|
+
f"Failed to open database:\n\n{str(e)}")
|
|
850
|
+
self.statusBar().showMessage("Failed to open database")
|
|
851
|
+
|
|
852
|
+
except Exception as e:
|
|
853
|
+
QMessageBox.critical(self, "Error",
|
|
854
|
+
f"Unexpected error:\n\n{str(e)}")
|
|
855
|
+
self.statusBar().showMessage("Error opening database")
|
|
856
|
+
|
|
857
|
+
def update_completer(self):
|
|
858
|
+
"""Update the completer with table and column names in a non-blocking way"""
|
|
1080
859
|
try:
|
|
1081
|
-
#
|
|
1082
|
-
|
|
860
|
+
# Check if any tabs exist
|
|
861
|
+
if self.tab_widget.count() == 0:
|
|
862
|
+
return
|
|
1083
863
|
|
|
1084
|
-
#
|
|
1085
|
-
|
|
1086
|
-
self.conn.close()
|
|
864
|
+
# Start a background update with a timer
|
|
865
|
+
self.statusBar().showMessage("Updating auto-completion...", 2000)
|
|
1087
866
|
|
|
1088
|
-
#
|
|
1089
|
-
if
|
|
1090
|
-
self.
|
|
1091
|
-
self.
|
|
1092
|
-
else:
|
|
1093
|
-
self.conn = duckdb.connect(file_name)
|
|
1094
|
-
self.current_db_type = 'duckdb'
|
|
867
|
+
# Track query history and frequently used terms
|
|
868
|
+
if not hasattr(self, 'query_history'):
|
|
869
|
+
self.query_history = []
|
|
870
|
+
self.completion_usage = {} # Track usage frequency
|
|
1095
871
|
|
|
1096
|
-
#
|
|
1097
|
-
|
|
1098
|
-
|
|
872
|
+
# Get completion words from the database manager
|
|
873
|
+
try:
|
|
874
|
+
completion_words = self.db_manager.get_all_table_columns()
|
|
875
|
+
except Exception as e:
|
|
876
|
+
self.statusBar().showMessage(f"Error getting completions: {str(e)}", 2000)
|
|
877
|
+
completion_words = []
|
|
878
|
+
|
|
879
|
+
# Add frequently used terms from query history with higher priority
|
|
880
|
+
if hasattr(self, 'completion_usage') and self.completion_usage:
|
|
881
|
+
# Get the most frequently used terms (top 100)
|
|
882
|
+
frequent_terms = sorted(
|
|
883
|
+
self.completion_usage.items(),
|
|
884
|
+
key=lambda x: x[1],
|
|
885
|
+
reverse=True
|
|
886
|
+
)[:100]
|
|
887
|
+
|
|
888
|
+
# Add these to our completion words
|
|
889
|
+
for term, _ in frequent_terms:
|
|
890
|
+
if term not in completion_words:
|
|
891
|
+
completion_words.append(term)
|
|
892
|
+
|
|
893
|
+
# Limit to a reasonable number of items to prevent performance issues
|
|
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]
|
|
1099
947
|
|
|
1100
|
-
#
|
|
1101
|
-
|
|
948
|
+
# Use a single model for all tabs to save memory and improve performance
|
|
949
|
+
model = QStringListModel(completion_words)
|
|
1102
950
|
|
|
1103
|
-
#
|
|
1104
|
-
|
|
1105
|
-
self.db_info_label.setText(f"Connected to: {os.path.basename(file_name)} ({db_type})")
|
|
1106
|
-
self.statusBar().showMessage(f'Successfully opened {db_type} database: {file_name}')
|
|
951
|
+
# Keep a reference to the model to prevent garbage collection
|
|
952
|
+
self._current_completer_model = model
|
|
1107
953
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
"""Check if the file is a SQLite database"""
|
|
1114
|
-
try:
|
|
1115
|
-
with open(filename, 'rb') as f:
|
|
1116
|
-
header = f.read(16)
|
|
1117
|
-
return header[:16] == b'SQLite format 3\x00'
|
|
1118
|
-
except:
|
|
1119
|
-
return False
|
|
1120
|
-
|
|
1121
|
-
def load_database_tables(self):
|
|
1122
|
-
"""Load all tables from the current database"""
|
|
1123
|
-
try:
|
|
1124
|
-
if self.current_db_type == 'sqlite':
|
|
1125
|
-
query = "SELECT name FROM sqlite_master WHERE type='table'"
|
|
1126
|
-
cursor = self.conn.cursor()
|
|
1127
|
-
tables = cursor.execute(query).fetchall()
|
|
1128
|
-
for (table_name,) in tables:
|
|
1129
|
-
self.loaded_tables[table_name] = 'database'
|
|
1130
|
-
self.tables_list.addItem(f"{table_name} (database)")
|
|
1131
|
-
|
|
1132
|
-
# Get column names for each table
|
|
1133
|
-
try:
|
|
1134
|
-
column_query = f"PRAGMA table_info({table_name})"
|
|
1135
|
-
columns = cursor.execute(column_query).fetchall()
|
|
1136
|
-
self.table_columns[table_name] = [col[1] for col in columns] # Column name is at index 1
|
|
1137
|
-
except Exception:
|
|
1138
|
-
self.table_columns[table_name] = []
|
|
1139
|
-
else: # duckdb
|
|
1140
|
-
query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
1141
|
-
result = self.conn.execute(query).fetchdf()
|
|
1142
|
-
for table_name in result['table_name']:
|
|
1143
|
-
self.loaded_tables[table_name] = 'database'
|
|
1144
|
-
self.tables_list.addItem(f"{table_name} (database)")
|
|
1145
|
-
|
|
1146
|
-
# Get column names for each table
|
|
954
|
+
# Only update the current tab immediately
|
|
955
|
+
current_index = self.tab_widget.currentIndex()
|
|
956
|
+
if current_index >= 0:
|
|
957
|
+
current_tab = self.tab_widget.widget(current_index)
|
|
958
|
+
if current_tab and hasattr(current_tab, 'query_edit'):
|
|
1147
959
|
try:
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
self.
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
960
|
+
current_tab.query_edit.update_completer_model(model)
|
|
961
|
+
except Exception as e:
|
|
962
|
+
self.statusBar().showMessage(f"Error updating current tab completer: {str(e)}", 2000)
|
|
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)
|
|
997
|
+
|
|
998
|
+
# Process events to keep UI responsive
|
|
999
|
+
QApplication.processEvents()
|
|
1000
|
+
|
|
1001
|
+
# Return True to indicate success
|
|
1002
|
+
return True
|
|
1003
|
+
|
|
1156
1004
|
except Exception as e:
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
"""Update the completer with table and column names"""
|
|
1161
|
-
# Collect all table names and column names
|
|
1162
|
-
completion_words = list(self.loaded_tables.keys())
|
|
1163
|
-
|
|
1164
|
-
# Add column names with table name prefix (for joins)
|
|
1165
|
-
for table, columns in self.table_columns.items():
|
|
1166
|
-
completion_words.extend(columns)
|
|
1167
|
-
completion_words.extend([f"{table}.{col}" for col in columns])
|
|
1005
|
+
# Catch any errors to prevent hanging
|
|
1006
|
+
self.statusBar().showMessage(f"Auto-completion update error: {str(e)}", 2000)
|
|
1007
|
+
return False
|
|
1168
1008
|
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
|
1171
1024
|
|
|
1172
1025
|
def execute_query(self):
|
|
1173
|
-
query = self.query_edit.toPlainText().strip()
|
|
1174
|
-
if not query:
|
|
1175
|
-
return
|
|
1176
|
-
|
|
1177
|
-
if not self.conn:
|
|
1178
|
-
QMessageBox.warning(self, "No Connection", "No database connection available. Creating an in-memory DuckDB database.")
|
|
1179
|
-
self.conn = duckdb.connect(':memory:')
|
|
1180
|
-
self.current_db_type = 'duckdb'
|
|
1181
|
-
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
1182
|
-
|
|
1183
|
-
# Show loading indicator in status bar
|
|
1184
|
-
self.statusBar().showMessage('Executing query...')
|
|
1185
|
-
|
|
1186
1026
|
try:
|
|
1187
|
-
#
|
|
1188
|
-
|
|
1189
|
-
if
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if self.current_db_type == 'sqlite':
|
|
1193
|
-
# Execute SQLite query and convert to DataFrame
|
|
1194
|
-
result = pd.read_sql_query(query, self.conn)
|
|
1195
|
-
else:
|
|
1196
|
-
# Execute DuckDB query
|
|
1197
|
-
result = self.conn.execute(query).fetchdf()
|
|
1027
|
+
# Get the current tab
|
|
1028
|
+
current_tab = self.get_current_tab()
|
|
1029
|
+
if not current_tab:
|
|
1030
|
+
return
|
|
1198
1031
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1032
|
+
query = current_tab.get_query_text().strip()
|
|
1033
|
+
if not query:
|
|
1034
|
+
QMessageBox.warning(self, "Empty Query", "Please enter a SQL query to execute.")
|
|
1035
|
+
return
|
|
1036
|
+
|
|
1037
|
+
start_time = datetime.now()
|
|
1205
1038
|
|
|
1039
|
+
try:
|
|
1040
|
+
# Use the database manager to execute the query
|
|
1041
|
+
result = self.db_manager.execute_query(query)
|
|
1042
|
+
|
|
1043
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
1044
|
+
self.populate_table(result)
|
|
1045
|
+
self.statusBar().showMessage(f"Query executed successfully. Time: {execution_time:.2f}s. Rows: {len(result)}")
|
|
1046
|
+
|
|
1047
|
+
# Record query in history and update completion usage
|
|
1048
|
+
self._update_query_history(query)
|
|
1049
|
+
|
|
1050
|
+
except SyntaxError as e:
|
|
1051
|
+
QMessageBox.critical(self, "SQL Syntax Error", str(e))
|
|
1052
|
+
self.statusBar().showMessage("Query execution failed: syntax error")
|
|
1053
|
+
except ValueError as e:
|
|
1054
|
+
QMessageBox.critical(self, "Query Error", str(e))
|
|
1055
|
+
self.statusBar().showMessage("Query execution failed")
|
|
1056
|
+
except Exception as e:
|
|
1057
|
+
QMessageBox.critical(self, "Database Error", str(e))
|
|
1058
|
+
self.statusBar().showMessage("Query execution failed")
|
|
1059
|
+
|
|
1206
1060
|
except Exception as e:
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
self.
|
|
1210
|
-
self.statusBar().showMessage('Error executing query')
|
|
1211
|
-
|
|
1212
|
-
# Show error message with modern styling
|
|
1213
|
-
error_msg = str(e)
|
|
1214
|
-
if "not found" in error_msg.lower():
|
|
1215
|
-
error_msg += "\nMake sure the table name is correct and the table is loaded."
|
|
1216
|
-
elif "syntax error" in error_msg.lower():
|
|
1217
|
-
error_msg += "\nPlease check your SQL syntax."
|
|
1218
|
-
|
|
1219
|
-
error_box = QMessageBox(self)
|
|
1220
|
-
error_box.setIcon(QMessageBox.Icon.Critical)
|
|
1221
|
-
error_box.setWindowTitle("Query Error")
|
|
1222
|
-
error_box.setText("Error executing query")
|
|
1223
|
-
error_box.setInformativeText(error_msg)
|
|
1224
|
-
error_box.setDetailedText(f"Query:\n{query}")
|
|
1225
|
-
error_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
1226
|
-
error_box.exec()
|
|
1061
|
+
QMessageBox.critical(self, "Unexpected Error",
|
|
1062
|
+
f"An unexpected error occurred:\n\n{str(e)}")
|
|
1063
|
+
self.statusBar().showMessage("Query execution failed")
|
|
1227
1064
|
|
|
1065
|
+
def _update_query_history(self, query):
|
|
1066
|
+
"""Update query history and track term usage for improved autocompletion"""
|
|
1067
|
+
import re
|
|
1068
|
+
|
|
1069
|
+
# Initialize history if it doesn't exist
|
|
1070
|
+
if not hasattr(self, 'query_history'):
|
|
1071
|
+
self.query_history = []
|
|
1072
|
+
self.completion_usage = {}
|
|
1073
|
+
|
|
1074
|
+
# Add query to history (limit to 100 queries)
|
|
1075
|
+
self.query_history.append(query)
|
|
1076
|
+
if len(self.query_history) > 100:
|
|
1077
|
+
self.query_history.pop(0)
|
|
1078
|
+
|
|
1079
|
+
# Extract terms and patterns from the query to update usage frequency
|
|
1080
|
+
|
|
1081
|
+
# Extract table and column names
|
|
1082
|
+
table_pattern = r'\b([a-zA-Z0-9_]+)\b\.([a-zA-Z0-9_]+)\b'
|
|
1083
|
+
qualified_columns = re.findall(table_pattern, query)
|
|
1084
|
+
for table, column in qualified_columns:
|
|
1085
|
+
qualified_name = f"{table}.{column}"
|
|
1086
|
+
self.completion_usage[qualified_name] = self.completion_usage.get(qualified_name, 0) + 1
|
|
1087
|
+
|
|
1088
|
+
# Also count the table and column separately
|
|
1089
|
+
self.completion_usage[table] = self.completion_usage.get(table, 0) + 1
|
|
1090
|
+
self.completion_usage[column] = self.completion_usage.get(column, 0) + 1
|
|
1091
|
+
|
|
1092
|
+
# Extract SQL keywords
|
|
1093
|
+
keyword_pattern = r'\b([A-Z_]{2,})\b'
|
|
1094
|
+
keywords = re.findall(keyword_pattern, query.upper())
|
|
1095
|
+
for keyword in keywords:
|
|
1096
|
+
self.completion_usage[keyword] = self.completion_usage.get(keyword, 0) + 1
|
|
1097
|
+
|
|
1098
|
+
# Extract common SQL patterns
|
|
1099
|
+
patterns = [
|
|
1100
|
+
r'(SELECT\s+.*?\s+FROM)',
|
|
1101
|
+
r'(GROUP\s+BY\s+.*?(?:HAVING|ORDER|LIMIT|$))',
|
|
1102
|
+
r'(ORDER\s+BY\s+.*?(?:LIMIT|$))',
|
|
1103
|
+
r'(INNER\s+JOIN|LEFT\s+JOIN|RIGHT\s+JOIN|FULL\s+JOIN).*?ON\s+.*?=\s+.*?(?:WHERE|JOIN|GROUP|ORDER|LIMIT|$)',
|
|
1104
|
+
r'(INSERT\s+INTO\s+.*?\s+VALUES)',
|
|
1105
|
+
r'(UPDATE\s+.*?\s+SET\s+.*?\s+WHERE)',
|
|
1106
|
+
r'(DELETE\s+FROM\s+.*?\s+WHERE)'
|
|
1107
|
+
]
|
|
1108
|
+
|
|
1109
|
+
for pattern in patterns:
|
|
1110
|
+
matches = re.findall(pattern, query, re.IGNORECASE | re.DOTALL)
|
|
1111
|
+
for match in matches:
|
|
1112
|
+
# Normalize pattern by removing extra whitespace and converting to uppercase
|
|
1113
|
+
normalized = re.sub(r'\s+', ' ', match).strip().upper()
|
|
1114
|
+
if len(normalized) < 50: # Only track reasonably sized patterns
|
|
1115
|
+
self.completion_usage[normalized] = self.completion_usage.get(normalized, 0) + 1
|
|
1116
|
+
|
|
1117
|
+
# Schedule an update of the completion model (but not too often to avoid performance issues)
|
|
1118
|
+
if not hasattr(self, '_last_completer_update') or \
|
|
1119
|
+
(datetime.now() - self._last_completer_update).total_seconds() > 30:
|
|
1120
|
+
self._last_completer_update = datetime.now()
|
|
1121
|
+
|
|
1122
|
+
# Use a timer to delay the update to avoid blocking the UI
|
|
1123
|
+
update_timer = QTimer()
|
|
1124
|
+
update_timer.setSingleShot(True)
|
|
1125
|
+
update_timer.timeout.connect(self.update_completer)
|
|
1126
|
+
update_timer.start(1000) # Update after 1 second
|
|
1127
|
+
|
|
1228
1128
|
def clear_query(self):
|
|
1229
1129
|
"""Clear the query editor with animation"""
|
|
1130
|
+
# Get the current tab
|
|
1131
|
+
current_tab = self.get_current_tab()
|
|
1132
|
+
if not current_tab:
|
|
1133
|
+
return
|
|
1134
|
+
|
|
1230
1135
|
# Save current text for animation
|
|
1231
|
-
current_text =
|
|
1136
|
+
current_text = current_tab.get_query_text()
|
|
1232
1137
|
if not current_text:
|
|
1233
1138
|
return
|
|
1234
1139
|
|
|
1235
1140
|
# Clear the editor
|
|
1236
|
-
|
|
1141
|
+
current_tab.set_query_text("")
|
|
1237
1142
|
|
|
1238
1143
|
# Show success message
|
|
1239
1144
|
self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
|
|
1240
1145
|
|
|
1241
1146
|
def show_table_preview(self, item):
|
|
1242
1147
|
"""Show a preview of the selected table"""
|
|
1243
|
-
if item:
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
|
|
1256
|
-
if results_title and results_title.text() == "RESULTS":
|
|
1257
|
-
results_title.setText(f"PREVIEW: {table_name}")
|
|
1148
|
+
if not item:
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
# Get the current tab
|
|
1152
|
+
current_tab = self.get_current_tab()
|
|
1153
|
+
if not current_tab:
|
|
1154
|
+
return
|
|
1155
|
+
|
|
1156
|
+
table_name = item.text().split(' (')[0]
|
|
1157
|
+
try:
|
|
1158
|
+
# Use the database manager to get a preview of the table
|
|
1159
|
+
preview_df = self.db_manager.get_table_preview(table_name)
|
|
1258
1160
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1161
|
+
self.populate_table(preview_df)
|
|
1162
|
+
self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
|
|
1163
|
+
|
|
1164
|
+
# Update the results title to show which table is being previewed
|
|
1165
|
+
current_tab.results_title.setText(f"PREVIEW: {table_name}")
|
|
1166
|
+
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
current_tab.results_table.setRowCount(0)
|
|
1169
|
+
current_tab.results_table.setColumnCount(0)
|
|
1170
|
+
current_tab.row_count_label.setText("")
|
|
1171
|
+
self.statusBar().showMessage('Error showing table preview')
|
|
1172
|
+
|
|
1173
|
+
# Show error message with modern styling
|
|
1174
|
+
QMessageBox.critical(
|
|
1175
|
+
self,
|
|
1176
|
+
"Error",
|
|
1177
|
+
f"Error showing preview: {str(e)}",
|
|
1178
|
+
QMessageBox.StandardButton.Ok
|
|
1179
|
+
)
|
|
1272
1180
|
|
|
1273
1181
|
def load_test_data(self):
|
|
1274
1182
|
"""Generate and load test data"""
|
|
1275
1183
|
try:
|
|
1184
|
+
# Ensure we have a DuckDB connection
|
|
1185
|
+
if not self.db_manager.is_connected() or self.db_manager.connection_type != 'duckdb':
|
|
1186
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
1187
|
+
self.db_info_label.setText(connection_info)
|
|
1188
|
+
|
|
1276
1189
|
# Show loading indicator
|
|
1277
1190
|
self.statusBar().showMessage('Generating test data...')
|
|
1278
1191
|
|
|
@@ -1289,45 +1202,30 @@ class SQLShell(QMainWindow):
|
|
|
1289
1202
|
customer_df.to_parquet('test_data/customer_data.parquet', index=False)
|
|
1290
1203
|
product_df.to_excel('test_data/product_catalog.xlsx', index=False)
|
|
1291
1204
|
|
|
1292
|
-
#
|
|
1293
|
-
self.
|
|
1294
|
-
self.
|
|
1295
|
-
self.
|
|
1296
|
-
|
|
1297
|
-
# Update loaded tables tracking
|
|
1298
|
-
self.loaded_tables['sample_sales_data'] = 'test_data/sample_sales_data.xlsx'
|
|
1299
|
-
self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
|
|
1300
|
-
self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
|
|
1301
|
-
|
|
1302
|
-
# Store column names
|
|
1303
|
-
self.table_columns['sample_sales_data'] = sales_df.columns.tolist()
|
|
1304
|
-
self.table_columns['product_catalog'] = product_df.columns.tolist()
|
|
1305
|
-
self.table_columns['customer_data'] = customer_df.columns.tolist()
|
|
1205
|
+
# Register the tables in the database manager
|
|
1206
|
+
self.db_manager.register_dataframe(sales_df, 'sample_sales_data', 'test_data/sample_sales_data.xlsx')
|
|
1207
|
+
self.db_manager.register_dataframe(product_df, 'product_catalog', 'test_data/product_catalog.xlsx')
|
|
1208
|
+
self.db_manager.register_dataframe(customer_df, 'customer_data', 'test_data/customer_data.parquet')
|
|
1306
1209
|
|
|
1307
1210
|
# Update UI
|
|
1308
1211
|
self.tables_list.clear()
|
|
1309
|
-
for table_name, file_path in self.loaded_tables.items():
|
|
1212
|
+
for table_name, file_path in self.db_manager.loaded_tables.items():
|
|
1310
1213
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
1311
1214
|
|
|
1312
|
-
# Set the sample query
|
|
1313
|
-
|
|
1215
|
+
# Set the sample query in the current tab
|
|
1216
|
+
current_tab = self.get_current_tab()
|
|
1217
|
+
if current_tab:
|
|
1218
|
+
sample_query = """
|
|
1314
1219
|
SELECT
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
c.customername,
|
|
1318
|
-
p.productname,
|
|
1319
|
-
s.quantity,
|
|
1320
|
-
s.unitprice,
|
|
1321
|
-
(s.quantity * s.unitprice) AS total_amount
|
|
1220
|
+
DISTINCT
|
|
1221
|
+
c.customername
|
|
1322
1222
|
FROM
|
|
1323
1223
|
sample_sales_data s
|
|
1324
1224
|
INNER JOIN customer_data c ON c.customerid = s.customerid
|
|
1325
1225
|
INNER JOIN product_catalog p ON p.productid = s.productid
|
|
1326
|
-
ORDER BY
|
|
1327
|
-
s.orderdate DESC
|
|
1328
1226
|
LIMIT 10
|
|
1329
1227
|
"""
|
|
1330
|
-
|
|
1228
|
+
current_tab.set_query_text(sample_query.strip())
|
|
1331
1229
|
|
|
1332
1230
|
# Update completer
|
|
1333
1231
|
self.update_completer()
|
|
@@ -1343,7 +1241,12 @@ LIMIT 10
|
|
|
1343
1241
|
QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
|
|
1344
1242
|
|
|
1345
1243
|
def export_to_excel(self):
|
|
1346
|
-
|
|
1244
|
+
# Get the current tab
|
|
1245
|
+
current_tab = self.get_current_tab()
|
|
1246
|
+
if not current_tab:
|
|
1247
|
+
return
|
|
1248
|
+
|
|
1249
|
+
if current_tab.results_table.rowCount() == 0:
|
|
1347
1250
|
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
1348
1251
|
return
|
|
1349
1252
|
|
|
@@ -1359,13 +1262,36 @@ LIMIT 10
|
|
|
1359
1262
|
df = self.get_table_data_as_dataframe()
|
|
1360
1263
|
df.to_excel(file_name, index=False)
|
|
1361
1264
|
|
|
1362
|
-
|
|
1265
|
+
# Generate table name from file name
|
|
1266
|
+
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1267
|
+
table_name = self.db_manager.sanitize_table_name(base_name)
|
|
1268
|
+
|
|
1269
|
+
# Ensure unique table name
|
|
1270
|
+
original_name = table_name
|
|
1271
|
+
counter = 1
|
|
1272
|
+
while table_name in self.db_manager.loaded_tables:
|
|
1273
|
+
table_name = f"{original_name}_{counter}"
|
|
1274
|
+
counter += 1
|
|
1275
|
+
|
|
1276
|
+
# Register the table in the database manager
|
|
1277
|
+
self.db_manager.register_dataframe(df, table_name, file_name)
|
|
1278
|
+
|
|
1279
|
+
# Update tracking
|
|
1280
|
+
self.db_manager.loaded_tables[table_name] = file_name
|
|
1281
|
+
self.db_manager.table_columns[table_name] = df.columns.tolist()
|
|
1282
|
+
|
|
1283
|
+
# Update UI
|
|
1284
|
+
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
1285
|
+
self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
|
|
1286
|
+
|
|
1287
|
+
# Update completer with new table and column names
|
|
1288
|
+
self.update_completer()
|
|
1363
1289
|
|
|
1364
1290
|
# Show success message
|
|
1365
1291
|
QMessageBox.information(
|
|
1366
1292
|
self,
|
|
1367
1293
|
"Export Successful",
|
|
1368
|
-
f"Data has been exported to:\n{file_name}",
|
|
1294
|
+
f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
|
|
1369
1295
|
QMessageBox.StandardButton.Ok
|
|
1370
1296
|
)
|
|
1371
1297
|
except Exception as e:
|
|
@@ -1373,7 +1299,12 @@ LIMIT 10
|
|
|
1373
1299
|
self.statusBar().showMessage('Error exporting data')
|
|
1374
1300
|
|
|
1375
1301
|
def export_to_parquet(self):
|
|
1376
|
-
|
|
1302
|
+
# Get the current tab
|
|
1303
|
+
current_tab = self.get_current_tab()
|
|
1304
|
+
if not current_tab:
|
|
1305
|
+
return
|
|
1306
|
+
|
|
1307
|
+
if current_tab.results_table.rowCount() == 0:
|
|
1377
1308
|
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
1378
1309
|
return
|
|
1379
1310
|
|
|
@@ -1389,13 +1320,36 @@ LIMIT 10
|
|
|
1389
1320
|
df = self.get_table_data_as_dataframe()
|
|
1390
1321
|
df.to_parquet(file_name, index=False)
|
|
1391
1322
|
|
|
1392
|
-
|
|
1323
|
+
# Generate table name from file name
|
|
1324
|
+
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1325
|
+
table_name = self.db_manager.sanitize_table_name(base_name)
|
|
1326
|
+
|
|
1327
|
+
# Ensure unique table name
|
|
1328
|
+
original_name = table_name
|
|
1329
|
+
counter = 1
|
|
1330
|
+
while table_name in self.db_manager.loaded_tables:
|
|
1331
|
+
table_name = f"{original_name}_{counter}"
|
|
1332
|
+
counter += 1
|
|
1333
|
+
|
|
1334
|
+
# Register the table in the database manager
|
|
1335
|
+
self.db_manager.register_dataframe(df, table_name, file_name)
|
|
1336
|
+
|
|
1337
|
+
# Update tracking
|
|
1338
|
+
self.db_manager.loaded_tables[table_name] = file_name
|
|
1339
|
+
self.db_manager.table_columns[table_name] = df.columns.tolist()
|
|
1340
|
+
|
|
1341
|
+
# Update UI
|
|
1342
|
+
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
1343
|
+
self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
|
|
1344
|
+
|
|
1345
|
+
# Update completer with new table and column names
|
|
1346
|
+
self.update_completer()
|
|
1393
1347
|
|
|
1394
1348
|
# Show success message
|
|
1395
1349
|
QMessageBox.information(
|
|
1396
1350
|
self,
|
|
1397
1351
|
"Export Successful",
|
|
1398
|
-
f"Data has been exported to:\n{file_name}",
|
|
1352
|
+
f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
|
|
1399
1353
|
QMessageBox.StandardButton.Ok
|
|
1400
1354
|
)
|
|
1401
1355
|
except Exception as e:
|
|
@@ -1404,12 +1358,17 @@ LIMIT 10
|
|
|
1404
1358
|
|
|
1405
1359
|
def get_table_data_as_dataframe(self):
|
|
1406
1360
|
"""Helper function to convert table widget data to a DataFrame"""
|
|
1407
|
-
|
|
1361
|
+
# Get the current tab
|
|
1362
|
+
current_tab = self.get_current_tab()
|
|
1363
|
+
if not current_tab:
|
|
1364
|
+
return pd.DataFrame()
|
|
1365
|
+
|
|
1366
|
+
headers = [current_tab.results_table.horizontalHeaderItem(i).text() for i in range(current_tab.results_table.columnCount())]
|
|
1408
1367
|
data = []
|
|
1409
|
-
for row in range(
|
|
1368
|
+
for row in range(current_tab.results_table.rowCount()):
|
|
1410
1369
|
row_data = []
|
|
1411
|
-
for column in range(
|
|
1412
|
-
item =
|
|
1370
|
+
for column in range(current_tab.results_table.columnCount()):
|
|
1371
|
+
item = current_tab.results_table.item(row, column)
|
|
1413
1372
|
row_data.append(item.text() if item else '')
|
|
1414
1373
|
data.append(row_data)
|
|
1415
1374
|
return pd.DataFrame(data, columns=headers)
|
|
@@ -1418,56 +1377,1040 @@ LIMIT 10
|
|
|
1418
1377
|
"""Handle global keyboard shortcuts"""
|
|
1419
1378
|
# Execute query with Ctrl+Enter or Cmd+Enter (for Mac)
|
|
1420
1379
|
if event.key() == Qt.Key.Key_Return and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1421
|
-
self.
|
|
1380
|
+
self.execute_query()
|
|
1422
1381
|
return
|
|
1423
1382
|
|
|
1424
|
-
#
|
|
1425
|
-
if event.key() == Qt.Key.
|
|
1426
|
-
self.
|
|
1383
|
+
# Add new tab with Ctrl+T
|
|
1384
|
+
if event.key() == Qt.Key.Key_T and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1385
|
+
self.add_tab()
|
|
1386
|
+
return
|
|
1387
|
+
|
|
1388
|
+
# Close current tab with Ctrl+W
|
|
1389
|
+
if event.key() == Qt.Key.Key_W and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1390
|
+
self.close_current_tab()
|
|
1391
|
+
return
|
|
1392
|
+
|
|
1393
|
+
# Duplicate tab with Ctrl+D
|
|
1394
|
+
if event.key() == Qt.Key.Key_D and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1395
|
+
self.duplicate_current_tab()
|
|
1396
|
+
return
|
|
1397
|
+
|
|
1398
|
+
# Rename tab with Ctrl+R
|
|
1399
|
+
if event.key() == Qt.Key.Key_R and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1400
|
+
self.rename_current_tab()
|
|
1427
1401
|
return
|
|
1428
1402
|
|
|
1429
1403
|
super().keyPressEvent(event)
|
|
1430
1404
|
|
|
1405
|
+
def closeEvent(self, event):
|
|
1406
|
+
"""Ensure proper cleanup of database connections when closing the application"""
|
|
1407
|
+
try:
|
|
1408
|
+
# Check for unsaved changes
|
|
1409
|
+
if self.has_unsaved_changes():
|
|
1410
|
+
reply = QMessageBox.question(self, 'Save Changes',
|
|
1411
|
+
'Do you want to save your changes before closing?',
|
|
1412
|
+
QMessageBox.StandardButton.Save |
|
|
1413
|
+
QMessageBox.StandardButton.Discard |
|
|
1414
|
+
QMessageBox.StandardButton.Cancel)
|
|
1415
|
+
|
|
1416
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
1417
|
+
self.save_project()
|
|
1418
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
1419
|
+
event.ignore()
|
|
1420
|
+
return
|
|
1421
|
+
|
|
1422
|
+
# Save window state and settings
|
|
1423
|
+
self.save_recent_projects()
|
|
1424
|
+
|
|
1425
|
+
# Close database connections
|
|
1426
|
+
self.db_manager.close_connection()
|
|
1427
|
+
event.accept()
|
|
1428
|
+
except Exception as e:
|
|
1429
|
+
QMessageBox.warning(self, "Cleanup Warning",
|
|
1430
|
+
f"Warning: Could not properly close database connection:\n{str(e)}")
|
|
1431
|
+
event.accept()
|
|
1432
|
+
|
|
1433
|
+
def has_unsaved_changes(self):
|
|
1434
|
+
"""Check if there are unsaved changes in the project"""
|
|
1435
|
+
if not self.current_project_file:
|
|
1436
|
+
return (self.tab_widget.count() > 0 and any(self.tab_widget.widget(i).get_query_text().strip()
|
|
1437
|
+
for i in range(self.tab_widget.count()))) or bool(self.db_manager.loaded_tables)
|
|
1438
|
+
|
|
1439
|
+
try:
|
|
1440
|
+
# Load the last saved state
|
|
1441
|
+
with open(self.current_project_file, 'r') as f:
|
|
1442
|
+
saved_data = json.load(f)
|
|
1443
|
+
|
|
1444
|
+
# Prepare current tab data
|
|
1445
|
+
current_tabs_data = []
|
|
1446
|
+
for i in range(self.tab_widget.count()):
|
|
1447
|
+
tab = self.tab_widget.widget(i)
|
|
1448
|
+
tab_data = {
|
|
1449
|
+
'title': self.tab_widget.tabText(i),
|
|
1450
|
+
'query': tab.get_query_text()
|
|
1451
|
+
}
|
|
1452
|
+
current_tabs_data.append(tab_data)
|
|
1453
|
+
|
|
1454
|
+
# Compare current state with saved state
|
|
1455
|
+
current_data = {
|
|
1456
|
+
'tables': {
|
|
1457
|
+
name: {
|
|
1458
|
+
'file_path': path,
|
|
1459
|
+
'columns': self.db_manager.table_columns.get(name, [])
|
|
1460
|
+
}
|
|
1461
|
+
for name, path in self.db_manager.loaded_tables.items()
|
|
1462
|
+
},
|
|
1463
|
+
'tabs': current_tabs_data,
|
|
1464
|
+
'connection_type': self.db_manager.connection_type
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
# Compare tables and connection type
|
|
1468
|
+
if (current_data['connection_type'] != saved_data.get('connection_type') or
|
|
1469
|
+
len(current_data['tables']) != len(saved_data.get('tables', {}))):
|
|
1470
|
+
return True
|
|
1471
|
+
|
|
1472
|
+
# Compare tab data
|
|
1473
|
+
if 'tabs' not in saved_data or len(current_data['tabs']) != len(saved_data['tabs']):
|
|
1474
|
+
return True
|
|
1475
|
+
|
|
1476
|
+
for i, tab_data in enumerate(current_data['tabs']):
|
|
1477
|
+
saved_tab = saved_data['tabs'][i]
|
|
1478
|
+
if (tab_data['title'] != saved_tab.get('title', '') or
|
|
1479
|
+
tab_data['query'] != saved_tab.get('query', '')):
|
|
1480
|
+
return True
|
|
1481
|
+
|
|
1482
|
+
# If we get here, everything matches
|
|
1483
|
+
return False
|
|
1484
|
+
|
|
1485
|
+
except Exception:
|
|
1486
|
+
# If there's any error reading the saved file, assume there are unsaved changes
|
|
1487
|
+
return True
|
|
1488
|
+
|
|
1489
|
+
def show_tables_context_menu(self, position):
|
|
1490
|
+
"""Show context menu for tables list"""
|
|
1491
|
+
item = self.tables_list.itemAt(position)
|
|
1492
|
+
if not item:
|
|
1493
|
+
return
|
|
1494
|
+
|
|
1495
|
+
# Get current tab
|
|
1496
|
+
current_tab = self.get_current_tab()
|
|
1497
|
+
if not current_tab:
|
|
1498
|
+
return
|
|
1499
|
+
|
|
1500
|
+
# Get table name without the file info in parentheses
|
|
1501
|
+
table_name = item.text().split(' (')[0]
|
|
1502
|
+
|
|
1503
|
+
# Create context menu
|
|
1504
|
+
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
|
+
""")
|
|
1519
|
+
|
|
1520
|
+
# Add menu actions
|
|
1521
|
+
select_from_action = context_menu.addAction("Select from")
|
|
1522
|
+
add_to_editor_action = context_menu.addAction("Just add to editor")
|
|
1523
|
+
context_menu.addSeparator()
|
|
1524
|
+
rename_action = context_menu.addAction("Rename table...")
|
|
1525
|
+
delete_action = context_menu.addAction("Delete table")
|
|
1526
|
+
delete_action.setIcon(QIcon.fromTheme("edit-delete"))
|
|
1527
|
+
|
|
1528
|
+
# Show menu and get selected action
|
|
1529
|
+
action = context_menu.exec(self.tables_list.mapToGlobal(position))
|
|
1530
|
+
|
|
1531
|
+
if action == select_from_action:
|
|
1532
|
+
# Insert "SELECT * FROM table_name" at cursor position
|
|
1533
|
+
cursor = current_tab.query_edit.textCursor()
|
|
1534
|
+
cursor.insertText(f"SELECT * FROM {table_name}")
|
|
1535
|
+
current_tab.query_edit.setFocus()
|
|
1536
|
+
elif action == add_to_editor_action:
|
|
1537
|
+
# Just insert the table name at cursor position
|
|
1538
|
+
cursor = current_tab.query_edit.textCursor()
|
|
1539
|
+
cursor.insertText(table_name)
|
|
1540
|
+
current_tab.query_edit.setFocus()
|
|
1541
|
+
elif action == rename_action:
|
|
1542
|
+
# Show rename dialog
|
|
1543
|
+
new_name, ok = QInputDialog.getText(
|
|
1544
|
+
self,
|
|
1545
|
+
"Rename Table",
|
|
1546
|
+
"Enter new table name:",
|
|
1547
|
+
QLineEdit.EchoMode.Normal,
|
|
1548
|
+
table_name
|
|
1549
|
+
)
|
|
1550
|
+
if ok and new_name:
|
|
1551
|
+
if self.rename_table(table_name, new_name):
|
|
1552
|
+
# Update the item text
|
|
1553
|
+
source = item.text().split(' (')[1][:-1] # Get the source part
|
|
1554
|
+
item.setText(f"{new_name} ({source})")
|
|
1555
|
+
self.statusBar().showMessage(f'Table renamed to "{new_name}"')
|
|
1556
|
+
elif action == delete_action:
|
|
1557
|
+
# Show confirmation dialog
|
|
1558
|
+
reply = QMessageBox.question(
|
|
1559
|
+
self,
|
|
1560
|
+
"Delete Table",
|
|
1561
|
+
f"Are you sure you want to delete table '{table_name}'?",
|
|
1562
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
1563
|
+
)
|
|
1564
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
1565
|
+
self.remove_selected_table()
|
|
1566
|
+
|
|
1567
|
+
def new_project(self):
|
|
1568
|
+
"""Create a new project by clearing current state"""
|
|
1569
|
+
if self.db_manager.is_connected():
|
|
1570
|
+
reply = QMessageBox.question(self, 'New Project',
|
|
1571
|
+
'Are you sure you want to start a new project? All unsaved changes will be lost.',
|
|
1572
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
1573
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
1574
|
+
# Close existing connection
|
|
1575
|
+
self.db_manager.close_connection()
|
|
1576
|
+
|
|
1577
|
+
# Reset state
|
|
1578
|
+
self.tables_list.clear()
|
|
1579
|
+
|
|
1580
|
+
# Clear all tabs except one
|
|
1581
|
+
while self.tab_widget.count() > 1:
|
|
1582
|
+
self.close_tab(1) # Always close tab at index 1 to keep at least one tab
|
|
1583
|
+
|
|
1584
|
+
# Clear the remaining tab
|
|
1585
|
+
first_tab = self.get_tab_at_index(0)
|
|
1586
|
+
if first_tab:
|
|
1587
|
+
first_tab.set_query_text("")
|
|
1588
|
+
first_tab.results_table.setRowCount(0)
|
|
1589
|
+
first_tab.results_table.setColumnCount(0)
|
|
1590
|
+
first_tab.row_count_label.setText("")
|
|
1591
|
+
first_tab.results_title.setText("RESULTS")
|
|
1592
|
+
|
|
1593
|
+
self.current_project_file = None
|
|
1594
|
+
self.setWindowTitle('SQL Shell')
|
|
1595
|
+
self.db_info_label.setText("No database connected")
|
|
1596
|
+
self.statusBar().showMessage('New project created')
|
|
1597
|
+
|
|
1598
|
+
def save_project(self):
|
|
1599
|
+
"""Save the current project"""
|
|
1600
|
+
if not self.current_project_file:
|
|
1601
|
+
self.save_project_as()
|
|
1602
|
+
return
|
|
1603
|
+
|
|
1604
|
+
self.save_project_to_file(self.current_project_file)
|
|
1605
|
+
|
|
1606
|
+
def save_project_as(self):
|
|
1607
|
+
"""Save the current project to a new file"""
|
|
1608
|
+
file_name, _ = QFileDialog.getSaveFileName(
|
|
1609
|
+
self,
|
|
1610
|
+
"Save Project",
|
|
1611
|
+
"",
|
|
1612
|
+
"SQL Shell Project (*.sqls);;All Files (*)"
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1615
|
+
if file_name:
|
|
1616
|
+
if not file_name.endswith('.sqls'):
|
|
1617
|
+
file_name += '.sqls'
|
|
1618
|
+
self.save_project_to_file(file_name)
|
|
1619
|
+
self.current_project_file = file_name
|
|
1620
|
+
self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
|
|
1621
|
+
|
|
1622
|
+
def save_project_to_file(self, file_name):
|
|
1623
|
+
"""Save project data to a file"""
|
|
1624
|
+
try:
|
|
1625
|
+
# Save tab information
|
|
1626
|
+
tabs_data = []
|
|
1627
|
+
for i in range(self.tab_widget.count()):
|
|
1628
|
+
tab = self.tab_widget.widget(i)
|
|
1629
|
+
tab_data = {
|
|
1630
|
+
'title': self.tab_widget.tabText(i),
|
|
1631
|
+
'query': tab.get_query_text()
|
|
1632
|
+
}
|
|
1633
|
+
tabs_data.append(tab_data)
|
|
1634
|
+
|
|
1635
|
+
project_data = {
|
|
1636
|
+
'tables': {},
|
|
1637
|
+
'tabs': tabs_data,
|
|
1638
|
+
'connection_type': self.db_manager.connection_type,
|
|
1639
|
+
'database_path': None # Initialize to None
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
# If we have a database connection, save the path
|
|
1643
|
+
if self.db_manager.is_connected() and hasattr(self.db_manager, 'database_path'):
|
|
1644
|
+
project_data['database_path'] = self.db_manager.database_path
|
|
1645
|
+
|
|
1646
|
+
# Save table information
|
|
1647
|
+
for table_name, file_path in self.db_manager.loaded_tables.items():
|
|
1648
|
+
# For database tables and query results, store the special identifier
|
|
1649
|
+
if file_path in ['database', 'query_result']:
|
|
1650
|
+
source_path = file_path
|
|
1651
|
+
else:
|
|
1652
|
+
# For file-based tables, store the absolute path
|
|
1653
|
+
source_path = os.path.abspath(file_path)
|
|
1654
|
+
|
|
1655
|
+
project_data['tables'][table_name] = {
|
|
1656
|
+
'file_path': source_path,
|
|
1657
|
+
'columns': self.db_manager.table_columns.get(table_name, [])
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
with open(file_name, 'w') as f:
|
|
1661
|
+
json.dump(project_data, f, indent=4)
|
|
1662
|
+
|
|
1663
|
+
# Add to recent projects
|
|
1664
|
+
self.add_recent_project(os.path.abspath(file_name))
|
|
1665
|
+
|
|
1666
|
+
self.statusBar().showMessage(f'Project saved to {file_name}')
|
|
1667
|
+
|
|
1668
|
+
except Exception as e:
|
|
1669
|
+
QMessageBox.critical(self, "Error",
|
|
1670
|
+
f"Failed to save project:\n\n{str(e)}")
|
|
1671
|
+
|
|
1672
|
+
def open_project(self, file_name=None):
|
|
1673
|
+
"""Open a project file"""
|
|
1674
|
+
if not file_name:
|
|
1675
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
1676
|
+
self,
|
|
1677
|
+
"Open Project",
|
|
1678
|
+
"",
|
|
1679
|
+
"SQL Shell Project (*.sqls);;All Files (*)"
|
|
1680
|
+
)
|
|
1681
|
+
|
|
1682
|
+
if file_name:
|
|
1683
|
+
try:
|
|
1684
|
+
# Create a progress dialog to keep UI responsive
|
|
1685
|
+
progress = QProgressDialog("Loading project...", "Cancel", 0, 100, self)
|
|
1686
|
+
progress.setWindowTitle("Opening Project")
|
|
1687
|
+
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
1688
|
+
progress.setMinimumDuration(500) # Show after 500ms delay
|
|
1689
|
+
progress.setValue(0)
|
|
1690
|
+
|
|
1691
|
+
# Load project data
|
|
1692
|
+
with open(file_name, 'r') as f:
|
|
1693
|
+
project_data = json.load(f)
|
|
1694
|
+
|
|
1695
|
+
# Update progress
|
|
1696
|
+
progress.setValue(10)
|
|
1697
|
+
QApplication.processEvents()
|
|
1698
|
+
|
|
1699
|
+
# Start fresh
|
|
1700
|
+
self.new_project()
|
|
1701
|
+
progress.setValue(15)
|
|
1702
|
+
QApplication.processEvents()
|
|
1703
|
+
|
|
1704
|
+
# Check if there's a database path in the project
|
|
1705
|
+
has_database_path = 'database_path' in project_data and project_data['database_path']
|
|
1706
|
+
has_database_tables = any(table_info.get('file_path') == 'database'
|
|
1707
|
+
for table_info in project_data.get('tables', {}).values())
|
|
1708
|
+
|
|
1709
|
+
# Set a flag to track if database tables are loaded
|
|
1710
|
+
database_tables_loaded = False
|
|
1711
|
+
|
|
1712
|
+
# If the project contains database tables and a database path, try to connect to it
|
|
1713
|
+
progress.setLabelText("Connecting to database...")
|
|
1714
|
+
if has_database_path and has_database_tables:
|
|
1715
|
+
database_path = project_data['database_path']
|
|
1716
|
+
try:
|
|
1717
|
+
if os.path.exists(database_path):
|
|
1718
|
+
# Connect to the database
|
|
1719
|
+
self.db_manager.open_database(database_path)
|
|
1720
|
+
self.db_info_label.setText(self.db_manager.get_connection_info())
|
|
1721
|
+
self.statusBar().showMessage(f"Connected to database: {database_path}")
|
|
1722
|
+
|
|
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
|
+
# Mark database tables as loaded
|
|
1729
|
+
database_tables_loaded = True
|
|
1730
|
+
else:
|
|
1731
|
+
database_tables_loaded = False
|
|
1732
|
+
QMessageBox.warning(self, "Database Not Found",
|
|
1733
|
+
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.")
|
|
1735
|
+
except Exception as e:
|
|
1736
|
+
database_tables_loaded = False
|
|
1737
|
+
QMessageBox.warning(self, "Database Connection Error",
|
|
1738
|
+
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.")
|
|
1740
|
+
else:
|
|
1741
|
+
# Create connection if needed (we don't have a specific database to connect to)
|
|
1742
|
+
database_tables_loaded = False
|
|
1743
|
+
if not self.db_manager.is_connected():
|
|
1744
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
1745
|
+
self.db_info_label.setText(connection_info)
|
|
1746
|
+
elif 'connection_type' in project_data and project_data['connection_type'] != self.db_manager.connection_type:
|
|
1747
|
+
# If connected but with a different database type than what was saved in the project
|
|
1748
|
+
QMessageBox.warning(self, "Database Type Mismatch",
|
|
1749
|
+
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.")
|
|
1751
|
+
|
|
1752
|
+
progress.setValue(30)
|
|
1753
|
+
QApplication.processEvents()
|
|
1754
|
+
|
|
1755
|
+
# Calculate progress steps for loading tables
|
|
1756
|
+
table_count = len(project_data.get('tables', {}))
|
|
1757
|
+
table_progress_start = 30
|
|
1758
|
+
table_progress_end = 70
|
|
1759
|
+
table_progress_step = (table_progress_end - table_progress_start) / max(1, table_count)
|
|
1760
|
+
current_progress = table_progress_start
|
|
1761
|
+
|
|
1762
|
+
# Load tables
|
|
1763
|
+
for table_name, table_info in project_data.get('tables', {}).items():
|
|
1764
|
+
if progress.wasCanceled():
|
|
1765
|
+
break
|
|
1766
|
+
|
|
1767
|
+
progress.setLabelText(f"Loading table: {table_name}")
|
|
1768
|
+
file_path = table_info['file_path']
|
|
1769
|
+
try:
|
|
1770
|
+
if file_path == 'database':
|
|
1771
|
+
# Skip if we already loaded database tables by connecting to the database
|
|
1772
|
+
if database_tables_loaded:
|
|
1773
|
+
continue
|
|
1774
|
+
|
|
1775
|
+
# For database tables, we need to check if the original database is connected
|
|
1776
|
+
# Don't try to SELECT from non-existent tables
|
|
1777
|
+
# Instead just register the table name for UI display
|
|
1778
|
+
self.db_manager.loaded_tables[table_name] = 'database'
|
|
1779
|
+
|
|
1780
|
+
# If we have column information, use it
|
|
1781
|
+
if 'columns' in table_info:
|
|
1782
|
+
self.db_manager.table_columns[table_name] = table_info['columns']
|
|
1783
|
+
|
|
1784
|
+
# Add to the UI list
|
|
1785
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
1786
|
+
elif file_path == 'query_result':
|
|
1787
|
+
# For tables from query results, we'll need to re-run the query
|
|
1788
|
+
# For now, just note it as a query result table
|
|
1789
|
+
self.db_manager.loaded_tables[table_name] = 'query_result'
|
|
1790
|
+
self.tables_list.addItem(f"{table_name} (query result)")
|
|
1791
|
+
elif os.path.exists(file_path):
|
|
1792
|
+
# Use the database manager to load the file
|
|
1793
|
+
try:
|
|
1794
|
+
loaded_table_name, df = self.db_manager.load_file(file_path)
|
|
1795
|
+
self.tables_list.addItem(f"{loaded_table_name} ({os.path.basename(file_path)})")
|
|
1796
|
+
except Exception as e:
|
|
1797
|
+
QMessageBox.warning(self, "Warning",
|
|
1798
|
+
f"Failed to load file for table {table_name}:\n{str(e)}")
|
|
1799
|
+
continue
|
|
1800
|
+
else:
|
|
1801
|
+
QMessageBox.warning(self, "Warning",
|
|
1802
|
+
f"Could not find file for table {table_name}: {file_path}")
|
|
1803
|
+
continue
|
|
1804
|
+
|
|
1805
|
+
except Exception as e:
|
|
1806
|
+
QMessageBox.warning(self, "Warning",
|
|
1807
|
+
f"Failed to load table {table_name}:\n{str(e)}")
|
|
1808
|
+
|
|
1809
|
+
# Update progress for this table
|
|
1810
|
+
current_progress += table_progress_step
|
|
1811
|
+
progress.setValue(int(current_progress))
|
|
1812
|
+
QApplication.processEvents() # Keep UI responsive
|
|
1813
|
+
|
|
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
|
+
# Check if the operation was canceled
|
|
1821
|
+
if progress.wasCanceled():
|
|
1822
|
+
self.statusBar().showMessage("Project loading was canceled")
|
|
1823
|
+
progress.close()
|
|
1824
|
+
return
|
|
1825
|
+
|
|
1826
|
+
progress.setValue(75)
|
|
1827
|
+
progress.setLabelText("Setting up tabs...")
|
|
1828
|
+
QApplication.processEvents()
|
|
1829
|
+
|
|
1830
|
+
# Load tabs in a more efficient way
|
|
1831
|
+
if 'tabs' in project_data and project_data['tabs']:
|
|
1832
|
+
try:
|
|
1833
|
+
# Temporarily disable signals
|
|
1834
|
+
self.tab_widget.blockSignals(True)
|
|
1835
|
+
|
|
1836
|
+
# First, pre-remove any existing tabs
|
|
1837
|
+
while self.tab_widget.count() > 0:
|
|
1838
|
+
widget = self.tab_widget.widget(0)
|
|
1839
|
+
self.tab_widget.removeTab(0)
|
|
1840
|
+
if widget in self.tabs:
|
|
1841
|
+
self.tabs.remove(widget)
|
|
1842
|
+
widget.deleteLater()
|
|
1843
|
+
|
|
1844
|
+
# Then create all tab widgets at once (empty)
|
|
1845
|
+
tab_count = len(project_data['tabs'])
|
|
1846
|
+
tab_progress_step = 15 / max(1, tab_count)
|
|
1847
|
+
progress.setValue(80)
|
|
1848
|
+
QApplication.processEvents()
|
|
1849
|
+
|
|
1850
|
+
# Create all tab widgets first without setting content
|
|
1851
|
+
for i, tab_data in enumerate(project_data['tabs']):
|
|
1852
|
+
# Create a new tab
|
|
1853
|
+
tab = QueryTab(self)
|
|
1854
|
+
self.tabs.append(tab)
|
|
1855
|
+
|
|
1856
|
+
# Add to tab widget
|
|
1857
|
+
title = tab_data.get('title', f'Query {i+1}')
|
|
1858
|
+
self.tab_widget.addTab(tab, title)
|
|
1859
|
+
|
|
1860
|
+
progress.setValue(int(80 + i * tab_progress_step/2))
|
|
1861
|
+
QApplication.processEvents()
|
|
1862
|
+
|
|
1863
|
+
# Now set the content for each tab
|
|
1864
|
+
for i, tab_data in enumerate(project_data['tabs']):
|
|
1865
|
+
# Get the tab and set its query text
|
|
1866
|
+
tab = self.tab_widget.widget(i)
|
|
1867
|
+
if tab and 'query' in tab_data:
|
|
1868
|
+
tab.set_query_text(tab_data['query'])
|
|
1869
|
+
|
|
1870
|
+
progress.setValue(int(87 + i * tab_progress_step/2))
|
|
1871
|
+
QApplication.processEvents()
|
|
1872
|
+
|
|
1873
|
+
# Re-enable signals
|
|
1874
|
+
self.tab_widget.blockSignals(False)
|
|
1875
|
+
|
|
1876
|
+
# Set current tab
|
|
1877
|
+
if self.tab_widget.count() > 0:
|
|
1878
|
+
self.tab_widget.setCurrentIndex(0)
|
|
1879
|
+
|
|
1880
|
+
except Exception as e:
|
|
1881
|
+
# If there's an error, ensure we restore signals
|
|
1882
|
+
self.tab_widget.blockSignals(False)
|
|
1883
|
+
self.statusBar().showMessage(f"Error loading tabs: {str(e)}")
|
|
1884
|
+
# Create a single default tab if all fails
|
|
1885
|
+
if self.tab_widget.count() == 0:
|
|
1886
|
+
self.add_tab()
|
|
1887
|
+
else:
|
|
1888
|
+
# Create default tab if no tabs in project
|
|
1889
|
+
self.add_tab()
|
|
1890
|
+
|
|
1891
|
+
progress.setValue(90)
|
|
1892
|
+
progress.setLabelText("Finishing up...")
|
|
1893
|
+
QApplication.processEvents()
|
|
1894
|
+
|
|
1895
|
+
# Update UI
|
|
1896
|
+
self.current_project_file = file_name
|
|
1897
|
+
self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
|
|
1898
|
+
|
|
1899
|
+
# Add to recent projects
|
|
1900
|
+
self.add_recent_project(os.path.abspath(file_name))
|
|
1901
|
+
|
|
1902
|
+
# Defer the auto-completer update to after loading is complete
|
|
1903
|
+
# This helps prevent UI freezing during project loading
|
|
1904
|
+
progress.setValue(95)
|
|
1905
|
+
QApplication.processEvents()
|
|
1906
|
+
|
|
1907
|
+
# Use a timer to update the completer after the UI is responsive
|
|
1908
|
+
complete_timer = QTimer()
|
|
1909
|
+
complete_timer.setSingleShot(True)
|
|
1910
|
+
complete_timer.timeout.connect(self.update_completer)
|
|
1911
|
+
complete_timer.start(100) # Short delay before updating completer
|
|
1912
|
+
|
|
1913
|
+
# Queue another update for reliability - sometimes the first update might not fully complete
|
|
1914
|
+
failsafe_timer = QTimer()
|
|
1915
|
+
failsafe_timer.setSingleShot(True)
|
|
1916
|
+
failsafe_timer.timeout.connect(self.update_completer)
|
|
1917
|
+
failsafe_timer.start(2000) # Try again after 2 seconds to ensure completion is loaded
|
|
1918
|
+
|
|
1919
|
+
progress.setValue(100)
|
|
1920
|
+
QApplication.processEvents()
|
|
1921
|
+
|
|
1922
|
+
self.statusBar().showMessage(f'Project loaded from {file_name}')
|
|
1923
|
+
progress.close()
|
|
1924
|
+
|
|
1925
|
+
except Exception as e:
|
|
1926
|
+
QMessageBox.critical(self, "Error",
|
|
1927
|
+
f"Failed to open project:\n\n{str(e)}")
|
|
1928
|
+
|
|
1929
|
+
def rename_table(self, old_name, new_name):
|
|
1930
|
+
"""Rename a table in the database and update tracking"""
|
|
1931
|
+
try:
|
|
1932
|
+
# Use the database manager to rename the table
|
|
1933
|
+
result = self.db_manager.rename_table(old_name, new_name)
|
|
1934
|
+
|
|
1935
|
+
if result:
|
|
1936
|
+
# Update completer
|
|
1937
|
+
self.update_completer()
|
|
1938
|
+
return True
|
|
1939
|
+
|
|
1940
|
+
return False
|
|
1941
|
+
|
|
1942
|
+
except Exception as e:
|
|
1943
|
+
QMessageBox.critical(self, "Error", f"Failed to rename table:\n\n{str(e)}")
|
|
1944
|
+
return False
|
|
1945
|
+
|
|
1946
|
+
def load_recent_projects(self):
|
|
1947
|
+
"""Load recent projects from settings file"""
|
|
1948
|
+
try:
|
|
1949
|
+
settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
|
|
1950
|
+
if os.path.exists(settings_file):
|
|
1951
|
+
with open(settings_file, 'r') as f:
|
|
1952
|
+
settings = json.load(f)
|
|
1953
|
+
self.recent_projects = settings.get('recent_projects', [])
|
|
1954
|
+
|
|
1955
|
+
# Load window settings if available
|
|
1956
|
+
window_settings = settings.get('window', {})
|
|
1957
|
+
if window_settings:
|
|
1958
|
+
self.restore_window_state(window_settings)
|
|
1959
|
+
except Exception:
|
|
1960
|
+
self.recent_projects = []
|
|
1961
|
+
|
|
1962
|
+
def save_recent_projects(self):
|
|
1963
|
+
"""Save recent projects to settings file"""
|
|
1964
|
+
try:
|
|
1965
|
+
settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
|
|
1966
|
+
settings = {}
|
|
1967
|
+
if os.path.exists(settings_file):
|
|
1968
|
+
with open(settings_file, 'r') as f:
|
|
1969
|
+
settings = json.load(f)
|
|
1970
|
+
settings['recent_projects'] = self.recent_projects
|
|
1971
|
+
|
|
1972
|
+
# Save window settings
|
|
1973
|
+
window_settings = self.save_window_state()
|
|
1974
|
+
settings['window'] = window_settings
|
|
1975
|
+
|
|
1976
|
+
with open(settings_file, 'w') as f:
|
|
1977
|
+
json.dump(settings, f, indent=4)
|
|
1978
|
+
except Exception as e:
|
|
1979
|
+
print(f"Error saving recent projects: {e}")
|
|
1980
|
+
|
|
1981
|
+
def save_window_state(self):
|
|
1982
|
+
"""Save current window state"""
|
|
1983
|
+
window_settings = {
|
|
1984
|
+
'maximized': self.isMaximized(),
|
|
1985
|
+
'geometry': {
|
|
1986
|
+
'x': self.geometry().x(),
|
|
1987
|
+
'y': self.geometry().y(),
|
|
1988
|
+
'width': self.geometry().width(),
|
|
1989
|
+
'height': self.geometry().height()
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return window_settings
|
|
1993
|
+
|
|
1994
|
+
def restore_window_state(self, window_settings):
|
|
1995
|
+
"""Restore window state from settings"""
|
|
1996
|
+
try:
|
|
1997
|
+
# Check if we have valid geometry settings
|
|
1998
|
+
geometry = window_settings.get('geometry', {})
|
|
1999
|
+
if all(key in geometry for key in ['x', 'y', 'width', 'height']):
|
|
2000
|
+
x, y = geometry['x'], geometry['y']
|
|
2001
|
+
width, height = geometry['width'], geometry['height']
|
|
2002
|
+
|
|
2003
|
+
# Ensure the window is visible on the current screen
|
|
2004
|
+
screen = QApplication.primaryScreen()
|
|
2005
|
+
screen_geometry = screen.availableGeometry()
|
|
2006
|
+
|
|
2007
|
+
# Adjust if window would be off-screen
|
|
2008
|
+
if x < 0 or x + 100 > screen_geometry.width():
|
|
2009
|
+
x = 100
|
|
2010
|
+
if y < 0 or y + 100 > screen_geometry.height():
|
|
2011
|
+
y = 100
|
|
2012
|
+
|
|
2013
|
+
# Adjust if window is too large for the current screen
|
|
2014
|
+
if width > screen_geometry.width():
|
|
2015
|
+
width = int(screen_geometry.width() * 0.85)
|
|
2016
|
+
if height > screen_geometry.height():
|
|
2017
|
+
height = int(screen_geometry.height() * 0.85)
|
|
2018
|
+
|
|
2019
|
+
self.setGeometry(x, y, width, height)
|
|
2020
|
+
|
|
2021
|
+
# Set maximized state if needed
|
|
2022
|
+
if window_settings.get('maximized', False):
|
|
2023
|
+
self.showMaximized()
|
|
2024
|
+
self.was_maximized = True
|
|
2025
|
+
|
|
2026
|
+
except Exception as e:
|
|
2027
|
+
print(f"Error restoring window state: {e}")
|
|
2028
|
+
# Fall back to default geometry
|
|
2029
|
+
screen = QApplication.primaryScreen()
|
|
2030
|
+
screen_geometry = screen.availableGeometry()
|
|
2031
|
+
self.setGeometry(100, 100,
|
|
2032
|
+
min(1400, int(screen_geometry.width() * 0.85)),
|
|
2033
|
+
min(800, int(screen_geometry.height() * 0.85)))
|
|
2034
|
+
|
|
2035
|
+
def add_recent_project(self, project_path):
|
|
2036
|
+
"""Add a project to recent projects list"""
|
|
2037
|
+
if project_path in self.recent_projects:
|
|
2038
|
+
self.recent_projects.remove(project_path)
|
|
2039
|
+
self.recent_projects.insert(0, project_path)
|
|
2040
|
+
self.recent_projects = self.recent_projects[:self.max_recent_projects]
|
|
2041
|
+
self.save_recent_projects()
|
|
2042
|
+
self.update_recent_projects_menu()
|
|
2043
|
+
|
|
2044
|
+
def update_recent_projects_menu(self):
|
|
2045
|
+
"""Update the recent projects menu"""
|
|
2046
|
+
self.recent_projects_menu.clear()
|
|
2047
|
+
|
|
2048
|
+
if not self.recent_projects:
|
|
2049
|
+
no_recent = self.recent_projects_menu.addAction("No Recent Projects")
|
|
2050
|
+
no_recent.setEnabled(False)
|
|
2051
|
+
return
|
|
2052
|
+
|
|
2053
|
+
for project_path in self.recent_projects:
|
|
2054
|
+
if os.path.exists(project_path):
|
|
2055
|
+
action = self.recent_projects_menu.addAction(os.path.basename(project_path))
|
|
2056
|
+
action.setData(project_path)
|
|
2057
|
+
action.triggered.connect(lambda checked, path=project_path: self.open_recent_project(path))
|
|
2058
|
+
|
|
2059
|
+
if self.recent_projects:
|
|
2060
|
+
self.recent_projects_menu.addSeparator()
|
|
2061
|
+
clear_action = self.recent_projects_menu.addAction("Clear Recent Projects")
|
|
2062
|
+
clear_action.triggered.connect(self.clear_recent_projects)
|
|
2063
|
+
|
|
2064
|
+
def open_recent_project(self, project_path):
|
|
2065
|
+
"""Open a project from the recent projects list"""
|
|
2066
|
+
if os.path.exists(project_path):
|
|
2067
|
+
self.current_project_file = project_path
|
|
2068
|
+
self.open_project(project_path)
|
|
2069
|
+
else:
|
|
2070
|
+
QMessageBox.warning(self, "Warning",
|
|
2071
|
+
f"Project file not found:\n{project_path}")
|
|
2072
|
+
self.recent_projects.remove(project_path)
|
|
2073
|
+
self.save_recent_projects()
|
|
2074
|
+
self.update_recent_projects_menu()
|
|
2075
|
+
|
|
2076
|
+
def clear_recent_projects(self):
|
|
2077
|
+
"""Clear the list of recent projects"""
|
|
2078
|
+
self.recent_projects.clear()
|
|
2079
|
+
self.save_recent_projects()
|
|
2080
|
+
self.update_recent_projects_menu()
|
|
2081
|
+
|
|
2082
|
+
def add_tab(self, title="Query 1"):
|
|
2083
|
+
"""Add a new query tab"""
|
|
2084
|
+
# Ensure title is a string
|
|
2085
|
+
title = str(title)
|
|
2086
|
+
|
|
2087
|
+
# Create a new tab with a unique name if needed
|
|
2088
|
+
if title == "Query 1" and self.tab_widget.count() > 0:
|
|
2089
|
+
# Generate a unique tab name (Query 2, Query 3, etc.)
|
|
2090
|
+
# Use a more efficient approach to find a unique name
|
|
2091
|
+
base_name = "Query"
|
|
2092
|
+
existing_names = set()
|
|
2093
|
+
|
|
2094
|
+
# Collect existing tab names first (more efficient than checking each time)
|
|
2095
|
+
for i in range(self.tab_widget.count()):
|
|
2096
|
+
existing_names.add(self.tab_widget.tabText(i))
|
|
2097
|
+
|
|
2098
|
+
# Find the next available number
|
|
2099
|
+
counter = 1
|
|
2100
|
+
while f"{base_name} {counter}" in existing_names:
|
|
2101
|
+
counter += 1
|
|
2102
|
+
title = f"{base_name} {counter}"
|
|
2103
|
+
|
|
2104
|
+
# Create the tab content
|
|
2105
|
+
tab = QueryTab(self)
|
|
2106
|
+
|
|
2107
|
+
# Add to our list of tabs
|
|
2108
|
+
self.tabs.append(tab)
|
|
2109
|
+
|
|
2110
|
+
# Block signals temporarily to improve performance when adding many tabs
|
|
2111
|
+
was_blocked = self.tab_widget.blockSignals(True)
|
|
2112
|
+
|
|
2113
|
+
# Add tab to widget
|
|
2114
|
+
index = self.tab_widget.addTab(tab, title)
|
|
2115
|
+
self.tab_widget.setCurrentIndex(index)
|
|
2116
|
+
|
|
2117
|
+
# Restore signals
|
|
2118
|
+
self.tab_widget.blockSignals(was_blocked)
|
|
2119
|
+
|
|
2120
|
+
# Focus the new tab's query editor
|
|
2121
|
+
tab.query_edit.setFocus()
|
|
2122
|
+
|
|
2123
|
+
# Process events to keep UI responsive
|
|
2124
|
+
QApplication.processEvents()
|
|
2125
|
+
|
|
2126
|
+
return tab
|
|
2127
|
+
|
|
2128
|
+
def duplicate_current_tab(self):
|
|
2129
|
+
"""Duplicate the current tab"""
|
|
2130
|
+
if self.tab_widget.count() == 0:
|
|
2131
|
+
return self.add_tab()
|
|
2132
|
+
|
|
2133
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2134
|
+
if current_idx == -1:
|
|
2135
|
+
return
|
|
2136
|
+
|
|
2137
|
+
# Get current tab data
|
|
2138
|
+
current_tab = self.get_current_tab()
|
|
2139
|
+
current_title = self.tab_widget.tabText(current_idx)
|
|
2140
|
+
|
|
2141
|
+
# Create a new tab with "(Copy)" suffix
|
|
2142
|
+
new_title = f"{current_title} (Copy)"
|
|
2143
|
+
new_tab = self.add_tab(new_title)
|
|
2144
|
+
|
|
2145
|
+
# Copy query text
|
|
2146
|
+
new_tab.set_query_text(current_tab.get_query_text())
|
|
2147
|
+
|
|
2148
|
+
# Return focus to the new tab
|
|
2149
|
+
new_tab.query_edit.setFocus()
|
|
2150
|
+
|
|
2151
|
+
return new_tab
|
|
2152
|
+
|
|
2153
|
+
def rename_current_tab(self):
|
|
2154
|
+
"""Rename the current tab"""
|
|
2155
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2156
|
+
if current_idx == -1:
|
|
2157
|
+
return
|
|
2158
|
+
|
|
2159
|
+
current_title = self.tab_widget.tabText(current_idx)
|
|
2160
|
+
|
|
2161
|
+
new_title, ok = QInputDialog.getText(
|
|
2162
|
+
self,
|
|
2163
|
+
"Rename Tab",
|
|
2164
|
+
"Enter new tab name:",
|
|
2165
|
+
QLineEdit.EchoMode.Normal,
|
|
2166
|
+
current_title
|
|
2167
|
+
)
|
|
2168
|
+
|
|
2169
|
+
if ok and new_title:
|
|
2170
|
+
self.tab_widget.setTabText(current_idx, new_title)
|
|
2171
|
+
|
|
2172
|
+
def handle_tab_double_click(self, index):
|
|
2173
|
+
"""Handle double-clicking on a tab by starting rename immediately"""
|
|
2174
|
+
if index == -1:
|
|
2175
|
+
return
|
|
2176
|
+
|
|
2177
|
+
current_title = self.tab_widget.tabText(index)
|
|
2178
|
+
|
|
2179
|
+
new_title, ok = QInputDialog.getText(
|
|
2180
|
+
self,
|
|
2181
|
+
"Rename Tab",
|
|
2182
|
+
"Enter new tab name:",
|
|
2183
|
+
QLineEdit.EchoMode.Normal,
|
|
2184
|
+
current_title
|
|
2185
|
+
)
|
|
2186
|
+
|
|
2187
|
+
if ok and new_title:
|
|
2188
|
+
self.tab_widget.setTabText(index, new_title)
|
|
2189
|
+
|
|
2190
|
+
def close_tab(self, index):
|
|
2191
|
+
"""Close the tab at the given index"""
|
|
2192
|
+
if self.tab_widget.count() <= 1:
|
|
2193
|
+
# Don't close the last tab, just clear it
|
|
2194
|
+
tab = self.get_tab_at_index(index)
|
|
2195
|
+
if tab:
|
|
2196
|
+
tab.set_query_text("")
|
|
2197
|
+
tab.results_table.clearContents()
|
|
2198
|
+
tab.results_table.setRowCount(0)
|
|
2199
|
+
tab.results_table.setColumnCount(0)
|
|
2200
|
+
return
|
|
2201
|
+
|
|
2202
|
+
# Block signals temporarily to improve performance when removing multiple tabs
|
|
2203
|
+
was_blocked = self.tab_widget.blockSignals(True)
|
|
2204
|
+
|
|
2205
|
+
# Remove the tab
|
|
2206
|
+
widget = self.tab_widget.widget(index)
|
|
2207
|
+
self.tab_widget.removeTab(index)
|
|
2208
|
+
|
|
2209
|
+
# Restore signals
|
|
2210
|
+
self.tab_widget.blockSignals(was_blocked)
|
|
2211
|
+
|
|
2212
|
+
# Remove from our list of tabs
|
|
2213
|
+
if widget in self.tabs:
|
|
2214
|
+
self.tabs.remove(widget)
|
|
2215
|
+
|
|
2216
|
+
# Schedule the widget for deletion instead of immediate deletion
|
|
2217
|
+
widget.deleteLater()
|
|
2218
|
+
|
|
2219
|
+
# Process events to keep UI responsive
|
|
2220
|
+
QApplication.processEvents()
|
|
2221
|
+
|
|
2222
|
+
def close_current_tab(self):
|
|
2223
|
+
"""Close the current tab"""
|
|
2224
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2225
|
+
if current_idx != -1:
|
|
2226
|
+
self.close_tab(current_idx)
|
|
2227
|
+
|
|
2228
|
+
def get_current_tab(self):
|
|
2229
|
+
"""Get the currently active tab"""
|
|
2230
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2231
|
+
if current_idx == -1:
|
|
2232
|
+
return None
|
|
2233
|
+
return self.tab_widget.widget(current_idx)
|
|
2234
|
+
|
|
2235
|
+
def get_tab_at_index(self, index):
|
|
2236
|
+
"""Get the tab at the specified index"""
|
|
2237
|
+
if index < 0 or index >= self.tab_widget.count():
|
|
2238
|
+
return None
|
|
2239
|
+
return self.tab_widget.widget(index)
|
|
2240
|
+
|
|
2241
|
+
def toggle_maximize_window(self):
|
|
2242
|
+
"""Toggle between maximized and normal window state"""
|
|
2243
|
+
if self.isMaximized():
|
|
2244
|
+
self.showNormal()
|
|
2245
|
+
self.was_maximized = False
|
|
2246
|
+
else:
|
|
2247
|
+
self.showMaximized()
|
|
2248
|
+
self.was_maximized = True
|
|
2249
|
+
|
|
2250
|
+
def change_zoom(self, factor):
|
|
2251
|
+
"""Change the zoom level of the application by adjusting font sizes"""
|
|
2252
|
+
try:
|
|
2253
|
+
# Update font sizes for SQL editors
|
|
2254
|
+
for i in range(self.tab_widget.count()):
|
|
2255
|
+
tab = self.tab_widget.widget(i)
|
|
2256
|
+
if hasattr(tab, 'query_edit'):
|
|
2257
|
+
# Get current font
|
|
2258
|
+
current_font = tab.query_edit.font()
|
|
2259
|
+
current_size = current_font.pointSizeF()
|
|
2260
|
+
|
|
2261
|
+
# Calculate new size with limits to prevent too small/large fonts
|
|
2262
|
+
new_size = current_size * factor
|
|
2263
|
+
if 6 <= new_size <= 72: # Reasonable limits
|
|
2264
|
+
current_font.setPointSizeF(new_size)
|
|
2265
|
+
tab.query_edit.setFont(current_font)
|
|
2266
|
+
|
|
2267
|
+
# Also update the line number area
|
|
2268
|
+
tab.query_edit.update_line_number_area_width(0)
|
|
2269
|
+
|
|
2270
|
+
# Update results table font if needed
|
|
2271
|
+
if hasattr(tab, 'results_table'):
|
|
2272
|
+
table_font = tab.results_table.font()
|
|
2273
|
+
table_size = table_font.pointSizeF()
|
|
2274
|
+
new_table_size = table_size * factor
|
|
2275
|
+
|
|
2276
|
+
if 6 <= new_table_size <= 72:
|
|
2277
|
+
table_font.setPointSizeF(new_table_size)
|
|
2278
|
+
tab.results_table.setFont(table_font)
|
|
2279
|
+
# Resize rows and columns to fit new font size
|
|
2280
|
+
tab.results_table.resizeColumnsToContents()
|
|
2281
|
+
tab.results_table.resizeRowsToContents()
|
|
2282
|
+
|
|
2283
|
+
# Update status bar
|
|
2284
|
+
self.statusBar().showMessage(f"Zoom level adjusted to {int(current_size * factor)}", 2000)
|
|
2285
|
+
|
|
2286
|
+
except Exception as e:
|
|
2287
|
+
self.statusBar().showMessage(f"Error adjusting zoom: {str(e)}", 2000)
|
|
2288
|
+
|
|
2289
|
+
def reset_zoom(self):
|
|
2290
|
+
"""Reset zoom level to default"""
|
|
2291
|
+
try:
|
|
2292
|
+
# Default font sizes
|
|
2293
|
+
sql_editor_size = 12
|
|
2294
|
+
table_size = 10
|
|
2295
|
+
|
|
2296
|
+
# Update all tabs
|
|
2297
|
+
for i in range(self.tab_widget.count()):
|
|
2298
|
+
tab = self.tab_widget.widget(i)
|
|
2299
|
+
|
|
2300
|
+
# Reset editor font
|
|
2301
|
+
if hasattr(tab, 'query_edit'):
|
|
2302
|
+
editor_font = tab.query_edit.font()
|
|
2303
|
+
editor_font.setPointSizeF(sql_editor_size)
|
|
2304
|
+
tab.query_edit.setFont(editor_font)
|
|
2305
|
+
tab.query_edit.update_line_number_area_width(0)
|
|
2306
|
+
|
|
2307
|
+
# Reset table font
|
|
2308
|
+
if hasattr(tab, 'results_table'):
|
|
2309
|
+
table_font = tab.results_table.font()
|
|
2310
|
+
table_font.setPointSizeF(table_size)
|
|
2311
|
+
tab.results_table.setFont(table_font)
|
|
2312
|
+
tab.results_table.resizeColumnsToContents()
|
|
2313
|
+
tab.results_table.resizeRowsToContents()
|
|
2314
|
+
|
|
2315
|
+
self.statusBar().showMessage("Zoom level reset to default", 2000)
|
|
2316
|
+
|
|
2317
|
+
except Exception as e:
|
|
2318
|
+
self.statusBar().showMessage(f"Error resetting zoom: {str(e)}", 2000)
|
|
2319
|
+
|
|
1431
2320
|
def main():
|
|
1432
2321
|
app = QApplication(sys.argv)
|
|
2322
|
+
app.setStyle(QStyleFactory.create('Fusion'))
|
|
1433
2323
|
|
|
1434
|
-
# Set application
|
|
1435
|
-
|
|
2324
|
+
# Set application icon
|
|
2325
|
+
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "icon.png")
|
|
2326
|
+
if os.path.exists(icon_path):
|
|
2327
|
+
app.setWindowIcon(QIcon(icon_path))
|
|
2328
|
+
else:
|
|
2329
|
+
# Fallback to the main logo if the icon isn't found
|
|
2330
|
+
main_logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "sqlshell_logo.png")
|
|
2331
|
+
if os.path.exists(main_logo_path):
|
|
2332
|
+
app.setWindowIcon(QIcon(main_logo_path))
|
|
1436
2333
|
|
|
1437
|
-
#
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
palette.setColor(QPalette.ColorRole.WindowText, QColor("#2C3E50"))
|
|
1441
|
-
palette.setColor(QPalette.ColorRole.Base, QColor("#FFFFFF"))
|
|
1442
|
-
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#F5F5F5"))
|
|
1443
|
-
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#2C3E50"))
|
|
1444
|
-
palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#FFFFFF"))
|
|
1445
|
-
palette.setColor(QPalette.ColorRole.Text, QColor("#2C3E50"))
|
|
1446
|
-
palette.setColor(QPalette.ColorRole.Button, QColor("#3498DB"))
|
|
1447
|
-
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#FFFFFF"))
|
|
1448
|
-
palette.setColor(QPalette.ColorRole.BrightText, QColor("#FFFFFF"))
|
|
1449
|
-
palette.setColor(QPalette.ColorRole.Link, QColor("#3498DB"))
|
|
1450
|
-
palette.setColor(QPalette.ColorRole.Highlight, QColor("#3498DB"))
|
|
1451
|
-
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
|
|
1452
|
-
app.setPalette(palette)
|
|
2334
|
+
# Ensure we have a valid working directory with pool.db
|
|
2335
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
2336
|
+
working_dir = os.getcwd()
|
|
1453
2337
|
|
|
1454
|
-
#
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
2338
|
+
# If pool.db doesn't exist in current directory, copy it from package
|
|
2339
|
+
if not os.path.exists(os.path.join(working_dir, 'pool.db')):
|
|
2340
|
+
import shutil
|
|
2341
|
+
package_db = os.path.join(package_dir, 'pool.db')
|
|
2342
|
+
if os.path.exists(package_db):
|
|
2343
|
+
shutil.copy2(package_db, working_dir)
|
|
2344
|
+
else:
|
|
2345
|
+
package_db = os.path.join(os.path.dirname(package_dir), 'pool.db')
|
|
2346
|
+
if os.path.exists(package_db):
|
|
2347
|
+
shutil.copy2(package_db, working_dir)
|
|
1460
2348
|
|
|
1461
|
-
# Set application icon (if available)
|
|
1462
2349
|
try:
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
2350
|
+
# Show splash screen
|
|
2351
|
+
splash = AnimatedSplashScreen()
|
|
2352
|
+
splash.show()
|
|
2353
|
+
|
|
2354
|
+
# Process events immediately to ensure the splash screen appears
|
|
2355
|
+
app.processEvents()
|
|
2356
|
+
|
|
2357
|
+
# Create main window but don't show it yet
|
|
2358
|
+
print("Initializing main application...")
|
|
2359
|
+
window = SQLShell()
|
|
2360
|
+
|
|
2361
|
+
# Define the function to show main window and hide splash
|
|
2362
|
+
def show_main_window():
|
|
2363
|
+
# Properly finish the splash screen
|
|
2364
|
+
if splash:
|
|
2365
|
+
splash.finish(window)
|
|
2366
|
+
|
|
2367
|
+
# Show the main window
|
|
2368
|
+
window.show()
|
|
2369
|
+
timer.stop()
|
|
2370
|
+
|
|
2371
|
+
# Also stop the failsafe timer if it's still running
|
|
2372
|
+
if failsafe_timer.isActive():
|
|
2373
|
+
failsafe_timer.stop()
|
|
2374
|
+
|
|
2375
|
+
print("Main application started")
|
|
2376
|
+
|
|
2377
|
+
# Create a failsafe timer in case the splash screen fails to show
|
|
2378
|
+
def failsafe_show_window():
|
|
2379
|
+
if not window.isVisible():
|
|
2380
|
+
print("Failsafe timer activated - showing main window")
|
|
2381
|
+
if splash:
|
|
2382
|
+
try:
|
|
2383
|
+
# First try to use the proper finish method
|
|
2384
|
+
splash.finish(window)
|
|
2385
|
+
except Exception as e:
|
|
2386
|
+
print(f"Error in failsafe finish: {e}")
|
|
2387
|
+
try:
|
|
2388
|
+
# Fall back to direct close if finish fails
|
|
2389
|
+
splash.close()
|
|
2390
|
+
except Exception:
|
|
2391
|
+
pass
|
|
2392
|
+
window.show()
|
|
2393
|
+
|
|
2394
|
+
# Create and show main window after delay
|
|
2395
|
+
timer = QTimer()
|
|
2396
|
+
timer.setSingleShot(True) # Ensure it only fires once
|
|
2397
|
+
timer.timeout.connect(show_main_window)
|
|
2398
|
+
timer.start(2000) # 2 second delay
|
|
2399
|
+
|
|
2400
|
+
# Failsafe timer - show the main window after 5 seconds even if splash screen fails
|
|
2401
|
+
failsafe_timer = QTimer()
|
|
2402
|
+
failsafe_timer.setSingleShot(True)
|
|
2403
|
+
failsafe_timer.timeout.connect(failsafe_show_window)
|
|
2404
|
+
failsafe_timer.start(5000) # 5 second delay
|
|
2405
|
+
|
|
2406
|
+
sys.exit(app.exec())
|
|
2407
|
+
|
|
2408
|
+
except Exception as e:
|
|
2409
|
+
print(f"Error during startup: {e}")
|
|
2410
|
+
# If there's any error with the splash screen, just show the main window directly
|
|
2411
|
+
window = SQLShell()
|
|
2412
|
+
window.show()
|
|
2413
|
+
sys.exit(app.exec())
|
|
1471
2414
|
|
|
1472
2415
|
if __name__ == '__main__':
|
|
1473
2416
|
main()
|