mcp-dbutils 0.8.0__py3-none-any.whl → 0.10.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.
@@ -4,13 +4,13 @@ from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
  from typing import Dict, Any, Optional, Literal
6
6
  from urllib.parse import urlparse, parse_qs
7
- from ..config import DatabaseConfig
7
+ from ..config import ConnectionConfig
8
8
 
9
9
  def parse_jdbc_url(jdbc_url: str) -> Dict[str, str]:
10
10
  """Parse JDBC URL into connection parameters
11
11
 
12
12
  Args:
13
- jdbc_url: JDBC URL (e.g. jdbc:sqlite:file:/path/to/database.db or jdbc:sqlite:/path/to/database.db)
13
+ jdbc_url: JDBC URL (e.g. jdbc:sqlite:file:/path/to/sqlite.db or jdbc:sqlite:/path/to/sqlite.db)
14
14
 
15
15
  Returns:
16
16
  Dictionary of connection parameters
@@ -40,7 +40,7 @@ def parse_jdbc_url(jdbc_url: str) -> Dict[str, str]:
40
40
  params[key] = values[0]
41
41
 
42
42
  if not path:
43
- raise ValueError("Database path must be specified in URL")
43
+ raise ValueError("SQLite file path must be specified in URL")
44
44
 
45
45
  return {
46
46
  'path': path,
@@ -48,22 +48,22 @@ def parse_jdbc_url(jdbc_url: str) -> Dict[str, str]:
48
48
  }
49
49
 
50
50
  @dataclass
51
- class SqliteConfig(DatabaseConfig):
51
+ class SQLiteConfig(ConnectionConfig):
52
52
  path: str
53
53
  password: Optional[str] = None
54
54
  uri: bool = True # Enable URI mode to support parameters like password
55
55
  type: Literal['sqlite'] = 'sqlite'
56
56
 
57
57
  @classmethod
58
- def from_jdbc_url(cls, jdbc_url: str, password: Optional[str] = None) -> 'SqliteConfig':
58
+ def from_jdbc_url(cls, jdbc_url: str, password: Optional[str] = None) -> 'SQLiteConfig':
59
59
  """Create configuration from JDBC URL
60
60
 
61
61
  Args:
62
- jdbc_url: JDBC URL (e.g. jdbc:sqlite:file:/path/to/database.db)
62
+ jdbc_url: JDBC URL (e.g. jdbc:sqlite:file:/path/to/sqlite.db)
63
63
  password: Optional password for database encryption
64
64
 
65
65
  Returns:
66
- SqliteConfig instance
66
+ SQLiteConfig instance
67
67
 
68
68
  Raises:
69
69
  ValueError: If URL format is invalid
@@ -80,7 +80,7 @@ class SqliteConfig(DatabaseConfig):
80
80
 
81
81
  @property
82
82
  def absolute_path(self) -> str:
83
- """Return absolute path to database file"""
83
+ """Return absolute path to SQLite database file"""
84
84
  return str(Path(self.path).expanduser().resolve())
85
85
 
86
86
  def get_connection_params(self) -> Dict[str, Any]:
@@ -109,23 +109,23 @@ class SqliteConfig(DatabaseConfig):
109
109
  return info
110
110
 
111
111
  @classmethod
112
- def from_yaml(cls, yaml_path: str, db_name: str, **kwargs) -> 'SqliteConfig':
112
+ def from_yaml(cls, yaml_path: str, db_name: str, **kwargs) -> 'SQLiteConfig':
113
113
  """Create SQLite configuration from YAML
114
114
 
115
115
  Args:
116
116
  yaml_path: Path to YAML configuration file
117
- db_name: Database configuration name
117
+ db_name: Connection configuration name
118
118
  """
119
119
  configs = cls.load_yaml_config(yaml_path)
120
120
 
121
121
  if db_name not in configs:
122
122
  available_dbs = list(configs.keys())
123
- raise ValueError(f"Database configuration not found: {db_name}. Available configurations: {available_dbs}")
123
+ raise ValueError(f"Connection configuration not found: {db_name}. Available configurations: {available_dbs}")
124
124
 
