mcp-dbutils 0.17.0__py3-none-any.whl → 0.19.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.
- mcp_dbutils/base.py +364 -178
- mcp_dbutils/mysql/handler.py +52 -32
- mcp_dbutils/postgres/handler.py +50 -30
- mcp_dbutils/sqlite/handler.py +68 -53
- mcp_dbutils-0.19.0.dist-info/METADATA +145 -0
- {mcp_dbutils-0.17.0.dist-info → mcp_dbutils-0.19.0.dist-info}/RECORD +9 -9
- mcp_dbutils-0.17.0.dist-info/METADATA +0 -308
- {mcp_dbutils-0.17.0.dist-info → mcp_dbutils-0.19.0.dist-info}/WHEEL +0 -0
- {mcp_dbutils-0.17.0.dist-info → mcp_dbutils-0.19.0.dist-info}/entry_points.txt +0 -0
- {mcp_dbutils-0.17.0.dist-info → mcp_dbutils-0.19.0.dist-info}/licenses/LICENSE +0 -0
mcp_dbutils/mysql/handler.py
CHANGED
@@ -33,11 +33,11 @@ class MySQLHandler(ConnectionHandler):
|
|
33
33
|
|
34
34
|
async def _check_table_exists(self, cursor, table_name: str) -> None:
|
35
35
|
"""检查表是否存在
|
36
|
-
|
36
|
+
|
37
37
|
Args:
|
38
38
|
cursor: 数据库游标
|
39
39
|
table_name: 表名
|
40
|
-
|
40
|
+
|
41
41
|
Raises:
|
42
42
|
ConnectionHandlerError: 如果表不存在
|
43
43
|
"""
|
@@ -47,19 +47,19 @@ class MySQLHandler(ConnectionHandler):
|
|
47
47
|
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
48
48
|
""", (self.config.database, table_name))
|
49
49
|
table_exists = cursor.fetchone()
|
50
|
-
|
50
|
+
|
51
51
|
# Handle different formats of cursor results (dict or tuple)
|
52
52
|
if not table_exists:
|
53
53
|
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
54
|
-
|
54
|
+
|
55
55
|
# If fetchone returns a dictionary (dictionary=True was used)
|
56
56
|
if isinstance(table_exists, dict) and 'count' in table_exists and table_exists['count'] == 0:
|
57
57
|
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
58
|
-
|
58
|
+
|
59
59
|
# If fetchone returns a tuple
|
60
60
|
if isinstance(table_exists, tuple) and table_exists[0] == 0:
|
61
61
|
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
62
|
-
|
62
|
+
|
63
63
|
async def get_tables(self) -> list[types.Resource]:
|
64
64
|
"""Get all table resources"""
|
65
65
|
try:
|
@@ -67,7 +67,7 @@ class MySQLHandler(ConnectionHandler):
|
|
67
67
|
conn = mysql.connector.connect(**conn_params)
|
68
68
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
69
69
|
cur.execute("""
|
70
|
-
SELECT
|
70
|
+
SELECT
|
71
71
|
TABLE_NAME as table_name,
|
72
72
|
TABLE_COMMENT as description
|
73
73
|
FROM information_schema.tables
|
@@ -98,7 +98,7 @@ class MySQLHandler(ConnectionHandler):
|
|
98
98
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
99
99
|
# Get column information
|
100
100
|
cur.execute("""
|
101
|
-
SELECT
|
101
|
+
SELECT
|
102
102
|
COLUMN_NAME as column_name,
|
103
103
|
DATA_TYPE as data_type,
|
104
104
|
IS_NULLABLE as is_nullable,
|
@@ -151,7 +151,7 @@ class MySQLHandler(ConnectionHandler):
|
|
151
151
|
# Check if the query is a SELECT statement
|
152
152
|
sql_upper = sql.strip().upper()
|
153
153
|
is_select = sql_upper.startswith("SELECT")
|
154
|
-
|
154
|
+
|
155
155
|
# Only set read-only transaction for SELECT statements
|
156
156
|
if is_select:
|
157
157
|
cur.execute("SET TRANSACTION READ ONLY")
|
@@ -188,12 +188,12 @@ class MySQLHandler(ConnectionHandler):
|
|
188
188
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
189
189
|
# Check if table exists
|
190
190
|
await self._check_table_exists(cur, table_name)
|
191
|
-
|
191
|
+
|
192
192
|
# Get table information and comment
|
193
193
|
cur.execute("""
|
194
|
-
SELECT
|
194
|
+
SELECT
|
195
195
|
TABLE_COMMENT as table_comment
|
196
|
-
FROM information_schema.tables
|
196
|
+
FROM information_schema.tables
|
197
197
|
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
198
198
|
""", (self.config.database, table_name))
|
199
199
|
table_info = cur.fetchone()
|
@@ -201,7 +201,7 @@ class MySQLHandler(ConnectionHandler):
|
|
201
201
|
|
202
202
|
# Get column information
|
203
203
|
cur.execute("""
|
204
|
-
SELECT
|
204
|
+
SELECT
|
205
205
|
COLUMN_NAME as column_name,
|
206
206
|
DATA_TYPE as data_type,
|
207
207
|
COLUMN_DEFAULT as column_default,
|
@@ -222,14 +222,14 @@ class MySQLHandler(ConnectionHandler):
|
|
222
222
|
f"Comment: {table_comment or 'No comment'}\n",
|
223
223
|
COLUMNS_HEADER
|
224
224
|
]
|
225
|
-
|
225
|
+
|
226
226
|
for col in columns:
|
227
227
|
col_info = [
|
228
228
|
f" {col['column_name']} ({col['data_type']})",
|
229
229
|
f" Nullable: {col['is_nullable']}",
|
230
230
|
f" Default: {col['column_default'] or 'None'}"
|
231
231
|
]
|
232
|
-
|
232
|
+
|
233
233
|
if col['character_maximum_length']:
|
234
234
|
col_info.append(f" Max Length: {col['character_maximum_length']}")
|
235
235
|
if col['numeric_precision']:
|
@@ -238,12 +238,12 @@ class MySQLHandler(ConnectionHandler):
|
|
238
238
|
col_info.append(f" Scale: {col['numeric_scale']}")
|
239
239
|
if col['column_comment']:
|
240
240
|
col_info.append(f" Comment: {col['column_comment']}")
|
241
|
-
|
241
|
+
|
242
242
|
description.extend(col_info)
|
243
243
|
description.append("") # Empty line between columns
|
244
|
-
|
244
|
+
|
245
245
|
return "\n".join(description)
|
246
|
-
|
246
|
+
|
247
247
|
except mysql.connector.Error as e:
|
248
248
|
error_msg = f"Failed to get table description: {str(e)}"
|
249
249
|
self.stats.record_error(e.__class__.__name__)
|
@@ -265,7 +265,7 @@ class MySQLHandler(ConnectionHandler):
|
|
265
265
|
if result:
|
266
266
|
return result['Create Table']
|
267
267
|
return f"Failed to get DDL for table {table_name}"
|
268
|
-
|
268
|
+
|
269
269
|
except mysql.connector.Error as e:
|
270
270
|
error_msg = f"Failed to get table DDL: {str(e)}"
|
271
271
|
self.stats.record_error(e.__class__.__name__)
|
@@ -286,7 +286,7 @@ class MySQLHandler(ConnectionHandler):
|
|
286
286
|
|
287
287
|
# Get index information
|
288
288
|
cur.execute("""
|
289
|
-
SELECT
|
289
|
+
SELECT
|
290
290
|
INDEX_NAME as index_name,
|
291
291
|
COLUMN_NAME as column_name,
|
292
292
|
NON_UNIQUE as non_unique,
|
@@ -305,7 +305,7 @@ class MySQLHandler(ConnectionHandler):
|
|
305
305
|
current_index = None
|
306
306
|
formatted_indexes = []
|
307
307
|
index_info = []
|
308
|
-
|
308
|
+
|
309
309
|
for idx in indexes:
|
310
310
|
if current_index != idx['index_name']:
|
311
311
|
if index_info:
|
@@ -320,14 +320,14 @@ class MySQLHandler(ConnectionHandler):
|
|
320
320
|
]
|
321
321
|
if idx['index_comment']:
|
322
322
|
index_info.insert(1, f"Comment: {idx['index_comment']}")
|
323
|
-
|
323
|
+
|
324
324
|
index_info.append(f" - {idx['column_name']}")
|
325
325
|
|
326
326
|
if index_info:
|
327
327
|
formatted_indexes.extend(index_info)
|
328
328
|
|
329
329
|
return "\n".join(formatted_indexes)
|
330
|
-
|
330
|
+
|
331
331
|
except mysql.connector.Error as e:
|
332
332
|
error_msg = f"Failed to get index information: {str(e)}"
|
333
333
|
self.stats.record_error(e.__class__.__name__)
|
@@ -345,10 +345,10 @@ class MySQLHandler(ConnectionHandler):
|
|
345
345
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
346
346
|
# Check if table exists
|
347
347
|
await self._check_table_exists(cur, table_name)
|
348
|
-
|
348
|
+
|
349
349
|
# Get table statistics
|
350
350
|
cur.execute("""
|
351
|
-
SELECT
|
351
|
+
SELECT
|
352
352
|
TABLE_ROWS as table_rows,
|
353
353
|
AVG_ROW_LENGTH as avg_row_length,
|
354
354
|
DATA_LENGTH as data_length,
|
@@ -364,7 +364,7 @@ class MySQLHandler(ConnectionHandler):
|
|
364
364
|
|
365
365
|
# Get column statistics
|
366
366
|
cur.execute("""
|
367
|
-
SELECT
|
367
|
+
SELECT
|
368
368
|
COLUMN_NAME as column_name,
|
369
369
|
DATA_TYPE as data_type,
|
370
370
|
COLUMN_TYPE as column_type
|
@@ -413,21 +413,21 @@ class MySQLHandler(ConnectionHandler):
|
|
413
413
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
414
414
|
# Check if table exists
|
415
415
|
await self._check_table_exists(cur, table_name)
|
416
|
-
|
416
|
+
|
417
417
|
# Get constraint information
|
418
418
|
cur.execute("""
|
419
|
-
SELECT
|
419
|
+
SELECT
|
420
420
|
k.CONSTRAINT_NAME as constraint_name,
|
421
421
|
t.CONSTRAINT_TYPE as constraint_type,
|
422
422
|
k.COLUMN_NAME as column_name,
|
423
423
|
k.REFERENCED_TABLE_NAME as referenced_table_name,
|
424
424
|
k.REFERENCED_COLUMN_NAME as referenced_column_name
|
425
425
|
FROM information_schema.key_column_usage k
|
426
|
-
JOIN information_schema.table_constraints t
|
426
|
+
JOIN information_schema.table_constraints t
|
427
427
|
ON k.CONSTRAINT_NAME = t.CONSTRAINT_NAME
|
428
428
|
AND k.TABLE_SCHEMA = t.TABLE_SCHEMA
|
429
429
|
AND k.TABLE_NAME = t.TABLE_NAME
|
430
|
-
WHERE k.TABLE_SCHEMA = %s
|
430
|
+
WHERE k.TABLE_SCHEMA = %s
|
431
431
|
AND k.TABLE_NAME = %s
|
432
432
|
ORDER BY t.CONSTRAINT_TYPE, k.CONSTRAINT_NAME, k.ORDINAL_POSITION
|
433
433
|
""", (self.config.database, table_name))
|
@@ -451,7 +451,7 @@ class MySQLHandler(ConnectionHandler):
|
|
451
451
|
f"\n{con['constraint_type']} Constraint: {con['constraint_name']}",
|
452
452
|
COLUMNS_HEADER
|
453
453
|
]
|
454
|
-
|
454
|
+
|
455
455
|
col_info = f" - {con['column_name']}"
|
456
456
|
if con['referenced_table_name']:
|
457
457
|
col_info += f" -> {con['referenced_table_name']}.{con['referenced_column_name']}"
|
@@ -493,7 +493,7 @@ class MySQLHandler(ConnectionHandler):
|
|
493
493
|
]
|
494
494
|
for row in explain_result:
|
495
495
|
output.append(str(row['EXPLAIN']))
|
496
|
-
|
496
|
+
|
497
497
|
output.extend([
|
498
498
|
"\nActual Plan (ANALYZE):",
|
499
499
|
"----------------------"
|
@@ -511,6 +511,26 @@ class MySQLHandler(ConnectionHandler):
|
|
511
511
|
if conn:
|
512
512
|
conn.close()
|
513
513
|
|
514
|
+
async def test_connection(self) -> bool:
|
515
|
+
"""Test database connection
|
516
|
+
|
517
|
+
Returns:
|
518
|
+
bool: True if connection is successful, False otherwise
|
519
|
+
"""
|
520
|
+
conn = None
|
521
|
+
try:
|
522
|
+
conn_params = self.config.get_connection_params()
|
523
|
+
conn = mysql.connector.connect(**conn_params)
|
524
|
+
with conn.cursor() as cur:
|
525
|
+
cur.execute("SELECT 1")
|
526
|
+
return True
|
527
|
+
except mysql.connector.Error as e:
|
528
|
+
self.log("error", f"Connection test failed: {str(e)}")
|
529
|
+
return False
|
530
|
+
finally:
|
531
|
+
if conn:
|
532
|
+
conn.close()
|
533
|
+
|
514
534
|
async def cleanup(self):
|
515
535
|
"""Cleanup resources"""
|
516
536
|
# Log final stats before cleanup
|
mcp_dbutils/postgres/handler.py
CHANGED
@@ -165,7 +165,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
165
165
|
(quote_ident(table_schema) || '.' || quote_ident(table_name))::regclass,
|
166
166
|
'pg_class'
|
167
167
|
) as table_comment
|
168
|
-
FROM information_schema.tables
|
168
|
+
FROM information_schema.tables
|
169
169
|
WHERE table_name = %s
|
170
170
|
""", (table_name,))
|
171
171
|
table_info = cur.fetchone()
|
@@ -173,7 +173,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
173
173
|
|
174
174
|
# 获取列信息
|
175
175
|
cur.execute("""
|
176
|
-
SELECT
|
176
|
+
SELECT
|
177
177
|
column_name,
|
178
178
|
data_type,
|
179
179
|
column_default,
|
@@ -197,14 +197,14 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
197
197
|
f"Comment: {table_comment or 'No comment'}\n",
|
198
198
|
COLUMNS_HEADER
|
199
199
|
]
|
200
|
-
|
200
|
+
|
201
201
|
for col in columns:
|
202
202
|
col_info = [
|
203
203
|
f" {col[0]} ({col[1]})",
|
204
204
|
f" Nullable: {col[3]}",
|
205
205
|
f" Default: {col[2] or 'None'}"
|
206
206
|
]
|
207
|
-
|
207
|
+
|
208
208
|
if col[4]: # character_maximum_length
|
209
209
|
col_info.append(f" Max Length: {col[4]}")
|
210
210
|
if col[5]: # numeric_precision
|
@@ -213,12 +213,12 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
213
213
|
col_info.append(f" Scale: {col[6]}")
|
214
214
|
if col[7]: # column_comment
|
215
215
|
col_info.append(f" Comment: {col[7]}")
|
216
|
-
|
216
|
+
|
217
217
|
description.extend(col_info)
|
218
218
|
description.append("") # Empty line between columns
|
219
|
-
|
219
|
+
|
220
220
|
return "\n".join(description)
|
221
|
-
|
221
|
+
|
222
222
|
except psycopg2.Error as e:
|
223
223
|
error_msg = f"Failed to get index information: [Code: {e.pgcode}] {e.pgerror or str(e)}"
|
224
224
|
self.stats.record_error(e.__class__.__name__)
|
@@ -236,7 +236,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
236
236
|
with conn.cursor() as cur:
|
237
237
|
# 获取列定义
|
238
238
|
cur.execute("""
|
239
|
-
SELECT
|
239
|
+
SELECT
|
240
240
|
column_name,
|
241
241
|
data_type,
|
242
242
|
column_default,
|
@@ -252,7 +252,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
252
252
|
|
253
253
|
# 获取约束
|
254
254
|
cur.execute("""
|
255
|
-
SELECT
|
255
|
+
SELECT
|
256
256
|
conname as constraint_name,
|
257
257
|
pg_get_constraintdef(c.oid) as constraint_def
|
258
258
|
FROM pg_constraint c
|
@@ -263,12 +263,12 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
263
263
|
|
264
264
|
# 构建CREATE TABLE语句
|
265
265
|
ddl = [f"CREATE TABLE {table_name} ("]
|
266
|
-
|
266
|
+
|
267
267
|
# 添加列定义
|
268
268
|
column_defs = []
|
269
269
|
for col in columns:
|
270
270
|
col_def = [f" {col[0]} {col[1]}"]
|
271
|
-
|
271
|
+
|
272
272
|
if col[4]: # character_maximum_length
|
273
273
|
col_def[0] = f"{col_def[0]}({col[4]})"
|
274
274
|
elif col[5]: # numeric_precision
|
@@ -276,24 +276,24 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
276
276
|
col_def[0] = f"{col_def[0]}({col[5]},{col[6]})"
|
277
277
|
else:
|
278
278
|
col_def[0] = f"{col_def[0]}({col[5]})"
|
279
|
-
|
279
|
+
|
280
280
|
if col[2]: # default
|
281
281
|
col_def.append(f"DEFAULT {col[2]}")
|
282
282
|
if col[3] == 'NO': # not null
|
283
283
|
col_def.append("NOT NULL")
|
284
|
-
|
284
|
+
|
285
285
|
column_defs.append(" ".join(col_def))
|
286
|
-
|
286
|
+
|
287
287
|
# 添加约束定义
|
288
288
|
for con in constraints:
|
289
289
|
column_defs.append(f" CONSTRAINT {con[0]} {con[1]}")
|
290
|
-
|
290
|
+
|
291
291
|
ddl.append(",\n".join(column_defs))
|
292
292
|
ddl.append(");")
|
293
|
-
|
293
|
+
|
294
294
|
# 添加注释
|
295
295
|
cur.execute("""
|
296
|
-
SELECT
|
296
|
+
SELECT
|
297
297
|
c.column_name,
|
298
298
|
col_description(
|
299
299
|
(quote_ident(table_schema) || '.' || quote_ident(table_name))::regclass,
|
@@ -307,15 +307,15 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
307
307
|
WHERE c.table_name = %s
|
308
308
|
""", (table_name,))
|
309
309
|
comments = cur.fetchall()
|
310
|
-
|
310
|
+
|
311
311
|
for comment in comments:
|
312
312
|
if comment[2]: # table comment
|
313
313
|
ddl.append(f"\nCOMMENT ON TABLE {table_name} IS '{comment[2]}';")
|
314
314
|
if comment[1]: # column comment
|
315
315
|
ddl.append(f"COMMENT ON COLUMN {table_name}.{comment[0]} IS '{comment[1]}';")
|
316
|
-
|
316
|
+
|
317
317
|
return "\n".join(ddl)
|
318
|
-
|
318
|
+
|
319
319
|
except psycopg2.Error as e:
|
320
320
|
error_msg = f"Failed to get table DDL: [Code: {e.pgcode}] {e.pgerror or str(e)}"
|
321
321
|
self.stats.record_error(e.__class__.__name__)
|
@@ -336,7 +336,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
336
336
|
SELECT
|
337
337
|
i.relname as index_name,
|
338
338
|
a.attname as column_name,
|
339
|
-
CASE
|
339
|
+
CASE
|
340
340
|
WHEN ix.indisprimary THEN 'PRIMARY KEY'
|
341
341
|
WHEN ix.indisunique THEN 'UNIQUE'
|
342
342
|
ELSE 'INDEX'
|
@@ -362,7 +362,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
362
362
|
current_index = None
|
363
363
|
formatted_indexes = []
|
364
364
|
index_info = []
|
365
|
-
|
365
|
+
|
366
366
|
for idx in indexes:
|
367
367
|
if current_index != idx[0]:
|
368
368
|
if index_info:
|
@@ -377,14 +377,14 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
377
377
|
]
|
378
378
|
if idx[5]: # index comment
|
379
379
|
index_info.insert(1, f"Comment: {idx[5]}")
|
380
|
-
|
380
|
+
|
381
381
|
index_info.append(f" - {idx[1]}")
|
382
382
|
|
383
383
|
if index_info:
|
384
384
|
formatted_indexes.extend(index_info)
|
385
385
|
|
386
386
|
return "\n".join(formatted_indexes)
|
387
|
-
|
387
|
+
|
388
388
|
except psycopg2.Error as e:
|
389
389
|
error_msg = f"Failed to get index information: [Code: {e.pgcode}] {e.pgerror or str(e)}"
|
390
390
|
self.stats.record_error(e.__class__.__name__)
|
@@ -402,7 +402,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
402
402
|
with conn.cursor() as cur:
|
403
403
|
# Get table statistics
|
404
404
|
cur.execute("""
|
405
|
-
SELECT
|
405
|
+
SELECT
|
406
406
|
c.reltuples::bigint as row_estimate,
|
407
407
|
pg_size_pretty(pg_total_relation_size(c.oid)) as total_size,
|
408
408
|
pg_size_pretty(pg_table_size(c.oid)) as table_size,
|
@@ -428,8 +428,8 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
428
428
|
s.n_distinct as distinct_values,
|
429
429
|
pg_column_size(a.attname::text) as approx_width
|
430
430
|
FROM pg_stats s
|
431
|
-
JOIN pg_attribute a ON a.attrelid = %s::regclass
|
432
|
-
AND a.attnum > 0
|
431
|
+
JOIN pg_attribute a ON a.attrelid = %s::regclass
|
432
|
+
AND a.attnum > 0
|
433
433
|
AND a.attname = s.attname
|
434
434
|
WHERE s.schemaname = 'public'
|
435
435
|
AND s.tablename = %s
|
@@ -522,10 +522,10 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
522
522
|
|
523
523
|
if con[4]: # is_deferrable
|
524
524
|
output.append(f" Deferrable: {'Deferred' if con[5] else 'Immediate'}")
|
525
|
-
|
525
|
+
|
526
526
|
if con[6]: # comment
|
527
527
|
output.append(f" Comment: {con[6]}")
|
528
|
-
|
528
|
+
|
529
529
|
output.append("") # Empty line between constraints
|
530
530
|
|
531
531
|
return "\n".join(output)
|
@@ -567,7 +567,7 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
567
567
|
"----------------"
|
568
568
|
]
|
569
569
|
output.extend(line[0] for line in regular_plan)
|
570
|
-
|
570
|
+
|
571
571
|
output.extend([
|
572
572
|
"\nActual Plan (ANALYZE):",
|
573
573
|
"----------------------"
|
@@ -584,6 +584,26 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
584
584
|
if conn:
|
585
585
|
conn.close()
|
586
586
|
|
587
|
+
async def test_connection(self) -> bool:
|
588
|
+
"""Test database connection
|
589
|
+
|
590
|
+
Returns:
|
591
|
+
bool: True if connection is successful, False otherwise
|
592
|
+
"""
|
593
|
+
conn = None
|
594
|
+
try:
|
595
|
+
conn_params = self.config.get_connection_params()
|
596
|
+
conn = psycopg2.connect(**conn_params)
|
597
|
+
with conn.cursor() as cur:
|
598
|
+
cur.execute("SELECT 1")
|
599
|
+
return True
|
600
|
+
except psycopg2.Error as e:
|
601
|
+
self.log("error", f"Connection test failed: [Code: {e.pgcode}] {e.pgerror or str(e)}")
|
602
|
+
return False
|
603
|
+
finally:
|
604
|
+
if conn:
|
605
|
+
conn.close()
|
606
|
+
|
587
607
|
async def cleanup(self):
|
588
608
|
"""Cleanup resources"""
|
589
609
|
# Log final stats before cleanup
|