sqlbench 0.1.56__tar.gz → 0.1.59__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.56 → sqlbench-0.1.59}/CLAUDE.md +2 -1
  2. {sqlbench-0.1.56 → sqlbench-0.1.59}/PKG-INFO +1 -1
  3. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/connection_tree.py +24 -67
  4. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/dialogs/connection_dialog.py +49 -15
  5. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/main_window.py +19 -10
  6. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/tabs/sql_tab.py +53 -1
  7. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/version.py +1 -1
  8. {sqlbench-0.1.56 → sqlbench-0.1.59}/.github/workflows/pypi-publish.yml +0 -0
  9. {sqlbench-0.1.56 → sqlbench-0.1.59}/.gitignore +0 -0
  10. {sqlbench-0.1.56 → sqlbench-0.1.59}/Makefile +0 -0
  11. {sqlbench-0.1.56 → sqlbench-0.1.59}/PYQT_MIGRATION_FEATURES.md +0 -0
  12. {sqlbench-0.1.56 → sqlbench-0.1.59}/README.md +0 -0
  13. {sqlbench-0.1.56 → sqlbench-0.1.59}/pyproject.toml +0 -0
  14. {sqlbench-0.1.56 → sqlbench-0.1.59}/requirements.txt +0 -0
  15. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/__init__.py +0 -0
  16. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/__main__.py +0 -0
  17. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/adapters.py +0 -0
  18. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/app.py +0 -0
  19. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/database.py +0 -0
  20. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/dialogs/__init__.py +0 -0
  21. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/dialogs/connection_dialog.py +0 -0
  22. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/dialogs/regex_builder_dialog.py +0 -0
  23. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/launcher.py +0 -0
  24. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/__init__.py +0 -0
  25. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/dialogs/__init__.py +0 -0
  26. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/dialogs/query_manager_dialog.py +0 -0
  27. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/dialogs/record_viewer_dialog.py +0 -0
  28. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/dialogs/regex_builder_dialog.py +0 -0
  29. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/dialogs/settings_dialog.py +0 -0
  30. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/icons.py +0 -0
  31. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/syntax.py +0 -0
  32. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/tab_widget.py +0 -0
  33. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/tabs/__init__.py +0 -0
  34. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/tabs/spool_tab.py +0 -0
  35. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/qt/theme.py +0 -0
  36. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/resources/db_ibmi.png +0 -0
  37. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/resources/db_mysql.png +0 -0
  38. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/resources/db_postgresql.png +0 -0
  39. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/resources/db_unknown.png +0 -0
  40. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/resources/sqlbench.png +0 -0
  41. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/tabs/__init__.py +0 -0
  42. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/tabs/spool_tab.py +0 -0
  43. {sqlbench-0.1.56 → sqlbench-0.1.59}/sqlbench/tabs/sql_tab.py +0 -0
@@ -1,4 +1,5 @@
1
1
  # Claude Code Instructions
2
2
 
3
- ## Git Commits
3
+ ## Git
4
+ - NEVER run git commands (add, commit, push, tag, etc.) unless the user explicitly asks
4
5
  - NEVER mention Claude, AI, or include Co-Authored-By lines in git commit messages
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlbench
3
- Version: 0.1.56
3
+ Version: 0.1.59
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
@@ -38,8 +38,7 @@ class ConnectionTreeWidget(QWidget):
38
38
  show_rows_requested = pyqtSignal(str, str, str) # connection, schema, table
39
39
  edit_connection_requested = pyqtSignal(str) # connection_name
40
40
  new_connection_requested = pyqtSignal()
41
- connect_requested = pyqtSignal(str) # connection_name
42
- disconnect_requested = pyqtSignal(str) # connection_name
41
+ connect_requested = pyqtSignal(str) # connection_name (auto-activate on expand)
43
42
 
44
43
  def __init__(self, parent: Optional[QWidget] = None):
45
44
  super().__init__(parent)
@@ -130,12 +129,6 @@ class ConnectionTreeWidget(QWidget):
130
129
  """Set up right-click context menu."""
131
130
  self.context_menu = QMenu(self)
132
131
 
133
- self.action_connect = QAction("Connect", self)
134
- self.action_connect.triggered.connect(self._connect_selected)
135
-
136
- self.action_disconnect = QAction("Disconnect", self)
137
- self.action_disconnect.triggered.connect(self._disconnect_selected)
138
-
139
132
  self.action_new_sql = QAction("New SQL", self)
140
133
  self.action_new_sql.triggered.connect(self._new_sql_for_selected)
