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.
- {sqlbench-0.1.51 → sqlbench-0.1.55}/PKG-INFO +1 -1
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/adapters.py +20 -3
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/connection_tree.py +30 -9
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/main_window.py +59 -58
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tabs/spool_tab.py +26 -10
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tabs/sql_tab.py +157 -107
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/version.py +1 -1
- {sqlbench-0.1.51 → sqlbench-0.1.55}/.github/workflows/pypi-publish.yml +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/.gitignore +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/CLAUDE.md +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/Makefile +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/PYQT_MIGRATION_FEATURES.md +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/README.md +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/pyproject.toml +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/requirements.txt +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/__init__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/__main__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/app.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/database.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/dialogs/__init__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/dialogs/connection_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/dialogs/regex_builder_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/launcher.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/__init__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/__init__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/connection_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/query_manager_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/record_viewer_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/regex_builder_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/dialogs/settings_dialog.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/icons.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/syntax.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tab_widget.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/tabs/__init__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/qt/theme.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_ibmi.png +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_mysql.png +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_postgresql.png +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/db_unknown.png +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/resources/sqlbench.png +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/tabs/__init__.py +0 -0
- {sqlbench-0.1.51 → sqlbench-0.1.55}/sqlbench/tabs/spool_tab.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
258
|
+
# Get connection info from main window
|
|
257
259
|
main_window = self.window()
|
|
258
|
-
if not hasattr(main_window, '
|
|
260
|
+
if not hasattr(main_window, 'get_conn_info'):
|
|
259
261
|
self._loading_tables.discard(connection_name)
|
|
260
262
|
return
|
|
261
263
|
|
|
262
|
-
|
|
263
|
-
if not
|
|
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, '
|
|
366
|
+
if not hasattr(main_window, 'get_conn_info'):
|
|
355
367
|
self._loading_fields.discard(field_key)
|
|
356
368
|
return
|
|
357
369
|
|
|
358
|
-
|
|
359
|
-
if not
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
396
|
-
if connection_name not in self.
|
|
387
|
+
# Ensure connection is activated
|
|
388
|
+
if connection_name not in self._conn_infos:
|
|
397
389
|
self._connect(connection_name)
|
|
398
390
|
|
|
399
|
-
|
|
400
|
-
if
|
|
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,
|
|
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.
|
|
407
|
+
if connection_name not in self._conn_infos:
|
|
416
408
|
self._connect(connection_name)
|
|
417
409
|
|
|
418
|
-
|
|
419
|
-
if
|
|
420
|
-
|
|
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.
|
|
423
|
+
if connection_name not in self._conn_infos:
|
|
431
424
|
self._connect(connection_name)
|
|
432
425
|
|
|
433
|
-
|
|
434
|
-
if
|
|
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,
|
|
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
|
-
"""
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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.
|
|
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.
|
|
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
|
|
554
|
-
self.
|
|
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.
|
|
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
|
|
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
|
|
584
|
-
|
|
585
|
-
|
|
576
|
+
def on_done():
|
|
577
|
+
version = getattr(self, '_update_version', None)
|
|
578
|
+
if version:
|
|
579
|
+
self._show_update_dialog(version)
|
|
586
580
|
|
|
587
|
-
|
|
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
|
|
635
|
-
"""Get
|
|
636
|
-
return self.
|
|
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,
|
|
49
|
+
def __init__(self, conn_info: dict, adapter: Any, operation: str, **kwargs):
|
|
50
50
|
super().__init__()
|
|
51
|
-
self.
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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,
|
|
242
|
+
def __init__(self, conn_info: dict, adapter: Any, statements: List[str]):
|
|
234
243
|
super().__init__()
|
|
235
|
-
self.
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
stmt_stripped =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
conn.rollback()
|
|
295
322
|
except Exception:
|
|
296
323
|
pass
|
|
297
324
|
|
|
298
|
-
result
|
|
299
|
-
cursor.close()
|
|
325
|
+
results.append(result)
|
|
300
326
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1994
|
-
|
|
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
|
-
|
|
2112
|
-
|
|
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
|
-
|
|
2142
|
-
|
|
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
|
-
|
|
2194
|
+
conn.close()
|
|
2145
2195
|
except Exception:
|
|
2146
2196
|
pass
|
|
2147
2197
|
|
|
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
|