mcp-dbutils 0.16.1__py3-none-any.whl → 0.18.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 +102 -35
- mcp_dbutils/postgres/handler.py +50 -30
- mcp_dbutils/sqlite/handler.py +124 -67
- mcp_dbutils-0.18.0.dist-info/METADATA +138 -0
- {mcp_dbutils-0.16.1.dist-info → mcp_dbutils-0.18.0.dist-info}/RECORD +9 -9
- mcp_dbutils-0.16.1.dist-info/METADATA +0 -572
- {mcp_dbutils-0.16.1.dist-info → mcp_dbutils-0.18.0.dist-info}/WHEEL +0 -0
- {mcp_dbutils-0.16.1.dist-info → mcp_dbutils-0.18.0.dist-info}/entry_points.txt +0 -0
- {mcp_dbutils-0.16.1.dist-info → mcp_dbutils-0.18.0.dist-info}/licenses/LICENSE +0 -0
mcp_dbutils/mysql/handler.py
CHANGED
@@ -31,6 +31,35 @@ class MySQLHandler(ConnectionHandler):
|
|
31
31
|
self.log("debug", f"Configuring connection with parameters: {masked_params}")
|
32
32
|
self.pool = None
|
33
33
|
|
34
|
+
async def _check_table_exists(self, cursor, table_name: str) -> None:
|
35
|
+
"""检查表是否存在
|
36
|
+
|
37
|
+
Args:
|
38
|
+
cursor: 数据库游标
|
39
|
+
table_name: 表名
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
ConnectionHandlerError: 如果表不存在
|
43
|
+
"""
|
44
|
+
cursor.execute("""
|
45
|
+
SELECT COUNT(*) as count
|
46
|
+
FROM information_schema.tables
|
47
|
+
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
48
|
+
""", (self.config.database, table_name))
|
49
|
+
table_exists = cursor.fetchone()
|
50
|
+
|
51
|
+
# Handle different formats of cursor results (dict or tuple)
|
52
|
+
if not table_exists:
|
53
|
+
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
54
|
+
|
55
|
+
# If fetchone returns a dictionary (dictionary=True was used)
|
56
|
+
if isinstance(table_exists, dict) and 'count' in table_exists and table_exists['count'] == 0:
|
57
|
+
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
58
|
+
|
59
|
+
# If fetchone returns a tuple
|
60
|
+
if isinstance(table_exists, tuple) and table_exists[0] == 0:
|
61
|
+
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
62
|
+
|
34
63
|
async def get_tables(self) -> list[types.Resource]:
|
35
64
|
"""Get all table resources"""
|
36
65
|
try:
|
@@ -38,7 +67,7 @@ class MySQLHandler(ConnectionHandler):
|
|
38
67
|
conn = mysql.connector.connect(**conn_params)
|
39
68
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
40
69
|
cur.execute("""
|
41
|
-
SELECT
|
70
|
+
SELECT
|
42
71
|
TABLE_NAME as table_name,
|
43
72
|
TABLE_COMMENT as description
|
44
73
|
FROM information_schema.tables
|
@@ -69,7 +98,7 @@ class MySQLHandler(ConnectionHandler):
|
|
69
98
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
70
99
|
# Get column information
|
71
100
|
cur.execute("""
|
72
|
-
SELECT
|
101
|
+
SELECT
|
73
102
|
COLUMN_NAME as column_name,
|
74
103
|
DATA_TYPE as data_type,
|
75
104
|
IS_NULLABLE as is_nullable,
|
@@ -119,24 +148,30 @@ class MySQLHandler(ConnectionHandler):
|
|
119
148
|
self.log("debug", f"Executing query: {sql}")
|
120
149
|
|
121
150
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
122
|
-
#
|
123
|
-
|
151
|
+
# Check if the query is a SELECT statement
|
152
|
+
sql_upper = sql.strip().upper()
|
153
|
+
is_select = sql_upper.startswith("SELECT")
|
154
|
+
|
155
|
+
# Only set read-only transaction for SELECT statements
|
156
|
+
if is_select:
|
157
|
+
cur.execute("SET TRANSACTION READ ONLY")
|
124
158
|
try:
|
125
159
|
cur.execute(sql)
|
126
|
-
|
160
|
+
if not is_select:
|
161
|
+
conn.commit()
|
162
|
+
results = cur.fetchall() if is_select else []
|
163
|
+
if cur.description is None: # DDL statements
|
164
|
+
return "Query executed successfully"
|
127
165
|
columns = [desc[0] for desc in cur.description]
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
'columns': columns,
|
132
|
-
'rows': results,
|
133
|
-
'row_count': len(results)
|
166
|
+
return str({
|
167
|
+
"columns": columns,
|
168
|
+
"rows": results
|
134
169
|
})
|
135
|
-
|
136
|
-
self.log("
|
137
|
-
|
170
|
+
except mysql.connector.Error as e:
|
171
|
+
self.log("error", f"Query error: {str(e)}")
|
172
|
+
raise ConnectionHandlerError(str(e))
|
138
173
|
finally:
|
139
|
-
cur.
|
174
|
+
cur.close()
|
140
175
|
except mysql.connector.Error as e:
|
141
176
|
error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
|
142
177
|
raise ConnectionHandlerError(error_msg)
|
@@ -151,11 +186,14 @@ class MySQLHandler(ConnectionHandler):
|
|
151
186
|
conn_params = self.config.get_connection_params()
|
152
187
|
conn = mysql.connector.connect(**conn_params)
|
153
188
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
189
|
+
# Check if table exists
|
190
|
+
await self._check_table_exists(cur, table_name)
|
191
|
+
|
154
192
|
# Get table information and comment
|
155
193
|
cur.execute("""
|
156
|
-
SELECT
|
194
|
+
SELECT
|
157
195
|
TABLE_COMMENT as table_comment
|
158
|
-
FROM information_schema.tables
|
196
|
+
FROM information_schema.tables
|
159
197
|
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
160
198
|
""", (self.config.database, table_name))
|
161
199
|
table_info = cur.fetchone()
|
@@ -163,7 +201,7 @@ class MySQLHandler(ConnectionHandler):
|
|
163
201
|
|
164
202
|
# Get column information
|
165
203
|
cur.execute("""
|
166
|
-
SELECT
|
204
|
+
SELECT
|
167
205
|
COLUMN_NAME as column_name,
|
168
206
|
DATA_TYPE as data_type,
|
169
207
|
COLUMN_DEFAULT as column_default,
|
@@ -184,14 +222,14 @@ class MySQLHandler(ConnectionHandler):
|
|
184
222
|
f"Comment: {table_comment or 'No comment'}\n",
|
185
223
|
COLUMNS_HEADER
|
186
224
|
]
|
187
|
-
|
225
|
+
|
188
226
|
for col in columns:
|
189
227
|
col_info = [
|
190
228
|
f" {col['column_name']} ({col['data_type']})",
|
191
229
|
f" Nullable: {col['is_nullable']}",
|
192
230
|
f" Default: {col['column_default'] or 'None'}"
|
193
231
|
]
|
194
|
-
|
232
|
+
|
195
233
|
if col['character_maximum_length']:
|
196
234
|
col_info.append(f" Max Length: {col['character_maximum_length']}")
|
197
235
|
if col['numeric_precision']:
|
@@ -200,12 +238,12 @@ class MySQLHandler(ConnectionHandler):
|
|
200
238
|
col_info.append(f" Scale: {col['numeric_scale']}")
|
201
239
|
if col['column_comment']:
|
202
240
|
col_info.append(f" Comment: {col['column_comment']}")
|
203
|
-
|
241
|
+
|
204
242
|
description.extend(col_info)
|
205
243
|
description.append("") # Empty line between columns
|
206
|
-
|
244
|
+
|
207
245
|
return "\n".join(description)
|
208
|
-
|
246
|
+
|
209
247
|
except mysql.connector.Error as e:
|
210
248
|
error_msg = f"Failed to get table description: {str(e)}"
|
211
249
|
self.stats.record_error(e.__class__.__name__)
|
@@ -227,7 +265,7 @@ class MySQLHandler(ConnectionHandler):
|
|
227
265
|
if result:
|
228
266
|
return result['Create Table']
|
229
267
|
return f"Failed to get DDL for table {table_name}"
|
230
|
-
|
268
|
+
|
231
269
|
except mysql.connector.Error as e:
|
232
270
|
error_msg = f"Failed to get table DDL: {str(e)}"
|
233
271
|
self.stats.record_error(e.__class__.__name__)
|
@@ -243,9 +281,12 @@ class MySQLHandler(ConnectionHandler):
|
|
243
281
|
conn_params = self.config.get_connection_params()
|
244
282
|
conn = mysql.connector.connect(**conn_params)
|
245
283
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
284
|
+
# Check if table exists
|
285
|
+
await self._check_table_exists(cur, table_name)
|
286
|
+
|
246
287
|
# Get index information
|
247
288
|
cur.execute("""
|
248
|
-
SELECT
|
289
|
+
SELECT
|
249
290
|
INDEX_NAME as index_name,
|
250
291
|
COLUMN_NAME as column_name,
|
251
292
|
NON_UNIQUE as non_unique,
|
@@ -264,7 +305,7 @@ class MySQLHandler(ConnectionHandler):
|
|
264
305
|
current_index = None
|
265
306
|
formatted_indexes = []
|
266
307
|
index_info = []
|
267
|
-
|
308
|
+
|
268
309
|
for idx in indexes:
|
269
310
|
if current_index != idx['index_name']:
|
270
311
|
if index_info:
|
@@ -279,14 +320,14 @@ class MySQLHandler(ConnectionHandler):
|
|
279
320
|
]
|
280
321
|
if idx['index_comment']:
|
281
322
|
index_info.insert(1, f"Comment: {idx['index_comment']}")
|
282
|
-
|
323
|
+
|
283
324
|
index_info.append(f" - {idx['column_name']}")
|
284
325
|
|
285
326
|
if index_info:
|
286
327
|
formatted_indexes.extend(index_info)
|
287
328
|
|
288
329
|
return "\n".join(formatted_indexes)
|
289
|
-
|
330
|
+
|
290
331
|
except mysql.connector.Error as e:
|
291
332
|
error_msg = f"Failed to get index information: {str(e)}"
|
292
333
|
self.stats.record_error(e.__class__.__name__)
|
@@ -302,9 +343,12 @@ class MySQLHandler(ConnectionHandler):
|
|
302
343
|
conn_params = self.config.get_connection_params()
|
303
344
|
conn = mysql.connector.connect(**conn_params)
|
304
345
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
346
|
+
# Check if table exists
|
347
|
+
await self._check_table_exists(cur, table_name)
|
348
|
+
|
305
349
|
# Get table statistics
|
306
350
|
cur.execute("""
|
307
|
-
SELECT
|
351
|
+
SELECT
|
308
352
|
TABLE_ROWS as table_rows,
|
309
353
|
AVG_ROW_LENGTH as avg_row_length,
|
310
354
|
DATA_LENGTH as data_length,
|
@@ -320,7 +364,7 @@ class MySQLHandler(ConnectionHandler):
|
|
320
364
|
|
321
365
|
# Get column statistics
|
322
366
|
cur.execute("""
|
323
|
-
SELECT
|
367
|
+
SELECT
|
324
368
|
COLUMN_NAME as column_name,
|
325
369
|
DATA_TYPE as data_type,
|
326
370
|
COLUMN_TYPE as column_type
|
@@ -367,20 +411,23 @@ class MySQLHandler(ConnectionHandler):
|
|
367
411
|
conn_params = self.config.get_connection_params()
|
368
412
|
conn = mysql.connector.connect(**conn_params)
|
369
413
|
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
414
|
+
# Check if table exists
|
415
|
+
await self._check_table_exists(cur, table_name)
|
416
|
+
|
370
417
|
# Get constraint information
|
371
418
|
cur.execute("""
|
372
|
-
SELECT
|
419
|
+
SELECT
|
373
420
|
k.CONSTRAINT_NAME as constraint_name,
|
374
421
|
t.CONSTRAINT_TYPE as constraint_type,
|
375
422
|
k.COLUMN_NAME as column_name,
|
376
423
|
k.REFERENCED_TABLE_NAME as referenced_table_name,
|
377
424
|
k.REFERENCED_COLUMN_NAME as referenced_column_name
|
378
425
|
FROM information_schema.key_column_usage k
|
379
|
-
JOIN information_schema.table_constraints t
|
426
|
+
JOIN information_schema.table_constraints t
|
380
427
|
ON k.CONSTRAINT_NAME = t.CONSTRAINT_NAME
|
381
428
|
AND k.TABLE_SCHEMA = t.TABLE_SCHEMA
|
382
429
|
AND k.TABLE_NAME = t.TABLE_NAME
|
383
|
-
WHERE k.TABLE_SCHEMA = %s
|
430
|
+
WHERE k.TABLE_SCHEMA = %s
|
384
431
|
AND k.TABLE_NAME = %s
|
385
432
|
ORDER BY t.CONSTRAINT_TYPE, k.CONSTRAINT_NAME, k.ORDINAL_POSITION
|
386
433
|
""", (self.config.database, table_name))
|
@@ -404,7 +451,7 @@ class MySQLHandler(ConnectionHandler):
|
|
404
451
|
f"\n{con['constraint_type']} Constraint: {con['constraint_name']}",
|
405
452
|
COLUMNS_HEADER
|
406
453
|
]
|
407
|
-
|
454
|
+
|
408
455
|
col_info = f" - {con['column_name']}"
|
409
456
|
if con['referenced_table_name']:
|
410
457
|
col_info += f" -> {con['referenced_table_name']}.{con['referenced_column_name']}"
|
@@ -446,7 +493,7 @@ class MySQLHandler(ConnectionHandler):
|
|
446
493
|
]
|
447
494
|
for row in explain_result:
|
448
495
|
output.append(str(row['EXPLAIN']))
|
449
|
-
|
496
|
+
|
450
497
|
output.extend([
|
451
498
|
"\nActual Plan (ANALYZE):",
|
452
499
|
"----------------------"
|
@@ -464,6 +511,26 @@ class MySQLHandler(ConnectionHandler):
|
|
464
511
|
if conn:
|
465
512
|
conn.close()
|
466
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
|
+
|
467
534
|
async def cleanup(self):
|
468
535
|
"""Cleanup resources"""
|
469
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
|