mcp-dbutils 0.17.0__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.
@@ -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
@@ -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