sqlbench 0.1.7__tar.gz → 0.1.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlbench
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -24,23 +24,18 @@ Classifier: Topic :: Database :: Front-Ends
24
24
  Requires-Python: >=3.9
25
25
  Requires-Dist: sqlparse>=0.5.0
26
26
  Provides-Extra: all
27
- Requires-Dist: ibm-db>=3.0.0; extra == 'all'
28
27
  Requires-Dist: mysql-connector-python>=8.0.0; extra == 'all'
29
28
  Requires-Dist: openpyxl>=3.1.0; extra == 'all'
30
29
  Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
31
30
  Requires-Dist: pyodbc>=4.0.0; extra == 'all'
32
- Requires-Dist: reportlab>=4.0.0; extra == 'all'
33
31
  Provides-Extra: dev
34
32
  Requires-Dist: black>=23.0.0; extra == 'dev'
35
33
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
36
34
  Requires-Dist: ruff>=0.1.0; extra == 'dev'
37
35
  Provides-Extra: export
38
36
  Requires-Dist: openpyxl>=3.1.0; extra == 'export'
39
- Requires-Dist: reportlab>=4.0.0; extra == 'export'
40
37
  Provides-Extra: ibmi
41
38
  Requires-Dist: pyodbc>=4.0.0; extra == 'ibmi'
42
- Provides-Extra: ibmi-db
43
- Requires-Dist: ibm-db>=3.0.0; extra == 'ibmi-db'
44
39
  Provides-Extra: mysql
45
40
  Requires-Dist: mysql-connector-python>=8.0.0; extra == 'mysql'
