sqlbench 0.1.51__tar.gz → 0.1.55__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.51 → sqlbench-0.1.55}/PKG-INFO +1 -1
  2. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/adapters.py +20 -3
  3. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/connection_tree.py +30 -9
  4. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/main_window.py +59 -58
  5. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tabs/spool_tab.py +26 -10
  6. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tabs/sql_tab.py +157 -107
  7. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/version.py +1 -1
  8. {sqlbench-0.1.51 → sqlbench-0.1.55}/.github/workflows/pypi-publish.yml +0 -0
  9. {sqlbench-0.1.51 → sqlbench-0.1.55}/.gitignore +0 -0
  10. {sqlbench-0.1.51 → sqlbench-0.1.55}/CLAUDE.md +0 -0
  11. {sqlbench-0.1.51 → sqlbench-0.1.55}/Makefile +0 -0
  12. {sqlbench-0.1.51 → sqlbench-0.1.55}/PYQT_MIGRATION_FEATURES.md +0 -0
  13. {sqlbench-0.1.51 → sqlbench-0.1.55}/README.md +0 -0
  14. {sqlbench-0.1.51 → sqlbench-0.1.55}/pyproject.toml +0 -0
  15. {sqlbench-0.1.51 → sqlbench-0.1.55}/requirements.txt +0 -0
  16. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/__init__.py +0 -0
  17. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/__main__.py +0 -0
  18. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/app.py +0 -0
  19. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/database.py +0 -0
  20. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/dialogs/__init__.py +0 -0
  21. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/dialogs/connection_dialog.py +0 -0
  22. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/dialogs/regex_builder_dialog.py +0 -0
  23. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/launcher.py +0 -0
  24. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/__init__.py +0 -0
  25. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/__init__.py +0 -0
  26. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/connection_dialog.py +0 -0
  27. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/query_manager_dialog.py +0 -0
  28. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/record_viewer_dialog.py +0 -0
  29. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/regex_builder_dialog.py +0 -0
  30. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/settings_dialog.py +0 -0
  31. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/icons.py +0 -0
  32. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/syntax.py +0 -0
  33. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tab_widget.py +0 -0
  34. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tabs/__init__.py +0 -0
  35. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/theme.py +0 -0
  36. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_ibmi.png +0 -0
  37. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_mysql.png +0 -0
  38. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_postgresql.png +0 -0
  39. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_unknown.png +0 -0
  40. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/sqlbench.png +0 -0
  41. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/tabs/__init__.py +0 -0
  42. {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/tabs/spool_tab.py +0 -0
  43. {sqlbench-0.1.51 → sqlbench-0.1.55}/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.51
3
+ Version: 0.1.55
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
@@ -117,7 +117,9 @@ class IBMiAdapter(DBAdapter):
117
117
  f"UID={user};"
118
118
  f"PWD={password};"
119
119
  )
120
- return pyodbc.connect(conn_str)
120
+ conn = pyodbc.connect(conn_str)
121
+ conn.autocommit = True
122
+ return conn
121
123
 
122
124
  def get_version(self, conn):
123
125
  try:
@@ -328,7 +330,9 @@ class MySQLAdapter(DBAdapter):
328
330
  }
329
331
  if port:
330
332
  config['port'] = int(port)
331
- return mysql.connector.connect(**config)
333
+ conn = mysql.connector.connect(**config)
334
+ conn.autocommit = True
335
+ return conn
332
336
 
333
337
  def get_version(self, conn):
334
338
  try:
@@ -476,13 +480,15 @@ class PostgreSQLAdapter(DBAdapter):
476
480
 
477
481
  def connect(self, host, user, password, port=None, database=None):
478
482
  import psycopg2
