sqlbench 0.1.4__tar.gz → 0.1.6__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.4
3
+ Version: 0.1.6
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,6 +24,7 @@ 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'
27
28
  Requires-Dist: mysql-connector-python>=8.0.0; extra == 'all'
28
29
  Requires-Dist: openpyxl>=3.1.0; extra == 'all'
29
30
  Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
@@ -38,6 +39,8 @@ Requires-Dist: openpyxl>=3.1.0; extra == 'export'
38
39
  Requires-Dist: reportlab>=4.0.0; extra == 'export'
39
40
  Provides-Extra: ibmi
40
41
  Requires-Dist: pyodbc>=4.0.0; extra == 'ibmi'
42
+ Provides-Extra: ibmi-db
43
+ Requires-Dist: ibm-db>=3.0.0; extra == 'ibmi-db'
41
44
  Provides-Extra: mysql
42
45
  Requires-Dist: mysql-connector-python>=8.0.0; extra == 'mysql'
43
46
  Provides-Extra: postgresql
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlbench"
7
- version = "0.1.4"
7
+ version = "0.1.6"
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,11 +36,13 @@ 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"]
39
40
  export = ["reportlab>=4.0.0", "openpyxl>=3.1.0"]
40
41
  all = [
41
42
  "mysql-connector-python>=8.0.0",
42
43
  "psycopg2-binary>=2.9.0",
43
44
  "pyodbc>=4.0.0",
45
+ "ibm_db>=3.0.0",
44
46
  "reportlab>=4.0.0",
45
47
  "openpyxl>=3.1.0",
46
48
  ]
@@ -1,6 +1,32 @@
1
1
  """Database adapters for different database types."""
2
2
 
3
+ import os
3
4
  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()
4
30
 
5
31
 
6
32
  class DBAdapter(ABC):
@@ -175,6 +201,109 @@ class IBMiAdapter(DBAdapter):
175
201
  """
176
202
 
177
203
 
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
+
178
307
  class MySQLAdapter(DBAdapter):
179
308
  """Adapter for MySQL."""
180
309
 
@@ -384,6 +513,7 @@ class PostgreSQLAdapter(DBAdapter):
384
513
  # Registry of available adapters
385
514
  ADAPTERS = {
386
515
  'ibmi': IBMiAdapter,
516
+ 'ibmi_db': IBMiDBAdapter,
387
517
  'mysql': MySQLAdapter,
388
518
  'postgresql': PostgreSQLAdapter,
389
519
  }
@@ -7,6 +7,7 @@ from tkinter import ttk
7
7
 
8
8
  from sqlbench.database import Database
9
9
  from sqlbench.adapters import get_adapter, ADAPTERS
10
+ from sqlbench.version import __version__
10
11
  from sqlbench.tabs.sql_tab import SQLTab
11
12
  from sqlbench.tabs.spool_tab import SpoolTab
12
13
  from sqlbench.dialogs.connection_dialog import ConnectionDialog
@@ -18,7 +19,7 @@ class SQLBenchApp:
18
19
  # Set className for proper window manager integration (Linux/X11)
19
20
  # This makes the app show as "SQLBench" in window lists and match the .desktop file
20
21
  self.root = tk.Tk(className="sqlbench")
21
- self.root.title("SQLBench")
22
+ self.root.title(f"SQLBench v{__version__}")
22
23
 
23
24
  # Set window icon
24
25
  self._set_window_icon()
@@ -1,7 +1,9 @@
1
1
  """Connection management dialog."""
2
2
 
3
+ import os
3
4
  import tkinter as tk
4
5
  from tkinter import ttk, messagebox
6
+ from pathlib import Path
5
7
  import threading
6
8
 
7
9
  from sqlbench.adapters import get_adapter_choices, get_adapter, get_unavailable_adapters, ADAPTERS
@@ -204,7 +206,7 @@ class ConnectionDialog:
204
206
  for conn in self._connections:
205
207
  # Show type indicator
206
208
  db_type = conn.get("db_type", "ibmi")
207
- type_indicator = {"ibmi": "[i]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
209
+ type_indicator = {"ibmi": "[i]", "ibmi_db": "[I]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
208
210
  # Mark unavailable connections
209
211
  if db_type not in available_types:
210
212
  type_indicator = "[!]"
@@ -430,6 +432,97 @@ class ConnectionDialog:
430
432
  status_text.config(state=tk.DISABLED)
431
433
  dialog.update()
432
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
+
433
526
  def do_install():
434
527
  install_btn.config(state=tk.DISABLED)
435
528
  selected = [db_type for db_type, var in check_vars.items() if var.get()]
@@ -440,13 +533,35 @@ class ConnectionDialog:
440
533
 
441
534
  python = sys.executable
442
535
  for db_type in selected:
443
- extra = {"ibmi": "ibmi", "mysql": "mysql", "postgresql": "postgresql"}.get(db_type)
536
+ extra = {"ibmi": "ibmi", "ibmi_db": "ibmi-db", "mysql": "mysql", "postgresql": "postgresql"}.get(db_type)
444
537
  if extra:
445
- log_status(f"Installing sqlbench[{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
+ # Map db_type to actual pip package
546
+ packages = {
547
+ "ibmi": "pyodbc",
548
+ "ibmi_db": "ibm_db",
549
+ "mysql": "mysql-connector-python",
550
+ "postgresql": "psycopg2-binary",
551
+ }
552
+ package = packages.get(db_type, extra)
553
+ log_status(f"Installing {package}...")
446
554
  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
+
447
562
  result = subprocess.run(
448
- [python, "-m", "pip", "install", f"sqlbench[{extra}]"],
449
- capture_output=True, text=True, timeout=120
563
+ [python, "-m", "pip", "install", package],
564
+ capture_output=True, text=True, timeout=180, env=env
450
565
  )
451
566
  if result.returncode == 0:
452
567
  log_status(f" {extra}: OK")
@@ -4,7 +4,7 @@ import threading
4
4
  import urllib.request
5
5
  import json
6
6
 
7
- __version__ = "0.1.4"
7
+ __version__ = "0.1.6"
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