125
125
  db_config = configs[db_name]
126
126
 
127
127
  if 'type' not in db_config:
128
- raise ValueError("Database configuration must include 'type' field")
128
+ raise ValueError("Connection configuration must include 'type' field")
129
129
  if db_config['type'] != 'sqlite':
130
130
  raise ValueError(f"Configuration is not SQLite type: {db_config['type']}")
131
131
 
@@ -1,133 +1,417 @@
1
- """SQLite database handler implementation"""
1
+ """SQLite connection handler implementation"""
2
2
 
3
3
  import sqlite3
4
- from pathlib import Path
5
- from contextlib import closing
4
+ import json
5
+ from typing import Any
6
6
  import mcp.types as types
7
7
 
8
- from ..base import DatabaseHandler, DatabaseError
9
- from .config import SqliteConfig
8
+ from ..base import ConnectionHandler, ConnectionHandlerError
9
+ from .config import SQLiteConfig
10
10
 
11
- class SqliteHandler(DatabaseHandler):
11
+ class SQLiteHandler(ConnectionHandler):
12
12
  @property
13
13
  def db_type(self) -> str:
14
14
  return 'sqlite'
15
15
 
16
- def __init__(self, config_path: str, database: str, debug: bool = False):
16
+ def __init__(self, config_path: str, connection: str, debug: bool = False):
17
17
  """Initialize SQLite handler
18
18
 
19
19
  Args:
20
20
  config_path: Path to configuration file
21
- database: Database configuration name
21
+ connection: Database connection name
22
22
  debug: Enable debug mode
23
23
  """
24
- super().__init__(config_path, database, debug)
25
- self.config = SqliteConfig.from_yaml(config_path, database)
26
-
27
- # Ensure database directory exists
28
- db_file = Path(self.config.absolute_path)
29
- db_file.parent.mkdir(parents=True, exist_ok=True)
30
-
31
- # No connection test during initialization
32
- self.log("debug", f"Configuring database: {self.config.get_masked_connection_info()}")
33
-
34
- def _get_connection(self):
35
- """Get database connection"""
36
- connection_params = self.config.get_connection_params()
37
- conn = sqlite3.connect(**connection_params)
38
- conn.row_factory = sqlite3.Row
39
- return conn
24
+ super().__init__(config_path, connection, debug)
25
+ self.config = SQLiteConfig.from_yaml(config_path, connection)
40
26
 
41
27
  async def get_tables(self) -> list[types.Resource]:
42
28
  """Get all table resources"""
43
29
  try:
44
- with closing(self._get_connection()) as conn:
45
- cursor = conn.execute(
46
- "SELECT name FROM sqlite_master WHERE type='table'"
47
- )
48
- tables = cursor.fetchall()
49
-
30
+ with sqlite3.connect(self.config.path) as conn:
31
+ cur = conn.cursor()
32
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
33
+ tables = cur.fetchall()
50
34
  return [
51
35
  types.Resource(
52
- uri=f"sqlite://{self.database}/{table[0]}/schema",
36
+ uri=f"sqlite://{self.connection}/{table[0]}/schema",
53
37
  name=f"{table[0]} schema",
54
38
  mimeType="application/json"
55
39
  ) for table in tables
56
40
  ]
57
41
  except sqlite3.Error as e:
58
42
  error_msg = f"Failed to get table list: {str(e)}"
59
- self.log("error", error_msg)
60
- raise
43
+ self.stats.record_error(e.__class__.__name__)
44
+ raise ConnectionHandlerError(error_msg)
61
45
 
62
46
  async def get_schema(self, table_name: str) -> str:
63
47
  """Get table schema information"""
64
48
  try:
65
- with closing(self._get_connection()) as conn:
66
- # Get table structure
67
- cursor = conn.execute(f"PRAGMA table_info({table_name})")
68
- columns = cursor.fetchall()
49
+ with sqlite3.connect(self.config.path) as conn:
50
+ cur = conn.cursor()
51
+ cur.execute(f"PRAGMA table_info({table_name})")
52
+ columns = cur.fetchall()
69
53
 