479
- return psycopg2.connect(
483
+ conn = psycopg2.connect(
480
484
  host=host,
481
485
  user=user,
482
486
  password=password,
483
487
  dbname=database or 'postgres',
484
488
  port=port or 5432
485
489
  )
490
+ conn.autocommit = True
491
+ return conn
486
492
 
487
493
  def get_version_query(self):
488
494
  return "SELECT version()"
@@ -682,3 +688,14 @@ def get_available_adapters():
682
688
  Returns dict of {db_type: is_available}.
683
689
  """
684
690
  return {key: cls.is_available() for key, cls in ADAPTERS.items()}
691
+
692
+
693
+ def connect_from_info(adapter, conn_info):
694
+ """Create a database connection from an adapter and connection info dict."""
695
+ return adapter.connect(
696
+ host=conn_info['host'],
697
+ port=conn_info.get('port'),
698
+ database=conn_info.get('database'),
699
+ user=conn_info['user'],
700
+ password=conn_info['password']
701
+ )
@@ -249,27 +249,31 @@ class ConnectionTreeWidget(QWidget):
249
249
 
250
250
  def _load_schemas(self, item: QTreeWidgetItem, connection_name: str) -> None:
251
251
  """Load schemas and tables for a connection."""
252
+ from ..adapters import connect_from_info
253
+
252
254
  if connection_name in self._loading_tables:
253
255
  return
254
256
  self._loading_tables.add(connection_name)
255
257
 
256
- # Get main window to access connection
258
+ # Get connection info from main window
257
259
  main_window = self.window()
258
- if not hasattr(main_window, 'get_connection'):
260
+ if not hasattr(main_window, 'get_conn_info'):
259
261
  self._loading_tables.discard(connection_name)
260
262
  return
261
263
 
262
- connection = main_window.get_connection(connection_name)
263
- if not connection:
264
+ conn_info = main_window.get_conn_info(connection_name)
265
+ if not conn_info:
264
266
  self._loading_tables.discard(connection_name)
265
267
  return
266
268
 
267
- conn_info = self._connections_info.get(connection_name, {})
268
269
  adapter = get_adapter(conn_info.get('db_type'))
269
270
  if not adapter:
271
+ self._loading_tables.discard(connection_name)
270
272
  return
271
273
 
274
+ connection = None
272
275
  try:
276
+ connection = connect_from_info(adapter, conn_info)
273
277
  cursor = connection.cursor()
274
278
  cursor.execute(adapter.get_tables_query())
275
279
  tables = cursor.fetchall()
@@ -329,6 +333,12 @@ class ConnectionTreeWidget(QWidget):
329
333
  error_item = QTreeWidgetItem([f"Error: {str(e)[:50]}"])
330
334
  item.addChild(error_item)
331
335
  self._loading_tables.discard(connection_name)
336
+ finally:
337
+ if connection:
338
+ try:
339
+ connection.close()
340
+ except Exception:
341
+ pass
332
342
 
333
343
  def _load_tables(self, schema_item: QTreeWidgetItem) -> None:
334
344
  """Load tables for a schema.
@@ -340,6 +350,8 @@ class ConnectionTreeWidget(QWidget):
340
350
 
341
351
  def _load_columns(self, table_item: QTreeWidgetItem) -> None:
342
352
  """Load columns for a table."""
353
+ from ..adapters import connect_from_info
354
+
343
355
  data = table_item.data(0, Qt.ItemDataRole.UserRole)
344
356
  connection_name = data.get('connection')
345
357
  schema_name = data.get('schema')
@@ -351,21 +363,24 @@ class ConnectionTreeWidget(QWidget):
351
363
  self._loading_fields.add(field_key)
352
364
 
353
365
  main_window = self.window()
354
- if not hasattr(main_window, 'get_connection'):
366
+ if not hasattr(main_window, 'get_conn_info'):
355
367
  self._loading_fields.discard(field_key)
356
368
  return
357
369
 
358
- connection = main_window.get_connection(connection_name)
359
- if not connection:
370
+ conn_info = main_window.get_conn_info(connection_name)
371
+ if not conn_info:
360
372
  self._loading_fields.discard(field_key)
361
373
  return
362
374
 
363
- conn_info = self._connections_info.get(connection_name, {})
364
375
  adapter = get_adapter(conn_info.get('db_type'))
365
376
  if not adapter:
377
+ self._loading_fields.discard(field_key)
366
378
  return
367
379
 
380
+ connection = None
368
381
  try:
382
+ connection = connect_from_info(adapter, conn_info)
383
+
369
384
  # Build table reference for adapter
370
385
  table_ref = f"{schema_name}.{table_name}" if schema_name else table_name
371
386
 
@@ -412,6 +427,12 @@ class ConnectionTreeWidget(QWidget):
412
427
  error_item = QTreeWidgetItem([f"Error: {str(e)[:50]}"])
413
428
  table_item.addChild(error_item)
414
429
  self._loading_fields.discard(field_key)
430
+ finally:
431
+ if connection:
432
+ try:
433
+ connection.close()
434
+ except Exception:
435
+ pass
415
436
 
416
437
  def _on_filter_changed(self, text: str) -> None:
417
438
  """Handle filter text change."""
@@ -43,8 +43,8 @@ class MainWindow(QMainWindow):
43
43
  Theme.set_dark(dark_mode)
44
44
  Theme.apply(QApplication.instance())
45
45
 
46
- # Track active connections
47
- self._connections: Dict[str, Any] = {}
46
+ # Track active connections (credentials only, no persistent connection objects)
47
+ self._conn_infos: Dict[str, Dict] = {}
48
48
  self._adapters: Dict[str, Any] = {}
49
49
  self._db_types: Dict[str, str] = {}
50
50
 
@@ -203,7 +203,7 @@ class MainWindow(QMainWindow):
203
203
 
204
204
  # Auto-connect last used connection
205
205
  last_conn = get_setting("last_connection")
206
- if last_conn and last_conn not in self._connections:
206
+ if last_conn and last_conn not in self._conn_infos:
207
207
  conn_info = get_connection(last_conn)
208
208
  if conn_info:
209
209
  self._connect(last_conn)
@@ -294,14 +294,6 @@ class MainWindow(QMainWindow):
294
294
  tab._save_changes()
295
295
 
296
296
  self._save_state()
297
-
298
- # Close all connections
299
- for conn in self._connections.values():
300
- try:
301
- conn.close()
302
- except Exception:
303
- pass
304
-
305
297
  event.accept()
306
298
 
307
299
  def _toggle_dark_mode(self) -> None:
@@ -392,15 +384,15 @@ class MainWindow(QMainWindow):
392
384
  """Create new SQL tab for connection."""
393
385
  from .tabs.sql_tab import SQLTab
394
386
 
395
- # Get or create connection
396
- if connection_name not in self._connections:
387
+ # Ensure connection is activated
388
+ if connection_name not in self._conn_infos:
397
389
  self._connect(connection_name)
398
390
 
399
- connection = self._connections.get(connection_name)
400
- if connection:
391
+ conn_info = self._conn_infos.get(connection_name)
392
+ if conn_info:
401
393
  adapter = self._adapters.get(connection_name)
402
394
  db_type = self._db_types.get(connection_name, '')
403
- tab = SQLTab(connection_name, connection, adapter, db_type, self)
395
+ tab = SQLTab(connection_name, conn_info, adapter, db_type, self)
404
396
  index = self.tab_container.add_tab(tab, f"{connection_name} SQL")
405
397
 
406
398
  # Set tab icon based on database type
@@ -412,12 +404,13 @@ class MainWindow(QMainWindow):
412
404
  """Create new spool tab for IBM i connection."""
413
405
  from .tabs.spool_tab import SpoolTab
414
406
 
415
- if connection_name not in self._connections:
407
+ if connection_name not in self._conn_infos:
416
408
  self._connect(connection_name)
417
409
 
418
- connection = self._connections.get(connection_name)
419
- if connection:
420
- tab = SpoolTab(connection_name, connection, self)
410
+ conn_info = self._conn_infos.get(connection_name)
411
+ if conn_info:
412
+ adapter = self._adapters.get(connection_name)
413
+ tab = SpoolTab(connection_name, conn_info, adapter, self)
421
414
  index = self.tab_container.add_tab(tab, f"{connection_name} Spool")
422
415
 
423
416
  # IBM i spool tab - set icon
@@ -427,14 +420,14 @@ class MainWindow(QMainWindow):
427
420
  """Show first 1000 rows of a table."""
428
421
  from .tabs.sql_tab import SQLTab
429
422
 
430
- if connection_name not in self._connections:
423
+ if connection_name not in self._conn_infos:
431
424
  self._connect(connection_name)
432
425
 
433
- connection = self._connections.get(connection_name)
434
- if connection:
426
+ conn_info = self._conn_infos.get(connection_name)
427
+ if conn_info:
435
428
  adapter = self._adapters.get(connection_name)
436
429
  db_type = self._db_types.get(connection_name, '')
437
- tab = SQLTab(connection_name, connection, adapter, db_type, self)
430
+ tab = SQLTab(connection_name, conn_info, adapter, db_type, self)
438
431
  index = self.tab_container.add_tab(tab, f"{connection_name} SQL")
439
432
 
440
433
  # Set tab icon based on database type
@@ -465,9 +458,9 @@ class MainWindow(QMainWindow):
465
458
  self._on_edit_connection(None)
466
459
 
467
460
  def _connect(self, connection_name: str) -> bool:
468
- """Establish connection to database."""
461
+ """Verify connection credentials and activate connection."""
469
462
  from ..database import get_connection
470
- from ..adapters import get_adapter
463
+ from ..adapters import get_adapter, connect_from_info
471
464
 
472
465
  try:
473
466
  conn_info = get_connection(connection_name)
@@ -491,15 +484,12 @@ class MainWindow(QMainWindow):
491
484
  self.status_bar.showMessage(f"Connecting to {connection_name}...")
492
485
  QApplication.processEvents()
493
486
 
494
- connection = adapter.connect(
495
- host=conn_info['host'],
496
- port=conn_info.get('port'),
497
- database=conn_info.get('database'),
498
- user=conn_info['user'],
499
- password=conn_info['password']
500
- )
487
+ # Test connection then close immediately
488
+ test_conn = connect_from_info(adapter, conn_info)
489
+ test_conn.close()
501
490
 
502
- self._connections[connection_name] = connection
491
+ # Store credentials (no persistent connection)
492
+ self._conn_infos[connection_name] = conn_info
503
493
  self._adapters[connection_name] = adapter
504
494
  self._db_types[connection_name] = conn_info['db_type']
505
495
  self.connection_tree.set_connected(connection_name, True)
@@ -517,17 +507,12 @@ class MainWindow(QMainWindow):
517
507
  return False
518
508
 
519
509
  def disconnect(self, connection_name: str) -> None:
520
- """Disconnect from database."""
521
- if connection_name in self._connections:
522
- try:
523
- self._connections[connection_name].close()
524
- except Exception:
525
- pass
526
- del self._connections[connection_name]
527
- self._adapters.pop(connection_name, None)
528
- self._db_types.pop(connection_name, None)
529
- self.connection_tree.set_connected(connection_name, False)
530
- self.status_bar.showMessage(f"Disconnected from {connection_name}", 3000)
510
+ """Deactivate connection."""
511
+ self._conn_infos.pop(connection_name, None)
512
+ self._adapters.pop(connection_name, None)
513
+ self._db_types.pop(connection_name, None)
514
+ self.connection_tree.set_connected(connection_name, False)
515
+ self.status_bar.showMessage(f"Disconnected from {connection_name}", 3000)
531
516
 
532
517
  def _update_tab_names(self, old_name: str) -> None:
533
518
  """Update tab names if a connection was renamed."""
@@ -536,11 +521,11 @@ class MainWindow(QMainWindow):
536
521
  if conn:
537
522
  return
538
523
 
539
- if old_name not in self._connections:
524
+ if old_name not in self._conn_infos:
540
525
  return
541
526
 
542
527
  # Find the new name: look for a connection name we don't recognize
543
- known_names = set(self._connections.keys())
528
+ known_names = set(self._conn_infos.keys())
544
529
  new_name = None
545
530
  for c in get_connections():
546
531
  if c['name'] not in known_names:
@@ -550,8 +535,8 @@ class MainWindow(QMainWindow):
550
535
  if not new_name or new_name == old_name:
551
536
  return
552
537
 
553
- # Update connections dict
554
- self._connections[new_name] = self._connections.pop(old_name)
538
+ # Update dicts
539
+ self._conn_infos[new_name] = self._conn_infos.pop(old_name)
555
540
  if old_name in self._adapters:
556
541
  self._adapters[new_name] = self._adapters.pop(old_name)
557
542
  if old_name in self._db_types:
@@ -562,7 +547,7 @@ class MainWindow(QMainWindow):
562
547
  tab = self.tab_container.widget(i)
563
548
  if hasattr(tab, 'connection_name') and tab.connection_name == old_name:
564
549
  tab.connection_name = new_name
565
- tab.connection = self._connections[new_name]
550
+ tab.conn_info = self._conn_infos[new_name]
566
551
  current_text = self.tab_container.tabText(i)
567
552
  new_text = current_text.replace(old_name, new_name)
568
553
  self.tab_container.setTabText(i, new_text)
@@ -578,13 +563,25 @@ class MainWindow(QMainWindow):
578
563
 
579
564
  def _check_for_updates(self) -> None:
580
565
  """Check for updates in background."""
581
- from ..version import check_for_updates
566
+ from ..version import get_pypi_version, is_newer_version, __version__
567
+
568
+ def do_check():
569
+ try:
570
+ latest = get_pypi_version()
571
+ if latest and is_newer_version(latest, __version__):
572
+ self._update_version = latest
573
+ except Exception:
574
+ pass
582
575
 
583
- def on_result(has_update, latest_version):
584
- if has_update:
585
- QTimer.singleShot(0, lambda: self._show_update_dialog(latest_version))
576
+ def on_done():
577
+ version = getattr(self, '_update_version', None)
578
+ if version:
579
+ self._show_update_dialog(version)
586
580
 
587
- check_for_updates(on_result)
581
+ thread = threading.Thread(target=do_check, daemon=True)
582
+ thread.start()
583
+ # Poll from main thread until the background check completes
584
+ QTimer.singleShot(3000, on_done)
588
585
 
589
586
  def _show_update_dialog(self, latest_version: str) -> None:
590
587
  """Show update available dialog."""
@@ -631,6 +628,10 @@ class MainWindow(QMainWindow):
631
628
  """Set status bar message."""
632
629
  self.status_bar.showMessage(message, timeout)
633
630
 
634
- def get_connection(self, name: str) -> Optional[Any]:
635
- """Get active connection by name."""
636
- return self._connections.get(name)
631
+ def get_conn_info(self, name: str) -> Optional[Dict]:
632
+ """Get connection info by name."""
633
+ return self._conn_infos.get(name)
634
+
635
+ def get_adapter(self, name: str) -> Optional[Any]:
636
+ """Get adapter for a connection by name."""
637
+ return self._adapters.get(name)
@@ -46,15 +46,19 @@ class SpoolWorker(QThread):
46
46
  pdf_complete = pyqtSignal(str) # output_path
47
47
  error = pyqtSignal(str)
48
48
 
49
- def __init__(self, connection: Any, operation: str, **kwargs):
49
+ def __init__(self, conn_info: dict, adapter: Any, operation: str, **kwargs):
50
50
  super().__init__()
51
- self.connection = connection
51
+ self.conn_info = conn_info
52
+ self.adapter = adapter
53
+ self.connection = None
52
54
  self.operation = operation
53
55
  self.kwargs = kwargs
54
56
 
55
57
  def run(self) -> None:
56
58
  """Execute operation in background."""
59
+ from ...adapters import connect_from_info
57
60
  try:
61
+ self.connection = connect_from_info(self.adapter, self.conn_info)
58
62
  if self.operation == "list":
59
63
  self._list_spool_files()
60
64
  elif self.operation == "view":
@@ -65,6 +69,12 @@ class SpoolWorker(QThread):
65
69
  self._generate_pdf()
66
70
  except Exception as e:
67
71
  self.error.emit(str(e))
72
+ finally:
73
+ if self.connection:
74
+ try:
75
+ self.connection.close()
76
+ except Exception:
77
+ pass
68
78
 
69
79
  def _list_spool_files(self) -> None:
70
80
  """List spool files for user."""
@@ -247,12 +257,13 @@ class SpoolWorker(QThread):
247
257
  class SpoolTab(QWidget):
248
258
  """Tab widget for IBM i spool file management."""
249
259
 
250
- def __init__(self, connection_name: str, connection: Any,
251
- parent: Optional[QWidget] = None):
260
+ def __init__(self, connection_name: str, conn_info: dict,
261
+ adapter: Any, parent: Optional[QWidget] = None):
252
262
  super().__init__(parent)
253
263
 
254
264
  self.connection_name = connection_name
255
- self.connection = connection
265
+ self.conn_info = conn_info
266
+ self.adapter = adapter
256
267
  self._worker: Optional[SpoolWorker] = None
257
268
  self._search_matches: List[int] = []
258
269
  self._current_match: int = -1
@@ -436,7 +447,8 @@ class SpoolTab(QWidget):
436
447
  self.btn_refresh.setEnabled(False)
437
448
 
438
449
  self._worker = SpoolWorker(
439
- self.connection,
450
+ self.conn_info,
451
+ self.adapter,
440
452
  "list",
441
453
  user=user
442
454
  )
@@ -480,7 +492,8 @@ class SpoolTab(QWidget):
480
492
  self.btn_print.setEnabled(False)
481
493
 
482
494
  self._worker = SpoolWorker(
483
- self.connection,
495
+ self.conn_info,
496
+ self.adapter,
484
497
  "view",
485
498
  file_name=file_name,
486
499
  qualified_job=qualified_job,
@@ -534,7 +547,8 @@ class SpoolTab(QWidget):
534
547
  self.btn_refresh.setEnabled(False)
535
548
 
536
549
  self._worker = SpoolWorker(
537
- self.connection,
550
+ self.conn_info,
551
+ self.adapter,
538
552
  "delete",
539
553
  files=files_to_delete
540
554
  )
@@ -584,7 +598,8 @@ class SpoolTab(QWidget):
584
598
  self.viewer_status.setText("Generating PDF...")
585
599
 
586
600
  self._worker = SpoolWorker(
587
- self.connection,
601
+ self.conn_info,
602
+ self.adapter,
588
603
  "pdf",
589
604
  file_name=self._current_spool_info["file_name"],
590
605
  qualified_job=self._current_spool_info["qualified_job"],
@@ -713,7 +728,8 @@ class SpoolTab(QWidget):
713
728
  self._print_temp_path = temp_path
714
729
 
715
730
  self._worker = SpoolWorker(
716
- self.connection,
731
+ self.conn_info,
732
+ self.adapter,
717
733
  "pdf",
718
734
  file_name=self._current_spool_info["file_name"],
719
735
  qualified_job=self._current_spool_info["qualified_job"],
@@ -137,11 +137,11 @@ class QueryWorker(QThread):
137
137
  error = pyqtSignal(str)
138
138
  row_count = pyqtSignal(int)
139
139
 
140
- def __init__(self, connection: Any, sql: str, adapter: Any = None,
140
+ def __init__(self, conn_info: dict, sql: str, adapter: Any = None,
141
141
  limit: int = 1000, offset: int = 0,
142
142
  fetch_all: bool = False, run_count: bool = True):
143
143
  super().__init__()
144
- self.connection = connection
144
+ self.conn_info = conn_info
145
145
  self.sql = sql
146
146
  self.adapter = adapter
147
147
  self.limit = limit
@@ -162,8 +162,11 @@ class QueryWorker(QThread):
162
162
 
163
163
  def run(self) -> None:
164
164
  """Execute query in background."""
165
+ from ...adapters import connect_from_info
166
+ conn = None
165
167
  try:
166
- cursor = self.connection.cursor()
168
+ conn = connect_from_info(self.adapter, self.conn_info)
169
+ cursor = conn.cursor()
167
170
 
168
171
  sql_stripped = self.sql.strip()
169
172
  while sql_stripped.endswith(';'):
@@ -222,6 +225,12 @@ class QueryWorker(QThread):
222
225
  except Exception as e:
223
226
  if not self._cancelled:
224
227
  self.error.emit(str(e))
228
+ finally:
229
+ if conn:
230
+ try:
231
+ conn.close()
232
+ except Exception:
233
+ pass
225
234
 
226
235
 
227
236
  class ScriptWorker(QThread):
@@ -230,9 +239,10 @@ class ScriptWorker(QThread):
230
239
  all_finished = pyqtSignal(list, float) # results_list, total_time
231
240
  error = pyqtSignal(str)
232
241
 
233
- def __init__(self, connection: Any, statements: List[str]):
242
+ def __init__(self, conn_info: dict, adapter: Any, statements: List[str]):
234
243
  super().__init__()
235
- self.connection = connection
244
+ self.conn_info = conn_info
245
+ self.adapter = adapter
236
246
  self.statements = statements
237
247
  self._cancelled = False
238
248
 
@@ -242,79 +252,92 @@ class ScriptWorker(QThread):
242
252
 
243
253
  def run(self) -> None:
244
254
  """Execute all statements sequentially."""
245
- results = []
246
- total_start = time.time()
247
-
248
- for i, stmt in enumerate(self.statements):
249
- if self._cancelled:
250
- break
255
+ from ...adapters import connect_from_info
256
+ conn = None
257
+ try:
258
+ conn = connect_from_info(self.adapter, self.conn_info)
259
+ results = []
260
+ total_start = time.time()
251
261
 
252
- stmt_stripped = stmt.strip()
253
- if not stmt_stripped:
254
- continue
255
- while stmt_stripped.endswith(';'):
256
- stmt_stripped = stmt_stripped[:-1].strip()
257
- if not stmt_stripped:
258
- continue
259
-
260
- result = {
261
- "stmt": i + 1,
262
- "sql": stmt_stripped[:200] + ('...' if len(stmt_stripped) > 200 else ''),
263
- "full_sql": stmt_stripped,
264
- "status": "",
265
- "time": 0.0,
266
- "row_count": 0,
267
- "success": True,
268
- "error": None,
269
- }
262
+ for i, stmt in enumerate(self.statements):
263
+ if self._cancelled:
264
+ break
265
+
266
+ stmt_stripped = stmt.strip()
267
+ if not stmt_stripped:
268
+ continue
269
+ while stmt_stripped.endswith(';'):
270
+ stmt_stripped = stmt_stripped[:-1].strip()
271
+ if not stmt_stripped:
272
+ continue
273
+
274
+ result = {
275
+ "stmt": i + 1,
276
+ "sql": stmt_stripped[:200] + ('...' if len(stmt_stripped) > 200 else ''),
277
+ "full_sql": stmt_stripped,
278
+ "status": "",
279
+ "time": 0.0,
280
+ "row_count": 0,
281
+ "success": True,
282
+ "error": None,
283
+ }
270
284
 
271
- try:
272
- cursor = self.connection.cursor()
273
- start = time.time()
274
- cursor.execute(stmt_stripped)
275
- elapsed = time.time() - start
276
-
277
- if cursor.description:
278
- rows = cursor.fetchall()
279
- result["row_count"] = len(rows)
280
- result["status"] = f"{len(rows)} row(s) returned"
281
- else:
282
- rc = cursor.rowcount if cursor.rowcount >= 0 else 0
283
- result["row_count"] = rc
284
- sql_upper = stmt_stripped.upper()
285
- if sql_upper.startswith("INSERT"):
286
- result["status"] = f"{rc} row(s) inserted"
287
- elif sql_upper.startswith("UPDATE"):
288
- result["status"] = f"{rc} row(s) updated"
289
- elif sql_upper.startswith("DELETE"):
290
- result["status"] = f"{rc} row(s) deleted"
285
+ try:
286
+ cursor = conn.cursor()
287
+ start = time.time()
288
+ cursor.execute(stmt_stripped)
289
+ elapsed = time.time() - start
290
+
291
+ if cursor.description:
292
+ rows = cursor.fetchall()
293
+ result["row_count"] = len(rows)
294
+ result["status"] = f"{len(rows)} row(s) returned"
291
295
  else:
292
- result["status"] = f"OK ({rc} row(s) affected)"
296
+ rc = cursor.rowcount if cursor.rowcount >= 0 else 0
297
+ result["row_count"] = rc
298
+ sql_upper = stmt_stripped.upper()
299
+ if sql_upper.startswith("INSERT"):
300
+ result["status"] = f"{rc} row(s) inserted"
301
+ elif sql_upper.startswith("UPDATE"):
302
+ result["status"] = f"{rc} row(s) updated"
303
+ elif sql_upper.startswith("DELETE"):
304
+ result["status"] = f"{rc} row(s) deleted"
305
+ else:
306
+ result["status"] = f"OK ({rc} row(s) affected)"
307
+ try:
308
+ conn.commit()
309
+ except Exception:
310
+ pass
311
+
312
+ result["time"] = elapsed
313
+ cursor.close()
314
+
315
+ except Exception as e:
316
+ result["success"] = False
317
+ result["status"] = "ERROR"
318
+ result["error"] = str(e)
319
+ result["time"] = time.time() - start
293
320
  try:
294
- self.connection.commit()
321
+ conn.rollback()
295
322
  except Exception:
296
323
  pass
297
324
 
298
- result["time"] = elapsed
299
- cursor.close()
325
+ results.append(result)
300
326
 
301
- except Exception as e:
302
- result["success"] = False
303
- result["status"] = "ERROR"
304
- result["error"] = str(e)
305
- result["time"] = time.time() - start
327
+ total_time = time.time() - total_start
328
+
329
+ if not self._cancelled:
330
+ self.all_finished.emit(results, total_time)
331
+ except Exception as e:
332
+ if not self._cancelled:
333
+ self.error.emit(str(e))
334
+ finally:
335
+ if conn:
306
336
  try:
307
- self.connection.rollback()
337
+ conn.close()
308
338
  except Exception:
309
339
  pass
310
340
 
311
- results.append(result)
312
-
313
- total_time = time.time() - total_start
314
-
315
- if not self._cancelled:
316
- self.all_finished.emit(results, total_time)
317
-
318
341
 
319
342
  class SQLEditor(QPlainTextEdit):
320
343
  """SQL code editor with syntax highlighting."""
@@ -611,13 +634,13 @@ class ResultsTable(QTableWidget):
611
634
  class SQLTab(QWidget):
612
635
  """Tab widget for SQL editing and execution."""
613
636
 
614
- def __init__(self, connection_name: str, connection: Any,
637
+ def __init__(self, connection_name: str, conn_info: dict,
615
638
  adapter: Any = None, db_type: str = '',
616
639
  parent: Optional[QWidget] = None):
617
640
  super().__init__(parent)
618
641
 
619
642
  self.connection_name = connection_name
620
- self.connection = connection
643
+ self.conn_info = conn_info
621
644
  self.adapter = adapter
622
645
  self.db_type = db_type
623
646
  self._worker: Optional[QueryWorker] = None
@@ -1141,7 +1164,7 @@ class SQLTab(QWidget):
1141
1164
  self.btn_cancel.setEnabled(True)
1142
1165
  self._set_status(f"Executing {len(statements)} statement(s)...")
1143
1166
 
1144
- self._script_worker = ScriptWorker(self.connection, statements)
1167
+ self._script_worker = ScriptWorker(self.conn_info, self.adapter, statements)
1145
1168
  self._script_worker.all_finished.connect(self._on_script_finished)
1146
1169
  self._script_worker.error.connect(self._on_query_error)
1147
1170
  self._script_worker.start()
@@ -1272,7 +1295,7 @@ class SQLTab(QWidget):
1272
1295
 
1273
1296
  # Start worker
1274
1297
  self._worker = QueryWorker(
1275
- self.connection, sql, self.adapter,
1298
+ self.conn_info, sql, self.adapter,
1276
1299
  self._rows_per_page, 0,
1277
1300
  self.chk_show_all.isChecked(), run_count=True
1278
1301
  )
@@ -1476,7 +1499,7 @@ class SQLTab(QWidget):
1476
1499
  self._set_status("Loading page...")
1477
1500
 
1478
1501
  self._worker = QueryWorker(
1479
- self.connection, self._last_sql, self.adapter,
1502
+ self.conn_info, self._last_sql, self.adapter,
1480
1503
  self._rows_per_page, offset,
1481
1504
  fetch_all=False, run_count=False
1482
1505
  )
@@ -1850,9 +1873,12 @@ class SQLTab(QWidget):
1850
1873
  if not self.adapter or self.db_type != "ibmi":
1851
1874
  return None
1852
1875
 
1876
+ from ...adapters import connect_from_info
1877
+ conn = None
1853
1878
  explain_data = []
1854
1879
  try:
1855
- explain_cursor = self.connection.cursor()
1880
+ conn = connect_from_info(self.adapter, self.conn_info)
1881
+ explain_cursor = conn.cursor()
1856
1882
  try:
1857
1883
  explain_cursor.execute("CALL QSYS2.OVERRIDE_QAQQINI(1, '', '')")
1858
1884
  except Exception:
@@ -1861,7 +1887,7 @@ class SQLTab(QWidget):
1861
1887
  tables = self._extract_tables_from_sql(sql)
1862
1888
  for table in tables[:5]:
1863
1889
  try:
1864
- idx_cursor = self.connection.cursor()
1890
+ idx_cursor = conn.cursor()
1865
1891
  idx_cursor.execute("""
1866
1892
  SELECT INDEX_NAME, COLUMN_NAME, INDEX_TYPE, IS_UNIQUE
1867
1893
  FROM QSYS2.SYSINDEXES I
@@ -1889,6 +1915,12 @@ class SQLTab(QWidget):
1889
1915
  explain_cursor.close()
1890
1916
  except Exception:
1891
1917
  pass
1918
+ finally:
1919
+ if conn:
1920
+ try:
1921
+ conn.close()
1922
+ except Exception:
1923
+ pass
1892
1924
 
1893
1925
  return "\n".join(explain_data) if explain_data else None
1894
1926
 
@@ -1990,8 +2022,13 @@ class SQLTab(QWidget):
1990
2022
  return
1991
2023
 
1992
2024
  try:
1993
- pk_cols = self.adapter.get_primary_key_columns(
1994
- self.connection, schema, table)
2025
+ from ...adapters import connect_from_info
2026
+ pk_conn = connect_from_info(self.adapter, self.conn_info)
2027
+ try:
2028
+ pk_cols = self.adapter.get_primary_key_columns(
2029
+ pk_conn, schema, table)
2030
+ finally:
2031
+ pk_conn.close()
1995
2032
  except Exception:
1996
2033
  pk_cols = []
1997
2034
 
@@ -2104,44 +2141,57 @@ class SQLTab(QWidget):
2104
2141
  if msg.exec() != QMessageBox.StandardButton.Yes:
2105
2142
  return
2106
2143
 
2144
+ from ...adapters import connect_from_info
2107
2145
  errors = []
2108
2146
  success_count = 0
2109
2147
  db = _get_db()
2148
+ conn = None
2110
2149
 
2111
- for row_idx, changes in list(self._modified_cells.items()):
2112
- original = self._original_values.get(row_idx)
2113
- if not original:
2114
- continue
2115
- try:
2116
- sql, params = self._generate_update_sql(changes, original)
2117
- if sql:
2118
- cursor = self.connection.cursor()
2119
- start_time = time.time()
2120
- cursor.execute(sql, params)
2121
- self.connection.commit()
2122
- duration = time.time() - start_time
2123
- cursor.close()
2124
- success_count += 1
2125
-
2126
- log_sql = self._format_sql_with_params(sql, params)
2127
- db.log_query(self.connection_name, log_sql, duration, 1, "success")
2128
-
2129
- # Update original values
2130
- current = list(original)
2131
- for col_idx, val in changes.items():
2132
- current[col_idx] = val
2133
- self._original_values[row_idx] = tuple(current)
2134
-
2135
- # Remove highlight
2136
- for c in range(self.results_table.columnCount()):
2137
- it = self.results_table.item(row_idx, c)
2138
- if it:
2139
- it.setBackground(QColor(0, 0, 0, 0))
2150
+ try:
2151
+ conn = connect_from_info(self.adapter, self.conn_info)
2140
2152
 
2141
- except Exception as e:
2142
- errors.append(f"Row {row_idx + 1}: {e}")
2153
+ for row_idx, changes in list(self._modified_cells.items()):
2154
+ original = self._original_values.get(row_idx)
2155
+ if not original:
2156
+ continue
2157
+ try:
2158
+ sql, params = self._generate_update_sql(changes, original)
2159
+ if sql:
2160
+ cursor = conn.cursor()
2161
+ start_time = time.time()
2162
+ cursor.execute(sql, params)
2163
+ conn.commit()
2164
+ duration = time.time() - start_time
2165
+ cursor.close()
2166
+ success_count += 1
2167
+
2168
+ log_sql = self._format_sql_with_params(sql, params)
2169
+ db.log_query(self.connection_name, log_sql, duration, 1, "success")
2170
+
2171
+ # Update original values
2172
+ current = list(original)
2173
+ for col_idx, val in changes.items():
2174
+ current[col_idx] = val
2175
+ self._original_values[row_idx] = tuple(current)
2176
+
2177
+ # Remove highlight
2178
+ for c in range(self.results_table.columnCount()):
2179
+ it = self.results_table.item(row_idx, c)
2180
+ if it:
2181
+ it.setBackground(QColor(0, 0, 0, 0))
2182
+
2183
+ except Exception as e:
2184
+ errors.append(f"Row {row_idx + 1}: {e}")
2185
+ try:
2186
+ conn.rollback()
2187
+ except Exception:
2188
+ pass
2189
+ except Exception as e:
2190
+ errors.append(f"Connection error: {e}")
2191
+ finally:
2192
+ if conn:
2143
2193
  try:
2144
- self.connection.rollback()
2194
+ conn.close()
2145
2195
  except Exception:
2146
2196
  pass
2147
2197
 
@@ -4,7 +4,7 @@ import threading
4
4
  import urllib.request
5
5
  import json
6
6
 
7
- __version__ = "0.1.51"
7
+ __version__ = "0.1.55"
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