141
134
 
@@ -173,7 +166,9 @@ class ConnectionTreeWidget(QWidget):
173
166
  self._connections_info[name] = conn
174
167
  self._connected[name] = False
175
168
 
176
- item = QTreeWidgetItem([name])
169
+ host = conn.get('host', '')
170
+ display_name = f"{name} - {host}" if host else name
171
+ item = QTreeWidgetItem([display_name])
177
172
  item.setData(0, Qt.ItemDataRole.UserRole, {
178
173
  'type': 'connection',
179
174
  'name': name,
@@ -193,29 +188,31 @@ class ConnectionTreeWidget(QWidget):
193
188
  return item
194
189
 
195
190
  def set_connected(self, connection_name: str, connected: bool) -> None:
196
- """Update connection status."""
191
+ """Update connection activation status."""
197
192
  self._connected[connection_name] = connected
198
193
 
199
- # Update item text
200
- for i in range(self.tree.topLevelItemCount()):
201
- item = self.tree.topLevelItem(i)
202
- data = item.data(0, Qt.ItemDataRole.UserRole)
203
- if data and data.get('name') == connection_name:
204
- if connected:
205
- item.setText(0, f"{connection_name} (connected)")
206
- # Expand if this was pending from double-click
207
- if hasattr(self, '_pending_expand') and self._pending_expand == item:
208
- item.setExpanded(True)
209
- self._pending_expand = None
210
- else:
211
- item.setText(0, connection_name)
212
- # Clear children
194
+ if not connected:
195
+ # Clear cached schemas
196
+ for i in range(self.tree.topLevelItemCount()):
197
+ item = self.tree.topLevelItem(i)
198
+ data = item.data(0, Qt.ItemDataRole.UserRole)
199
+ if data and data.get('name') == connection_name:
213
200
  item.takeChildren()
214
201
  placeholder = QTreeWidgetItem(["Loading..."])
215
202
  placeholder.setData(0, Qt.ItemDataRole.UserRole, {'type': 'placeholder'})
216
203
  item.addChild(placeholder)
217
204
  self._loaded_schemas[connection_name] = False
218
- break
205
+ break
206
+ else:
207
+ # Expand if pending from expand attempt
208
+ if self._pending_expand:
209
+ for i in range(self.tree.topLevelItemCount()):
210
+ item = self.tree.topLevelItem(i)
211
+ data = item.data(0, Qt.ItemDataRole.UserRole)
212
+ if data and data.get('name') == connection_name and item == self._pending_expand:
213
+ item.setExpanded(True)
214
+ self._pending_expand = None
215
+ break
219
216
 
220
217
  def _on_item_expanded(self, item: QTreeWidgetItem) -> None:
221
218
  """Handle item expansion - load children if needed."""
@@ -541,14 +538,7 @@ class ConnectionTreeWidget(QWidget):
541
538
 
542
539
  if item_type == 'connection':
543
540
  connection_name = data.get('name')
544
- is_connected = self._connected.get(connection_name, False)
545
-
546
- if is_connected:
547
- menu.addAction(self.action_disconnect)
548
- else:
549
- menu.addAction(self.action_connect)
550
541
 
551
- menu.addSeparator()
552
542
  menu.addAction(self.action_new_sql)
553
543
 
554
544
  # Check if IBM i for spool files
@@ -576,16 +566,7 @@ class ConnectionTreeWidget(QWidget):
576
566
 
577
567
  item_type = data.get('type')
578
568
 
579
- if item_type == 'connection':
580
- connection_name = data.get('name')
581
- if not self._connected.get(connection_name):
582
- self.connect_requested.emit(connection_name)
583
- # Store item to expand after connect succeeds
584
- self._pending_expand = item
585
- else:
586
- item.setExpanded(not item.isExpanded())
587
-
588
- elif item_type in ('schema', 'table'):
569
+ if item_type in ('connection', 'schema', 'table'):
589
570
  item.setExpanded(not item.isExpanded())
590
571
 
591
572
  def _on_current_changed(self, current: QTreeWidgetItem,
@@ -609,22 +590,6 @@ class ConnectionTreeWidget(QWidget):
609
590
  else:
610
591
  return data.get('connection')
611
592
 
612
- def _connect_selected(self) -> None:
613
- """Connect the selected connection."""
614
- item = self.tree.currentItem()
615
- if item:
616
- connection_name = self._get_connection_for_item(item)
617
- if connection_name:
618
- self.connect_requested.emit(connection_name)
619
-
620
- def _disconnect_selected(self) -> None:
621
- """Disconnect the selected connection."""
622
- item = self.tree.currentItem()
623
- if item:
624
- connection_name = self._get_connection_for_item(item)
625
- if connection_name:
626
- self.disconnect_requested.emit(connection_name)
627
-
628
593
  def _new_sql_for_selected(self) -> None:
629
594
  """Create new SQL tab for selected connection."""
630
595
  item = self.tree.currentItem()
@@ -707,14 +672,6 @@ class ConnectionTreeWidget(QWidget):
707
672
  elif item.parent():
708
673
  self.tree.setCurrentItem(item.parent())
709
674
  elif key in (Qt.Key.Key_Return, Qt.Key.Key_Space):
710
- data = item.data(0, Qt.ItemDataRole.UserRole)
711
- if data:
712
- if data.get('type') == 'connection':
713
- if not self._connected.get(data.get('name')):
714
- self.connect_requested.emit(data.get('name'))
715
- else:
716
- item.setExpanded(not item.isExpanded())
717
- else:
718
- item.setExpanded(not item.isExpanded())
675
+ item.setExpanded(not item.isExpanded())
719
676
  else:
720
677
  super().keyPressEvent(event)
@@ -91,12 +91,14 @@ class ConnectionDialog(QDialog):
91
91
  self.btn_new = QPushButton("+")
92
92
  self.btn_new.setFixedWidth(32)
93
93
  self.btn_new.setToolTip("New connection")
94
+ self.btn_new.setAutoDefault(False)
94
95
  self.btn_new.clicked.connect(self._new_connection)
95
96
  btn_layout.addWidget(self.btn_new)
96
97
 
97
98
  self.btn_delete = QPushButton("−")
98
99
  self.btn_delete.setFixedWidth(32)
99
100
  self.btn_delete.setToolTip("Delete connection")
101
+ self.btn_delete.setAutoDefault(False)
100
102
  self.btn_delete.clicked.connect(self._delete_connection)
101
103
  btn_layout.addWidget(self.btn_delete)
102
104
 
@@ -170,6 +172,11 @@ class ConnectionDialog(QDialog):
170
172
  self.txt_password.setPlaceholderText("password")
171
173
  form_layout.addRow("Password:", self.txt_password)
172
174
 
175
+ # Enter key in any field triggers Save
176
+ for field in (self.txt_name, self.txt_host, self.txt_port,
177
+ self.txt_database, self.txt_user, self.txt_password):
178
+ field.returnPressed.connect(self._save_connection)
179
+
173
180
  # Options
174
181
  options_layout = QVBoxLayout()
175
182
 
@@ -194,6 +201,7 @@ class ConnectionDialog(QDialog):
194
201
  btn_layout = QHBoxLayout()
195
202
 
196
203
  self.btn_test = QPushButton("Test Connection")
204
+ self.btn_test.setAutoDefault(False)
197
205
  self.btn_test.clicked.connect(self._test_connection)
198
206
  btn_layout.addWidget(self.btn_test)
199
207
 
@@ -201,10 +209,12 @@ class ConnectionDialog(QDialog):
201
209
 
202
210
  self.btn_save = QPushButton("Save")
203
211
  self.btn_save.setProperty("primary", True)
212
+ self.btn_save.setDefault(True)
204
213
  self.btn_save.clicked.connect(self._save_connection)
205
214
  btn_layout.addWidget(self.btn_save)
206
215
 
207
216
  self.btn_close = QPushButton("Close")
217
+ self.btn_close.setAutoDefault(False)
208
218
  self.btn_close.clicked.connect(self.accept)
209
219
  btn_layout.addWidget(self.btn_close)
210
220
 
@@ -251,7 +261,7 @@ class ConnectionDialog(QDialog):
251
261
 
252
262
  self.txt_name.setText(conn.get('name', ''))
253
263
  self.txt_host.setText(conn.get('host', ''))
254
- self.txt_port.setText(str(conn.get('port', '')))
264
+ self.txt_port.setText(str(conn['port']) if conn.get('port') else '')
255
265
  self.txt_database.setText(conn.get('database', ''))
256
266
  self.txt_user.setText(conn.get('user', ''))
257
267
  self.txt_password.setText(conn.get('password', ''))
@@ -284,15 +294,20 @@ class ConnectionDialog(QDialog):
284
294
  def _on_type_changed(self) -> None:
285
295
  """Handle database type change."""
286
296
  db_type = self.cmb_type.currentData()
297
+ adapter_cls = ADAPTERS.get(db_type)
287
298
 
288
- # IBM i doesn't use port/database
289
- is_ibmi = db_type == "ibmi"
299
+ has_port = adapter_cls.default_port is not None if adapter_cls else True
300
+ has_database = adapter_cls.requires_database if adapter_cls else True
290
301
 
291
302
  # Hide/show port and database fields with their labels
292
- self.txt_port.setVisible(not is_ibmi)
293
- self.txt_database.setVisible(not is_ibmi)
294
- self.lbl_port.setVisible(not is_ibmi)
295
- self.lbl_database.setVisible(not is_ibmi)
303
+ self.txt_port.setVisible(has_port)
304
+ self.lbl_port.setVisible(has_port)
305
+ self.txt_database.setVisible(has_database)
306
+ self.lbl_database.setVisible(has_database)
307
+
308
+ # Pre-fill default port if empty
309
+ if has_port and not self.txt_port.text().strip() and adapter_cls:
310
+ self.txt_port.setText(str(adapter_cls.default_port))
296
311
 
297
312
  # Show install button if driver not available
298
313
  adapters = get_available_adapters()
@@ -303,6 +318,7 @@ class ConnectionDialog(QDialog):
303
318
  import subprocess
304
319
  import sys
305
320
  import threading
321
+ from PyQt6.QtCore import QTimer
306
322
 
307
323
  db_type = self.cmb_type.currentData()
308
324
  adapter_cls = ADAPTERS.get(db_type)
@@ -317,23 +333,34 @@ class ConnectionDialog(QDialog):
317
333
 
318
334
  self.btn_install_driver.setEnabled(False)
319
335
  self.btn_install_driver.setText("Installing...")
336
+ self._install_result = None # (success: bool, error: str or None)
320
337
 
321
338
  def do_install():
322
339
  try:
323
340
  result = subprocess.run(
324
341
  [sys.executable, "-m", "pip", "install", pkg],
325
- capture_output=True, text=True)
326
- from PyQt6.QtCore import QTimer
342
+ capture_output=True, text=True,
343
+ stdin=subprocess.DEVNULL, timeout=120)
327
344
  if result.returncode == 0:
328
- QTimer.singleShot(0, self._on_driver_installed)
345
+ self._install_result = (True, None)
329
346
  else:
330
347
  error = result.stderr or result.stdout or "Unknown error"
331
- QTimer.singleShot(0, lambda: self._on_driver_install_failed(error))
348
+ self._install_result = (False, error)
332
349
  except Exception as e:
333
- from PyQt6.QtCore import QTimer
334
- QTimer.singleShot(0, lambda: self._on_driver_install_failed(str(e)))
350
+ self._install_result = (False, str(e))
351
+
352
+ def poll_result():
353
+ if self._install_result is None:
354
+ QTimer.singleShot(500, poll_result)
355
+ return
356
+ success, error = self._install_result
357
+ if success:
358
+ self._on_driver_installed()
359
+ else:
360
+ self._on_driver_install_failed(error)
335
361
 
336
362
  threading.Thread(target=do_install, daemon=True).start()
363
+ QTimer.singleShot(2000, poll_result)
337
364
 
338
365
  def _on_driver_installed(self) -> None:
339
366
  """Handle successful driver installation."""
@@ -429,13 +456,20 @@ class ConnectionDialog(QDialog):
429
456
  return
430
457
 
431
458
  db_type = self.cmb_type.currentData()
432
- port = self.txt_port.text()
459
+ port = self.txt_port.text().strip()
460
+
461
+ # Use adapter default port if blank
462
+ if not port or not port.isdigit():
463
+ adapter_cls = ADAPTERS.get(db_type)
464
+ port = adapter_cls.default_port if adapter_cls else None
465
+ else:
466
+ port = int(port)
433
467
 
434
468
  conn_data = {
435
469
  'name': name,
436
470
  'db_type': db_type,
437
471
  'host': self.txt_host.text(),
438
- 'port': int(port) if port else None,
472
+ 'port': port,
439
473
  'database': self.txt_database.text(),
440
474
  'user': self.txt_user.text(),
441
475
  'password': self.txt_password.text(),
@@ -35,7 +35,8 @@ class MainWindow(QMainWindow):
35
35
  def __init__(self):
36
36
  super().__init__()
37
37
 
38
- self.setWindowTitle("SQLBench")
38
+ from ..version import __version__
39
+ self.setWindowTitle(f"SQLBench v{__version__}")
39
40
  self.setMinimumSize(1024, 600)
40
41
 
41
42
  # Load theme preference
@@ -114,7 +115,6 @@ class MainWindow(QMainWindow):
114
115
  self.connection_tree = ConnectionTreeWidget()
115
116
  self.connection_tree.connection_selected.connect(self._on_connection_selected)
116
117
  self.connection_tree.connect_requested.connect(self._on_connect_requested)
117
- self.connection_tree.disconnect_requested.connect(self._on_disconnect_requested)
118
118
  self.connection_tree.new_sql_requested.connect(self._on_new_sql_tab)
119
119
  self.connection_tree.new_spool_requested.connect(self._on_new_spool_tab)
120
120
  self.connection_tree.show_rows_requested.connect(self._on_show_rows)
@@ -315,8 +315,17 @@ class MainWindow(QMainWindow):
315
315
  from .dialogs.settings_dialog import SettingsDialog
316
316
  dialog = SettingsDialog(self)
317
317
  if dialog.exec():
318
- # Apply settings
319
- pass
318
+ self._apply_font_size()
319
+
320
+ def _apply_font_size(self) -> None:
321
+ """Apply current font size setting to all open tabs."""
322
+ from .tabs.sql_tab import SQLTab
323
+ size = int(get_setting("font_size", "13"))
324
+ for i in range(self.tab_container.count()):
325
+ tab = self.tab_container.widget(i)
326
+ if isinstance(tab, SQLTab):
327
+ tab.set_font_size(size)
328
+ self.status_bar.showMessage(f"Font size set to {size}", 3000)
320
329
 
321
330
  def _reset_layout(self) -> None:
322
331
  """Reset window layout to defaults."""
@@ -331,6 +340,7 @@ class MainWindow(QMainWindow):
331
340
  )
332
341
 
333
342
  set_setting("font_size", "13")
343
+ self._apply_font_size()
334
344
  self.status_bar.showMessage("Layout reset to defaults", 3000)
335
345
 
336
346
  def _restore_tab_layouts(self) -> None:
@@ -373,13 +383,9 @@ class MainWindow(QMainWindow):
373
383
  self.status_bar.showMessage(f"Selected: {connection_name}")
374
384
 
375
385
  def _on_connect_requested(self, connection_name: str) -> None:
376
- """Handle request to connect to a database."""
386
+ """Handle request to activate a connection (verify credentials)."""
377
387
  self._connect(connection_name)
378
388
 
379
- def _on_disconnect_requested(self, connection_name: str) -> None:
380
- """Handle request to disconnect from a database."""
381
- self.disconnect(connection_name)
382
-
383
389
  def _on_new_sql_tab(self, connection_name: str) -> None:
384
390
  """Create new SQL tab for connection."""
385
391
  from .tabs.sql_tab import SQLTab
@@ -605,12 +611,15 @@ class MainWindow(QMainWindow):
605
611
  try:
606
612
  result = subprocess.run(
607
613
  ["pipx", "upgrade", "sqlbench"],
608
- capture_output=True, text=True)
614
+ capture_output=True, text=True,
615
+ stdin=subprocess.DEVNULL, timeout=120)
609
616
  if result.returncode == 0:
610
617
  self._upgrade_result = (True, "SQLBench has been upgraded.\nPlease restart to use the new version.")
611
618
  else:
612
619
  error = result.stderr or result.stdout or "Unknown error"
613
620
  self._upgrade_result = (False, f"Failed to upgrade:\n{error}")
621
+ except subprocess.TimeoutExpired:
622
+ self._upgrade_result = (False, "Upgrade timed out. Please upgrade manually:\n\npipx upgrade sqlbench")
614
623
  except FileNotFoundError:
615
624
  self._upgrade_result = (False, "pipx not found. Please upgrade manually:\n\npipx upgrade sqlbench")
616
625
  except Exception as e:
@@ -44,6 +44,7 @@ from PyQt6.QtWidgets import (
44
44
  QMessageBox,
45
45
  QHeaderView,
46
46
  QAbstractItemView,
47
+ QAbstractItemDelegate,
47
48
  QApplication,
48
49
  QFrame,
49
50
  )
@@ -498,10 +499,36 @@ class ResultsTable(QTableWidget):
498
499
  self.horizontalHeader().sectionDoubleClicked.connect(self._auto_fit_column)
499
500
  self.verticalHeader().setDefaultSectionSize(24)
500
501
 
502
+ # Column width tracking for font scaling
503
+ self._base_widths: dict = {} # col_index -> base width at scale 1.0
504
+ self._pk_indices: List[int] = [] # set by SQLTab for tab-to-next-cell
505
+
501
506
  # Context menu
502
507
  self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
503
508
  self.customContextMenuRequested.connect(self._show_context_menu)
504
509
 
510
+ def closeEditor(self, editor, hint) -> None:
511
+ """Override to skip PK columns when Tab is pressed during editing."""
512
+ if hint == QAbstractItemDelegate.EndEditHint.EditNextItem and self._pk_indices:
513
+ # Tab was pressed — commit and move to next non-PK column
514
+ super().closeEditor(editor, QAbstractItemDelegate.EndEditHint.NoHint)
515
+ row = self.currentRow()
516
+ col = self.currentColumn()
517
+ next_col = col + 1
518
+ while next_col < self.columnCount():
519
+ if next_col not in self._pk_indices:
520
+ self.setCurrentCell(row, next_col)
521
+ self.editItem(self.item(row, next_col))
522
+ return
523
+ next_col += 1
524
+ return
525
+ super().closeEditor(editor, hint)
526
+
527
+ def scale_columns(self, scale: float) -> None:
528
+ """Scale column widths based on font size ratio."""
529
+ for col, base_w in self._base_widths.items():
530
+ self.setColumnWidth(col, int(base_w * scale))
531
+
505
532
  def _show_context_menu(self, pos) -> None:
506
533
  """Show context menu."""
507
534
  menu = QMenu(self)
@@ -623,10 +650,15 @@ class ResultsTable(QTableWidget):
623
650
  # Resize columns to content
624
651
  self.resizeColumnsToContents()
625
652
 
626
- # Cap column widths
653
+ # Cap column widths and store base widths for font scaling
654
+ font_size = int(get_setting("font_size", "13"))
655
+ scale = font_size / 13.0
656
+ self._base_widths = {}
627
657
  for i in range(self.columnCount()):
628
658
  if self.columnWidth(i) > 300:
629
659
  self.setColumnWidth(i, 300)
660
+ # Store base width (at scale 1.0) for later rescaling
661
+ self._base_widths[i] = int(self.columnWidth(i) / scale) if scale else self.columnWidth(i)
630
662
 
631
663
  self.setSortingEnabled(True)
632
664
 
@@ -1133,6 +1165,24 @@ class SQLTab(QWidget):
1133
1165
  """Check if this tab has unsaved cell edits."""
1134
1166
  return bool(self._modified_cells)
1135
1167
 
1168
+ def set_font_size(self, size: int) -> None:
1169
+ """Apply new font size to editor and results."""
1170
+ self.editor.set_font_size(size)
1171
+
1172
+ # Scale results and fields column widths
1173
+ scale = size / 13.0
1174
+ self.results_table.scale_columns(scale)
1175
+
1176
+ # Scale row heights
1177
+ row_height = max(24, int(size * 1.8))
1178
+ self.results_table.verticalHeader().setDefaultSectionSize(row_height)
1179
+ self.fields_table.verticalHeader().setDefaultSectionSize(row_height)
1180
+
1181
+ # Update stats font
1182
+ font = self.stats_text.font()
1183
+ font.setPointSize(size)
1184
+ self.stats_text.setFont(font)
1185
+
1136
1186
  def set_sql(self, sql: str) -> None:
1137
1187
  """Set the SQL text."""
1138
1188
  self.editor.setPlainText(sql)
@@ -2005,6 +2055,7 @@ class SQLTab(QWidget):
2005
2055
  self._edit_schema = None
2006
2056
  self._pk_columns = []
2007
2057
  self._pk_indices = []
2058
+ self.results_table._pk_indices = []
2008
2059
  self._original_values = {}
2009
2060
  self._modified_cells = {}
2010
2061
  self.btn_save_changes.hide()
@@ -2053,6 +2104,7 @@ class SQLTab(QWidget):
2053
2104
  self._edit_schema = schema
2054
2105
  self._pk_columns = pk_cols
2055
2106
  self._pk_indices = pk_indices
2107
+ self.results_table._pk_indices = pk_indices
2056
2108
 
2057
2109
  # Store original values
2058
2110
  for row_idx, row in enumerate(rows):
@@ -4,7 +4,7 @@ import threading
4
4
  import urllib.request
5
5
  import json
6
6
 
7
- __version__ = "0.1.56"
7
+ __version__ = "0.1.59"
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