sqlshell 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlshell might be problematic. Click here for more details.
- sqlshell/__init__.py +4 -2
- sqlshell/create_test_data.py +50 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/editor.py +355 -0
- sqlshell/main.py +1016 -656
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/create_icon.py +53 -0
- sqlshell/resources/create_splash.py +66 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/splash_screen.py +177 -0
- sqlshell/sqlshell/create_test_data.py +4 -23
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/syntax_highlighter.py +123 -0
- sqlshell-0.1.8.dist-info/METADATA +120 -0
- sqlshell-0.1.8.dist-info/RECORD +21 -0
- {sqlshell-0.1.6.dist-info → sqlshell-0.1.8.dist-info}/WHEEL +1 -1
- sqlshell-0.1.6.dist-info/METADATA +0 -92
- sqlshell-0.1.6.dist-info/RECORD +0 -11
- {sqlshell-0.1.6.dist-info → sqlshell-0.1.8.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.6.dist-info → sqlshell-0.1.8.dist-info}/top_level.txt +0 -0
sqlshell/main.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
# Ensure proper path setup for resources when running directly
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
+
sys.path.insert(0, project_root)
|
|
9
|
+
|
|
3
10
|
import duckdb
|
|
4
11
|
import sqlite3
|
|
5
12
|
import pandas as pd
|
|
@@ -8,493 +15,471 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
8
15
|
QLabel, QSplitter, QListWidget, QTableWidget,
|
|
9
16
|
QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
|
|
10
17
|
QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
|
|
11
|
-
QStyleFactory, QToolBar, QStatusBar
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
QStyleFactory, QToolBar, QStatusBar, QLineEdit, QMenu,
|
|
19
|
+
QCheckBox, QWidgetAction, QMenuBar, QInputDialog,
|
|
20
|
+
QStyledItemDelegate)
|
|
21
|
+
from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint
|
|
22
|
+
from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath
|
|
14
23
|
import numpy as np
|
|
15
24
|
from datetime import datetime
|
|
16
|
-
from sqlshell.sqlshell import create_test_data # Import from the correct location
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
from sqlshell import create_test_data
|
|
27
|
+
from sqlshell.splash_screen import AnimatedSplashScreen
|
|
28
|
+
from sqlshell.syntax_highlighter import SQLSyntaxHighlighter
|
|
29
|
+
from sqlshell.editor import LineNumberArea, SQLEditor
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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))
|
|
31
|
+
class BarChartDelegate(QStyledItemDelegate):
|
|
32
|
+
def __init__(self, parent=None):
|
|
33
|
+
super().__init__(parent)
|
|
34
|
+
self.min_val = 0
|
|
35
|
+
self.max_val = 1
|
|
36
|
+
self.bar_color = QColor("#3498DB")
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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))
|
|
38
|
+
def set_range(self, min_val, max_val):
|
|
39
|
+
self.min_val = min_val
|
|
40
|
+
self.max_val = max_val
|
|
60
41
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
42
|
+
def paint(self, painter, option, index):
|
|
43
|
+
# Draw the default background
|
|
44
|
+
super().paint(painter, option, index)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
text = index.data()
|
|
48
|
+
value = float(text.replace(',', ''))
|
|
49
|
+
|
|
50
|
+
# Calculate normalized value
|
|
51
|
+
range_val = self.max_val - self.min_val if self.max_val != self.min_val else 1
|
|
52
|
+
normalized = (value - self.min_val) / range_val
|
|
53
|
+
|
|
54
|
+
# Define bar dimensions
|
|
55
|
+
bar_height = 16
|
|
56
|
+
max_bar_width = 100
|
|
57
|
+
bar_width = max(5, int(max_bar_width * normalized))
|
|
58
|
+
|
|
59
|
+
# Calculate positions
|
|
60
|
+
text_width = option.fontMetrics.horizontalAdvance(text) + 10
|
|
61
|
+
bar_x = option.rect.left() + text_width + 10
|
|
62
|
+
bar_y = option.rect.center().y() - bar_height // 2
|
|
63
|
+
|
|
64
|
+
# Draw the bar
|
|
65
|
+
bar_rect = QRect(bar_x, bar_y, bar_width, bar_height)
|
|
66
|
+
painter.fillRect(bar_rect, self.bar_color)
|
|
67
|
+
|
|
68
|
+
# Draw the text
|
|
69
|
+
text_rect = QRect(option.rect.left() + 4, option.rect.top(),
|
|
70
|
+
text_width, option.rect.height())
|
|
71
|
+
painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
|
|
72
|
+
|
|
73
|
+
except (ValueError, AttributeError):
|
|
74
|
+
# If not a number, just draw the text
|
|
75
|
+
super().paint(painter, option, index)
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
self.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
self.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
class FilterHeader(QHeaderView):
|
|
78
|
+
def __init__(self, parent=None):
|
|
79
|
+
super().__init__(Qt.Orientation.Horizontal, parent)
|
|
80
|
+
self.filter_buttons = []
|
|
81
|
+
self.active_filters = {} # Track active filters for each column
|
|
82
|
+
self.columns_with_bars = set() # Track which columns show bar charts
|
|
83
|
+
self.bar_delegates = {} # Store delegates for columns with bars
|
|
84
|
+
self.setSectionsClickable(True)
|
|
85
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
86
|
+
self.customContextMenuRequested.connect(self.show_header_context_menu)
|
|
87
|
+
self.main_window = None # Store reference to main window
|
|
88
|
+
self.filter_icon_color = QColor("#3498DB") # Bright blue color for filter icon
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
90
|
+
def toggle_bar_chart(self, column_index):
|
|
91
|
+
"""Toggle bar chart visualization for a column"""
|
|
92
|
+
table = self.parent()
|
|
93
|
+
if not table:
|
|
94
|
+
return
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
if column_index in self.columns_with_bars:
|
|
97
|
+
# Remove bars
|
|
98
|
+
self.columns_with_bars.remove(column_index)
|
|
99
|
+
if column_index in self.bar_delegates:
|
|
100
|
+
table.setItemDelegateForColumn(column_index, None)
|
|
101
|
+
del self.bar_delegates[column_index]
|
|
102
|
+
else:
|
|
103
|
+
# Add bars
|
|
104
|
+
self.columns_with_bars.add(column_index)
|
|
105
|
+
|
|
106
|
+
# Get all values for normalization
|
|
107
|
+
values = []
|
|
108
|
+
for row in range(table.rowCount()):
|
|
109
|
+
item = table.item(row, column_index)
|
|
110
|
+
if item:
|
|
111
|
+
try:
|
|
112
|
+
value = float(item.text().replace(',', ''))
|
|
113
|
+
values.append(value)
|
|
114
|
+
except ValueError:
|
|
115
|
+
continue
|
|
102
116
|
|
|
103
|
-
|
|
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:
|
|
117
|
+
if not values:
|
|
114
118
|
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
119
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
120
|
+
# Calculate min and max for normalization
|
|
121
|
+
min_val = min(values)
|
|
122
|
+
max_val = max(values)
|
|
123
|
+
|
|
124
|
+
# Create and set up delegate
|
|
125
|
+
delegate = BarChartDelegate(table)
|
|
126
|
+
delegate.set_range(min_val, max_val)
|
|
127
|
+
self.bar_delegates[column_index] = delegate
|
|
128
|
+
table.setItemDelegateForColumn(column_index, delegate)
|
|
143
129
|
|
|
144
|
-
|
|
145
|
-
|
|
130
|
+
# Update the view
|
|
131
|
+
table.viewport().update()
|
|
146
132
|
|
|
147
|
-
def
|
|
148
|
-
|
|
133
|
+
def show_header_context_menu(self, pos):
|
|
134
|
+
"""Show context menu for header section"""
|
|
135
|
+
logical_index = self.logicalIndexAt(pos)
|
|
136
|
+
if logical_index < 0:
|
|
137
|
+
return
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
139
|
+
# Create context menu
|
|
140
|
+
context_menu = QMenu(self)
|
|
141
|
+
context_menu.setStyleSheet("""
|
|
142
|
+
QMenu {
|
|
143
|
+
background-color: white;
|
|
144
|
+
border: 1px solid #BDC3C7;
|
|
145
|
+
padding: 5px;
|
|
146
|
+
}
|
|
147
|
+
QMenu::item {
|
|
148
|
+
padding: 5px 20px;
|
|
149
|
+
}
|
|
150
|
+
QMenu::item:selected {
|
|
151
|
+
background-color: #3498DB;
|
|
152
|
+
color: white;
|
|
153
|
+
}
|
|
154
|
+
""")
|
|
193
155
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
156
|
+
# Add sort actions
|
|
157
|
+
sort_asc_action = context_menu.addAction("Sort Ascending")
|
|
158
|
+
sort_desc_action = context_menu.addAction("Sort Descending")
|
|
159
|
+
context_menu.addSeparator()
|
|
160
|
+
filter_action = context_menu.addAction("Filter...")
|
|
161
|
+
|
|
162
|
+
# Add bar chart action if column is numeric
|
|
163
|
+
table = self.parent()
|
|
164
|
+
if table and table.rowCount() > 0:
|
|
165
|
+
try:
|
|
166
|
+
# Check if column contains numeric values
|
|
167
|
+
sample_value = table.item(0, logical_index).text()
|
|
168
|
+
float(sample_value.replace(',', '')) # Try converting to float
|
|
169
|
+
|
|
170
|
+
context_menu.addSeparator()
|
|
171
|
+
toggle_bar_action = context_menu.addAction(
|
|
172
|
+
"Remove Bar Chart" if logical_index in self.columns_with_bars
|
|
173
|
+
else "Add Bar Chart"
|
|
174
|
+
)
|
|
175
|
+
except (ValueError, AttributeError):
|
|
176
|
+
toggle_bar_action = None
|
|
177
|
+
else:
|
|
178
|
+
toggle_bar_action = None
|
|
179
|
+
|
|
180
|
+
# Show menu and get selected action
|
|
181
|
+
action = context_menu.exec(self.mapToGlobal(pos))
|
|
182
|
+
|
|
183
|
+
if not action:
|
|
202
184
|
return
|
|
203
|
-
|
|
204
|
-
self.
|
|
205
|
-
|
|
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:
|
|
185
|
+
|
|
186
|
+
table = self.parent()
|
|
187
|
+
if not table:
|
|
212
188
|
return
|
|
189
|
+
|
|
190
|
+
if action == sort_asc_action:
|
|
191
|
+
table.sortItems(logical_index, Qt.SortOrder.AscendingOrder)
|
|
192
|
+
elif action == sort_desc_action:
|
|
193
|
+
table.sortItems(logical_index, Qt.SortOrder.DescendingOrder)
|
|
194
|
+
elif action == filter_action:
|
|
195
|
+
self.show_filter_menu(logical_index)
|
|
196
|
+
elif action == toggle_bar_action:
|
|
197
|
+
self.toggle_bar_chart(logical_index)
|
|
198
|
+
|
|
199
|
+
def set_main_window(self, window):
|
|
200
|
+
"""Set the reference to the main window"""
|
|
201
|
+
self.main_window = window
|
|
202
|
+
|
|
203
|
+
def paintSection(self, painter, rect, logical_index):
|
|
204
|
+
"""Override paint section to add filter indicator"""
|
|
205
|
+
super().paintSection(painter, rect, logical_index)
|
|
206
|
+
|
|
207
|
+
if logical_index in self.active_filters:
|
|
208
|
+
# Draw background highlight for filtered columns
|
|
209
|
+
highlight_color = QColor(52, 152, 219, 30) # Light blue background
|
|
210
|
+
painter.fillRect(rect, highlight_color)
|
|
211
|
+
|
|
212
|
+
# Make icon larger and more visible
|
|
213
|
+
icon_size = min(rect.height() - 8, 24) # Larger icon, but not too large
|
|
214
|
+
margin = 6
|
|
215
|
+
icon_rect = QRect(
|
|
216
|
+
rect.right() - icon_size - margin,
|
|
217
|
+
rect.top() + (rect.height() - icon_size) // 2,
|
|
218
|
+
icon_size,
|
|
219
|
+
icon_size
|
|
220
|
+
)
|
|
213
221
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
# Draw filter icon with improved visibility
|
|
223
|
+
painter.save()
|
|
224
|
+
|
|
225
|
+
# Set up the pen for better visibility
|
|
226
|
+
pen = painter.pen()
|
|
227
|
+
pen.setWidth(3) # Thicker lines
|
|
228
|
+
pen.setColor(self.filter_icon_color)
|
|
229
|
+
painter.setPen(pen)
|
|
230
|
+
|
|
231
|
+
# Calculate points for larger funnel shape
|
|
232
|
+
points = [
|
|
233
|
+
QPoint(icon_rect.left(), icon_rect.top()),
|
|
234
|
+
QPoint(icon_rect.right(), icon_rect.top()),
|
|
235
|
+
QPoint(icon_rect.center().x() + icon_size//3, icon_rect.center().y()),
|
|
236
|
+
QPoint(icon_rect.center().x() + icon_size//3, icon_rect.bottom()),
|
|
237
|
+
QPoint(icon_rect.center().x() - icon_size//3, icon_rect.bottom()),
|
|
238
|
+
QPoint(icon_rect.center().x() - icon_size//3, icon_rect.center().y()),
|
|
239
|
+
QPoint(icon_rect.left(), icon_rect.top())
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# Create and fill path
|
|
243
|
+
path = QPainterPath()
|
|
244
|
+
path.moveTo(float(points[0].x()), float(points[0].y()))
|
|
245
|
+
for point in points[1:]:
|
|
246
|
+
path.lineTo(float(point.x()), float(point.y()))
|
|
247
|
+
|
|
248
|
+
# Fill with semi-transparent blue
|
|
249
|
+
painter.fillPath(path, QBrush(QColor(52, 152, 219, 120))) # More opaque fill
|
|
250
|
+
|
|
251
|
+
# Draw outline
|
|
252
|
+
painter.drawPolyline(QPolygon(points))
|
|
253
|
+
|
|
254
|
+
# If multiple values are filtered, add a number
|
|
255
|
+
if len(self.active_filters[logical_index]) > 1:
|
|
256
|
+
# Draw number with better visibility
|
|
257
|
+
number_rect = QRect(icon_rect.left(), icon_rect.top(),
|
|
258
|
+
icon_rect.width(), icon_rect.height())
|
|
259
|
+
painter.setFont(QFont("Arial", icon_size//2, QFont.Weight.Bold))
|
|
260
|
+
|
|
261
|
+
# Draw text shadow for better contrast
|
|
262
|
+
painter.setPen(QColor("white"))
|
|
263
|
+
painter.drawText(number_rect.adjusted(1, 1, 1, 1),
|
|
264
|
+
Qt.AlignmentFlag.AlignCenter,
|
|
265
|
+
str(len(self.active_filters[logical_index])))
|
|
266
|
+
|
|
267
|
+
# Draw main text
|
|
268
|
+
painter.setPen(self.filter_icon_color)
|
|
269
|
+
painter.drawText(number_rect, Qt.AlignmentFlag.AlignCenter,
|
|
270
|
+
str(len(self.active_filters[logical_index])))
|
|
271
|
+
|
|
272
|
+
painter.restore()
|
|
273
|
+
|
|
274
|
+
# Draw a more visible indicator at the bottom of the header section
|
|
275
|
+
painter.save()
|
|
276
|
+
indicator_height = 3 # Thicker indicator line
|
|
277
|
+
indicator_rect = QRect(rect.left(), rect.bottom() - indicator_height,
|
|
278
|
+
rect.width(), indicator_height)
|
|
279
|
+
painter.fillRect(indicator_rect, self.filter_icon_color)
|
|
280
|
+
painter.restore()
|
|
281
|
+
|
|
282
|
+
def show_filter_menu(self, logical_index):
|
|
283
|
+
if not self.parent() or not isinstance(self.parent(), QTableWidget):
|
|
233
284
|
return
|
|
234
285
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
tc.movePosition(QTextCursor.MoveOperation.Left)
|
|
238
|
-
tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
|
|
239
|
-
tc.insertText(completion[-extra:] + " ")
|
|
240
|
-
self.setTextCursor(tc)
|
|
286
|
+
table = self.parent()
|
|
287
|
+
unique_values = set()
|
|
241
288
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
289
|
+
# Collect unique values from the column
|
|
290
|
+
for row in range(table.rowCount()):
|
|
291
|
+
item = table.item(row, logical_index)
|
|
292
|
+
if item and not table.isRowHidden(row):
|
|
293
|
+
unique_values.add(item.text())
|
|
245
294
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
295
|
+
# Create and show the filter menu
|
|
296
|
+
menu = QMenu(self)
|
|
297
|
+
menu.setStyleSheet("""
|
|
298
|
+
QMenu {
|
|
299
|
+
background-color: white;
|
|
300
|
+
border: 1px solid #BDC3C7;
|
|
301
|
+
padding: 5px;
|
|
302
|
+
}
|
|
303
|
+
QMenu::item {
|
|
304
|
+
padding: 5px 20px;
|
|
305
|
+
}
|
|
306
|
+
QMenu::item:selected {
|
|
307
|
+
background-color: #3498DB;
|
|
308
|
+
color: white;
|
|
309
|
+
}
|
|
310
|
+
QCheckBox {
|
|
311
|
+
padding: 5px;
|
|
312
|
+
}
|
|
313
|
+
QScrollArea {
|
|
314
|
+
border: none;
|
|
315
|
+
}
|
|
316
|
+
""")
|
|
252
317
|
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
318
|
+
# Add search box at the top
|
|
319
|
+
search_widget = QWidget(menu)
|
|
320
|
+
search_layout = QVBoxLayout(search_widget)
|
|
321
|
+
search_edit = QLineEdit(search_widget)
|
|
322
|
+
search_edit.setPlaceholderText("Search values...")
|
|
323
|
+
search_layout.addWidget(search_edit)
|
|
324
|
+
|
|
325
|
+
# Add action for search widget
|
|
326
|
+
search_action = QWidgetAction(menu)
|
|
327
|
+
search_action.setDefaultWidget(search_widget)
|
|
328
|
+
menu.addAction(search_action)
|
|
329
|
+
menu.addSeparator()
|
|
330
|
+
|
|
331
|
+
# Add "Select All" checkbox
|
|
332
|
+
select_all = QCheckBox("Select All", menu)
|
|
333
|
+
select_all.setChecked(True)
|
|
334
|
+
select_all_action = QWidgetAction(menu)
|
|
335
|
+
select_all_action.setDefaultWidget(select_all)
|
|
336
|
+
menu.addAction(select_all_action)
|
|
337
|
+
menu.addSeparator()
|
|
338
|
+
|
|
339
|
+
# Create scrollable area for checkboxes
|
|
340
|
+
scroll_widget = QWidget(menu)
|
|
341
|
+
scroll_layout = QVBoxLayout(scroll_widget)
|
|
342
|
+
scroll_layout.setSpacing(2)
|
|
343
|
+
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
|
344
|
+
|
|
345
|
+
# Add checkboxes for unique values
|
|
346
|
+
value_checkboxes = {}
|
|
347
|
+
for value in sorted(unique_values):
|
|
348
|
+
checkbox = QCheckBox(str(value), scroll_widget)
|
|
349
|
+
# Set checked state based on active filters
|
|
350
|
+
checkbox.setChecked(logical_index not in self.active_filters or
|
|
351
|
+
value in self.active_filters[logical_index])
|
|
352
|
+
value_checkboxes[value] = checkbox
|
|
353
|
+
scroll_layout.addWidget(checkbox)
|
|
354
|
+
|
|
355
|
+
# Add scrollable area to menu
|
|
356
|
+
scroll_action = QWidgetAction(menu)
|
|
357
|
+
scroll_action.setDefaultWidget(scroll_widget)
|
|
358
|
+
menu.addAction(scroll_action)
|
|
359
|
+
|
|
360
|
+
# Connect search box to filter checkboxes
|
|
361
|
+
def filter_checkboxes(text):
|
|
362
|
+
for value, checkbox in value_checkboxes.items():
|
|
363
|
+
checkbox.setVisible(text.lower() in str(value).lower())
|
|
364
|
+
|
|
365
|
+
search_edit.textChanged.connect(filter_checkboxes)
|
|
366
|
+
|
|
367
|
+
# Connect select all to other checkboxes
|
|
368
|
+
def toggle_all(state):
|
|
369
|
+
for checkbox in value_checkboxes.values():
|
|
370
|
+
if not checkbox.isHidden(): # Only toggle visible checkboxes
|
|
371
|
+
checkbox.setChecked(state)
|
|
372
|
+
|
|
373
|
+
select_all.stateChanged.connect(toggle_all)
|
|
374
|
+
|
|
375
|
+
# Add Apply and Clear buttons
|
|
376
|
+
menu.addSeparator()
|
|
377
|
+
apply_button = QPushButton("Apply Filter", menu)
|
|
378
|
+
apply_button.setStyleSheet("""
|
|
379
|
+
QPushButton {
|
|
380
|
+
background-color: #2ECC71;
|
|
381
|
+
color: white;
|
|
382
|
+
border: none;
|
|
383
|
+
padding: 5px 15px;
|
|
384
|
+
border-radius: 3px;
|
|
385
|
+
}
|
|
386
|
+
QPushButton:hover {
|
|
387
|
+
background-color: #27AE60;
|
|
388
|
+
}
|
|
389
|
+
""")
|
|
261
390
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
391
|
+
clear_button = QPushButton("Clear Filter", menu)
|
|
392
|
+
clear_button.setStyleSheet("""
|
|
393
|
+
QPushButton {
|
|
394
|
+
background-color: #E74C3C;
|
|
395
|
+
color: white;
|
|
396
|
+
border: none;
|
|
397
|
+
padding: 5px 15px;
|
|
398
|
+
border-radius: 3px;
|
|
399
|
+
}
|
|
400
|
+
QPushButton:hover {
|
|
401
|
+
background-color: #C0392B;
|
|
402
|
+
}
|
|
403
|
+
""")
|
|
266
404
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
405
|
+
button_widget = QWidget(menu)
|
|
406
|
+
button_layout = QHBoxLayout(button_widget)
|
|
407
|
+
button_layout.addWidget(apply_button)
|
|
408
|
+
button_layout.addWidget(clear_button)
|
|
278
409
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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)
|
|
410
|
+
button_action = QWidgetAction(menu)
|
|
411
|
+
button_action.setDefaultWidget(button_widget)
|
|
412
|
+
menu.addAction(button_action)
|
|
323
413
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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)
|
|
414
|
+
def apply_filter():
|
|
415
|
+
# Get selected values
|
|
416
|
+
selected_values = {value for value, checkbox in value_checkboxes.items()
|
|
417
|
+
if checkbox.isChecked()}
|
|
423
418
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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()
|
|
419
|
+
if len(selected_values) < len(unique_values):
|
|
420
|
+
# Store active filter only if not all values are selected
|
|
421
|
+
self.active_filters[logical_index] = selected_values
|
|
438
422
|
else:
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
self.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
bottom = top + round(self.blockBoundingRect(block).height())
|
|
489
|
-
block_number += 1
|
|
423
|
+
# Remove filter if all values are selected
|
|
424
|
+
self.active_filters.pop(logical_index, None)
|
|
425
|
+
|
|
426
|
+
# Apply all active filters
|
|
427
|
+
self.apply_all_filters(table)
|
|
428
|
+
|
|
429
|
+
menu.close()
|
|
430
|
+
self.updateSection(logical_index) # Redraw section to show/hide filter icon
|
|
431
|
+
|
|
432
|
+
def clear_filter():
|
|
433
|
+
# Remove filter for this column
|
|
434
|
+
if logical_index in self.active_filters:
|
|
435
|
+
del self.active_filters[logical_index]
|
|
436
|
+
|
|
437
|
+
# Apply remaining filters
|
|
438
|
+
self.apply_all_filters(table)
|
|
439
|
+
|
|
440
|
+
menu.close()
|
|
441
|
+
self.updateSection(logical_index) # Redraw section to hide filter icon
|
|
442
|
+
|
|
443
|
+
apply_button.clicked.connect(apply_filter)
|
|
444
|
+
clear_button.clicked.connect(clear_filter)
|
|
445
|
+
|
|
446
|
+
# Show menu under the header section
|
|
447
|
+
header_pos = self.mapToGlobal(self.geometry().bottomLeft())
|
|
448
|
+
header_pos.setX(header_pos.x() + self.sectionPosition(logical_index))
|
|
449
|
+
menu.exec(header_pos)
|
|
450
|
+
|
|
451
|
+
def apply_all_filters(self, table):
|
|
452
|
+
"""Apply all active filters to the table"""
|
|
453
|
+
# Show all rows first
|
|
454
|
+
for row in range(table.rowCount()):
|
|
455
|
+
table.setRowHidden(row, False)
|
|
456
|
+
|
|
457
|
+
# Apply each active filter
|
|
458
|
+
for col_idx, allowed_values in self.active_filters.items():
|
|
459
|
+
for row in range(table.rowCount()):
|
|
460
|
+
item = table.item(row, col_idx)
|
|
461
|
+
if item and not table.isRowHidden(row):
|
|
462
|
+
table.setRowHidden(row, item.text() not in allowed_values)
|
|
463
|
+
|
|
464
|
+
# Update status bar with visible row count
|
|
465
|
+
if self.main_window:
|
|
466
|
+
visible_rows = sum(1 for row in range(table.rowCount())
|
|
467
|
+
if not table.isRowHidden(row))
|
|
468
|
+
total_filters = len(self.active_filters)
|
|
469
|
+
filter_text = f" ({total_filters} filter{'s' if total_filters != 1 else ''} active)" if total_filters > 0 else ""
|
|
470
|
+
self.main_window.statusBar().showMessage(
|
|
471
|
+
f"Showing {visible_rows:,} rows{filter_text}")
|
|
490
472
|
|
|
491
473
|
class SQLShell(QMainWindow):
|
|
492
474
|
def __init__(self):
|
|
493
475
|
super().__init__()
|
|
494
|
-
self.
|
|
495
|
-
self.
|
|
476
|
+
self.conn = None
|
|
477
|
+
self.current_connection_type = None
|
|
496
478
|
self.loaded_tables = {} # Keep track of loaded tables
|
|
497
479
|
self.table_columns = {} # Keep track of table columns
|
|
480
|
+
self.current_df = None # Store the current DataFrame for filtering
|
|
481
|
+
self.filter_widgets = [] # Store filter line edits
|
|
482
|
+
self.current_project_file = None # Store the current project file path
|
|
498
483
|
|
|
499
484
|
# Define color scheme
|
|
500
485
|
self.colors = {
|
|
@@ -674,6 +659,33 @@ class SQLShell(QMainWindow):
|
|
|
674
659
|
self.setWindowTitle('SQL Shell')
|
|
675
660
|
self.setGeometry(100, 100, 1400, 800)
|
|
676
661
|
|
|
662
|
+
# Create menu bar
|
|
663
|
+
menubar = self.menuBar()
|
|
664
|
+
file_menu = menubar.addMenu('&File')
|
|
665
|
+
|
|
666
|
+
# Project management actions
|
|
667
|
+
new_project_action = file_menu.addAction('New Project')
|
|
668
|
+
new_project_action.setShortcut('Ctrl+N')
|
|
669
|
+
new_project_action.triggered.connect(self.new_project)
|
|
670
|
+
|
|
671
|
+
open_project_action = file_menu.addAction('Open Project...')
|
|
672
|
+
open_project_action.setShortcut('Ctrl+O')
|
|
673
|
+
open_project_action.triggered.connect(self.open_project)
|
|
674
|
+
|
|
675
|
+
save_project_action = file_menu.addAction('Save Project')
|
|
676
|
+
save_project_action.setShortcut('Ctrl+S')
|
|
677
|
+
save_project_action.triggered.connect(self.save_project)
|
|
678
|
+
|
|
679
|
+
save_project_as_action = file_menu.addAction('Save Project As...')
|
|
680
|
+
save_project_as_action.setShortcut('Ctrl+Shift+S')
|
|
681
|
+
save_project_as_action.triggered.connect(self.save_project_as)
|
|
682
|
+
|
|
683
|
+
file_menu.addSeparator()
|
|
684
|
+
|
|
685
|
+
exit_action = file_menu.addAction('Exit')
|
|
686
|
+
exit_action.setShortcut('Ctrl+Q')
|
|
687
|
+
exit_action.triggered.connect(self.close)
|
|
688
|
+
|
|
677
689
|
# Create custom status bar
|
|
678
690
|
status_bar = QStatusBar()
|
|
679
691
|
self.setStatusBar(status_bar)
|
|
@@ -759,6 +771,8 @@ class SQLShell(QMainWindow):
|
|
|
759
771
|
}
|
|
760
772
|
""")
|
|
761
773
|
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
774
|
+
self.tables_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
775
|
+
self.tables_list.customContextMenuRequested.connect(self.show_tables_context_menu)
|
|
762
776
|
left_layout.addWidget(self.tables_list)
|
|
763
777
|
|
|
764
778
|
# Add spacer at the bottom
|
|
@@ -856,8 +870,14 @@ class SQLShell(QMainWindow):
|
|
|
856
870
|
self.results_table = QTableWidget()
|
|
857
871
|
self.results_table.setSortingEnabled(True)
|
|
858
872
|
self.results_table.setAlternatingRowColors(True)
|
|
859
|
-
|
|
860
|
-
|
|
873
|
+
|
|
874
|
+
# Set custom header for filtering
|
|
875
|
+
header = FilterHeader(self.results_table)
|
|
876
|
+
header.set_main_window(self) # Set reference to main window
|
|
877
|
+
self.results_table.setHorizontalHeader(header)
|
|
878
|
+
header.setStretchLastSection(True)
|
|
879
|
+
header.setSectionsMovable(True)
|
|
880
|
+
|
|
861
881
|
self.results_table.verticalHeader().setVisible(False)
|
|
862
882
|
self.results_table.setShowGrid(True)
|
|
863
883
|
self.results_table.setGridStyle(Qt.PenStyle.SolidLine)
|
|
@@ -890,83 +910,141 @@ class SQLShell(QMainWindow):
|
|
|
890
910
|
"Ctrl+Space: Show autocomplete"
|
|
891
911
|
)
|
|
892
912
|
|
|
893
|
-
def format_value(self, value):
|
|
894
|
-
"""Format values for display"""
|
|
895
|
-
if pd.isna(value):
|
|
896
|
-
return 'NULL'
|
|
897
|
-
elif isinstance(value, (int, np.integer)):
|
|
898
|
-
return f"{value:,}"
|
|
899
|
-
elif isinstance(value, (float, np.floating)):
|
|
900
|
-
return f"{value:,.2f}"
|
|
901
|
-
elif isinstance(value, (datetime, pd.Timestamp)):
|
|
902
|
-
return value.strftime('%Y-%m-%d %H:%M:%S')
|
|
903
|
-
return str(value)
|
|
904
|
-
|
|
905
913
|
def populate_table(self, df):
|
|
906
|
-
"""Populate the table
|
|
907
|
-
|
|
914
|
+
"""Populate the results table with DataFrame data using memory-efficient chunking"""
|
|
915
|
+
try:
|
|
916
|
+
# Store the current DataFrame for filtering
|
|
917
|
+
self.current_df = df.copy()
|
|
918
|
+
|
|
919
|
+
# Remember which columns had bar charts
|
|
920
|
+
header = self.results_table.horizontalHeader()
|
|
921
|
+
if isinstance(header, FilterHeader):
|
|
922
|
+
columns_with_bars = header.columns_with_bars.copy()
|
|
923
|
+
else:
|
|
924
|
+
columns_with_bars = set()
|
|
925
|
+
|
|
926
|
+
# Clear existing data
|
|
927
|
+
self.results_table.clearContents()
|
|
908
928
|
self.results_table.setRowCount(0)
|
|
909
929
|
self.results_table.setColumnCount(0)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
self.results_table.setRowCount(len(df))
|
|
915
|
-
self.results_table.setColumnCount(len(df.columns))
|
|
916
|
-
|
|
917
|
-
# Set headers
|
|
918
|
-
self.results_table.setHorizontalHeaderLabels(df.columns)
|
|
919
|
-
|
|
920
|
-
# Populate data
|
|
921
|
-
for i, (_, row) in enumerate(df.iterrows()):
|
|
922
|
-
for j, value in enumerate(row):
|
|
923
|
-
formatted_value = self.format_value(value)
|
|
924
|
-
item = QTableWidgetItem(formatted_value)
|
|
925
|
-
|
|
926
|
-
# Set alignment based on data type
|
|
927
|
-
if isinstance(value, (int, float, np.integer, np.floating)):
|
|
928
|
-
item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
929
|
-
else:
|
|
930
|
-
item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
930
|
+
|
|
931
|
+
if df.empty:
|
|
932
|
+
self.statusBar().showMessage("Query returned no results")
|
|
933
|
+
return
|
|
931
934
|
|
|
932
|
-
|
|
933
|
-
|
|
935
|
+
# Set up the table dimensions
|
|
936
|
+
row_count = len(df)
|
|
937
|
+
col_count = len(df.columns)
|
|
938
|
+
self.results_table.setColumnCount(col_count)
|
|
939
|
+
|
|
940
|
+
# Set column headers
|
|
941
|
+
headers = [str(col) for col in df.columns]
|
|
942
|
+
self.results_table.setHorizontalHeaderLabels(headers)
|
|
943
|
+
|
|
944
|
+
# Calculate chunk size (adjust based on available memory)
|
|
945
|
+
CHUNK_SIZE = 1000
|
|
946
|
+
|
|
947
|
+
# Process data in chunks to avoid memory issues with large datasets
|
|
948
|
+
for chunk_start in range(0, row_count, CHUNK_SIZE):
|
|
949
|
+
chunk_end = min(chunk_start + CHUNK_SIZE, row_count)
|
|
950
|
+
chunk = df.iloc[chunk_start:chunk_end]
|
|
934
951
|
|
|
935
|
-
#
|
|
936
|
-
|
|
937
|
-
item.setForeground(QColor(self.colors['text_light']))
|
|
938
|
-
item.setBackground(QColor("#F8F9FA"))
|
|
952
|
+
# Add rows for this chunk
|
|
953
|
+
self.results_table.setRowCount(chunk_end)
|
|
939
954
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if
|
|
955
|
-
for
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
955
|
+
for row_idx, (_, row_data) in enumerate(chunk.iterrows(), start=chunk_start):
|
|
956
|
+
for col_idx, value in enumerate(row_data):
|
|
957
|
+
formatted_value = self.format_value(value)
|
|
958
|
+
item = QTableWidgetItem(formatted_value)
|
|
959
|
+
self.results_table.setItem(row_idx, col_idx, item)
|
|
960
|
+
|
|
961
|
+
# Process events to keep UI responsive
|
|
962
|
+
QApplication.processEvents()
|
|
963
|
+
|
|
964
|
+
# Optimize column widths
|
|
965
|
+
self.results_table.resizeColumnsToContents()
|
|
966
|
+
|
|
967
|
+
# Restore bar charts for columns that previously had them
|
|
968
|
+
header = self.results_table.horizontalHeader()
|
|
969
|
+
if isinstance(header, FilterHeader):
|
|
970
|
+
for col_idx in columns_with_bars:
|
|
971
|
+
if col_idx < col_count: # Only if column still exists
|
|
972
|
+
header.toggle_bar_chart(col_idx)
|
|
973
|
+
|
|
974
|
+
# Update status
|
|
975
|
+
memory_usage = df.memory_usage(deep=True).sum() / (1024 * 1024) # Convert to MB
|
|
976
|
+
self.statusBar().showMessage(
|
|
977
|
+
f"Loaded {row_count:,} rows, {col_count} columns. Memory usage: {memory_usage:.1f} MB"
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
except Exception as e:
|
|
981
|
+
QMessageBox.critical(self, "Error",
|
|
982
|
+
f"Failed to populate results table:\n\n{str(e)}")
|
|
983
|
+
self.statusBar().showMessage("Failed to display results")
|
|
984
|
+
|
|
985
|
+
def apply_filters(self):
|
|
986
|
+
"""Apply filters to the table based on filter inputs"""
|
|
987
|
+
if self.current_df is None or not self.filter_widgets:
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
try:
|
|
991
|
+
# Start with the original DataFrame
|
|
992
|
+
filtered_df = self.current_df.copy()
|
|
993
|
+
|
|
994
|
+
# Apply each non-empty filter
|
|
995
|
+
for col_idx, filter_widget in enumerate(self.filter_widgets):
|
|
996
|
+
filter_text = filter_widget.text().strip()
|
|
997
|
+
if filter_text:
|
|
998
|
+
col_name = self.current_df.columns[col_idx]
|
|
999
|
+
# Convert column to string for filtering
|
|
1000
|
+
filtered_df[col_name] = filtered_df[col_name].astype(str)
|
|
1001
|
+
filtered_df = filtered_df[filtered_df[col_name].str.contains(filter_text, case=False, na=False)]
|
|
1002
|
+
|
|
1003
|
+
# Update table with filtered data
|
|
1004
|
+
row_count = len(filtered_df)
|
|
1005
|
+
for row_idx in range(row_count):
|
|
1006
|
+
for col_idx, value in enumerate(filtered_df.iloc[row_idx]):
|
|
1007
|
+
formatted_value = self.format_value(value)
|
|
1008
|
+
item = QTableWidgetItem(formatted_value)
|
|
1009
|
+
self.results_table.setItem(row_idx, col_idx, item)
|
|
1010
|
+
|
|
1011
|
+
# Hide rows that don't match filter
|
|
1012
|
+
for row_idx in range(row_count + 1, self.results_table.rowCount()):
|
|
1013
|
+
self.results_table.hideRow(row_idx)
|
|
1014
|
+
|
|
1015
|
+
# Show all filtered rows
|
|
1016
|
+
for row_idx in range(1, row_count + 1):
|
|
1017
|
+
self.results_table.showRow(row_idx)
|
|
1018
|
+
|
|
1019
|
+
# Update status
|
|
1020
|
+
self.statusBar().showMessage(f"Showing {row_count:,} rows after filtering")
|
|
1021
|
+
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
self.statusBar().showMessage(f"Error applying filters: {str(e)}")
|
|
1024
|
+
|
|
1025
|
+
def format_value(self, value):
|
|
1026
|
+
"""Format cell values efficiently"""
|
|
1027
|
+
if pd.isna(value):
|
|
1028
|
+
return "NULL"
|
|
1029
|
+
elif isinstance(value, (float, np.floating)):
|
|
1030
|
+
if value.is_integer():
|
|
1031
|
+
return str(int(value))
|
|
1032
|
+
return f"{value:.6g}" # Use general format with up to 6 significant digits
|
|
1033
|
+
elif isinstance(value, (pd.Timestamp, datetime)):
|
|
1034
|
+
return value.strftime("%Y-%m-%d %H:%M:%S")
|
|
1035
|
+
elif isinstance(value, (np.integer, int)):
|
|
1036
|
+
return str(value)
|
|
1037
|
+
elif isinstance(value, bool):
|
|
1038
|
+
return str(value)
|
|
1039
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
1040
|
+
return value.hex()
|
|
1041
|
+
return str(value)
|
|
964
1042
|
|
|
965
1043
|
def browse_files(self):
|
|
966
1044
|
if not self.conn:
|
|
967
1045
|
# Create a default in-memory DuckDB connection if none exists
|
|
968
1046
|
self.conn = duckdb.connect(':memory:')
|
|
969
|
-
self.
|
|
1047
|
+
self.current_connection_type = 'duckdb'
|
|
970
1048
|
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
971
1049
|
|
|
972
1050
|
file_names, _ = QFileDialog.getOpenFileNames(
|
|
@@ -999,7 +1077,7 @@ class SQLShell(QMainWindow):
|
|
|
999
1077
|
counter += 1
|
|
1000
1078
|
|
|
1001
1079
|
# Handle table creation based on database type
|
|
1002
|
-
if self.
|
|
1080
|
+
if self.current_connection_type == 'sqlite':
|
|
1003
1081
|
# For SQLite, create a table from the DataFrame
|
|
1004
1082
|
df.to_sql(table_name, self.conn, index=False, if_exists='replace')
|
|
1005
1083
|
else:
|
|
@@ -1066,48 +1144,41 @@ class SQLShell(QMainWindow):
|
|
|
1066
1144
|
self.update_completer()
|
|
1067
1145
|
|
|
1068
1146
|
def open_database(self):
|
|
1069
|
-
"""Open a database
|
|
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
|
-
|
|
1147
|
+
"""Open a database connection with proper error handling and resource management"""
|
|
1080
1148
|
try:
|
|
1081
|
-
# Try to detect database type
|
|
1082
|
-
is_sqlite = self.is_sqlite_db(file_name)
|
|
1083
|
-
|
|
1084
|
-
# Close existing connection if any
|
|
1085
1149
|
if self.conn:
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
self.
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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}')
|
|
1150
|
+
# Close existing connection before opening new one
|
|
1151
|
+
if self.current_connection_type == "duckdb":
|
|
1152
|
+
self.conn.close()
|
|
1153
|
+
else: # sqlite
|
|
1154
|
+
self.conn.close()
|
|
1155
|
+
self.conn = None
|
|
1156
|
+
self.current_connection_type = None
|
|
1157
|
+
|
|
1158
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
1159
|
+
self,
|
|
1160
|
+
"Open Database",
|
|
1161
|
+
"",
|
|
1162
|
+
"All Database Files (*.db *.sqlite *.sqlite3);;All Files (*)"
|
|
1163
|
+
)
|
|
1107
1164
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1165
|
+
if filename:
|
|
1166
|
+
if self.is_sqlite_db(filename):
|
|
1167
|
+
self.conn = sqlite3.connect(filename)
|
|
1168
|
+
self.current_connection_type = "sqlite"
|
|
1169
|
+
else:
|
|
1170
|
+
self.conn = duckdb.connect(filename)
|
|
1171
|
+
self.current_connection_type = "duckdb"
|
|
1172
|
+
|
|
1173
|
+
self.load_database_tables()
|
|
1174
|
+
self.statusBar().showMessage(f"Connected to database: {filename}")
|
|
1175
|
+
|
|
1176
|
+
except (sqlite3.Error, duckdb.Error) as e:
|
|
1177
|
+
QMessageBox.critical(self, "Database Connection Error",
|
|
1178
|
+
f"Failed to open database:\n\n{str(e)}")
|
|
1179
|
+
self.statusBar().showMessage("Failed to open database")
|
|
1180
|
+
self.conn = None
|
|
1181
|
+
self.current_connection_type = None
|
|
1111
1182
|
|
|
1112
1183
|
def is_sqlite_db(self, filename):
|
|
1113
1184
|
"""Check if the file is a SQLite database"""
|
|
@@ -1121,7 +1192,7 @@ class SQLShell(QMainWindow):
|
|
|
1121
1192
|
def load_database_tables(self):
|
|
1122
1193
|
"""Load all tables from the current database"""
|
|
1123
1194
|
try:
|
|
1124
|
-
if self.
|
|
1195
|
+
if self.current_connection_type == 'sqlite':
|
|
1125
1196
|
query = "SELECT name FROM sqlite_master WHERE type='table'"
|
|
1126
1197
|
cursor = self.conn.cursor()
|
|
1127
1198
|
tables = cursor.execute(query).fetchall()
|
|
@@ -1170,60 +1241,44 @@ class SQLShell(QMainWindow):
|
|
|
1170
1241
|
self.query_edit.update_completer_model(completion_words)
|
|
1171
1242
|
|
|
1172
1243
|
def execute_query(self):
|
|
1173
|
-
query = self.query_edit.toPlainText().strip()
|
|
1174
|
-
if not query:
|
|
1175
|
-
return
|
|
1176
|
-
|
|
1177
|
-
if not self.conn:
|
|
1178
|
-
QMessageBox.warning(self, "No Connection", "No database connection available. Creating an in-memory DuckDB database.")
|
|
1179
|
-
self.conn = duckdb.connect(':memory:')
|
|
1180
|
-
self.current_db_type = 'duckdb'
|
|
1181
|
-
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
1182
|
-
|
|
1183
|
-
# Show loading indicator in status bar
|
|
1184
|
-
self.statusBar().showMessage('Executing query...')
|
|
1185
|
-
|
|
1186
1244
|
try:
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
1199
|
-
self.populate_table(result)
|
|
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')
|
|
1245
|
+
query = self.query_edit.toPlainText().strip()
|
|
1246
|
+
if not query:
|
|
1247
|
+
QMessageBox.warning(self, "Empty Query", "Please enter a SQL query to execute.")
|
|
1248
|
+
return
|
|
1249
|
+
|
|
1250
|
+
start_time = datetime.now()
|
|
1205
1251
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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."
|
|
1252
|
+
try:
|
|
1253
|
+
if self.current_connection_type == "duckdb":
|
|
1254
|
+
result = self.conn.execute(query).fetchdf()
|
|
1255
|
+
else: # sqlite
|
|
1256
|
+
result = pd.read_sql_query(query, self.conn)
|
|
1218
1257
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1258
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
1259
|
+
self.populate_table(result)
|
|
1260
|
+
self.statusBar().showMessage(f"Query executed successfully. Time: {execution_time:.2f}s. Rows: {len(result)}")
|
|
1261
|
+
|
|
1262
|
+
except (duckdb.Error, sqlite3.Error) as e:
|
|
1263
|
+
error_msg = str(e)
|
|
1264
|
+
if "syntax error" in error_msg.lower():
|
|
1265
|
+
QMessageBox.critical(self, "SQL Syntax Error",
|
|
1266
|
+
f"There is a syntax error in your query:\n\n{error_msg}")
|
|
1267
|
+
elif "no such table" in error_msg.lower():
|
|
1268
|
+
QMessageBox.critical(self, "Table Not Found",
|
|
1269
|
+
f"The referenced table does not exist:\n\n{error_msg}")
|
|
1270
|
+
elif "no such column" in error_msg.lower():
|
|
1271
|
+
QMessageBox.critical(self, "Column Not Found",
|
|
1272
|
+
f"The referenced column does not exist:\n\n{error_msg}")
|
|
1273
|
+
else:
|
|
1274
|
+
QMessageBox.critical(self, "Database Error",
|
|
1275
|
+
f"An error occurred while executing the query:\n\n{error_msg}")
|
|
1276
|
+
self.statusBar().showMessage("Query execution failed")
|
|
1277
|
+
|
|
1278
|
+
except Exception as e:
|
|
1279
|
+
QMessageBox.critical(self, "Unexpected Error",
|
|
1280
|
+
f"An unexpected error occurred:\n\n{str(e)}")
|
|
1281
|
+
self.statusBar().showMessage("Query execution failed")
|
|
1227
1282
|
|
|
1228
1283
|
def clear_query(self):
|
|
1229
1284
|
"""Clear the query editor with animation"""
|
|
@@ -1243,7 +1298,7 @@ class SQLShell(QMainWindow):
|
|
|
1243
1298
|
if item:
|
|
1244
1299
|
table_name = item.text().split(' (')[0]
|
|
1245
1300
|
try:
|
|
1246
|
-
if self.
|
|
1301
|
+
if self.current_connection_type == 'sqlite':
|
|
1247
1302
|
preview_df = pd.read_sql_query(f'SELECT * FROM "{table_name}" LIMIT 5', self.conn)
|
|
1248
1303
|
else:
|
|
1249
1304
|
preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
|
|
@@ -1273,6 +1328,12 @@ class SQLShell(QMainWindow):
|
|
|
1273
1328
|
def load_test_data(self):
|
|
1274
1329
|
"""Generate and load test data"""
|
|
1275
1330
|
try:
|
|
1331
|
+
# Ensure we have a DuckDB connection
|
|
1332
|
+
if not self.conn or self.current_connection_type != 'duckdb':
|
|
1333
|
+
self.conn = duckdb.connect(':memory:')
|
|
1334
|
+
self.current_connection_type = 'duckdb'
|
|
1335
|
+
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
1336
|
+
|
|
1276
1337
|
# Show loading indicator
|
|
1277
1338
|
self.statusBar().showMessage('Generating test data...')
|
|
1278
1339
|
|
|
@@ -1312,19 +1373,12 @@ class SQLShell(QMainWindow):
|
|
|
1312
1373
|
# Set the sample query
|
|
1313
1374
|
sample_query = """
|
|
1314
1375
|
SELECT
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
c.customername,
|
|
1318
|
-
p.productname,
|
|
1319
|
-
s.quantity,
|
|
1320
|
-
s.unitprice,
|
|
1321
|
-
(s.quantity * s.unitprice) AS total_amount
|
|
1376
|
+
DISTINCT
|
|
1377
|
+
c.customername
|
|
1322
1378
|
FROM
|
|
1323
1379
|
sample_sales_data s
|
|
1324
1380
|
INNER JOIN customer_data c ON c.customerid = s.customerid
|
|
1325
1381
|
INNER JOIN product_catalog p ON p.productid = s.productid
|
|
1326
|
-
ORDER BY
|
|
1327
|
-
s.orderdate DESC
|
|
1328
1382
|
LIMIT 10
|
|
1329
1383
|
"""
|
|
1330
1384
|
self.query_edit.setPlainText(sample_query.strip())
|
|
@@ -1359,13 +1413,36 @@ LIMIT 10
|
|
|
1359
1413
|
df = self.get_table_data_as_dataframe()
|
|
1360
1414
|
df.to_excel(file_name, index=False)
|
|
1361
1415
|
|
|
1362
|
-
|
|
1416
|
+
# Generate table name from file name
|
|
1417
|
+
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1418
|
+
table_name = self.sanitize_table_name(base_name)
|
|
1419
|
+
|
|
1420
|
+
# Ensure unique table name
|
|
1421
|
+
original_name = table_name
|
|
1422
|
+
counter = 1
|
|
1423
|
+
while table_name in self.loaded_tables:
|
|
1424
|
+
table_name = f"{original_name}_{counter}"
|
|
1425
|
+
counter += 1
|
|
1426
|
+
|
|
1427
|
+
# Register the table in DuckDB
|
|
1428
|
+
self.conn.register(table_name, df)
|
|
1429
|
+
|
|
1430
|
+
# Update tracking
|
|
1431
|
+
self.loaded_tables[table_name] = file_name
|
|
1432
|
+
self.table_columns[table_name] = df.columns.tolist()
|
|
1433
|
+
|
|
1434
|
+
# Update UI
|
|
1435
|
+
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
1436
|
+
self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
|
|
1437
|
+
|
|
1438
|
+
# Update completer with new table and column names
|
|
1439
|
+
self.update_completer()
|
|
1363
1440
|
|
|
1364
1441
|
# Show success message
|
|
1365
1442
|
QMessageBox.information(
|
|
1366
1443
|
self,
|
|
1367
1444
|
"Export Successful",
|
|
1368
|
-
f"Data has been exported to:\n{file_name}",
|
|
1445
|
+
f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
|
|
1369
1446
|
QMessageBox.StandardButton.Ok
|
|
1370
1447
|
)
|
|
1371
1448
|
except Exception as e:
|
|
@@ -1389,13 +1466,36 @@ LIMIT 10
|
|
|
1389
1466
|
df = self.get_table_data_as_dataframe()
|
|
1390
1467
|
df.to_parquet(file_name, index=False)
|
|
1391
1468
|
|
|
1392
|
-
|
|
1469
|
+
# Generate table name from file name
|
|
1470
|
+
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1471
|
+
table_name = self.sanitize_table_name(base_name)
|
|
1472
|
+
|
|
1473
|
+
# Ensure unique table name
|
|
1474
|
+
original_name = table_name
|
|
1475
|
+
counter = 1
|
|
1476
|
+
while table_name in self.loaded_tables:
|
|
1477
|
+
table_name = f"{original_name}_{counter}"
|
|
1478
|
+
counter += 1
|
|
1479
|
+
|
|
1480
|
+
# Register the table in DuckDB
|
|
1481
|
+
self.conn.register(table_name, df)
|
|
1482
|
+
|
|
1483
|
+
# Update tracking
|
|
1484
|
+
self.loaded_tables[table_name] = file_name
|
|
1485
|
+
self.table_columns[table_name] = df.columns.tolist()
|
|
1486
|
+
|
|
1487
|
+
# Update UI
|
|
1488
|
+
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
1489
|
+
self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
|
|
1490
|
+
|
|
1491
|
+
# Update completer with new table and column names
|
|
1492
|
+
self.update_completer()
|
|
1393
1493
|
|
|
1394
1494
|
# Show success message
|
|
1395
1495
|
QMessageBox.information(
|
|
1396
1496
|
self,
|
|
1397
1497
|
"Export Successful",
|
|
1398
|
-
f"Data has been exported to:\n{file_name}",
|
|
1498
|
+
f"Data has been exported to:\n{file_name}\nand loaded as table: {table_name}",
|
|
1399
1499
|
QMessageBox.StandardButton.Ok
|
|
1400
1500
|
)
|
|
1401
1501
|
except Exception as e:
|
|
@@ -1428,45 +1528,305 @@ LIMIT 10
|
|
|
1428
1528
|
|
|
1429
1529
|
super().keyPressEvent(event)
|
|
1430
1530
|
|
|
1531
|
+
def closeEvent(self, event):
|
|
1532
|
+
"""Ensure proper cleanup of database connections when closing the application"""
|
|
1533
|
+
try:
|
|
1534
|
+
# Check for unsaved changes
|
|
1535
|
+
if self.has_unsaved_changes():
|
|
1536
|
+
reply = QMessageBox.question(self, 'Save Changes',
|
|
1537
|
+
'Do you want to save your changes before closing?',
|
|
1538
|
+
QMessageBox.StandardButton.Save |
|
|
1539
|
+
QMessageBox.StandardButton.Discard |
|
|
1540
|
+
QMessageBox.StandardButton.Cancel)
|
|
1541
|
+
|
|
1542
|
+
if reply == QMessageBox.StandardButton.Save:
|
|
1543
|
+
self.save_project()
|
|
1544
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
1545
|
+
event.ignore()
|
|
1546
|
+
return
|
|
1547
|
+
|
|
1548
|
+
# Close database connections
|
|
1549
|
+
if self.conn:
|
|
1550
|
+
if self.current_connection_type == "duckdb":
|
|
1551
|
+
self.conn.close()
|
|
1552
|
+
else: # sqlite
|
|
1553
|
+
self.conn.close()
|
|
1554
|
+
event.accept()
|
|
1555
|
+
except Exception as e:
|
|
1556
|
+
QMessageBox.warning(self, "Cleanup Warning",
|
|
1557
|
+
f"Warning: Could not properly close database connection:\n{str(e)}")
|
|
1558
|
+
event.accept()
|
|
1559
|
+
|
|
1560
|
+
def has_unsaved_changes(self):
|
|
1561
|
+
"""Check if there are unsaved changes in the project"""
|
|
1562
|
+
if not self.current_project_file:
|
|
1563
|
+
return bool(self.loaded_tables or self.query_edit.toPlainText().strip())
|
|
1564
|
+
|
|
1565
|
+
try:
|
|
1566
|
+
# Load the last saved state
|
|
1567
|
+
with open(self.current_project_file, 'r') as f:
|
|
1568
|
+
saved_data = json.load(f)
|
|
1569
|
+
|
|
1570
|
+
# Compare current state with saved state
|
|
1571
|
+
current_data = {
|
|
1572
|
+
'tables': {
|
|
1573
|
+
name: {
|
|
1574
|
+
'file_path': path,
|
|
1575
|
+
'columns': self.table_columns.get(name, [])
|
|
1576
|
+
}
|
|
1577
|
+
for name, path in self.loaded_tables.items()
|
|
1578
|
+
},
|
|
1579
|
+
'query': self.query_edit.toPlainText(),
|
|
1580
|
+
'connection_type': self.current_connection_type
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return current_data != saved_data
|
|
1584
|
+
|
|
1585
|
+
except Exception:
|
|
1586
|
+
# If there's any error reading the saved file, assume there are unsaved changes
|
|
1587
|
+
return True
|
|
1588
|
+
|
|
1589
|
+
def show_tables_context_menu(self, position):
|
|
1590
|
+
"""Show context menu for tables list"""
|
|
1591
|
+
item = self.tables_list.itemAt(position)
|
|
1592
|
+
if not item:
|
|
1593
|
+
return
|
|
1594
|
+
|
|
1595
|
+
# Get table name without the file info in parentheses
|
|
1596
|
+
table_name = item.text().split(' (')[0]
|
|
1597
|
+
|
|
1598
|
+
# Create context menu
|
|
1599
|
+
context_menu = QMenu(self)
|
|
1600
|
+
context_menu.setStyleSheet("""
|
|
1601
|
+
QMenu {
|
|
1602
|
+
background-color: white;
|
|
1603
|
+
border: 1px solid #BDC3C7;
|
|
1604
|
+
padding: 5px;
|
|
1605
|
+
}
|
|
1606
|
+
QMenu::item {
|
|
1607
|
+
padding: 5px 20px;
|
|
1608
|
+
}
|
|
1609
|
+
QMenu::item:selected {
|
|
1610
|
+
background-color: #3498DB;
|
|
1611
|
+
color: white;
|
|
1612
|
+
}
|
|
1613
|
+
""")
|
|
1614
|
+
|
|
1615
|
+
# Add menu actions
|
|
1616
|
+
select_from_action = context_menu.addAction("Select from")
|
|
1617
|
+
add_to_editor_action = context_menu.addAction("Just add to editor")
|
|
1618
|
+
|
|
1619
|
+
# Show menu and get selected action
|
|
1620
|
+
action = context_menu.exec(self.tables_list.mapToGlobal(position))
|
|
1621
|
+
|
|
1622
|
+
if action == select_from_action:
|
|
1623
|
+
# Insert "SELECT * FROM table_name" at cursor position
|
|
1624
|
+
cursor = self.query_edit.textCursor()
|
|
1625
|
+
cursor.insertText(f"SELECT * FROM {table_name}")
|
|
1626
|
+
self.query_edit.setFocus()
|
|
1627
|
+
elif action == add_to_editor_action:
|
|
1628
|
+
# Just insert the table name at cursor position
|
|
1629
|
+
cursor = self.query_edit.textCursor()
|
|
1630
|
+
cursor.insertText(table_name)
|
|
1631
|
+
self.query_edit.setFocus()
|
|
1632
|
+
|
|
1633
|
+
def new_project(self):
|
|
1634
|
+
"""Create a new project by clearing current state"""
|
|
1635
|
+
if self.conn:
|
|
1636
|
+
reply = QMessageBox.question(self, 'New Project',
|
|
1637
|
+
'Are you sure you want to start a new project? All unsaved changes will be lost.',
|
|
1638
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
1639
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
1640
|
+
# Close existing connection
|
|
1641
|
+
if self.current_connection_type == "duckdb":
|
|
1642
|
+
self.conn.close()
|
|
1643
|
+
else: # sqlite
|
|
1644
|
+
self.conn.close()
|
|
1645
|
+
|
|
1646
|
+
# Reset state
|
|
1647
|
+
self.conn = None
|
|
1648
|
+
self.current_connection_type = None
|
|
1649
|
+
self.loaded_tables.clear()
|
|
1650
|
+
self.table_columns.clear()
|
|
1651
|
+
self.tables_list.clear()
|
|
1652
|
+
self.query_edit.clear()
|
|
1653
|
+
self.results_table.setRowCount(0)
|
|
1654
|
+
self.results_table.setColumnCount(0)
|
|
1655
|
+
self.current_project_file = None
|
|
1656
|
+
self.setWindowTitle('SQL Shell')
|
|
1657
|
+
self.statusBar().showMessage('New project created')
|
|
1658
|
+
|
|
1659
|
+
def save_project(self):
|
|
1660
|
+
"""Save the current project"""
|
|
1661
|
+
if not self.current_project_file:
|
|
1662
|
+
self.save_project_as()
|
|
1663
|
+
return
|
|
1664
|
+
|
|
1665
|
+
self.save_project_to_file(self.current_project_file)
|
|
1666
|
+
|
|
1667
|
+
def save_project_as(self):
|
|
1668
|
+
"""Save the current project to a new file"""
|
|
1669
|
+
file_name, _ = QFileDialog.getSaveFileName(
|
|
1670
|
+
self,
|
|
1671
|
+
"Save Project",
|
|
1672
|
+
"",
|
|
1673
|
+
"SQL Shell Project (*.sqls);;All Files (*)"
|
|
1674
|
+
)
|
|
1675
|
+
|
|
1676
|
+
if file_name:
|
|
1677
|
+
if not file_name.endswith('.sqls'):
|
|
1678
|
+
file_name += '.sqls'
|
|
1679
|
+
self.save_project_to_file(file_name)
|
|
1680
|
+
self.current_project_file = file_name
|
|
1681
|
+
self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
|
|
1682
|
+
|
|
1683
|
+
def save_project_to_file(self, file_name):
|
|
1684
|
+
"""Save project data to a file"""
|
|
1685
|
+
try:
|
|
1686
|
+
project_data = {
|
|
1687
|
+
'tables': {},
|
|
1688
|
+
'query': self.query_edit.toPlainText(),
|
|
1689
|
+
'connection_type': self.current_connection_type
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
# Save table information
|
|
1693
|
+
for table_name, file_path in self.loaded_tables.items():
|
|
1694
|
+
# For database tables and query results, store the special identifier
|
|
1695
|
+
if file_path in ['database', 'query_result']:
|
|
1696
|
+
source_path = file_path
|
|
1697
|
+
else:
|
|
1698
|
+
# For file-based tables, store the absolute path
|
|
1699
|
+
source_path = os.path.abspath(file_path)
|
|
1700
|
+
|
|
1701
|
+
project_data['tables'][table_name] = {
|
|
1702
|
+
'file_path': source_path,
|
|
1703
|
+
'columns': self.table_columns.get(table_name, [])
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
with open(file_name, 'w') as f:
|
|
1707
|
+
json.dump(project_data, f, indent=4)
|
|
1708
|
+
|
|
1709
|
+
self.statusBar().showMessage(f'Project saved to {file_name}')
|
|
1710
|
+
|
|
1711
|
+
except Exception as e:
|
|
1712
|
+
QMessageBox.critical(self, "Error",
|
|
1713
|
+
f"Failed to save project:\n\n{str(e)}")
|
|
1714
|
+
|
|
1715
|
+
def open_project(self):
|
|
1716
|
+
"""Open a project file"""
|
|
1717
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
1718
|
+
self,
|
|
1719
|
+
"Open Project",
|
|
1720
|
+
"",
|
|
1721
|
+
"SQL Shell Project (*.sqls);;All Files (*)"
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
if file_name:
|
|
1725
|
+
try:
|
|
1726
|
+
with open(file_name, 'r') as f:
|
|
1727
|
+
project_data = json.load(f)
|
|
1728
|
+
|
|
1729
|
+
# Start fresh
|
|
1730
|
+
self.new_project()
|
|
1731
|
+
|
|
1732
|
+
# Create connection if needed
|
|
1733
|
+
if not self.conn:
|
|
1734
|
+
self.conn = duckdb.connect(':memory:')
|
|
1735
|
+
self.current_connection_type = 'duckdb'
|
|
1736
|
+
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
1737
|
+
|
|
1738
|
+
# Load tables
|
|
1739
|
+
for table_name, table_info in project_data['tables'].items():
|
|
1740
|
+
file_path = table_info['file_path']
|
|
1741
|
+
try:
|
|
1742
|
+
if file_path == 'database':
|
|
1743
|
+
# For tables from database, we need to recreate them from their data
|
|
1744
|
+
# Execute a SELECT to get the data and recreate the table
|
|
1745
|
+
query = f"SELECT * FROM {table_name}"
|
|
1746
|
+
df = pd.read_sql_query(query, self.conn)
|
|
1747
|
+
self.conn.register(table_name, df)
|
|
1748
|
+
self.loaded_tables[table_name] = 'database'
|
|
1749
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
1750
|
+
elif file_path == 'query_result':
|
|
1751
|
+
# For tables from query results, we'll need to re-run the query
|
|
1752
|
+
# For now, just note it as a query result table
|
|
1753
|
+
self.loaded_tables[table_name] = 'query_result'
|
|
1754
|
+
self.tables_list.addItem(f"{table_name} (query result)")
|
|
1755
|
+
elif os.path.exists(file_path):
|
|
1756
|
+
# Load the file based on its extension
|
|
1757
|
+
if file_path.endswith(('.xlsx', '.xls')):
|
|
1758
|
+
df = pd.read_excel(file_path)
|
|
1759
|
+
elif file_path.endswith('.csv'):
|
|
1760
|
+
df = pd.read_csv(file_path)
|
|
1761
|
+
elif file_path.endswith('.parquet'):
|
|
1762
|
+
df = pd.read_parquet(file_path)
|
|
1763
|
+
else:
|
|
1764
|
+
continue
|
|
1765
|
+
|
|
1766
|
+
# Register the table
|
|
1767
|
+
self.conn.register(table_name, df)
|
|
1768
|
+
self.loaded_tables[table_name] = file_path
|
|
1769
|
+
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
1770
|
+
else:
|
|
1771
|
+
QMessageBox.warning(self, "Warning",
|
|
1772
|
+
f"Could not find file for table {table_name}: {file_path}")
|
|
1773
|
+
continue
|
|
1774
|
+
|
|
1775
|
+
# Store the columns
|
|
1776
|
+
self.table_columns[table_name] = table_info['columns']
|
|
1777
|
+
|
|
1778
|
+
except Exception as e:
|
|
1779
|
+
QMessageBox.warning(self, "Warning",
|
|
1780
|
+
f"Failed to load table {table_name}:\n{str(e)}")
|
|
1781
|
+
|
|
1782
|
+
# Restore query
|
|
1783
|
+
if 'query' in project_data:
|
|
1784
|
+
self.query_edit.setPlainText(project_data['query'])
|
|
1785
|
+
|
|
1786
|
+
# Update UI
|
|
1787
|
+
self.current_project_file = file_name
|
|
1788
|
+
self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
|
|
1789
|
+
self.statusBar().showMessage(f'Project loaded from {file_name}')
|
|
1790
|
+
self.update_completer()
|
|
1791
|
+
|
|
1792
|
+
except Exception as e:
|
|
1793
|
+
QMessageBox.critical(self, "Error",
|
|
1794
|
+
f"Failed to open project:\n\n{str(e)}")
|
|
1795
|
+
|
|
1431
1796
|
def main():
|
|
1432
1797
|
app = QApplication(sys.argv)
|
|
1798
|
+
app.setStyle(QStyleFactory.create('Fusion'))
|
|
1433
1799
|
|
|
1434
|
-
#
|
|
1435
|
-
|
|
1800
|
+
# Ensure we have a valid working directory with pool.db
|
|
1801
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1802
|
+
working_dir = os.getcwd()
|
|
1436
1803
|
|
|
1437
|
-
#
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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)
|
|
1804
|
+
# If pool.db doesn't exist in current directory, copy it from package
|
|
1805
|
+
if not os.path.exists(os.path.join(working_dir, 'pool.db')):
|
|
1806
|
+
import shutil
|
|
1807
|
+
package_db = os.path.join(package_dir, 'pool.db')
|
|
1808
|
+
if os.path.exists(package_db):
|
|
1809
|
+
shutil.copy2(package_db, working_dir)
|
|
1810
|
+
else:
|
|
1811
|
+
package_db = os.path.join(os.path.dirname(package_dir), 'pool.db')
|
|
1812
|
+
if os.path.exists(package_db):
|
|
1813
|
+
shutil.copy2(package_db, working_dir)
|
|
1453
1814
|
|
|
1454
|
-
#
|
|
1455
|
-
|
|
1456
|
-
|
|
1815
|
+
# Show splash screen
|
|
1816
|
+
splash = AnimatedSplashScreen()
|
|
1817
|
+
splash.show()
|
|
1457
1818
|
|
|
1458
|
-
# Create and show
|
|
1459
|
-
|
|
1819
|
+
# Create and show main window after delay
|
|
1820
|
+
timer = QTimer()
|
|
1821
|
+
window = SQLShell()
|
|
1822
|
+
timer.timeout.connect(lambda: show_main_window())
|
|
1823
|
+
timer.start(2000) # 2 second delay
|
|
1460
1824
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
except:
|
|
1466
|
-
# If icon not found, continue without it
|
|
1467
|
-
pass
|
|
1825
|
+
def show_main_window():
|
|
1826
|
+
window.show()
|
|
1827
|
+
splash.finish(window)
|
|
1828
|
+
timer.stop()
|
|
1468
1829
|
|
|
1469
|
-
sql_shell.show()
|
|
1470
1830
|
sys.exit(app.exec())
|
|
1471
1831
|
|
|
1472
1832
|
if __name__ == '__main__':
|