sqlbench 0.1.60__tar.gz → 0.1.62__tar.gz
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.
- {sqlbench-0.1.60 → sqlbench-0.1.62}/PKG-INFO +1 -1
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/main_window.py +20 -24
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tabs/sql_tab.py +163 -52
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/version.py +1 -1
- {sqlbench-0.1.60 → sqlbench-0.1.62}/.github/workflows/pypi-publish.yml +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/.gitignore +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/CLAUDE.md +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/Makefile +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/PYQT_MIGRATION_FEATURES.md +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/README.md +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/pyproject.toml +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/requirements.txt +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/__init__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/__main__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/adapters.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/app.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/database.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/dialogs/__init__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/dialogs/connection_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/dialogs/regex_builder_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/launcher.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/__init__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/connection_tree.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/__init__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/connection_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/query_manager_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/record_viewer_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/regex_builder_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/settings_dialog.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/icons.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/syntax.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tab_widget.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tabs/__init__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tabs/spool_tab.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/theme.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_ibmi.png +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_mysql.png +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_postgresql.png +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_unknown.png +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/sqlbench.png +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/tabs/__init__.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/tabs/spool_tab.py +0 -0
- {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/tabs/sql_tab.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlbench
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.62
|
|
4
4
|
Summary: A multi-database SQL workbench with support for IBM i, MySQL, and PostgreSQL
|
|
5
5
|
Project-URL: Homepage, https://github.com/jpsteil/sqlbench
|
|
6
6
|
Project-URL: Repository, https://github.com/jpsteil/sqlbench
|
|
@@ -8,7 +8,7 @@ and tabbed query/spool interface.
|
|
|
8
8
|
import subprocess
|
|
9
9
|
import threading
|
|
10
10
|
from typing import Optional, Dict, Any
|
|
11
|
-
from PyQt6.QtCore import Qt,
|
|
11
|
+
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
|
|
12
12
|
from PyQt6.QtGui import QAction, QIcon, QKeySequence, QCloseEvent
|
|
13
13
|
from PyQt6.QtWidgets import (
|
|
14
14
|
QMainWindow,
|
|
@@ -141,25 +141,28 @@ class MainWindow(QMainWindow):
|
|
|
141
141
|
self.status_bar.showMessage("Ready")
|
|
142
142
|
|
|
143
143
|
def _restore_state(self) -> None:
|
|
144
|
-
"""Restore window
|
|
145
|
-
|
|
144
|
+
"""Restore window size (position is compositor-controlled on Wayland)."""
|
|
145
|
+
w = get_setting("window_width")
|
|
146
|
+
h = get_setting("window_height")
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
if w and h:
|
|
149
|
+
try:
|
|
150
|
+
w, h = int(w), int(h)
|
|
151
|
+
w = max(w, self.minimumWidth())
|
|
152
|
+
h = max(h, self.minimumHeight())
|
|
153
|
+
# Clamp to fit on screen
|
|
154
|
+
screen = QApplication.primaryScreen().availableGeometry()
|
|
155
|
+
w = min(w, screen.width())
|
|
156
|
+
h = min(h, screen.height())
|
|
157
|
+
self.resize(w, h)
|
|
158
|
+
except (ValueError, TypeError):
|
|
159
|
+
self.resize(1400, 900)
|
|
151
160
|
else:
|
|
152
|
-
# Default size
|
|
153
161
|
self.resize(1400, 900)
|
|
154
|
-
# Center on screen
|
|
155
|
-
screen = QApplication.primaryScreen().geometry()
|
|
156
|
-
self.move(
|
|
157
|
-
(screen.width() - self.width()) // 2,
|
|
158
|
-
(screen.height() - self.height()) // 2
|
|
159
|
-
)
|
|
160
162
|
|
|
161
163
|
# Restore main splitter from ratio
|
|
162
164
|
ratio_str = get_setting("layout_main_ratio")
|
|
165
|
+
|
|
163
166
|
if ratio_str:
|
|
164
167
|
try:
|
|
165
168
|
ratio = float(ratio_str)
|
|
@@ -209,9 +212,9 @@ class MainWindow(QMainWindow):
|
|
|
209
212
|
self._connect(last_conn)
|
|
210
213
|
|
|
211
214
|
def _save_state(self) -> None:
|
|
212
|
-
"""Save window
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
"""Save window size and state."""
|
|
216
|
+
set_setting("window_width", str(self.width()))
|
|
217
|
+
set_setting("window_height", str(self.height()))
|
|
215
218
|
|
|
216
219
|
# Save main splitter as ratio
|
|
217
220
|
sizes = self.splitter.sizes()
|
|
@@ -332,13 +335,6 @@ class MainWindow(QMainWindow):
|
|
|
332
335
|
self.splitter.setSizes([250, 1000])
|
|
333
336
|
self.resize(1400, 900)
|
|
334
337
|
|
|
335
|
-
# Center on screen
|
|
336
|
-
screen = QApplication.primaryScreen().geometry()
|
|
337
|
-
self.move(
|
|
338
|
-
(screen.width() - self.width()) // 2,
|
|
339
|
-
(screen.height() - self.height()) // 2
|
|
340
|
-
)
|
|
341
|
-
|
|
342
338
|
set_setting("font_size", "13")
|
|
343
339
|
self._apply_font_size()
|
|
344
340
|
self.status_bar.showMessage("Layout reset to defaults", 3000)
|
|
@@ -134,7 +134,7 @@ def _make_icon(shape: str, color: str = "#ddd", size: int = 18) -> QIcon:
|
|
|
134
134
|
class QueryWorker(QThread):
|
|
135
135
|
"""Background thread for query execution."""
|
|
136
136
|
|
|
137
|
-
finished = pyqtSignal(object, object, float, float, int) # results, description, exec_time, fetch_time, total_rows
|
|
137
|
+
finished = pyqtSignal(object, object, float, float, int, int) # results, description, exec_time, fetch_time, total_rows, rowcount
|
|
138
138
|
error = pyqtSignal(str)
|
|
139
139
|
row_count = pyqtSignal(int)
|
|
140
140
|
|
|
@@ -212,16 +212,17 @@ class QueryWorker(QThread):
|
|
|
212
212
|
description = cursor.description
|
|
213
213
|
if total_rows == 0:
|
|
214
214
|
total_rows = len(rows)
|
|
215
|
+
rowcount = 0
|
|
215
216
|
else:
|
|
216
217
|
rows = []
|
|
217
218
|
description = None
|
|
218
|
-
|
|
219
|
+
rowcount = cursor.rowcount if cursor.rowcount >= 0 else 0
|
|
219
220
|
|
|
220
221
|
fetch_time = time.time() - fetch_start
|
|
221
222
|
cursor.close()
|
|
222
223
|
|
|
223
224
|
if not self._cancelled:
|
|
224
|
-
self.finished.emit(rows, description, exec_time, fetch_time, total_rows)
|
|
225
|
+
self.finished.emit(rows, description, exec_time, fetch_time, total_rows, rowcount)
|
|
225
226
|
|
|
226
227
|
except Exception as e:
|
|
227
228
|
if not self._cancelled:
|
|
@@ -281,6 +282,8 @@ class ScriptWorker(QThread):
|
|
|
281
282
|
"row_count": 0,
|
|
282
283
|
"success": True,
|
|
283
284
|
"error": None,
|
|
285
|
+
"rows": None,
|
|
286
|
+
"description": None,
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
try:
|
|
@@ -293,6 +296,8 @@ class ScriptWorker(QThread):
|
|
|
293
296
|
rows = cursor.fetchall()
|
|
294
297
|
result["row_count"] = len(rows)
|
|
295
298
|
result["status"] = f"{len(rows)} row(s) returned"
|
|
299
|
+
result["description"] = cursor.description
|
|
300
|
+
result["rows"] = rows[:10000]
|
|
296
301
|
else:
|
|
297
302
|
rc = cursor.rowcount if cursor.rowcount >= 0 else 0
|
|
298
303
|
result["row_count"] = rc
|
|
@@ -370,22 +375,31 @@ class SQLEditor(QPlainTextEdit):
|
|
|
370
375
|
# Shortcuts
|
|
371
376
|
self._setup_shortcuts()
|
|
372
377
|
|
|
378
|
+
def _add_menu_action(self, menu, text, slot, shortcut=None) -> QAction:
|
|
379
|
+
"""Add action to menu with proper PyQt6 overload handling."""
|
|
380
|
+
action = QAction(text, menu)
|
|
381
|
+
if shortcut:
|
|
382
|
+
action.setShortcut(shortcut)
|
|
383
|
+
action.triggered.connect(slot)
|
|
384
|
+
menu.addAction(action)
|
|
385
|
+
return action
|
|
386
|
+
|
|
373
387
|
def _show_context_menu(self, pos) -> None:
|
|
374
388
|
"""Show context menu."""
|
|
375
389
|
menu = QMenu(self)
|
|
376
390
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
391
|
+
undo = self._add_menu_action(menu, "Undo", self.undo, QKeySequence("Ctrl+Z"))
|
|
392
|
+
undo.setEnabled(self.document().isUndoAvailable())
|
|
393
|
+
redo = self._add_menu_action(menu, "Redo", self.redo, QKeySequence("Ctrl+Y"))
|
|
394
|
+
redo.setEnabled(self.document().isRedoAvailable())
|
|
381
395
|
menu.addSeparator()
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
396
|
+
self._add_menu_action(menu, "Cut", self.cut, QKeySequence("Ctrl+X"))
|
|
397
|
+
self._add_menu_action(menu, "Copy", self.copy, QKeySequence("Ctrl+C"))
|
|
398
|
+
self._add_menu_action(menu, "Paste", self.paste, QKeySequence("Ctrl+V"))
|
|
385
399
|
menu.addSeparator()
|
|
386
|
-
|
|
400
|
+
self._add_menu_action(menu, "Select All", self.selectAll, QKeySequence("Ctrl+A"))
|
|
387
401
|
menu.addSeparator()
|
|
388
|
-
|
|
402
|
+
self._add_menu_action(menu, "Find...", self.find_requested.emit, QKeySequence("Ctrl+F"))
|
|
389
403
|
|
|
390
404
|
menu.exec(self.mapToGlobal(pos))
|
|
391
405
|
|
|
@@ -399,16 +413,6 @@ class SQLEditor(QPlainTextEdit):
|
|
|
399
413
|
shortcut_exec_all = QShortcut(QKeySequence("Ctrl+F5"), self)
|
|
400
414
|
shortcut_exec_all.activated.connect(self.execute_all_requested.emit)
|
|
401
415
|
|
|
402
|
-
# Ctrl+Z - Undo
|
|
403
|
-
shortcut_undo = QShortcut(QKeySequence("Ctrl+Z"), self)
|
|
404
|
-
shortcut_undo.activated.connect(self.undo)
|
|
405
|
-
|
|
406
|
-
# Ctrl+Y or Ctrl+Shift+Z - Redo
|
|
407
|
-
shortcut_redo = QShortcut(QKeySequence("Ctrl+Y"), self)
|
|
408
|
-
shortcut_redo.activated.connect(self.redo)
|
|
409
|
-
shortcut_redo2 = QShortcut(QKeySequence("Ctrl+Shift+Z"), self)
|
|
410
|
-
shortcut_redo2.activated.connect(self.redo)
|
|
411
|
-
|
|
412
416
|
def update_theme(self) -> None:
|
|
413
417
|
"""Update colors when theme changes."""
|
|
414
418
|
self.highlighter.update_theme()
|
|
@@ -532,10 +536,20 @@ class ResultsTable(QTableWidget):
|
|
|
532
536
|
def _show_context_menu(self, pos) -> None:
|
|
533
537
|
"""Show context menu."""
|
|
534
538
|
menu = QMenu(self)
|
|
535
|
-
|
|
539
|
+
|
|
540
|
+
copy_act = QAction("Copy", menu)
|
|
541
|
+
copy_act.setShortcut(QKeySequence("Ctrl+C"))
|
|
542
|
+
copy_act.triggered.connect(self._copy_selection)
|
|
543
|
+
menu.addAction(copy_act)
|
|
544
|
+
|
|
536
545
|
menu.addAction("Copy with Headers", self._copy_with_headers)
|
|
537
546
|
menu.addSeparator()
|
|
538
|
-
|
|
547
|
+
|
|
548
|
+
sel_act = QAction("Select All", menu)
|
|
549
|
+
sel_act.setShortcut(QKeySequence("Ctrl+A"))
|
|
550
|
+
sel_act.triggered.connect(self.selectAll)
|
|
551
|
+
menu.addAction(sel_act)
|
|
552
|
+
|
|
539
553
|
menu.exec(self.mapToGlobal(pos))
|
|
540
554
|
|
|
541
555
|
def _copy_selection(self) -> None:
|
|
@@ -693,6 +707,8 @@ class SQLTab(QWidget):
|
|
|
693
707
|
self._original_values: dict = {} # row_index -> original row tuple
|
|
694
708
|
self._modified_cells: dict = {} # row_index -> {col_index: new_value}
|
|
695
709
|
self._loading_results = False # guard flag to ignore cellChanged during loads
|
|
710
|
+
self._in_script_mode = False
|
|
711
|
+
self._script_results = [] # result dicts for sub-tab field lookups
|
|
696
712
|
|
|
697
713
|
self._setup_ui()
|
|
698
714
|
self._connect_signals()
|
|
@@ -841,13 +857,19 @@ class SQLTab(QWidget):
|
|
|
841
857
|
results_layout.setSpacing(0)
|
|
842
858
|
|
|
843
859
|
# Results controls
|
|
844
|
-
|
|
845
|
-
results_layout.addWidget(
|
|
860
|
+
self._results_controls = self._create_results_controls()
|
|
861
|
+
results_layout.addWidget(self._results_controls)
|
|
846
862
|
|
|
847
863
|
# Results table
|
|
848
864
|
self.results_table = ResultsTable()
|
|
849
865
|
results_layout.addWidget(self.results_table)
|
|
850
866
|
|
|
867
|
+
# Script sub-tabs (hidden by default, shown in script mode)
|
|
868
|
+
self._script_sub_tabs = QTabWidget()
|
|
869
|
+
self._script_sub_tabs.setDocumentMode(True)
|
|
870
|
+
self._script_sub_tabs.hide()
|
|
871
|
+
results_layout.addWidget(self._script_sub_tabs)
|
|
872
|
+
|
|
851
873
|
# Status label
|
|
852
874
|
self.results_status = QLabel("No results")
|
|
853
875
|
self.results_status.setProperty("subheading", True)
|
|
@@ -1178,6 +1200,14 @@ class SQLTab(QWidget):
|
|
|
1178
1200
|
self.results_table.verticalHeader().setDefaultSectionSize(row_height)
|
|
1179
1201
|
self.fields_table.verticalHeader().setDefaultSectionSize(row_height)
|
|
1180
1202
|
|
|
1203
|
+
# Scale any visible script sub-tab tables
|
|
1204
|
+
if self._in_script_mode:
|
|
1205
|
+
for i in range(self._script_sub_tabs.count()):
|
|
1206
|
+
w = self._script_sub_tabs.widget(i)
|
|
1207
|
+
if isinstance(w, ResultsTable):
|
|
1208
|
+
w.scale_columns(scale)
|
|
1209
|
+
w.verticalHeader().setDefaultSectionSize(row_height)
|
|
1210
|
+
|
|
1181
1211
|
# Update stats font
|
|
1182
1212
|
font = self.stats_text.font()
|
|
1183
1213
|
font.setPointSize(size)
|
|
@@ -1219,6 +1249,93 @@ class SQLTab(QWidget):
|
|
|
1219
1249
|
self._script_worker.error.connect(self._on_query_error)
|
|
1220
1250
|
self._script_worker.start()
|
|
1221
1251
|
|
|
1252
|
+
def _enter_script_mode(self, results: List) -> None:
|
|
1253
|
+
"""Switch to script sub-tab layout."""
|
|
1254
|
+
self._in_script_mode = True
|
|
1255
|
+
self._script_results = results
|
|
1256
|
+
|
|
1257
|
+
# Hide single-query controls and table
|
|
1258
|
+
self._results_controls.hide()
|
|
1259
|
+
self.results_table.hide()
|
|
1260
|
+
|
|
1261
|
+
# Clear previous sub-tabs
|
|
1262
|
+
try:
|
|
1263
|
+
self._script_sub_tabs.currentChanged.disconnect(self._on_script_sub_tab_changed)
|
|
1264
|
+
except TypeError:
|
|
1265
|
+
pass
|
|
1266
|
+
while self._script_sub_tabs.count():
|
|
1267
|
+
w = self._script_sub_tabs.widget(0)
|
|
1268
|
+
self._script_sub_tabs.removeTab(0)
|
|
1269
|
+
w.deleteLater()
|
|
1270
|
+
|
|
1271
|
+
# Summary tab
|
|
1272
|
+
summary_table = ResultsTable()
|
|
1273
|
+
summary_table.setSortingEnabled(False)
|
|
1274
|
+
summary_table.setColumnCount(4)
|
|
1275
|
+
summary_table.setHorizontalHeaderLabels(["#", "SQL", "Result", "Time"])
|
|
1276
|
+
summary_table.setRowCount(len(results))
|
|
1277
|
+
for i, r in enumerate(results):
|
|
1278
|
+
summary_table.setItem(i, 0, QTableWidgetItem(str(r["stmt"])))
|
|
1279
|
+
summary_table.setItem(i, 1, QTableWidgetItem(r["sql"]))
|
|
1280
|
+
status_item = QTableWidgetItem(r["status"])
|
|
1281
|
+
if not r["success"]:
|
|
1282
|
+
status_item.setForeground(QColor(255, 80, 80))
|
|
1283
|
+
summary_table.setItem(i, 2, status_item)
|
|
1284
|
+
summary_table.setItem(i, 3, QTableWidgetItem(f"{r['time']:.3f}s"))
|
|
1285
|
+
summary_table.resizeColumnsToContents()
|
|
1286
|
+
for col in range(summary_table.columnCount()):
|
|
1287
|
+
if summary_table.columnWidth(col) > 400:
|
|
1288
|
+
summary_table.setColumnWidth(col, 400)
|
|
1289
|
+
summary_table.setSortingEnabled(True)
|
|
1290
|
+
self._script_sub_tabs.addTab(summary_table, "Summary")
|
|
1291
|
+
|
|
1292
|
+
# Per-SELECT result tabs
|
|
1293
|
+
for r in results:
|
|
1294
|
+
if r["rows"] is not None and r["description"] is not None:
|
|
1295
|
+
table = ResultsTable()
|
|
1296
|
+
table.load_results(r["rows"], r["description"])
|
|
1297
|
+
label = f"#{r['stmt']} {r['sql'][:40]}"
|
|
1298
|
+
self._script_sub_tabs.addTab(table, label)
|
|
1299
|
+
|
|
1300
|
+
# Connect tab-changed signal for Fields updates
|
|
1301
|
+
self._script_sub_tabs.currentChanged.connect(self._on_script_sub_tab_changed)
|
|
1302
|
+
|
|
1303
|
+
self._script_sub_tabs.show()
|
|
1304
|
+
|
|
1305
|
+
def _exit_script_mode(self) -> None:
|
|
1306
|
+
"""Restore single-query layout."""
|
|
1307
|
+
if not self._in_script_mode:
|
|
1308
|
+
return
|
|
1309
|
+
self._in_script_mode = False
|
|
1310
|
+
self._script_results = []
|
|
1311
|
+
|
|
1312
|
+
# Clear and hide sub-tabs
|
|
1313
|
+
self._script_sub_tabs.hide()
|
|
1314
|
+
while self._script_sub_tabs.count():
|
|
1315
|
+
w = self._script_sub_tabs.widget(0)
|
|
1316
|
+
self._script_sub_tabs.removeTab(0)
|
|
1317
|
+
w.deleteLater()
|
|
1318
|
+
|
|
1319
|
+
# Restore single-query widgets
|
|
1320
|
+
self._results_controls.show()
|
|
1321
|
+
self.results_table.show()
|
|
1322
|
+
|
|
1323
|
+
def _on_script_sub_tab_changed(self, index: int) -> None:
|
|
1324
|
+
"""Update Fields tab when a script sub-tab is selected."""
|
|
1325
|
+
if index == 0:
|
|
1326
|
+
# Summary tab — clear fields
|
|
1327
|
+
self._update_fields(None)
|
|
1328
|
+
return
|
|
1329
|
+
# Map sub-tab index to the result that has rows
|
|
1330
|
+
select_idx = 0
|
|
1331
|
+
for r in self._script_results:
|
|
1332
|
+
if r["rows"] is not None and r["description"] is not None:
|
|
1333
|
+
select_idx += 1
|
|
1334
|
+
if select_idx == index:
|
|
1335
|
+
self._update_fields(r["description"])
|
|
1336
|
+
return
|
|
1337
|
+
self._update_fields(None)
|
|
1338
|
+
|
|
1222
1339
|
def _on_script_finished(self, results: List, total_time: float) -> None:
|
|
1223
1340
|
"""Handle script execution completion."""
|
|
1224
1341
|
self._reset_buttons()
|
|
@@ -1237,29 +1354,8 @@ class SQLTab(QWidget):
|
|
|
1237
1354
|
except Exception:
|
|
1238
1355
|
pass
|
|
1239
1356
|
|
|
1240
|
-
#
|
|
1241
|
-
self.
|
|
1242
|
-
self.results_table.clear()
|
|
1243
|
-
self.results_table.setSortingEnabled(False)
|
|
1244
|
-
self.results_table.setColumnCount(4)
|
|
1245
|
-
self.results_table.setHorizontalHeaderLabels(["#", "SQL", "Result", "Time"])
|
|
1246
|
-
self.results_table.setRowCount(len(results))
|
|
1247
|
-
|
|
1248
|
-
for i, r in enumerate(results):
|
|
1249
|
-
self.results_table.setItem(i, 0, QTableWidgetItem(str(r["stmt"])))
|
|
1250
|
-
self.results_table.setItem(i, 1, QTableWidgetItem(r["sql"]))
|
|
1251
|
-
status_item = QTableWidgetItem(r["status"])
|
|
1252
|
-
if not r["success"]:
|
|
1253
|
-
status_item.setForeground(QColor(255, 80, 80))
|
|
1254
|
-
self.results_table.setItem(i, 2, status_item)
|
|
1255
|
-
self.results_table.setItem(i, 3, QTableWidgetItem(f"{r['time']:.3f}s"))
|
|
1256
|
-
|
|
1257
|
-
self.results_table.resizeColumnsToContents()
|
|
1258
|
-
for col in range(self.results_table.columnCount()):
|
|
1259
|
-
if self.results_table.columnWidth(col) > 400:
|
|
1260
|
-
self.results_table.setColumnWidth(col, 400)
|
|
1261
|
-
self.results_table.setSortingEnabled(True)
|
|
1262
|
-
self._loading_results = False
|
|
1357
|
+
# Build script sub-tabs (summary + per-SELECT result tabs)
|
|
1358
|
+
self._enter_script_mode(results)
|
|
1263
1359
|
|
|
1264
1360
|
# Update statistics
|
|
1265
1361
|
success = sum(1 for r in results if r["success"])
|
|
@@ -1368,18 +1464,20 @@ class SQLTab(QWidget):
|
|
|
1368
1464
|
|
|
1369
1465
|
def _on_query_finished(self, rows: List, description: Any,
|
|
1370
1466
|
exec_time: float, fetch_time: float,
|
|
1371
|
-
total_rows: int = 0) -> None:
|
|
1467
|
+
total_rows: int = 0, rowcount: int = 0) -> None:
|
|
1372
1468
|
"""Handle query completion."""
|
|
1469
|
+
self._exit_script_mode()
|
|
1373
1470
|
self._reset_buttons()
|
|
1374
1471
|
|
|
1375
1472
|
# Log to database
|
|
1376
1473
|
try:
|
|
1377
1474
|
db = _get_db()
|
|
1475
|
+
log_row_count = len(rows) if rows else rowcount
|
|
1378
1476
|
db.log_query(
|
|
1379
1477
|
self.connection_name,
|
|
1380
1478
|
self._last_sql,
|
|
1381
1479
|
duration=exec_time + fetch_time,
|
|
1382
|
-
row_count=
|
|
1480
|
+
row_count=log_row_count,
|
|
1383
1481
|
status="success"
|
|
1384
1482
|
)
|
|
1385
1483
|
except Exception as e:
|
|
@@ -1415,6 +1513,18 @@ class SQLTab(QWidget):
|
|
|
1415
1513
|
f"Showing {start:,}-{end:,} of {self._total_rows:,} row(s) "
|
|
1416
1514
|
f"(Page {self._current_page} of {total_pages}){edit_status}"
|
|
1417
1515
|
)
|
|
1516
|
+
elif description is None and rowcount >= 0:
|
|
1517
|
+
# Non-SELECT statement (UPDATE/DELETE/INSERT)
|
|
1518
|
+
sql_upper = self._last_sql.strip().upper()
|
|
1519
|
+
if sql_upper.startswith("INSERT"):
|
|
1520
|
+
status = f"{rowcount} row(s) inserted"
|
|
1521
|
+
elif sql_upper.startswith("UPDATE"):
|
|
1522
|
+
status = f"{rowcount} row(s) updated"
|
|
1523
|
+
elif sql_upper.startswith("DELETE"):
|
|
1524
|
+
status = f"{rowcount} row(s) deleted"
|
|
1525
|
+
else:
|
|
1526
|
+
status = f"{rowcount} row(s) affected"
|
|
1527
|
+
self.results_status.setText(status)
|
|
1418
1528
|
else:
|
|
1419
1529
|
self.results_status.setText("No results")
|
|
1420
1530
|
|
|
@@ -1559,8 +1669,9 @@ class SQLTab(QWidget):
|
|
|
1559
1669
|
|
|
1560
1670
|
def _on_page_loaded(self, rows: List, description: Any,
|
|
1561
1671
|
exec_time: float, fetch_time: float,
|
|
1562
|
-
total_rows: int = 0) -> None:
|
|
1672
|
+
total_rows: int = 0, rowcount: int = 0) -> None:
|
|
1563
1673
|
"""Handle page load completion."""
|
|
1674
|
+
self._exit_script_mode()
|
|
1564
1675
|
self._reset_buttons()
|
|
1565
1676
|
|
|
1566
1677
|
self._loading_results = True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|