sqlshell 0.1.5__py3-none-any.whl → 0.1.6__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 +1 -1
- sqlshell/main.py +1037 -67
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.6.dist-info}/METADATA +1 -1
- sqlshell-0.1.6.dist-info/RECORD +11 -0
- sqlshell-0.1.5.dist-info/RECORD +0 -11
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.6.dist-info}/WHEEL +0 -0
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.6.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.6.dist-info}/top_level.txt +0 -0
sqlshell/main.py
CHANGED
|
@@ -6,120 +6,863 @@ import pandas as pd
|
|
|
6
6
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
7
7
|
QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
|
|
8
8
|
QLabel, QSplitter, QListWidget, QTableWidget,
|
|
9
|
-
QTableWidgetItem, QHeaderView, QMessageBox
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
|
|
10
|
+
QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
|
|
11
|
+
QStyleFactory, QToolBar, QStatusBar)
|
|
12
|
+
from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve
|
|
13
|
+
from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap
|
|
12
14
|
import numpy as np
|
|
13
15
|
from datetime import datetime
|
|
14
16
|
from sqlshell.sqlshell import create_test_data # Import from the correct location
|
|
15
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
|
+
|
|
144
|
+
def sizeHint(self):
|
|
145
|
+
return QSize(self.editor.line_number_area_width(), 0)
|
|
146
|
+
|
|
147
|
+
def paintEvent(self, event):
|
|
148
|
+
self.editor.line_number_area_paint_event(event)
|
|
149
|
+
|
|
150
|
+
class SQLEditor(QPlainTextEdit):
|
|
151
|
+
def __init__(self, parent=None):
|
|
152
|
+
super().__init__(parent)
|
|
153
|
+
self.line_number_area = LineNumberArea(self)
|
|
154
|
+
|
|
155
|
+
# Set monospaced font
|
|
156
|
+
font = QFont("Consolas", 12) # Increased font size for better readability
|
|
157
|
+
font.setFixedPitch(True)
|
|
158
|
+
self.setFont(font)
|
|
159
|
+
|
|
160
|
+
# Connect signals
|
|
161
|
+
self.blockCountChanged.connect(self.update_line_number_area_width)
|
|
162
|
+
self.updateRequest.connect(self.update_line_number_area)
|
|
163
|
+
|
|
164
|
+
# Initialize
|
|
165
|
+
self.update_line_number_area_width(0)
|
|
166
|
+
|
|
167
|
+
# Set tab width to 4 spaces
|
|
168
|
+
self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
|
|
169
|
+
|
|
170
|
+
# Set placeholder text
|
|
171
|
+
self.setPlaceholderText("Enter your SQL query here...")
|
|
172
|
+
|
|
173
|
+
# Initialize completer
|
|
174
|
+
self.completer = None
|
|
175
|
+
|
|
176
|
+
# SQL Keywords for autocomplete
|
|
177
|
+
self.sql_keywords = [
|
|
178
|
+
"SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
|
|
179
|
+
"ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
|
|
180
|
+
"CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
|
|
181
|
+
"TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
|
|
182
|
+
"UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
|
|
183
|
+
"AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
|
|
184
|
+
"AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
# Initialize with SQL keywords
|
|
188
|
+
self.set_completer(QCompleter(self.sql_keywords))
|
|
189
|
+
|
|
190
|
+
# Set modern selection color
|
|
191
|
+
self.selection_color = QColor("#3498DB")
|
|
192
|
+
self.selection_color.setAlpha(50) # Make it semi-transparent
|
|
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()
|
|
245
|
+
|
|
246
|
+
if not prefix or len(prefix) < 2: # Only show completions for words with at least 2 characters
|
|
247
|
+
if self.completer.popup().isVisible():
|
|
248
|
+
self.completer.popup().hide()
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
self.completer.setCompletionPrefix(prefix)
|
|
252
|
+
|
|
253
|
+
# If no completions, hide popup
|
|
254
|
+
if self.completer.completionCount() == 0:
|
|
255
|
+
self.completer.popup().hide()
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# Get popup and position it under the current text
|
|
259
|
+
popup = self.completer.popup()
|
|
260
|
+
popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
|
|
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
|
|
278
|
+
|
|
279
|
+
# Handle special key combinations
|
|
280
|
+
if event.key() == Qt.Key.Key_Tab:
|
|
281
|
+
# Insert 4 spaces instead of a tab character
|
|
282
|
+
self.insertPlainText(" ")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Auto-indentation for new lines
|
|
286
|
+
if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
|
|
287
|
+
cursor = self.textCursor()
|
|
288
|
+
block = cursor.block()
|
|
289
|
+
text = block.text()
|
|
290
|
+
|
|
291
|
+
# Get the indentation of the current line
|
|
292
|
+
indentation = ""
|
|
293
|
+
for char in text:
|
|
294
|
+
if char.isspace():
|
|
295
|
+
indentation += char
|
|
296
|
+
else:
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
# Check if line ends with an opening bracket or keywords that should increase indentation
|
|
300
|
+
increase_indent = ""
|
|
301
|
+
if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
|
|
302
|
+
["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
|
|
303
|
+
increase_indent = " "
|
|
304
|
+
|
|
305
|
+
# Insert new line with proper indentation
|
|
306
|
+
super().keyPressEvent(event)
|
|
307
|
+
self.insertPlainText(indentation + increase_indent)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# Handle keyboard shortcuts
|
|
311
|
+
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
|
312
|
+
if event.key() == Qt.Key.Key_Space:
|
|
313
|
+
# Show completion popup
|
|
314
|
+
self.complete()
|
|
315
|
+
return
|
|
316
|
+
elif event.key() == Qt.Key.Key_K:
|
|
317
|
+
# Comment/uncomment the selected lines
|
|
318
|
+
self.toggle_comment()
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# For normal key presses
|
|
322
|
+
super().keyPressEvent(event)
|
|
323
|
+
|
|
324
|
+
# Check for autocomplete after typing
|
|
325
|
+
if event.text() and not event.text().isspace():
|
|
326
|
+
self.complete()
|
|
327
|
+
|
|
328
|
+
def paintEvent(self, event):
|
|
329
|
+
# Call the parent's paintEvent first
|
|
330
|
+
super().paintEvent(event)
|
|
331
|
+
|
|
332
|
+
# Get the current cursor
|
|
333
|
+
cursor = self.textCursor()
|
|
334
|
+
|
|
335
|
+
# If there's a selection, paint custom highlight
|
|
336
|
+
if cursor.hasSelection():
|
|
337
|
+
# Create a painter for this widget
|
|
338
|
+
painter = QPainter(self.viewport())
|
|
339
|
+
|
|
340
|
+
# Get the selection start and end positions
|
|
341
|
+
start = cursor.selectionStart()
|
|
342
|
+
end = cursor.selectionEnd()
|
|
343
|
+
|
|
344
|
+
# Create temporary cursor to get the rectangles
|
|
345
|
+
temp_cursor = QTextCursor(cursor)
|
|
346
|
+
|
|
347
|
+
# Move to start and get the starting position
|
|
348
|
+
temp_cursor.setPosition(start)
|
|
349
|
+
start_pos = self.cursorRect(temp_cursor)
|
|
350
|
+
|
|
351
|
+
# Move to end and get the ending position
|
|
352
|
+
temp_cursor.setPosition(end)
|
|
353
|
+
end_pos = self.cursorRect(temp_cursor)
|
|
354
|
+
|
|
355
|
+
# Set the highlight color with transparency
|
|
356
|
+
painter.setBrush(QBrush(self.selection_color))
|
|
357
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
358
|
+
|
|
359
|
+
# Draw the highlight rectangle
|
|
360
|
+
if start_pos.top() == end_pos.top():
|
|
361
|
+
# Single line selection
|
|
362
|
+
painter.drawRect(QRect(start_pos.left(), start_pos.top(),
|
|
363
|
+
end_pos.right() - start_pos.left(), start_pos.height()))
|
|
364
|
+
else:
|
|
365
|
+
# Multi-line selection
|
|
366
|
+
# First line
|
|
367
|
+
painter.drawRect(QRect(start_pos.left(), start_pos.top(),
|
|
368
|
+
self.viewport().width() - start_pos.left(), start_pos.height()))
|
|
369
|
+
|
|
370
|
+
# Middle lines (if any)
|
|
371
|
+
if end_pos.top() > start_pos.top() + start_pos.height():
|
|
372
|
+
painter.drawRect(QRect(0, start_pos.top() + start_pos.height(),
|
|
373
|
+
self.viewport().width(),
|
|
374
|
+
end_pos.top() - (start_pos.top() + start_pos.height())))
|
|
375
|
+
|
|
376
|
+
# Last line
|
|
377
|
+
painter.drawRect(QRect(0, end_pos.top(), end_pos.right(), end_pos.height()))
|
|
378
|
+
|
|
379
|
+
painter.end()
|
|
380
|
+
|
|
381
|
+
def focusInEvent(self, event):
|
|
382
|
+
super().focusInEvent(event)
|
|
383
|
+
# Show temporary hint in status bar when editor gets focus
|
|
384
|
+
if hasattr(self.parent(), 'statusBar'):
|
|
385
|
+
self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
|
|
386
|
+
|
|
387
|
+
def toggle_comment(self):
|
|
388
|
+
cursor = self.textCursor()
|
|
389
|
+
if cursor.hasSelection():
|
|
390
|
+
# Get the selected text
|
|
391
|
+
start = cursor.selectionStart()
|
|
392
|
+
end = cursor.selectionEnd()
|
|
393
|
+
|
|
394
|
+
# Remember the selection
|
|
395
|
+
cursor.setPosition(start)
|
|
396
|
+
start_block = cursor.blockNumber()
|
|
397
|
+
cursor.setPosition(end)
|
|
398
|
+
end_block = cursor.blockNumber()
|
|
399
|
+
|
|
400
|
+
# Process each line in the selection
|
|
401
|
+
cursor.setPosition(start)
|
|
402
|
+
cursor.beginEditBlock()
|
|
403
|
+
|
|
404
|
+
for _ in range(start_block, end_block + 1):
|
|
405
|
+
# Move to start of line
|
|
406
|
+
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
407
|
+
|
|
408
|
+
# Check if the line is already commented
|
|
409
|
+
line_text = cursor.block().text().lstrip()
|
|
410
|
+
if line_text.startswith('--'):
|
|
411
|
+
# Remove comment
|
|
412
|
+
pos = cursor.block().text().find('--')
|
|
413
|
+
cursor.setPosition(cursor.block().position() + pos)
|
|
414
|
+
cursor.deleteChar()
|
|
415
|
+
cursor.deleteChar()
|
|
416
|
+
else:
|
|
417
|
+
# Add comment
|
|
418
|
+
cursor.insertText('--')
|
|
419
|
+
|
|
420
|
+
# Move to next line if not at the end
|
|
421
|
+
if not cursor.atEnd():
|
|
422
|
+
cursor.movePosition(cursor.MoveOperation.NextBlock)
|
|
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
|
|
490
|
+
|
|
16
491
|
class SQLShell(QMainWindow):
|
|
17
492
|
def __init__(self):
|
|
18
493
|
super().__init__()
|
|
19
494
|
self.current_db_type = 'duckdb' # Default to DuckDB
|
|
20
495
|
self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
|
|
21
496
|
self.loaded_tables = {} # Keep track of loaded tables
|
|
497
|
+
self.table_columns = {} # Keep track of table columns
|
|
498
|
+
|
|
499
|
+
# Define color scheme
|
|
500
|
+
self.colors = {
|
|
501
|
+
'primary': "#2C3E50", # Dark blue-gray
|
|
502
|
+
'secondary': "#3498DB", # Bright blue
|
|
503
|
+
'accent': "#1ABC9C", # Teal
|
|
504
|
+
'background': "#ECF0F1", # Light gray
|
|
505
|
+
'text': "#2C3E50", # Dark blue-gray
|
|
506
|
+
'text_light': "#7F8C8D", # Medium gray
|
|
507
|
+
'success': "#2ECC71", # Green
|
|
508
|
+
'warning': "#F39C12", # Orange
|
|
509
|
+
'error': "#E74C3C", # Red
|
|
510
|
+
'dark_bg': "#34495E", # Darker blue-gray
|
|
511
|
+
'light_bg': "#F5F5F5", # Very light gray
|
|
512
|
+
'border': "#BDC3C7" # Light gray border
|
|
513
|
+
}
|
|
514
|
+
|
|
22
515
|
self.init_ui()
|
|
516
|
+
self.apply_stylesheet()
|
|
517
|
+
|
|
518
|
+
def apply_stylesheet(self):
|
|
519
|
+
"""Apply custom stylesheet to the application"""
|
|
520
|
+
self.setStyleSheet(f"""
|
|
521
|
+
QMainWindow {{
|
|
522
|
+
background-color: {self.colors['background']};
|
|
523
|
+
}}
|
|
524
|
+
|
|
525
|
+
QWidget {{
|
|
526
|
+
color: {self.colors['text']};
|
|
527
|
+
font-family: 'Segoe UI', 'Arial', sans-serif;
|
|
528
|
+
}}
|
|
529
|
+
|
|
530
|
+
QLabel {{
|
|
531
|
+
font-size: 13px;
|
|
532
|
+
padding: 2px;
|
|
533
|
+
}}
|
|
534
|
+
|
|
535
|
+
QLabel#header_label {{
|
|
536
|
+
font-size: 16px;
|
|
537
|
+
font-weight: bold;
|
|
538
|
+
color: {self.colors['primary']};
|
|
539
|
+
padding: 8px 0;
|
|
540
|
+
}}
|
|
541
|
+
|
|
542
|
+
QPushButton {{
|
|
543
|
+
background-color: {self.colors['secondary']};
|
|
544
|
+
color: white;
|
|
545
|
+
border: none;
|
|
546
|
+
border-radius: 4px;
|
|
547
|
+
padding: 8px 16px;
|
|
548
|
+
font-weight: bold;
|
|
549
|
+
font-size: 13px;
|
|
550
|
+
min-height: 30px;
|
|
551
|
+
}}
|
|
552
|
+
|
|
553
|
+
QPushButton:hover {{
|
|
554
|
+
background-color: #2980B9;
|
|
555
|
+
}}
|
|
556
|
+
|
|
557
|
+
QPushButton:pressed {{
|
|
558
|
+
background-color: #1F618D;
|
|
559
|
+
}}
|
|
560
|
+
|
|
561
|
+
QPushButton#primary_button {{
|
|
562
|
+
background-color: {self.colors['accent']};
|
|
563
|
+
}}
|
|
564
|
+
|
|
565
|
+
QPushButton#primary_button:hover {{
|
|
566
|
+
background-color: #16A085;
|
|
567
|
+
}}
|
|
568
|
+
|
|
569
|
+
QPushButton#primary_button:pressed {{
|
|
570
|
+
background-color: #0E6655;
|
|
571
|
+
}}
|
|
572
|
+
|
|
573
|
+
QPushButton#danger_button {{
|
|
574
|
+
background-color: {self.colors['error']};
|
|
575
|
+
}}
|
|
576
|
+
|
|
577
|
+
QPushButton#danger_button:hover {{
|
|
578
|
+
background-color: #CB4335;
|
|
579
|
+
}}
|
|
580
|
+
|
|
581
|
+
QToolButton {{
|
|
582
|
+
background-color: transparent;
|
|
583
|
+
border: none;
|
|
584
|
+
border-radius: 4px;
|
|
585
|
+
padding: 4px;
|
|
586
|
+
}}
|
|
587
|
+
|
|
588
|
+
QToolButton:hover {{
|
|
589
|
+
background-color: rgba(52, 152, 219, 0.2);
|
|
590
|
+
}}
|
|
591
|
+
|
|
592
|
+
QFrame#sidebar {{
|
|
593
|
+
background-color: {self.colors['primary']};
|
|
594
|
+
border-radius: 0px;
|
|
595
|
+
}}
|
|
596
|
+
|
|
597
|
+
QFrame#content_panel {{
|
|
598
|
+
background-color: white;
|
|
599
|
+
border-radius: 8px;
|
|
600
|
+
border: 1px solid {self.colors['border']};
|
|
601
|
+
}}
|
|
602
|
+
|
|
603
|
+
QListWidget {{
|
|
604
|
+
background-color: white;
|
|
605
|
+
border-radius: 4px;
|
|
606
|
+
border: 1px solid {self.colors['border']};
|
|
607
|
+
padding: 4px;
|
|
608
|
+
outline: none;
|
|
609
|
+
}}
|
|
610
|
+
|
|
611
|
+
QListWidget::item {{
|
|
612
|
+
padding: 8px;
|
|
613
|
+
border-radius: 4px;
|
|
614
|
+
}}
|
|
615
|
+
|
|
616
|
+
QListWidget::item:selected {{
|
|
617
|
+
background-color: {self.colors['secondary']};
|
|
618
|
+
color: white;
|
|
619
|
+
}}
|
|
620
|
+
|
|
621
|
+
QListWidget::item:hover:!selected {{
|
|
622
|
+
background-color: #E3F2FD;
|
|
623
|
+
}}
|
|
624
|
+
|
|
625
|
+
QTableWidget {{
|
|
626
|
+
background-color: white;
|
|
627
|
+
alternate-background-color: #F8F9FA;
|
|
628
|
+
border-radius: 4px;
|
|
629
|
+
border: 1px solid {self.colors['border']};
|
|
630
|
+
gridline-color: #E0E0E0;
|
|
631
|
+
outline: none;
|
|
632
|
+
}}
|
|
633
|
+
|
|
634
|
+
QTableWidget::item {{
|
|
635
|
+
padding: 4px;
|
|
636
|
+
}}
|
|
637
|
+
|
|
638
|
+
QTableWidget::item:selected {{
|
|
639
|
+
background-color: rgba(52, 152, 219, 0.2);
|
|
640
|
+
color: {self.colors['text']};
|
|
641
|
+
}}
|
|
642
|
+
|
|
643
|
+
QHeaderView::section {{
|
|
644
|
+
background-color: {self.colors['primary']};
|
|
645
|
+
color: white;
|
|
646
|
+
padding: 8px;
|
|
647
|
+
border: none;
|
|
648
|
+
font-weight: bold;
|
|
649
|
+
}}
|
|
650
|
+
|
|
651
|
+
QSplitter::handle {{
|
|
652
|
+
background-color: {self.colors['border']};
|
|
653
|
+
}}
|
|
654
|
+
|
|
655
|
+
QStatusBar {{
|
|
656
|
+
background-color: {self.colors['primary']};
|
|
657
|
+
color: white;
|
|
658
|
+
padding: 8px;
|
|
659
|
+
}}
|
|
660
|
+
|
|
661
|
+
QPlainTextEdit, QTextEdit {{
|
|
662
|
+
background-color: white;
|
|
663
|
+
border-radius: 4px;
|
|
664
|
+
border: 1px solid {self.colors['border']};
|
|
665
|
+
padding: 8px;
|
|
666
|
+
selection-background-color: #BBDEFB;
|
|
667
|
+
selection-color: {self.colors['text']};
|
|
668
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
669
|
+
font-size: 14px;
|
|
670
|
+
}}
|
|
671
|
+
""")
|
|
23
672
|
|
|
24
673
|
def init_ui(self):
|
|
25
674
|
self.setWindowTitle('SQL Shell')
|
|
26
675
|
self.setGeometry(100, 100, 1400, 800)
|
|
27
|
-
|
|
676
|
+
|
|
677
|
+
# Create custom status bar
|
|
678
|
+
status_bar = QStatusBar()
|
|
679
|
+
self.setStatusBar(status_bar)
|
|
680
|
+
|
|
28
681
|
# Create central widget and layout
|
|
29
682
|
central_widget = QWidget()
|
|
30
683
|
self.setCentralWidget(central_widget)
|
|
31
684
|
main_layout = QHBoxLayout(central_widget)
|
|
685
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
686
|
+
main_layout.setSpacing(0)
|
|
32
687
|
|
|
33
688
|
# Left panel for table list
|
|
34
|
-
left_panel =
|
|
689
|
+
left_panel = QFrame()
|
|
690
|
+
left_panel.setObjectName("sidebar")
|
|
691
|
+
left_panel.setMinimumWidth(300)
|
|
692
|
+
left_panel.setMaximumWidth(400)
|
|
35
693
|
left_layout = QVBoxLayout(left_panel)
|
|
694
|
+
left_layout.setContentsMargins(16, 16, 16, 16)
|
|
695
|
+
left_layout.setSpacing(12)
|
|
696
|
+
|
|
697
|
+
# Database info section
|
|
698
|
+
db_header = QLabel("DATABASE")
|
|
699
|
+
db_header.setObjectName("header_label")
|
|
700
|
+
db_header.setStyleSheet("color: white;")
|
|
701
|
+
left_layout.addWidget(db_header)
|
|
36
702
|
|
|
37
|
-
# Database info label
|
|
38
703
|
self.db_info_label = QLabel("No database connected")
|
|
704
|
+
self.db_info_label.setStyleSheet("color: white; background-color: rgba(255, 255, 255, 0.1); padding: 8px; border-radius: 4px;")
|
|
39
705
|
left_layout.addWidget(self.db_info_label)
|
|
40
706
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.tables_list = QListWidget()
|
|
45
|
-
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
46
|
-
left_layout.addWidget(self.tables_list)
|
|
707
|
+
# Database action buttons
|
|
708
|
+
db_buttons_layout = QHBoxLayout()
|
|
709
|
+
db_buttons_layout.setSpacing(8)
|
|
47
710
|
|
|
48
|
-
# Buttons for table management
|
|
49
|
-
table_buttons_layout = QHBoxLayout()
|
|
50
711
|
self.open_db_btn = QPushButton('Open Database')
|
|
712
|
+
self.open_db_btn.setIcon(QIcon.fromTheme("document-open"))
|
|
51
713
|
self.open_db_btn.clicked.connect(self.open_database)
|
|
714
|
+
|
|
715
|
+
self.test_btn = QPushButton('Load Test Data')
|
|
716
|
+
self.test_btn.clicked.connect(self.load_test_data)
|
|
717
|
+
|
|
718
|
+
db_buttons_layout.addWidget(self.open_db_btn)
|
|
719
|
+
db_buttons_layout.addWidget(self.test_btn)
|
|
720
|
+
left_layout.addLayout(db_buttons_layout)
|
|
721
|
+
|
|
722
|
+
# Tables section
|
|
723
|
+
tables_header = QLabel("TABLES")
|
|
724
|
+
tables_header.setObjectName("header_label")
|
|
725
|
+
tables_header.setStyleSheet("color: white; margin-top: 16px;")
|
|
726
|
+
left_layout.addWidget(tables_header)
|
|
727
|
+
|
|
728
|
+
# Table actions
|
|
729
|
+
table_actions_layout = QHBoxLayout()
|
|
730
|
+
table_actions_layout.setSpacing(8)
|
|
731
|
+
|
|
52
732
|
self.browse_btn = QPushButton('Load Files')
|
|
733
|
+
self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
|
|
53
734
|
self.browse_btn.clicked.connect(self.browse_files)
|
|
54
|
-
|
|
735
|
+
|
|
736
|
+
self.remove_table_btn = QPushButton('Remove')
|
|
737
|
+
self.remove_table_btn.setObjectName("danger_button")
|
|
738
|
+
self.remove_table_btn.setIcon(QIcon.fromTheme("edit-delete"))
|
|
55
739
|
self.remove_table_btn.clicked.connect(self.remove_selected_table)
|
|
56
|
-
self.test_btn = QPushButton('Test')
|
|
57
|
-
self.test_btn.clicked.connect(self.load_test_data)
|
|
58
740
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
741
|
+
table_actions_layout.addWidget(self.browse_btn)
|
|
742
|
+
table_actions_layout.addWidget(self.remove_table_btn)
|
|
743
|
+
left_layout.addLayout(table_actions_layout)
|
|
744
|
+
|
|
745
|
+
# Tables list with custom styling
|
|
746
|
+
self.tables_list = QListWidget()
|
|
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
|
+
""")
|
|
761
|
+
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
762
|
+
left_layout.addWidget(self.tables_list)
|
|
763
|
+
|
|
764
|
+
# Add spacer at the bottom
|
|
765
|
+
left_layout.addStretch()
|
|
766
|
+
|
|
65
767
|
# Right panel for query and results
|
|
66
|
-
right_panel =
|
|
768
|
+
right_panel = QFrame()
|
|
769
|
+
right_panel.setObjectName("content_panel")
|
|
67
770
|
right_layout = QVBoxLayout(right_panel)
|
|
771
|
+
right_layout.setContentsMargins(16, 16, 16, 16)
|
|
772
|
+
right_layout.setSpacing(16)
|
|
773
|
+
|
|
774
|
+
# Query section header
|
|
775
|
+
query_header = QLabel("SQL QUERY")
|
|
776
|
+
query_header.setObjectName("header_label")
|
|
777
|
+
right_layout.addWidget(query_header)
|
|
68
778
|
|
|
69
779
|
# Create splitter for query and results
|
|
70
780
|
splitter = QSplitter(Qt.Orientation.Vertical)
|
|
781
|
+
splitter.setHandleWidth(8)
|
|
782
|
+
splitter.setChildrenCollapsible(False)
|
|
71
783
|
|
|
72
784
|
# Top part - Query section
|
|
73
|
-
query_widget =
|
|
785
|
+
query_widget = QFrame()
|
|
786
|
+
query_widget.setObjectName("content_panel")
|
|
74
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)
|
|
75
796
|
|
|
76
797
|
# Button row
|
|
77
798
|
button_layout = QHBoxLayout()
|
|
78
|
-
|
|
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"))
|
|
79
804
|
self.execute_btn.clicked.connect(self.execute_query)
|
|
805
|
+
self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
|
|
806
|
+
|
|
80
807
|
self.clear_btn = QPushButton('Clear')
|
|
808
|
+
self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
|
|
81
809
|
self.clear_btn.clicked.connect(self.clear_query)
|
|
82
810
|
|
|
83
|
-
# Add export buttons
|
|
84
|
-
self.export_excel_btn = QPushButton('Export to Excel')
|
|
85
|
-
self.export_excel_btn.clicked.connect(self.export_to_excel)
|
|
86
|
-
self.export_parquet_btn = QPushButton('Export to Parquet')
|
|
87
|
-
self.export_parquet_btn.clicked.connect(self.export_to_parquet)
|
|
88
|
-
|
|
89
811
|
button_layout.addWidget(self.execute_btn)
|
|
90
812
|
button_layout.addWidget(self.clear_btn)
|
|
91
|
-
button_layout.addWidget(self.export_excel_btn)
|
|
92
|
-
button_layout.addWidget(self.export_parquet_btn)
|
|
93
813
|
button_layout.addStretch()
|
|
94
814
|
|
|
95
815
|
query_layout.addLayout(button_layout)
|
|
96
|
-
|
|
97
|
-
# Query input
|
|
98
|
-
self.query_edit = QTextEdit()
|
|
99
|
-
self.query_edit.setPlaceholderText("Enter your SQL query here...")
|
|
100
|
-
query_layout.addWidget(self.query_edit)
|
|
101
|
-
|
|
816
|
+
|
|
102
817
|
# Bottom part - Results section
|
|
103
|
-
results_widget =
|
|
818
|
+
results_widget = QFrame()
|
|
819
|
+
results_widget.setObjectName("content_panel")
|
|
104
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")
|
|
105
829
|
|
|
106
|
-
# Results header with row count
|
|
107
|
-
results_header = QWidget()
|
|
108
|
-
results_header_layout = QHBoxLayout(results_header)
|
|
109
|
-
self.results_label = QLabel("Results:")
|
|
110
830
|
self.row_count_label = QLabel("")
|
|
111
|
-
|
|
831
|
+
self.row_count_label.setStyleSheet(f"color: {self.colors['text_light']}; font-style: italic;")
|
|
832
|
+
|
|
833
|
+
results_header_layout.addWidget(results_title)
|
|
112
834
|
results_header_layout.addWidget(self.row_count_label)
|
|
113
835
|
results_header_layout.addStretch()
|
|
114
|
-
results_layout.addWidget(results_header)
|
|
115
836
|
|
|
116
|
-
#
|
|
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
|
|
117
856
|
self.results_table = QTableWidget()
|
|
118
857
|
self.results_table.setSortingEnabled(True)
|
|
119
858
|
self.results_table.setAlternatingRowColors(True)
|
|
120
859
|
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
121
860
|
self.results_table.horizontalHeader().setSectionsMovable(True)
|
|
122
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
|
+
|
|
123
866
|
results_layout.addWidget(self.results_table)
|
|
124
867
|
|
|
125
868
|
# Add widgets to splitter
|
|
@@ -136,7 +879,16 @@ class SQLShell(QMainWindow):
|
|
|
136
879
|
main_layout.addWidget(right_panel, 4)
|
|
137
880
|
|
|
138
881
|
# Status bar
|
|
139
|
-
self.statusBar().showMessage('Ready')
|
|
882
|
+
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment')
|
|
883
|
+
|
|
884
|
+
# Show keyboard shortcuts in a tooltip for the query editor
|
|
885
|
+
self.query_edit.setToolTip(
|
|
886
|
+
"Keyboard Shortcuts:\n"
|
|
887
|
+
"Ctrl+Enter: Execute Query\n"
|
|
888
|
+
"Ctrl+K: Toggle Comment\n"
|
|
889
|
+
"Tab: Insert 4 spaces\n"
|
|
890
|
+
"Ctrl+Space: Show autocomplete"
|
|
891
|
+
)
|
|
140
892
|
|
|
141
893
|
def format_value(self, value):
|
|
142
894
|
"""Format values for display"""
|
|
@@ -180,17 +932,35 @@ class SQLShell(QMainWindow):
|
|
|
180
932
|
# Make cells read-only
|
|
181
933
|
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
182
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
|
+
|
|
183
940
|
self.results_table.setItem(i, j, item)
|
|
184
941
|
|
|
185
942
|
# Auto-adjust column widths while ensuring minimum and maximum sizes
|
|
186
943
|
self.results_table.resizeColumnsToContents()
|
|
187
944
|
for i in range(len(df.columns)):
|
|
188
945
|
width = self.results_table.columnWidth(i)
|
|
189
|
-
self.results_table.setColumnWidth(i, min(max(width,
|
|
946
|
+
self.results_table.setColumnWidth(i, min(max(width, 80), 300))
|
|
190
947
|
|
|
191
948
|
# Update row count
|
|
192
949
|
row_text = "row" if len(df) == 1 else "rows"
|
|
193
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"))
|
|
194
964
|
|
|
195
965
|
def browse_files(self):
|
|
196
966
|
if not self.conn:
|
|
@@ -238,6 +1008,9 @@ class SQLShell(QMainWindow):
|
|
|
238
1008
|
|
|
239
1009
|
self.loaded_tables[table_name] = file_name
|
|
240
1010
|
|
|
1011
|
+
# Store column names
|
|
1012
|
+
self.table_columns[table_name] = df.columns.tolist()
|
|
1013
|
+
|
|
241
1014
|
# Update UI
|
|
242
1015
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
243
1016
|
self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
|
|
@@ -245,14 +1018,22 @@ class SQLShell(QMainWindow):
|
|
|
245
1018
|
# Show preview of loaded data
|
|
246
1019
|
preview_df = df.head()
|
|
247
1020
|
self.populate_table(preview_df)
|
|
248
|
-
|
|
1021
|
+
|
|
1022
|
+
# Update results title to show preview
|
|
1023
|
+
results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
|
|
1024
|
+
if results_title and results_title.text() == "RESULTS":
|
|
1025
|
+
results_title.setText(f"PREVIEW: {table_name}")
|
|
1026
|
+
|
|
1027
|
+
# Update completer with new table and column names
|
|
1028
|
+
self.update_completer()
|
|
249
1029
|
|
|
250
1030
|
except Exception as e:
|
|
251
|
-
|
|
1031
|
+
error_msg = f'Error loading file {os.path.basename(file_name)}: {str(e)}'
|
|
1032
|
+
self.statusBar().showMessage(error_msg)
|
|
1033
|
+
QMessageBox.critical(self, "Error", error_msg)
|
|
252
1034
|
self.results_table.setRowCount(0)
|
|
253
1035
|
self.results_table.setColumnCount(0)
|
|
254
1036
|
self.row_count_label.setText("")
|
|
255
|
-
self.results_label.setText(f"Error loading file: {str(e)}")
|
|
256
1037
|
|
|
257
1038
|
def sanitize_table_name(self, name):
|
|
258
1039
|
# Replace invalid characters with underscores
|
|
@@ -272,13 +1053,17 @@ class SQLShell(QMainWindow):
|
|
|
272
1053
|
self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
|
|
273
1054
|
# Remove from our tracking
|
|
274
1055
|
del self.loaded_tables[table_name]
|
|
1056
|
+
if table_name in self.table_columns:
|
|
1057
|
+
del self.table_columns[table_name]
|
|
275
1058
|
# Remove from list widget
|
|
276
1059
|
self.tables_list.takeItem(self.tables_list.row(current_item))
|
|
277
1060
|
self.statusBar().showMessage(f'Removed table "{table_name}"')
|
|
278
1061
|
self.results_table.setRowCount(0)
|
|
279
1062
|
self.results_table.setColumnCount(0)
|
|
280
1063
|
self.row_count_label.setText("")
|
|
281
|
-
|
|
1064
|
+
|
|
1065
|
+
# Update completer
|
|
1066
|
+
self.update_completer()
|
|
282
1067
|
|
|
283
1068
|
def open_database(self):
|
|
284
1069
|
"""Open a database file (DuckDB or SQLite)"""
|
|
@@ -343,21 +1128,67 @@ class SQLShell(QMainWindow):
|
|
|
343
1128
|
for (table_name,) in tables:
|
|
344
1129
|
self.loaded_tables[table_name] = 'database'
|
|
345
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] = []
|
|
346
1139
|
else: # duckdb
|
|
347
1140
|
query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
348
1141
|
result = self.conn.execute(query).fetchdf()
|
|
349
1142
|
for table_name in result['table_name']:
|
|
350
1143
|
self.loaded_tables[table_name] = 'database'
|
|
351
1144
|
self.tables_list.addItem(f"{table_name} (database)")
|
|
1145
|
+
|
|
1146
|
+
# Get column names for each table
|
|
1147
|
+
try:
|
|
1148
|
+
column_query = f"SELECT column_name FROM information_schema.columns WHERE table_name='{table_name}' AND table_schema='main'"
|
|
1149
|
+
columns = self.conn.execute(column_query).fetchdf()
|
|
1150
|
+
self.table_columns[table_name] = columns['column_name'].tolist()
|
|
1151
|
+
except Exception:
|
|
1152
|
+
self.table_columns[table_name] = []
|
|
1153
|
+
|
|
1154
|
+
# Update the completer with table and column names
|
|
1155
|
+
self.update_completer()
|
|
352
1156
|
except Exception as e:
|
|
353
1157
|
self.statusBar().showMessage(f'Error loading tables: {str(e)}')
|
|
354
1158
|
|
|
1159
|
+
def update_completer(self):
|
|
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])
|
|
1168
|
+
|
|
1169
|
+
# Update the completer in the query editor
|
|
1170
|
+
self.query_edit.update_completer_model(completion_words)
|
|
1171
|
+
|
|
355
1172
|
def execute_query(self):
|
|
356
1173
|
query = self.query_edit.toPlainText().strip()
|
|
357
1174
|
if not query:
|
|
358
1175
|
return
|
|
359
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
|
+
|
|
360
1186
|
try:
|
|
1187
|
+
# Reset results title
|
|
1188
|
+
results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
|
|
1189
|
+
if results_title:
|
|
1190
|
+
results_title.setText("RESULTS")
|
|
1191
|
+
|
|
361
1192
|
if self.current_db_type == 'sqlite':
|
|
362
1193
|
# Execute SQLite query and convert to DataFrame
|
|
363
1194
|
result = pd.read_sql_query(query, self.conn)
|
|
@@ -366,17 +1197,46 @@ class SQLShell(QMainWindow):
|
|
|
366
1197
|
result = self.conn.execute(query).fetchdf()
|
|
367
1198
|
|
|
368
1199
|
self.populate_table(result)
|
|
369
|
-
|
|
370
|
-
|
|
1200
|
+
|
|
1201
|
+
# Show success message with query stats
|
|
1202
|
+
execution_time = datetime.now().strftime("%H:%M:%S")
|
|
1203
|
+
row_count = len(result)
|
|
1204
|
+
self.statusBar().showMessage(f'Query executed successfully at {execution_time} - {row_count:,} rows returned')
|
|
1205
|
+
|
|
371
1206
|
except Exception as e:
|
|
372
1207
|
self.results_table.setRowCount(0)
|
|
373
1208
|
self.results_table.setColumnCount(0)
|
|
374
|
-
self.row_count_label.setText("")
|
|
375
|
-
self.results_label.setText(f"Error executing query: {str(e)}")
|
|
1209
|
+
self.row_count_label.setText("Error")
|
|
376
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()
|
|
377
1227
|
|
|
378
1228
|
def clear_query(self):
|
|
1229
|
+
"""Clear the query editor with animation"""
|
|
1230
|
+
# Save current text for animation
|
|
1231
|
+
current_text = self.query_edit.toPlainText()
|
|
1232
|
+
if not current_text:
|
|
1233
|
+
return
|
|
1234
|
+
|
|
1235
|
+
# Clear the editor
|
|
379
1236
|
self.query_edit.clear()
|
|
1237
|
+
|
|
1238
|
+
# Show success message
|
|
1239
|
+
self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
|
|
380
1240
|
|
|
381
1241
|
def show_table_preview(self, item):
|
|
382
1242
|
"""Show a preview of the selected table"""
|
|
@@ -389,24 +1249,33 @@ class SQLShell(QMainWindow):
|
|
|
389
1249
|
preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
|
|
390
1250
|
|
|
391
1251
|
self.populate_table(preview_df)
|
|
392
|
-
self.results_label.setText(f"Preview of {table_name}:")
|
|
393
1252
|
self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
|
|
1253
|
+
|
|
1254
|
+
# Update the results title to show which table is being previewed
|
|
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}")
|
|
1258
|
+
|
|
394
1259
|
except Exception as e:
|
|
395
1260
|
self.results_table.setRowCount(0)
|
|
396
1261
|
self.results_table.setColumnCount(0)
|
|
397
1262
|
self.row_count_label.setText("")
|
|
398
|
-
self.results_label.setText(f"Error showing preview: {str(e)}")
|
|
399
1263
|
self.statusBar().showMessage('Error showing table preview')
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1264
|
+
|
|
1265
|
+
# Show error message with modern styling
|
|
1266
|
+
QMessageBox.critical(
|
|
1267
|
+
self,
|
|
1268
|
+
"Error",
|
|
1269
|
+
f"Error showing preview: {str(e)}",
|
|
1270
|
+
QMessageBox.StandardButton.Ok
|
|
1271
|
+
)
|
|
406
1272
|
|
|
407
1273
|
def load_test_data(self):
|
|
408
1274
|
"""Generate and load test data"""
|
|
409
1275
|
try:
|
|
1276
|
+
# Show loading indicator
|
|
1277
|
+
self.statusBar().showMessage('Generating test data...')
|
|
1278
|
+
|
|
410
1279
|
# Create test data directory if it doesn't exist
|
|
411
1280
|
os.makedirs('test_data', exist_ok=True)
|
|
412
1281
|
|
|
@@ -430,18 +1299,48 @@ class SQLShell(QMainWindow):
|
|
|
430
1299
|
self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
|
|
431
1300
|
self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
|
|
432
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()
|
|
1306
|
+
|
|
433
1307
|
# Update UI
|
|
434
1308
|
self.tables_list.clear()
|
|
435
1309
|
for table_name, file_path in self.loaded_tables.items():
|
|
436
1310
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
437
1311
|
|
|
438
1312
|
# Set the sample query
|
|
439
|
-
|
|
1313
|
+
sample_query = """
|
|
1314
|
+
SELECT
|
|
1315
|
+
s.orderid,
|
|
1316
|
+
s.orderdate,
|
|
1317
|
+
c.customername,
|
|
1318
|
+
p.productname,
|
|
1319
|
+
s.quantity,
|
|
1320
|
+
s.unitprice,
|
|
1321
|
+
(s.quantity * s.unitprice) AS total_amount
|
|
1322
|
+
FROM
|
|
1323
|
+
sample_sales_data s
|
|
1324
|
+
INNER JOIN customer_data c ON c.customerid = s.customerid
|
|
1325
|
+
INNER JOIN product_catalog p ON p.productid = s.productid
|
|
1326
|
+
ORDER BY
|
|
1327
|
+
s.orderdate DESC
|
|
1328
|
+
LIMIT 10
|
|
1329
|
+
"""
|
|
1330
|
+
self.query_edit.setPlainText(sample_query.strip())
|
|
1331
|
+
|
|
1332
|
+
# Update completer
|
|
1333
|
+
self.update_completer()
|
|
440
1334
|
|
|
1335
|
+
# Show success message
|
|
441
1336
|
self.statusBar().showMessage('Test data loaded successfully')
|
|
442
1337
|
|
|
1338
|
+
# Show a preview of the sales data
|
|
1339
|
+
self.show_table_preview(self.tables_list.item(0))
|
|
1340
|
+
|
|
443
1341
|
except Exception as e:
|
|
444
1342
|
self.statusBar().showMessage(f'Error loading test data: {str(e)}')
|
|
1343
|
+
QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
|
|
445
1344
|
|
|
446
1345
|
def export_to_excel(self):
|
|
447
1346
|
if self.results_table.rowCount() == 0:
|
|
@@ -453,12 +1352,25 @@ class SQLShell(QMainWindow):
|
|
|
453
1352
|
return
|
|
454
1353
|
|
|
455
1354
|
try:
|
|
1355
|
+
# Show loading indicator
|
|
1356
|
+
self.statusBar().showMessage('Exporting data to Excel...')
|
|
1357
|
+
|
|
456
1358
|
# Convert table data to DataFrame
|
|
457
1359
|
df = self.get_table_data_as_dataframe()
|
|
458
1360
|
df.to_excel(file_name, index=False)
|
|
1361
|
+
|
|
459
1362
|
self.statusBar().showMessage(f'Data exported to {file_name}')
|
|
1363
|
+
|
|
1364
|
+
# Show success message
|
|
1365
|
+
QMessageBox.information(
|
|
1366
|
+
self,
|
|
1367
|
+
"Export Successful",
|
|
1368
|
+
f"Data has been exported to:\n{file_name}",
|
|
1369
|
+
QMessageBox.StandardButton.Ok
|
|
1370
|
+
)
|
|
460
1371
|
except Exception as e:
|
|
461
1372
|
QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
|
|
1373
|
+
self.statusBar().showMessage('Error exporting data')
|
|
462
1374
|
|
|
463
1375
|
def export_to_parquet(self):
|
|
464
1376
|
if self.results_table.rowCount() == 0:
|
|
@@ -470,12 +1382,25 @@ class SQLShell(QMainWindow):
|
|
|
470
1382
|
return
|
|
471
1383
|
|
|
472
1384
|
try:
|
|
1385
|
+
# Show loading indicator
|
|
1386
|
+
self.statusBar().showMessage('Exporting data to Parquet...')
|
|
1387
|
+
|
|
473
1388
|
# Convert table data to DataFrame
|
|
474
1389
|
df = self.get_table_data_as_dataframe()
|
|
475
1390
|
df.to_parquet(file_name, index=False)
|
|
1391
|
+
|
|
476
1392
|
self.statusBar().showMessage(f'Data exported to {file_name}')
|
|
1393
|
+
|
|
1394
|
+
# Show success message
|
|
1395
|
+
QMessageBox.information(
|
|
1396
|
+
self,
|
|
1397
|
+
"Export Successful",
|
|
1398
|
+
f"Data has been exported to:\n{file_name}",
|
|
1399
|
+
QMessageBox.StandardButton.Ok
|
|
1400
|
+
)
|
|
477
1401
|
except Exception as e:
|
|
478
1402
|
QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
|
|
1403
|
+
self.statusBar().showMessage('Error exporting data')
|
|
479
1404
|
|
|
480
1405
|
def get_table_data_as_dataframe(self):
|
|
481
1406
|
"""Helper function to convert table widget data to a DataFrame"""
|
|
@@ -489,13 +1414,58 @@ class SQLShell(QMainWindow):
|
|
|
489
1414
|
data.append(row_data)
|
|
490
1415
|
return pd.DataFrame(data, columns=headers)
|
|
491
1416
|
|
|
1417
|
+
def keyPressEvent(self, event):
|
|
1418
|
+
"""Handle global keyboard shortcuts"""
|
|
1419
|
+
# Execute query with Ctrl+Enter or Cmd+Enter (for Mac)
|
|
1420
|
+
if event.key() == Qt.Key.Key_Return and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1421
|
+
self.execute_btn.click() # Simply click the button instead of animating
|
|
1422
|
+
return
|
|
1423
|
+
|
|
1424
|
+
# Clear query with Ctrl+L
|
|
1425
|
+
if event.key() == Qt.Key.Key_L and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1426
|
+
self.clear_btn.click() # Simply click the button instead of animating
|
|
1427
|
+
return
|
|
1428
|
+
|
|
1429
|
+
super().keyPressEvent(event)
|
|
1430
|
+
|
|
492
1431
|
def main():
|
|
493
1432
|
app = QApplication(sys.argv)
|
|
494
1433
|
|
|
495
1434
|
# Set application style
|
|
496
1435
|
app.setStyle('Fusion')
|
|
497
1436
|
|
|
1437
|
+
# Apply custom palette for the entire application
|
|
1438
|
+
palette = QPalette()
|
|
1439
|
+
palette.setColor(QPalette.ColorRole.Window, QColor("#ECF0F1"))
|
|
1440
|
+
palette.setColor(QPalette.ColorRole.WindowText, QColor("#2C3E50"))
|
|
1441
|
+
palette.setColor(QPalette.ColorRole.Base, QColor("#FFFFFF"))
|
|
1442
|
+
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#F5F5F5"))
|
|
1443
|
+
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#2C3E50"))
|
|
1444
|
+
palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#FFFFFF"))
|
|
1445
|
+
palette.setColor(QPalette.ColorRole.Text, QColor("#2C3E50"))
|
|
1446
|
+
palette.setColor(QPalette.ColorRole.Button, QColor("#3498DB"))
|
|
1447
|
+
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#FFFFFF"))
|
|
1448
|
+
palette.setColor(QPalette.ColorRole.BrightText, QColor("#FFFFFF"))
|
|
1449
|
+
palette.setColor(QPalette.ColorRole.Link, QColor("#3498DB"))
|
|
1450
|
+
palette.setColor(QPalette.ColorRole.Highlight, QColor("#3498DB"))
|
|
1451
|
+
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
|
|
1452
|
+
app.setPalette(palette)
|
|
1453
|
+
|
|
1454
|
+
# Set default font
|
|
1455
|
+
default_font = QFont("Segoe UI", 10)
|
|
1456
|
+
app.setFont(default_font)
|
|
1457
|
+
|
|
1458
|
+
# Create and show the application
|
|
498
1459
|
sql_shell = SQLShell()
|
|
1460
|
+
|
|
1461
|
+
# Set application icon (if available)
|
|
1462
|
+
try:
|
|
1463
|
+
app_icon = QIcon("sqlshell/resources/icon.png")
|
|
1464
|
+
sql_shell.setWindowIcon(app_icon)
|
|
1465
|
+
except:
|
|
1466
|
+
# If icon not found, continue without it
|
|
1467
|
+
pass
|
|
1468
|
+
|
|
499
1469
|
sql_shell.show()
|
|
500
1470
|
sys.exit(app.exec())
|
|
501
1471
|
|