70
- # Get index information
71
- cursor = conn.execute(f"PRAGMA index_list({table_name})")
72
- indexes = cursor.fetchall()
54
+ cur.execute(f"PRAGMA index_list({table_name})")
55
+ indexes = cur.fetchall()
73
56
 
74
- schema_info = {
57
+ return str({
75
58
  'columns': [{
76
- 'name': col['name'],
77
- 'type': col['type'],
78
- 'nullable': not col['notnull'],
79
- 'primary_key': bool(col['pk'])
59
+ 'name': col[1],
60
+ 'type': col[2],
61
+ 'nullable': not col[3],
62
+ 'default': col[4],
63
+ 'primary_key': bool(col[5])
80
64
  } for col in columns],
81
65
  'indexes': [{
82
- 'name': idx['name'],
83
- 'unique': bool(idx['unique'])
66
+ 'name': idx[1],
67
+ 'unique': bool(idx[2])
84
68
  } for idx in indexes]
85
- }
86
-
87
- return str(schema_info)
69
+ })
88
70
  except sqlite3.Error as e:
89
71
  error_msg = f"Failed to read table schema: {str(e)}"
90
- self.log("error", error_msg)
91
- raise
72
+ self.stats.record_error(e.__class__.__name__)
73
+ raise ConnectionHandlerError(error_msg)
92
74
 
93
75
  async def _execute_query(self, sql: str) -> str:
94
76
  """Execute SQL query"""
95
- # Check for non-SELECT queries
96
- sql_lower = sql.lower().strip()
97
- if not sql_lower.startswith('select'):
98
- error_msg = "cannot execute DELETE statement"
99
- if sql_lower.startswith('delete'):
100
- error_msg = "cannot execute DELETE statement"
101
- elif sql_lower.startswith('update'):
102
- error_msg = "cannot execute UPDATE statement"
103
- elif sql_lower.startswith('insert'):
104
- error_msg = "cannot execute INSERT statement"
105
- raise DatabaseError(error_msg)
106
-
107
77
  try:
108
- with closing(self._get_connection()) as conn:
78
+ # Only allow SELECT statements
79
+ if not sql.strip().upper().startswith("SELECT"):
80
+ raise ConnectionHandlerError("cannot execute DELETE statement")
81
+
82
+ with sqlite3.connect(self.config.path) as conn:
83
+ conn.row_factory = sqlite3.Row
84
+ cur = conn.cursor()
109
85
  self.log("debug", f"Executing query: {sql}")
110
- cursor = conn.execute(sql)
111
- results = cursor.fetchall()
112
-
113
- columns = [desc[0] for desc in cursor.description]
114
- formatted_results = [dict(zip(columns, row)) for row in results]
86
+
87
+ cur.execute(sql)
88
+ results = cur.fetchall()
89
+ rows = [dict(row) for row in results]
115
90
 
116
91
  result_text = str({
117
92
  'type': self.db_type,
118
- 'columns': columns,
119
- 'rows': formatted_results,
120
- 'row_count': len(results)
93
+ 'columns': list(rows[0].keys()) if rows else [],
94
+ 'rows': rows,
95
+ 'row_count': len(rows)
121
96
  })
122
97
 
123
- self.log("debug", f"Query completed, returned {len(results)} rows")
98
+ self.log("debug", f"Query completed, returned {len(rows)} rows")
124
99
  return result_text
125
-
126
100
  except sqlite3.Error as e:
127
101
  error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
