sqlshell 0.1.4__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 +1189 -62
- sqlshell/sqlshell/create_test_databases.py +96 -0
- {sqlshell-0.1.4.dist-info → sqlshell-0.1.6.dist-info}/METADATA +8 -2
- sqlshell-0.1.6.dist-info/RECORD +11 -0
- sqlshell/sqlshell/main.py +0 -346
- sqlshell-0.1.4.dist-info/RECORD +0 -11
- {sqlshell-0.1.4.dist-info → sqlshell-0.1.6.dist-info}/WHEEL +0 -0
- {sqlshell-0.1.4.dist-info → sqlshell-0.1.6.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.4.dist-info → sqlshell-0.1.6.dist-info}/top_level.txt +0 -0
sqlshell/main.py
CHANGED
|
@@ -1,74 +1,811 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import os
|
|
3
3
|
import duckdb
|
|
4
|
+
import sqlite3
|
|
4
5
|
import pandas as pd
|
|
5
6
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
6
7
|
QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
|
|
7
8
|
QLabel, QSplitter, QListWidget, QTableWidget,
|
|
8
|
-
QTableWidgetItem, QHeaderView
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
14
|
import numpy as np
|
|
12
15
|
from datetime import datetime
|
|
13
|
-
import create_test_data # Import the
|
|
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
|
+
|
|
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
|
|
14
490
|
|
|
15
491
|
class SQLShell(QMainWindow):
|
|
16
492
|
def __init__(self):
|
|
17
493
|
super().__init__()
|
|
18
|
-
self.
|
|
494
|
+
self.current_db_type = 'duckdb' # Default to DuckDB
|
|
495
|
+
self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
|
|
19
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
|
+
|
|
20
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
|
+
""")
|
|
21
672
|
|
|
22
673
|
def init_ui(self):
|
|
23
674
|
self.setWindowTitle('SQL Shell')
|
|
24
675
|
self.setGeometry(100, 100, 1400, 800)
|
|
25
|
-
|
|
676
|
+
|
|
677
|
+
# Create custom status bar
|
|
678
|
+
status_bar = QStatusBar()
|
|
679
|
+
self.setStatusBar(status_bar)
|
|
680
|
+
|
|
26
681
|
# Create central widget and layout
|
|
27
682
|
central_widget = QWidget()
|
|
28
683
|
self.setCentralWidget(central_widget)
|
|
29
684
|
main_layout = QHBoxLayout(central_widget)
|
|
685
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
686
|
+
main_layout.setSpacing(0)
|
|
30
687
|
|
|
31
688
|
# Left panel for table list
|
|
32
|
-
left_panel =
|
|
689
|
+
left_panel = QFrame()
|
|
690
|
+
left_panel.setObjectName("sidebar")
|
|
691
|
+
left_panel.setMinimumWidth(300)
|
|
692
|
+
left_panel.setMaximumWidth(400)
|
|
33
693
|
left_layout = QVBoxLayout(left_panel)
|
|
694
|
+
left_layout.setContentsMargins(16, 16, 16, 16)
|
|
695
|
+
left_layout.setSpacing(12)
|
|
34
696
|
|
|
35
|
-
|
|
36
|
-
|
|
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)
|
|
37
702
|
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
left_layout.addWidget(self.
|
|
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;")
|
|
705
|
+
left_layout.addWidget(self.db_info_label)
|
|
706
|
+
|
|
707
|
+
# Database action buttons
|
|
708
|
+
db_buttons_layout = QHBoxLayout()
|
|
709
|
+
db_buttons_layout.setSpacing(8)
|
|
710
|
+
|
|
711
|
+
self.open_db_btn = QPushButton('Open Database')
|
|
712
|
+
self.open_db_btn.setIcon(QIcon.fromTheme("document-open"))
|
|
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)
|
|
41
731
|
|
|
42
|
-
# Buttons for table management
|
|
43
|
-
table_buttons_layout = QHBoxLayout()
|
|
44
732
|
self.browse_btn = QPushButton('Load Files')
|
|
733
|
+
self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
|
|
45
734
|
self.browse_btn.clicked.connect(self.browse_files)
|
|
46
|
-
|
|
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"))
|
|
47
739
|
self.remove_table_btn.clicked.connect(self.remove_selected_table)
|
|
48
|
-
self.test_btn = QPushButton('Test')
|
|
49
|
-
self.test_btn.clicked.connect(self.load_test_data)
|
|
50
740
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
|
|
56
767
|
# Right panel for query and results
|
|
57
|
-
right_panel =
|
|
768
|
+
right_panel = QFrame()
|
|
769
|
+
right_panel.setObjectName("content_panel")
|
|
58
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)
|
|
59
778
|
|
|
60
779
|
# Create splitter for query and results
|
|
61
780
|
splitter = QSplitter(Qt.Orientation.Vertical)
|
|
781
|
+
splitter.setHandleWidth(8)
|
|
782
|
+
splitter.setChildrenCollapsible(False)
|
|
62
783
|
|
|
63
784
|
# Top part - Query section
|
|
64
|
-
query_widget =
|
|
785
|
+
query_widget = QFrame()
|
|
786
|
+
query_widget.setObjectName("content_panel")
|
|
65
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)
|
|
66
796
|
|
|
67
797
|
# Button row
|
|
68
798
|
button_layout = QHBoxLayout()
|
|
69
|
-
|
|
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"))
|
|
70
804
|
self.execute_btn.clicked.connect(self.execute_query)
|
|
805
|
+
self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
|
|
806
|
+
|
|
71
807
|
self.clear_btn = QPushButton('Clear')
|
|
808
|
+
self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
|
|
72
809
|
self.clear_btn.clicked.connect(self.clear_query)
|
|
73
810
|
|
|
74
811
|
button_layout.addWidget(self.execute_btn)
|
|
@@ -76,33 +813,56 @@ class SQLShell(QMainWindow):
|
|
|
76
813
|
button_layout.addStretch()
|
|
77
814
|
|
|
78
815
|
query_layout.addLayout(button_layout)
|
|
79
|
-
|
|
80
|
-
# Query input
|
|
81
|
-
self.query_edit = QTextEdit()
|
|
82
|
-
self.query_edit.setPlaceholderText("Enter your SQL query here...")
|
|
83
|
-
query_layout.addWidget(self.query_edit)
|
|
84
|
-
|
|
816
|
+
|
|
85
817
|
# Bottom part - Results section
|
|
86
|
-
results_widget =
|
|
818
|
+
results_widget = QFrame()
|
|
819
|
+
results_widget.setObjectName("content_panel")
|
|
87
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")
|
|
88
829
|
|
|
89
|
-
# Results header with row count
|
|
90
|
-
results_header = QWidget()
|
|
91
|
-
results_header_layout = QHBoxLayout(results_header)
|
|
92
|
-
self.results_label = QLabel("Results:")
|
|
93
830
|
self.row_count_label = QLabel("")
|
|
94
|
-
|
|
831
|
+
self.row_count_label.setStyleSheet(f"color: {self.colors['text_light']}; font-style: italic;")
|
|
832
|
+
|
|
833
|
+
results_header_layout.addWidget(results_title)
|
|
95
834
|
results_header_layout.addWidget(self.row_count_label)
|
|
96
835
|
results_header_layout.addStretch()
|
|
97
|
-
results_layout.addWidget(results_header)
|
|
98
836
|
|
|
99
|
-
#
|
|
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
|
|
100
856
|
self.results_table = QTableWidget()
|
|
101
857
|
self.results_table.setSortingEnabled(True)
|
|
102
858
|
self.results_table.setAlternatingRowColors(True)
|
|
103
859
|
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
104
860
|
self.results_table.horizontalHeader().setSectionsMovable(True)
|
|
105
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
|
+
|
|
106
866
|
results_layout.addWidget(self.results_table)
|
|
107
867
|
|
|
108
868
|
# Add widgets to splitter
|
|
@@ -119,7 +879,16 @@ class SQLShell(QMainWindow):
|
|
|
119
879
|
main_layout.addWidget(right_panel, 4)
|
|
120
880
|
|
|
121
881
|
# Status bar
|
|
122
|
-
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
|
+
)
|
|
123
892
|
|
|
124
893
|
def format_value(self, value):
|
|
125
894
|
"""Format values for display"""
|
|
@@ -163,19 +932,43 @@ class SQLShell(QMainWindow):
|
|
|
163
932
|
# Make cells read-only
|
|
164
933
|
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
165
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
|
+
|
|
166
940
|
self.results_table.setItem(i, j, item)
|
|
167
941
|
|
|
168
942
|
# Auto-adjust column widths while ensuring minimum and maximum sizes
|
|
169
943
|
self.results_table.resizeColumnsToContents()
|
|
170
944
|
for i in range(len(df.columns)):
|
|
171
945
|
width = self.results_table.columnWidth(i)
|
|
172
|
-
self.results_table.setColumnWidth(i, min(max(width,
|
|
946
|
+
self.results_table.setColumnWidth(i, min(max(width, 80), 300))
|
|
173
947
|
|
|
174
948
|
# Update row count
|
|
175
949
|
row_text = "row" if len(df) == 1 else "rows"
|
|
176
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"))
|
|
177
964
|
|
|
178
965
|
def browse_files(self):
|
|
966
|
+
if not self.conn:
|
|
967
|
+
# Create a default in-memory DuckDB connection if none exists
|
|
968
|
+
self.conn = duckdb.connect(':memory:')
|
|
969
|
+
self.current_db_type = 'duckdb'
|
|
970
|
+
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
971
|
+
|
|
179
972
|
file_names, _ = QFileDialog.getOpenFileNames(
|
|
180
973
|
self,
|
|
181
974
|
"Open Data Files",
|
|
@@ -205,10 +998,19 @@ class SQLShell(QMainWindow):
|
|
|
205
998
|
table_name = f"{original_name}_{counter}"
|
|
206
999
|
counter += 1
|
|
207
1000
|
|
|
208
|
-
#
|
|
209
|
-
self.
|
|
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
|
+
|
|
210
1009
|
self.loaded_tables[table_name] = file_name
|
|
211
1010
|
|
|
1011
|
+
# Store column names
|
|
1012
|
+
self.table_columns[table_name] = df.columns.tolist()
|
|
1013
|
+
|
|
212
1014
|
# Update UI
|
|
213
1015
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
214
1016
|
self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
|
|
@@ -216,14 +1018,22 @@ class SQLShell(QMainWindow):
|
|
|
216
1018
|
# Show preview of loaded data
|
|
217
1019
|
preview_df = df.head()
|
|
218
1020
|
self.populate_table(preview_df)
|
|
219
|
-
|
|
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()
|
|
220
1029
|
|
|
221
1030
|
except Exception as e:
|
|
222
|
-
|
|
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)
|
|
223
1034
|
self.results_table.setRowCount(0)
|
|
224
1035
|
self.results_table.setColumnCount(0)
|
|
225
1036
|
self.row_count_label.setText("")
|
|
226
|
-
self.results_label.setText(f"Error loading file: {str(e)}")
|
|
227
1037
|
|
|
228
1038
|
def sanitize_table_name(self, name):
|
|
229
1039
|
# Replace invalid characters with underscores
|
|
@@ -243,59 +1053,229 @@ class SQLShell(QMainWindow):
|
|
|
243
1053
|
self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
|
|
244
1054
|
# Remove from our tracking
|
|
245
1055
|
del self.loaded_tables[table_name]
|
|
1056
|
+
if table_name in self.table_columns:
|
|
1057
|
+
del self.table_columns[table_name]
|
|
246
1058
|
# Remove from list widget
|
|
247
1059
|
self.tables_list.takeItem(self.tables_list.row(current_item))
|
|
248
1060
|
self.statusBar().showMessage(f'Removed table "{table_name}"')
|
|
249
1061
|
self.results_table.setRowCount(0)
|
|
250
1062
|
self.results_table.setColumnCount(0)
|
|
251
1063
|
self.row_count_label.setText("")
|
|
252
|
-
|
|
1064
|
+
|
|
1065
|
+
# Update completer
|
|
1066
|
+
self.update_completer()
|
|
1067
|
+
|
|
1068
|
+
def open_database(self):
|
|
1069
|
+
"""Open a database file (DuckDB or SQLite)"""
|
|
1070
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
1071
|
+
self,
|
|
1072
|
+
"Open Database File",
|
|
1073
|
+
"",
|
|
1074
|
+
"Database Files (*.db);;All Files (*)"
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
if not file_name:
|
|
1078
|
+
return
|
|
1079
|
+
|
|
1080
|
+
try:
|
|
1081
|
+
# Try to detect database type
|
|
1082
|
+
is_sqlite = self.is_sqlite_db(file_name)
|
|
1083
|
+
|
|
1084
|
+
# Close existing connection if any
|
|
1085
|
+
if self.conn:
|
|
1086
|
+
self.conn.close()
|
|
1087
|
+
|
|
1088
|
+
# Connect to the database
|
|
1089
|
+
if is_sqlite:
|
|
1090
|
+
self.conn = sqlite3.connect(file_name)
|
|
1091
|
+
self.current_db_type = 'sqlite'
|
|
1092
|
+
else:
|
|
1093
|
+
self.conn = duckdb.connect(file_name)
|
|
1094
|
+
self.current_db_type = 'duckdb'
|
|
1095
|
+
|
|
1096
|
+
# Clear existing tables
|
|
1097
|
+
self.loaded_tables.clear()
|
|
1098
|
+
self.tables_list.clear()
|
|
1099
|
+
|
|
1100
|
+
# Load tables
|
|
1101
|
+
self.load_database_tables()
|
|
1102
|
+
|
|
1103
|
+
# Update UI
|
|
1104
|
+
db_type = "SQLite" if is_sqlite else "DuckDB"
|
|
1105
|
+
self.db_info_label.setText(f"Connected to: {os.path.basename(file_name)} ({db_type})")
|
|
1106
|
+
self.statusBar().showMessage(f'Successfully opened {db_type} database: {file_name}')
|
|
1107
|
+
|
|
1108
|
+
except Exception as e:
|
|
1109
|
+
QMessageBox.critical(self, "Error", f"Failed to open database: {str(e)}")
|
|
1110
|
+
self.statusBar().showMessage('Error opening database')
|
|
1111
|
+
|
|
1112
|
+
def is_sqlite_db(self, filename):
|
|
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
|
|
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()
|
|
1156
|
+
except Exception as e:
|
|
1157
|
+
self.statusBar().showMessage(f'Error loading tables: {str(e)}')
|
|
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)
|
|
253
1171
|
|
|
254
1172
|
def execute_query(self):
|
|
255
1173
|
query = self.query_edit.toPlainText().strip()
|
|
256
1174
|
if not query:
|
|
257
1175
|
return
|
|
258
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
|
+
|
|
259
1186
|
try:
|
|
260
|
-
|
|
1187
|
+
# Reset results title
|
|
1188
|
+
results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
|
|
1189
|
+
if results_title:
|
|
1190
|
+
results_title.setText("RESULTS")
|
|
1191
|
+
|
|
1192
|
+
if self.current_db_type == 'sqlite':
|
|
1193
|
+
# Execute SQLite query and convert to DataFrame
|
|
1194
|
+
result = pd.read_sql_query(query, self.conn)
|
|
1195
|
+
else:
|
|
1196
|
+
# Execute DuckDB query
|
|
1197
|
+
result = self.conn.execute(query).fetchdf()
|
|
1198
|
+
|
|
261
1199
|
self.populate_table(result)
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
|
|
264
1206
|
except Exception as e:
|
|
265
1207
|
self.results_table.setRowCount(0)
|
|
266
1208
|
self.results_table.setColumnCount(0)
|
|
267
|
-
self.row_count_label.setText("")
|
|
268
|
-
self.results_label.setText(f"Error executing query: {str(e)}")
|
|
1209
|
+
self.row_count_label.setText("Error")
|
|
269
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()
|
|
270
1227
|
|
|
271
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
|
|
272
1236
|
self.query_edit.clear()
|
|
1237
|
+
|
|
1238
|
+
# Show success message
|
|
1239
|
+
self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
|
|
273
1240
|
|
|
274
1241
|
def show_table_preview(self, item):
|
|
275
1242
|
"""Show a preview of the selected table"""
|
|
276
1243
|
if item:
|
|
277
1244
|
table_name = item.text().split(' (')[0]
|
|
278
1245
|
try:
|
|
279
|
-
|
|
1246
|
+
if self.current_db_type == 'sqlite':
|
|
1247
|
+
preview_df = pd.read_sql_query(f'SELECT * FROM "{table_name}" LIMIT 5', self.conn)
|
|
1248
|
+
else:
|
|
1249
|
+
preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
|
|
1250
|
+
|
|
280
1251
|
self.populate_table(preview_df)
|
|
281
|
-
self.results_label.setText(f"Preview of {table_name}:")
|
|
282
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
|
+
|
|
283
1259
|
except Exception as e:
|
|
284
1260
|
self.results_table.setRowCount(0)
|
|
285
1261
|
self.results_table.setColumnCount(0)
|
|
286
1262
|
self.row_count_label.setText("")
|
|
287
|
-
self.results_label.setText(f"Error showing preview: {str(e)}")
|
|
288
1263
|
self.statusBar().showMessage('Error showing table preview')
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
)
|
|
295
1272
|
|
|
296
1273
|
def load_test_data(self):
|
|
297
1274
|
"""Generate and load test data"""
|
|
298
1275
|
try:
|
|
1276
|
+
# Show loading indicator
|
|
1277
|
+
self.statusBar().showMessage('Generating test data...')
|
|
1278
|
+
|
|
299
1279
|
# Create test data directory if it doesn't exist
|
|
300
1280
|
os.makedirs('test_data', exist_ok=True)
|
|
301
1281
|
|
|
@@ -319,18 +1299,134 @@ class SQLShell(QMainWindow):
|
|
|
319
1299
|
self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
|
|
320
1300
|
self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
|
|
321
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
|
+
|
|
322
1307
|
# Update UI
|
|
323
1308
|
self.tables_list.clear()
|
|
324
1309
|
for table_name, file_path in self.loaded_tables.items():
|
|
325
1310
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
326
1311
|
|
|
327
1312
|
# Set the sample query
|
|
328
|
-
|
|
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()
|
|
329
1334
|
|
|
1335
|
+
# Show success message
|
|
330
1336
|
self.statusBar().showMessage('Test data loaded successfully')
|
|
331
1337
|
|
|
1338
|
+
# Show a preview of the sales data
|
|
1339
|
+
self.show_table_preview(self.tables_list.item(0))
|
|
1340
|
+
|
|
332
1341
|
except Exception as e:
|
|
333
1342
|
self.statusBar().showMessage(f'Error loading test data: {str(e)}')
|
|
1343
|
+
QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
|
|
1344
|
+
|
|
1345
|
+
def export_to_excel(self):
|
|
1346
|
+
if self.results_table.rowCount() == 0:
|
|
1347
|
+
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
1348
|
+
return
|
|
1349
|
+
|
|
1350
|
+
file_name, _ = QFileDialog.getSaveFileName(self, "Save as Excel", "", "Excel Files (*.xlsx);;All Files (*)")
|
|
1351
|
+
if not file_name:
|
|
1352
|
+
return
|
|
1353
|
+
|
|
1354
|
+
try:
|
|
1355
|
+
# Show loading indicator
|
|
1356
|
+
self.statusBar().showMessage('Exporting data to Excel...')
|
|
1357
|
+
|
|
1358
|
+
# Convert table data to DataFrame
|
|
1359
|
+
df = self.get_table_data_as_dataframe()
|
|
1360
|
+
df.to_excel(file_name, index=False)
|
|
1361
|
+
|
|
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
|
+
)
|
|
1371
|
+
except Exception as e:
|
|
1372
|
+
QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
|
|
1373
|
+
self.statusBar().showMessage('Error exporting data')
|
|
1374
|
+
|
|
1375
|
+
def export_to_parquet(self):
|
|
1376
|
+
if self.results_table.rowCount() == 0:
|
|
1377
|
+
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
1378
|
+
return
|
|
1379
|
+
|
|
1380
|
+
file_name, _ = QFileDialog.getSaveFileName(self, "Save as Parquet", "", "Parquet Files (*.parquet);;All Files (*)")
|
|
1381
|
+
if not file_name:
|
|
1382
|
+
return
|
|
1383
|
+
|
|
1384
|
+
try:
|
|
1385
|
+
# Show loading indicator
|
|
1386
|
+
self.statusBar().showMessage('Exporting data to Parquet...')
|
|
1387
|
+
|
|
1388
|
+
# Convert table data to DataFrame
|
|
1389
|
+
df = self.get_table_data_as_dataframe()
|
|
1390
|
+
df.to_parquet(file_name, index=False)
|
|
1391
|
+
|
|
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
|
+
)
|
|
1401
|
+
except Exception as e:
|
|
1402
|
+
QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
|
|
1403
|
+
self.statusBar().showMessage('Error exporting data')
|
|
1404
|
+
|
|
1405
|
+
def get_table_data_as_dataframe(self):
|
|
1406
|
+
"""Helper function to convert table widget data to a DataFrame"""
|
|
1407
|
+
headers = [self.results_table.horizontalHeaderItem(i).text() for i in range(self.results_table.columnCount())]
|
|
1408
|
+
data = []
|
|
1409
|
+
for row in range(self.results_table.rowCount()):
|
|
1410
|
+
row_data = []
|
|
1411
|
+
for column in range(self.results_table.columnCount()):
|
|
1412
|
+
item = self.results_table.item(row, column)
|
|
1413
|
+
row_data.append(item.text() if item else '')
|
|
1414
|
+
data.append(row_data)
|
|
1415
|
+
return pd.DataFrame(data, columns=headers)
|
|
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)
|
|
334
1430
|
|
|
335
1431
|
def main():
|
|
336
1432
|
app = QApplication(sys.argv)
|
|
@@ -338,7 +1434,38 @@ def main():
|
|
|
338
1434
|
# Set application style
|
|
339
1435
|
app.setStyle('Fusion')
|
|
340
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
|
|
341
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
|
+
|
|
342
1469
|
sql_shell.show()
|
|
343
1470
|
sys.exit(app.exec())
|
|
344
1471
|
|