Lukynnnn commited on
Commit
65462d5
·
verified ·
1 Parent(s): bc50a60

Upload mcp_database_universal/engines/sqlite.py with huggingface_hub

Browse files
mcp_database_universal/engines/sqlite.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SQLite engine — built-in, no external dependencies."""
2
+
3
+ import sqlite3
4
+ import os
5
+ import time
6
+ from mcp_database_universal.engines.base import (
7
+ BaseEngine, DBInfo, ColumnInfo, TableInfo, TableDetail,
8
+ IndexInfo, ForeignKeyInfo, TableStats, QueryResult,
9
+ )
10
+
11
+
12
+ class SQLiteEngine(BaseEngine):
13
+ def __init__(self, path: str = ":memory:", read_only: bool = True):
14
+ self.path = path
15
+ self._read_only = read_only
16
+ self._conn: sqlite3.Connection | None = None
17
+
18
+ async def connect(self) -> None:
19
+ if self.path == ":memory:":
20
+ self._conn = sqlite3.connect(":memory:")
21
+ else:
22
+ dir_path = os.path.dirname(self.path)
23
+ if dir_path:
24
+ os.makedirs(dir_path, exist_ok=True)
25
+ if self._read_only:
26
+ uri = f"file:{self.path}?mode=ro"
27
+ self._conn = sqlite3.connect(uri, uri=True)
28
+ else:
29
+ self._conn = sqlite3.connect(self.path)
30
+ self._conn.row_factory = sqlite3.Row
31
+ if not self._read_only:
32
+ try:
33
+ self._conn.execute("PRAGMA journal_mode=WAL")
34
+ except sqlite3.OperationalError:
35
+ pass
36
+ self._conn.execute("PRAGMA foreign_keys=ON")
37
+
38
+ async def disconnect(self) -> None:
39
+ if self._conn:
40
+ self._conn.close()
41
+ self._conn = None
42
+
43
+ def _ensure_conn(self) -> sqlite3.Connection:
44
+ if self._conn is None:
45
+ raise RuntimeError("Not connected. Call connect() first.")
46
+ return self._conn
47
+
48
+ async def get_db_info(self) -> DBInfo:
49
+ conn = self._ensure_conn()
50
+ version = conn.execute("SELECT sqlite_version()").fetchone()[0]
51
+ if self.path == ":memory:":
52
+ name = ":memory:"
53
+ size = "~0 KB"
54
+ else:
55
+ name = self.path
56
+ try:
57
+ size_bytes = os.path.getsize(self.path)
58
+ if size_bytes < 1024:
59
+ size = f"~{size_bytes} B"
60
+ elif size_bytes < 1024 * 1024:
61
+ size = f"~{size_bytes // 1024} KB"
62
+ else:
63
+ size = f"~{size_bytes // (1024 * 1024)} MB"
64
+ except OSError:
65
+ size = "unknown"
66
+ return DBInfo(engine="sqlite", version=version, name=name, size_approx=size)
67
+
68
+ async def get_tables(self) -> list[TableInfo]:
69
+ conn = self._ensure_conn()
70
+ rows = conn.execute(
71
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
72
+ ).fetchall()
73
+
74
+ tables = []
75
+ for row in rows:
76
+ name = row["name"]
77
+ try:
78
+ count = conn.execute(f'SELECT COUNT(*) FROM "{name}"').fetchone()[0]
79
+ except Exception:
80
+ count = 0
81
+
82
+ try:
83
+ cols = conn.execute(f'PRAGMA table_info("{name}")').fetchall()
84
+ col_count = len(cols)
85
+ has_pk = any(c["pk"] for c in cols)
86
+ except Exception:
87
+ col_count = 0
88
+ has_pk = False
89
+
90
+ try:
91
+ fks = conn.execute(f'PRAGMA foreign_key_list("{name}")').fetchall()
92
+ fk_out = len(fks)
93
+ except Exception:
94
+ fk_out = 0
95
+
96
+ tables.append(TableInfo(
97
+ name=name,
98
+ row_count=count,
99
+ column_count=col_count,
100
+ has_primary_key=has_pk,
101
+ foreign_keys_out=fk_out,
102
+ foreign_keys_in=0,
103
+ ))
104
+
105
+ for t in tables:
106
+ for other in tables:
107
+ if other.name == t.name:
108
+ continue
109
+ try:
110
+ fks = conn.execute(f'PRAGMA foreign_key_list("{other.name}")').fetchall()
111
+ for fk in fks:
112
+ if fk["table"] == t.name:
113
+ t.foreign_keys_in += 1
114
+ except Exception:
115
+ pass
116
+
117
+ return tables
118
+
119
+ async def get_table_detail(self, table: str) -> TableDetail:
120
+ conn = self._ensure_conn()
121
+
122
+ col_rows = conn.execute(f'PRAGMA table_info("{table}")').fetchall()
123
+ columns = []
124
+ pk_name = None
125
+ for c in col_rows:
126
+ is_pk = bool(c["pk"])
127
+ if is_pk:
128
+ pk_name = c["name"]
129
+ columns.append(ColumnInfo(
130
+ name=c["name"],
131
+ type=c["type"] or "TEXT",
132
+ nullable=not c["notnull"],
133
+ default=c["dflt_value"],
134
+ is_primary_key=is_pk,
135
+ ))
136
+
137
+ fk_rows = conn.execute(f'PRAGMA foreign_key_list("{table}")').fetchall()
138
+ foreign_keys = []
139
+ fk_cols = set()
140
+ for fk in fk_rows:
141
+ foreign_keys.append(ForeignKeyInfo(
142
+ column=fk["from"],
143
+ references_table=fk["table"],
144
+ references_column=fk["to"],
145
+ ))
146
+ fk_cols.add(fk["from"])
147
+
148
+ for col in columns:
149
+ if col.name in fk_cols:
150
+ col.is_foreign_key = True
151
+ for fk in foreign_keys:
152
+ if fk.column == col.name:
153
+ col.foreign_key_table = fk.references_table
154
+ col.foreign_key_column = fk.references_column
155
+ break
156
+
157
+ idx_rows = conn.execute(f'PRAGMA index_list("{table}")').fetchall()
158
+ indexes = []
159
+ for idx in idx_rows:
160
+ idx_info = conn.execute(f'PRAGMA index_info("{idx["name"]}")').fetchall()
161
+ idx_cols = [i["name"] for i in idx_info]
162
+ indexes.append(IndexInfo(
163
+ name=idx["name"],
164
+ columns=idx_cols,
165
+ unique=bool(idx["unique"]),
166
+ ))
167
+
168
+ try:
169
+ sample = conn.execute(f'SELECT * FROM "{table}" LIMIT 5').fetchall()
170
+ sample_data = [dict(row) for row in sample]
171
+ except Exception:
172
+ sample_data = []
173
+
174
+ stats = await self.get_table_stats(table)
175
+
176
+ return TableDetail(
177
+ name=table,
178
+ columns=columns,
179
+ indexes=indexes,
180
+ primary_key=pk_name,
181
+ foreign_keys=foreign_keys,
182
+ sample_data=sample_data,
183
+ stats=stats,
184
+ )
185
+
186
+ async def execute_query(self, sql: str, params: dict | None = None) -> QueryResult:
187
+ conn = self._ensure_conn()
188
+ start = time.monotonic()
189
+ try:
190
+ if params:
191
+ cursor = conn.execute(sql, params)
192
+ else:
193
+ cursor = conn.execute(sql)
194
+ rows = cursor.fetchall()
195
+ columns = [desc[0] for desc in cursor.description] if cursor.description else []
196
+ elapsed = int((time.monotonic() - start) * 1000)
197
+ result_rows = [dict(row) for row in rows]
198
+ return QueryResult(
199
+ columns=columns,
200
+ rows=result_rows,
201
+ row_count=len(result_rows),
202
+ truncated=False,
203
+ execution_time_ms=elapsed,
204
+ sql=sql,
205
+ )
206
+ except Exception as e:
207
+ elapsed = int((time.monotonic() - start) * 1000)
208
+ return QueryResult(
209
+ sql=sql,
210
+ execution_time_ms=elapsed,
211
+ warning=f"Error: {str(e)}",
212
+ )
213
+
214
+ async def get_sample_data(self, table: str, limit: int = 5) -> QueryResult:
215
+ sql = f'SELECT * FROM "{table}" LIMIT {limit}'
216
+ return await self.execute_query(sql)
217
+
218
+ async def get_table_stats(self, table: str) -> TableStats:
219
+ conn = self._ensure_conn()
220
+ try:
221
+ row_count = conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0]
222
+ except Exception:
223
+ row_count = 0
224
+
225
+ try:
226
+ page_count = conn.execute("PRAGMA page_count").fetchone()[0]
227
+ page_size = conn.execute("PRAGMA page_size").fetchone()[0]
228
+ total_bytes = page_count * page_size
229
+ if total_bytes < 1024:
230
+ total_size = f"~{total_bytes} B"
231
+ elif total_bytes < 1024 * 1024:
232
+ total_size = f"~{total_bytes // 1024} KB"
233
+ else:
234
+ total_size = f"~{total_bytes // (1024 * 1024)} MB"
235
+ avg_row_size = f"~{total_bytes // max(row_count, 1)} B" if row_count > 0 else "~0 B"
236
+ except Exception:
237
+ total_size = "unknown"
238
+ avg_row_size = "unknown"
239
+
240
+ null_counts: dict[str, int] = {}
241
+ try:
242
+ col_rows = conn.execute(f'PRAGMA table_info("{table}")').fetchall()
243
+ for c in col_rows:
244
+ try:
245
+ cnt = conn.execute(
246
+ f'SELECT COUNT(*) FROM "{table}" WHERE "{c["name"]}" IS NULL'
247
+ ).fetchone()[0]
248
+ if cnt > 0:
249
+ null_counts[c["name"]] = cnt
250
+ except Exception:
251
+ pass
252
+ except Exception:
253
+ pass
254
+
255
+ return TableStats(
256
+ row_count=row_count,
257
+ avg_row_size=avg_row_size,
258
+ total_size=total_size,
259
+ null_counts=null_counts,
260
+ )
261
+
262
+ def is_read_only(self) -> bool:
263
+ return self._read_only