46
41
  Provides-Extra: postgresql
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlbench"
7
- version = "0.1.7"
7
+ version = "0.1.8"
8
8
  description = "A multi-database SQL workbench with support for IBM i, MySQL, and PostgreSQL"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -36,14 +36,11 @@ dependencies = [
36
36
  mysql = ["mysql-connector-python>=8.0.0"]
37
37
  postgresql = ["psycopg2-binary>=2.9.0"]
38
38
  ibmi = ["pyodbc>=4.0.0"]
39
- ibmi-db = ["ibm_db>=3.0.0"]
40
- export = ["reportlab>=4.0.0", "openpyxl>=3.1.0"]
39
+ export = ["openpyxl>=3.1.0"]
41
40
  all = [
42
41
  "mysql-connector-python>=8.0.0",
43
42
  "psycopg2-binary>=2.9.0",
44
43
  "pyodbc>=4.0.0",
45
- "ibm_db>=3.0.0",
46
- "reportlab>=4.0.0",
47
44
  "openpyxl>=3.1.0",
48
45
  ]
49
46
  dev = [
@@ -1,32 +1,6 @@
1
1
  """Database adapters for different database types."""
2
2
 
3
- import os
4
3
  from abc import ABC, abstractmethod
5
- from pathlib import Path
6
-
7
-
8
- def _setup_ibm_db_environment():
9
- """Set up environment variables for ibm_db if clidriver is installed."""
10
- if os.environ.get("IBM_DB_HOME"):
11
- return # Already configured
12
-
13
- # Check common install locations
14
- clidriver_paths = [
15
- Path.home() / "db2drivers" / "clidriver",
16
- Path("/opt/ibm/db2/clidriver"),
17
- Path("/opt/clidriver"),
18
- ]
19
-
20
- for cli_path in clidriver_paths:
21
- if (cli_path / "lib").exists():
22
- os.environ["IBM_DB_HOME"] = str(cli_path)
23
- lib_path = os.environ.get("LD_LIBRARY_PATH", "")
24
- os.environ["LD_LIBRARY_PATH"] = f"{cli_path}/lib:{lib_path}"
25
- break
26
-
27
-
28
- # Set up ibm_db environment before any imports
29
- _setup_ibm_db_environment()
30
4
 
31
5
 
32
6
  class DBAdapter(ABC):
@@ -201,109 +175,6 @@ class IBMiAdapter(DBAdapter):
201
175
  """
202
176
 
203
177
 
204
- class IBMiDBAdapter(DBAdapter):
205
- """Adapter for IBM i (AS/400) via ibm_db (native CLI driver)."""
206
-
207
- db_type = "ibmi_db"
208
- display_name = "IBM i (ibm_db)"
209
- default_port = 446 # DRDA port
210
- requires_database = False
211
- supports_spool = True
212
- required_module = "ibm_db"
213
- install_hint = "pip install sqlbench[ibmi-db]"
214
-
215
- def connect(self, host, user, password, port=None, database=None):
216
- import ibm_db_dbi
217
-
218
- # Build connection string for IBM i
219
- # DATABASE can be *LOCAL or the RDB name
220
- db_name = database if database else "*LOCAL"
221
- port_num = port if port else 446
222
-
223
- conn_str = (
224
- f"DATABASE={db_name};"
225
- f"HOSTNAME={host};"
226
- f"PORT={port_num};"
227
- f"PROTOCOL=TCPIP;"
228
- f"UID={user};"
229
- f"PWD={password};"
230
- )
231
-
232
- # ibm_db_dbi provides DB-API 2.0 compliant interface
233
- return ibm_db_dbi.connect(conn_str, "", "")
234
-
235
- def get_version(self, conn):
236
- try:
237
- cursor = conn.cursor()
238
- cursor.execute("SELECT OS_VERSION, OS_RELEASE FROM SYSIBMADM.ENV_SYS_INFO")
239
- row = cursor.fetchone()
240
- cursor.close()
241
- if row:
242
- return f"{row[0]}.{row[1]}"
243
- except Exception:
244
- pass
245
- return None
246
-
247
- def add_pagination(self, sql, limit, offset=0):
248
- """IBM i uses OFFSET/FETCH syntax."""
249
- sql_stripped = sql.strip()
250
- while sql_stripped.endswith(';'):
251
- sql_stripped = sql_stripped[:-1].strip()
252
-
253
- if offset > 0:
254
- return f"{sql_stripped} OFFSET {offset} ROWS FETCH FIRST {limit} ROWS ONLY"
255
- return f"{sql_stripped} FETCH FIRST {limit} ROWS ONLY"
256
-
257
- def get_select_limit_query(self, table_ref, limit):
258
- """Get a SELECT query with row limit for IBM i."""
259
- return f"SELECT * FROM {table_ref} FETCH FIRST {limit} ROWS ONLY"
260
-
261
- def get_version_query(self):
262
- """Get the SQL to retrieve IBM i version."""
263
- return "SELECT OS_VERSION || '.' || OS_RELEASE FROM SYSIBMADM.ENV_SYS_INFO"
264
-
265
- def get_columns_query(self, tables):
266
- if not tables:
267
- return None
268
-
269
- table_conditions = []
270
- for table in tables:
271
- if '.' in table:
272
- schema, tbl = table.split('.', 1)
273
- table_conditions.append(
274
- f"(TABLE_SCHEMA = '{schema.upper()}' AND TABLE_NAME = '{tbl.upper()}')"
275
- )
276
- else:
277
- table_conditions.append(f"TABLE_NAME = '{table.upper()}'")
278
-
279
- where_clause = " OR ".join(table_conditions)
280
- return f"""
281
- SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, LENGTH, NUMERIC_SCALE
282
- FROM QSYS2.SYSCOLUMNS
283
- WHERE {where_clause}
284
- ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
285
- """
286
-
287
- def get_tables_query(self):
288
- """Get tables from IBM i - returns schema, table_name, table_type."""
289
- return """
290
- SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
291
- FROM QSYS2.SYSTABLES
292
- WHERE TABLE_TYPE IN ('T', 'P', 'V')
293
- ORDER BY TABLE_SCHEMA, TABLE_NAME
294
- """
295
-
296
- def is_numeric_type(self, type_code):
297
- """Check if a type_code represents a numeric type for ibm_db."""
298
- # ibm_db uses type codes similar to CLI
299
- # Check for Python numeric types as fallback
300
- from decimal import Decimal
301
- if isinstance(type_code, type):
302
- return type_code in (int, float, Decimal)
303
- # ibm_db type codes for numeric: various integer constants
304
- return False
305
-
306
-
307
178
  class MySQLAdapter(DBAdapter):
308
179
  """Adapter for MySQL."""
309
180
 
@@ -513,7 +384,6 @@ class PostgreSQLAdapter(DBAdapter):
513
384
  # Registry of available adapters
514
385
  ADAPTERS = {
515
386
  'ibmi': IBMiAdapter,
516
- 'ibmi_db': IBMiDBAdapter,
517
387
  'mysql': MySQLAdapter,
518
388
  'postgresql': PostgreSQLAdapter,
519
389
  }
@@ -1037,11 +1037,7 @@ class SQLBenchApp:
1037
1037
  text=True
1038
1038
  )
1039
1039
  if result.returncode == 0:
1040
- self.root.after(0, lambda: tk.messagebox.showinfo(
1041
- "Upgrade Complete",
1042
- "SQLBench has been upgraded.\n\n"
1043
- "Please restart the application to use the new version."
1044
- ))
1040
+ self.root.after(0, self._show_restart_dialog)
1045
1041
  else:
1046
1042
  error = result.stderr or result.stdout or "Unknown error"
1047
1043
  self.root.after(0, lambda: tk.messagebox.showerror(
@@ -1068,6 +1064,56 @@ class SQLBenchApp:
1068
1064
  from sqlbench.version import __version__
1069
1065
  tk.messagebox.showinfo("About", f"SQLBench v{__version__}\nMulti-database SQL Workbench")
1070
1066
 
1067
+ def _show_restart_dialog(self, message="SQLBench has been upgraded.\n\nPlease restart the application to use the new version."):
1068
+ """Show a dialog with restart option."""
1069
+ dialog = tk.Toplevel(self.root)
1070
+ dialog.title("Restart Required")
1071
+ dialog.transient(self.root)
1072
+ dialog.grab_set()
1073
+ dialog.resizable(False, False)
1074
+
1075
+ # Message
1076
+ msg_frame = ttk.Frame(dialog, padding=20)
1077
+ msg_frame.pack(fill=tk.BOTH, expand=True)
1078
+ ttk.Label(msg_frame, text=message, wraplength=300).pack()
1079
+
1080
+ # Buttons
1081
+ btn_frame = ttk.Frame(dialog, padding=(20, 0, 20, 20))
1082
+ btn_frame.pack(fill=tk.X)
1083
+ ttk.Button(btn_frame, text="Restart Now", command=lambda: self._restart_app()).pack(side=tk.LEFT, padx=5)
1084
+ ttk.Button(btn_frame, text="Later", command=dialog.destroy).pack(side=tk.LEFT, padx=5)
1085
+
1086
+ # Center on parent
1087
+ dialog.update_idletasks()
1088
+ w = dialog.winfo_width()
1089
+ h = dialog.winfo_height()
1090
+ x = self.root.winfo_x() + (self.root.winfo_width() - w) // 2
1091
+ y = self.root.winfo_y() + (self.root.winfo_height() - h) // 2
1092
+ dialog.geometry(f"+{x}+{y}")
1093
+
1094
+ def _restart_app(self):
1095
+ """Restart the application."""
1096
+ import sys
1097
+ import os
1098
+
1099
+ # Save current state before restart
1100
+ self._save_geometry()
1101
+ self._save_active_tab()
1102
+ self._save_connection_state()
1103
+
1104
+ # Close connections gracefully
1105
+ for name in list(self.connections.keys()):
1106
+ try:
1107
+ self.connections[name]['conn'].close()
1108
+ except Exception:
1109
+ pass
1110
+
1111
+ self.root.destroy()
1112
+
1113
+ # Re-execute the application
1114
+ python = sys.executable
1115
+ os.execv(python, [python] + sys.argv)
1116
+
1071
1117
  def _show_settings(self):
1072
1118
  """Show settings dialog."""
1073
1119
  settings_win = tk.Toplevel(self.root)
@@ -206,7 +206,7 @@ class ConnectionDialog:
206
206
  for conn in self._connections:
207
207
  # Show type indicator
208
208
  db_type = conn.get("db_type", "ibmi")
209
- type_indicator = {"ibmi": "[i]", "ibmi_db": "[I]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
209
+ type_indicator = {"ibmi": "[i]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
210
210
  # Mark unavailable connections
211
211
  if db_type not in available_types:
212
212
  type_indicator = "[!]"
@@ -432,97 +432,6 @@ class ConnectionDialog:
432
432
  status_text.config(state=tk.DISABLED)
433
433
  dialog.update()
434
434
 
435
- def install_clidriver(log_status):
436
- """Download and install IBM Db2 clidriver for ibm_db."""
437
- import platform
438
- import tarfile
439
- import urllib.request
440
- from pathlib import Path
441
-
442
- # Determine platform and download URL
443
- system = platform.system().lower()
444
- machine = platform.machine().lower()
445
-
446
- if system == "linux" and machine in ("x86_64", "amd64"):
447
- url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/linuxx64_odbc_cli.tar.gz"
448
- elif system == "linux" and machine in ("aarch64", "arm64"):
449
- url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/linuxarm64_odbc_cli.tar.gz"
450
- elif system == "darwin" and machine in ("x86_64", "amd64"):
451
- url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/macos64_odbc_cli.tar.gz"
452
- elif system == "darwin" and machine == "arm64":
453
- # M1/M2 Mac - use x86_64 version with Rosetta or native if available
454
- url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/macos64_odbc_cli.tar.gz"
455
- elif system == "windows":
456
- log_status(" Windows: Please download clidriver manually from IBM")
457
- return False
458
- else:
459
- log_status(f" Unsupported platform: {system}/{machine}")
460
- return False
461
-
462
- # Set up paths
463
- home = Path.home()
464
- driver_dir = home / "db2drivers"
465
- clidriver_path = driver_dir / "clidriver"
466
- tar_file = driver_dir / "odbc_cli.tar.gz"
467
-
468
- # Check if already installed
469
- if (clidriver_path / "lib").exists():
470
- log_status(" clidriver already installed")
471
- return True
472
-
473
- try:
474
- # Create directory
475
- driver_dir.mkdir(parents=True, exist_ok=True)
476
-
477
- # Download
478
- log_status(" Downloading clidriver (~100MB)...")
479
- urllib.request.urlretrieve(url, tar_file)
480
-
481
- # Extract
482
- log_status(" Extracting...")
483
- with tarfile.open(tar_file, "r:gz") as tar:
484
- tar.extractall(path=driver_dir)
485
-
486
- # Clean up tar file
487
- tar_file.unlink()
488
-
489
- # Set environment variable hint
490
- log_status(" clidriver installed to ~/db2drivers/clidriver")
491
-
492
- # Update environment for current process
493
- import os
494
- cli_path = str(clidriver_path)
495
- os.environ["IBM_DB_HOME"] = cli_path
496
- lib_path = os.environ.get("LD_LIBRARY_PATH", "")
497
- os.environ["LD_LIBRARY_PATH"] = f"{cli_path}/lib:{lib_path}"
498
-
499
- # Add to shell config
500
- shell_config = home / ".bashrc"
501
- if not shell_config.exists():
502
- shell_config = home / ".profile"
503
-
504
- config_lines = [
505
- f'\n# IBM Db2 clidriver (added by SQLBench)',
506
- f'export IBM_DB_HOME=~/db2drivers/clidriver',
507
- f'export LD_LIBRARY_PATH=$IBM_DB_HOME/lib:$LD_LIBRARY_PATH',
508
- ]
509
-
510
- # Check if already in config
511
- try:
512
- existing = shell_config.read_text() if shell_config.exists() else ""
513
- if "IBM_DB_HOME" not in existing:
514
- with open(shell_config, "a") as f:
515
- f.write("\n".join(config_lines) + "\n")
516
- log_status(f" Added environment vars to {shell_config.name}")
517
- except Exception:
518
- log_status(" Note: Add IBM_DB_HOME to your shell config manually")
519
-
520
- return True
521
-
522
- except Exception as e:
523
- log_status(f" clidriver install failed: {e}")
524
- return False
525
-
526
435
  def do_install():
527
436
  install_btn.config(state=tk.DISABLED)
528
437
  selected = [db_type for db_type, var in check_vars.items() if var.get()]
@@ -533,35 +442,20 @@ class ConnectionDialog:
533
442
 
534
443
  python = sys.executable
535
444
  for db_type in selected:
536
- extra = {"ibmi": "ibmi", "ibmi_db": "ibmi-db", "mysql": "mysql", "postgresql": "postgresql"}.get(db_type)
445
+ extra = {"ibmi": "ibmi", "mysql": "mysql", "postgresql": "postgresql"}.get(db_type)
537
446
  if extra:
538
- # Special handling for ibm_db - need clidriver first
539
- if db_type == "ibmi_db":
540
- log_status("Installing IBM clidriver...")
541
- if not install_clidriver(log_status):
542
- log_status(" Skipping ibm_db (clidriver required)")
543
- continue
544
-
545
447
  # Map db_type to actual pip package
546
448
  packages = {
547
449
  "ibmi": "pyodbc",
548
- "ibmi_db": "ibm_db",
549
450
  "mysql": "mysql-connector-python",
550
451
  "postgresql": "psycopg2-binary",
551
452
  }
552
453
  package = packages.get(db_type, extra)
553
454
  log_status(f"Installing {package}...")
554
455
  try:
555
- # Set up environment - inherit current env and add IBM_DB_HOME if needed
556
- env = os.environ.copy()
557
- if db_type == "ibmi_db":
558
- cli_path = Path.home() / "db2drivers" / "clidriver"
559
- if cli_path.exists():
560
- env["IBM_DB_HOME"] = str(cli_path)
561
-
562
456
  result = subprocess.run(
563
457
  [python, "-m", "pip", "install", package],
564
- capture_output=True, text=True, timeout=180, env=env
458
+ capture_output=True, text=True, timeout=180
565
459
  )
566
460
  if result.returncode == 0:
567
461
  log_status(f" {extra}: OK")
@@ -575,9 +469,17 @@ class ConnectionDialog:
575
469
 
576
470
  log_status("\nDone! Please restart SQLBench to use new drivers.")
577
471
  install_btn.config(state=tk.NORMAL)
472
+ dialog.after(0, lambda: restart_btn.pack(side=tk.LEFT, padx=5))
473
+
474
+ def do_restart():
475
+ dialog.destroy()
476
+ if self.app:
477
+ self.app._restart_app()
578
478
 
579
479
  btn_frame = ttk.Frame(dialog)
580
480
  btn_frame.pack(pady=10)
581
481
  install_btn = ttk.Button(btn_frame, text="Install Selected", command=lambda: threading.Thread(target=do_install, daemon=True).start())
582
482
  install_btn.pack(side=tk.LEFT, padx=5)
483
+ restart_btn = ttk.Button(btn_frame, text="Restart Now", command=do_restart)
484
+ # restart_btn is packed after installation completes
583
485
  ttk.Button(btn_frame, text="Close", command=dialog.destroy).pack(side=tk.LEFT, padx=5)
@@ -4,7 +4,7 @@ import threading
4
4
  import urllib.request
5
5
  import json
6
6
 
7
- __version__ = "0.1.7"
7
+ __version__ = "0.1.8"
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
File without changes