sqlbench 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- sqlbench/__main__.py +7 -0
- sqlbench/adapters.py +383 -0
- sqlbench/app.py +1398 -0
- sqlbench/database.py +215 -0
- sqlbench/dialogs/__init__.py +1 -0
- sqlbench/dialogs/connection_dialog.py +356 -0
- sqlbench/dialogs/regex_builder_dialog.py +542 -0
- sqlbench/tabs/__init__.py +1 -0
- sqlbench/tabs/spool_tab.py +572 -0
- sqlbench/tabs/sql_tab.py +1827 -0
- sqlbench-0.1.0.dist-info/METADATA +91 -0
- sqlbench-0.1.0.dist-info/RECORD +15 -0
- sqlbench-0.1.0.dist-info/WHEEL +4 -0
- sqlbench-0.1.0.dist-info/entry_points.txt +2 -0
sqlbench/database.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""SQLite database for storing connections and saved queries."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Database:
|
|
8
|
+
def __init__(self, db_path=None):
|
|
9
|
+
if db_path is None:
|
|
10
|
+
db_path = Path(__file__).parent / "iutil.db"
|
|
11
|
+
self.db_path = db_path
|
|
12
|
+
self._init_db()
|
|
13
|
+
|
|
14
|
+
def _get_conn(self):
|
|
15
|
+
return sqlite3.connect(self.db_path)
|
|
16
|
+
|
|
17
|
+
def _init_db(self):
|
|
18
|
+
with self._get_conn() as conn:
|
|
19
|
+
# Check if we need to migrate old connections table
|
|
20
|
+
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='connections'")
|
|
21
|
+
if cursor.fetchone():
|
|
22
|
+
# Check existing columns
|
|
23
|
+
cursor = conn.execute("PRAGMA table_info(connections)")
|
|
24
|
+
columns = [row[1] for row in cursor.fetchall()]
|
|
25
|
+
|
|
26
|
+
# Migration: add new columns if needed
|
|
27
|
+
needs_migration = 'db_type' not in columns or 'id' not in columns
|
|
28
|
+
|
|
29
|
+
if needs_migration:
|
|
30
|
+
conn.execute("ALTER TABLE connections RENAME TO connections_old")
|
|
31
|
+
conn.execute("""
|
|
32
|
+
CREATE TABLE connections (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
name TEXT NOT NULL UNIQUE,
|
|
35
|
+
db_type TEXT NOT NULL DEFAULT 'ibmi',
|
|
36
|
+
host TEXT NOT NULL,
|
|
37
|
+
port INTEGER,
|
|
38
|
+
database TEXT,
|
|
39
|
+
user TEXT NOT NULL,
|
|
40
|
+
password TEXT NOT NULL
|
|
41
|
+
)
|
|
42
|
+
""")
|
|
43
|
+
# Migrate data - existing connections become IBM i type
|
|
44
|
+
if 'id' in columns:
|
|
45
|
+
conn.execute("""
|
|
46
|
+
INSERT INTO connections (id, name, db_type, host, user, password)
|
|
47
|
+
SELECT id, name, 'ibmi', host, user, password FROM connections_old
|
|
48
|
+
""")
|
|
49
|
+
else:
|
|
50
|
+
conn.execute("""
|
|
51
|
+
INSERT INTO connections (name, db_type, host, user, password)
|
|
52
|
+
SELECT name, 'ibmi', host, user, password FROM connections_old
|
|
53
|
+
""")
|
|
54
|
+
conn.execute("DROP TABLE connections_old")
|
|
55
|
+
else:
|
|
56
|
+
conn.execute("""
|
|
57
|
+
CREATE TABLE connections (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
name TEXT NOT NULL UNIQUE,
|
|
60
|
+
db_type TEXT NOT NULL DEFAULT 'ibmi',
|
|
61
|
+
host TEXT NOT NULL,
|
|
62
|
+
port INTEGER,
|
|
63
|
+
database TEXT,
|
|
64
|
+
user TEXT NOT NULL,
|
|
65
|
+
password TEXT NOT NULL
|
|
66
|
+
)
|
|
67
|
+
""")
|
|
68
|
+
conn.execute("""
|
|
69
|
+
CREATE TABLE IF NOT EXISTS saved_queries (
|
|
70
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
71
|
+
name TEXT NOT NULL,
|
|
72
|
+
sql TEXT NOT NULL,
|
|
73
|
+
connection_name TEXT,
|
|
74
|
+
db_type TEXT,
|
|
75
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
78
|
+
# Migration: add db_type column if it doesn't exist
|
|
79
|
+
cursor = conn.execute("PRAGMA table_info(saved_queries)")
|
|
80
|
+
columns = [row[1] for row in cursor.fetchall()]
|
|
81
|
+
if 'db_type' not in columns:
|
|
82
|
+
conn.execute("ALTER TABLE saved_queries ADD COLUMN db_type TEXT")
|
|
83
|
+
conn.execute("""
|
|
84
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
85
|
+
key TEXT PRIMARY KEY,
|
|
86
|
+
value TEXT
|
|
87
|
+
)
|
|
88
|
+
""")
|
|
89
|
+
conn.execute("""
|
|
90
|
+
CREATE TABLE IF NOT EXISTS saved_tabs (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
tab_type TEXT NOT NULL,
|
|
93
|
+
connection_name TEXT NOT NULL,
|
|
94
|
+
tab_data TEXT,
|
|
95
|
+
tab_order INTEGER
|
|
96
|
+
)
|
|
97
|
+
""")
|
|
98
|
+
conn.commit()
|
|
99
|
+
|
|
100
|
+
# Connection methods
|
|
101
|
+
def get_connections(self):
|
|
102
|
+
with self._get_conn() as conn:
|
|
103
|
+
conn.row_factory = sqlite3.Row
|
|
104
|
+
cursor = conn.execute(
|
|
105
|
+
"SELECT id, name, db_type, host, port, database, user FROM connections ORDER BY name"
|
|
106
|
+
)
|
|
107
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
108
|
+
|
|
109
|
+
def get_connection(self, name):
|
|
110
|
+
with self._get_conn() as conn:
|
|
111
|
+
conn.row_factory = sqlite3.Row
|
|
112
|
+
cursor = conn.execute(
|
|
113
|
+
"SELECT id, name, db_type, host, port, database, user, password FROM connections WHERE name = ?",
|
|
114
|
+
(name,)
|
|
115
|
+
)
|
|
116
|
+
row = cursor.fetchone()
|
|
117
|
+
return dict(row) if row else None
|
|
118
|
+
|
|
119
|
+
def get_connection_by_id(self, conn_id):
|
|
120
|
+
with self._get_conn() as conn:
|
|
121
|
+
conn.row_factory = sqlite3.Row
|
|
122
|
+
cursor = conn.execute(
|
|
123
|
+
"SELECT id, name, db_type, host, port, database, user, password FROM connections WHERE id = ?",
|
|
124
|
+
(conn_id,)
|
|
125
|
+
)
|
|
126
|
+
row = cursor.fetchone()
|
|
127
|
+
return dict(row) if row else None
|
|
128
|
+
|
|
129
|
+
def save_connection(self, name, db_type, host, port, database, user, password, conn_id=None):
|
|
130
|
+
with self._get_conn() as conn:
|
|
131
|
+
if conn_id:
|
|
132
|
+
# Update existing connection
|
|
133
|
+
conn.execute(
|
|
134
|
+
"""UPDATE connections SET name = ?, db_type = ?, host = ?, port = ?,
|
|
135
|
+
database = ?, user = ?, password = ? WHERE id = ?""",
|
|
136
|
+
(name, db_type, host, port, database, user, password, conn_id)
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
# Insert new connection
|
|
140
|
+
conn.execute(
|
|
141
|
+
"""INSERT INTO connections (name, db_type, host, port, database, user, password)
|
|
142
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
143
|
+
(name, db_type, host, port, database, user, password)
|
|
144
|
+
)
|
|
145
|
+
conn.commit()
|
|
146
|
+
|
|
147
|
+
def delete_connection(self, conn_id):
|
|
148
|
+
with self._get_conn() as conn:
|
|
149
|
+
conn.execute("DELETE FROM connections WHERE id = ?", (conn_id,))
|
|
150
|
+
conn.commit()
|
|
151
|
+
|
|
152
|
+
# Saved query methods
|
|
153
|
+
def get_saved_queries(self, db_type=None):
|
|
154
|
+
with self._get_conn() as conn:
|
|
155
|
+
conn.row_factory = sqlite3.Row
|
|
156
|
+
if db_type:
|
|
157
|
+
cursor = conn.execute(
|
|
158
|
+
"SELECT id, name, sql, connection_name, db_type FROM saved_queries WHERE db_type = ? OR db_type IS NULL ORDER BY name",
|
|
159
|
+
(db_type,)
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
cursor = conn.execute(
|
|
163
|
+
"SELECT id, name, sql, connection_name, db_type FROM saved_queries ORDER BY name"
|
|
164
|
+
)
|
|
165
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
166
|
+
|
|
167
|
+
def save_query(self, name, sql, connection_name=None, db_type=None):
|
|
168
|
+
with self._get_conn() as conn:
|
|
169
|
+
conn.execute(
|
|
170
|
+
"""INSERT INTO saved_queries (name, sql, connection_name, db_type)
|
|
171
|
+
VALUES (?, ?, ?, ?)""",
|
|
172
|
+
(name, sql, connection_name, db_type)
|
|
173
|
+
)
|
|
174
|
+
conn.commit()
|
|
175
|
+
|
|
176
|
+
def delete_query(self, query_id):
|
|
177
|
+
with self._get_conn() as conn:
|
|
178
|
+
conn.execute("DELETE FROM saved_queries WHERE id = ?", (query_id,))
|
|
179
|
+
conn.commit()
|
|
180
|
+
|
|
181
|
+
# Tab state methods
|
|
182
|
+
def save_tabs(self, tabs):
|
|
183
|
+
"""Save tab state. tabs is a list of dicts with type, connection, data."""
|
|
184
|
+
with self._get_conn() as conn:
|
|
185
|
+
conn.execute("DELETE FROM saved_tabs")
|
|
186
|
+
for i, tab in enumerate(tabs):
|
|
187
|
+
conn.execute(
|
|
188
|
+
"INSERT INTO saved_tabs (tab_type, connection_name, tab_data, tab_order) VALUES (?, ?, ?, ?)",
|
|
189
|
+
(tab["type"], tab["connection"], tab.get("data", ""), i)
|
|
190
|
+
)
|
|
191
|
+
conn.commit()
|
|
192
|
+
|
|
193
|
+
def get_saved_tabs(self):
|
|
194
|
+
"""Get saved tab state."""
|
|
195
|
+
with self._get_conn() as conn:
|
|
196
|
+
conn.row_factory = sqlite3.Row
|
|
197
|
+
cursor = conn.execute(
|
|
198
|
+
"SELECT tab_type, connection_name, tab_data FROM saved_tabs ORDER BY tab_order"
|
|
199
|
+
)
|
|
200
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
201
|
+
|
|
202
|
+
# Settings methods
|
|
203
|
+
def get_setting(self, key, default=None):
|
|
204
|
+
with self._get_conn() as conn:
|
|
205
|
+
cursor = conn.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
|
206
|
+
row = cursor.fetchone()
|
|
207
|
+
return row[0] if row else default
|
|
208
|
+
|
|
209
|
+
def set_setting(self, key, value):
|
|
210
|
+
with self._get_conn() as conn:
|
|
211
|
+
conn.execute(
|
|
212
|
+
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
|
|
213
|
+
(key, value)
|
|
214
|
+
)
|
|
215
|
+
conn.commit()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dialog modules for SQLBench."""
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Connection management dialog."""
|
|
2
|
+
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from tkinter import ttk, messagebox
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
from sqlbench.adapters import get_adapter_choices, get_adapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConnectionDialog:
|
|
11
|
+
def __init__(self, parent, db, edit_name=None, app=None):
|
|
12
|
+
self.db = db
|
|
13
|
+
self.edit_name = edit_name
|
|
14
|
+
self.app = app
|
|
15
|
+
self._current_id = None # Track current connection ID
|
|
16
|
+
self._connections = [] # Store connections with IDs
|
|
17
|
+
self.top = tk.Toplevel(parent)
|
|
18
|
+
self.top.title("Connection" if edit_name else "Manage Connections")
|
|
19
|
+
self.top.geometry("720x500")
|
|
20
|
+
self.top.transient(parent)
|
|
21
|
+
self.top.grab_set()
|
|
22
|
+
|
|
23
|
+
# Apply theme
|
|
24
|
+
self._apply_theme()
|
|
25
|
+
|
|
26
|
+
self._create_widgets()
|
|
27
|
+
self._refresh_list()
|
|
28
|
+
|
|
29
|
+
# If editing specific connection, load it
|
|
30
|
+
if edit_name:
|
|
31
|
+
self._load_connection_by_name(edit_name)
|
|
32
|
+
|
|
33
|
+
# ESC to close
|
|
34
|
+
self.top.bind("<Escape>", lambda e: self.top.destroy())
|
|
35
|
+
|
|
36
|
+
def _apply_theme(self):
|
|
37
|
+
"""Apply dark/light theme colors."""
|
|
38
|
+
is_dark = self.app.dark_mode_var.get() if self.app else False
|
|
39
|
+
if is_dark:
|
|
40
|
+
self.bg = "#2b2b2b"
|
|
41
|
+
self.fg = "#a9b7c6"
|
|
42
|
+
self.list_bg = "#313335"
|
|
43
|
+
self.select_bg = "#214283"
|
|
44
|
+
self.select_fg = "#a9b7c6"
|
|
45
|
+
self.status_fg = "#a9b7c6" # For "Testing..." message
|
|
46
|
+
else:
|
|
47
|
+
self.bg = "#f0f0f0"
|
|
48
|
+
self.fg = "#000000"
|
|
49
|
+
self.list_bg = "#ffffff"
|
|
50
|
+
self.select_bg = "#0078d4"
|
|
51
|
+
self.select_fg = "#ffffff"
|
|
52
|
+
self.status_fg = "#000000"
|
|
53
|
+
|
|
54
|
+
self.top.configure(bg=self.bg)
|
|
55
|
+
|
|
56
|
+
def _create_widgets(self):
|
|
57
|
+
# Left side - list
|
|
58
|
+
list_frame = ttk.Frame(self.top)
|
|
59
|
+
list_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=5, pady=5)
|
|
60
|
+
|
|
61
|
+
self.conn_listbox = tk.Listbox(list_frame, width=25,
|
|
62
|
+
bg=self.list_bg, fg=self.fg,
|
|
63
|
+
selectbackground=self.select_bg,
|
|
64
|
+
selectforeground=self.select_fg,
|
|
65
|
+
highlightthickness=0)
|
|
66
|
+
self.conn_listbox.pack(fill=tk.BOTH, expand=True)
|
|
67
|
+
self.conn_listbox.bind("<<ListboxSelect>>", self._on_select)
|
|
68
|
+
|
|
69
|
+
btn_frame = ttk.Frame(list_frame)
|
|
70
|
+
btn_frame.pack(fill=tk.X, pady=5)
|
|
71
|
+
ttk.Button(btn_frame, text="New", command=self._new).pack(side=tk.LEFT, padx=2)
|
|
72
|
+
ttk.Button(btn_frame, text="Delete", command=self._delete).pack(side=tk.LEFT, padx=2)
|
|
73
|
+
|
|
74
|
+
# Right side - details
|
|
75
|
+
detail_frame = ttk.LabelFrame(self.top, text="Connection Details")
|
|
76
|
+
detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
77
|
+
|
|
78
|
+
# Configure column weights so entries expand
|
|
79
|
+
detail_frame.columnconfigure(1, weight=1)
|
|
80
|
+
|
|
81
|
+
row = 0
|
|
82
|
+
|
|
83
|
+
# Name
|
|
84
|
+
ttk.Label(detail_frame, text="Name:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
85
|
+
self.name_entry = ttk.Entry(detail_frame, width=40)
|
|
86
|
+
self.name_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
|
|
87
|
+
row += 1
|
|
88
|
+
|
|
89
|
+
# Database Type
|
|
90
|
+
ttk.Label(detail_frame, text="Type:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
91
|
+
self.db_type_var = tk.StringVar(value="ibmi")
|
|
92
|
+
self.db_type_combo = ttk.Combobox(
|
|
93
|
+
detail_frame,
|
|
94
|
+
textvariable=self.db_type_var,
|
|
95
|
+
values=[choice[1] for choice in get_adapter_choices()],
|
|
96
|
+
state="readonly",
|
|
97
|
+
width=37
|
|
98
|
+
)
|
|
99
|
+
self.db_type_combo.grid(row=row, column=1, columnspan=2, sticky=tk.W, padx=5, pady=5)
|
|
100
|
+
self.db_type_combo.bind("<<ComboboxSelected>>", self._on_type_change)
|
|
101
|
+
# Map display names to db_type keys
|
|
102
|
+
self._type_map = {choice[1]: choice[0] for choice in get_adapter_choices()}
|
|
103
|
+
self._type_map_reverse = {choice[0]: choice[1] for choice in get_adapter_choices()}
|
|
104
|
+
self.db_type_combo.set("IBM i")
|
|
105
|
+
row += 1
|
|
106
|
+
|
|
107
|
+
# Host
|
|
108
|
+
ttk.Label(detail_frame, text="Host:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
109
|
+
self.host_entry = ttk.Entry(detail_frame, width=40)
|
|
110
|
+
self.host_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
|
|
111
|
+
row += 1
|
|
112
|
+
|
|
113
|
+
# Port
|
|
114
|
+
self.port_label = ttk.Label(detail_frame, text="Port:")
|
|
115
|
+
self.port_label.grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
116
|
+
self.port_entry = ttk.Entry(detail_frame, width=10)
|
|
117
|
+
self.port_entry.grid(row=row, column=1, sticky=tk.W, padx=5, pady=5)
|
|
118
|
+
row += 1
|
|
119
|
+
|
|
120
|
+
# Database
|
|
121
|
+
self.database_label = ttk.Label(detail_frame, text="Database:")
|
|
122
|
+
self.database_label.grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
123
|
+
self.database_entry = ttk.Entry(detail_frame, width=40)
|
|
124
|
+
self.database_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
|
|
125
|
+
row += 1
|
|
126
|
+
|
|
127
|
+
# User
|
|
128
|
+
ttk.Label(detail_frame, text="User:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
129
|
+
self.user_entry = ttk.Entry(detail_frame, width=40)
|
|
130
|
+
self.user_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
|
|
131
|
+
row += 1
|
|
132
|
+
|
|
133
|
+
# Password
|
|
134
|
+
ttk.Label(detail_frame, text="Password:").grid(row=row, column=0, sticky=tk.W, padx=5, pady=5)
|
|
135
|
+
self.pass_entry = ttk.Entry(detail_frame, width=40, show="*")
|
|
136
|
+
self.pass_entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
|
|
137
|
+
row += 1
|
|
138
|
+
|
|
139
|
+
# Test and Save buttons
|
|
140
|
+
btn_frame = ttk.Frame(detail_frame)
|
|
141
|
+
btn_frame.grid(row=row, column=0, columnspan=3, pady=20)
|
|
142
|
+
|
|
143
|
+
self.test_btn = ttk.Button(btn_frame, text="Test", command=self._test)
|
|
144
|
+
self.test_btn.pack(side=tk.LEFT, padx=5)
|
|
145
|
+
|
|
146
|
+
ttk.Button(btn_frame, text="Save", command=self._save).pack(side=tk.LEFT, padx=5)
|
|
147
|
+
|
|
148
|
+
# Status label for test results (with word wrap)
|
|
149
|
+
self.test_status = ttk.Label(detail_frame, text="", wraplength=350)
|
|
150
|
+
self.test_status.grid(row=row + 1, column=0, columnspan=3, padx=10, pady=5)
|
|
151
|
+
|
|
152
|
+
# Initial visibility based on default type
|
|
153
|
+
self._update_field_visibility()
|
|
154
|
+
|
|
155
|
+
def _on_type_change(self, event=None):
|
|
156
|
+
"""Handle database type change."""
|
|
157
|
+
self._update_field_visibility()
|
|
158
|
+
|
|
159
|
+
def _update_field_visibility(self):
|
|
160
|
+
"""Show/hide fields based on database type."""
|
|
161
|
+
display_name = self.db_type_combo.get()
|
|
162
|
+
db_type = self._type_map.get(display_name, "ibmi")
|
|
163
|
+
adapter = get_adapter(db_type)
|
|
164
|
+
|
|
165
|
+
# Show/hide port field
|
|
166
|
+
if adapter.default_port:
|
|
167
|
+
self.port_label.grid()
|
|
168
|
+
self.port_entry.grid()
|
|
169
|
+
if not self.port_entry.get():
|
|
170
|
+
self.port_entry.delete(0, tk.END)
|
|
171
|
+
self.port_entry.insert(0, str(adapter.default_port))
|
|
172
|
+
else:
|
|
173
|
+
self.port_label.grid_remove()
|
|
174
|
+
self.port_entry.grid_remove()
|
|
175
|
+
|
|
176
|
+
# Show/hide database field
|
|
177
|
+
if adapter.requires_database:
|
|
178
|
+
self.database_label.grid()
|
|
179
|
+
self.database_entry.grid()
|
|
180
|
+
else:
|
|
181
|
+
self.database_label.grid_remove()
|
|
182
|
+
self.database_entry.grid_remove()
|
|
183
|
+
|
|
184
|
+
def _refresh_list(self):
|
|
185
|
+
self.conn_listbox.delete(0, tk.END)
|
|
186
|
+
self._connections = self.db.get_connections()
|
|
187
|
+
for conn in self._connections:
|
|
188
|
+
# Show type indicator
|
|
189
|
+
db_type = conn.get("db_type", "ibmi")
|
|
190
|
+
type_indicator = {"ibmi": "[i]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
|
|
191
|
+
self.conn_listbox.insert(tk.END, f"{type_indicator} {conn['name']}")
|
|
192
|
+
|
|
193
|
+
def _load_connection_by_name(self, name):
|
|
194
|
+
"""Load a specific connection into the form by name."""
|
|
195
|
+
conn = self.db.get_connection(name)
|
|
196
|
+
if conn:
|
|
197
|
+
self._current_id = conn["id"]
|
|
198
|
+
self._fill_form(conn)
|
|
199
|
+
|
|
200
|
+
def _fill_form(self, conn):
|
|
201
|
+
"""Fill the form with connection data."""
|
|
202
|
+
self.name_entry.delete(0, tk.END)
|
|
203
|
+
self.name_entry.insert(0, conn["name"])
|
|
204
|
+
|
|
205
|
+
# Set database type
|
|
206
|
+
db_type = conn.get("db_type", "ibmi")
|
|
207
|
+
display_name = self._type_map_reverse.get(db_type, "IBM i")
|
|
208
|
+
self.db_type_combo.set(display_name)
|
|
209
|
+
self._update_field_visibility()
|
|
210
|
+
|
|
211
|
+
self.host_entry.delete(0, tk.END)
|
|
212
|
+
self.host_entry.insert(0, conn["host"])
|
|
213
|
+
|
|
214
|
+
self.port_entry.delete(0, tk.END)
|
|
215
|
+
if conn.get("port"):
|
|
216
|
+
self.port_entry.insert(0, str(conn["port"]))
|
|
217
|
+
else:
|
|
218
|
+
# Set default port for type
|
|
219
|
+
adapter = get_adapter(db_type)
|
|
220
|
+
if adapter.default_port:
|
|
221
|
+
self.port_entry.insert(0, str(adapter.default_port))
|
|
222
|
+
|
|
223
|
+
self.database_entry.delete(0, tk.END)
|
|
224
|
+
if conn.get("database"):
|
|
225
|
+
self.database_entry.insert(0, conn["database"])
|
|
226
|
+
|
|
227
|
+
self.user_entry.delete(0, tk.END)
|
|
228
|
+
self.user_entry.insert(0, conn["user"])
|
|
229
|
+
|
|
230
|
+
self.pass_entry.delete(0, tk.END)
|
|
231
|
+
self.pass_entry.insert(0, conn["password"])
|
|
232
|
+
|
|
233
|
+
def _on_select(self, event):
|
|
234
|
+
selection = self.conn_listbox.curselection()
|
|
235
|
+
if not selection:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
idx = selection[0]
|
|
239
|
+
if idx < len(self._connections):
|
|
240
|
+
conn = self._connections[idx]
|
|
241
|
+
self._current_id = conn["id"]
|
|
242
|
+
# Fetch full connection with password
|
|
243
|
+
full_conn = self.db.get_connection_by_id(conn["id"])
|
|
244
|
+
if full_conn:
|
|
245
|
+
self._fill_form(full_conn)
|
|
246
|
+
|
|
247
|
+
def _new(self):
|
|
248
|
+
self._current_id = None
|
|
249
|
+
self.name_entry.delete(0, tk.END)
|
|
250
|
+
self.db_type_combo.set("IBM i")
|
|
251
|
+
self._update_field_visibility()
|
|
252
|
+
self.host_entry.delete(0, tk.END)
|
|
253
|
+
self.port_entry.delete(0, tk.END)
|
|
254
|
+
self.database_entry.delete(0, tk.END)
|
|
255
|
+
self.user_entry.delete(0, tk.END)
|
|
256
|
+
self.pass_entry.delete(0, tk.END)
|
|
257
|
+
self.name_entry.focus()
|
|
258
|
+
|
|
259
|
+
def _save(self):
|
|
260
|
+
name = self.name_entry.get().strip()
|
|
261
|
+
display_name = self.db_type_combo.get()
|
|
262
|
+
db_type = self._type_map.get(display_name, "ibmi")
|
|
263
|
+
host = self.host_entry.get().strip()
|
|
264
|
+
port_str = self.port_entry.get().strip()
|
|
265
|
+
port = int(port_str) if port_str else None
|
|
266
|
+
database = self.database_entry.get().strip() or None
|
|
267
|
+
user = self.user_entry.get().strip()
|
|
268
|
+
password = self.pass_entry.get()
|
|
269
|
+
|
|
270
|
+
if not all([name, host, user]):
|
|
271
|
+
messagebox.showwarning("Missing Fields", "Name, Host, and User are required.")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# Validate required database for certain types
|
|
275
|
+
adapter = get_adapter(db_type)
|
|
276
|
+
if adapter.requires_database and not database:
|
|
277
|
+
messagebox.showwarning("Missing Fields", "Database name is required for this connection type.")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
self.db.save_connection(name, db_type, host, port, database, user, password, conn_id=self._current_id)
|
|
281
|
+
self._refresh_list()
|
|
282
|
+
messagebox.showinfo("Saved", f"Connection '{name}' saved.")
|
|
283
|
+
|
|
284
|
+
def _delete(self):
|
|
285
|
+
selection = self.conn_listbox.curselection()
|
|
286
|
+
if not selection:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
idx = selection[0]
|
|
290
|
+
if idx < len(self._connections):
|
|
291
|
+
conn = self._connections[idx]
|
|
292
|
+
if messagebox.askyesno("Confirm Delete", f"Delete connection '{conn['name']}'?"):
|
|
293
|
+
self.db.delete_connection(conn["id"])
|
|
294
|
+
self._refresh_list()
|
|
295
|
+
self._new()
|
|
296
|
+
|
|
297
|
+
def _test(self):
|
|
298
|
+
"""Test the connection with current form values."""
|
|
299
|
+
display_name = self.db_type_combo.get()
|
|
300
|
+
db_type = self._type_map.get(display_name, "ibmi")
|
|
301
|
+
host = self.host_entry.get().strip()
|
|
302
|
+
port_str = self.port_entry.get().strip()
|
|
303
|
+
try:
|
|
304
|
+
port = int(port_str) if port_str else None
|
|
305
|
+
except ValueError:
|
|
306
|
+
self.test_status.config(text="Port must be a number", foreground="red")
|
|
307
|
+
return
|
|
308
|
+
database = self.database_entry.get().strip() or None
|
|
309
|
+
user = self.user_entry.get().strip()
|
|
310
|
+
password = self.pass_entry.get()
|
|
311
|
+
|
|
312
|
+
if not all([host, user]):
|
|
313
|
+
self.test_status.config(text="Host and User are required", foreground="red")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
adapter = get_adapter(db_type)
|
|
317
|
+
|
|
318
|
+
# Use default port if not specified
|
|
319
|
+
if port is None and adapter.default_port:
|
|
320
|
+
port = adapter.default_port
|
|
321
|
+
if adapter.requires_database and not database:
|
|
322
|
+
self.test_status.config(text="Database name required", foreground="red")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# Disable test button and show testing status
|
|
326
|
+
self.test_btn.config(state=tk.DISABLED)
|
|
327
|
+
self.test_status.config(text="Testing connection...", foreground=self.status_fg)
|
|
328
|
+
self.top.update()
|
|
329
|
+
|
|
330
|
+
# Run test in background thread
|
|
331
|
+
def do_test():
|
|
332
|
+
try:
|
|
333
|
+
conn = adapter.connect(host, user, password, port, database)
|
|
334
|
+
# Try a simple query to verify connection
|
|
335
|
+
cursor = conn.cursor()
|
|
336
|
+
cursor.execute(adapter.get_version_query())
|
|
337
|
+
version = cursor.fetchone()[0] if cursor.description else "Connected"
|
|
338
|
+
cursor.close()
|
|
339
|
+
conn.close()
|
|
340
|
+
self.top.after(0, self._test_success, str(version)[:50])
|
|
341
|
+
except Exception as e:
|
|
342
|
+
self.top.after(0, self._test_failure, str(e))
|
|
343
|
+
|
|
344
|
+
thread = threading.Thread(target=do_test, daemon=True)
|
|
345
|
+
thread.start()
|
|
346
|
+
|
|
347
|
+
def _test_success(self, version):
|
|
348
|
+
"""Handle successful connection test."""
|
|
349
|
+
self.test_btn.config(state=tk.NORMAL)
|
|
350
|
+
self.test_status.config(text=f"Success! {version}", foreground="green")
|
|
351
|
+
|
|
352
|
+
def _test_failure(self, error):
|
|
353
|
+
"""Handle failed connection test."""
|
|
354
|
+
self.test_btn.config(state=tk.NORMAL)
|
|
355
|
+
print(f"Connection test failed: {error}") # Debug output
|
|
356
|
+
self.test_status.config(text=f"Failed: {error}", foreground="red")
|