128
- raise DatabaseError(error_msg)
102
+ raise ConnectionHandlerError(error_msg)
103
+
104
+ async def get_table_description(self, table_name: str) -> str:
105
+ """Get detailed table description"""
106
+ try:
107
+ with sqlite3.connect(self.config.path) as conn:
108
+ cur = conn.cursor()
109
+ # 获取表信息
110
+ cur.execute(f"PRAGMA table_info({table_name})")
111
+ columns = cur.fetchall()
112
+
113
+ # SQLite不支持表级注释,但我们可以获取表的详细信息
114
+ description = [
115
+ f"Table: {table_name}\n",
116
+ "Columns:"
117
+ ]
118
+
119
+ for col in columns:
120
+ col_info = [
121
+ f" {col[1]} ({col[2]})",
122
+ f" Nullable: {'No' if col[3] else 'Yes'}",
123
+ f" Default: {col[4] or 'None'}",
124
+ f" Primary Key: {'Yes' if col[5] else 'No'}"
125
+ ]
126
+ description.extend(col_info)
127
+ description.append("") # Empty line between columns
128
+
129
+ return "\n".join(description)
130
+
131
+ except sqlite3.Error as e:
132
+ error_msg = f"Failed to get table description: {str(e)}"
133
+ self.stats.record_error(e.__class__.__name__)
134
+ raise ConnectionHandlerError(error_msg)
135
+
136
+ async def get_table_ddl(self, table_name: str) -> str:
137
+ """Get DDL statement for creating table"""
138
+ try:
139
+ with sqlite3.connect(self.config.path) as conn:
140
+ cur = conn.cursor()
141
+ # SQLite provides the complete CREATE statement
142
+ cur.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
143
+ result = cur.fetchone()
144
+
145
+ if not result:
146
+ return f"Table {table_name} not found"
147
+
148
+ ddl = result[0]
149
+
150
+ # Get indexes
151
+ cur.execute(f"SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?", (table_name,))
152
+ indexes = cur.fetchall()
153
+
154
+ # Add index definitions
155
+ if indexes:
156
+ ddl = ddl + "\n\n-- Indexes:"
157
+ for idx in indexes:
158
+ if idx[0]: # Some internal indexes might have NULL sql
159
+ ddl = ddl + "\n" + idx[0] + ";"
160
+
161
+ return ddl
162
+
163
+ except sqlite3.Error as e:
164
+ error_msg = f"Failed to get table DDL: {str(e)}"
165
+ self.stats.record_error(e.__class__.__name__)
166
+ raise ConnectionHandlerError(error_msg)
167
+
168
+ async def get_table_indexes(self, table_name: str) -> str:
169
+ """Get index information for table"""
170
+ try:
171
+ with sqlite3.connect(self.config.path) as conn:
172
+ cur = conn.cursor()
173
+ # 获取索引列表
174
+ cur.execute(f"PRAGMA index_list({table_name})")
175
+ indexes = cur.fetchall()
176
+
177
+ if not indexes:
178
+ return f"No indexes found on table {table_name}"
179
+
180
+ formatted_indexes = [f"Indexes for {table_name}:"]
181
+
182
+ for idx in indexes:
183
+ # 获取索引详细信息
184
+ cur.execute(f"PRAGMA index_info({idx[1]})")
185
+ index_info = cur.fetchall()
186
+
187
+ # 获取索引的SQL定义
188
+ cur.execute("SELECT sql FROM sqlite_master WHERE type='index' AND name=?", (idx[1],))
189
+ sql = cur.fetchone()
190
+
191
+ index_details = [
192
+ f"\nIndex: {idx[1]}",
193
+ f"Type: {'UNIQUE' if idx[2] else 'INDEX'}",
194
+ "Columns:"
195
+ ]
196
+
197
+ for col in index_info:
198
+ index_details.append(f" - {col[2]}")
199
+
200
+ if sql and sql[0]:
201
+ index_details.extend([
202
+ "Definition:",
203
+ f" {sql[0]}"
204
+ ])
205
+
206
+ formatted_indexes.extend(index_details)
207
+
208
+ return "\n".join(formatted_indexes)
209
+
210
+ except sqlite3.Error as e:
211
+ error_msg = f"Failed to get index information: {str(e)}"
212
+ self.stats.record_error(e.__class__.__name__)
213
+ raise ConnectionHandlerError(error_msg)
214
+
215
+ async def get_table_stats(self, table_name: str) -> str:
216
+ """Get table statistics information"""
217
+ try:
218
+ with sqlite3.connect(self.config.path) as conn:
219
+ cur = conn.cursor()
220
+
221
+ # Get basic table information
222
+ cur.execute(f"PRAGMA table_info({table_name})")
223
+ columns = cur.fetchall()
224
+
225
+ # Count rows
226
+ cur.execute(f"SELECT COUNT(*) FROM {table_name}")
227
+ row_count = cur.fetchone()[0]
228
+
229
+ # Get index information
230
+ cur.execute(f"PRAGMA index_list({table_name})")
231
+ indexes = cur.fetchall()
232
+
233
+ # Get page count and size
234
+ cur.execute(f"PRAGMA page_count")
235
+ page_count = cur.fetchone()[0]
236
+ cur.execute(f"PRAGMA page_size")
237
+ page_size = cur.fetchone()[0]
238
+
239
+ # Calculate total size
240
+ total_size = page_count * page_size
241
+
242
+ # Format size in human readable format
243
+ def format_size(size):
244
+ for unit in ['B', 'KB', 'MB', 'GB']:
245
+ if size < 1024:
246
+ return f"{size:.2f} {unit}"
247
+ size /= 1024
248
+ return f"{size:.2f} TB"
249
+
250
+ # Get column statistics
251
+ column_stats = []
252
+ for col in columns:
253
+ col_name = col[1]
254
+ # Get null count
255
+ cur.execute(f"SELECT COUNT(*) FROM {table_name} WHERE {col_name} IS NULL")
256
+ null_count = cur.fetchone()[0]
257
+ # Get distinct value count
258
+ cur.execute(f"SELECT COUNT(DISTINCT {col_name}) FROM {table_name}")
259
+ distinct_count = cur.fetchone()[0]
260
+
261
+ column_stats.append({
262
+ 'name': col_name,
263
+ 'type': col[2],
264
+ 'null_count': null_count,
265
+ 'null_percent': (null_count / row_count * 100) if row_count > 0 else 0,
266
+ 'distinct_count': distinct_count
267
+ })
268
+
269
+ # Format output
270
+ output = [
271
+ f"Table Statistics for {table_name}:",
272
+ f" Row Count: {row_count:,}",
273
+ f" Total Size: {format_size(total_size)}",
274
+ f" Page Count: {page_count:,}",
275
+ f" Page Size: {format_size(page_size)}",
276
+ f" Index Count: {len(indexes)}\n",
277
+ "Column Statistics:"
278
+ ]
279
+
280
+ for stat in column_stats:
281
+ col_info = [
282
+ f" {stat['name']} ({stat['type']}):",
283
+ f" Null Values: {stat['null_count']:,} ({stat['null_percent']:.1f}%)",
284
+ f" Distinct Values: {stat['distinct_count']:,}"
285
+ ]
286
+ output.extend(col_info)
287
+ output.append("") # Empty line between columns
288
+
289
+ return "\n".join(output)
290
+
291
+ except sqlite3.Error as e:
292
+ error_msg = f"Failed to get table statistics: {str(e)}"
293
+ self.stats.record_error(e.__class__.__name__)
294
+ raise ConnectionHandlerError(error_msg)
295
+
296
+ async def get_table_constraints(self, table_name: str) -> str:
297
+ """Get constraint information for table"""
298
+ try:
299
+ with sqlite3.connect(self.config.path) as conn:
300
+ cur = conn.cursor()
301
+
302
+ # Get table info (includes PRIMARY KEY)
303
+ cur.execute(f"PRAGMA table_info({table_name})")
304
+ columns = cur.fetchall()
305
+
306
+ # Get foreign keys
307
+ cur.execute(f"PRAGMA foreign_key_list({table_name})")
308
+ foreign_keys = cur.fetchall()
309
+
310
+ # Get indexes (for UNIQUE constraints)
311
+ cur.execute(f"PRAGMA index_list({table_name})")
312
+ indexes = cur.fetchall()
313
+
314
+ output = [f"Constraints for {table_name}:"]
315
+
316
+ # Primary Key constraints
317
+ pk_columns = [col[1] for col in columns if col[5]] # col[5] is pk flag
318
+ if pk_columns:
319
+ output.extend([
320
+ "\nPrimary Key Constraints:",
321
+ f" PRIMARY KEY ({', '.join(pk_columns)})"
322
+ ])
323
+
324
+ # Foreign Key constraints
325
+ if foreign_keys:
326
+ output.append("\nForeign Key Constraints:")
327
+ current_fk = None
328
+ fk_columns = []
329
+
330
+ for fk in foreign_keys:
331
+ # SQLite foreign_key_list format:
332
+ # id, seq, table, from, to, on_update, on_delete, match
333
+ if current_fk != fk[0]:
334
+ if fk_columns:
335
+ output.append(f" ({', '.join(fk_columns)})")
336
+ current_fk = fk[0]
337
+ fk_columns = []
338
+ output.append(f" FOREIGN KEY:")
339
+ output.append(f" Referenced Table: {fk[2]}")
340
+ fk_columns.append(f"{fk[3]} -> {fk[2]}.{fk[4]}")
341
+ if fk[5]: # on_update
342
+ output.append(f" ON UPDATE: {fk[5]}")
343
+ if fk[6]: # on_delete
344
+ output.append(f" ON DELETE: {fk[6]}")
345
+
346
+ if fk_columns:
347
+ output.append(f" ({', '.join(fk_columns)})")
348
+
349
+ # Unique constraints (from indexes)
350
+ unique_indexes = [idx for idx in indexes if idx[2]] # idx[2] is unique flag
351
+ if unique_indexes:
352
+ output.append("\nUnique Constraints:")
353
+ for idx in unique_indexes:
354
+ # Get columns in the unique index
355
+ cur.execute(f"PRAGMA index_info({idx[1]})")
356
+ index_info = cur.fetchall()
357
+ columns = [info[2] for info in index_info] # info[2] is column name
358
+ output.append(f" UNIQUE ({', '.join(columns)})")
359
+
360
+ # Check constraints
361
+ # Note: SQLite doesn't provide direct access to CHECK constraints through PRAGMA
362
+ # We need to parse the table creation SQL
363
+ cur.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
364
+ create_sql = cur.fetchone()[0]
365
+ if "CHECK" in create_sql.upper():
366
+ output.append("\nCheck Constraints:")
367
+ output.append(" See table DDL for CHECK constraints")
368
+
369
+ return "\n".join(output)
370
+
371
+ except sqlite3.Error as e:
372
+ error_msg = f"Failed to get constraint information: {str(e)}"
373
+ self.stats.record_error(e.__class__.__name__)
374
+ raise ConnectionHandlerError(error_msg)
375
+
376
+ async def explain_query(self, sql: str) -> str:
377
+ """Get query execution plan"""
378
+ try:
379
+ with sqlite3.connect(self.config.path) as conn:
380
+ cur = conn.cursor()
381
+
382
+ # Get EXPLAIN output
383
+ cur.execute(f"EXPLAIN QUERY PLAN {sql}")
384
+ plan = cur.fetchall()
385
+
386
+ # Format the output
387
+ output = [
388
+ "Query Execution Plan:",
389
+ "==================\n",
390
+ "Details:",
391
+ "--------"
392
+ ]
393
+
394
+ for step in plan:
395
+ # EXPLAIN QUERY PLAN format:
396
+ # id | parent | notused | detail
397
+ indent = " " * (step[0] - step[1] if step[1] >= 0 else step[0])
398
+ output.append(f"{indent}{step[3]}")
399
+
400
+ # Add query statistics
401
+ output.extend([
402
+ "\nNote: SQLite's EXPLAIN QUERY PLAN provides a high-level overview.",
403
+ "For detailed execution statistics, consider using EXPLAIN (not QUERY PLAN)",
404
+ "which shows the virtual machine instructions."
405
+ ])
406
+
407
+ return "\n".join(output)
408
+
409
+ except sqlite3.Error as e:
410
+ error_msg = f"Failed to explain query: {str(e)}"
411
+ self.stats.record_error(e.__class__.__name__)
412
+ raise ConnectionHandlerError(error_msg)
129
413
 
130
414
  async def cleanup(self):
131
415
  """Cleanup resources"""
132
- # Log final stats before cleanup
416
+ # Log final stats
133
417
  self.log("info", f"Final SQLite handler stats: {self.stats.to_dict()}")