Lukynnnn commited on
Commit
7d219b7
·
verified ·
1 Parent(s): 06f2c2b

Upload mcp_database_universal/engines/mysql.py with huggingface_hub

Browse files
mcp_database_universal/engines/mysql.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MySQL engine — optional dependency: pymysql>=1.1.0."""
2
+
3
+ import time
4
+ from mcp_database_universal.engines.base import (
5
+ BaseEngine, DBInfo, ColumnInfo, TableInfo, TableDetail,
6
+ IndexInfo, ForeignKeyInfo, TableStats, QueryResult,
7
+ )
8
+
9
+
10
+ class MySQLEngine(BaseEngine):
11
+ def __init__(
12
+ self,
13
+ host: str = "localhost",
14
+ port: int = 3306,
15
+ database: str = "mysql",
16
+ user: str = "",
17
+ password: str = "",
18
+ read_only: bool = True,
19
+ ):
20
+ self.host = host
21
+ self.port = port
22
+ self.database = database
23
+ self.user = user
24
+ self.password = password
25
+ self._read_only = read_only
26
+ self._conn = None
27
+
28
+ async def connect(self) -> None:
29
+ try:
30
+ import pymysql
31
+ except ImportError:
32
+ raise RuntimeError(
33
+ "MySQL engine requires pymysql. "
34
+ "Install with: pip install 'mcp-database-universal[mysql]'"
35
+ )
36
+ self._conn = pymysql.connect(
37
+ host=self.host,
38
+ port=self.port,
39
+ database=self.database,
40
+ user=self.user,
41
+ password=self.password,
42
+ cursorclass=pymysql.cursors.DictCursor,
43
+ )
44
+
45
+ async def disconnect(self) -> None:
46
+ if self._conn:
47
+ self._conn.close()
48
+ self._conn = None
49
+
50
+ def _ensure_conn(self):
51
+ if self._conn is None:
52
+ raise RuntimeError("Not connected. Call connect() first.")
53
+ return self._conn
54
+
55
+ async def get_db_info(self) -> DBInfo:
56
+ conn = self._ensure_conn()
57
+ with conn.cursor() as cur:
58
+ cur.execute("SELECT VERSION()")
59
+ version = cur.fetchone()["VERSION()"]
60
+ return DBInfo(engine="mysql", version=version, name=self.database, size_approx="unknown")
61
+
62
+ async def get_tables(self) -> list[TableInfo]:
63
+ conn = self._ensure_conn()
64
+ with conn.cursor() as cur:
65
+ cur.execute("""
66
+ SELECT TABLE_NAME, TABLE_ROWS
67
+ FROM information_schema.TABLES
68
+ WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = 'BASE TABLE'
69
+ ORDER BY TABLE_NAME
70
+ """, [self.database])
71
+ rows = cur.fetchall()
72
+
73
+ tables = []
74
+ for row in rows:
75
+ tables.append(TableInfo(
76
+ name=row["TABLE_NAME"],
77
+ row_count=row.get("TABLE_ROWS", 0) or 0,
78
+ ))
79
+ return tables
80
+
81
+ async def get_table_detail(self, table: str) -> TableDetail:
82
+ conn = self._ensure_conn()
83
+
84
+ with conn.cursor() as cur:
85
+ cur.execute("""
86
+ SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT,
87
+ COLUMN_KEY
88
+ FROM information_schema.COLUMNS
89
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
90
+ ORDER BY ORDINAL_POSITION
91
+ """, [self.database, table])
92
+ col_rows = cur.fetchall()
93
+
94
+ columns = []
95
+ pk_name = None
96
+ for c in col_rows:
97
+ is_pk = c["COLUMN_KEY"] == "PRI"
98
+ if is_pk:
99
+ pk_name = c["COLUMN_NAME"]
100
+ columns.append(ColumnInfo(
101
+ name=c["COLUMN_NAME"],
102
+ type=c["DATA_TYPE"],
103
+ nullable=(c["IS_NULLABLE"] == "YES"),
104
+ default=str(c["COLUMN_DEFAULT"]) if c["COLUMN_DEFAULT"] is not None else None,
105
+ is_primary_key=is_pk,
106
+ is_foreign_key=(c["COLUMN_KEY"] == "MUL"),
107
+ ))
108
+
109
+ foreign_keys = []
110
+ try:
111
+ with conn.cursor() as cur:
112
+ cur.execute("""
113
+ SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
114
+ FROM information_schema.KEY_COLUMN_USAGE
115
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
116
+ AND REFERENCED_TABLE_NAME IS NOT NULL
117
+ """, [self.database, table])
118
+ fk_rows = cur.fetchall()
119
+ for fk in fk_rows:
120
+ foreign_keys.append(ForeignKeyInfo(
121
+ column=fk["COLUMN_NAME"],
122
+ references_table=fk["REFERENCED_TABLE_NAME"],
123
+ references_column=fk["REFERENCED_COLUMN_NAME"],
124
+ ))
125
+ except Exception:
126
+ pass
127
+
128
+ stats = await self.get_table_stats(table)
129
+
130
+ return TableDetail(
131
+ name=table,
132
+ columns=columns,
133
+ foreign_keys=foreign_keys,
134
+ primary_key=pk_name,
135
+ stats=stats,
136
+ )
137
+
138
+ async def execute_query(self, sql: str, params: dict | None = None) -> QueryResult:
139
+ conn = self._ensure_conn()
140
+ start = time.monotonic()
141
+ try:
142
+ with conn.cursor() as cur:
143
+ if params:
144
+ cur.execute(sql, params)
145
+ else:
146
+ cur.execute(sql)
147
+ rows = cur.fetchall()
148
+ columns = [desc[0] for desc in cur.description] if cur.description else []
149
+ elapsed = int((time.monotonic() - start) * 1000)
150
+ return QueryResult(
151
+ columns=columns,
152
+ rows=rows,
153
+ row_count=len(rows),
154
+ truncated=False,
155
+ execution_time_ms=elapsed,
156
+ sql=sql,
157
+ )
158
+ except Exception as e:
159
+ elapsed = int((time.monotonic() - start) * 1000)
160
+ return QueryResult(sql=sql, execution_time_ms=elapsed, warning=f"Error: {str(e)}")
161
+
162
+ async def get_sample_data(self, table: str, limit: int = 5) -> QueryResult:
163
+ return await self.execute_query(f'SELECT * FROM `{table}` LIMIT {limit}')
164
+
165
+ async def get_table_stats(self, table: str) -> TableStats:
166
+ return TableStats(row_count=0, total_size="unknown")
167
+
168
+ def is_read_only(self) -> bool:
169
+ return self._read_only