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.
Files changed (43) hide show
  1. {sqlbench-0.1.60 → sqlbench-0.1.62}/PKG-INFO +1 -1
  2. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/main_window.py +20 -24
  3. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tabs/sql_tab.py +163 -52
  4. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/version.py +1 -1
  5. {sqlbench-0.1.60 → sqlbench-0.1.62}/.github/workflows/pypi-publish.yml +0 -0
  6. {sqlbench-0.1.60 → sqlbench-0.1.62}/.gitignore +0 -0
  7. {sqlbench-0.1.60 → sqlbench-0.1.62}/CLAUDE.md +0 -0
  8. {sqlbench-0.1.60 → sqlbench-0.1.62}/Makefile +0 -0
  9. {sqlbench-0.1.60 → sqlbench-0.1.62}/PYQT_MIGRATION_FEATURES.md +0 -0
  10. {sqlbench-0.1.60 → sqlbench-0.1.62}/README.md +0 -0
  11. {sqlbench-0.1.60 → sqlbench-0.1.62}/pyproject.toml +0 -0
  12. {sqlbench-0.1.60 → sqlbench-0.1.62}/requirements.txt +0 -0
  13. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/__init__.py +0 -0
  14. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/__main__.py +0 -0
  15. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/adapters.py +0 -0
  16. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/app.py +0 -0
  17. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/database.py +0 -0
  18. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/dialogs/__init__.py +0 -0
  19. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/dialogs/connection_dialog.py +0 -0
  20. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/dialogs/regex_builder_dialog.py +0 -0
  21. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/launcher.py +0 -0
  22. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/__init__.py +0 -0
  23. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/connection_tree.py +0 -0
  24. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/__init__.py +0 -0
  25. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/connection_dialog.py +0 -0
  26. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/query_manager_dialog.py +0 -0
  27. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/record_viewer_dialog.py +0 -0
  28. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/regex_builder_dialog.py +0 -0
  29. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/dialogs/settings_dialog.py +0 -0
  30. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/icons.py +0 -0
  31. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/syntax.py +0 -0
  32. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tab_widget.py +0 -0
  33. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tabs/__init__.py +0 -0
  34. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/tabs/spool_tab.py +0 -0
  35. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/qt/theme.py +0 -0
  36. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_ibmi.png +0 -0
  37. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_mysql.png +0 -0
  38. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_postgresql.png +0 -0
  39. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/db_unknown.png +0 -0
  40. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/resources/sqlbench.png +0 -0
  41. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/tabs/__init__.py +0 -0
  42. {sqlbench-0.1.60 → sqlbench-0.1.62}/sqlbench/tabs/spool_tab.py +0 -0
  43. {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.60
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, QSettings, QTimer, pyqtSignal
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 geometry and state."""
145
- settings = QSettings("SQLBench", "SQLBench")
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
- # Restore geometry
148
- geometry = settings.value("geometry")
149
- if geometry:
150
- self.restoreGeometry(geometry)
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 geometry and state."""
213
- settings = QSettings("SQLBench", "SQLBench")
214
- settings.setValue("geometry", self.saveGeometry())
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
- self.row_count.emit(cursor.rowcount)
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
- undo_action = menu.addAction("Undo", self.undo, QKeySequence("Ctrl+Z"))
378
- undo_action.setEnabled(self.document().isUndoAvailable())
379
- redo_action = menu.addAction("Redo", self.redo, QKeySequence("Ctrl+Y"))
380
- redo_action.setEnabled(self.document().isRedoAvailable())
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
- menu.addAction("Cut", self.cut, QKeySequence("Ctrl+X"))
383
- menu.addAction("Copy", self.copy, QKeySequence("Ctrl+C"))
384
- menu.addAction("Paste", self.paste, QKeySequence("Ctrl+V"))
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
- menu.addAction("Select All", self.selectAll, QKeySequence("Ctrl+A"))
400
+ self._add_menu_action(menu, "Select All", self.selectAll, QKeySequence("Ctrl+A"))
387
401
  menu.addSeparator()
388
- menu.addAction("Find...", self.find_requested.emit, QKeySequence("Ctrl+F"))
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
- menu.addAction("Copy", self._copy_selection, QKeySequence("Ctrl+C"))
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
- menu.addAction("Select All", self.selectAll, QKeySequence("Ctrl+A"))
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
- controls = self._create_results_controls()
845
- results_layout.addWidget(controls)
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
- # Display results in table with script columns
1241
- self._loading_results = True
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=len(rows) if rows else 0,
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
@@ -4,7 +4,7 @@ import threading
4
4
  import urllib.request
5
5
  import json
6
6
 
7
- __version__ = "0.1.60"
7
+ __version__ = "0.1.62"
8
8
 
9
9
 
10
10
  def get_installed_version():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes