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

Upload mcp_database_universal/engines/postgres.py with huggingface_hub

Browse files
mcp_database_universal/engines/postgres.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PostgreSQL engine — optional dependency: psycopg[binary]>=3.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 PostgresEngine(BaseEngine):
11
+ def __init__(
12
+ self,
13
+ host: str = "localhost",
14
+ port: int = 5432,
15
+ database: str = "postgres",
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 psycopg
31
+ except ImportError:
32
+ raise RuntimeError(
33
+ "PostgreSQL engine requires psycopg. "
34
+ "Install with: pip install 'mcp-database-universal[postgres]'"
35
+ )
36
+ self._conn = await psycopg.AsyncConnection.connect(
37
+ host=self.host,
38
+ port=self.port,
39
+ dbname=self.database,
40
+ user=self.user,
41
+ password=self.password,
42
+ autocommit=True,
43
+ )
44
+
45
+ async def disconnect(self) -> None:
46
+ if self._conn:
47
+ await 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
+ row = await conn.execute("SELECT version()").fetchone()
58
+ version = row[0] if row else "unknown"
59
+ try:
60
+ row2 = await conn.execute(
61
+ "SELECT pg_size_pretty(pg_database_size(current_database()))"
62
+ ).fetchone()
63
+ size = row2[0] if row2 else "unknown"
64
+ except Exception:
65
+ size = "unknown"
66
+ return DBInfo(engine="postgresql", version=version, name=self.database, size_approx=size)
67
+
68
+ async def get_tables(self) -> list[TableInfo]:
69
+ conn = self._ensure_conn()
70
+ rows = await conn.execute("""
71
+ SELECT t.table_name
72
+ FROM information_schema.tables t
73
+ WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
74
+ ORDER BY t.table_name
75
+ """).fetchall()
76
+
77
+ tables = []
78
+ for row in rows:
79
+ name = row[0]
80
+ try:
81
+ cnt = await conn.execute(f'SELECT COUNT(*) FROM "{name}"').fetchone()
82
+ row_count = cnt[0] if cnt else 0
83
+ except Exception:
84
+ row_count = 0
85
+
86
+ try:
87
+ cols = await conn.execute("""
88
+ SELECT COUNT(*) FROM information_schema.columns
89
+ WHERE table_schema = 'public' AND table_name = %s
90
+ """, [name]).fetchone()
91
+ col_count = cols[0] if cols else 0
92
+ except Exception:
93
+ col_count = 0
94
+
95
+ tables.append(TableInfo(
96
+ name=name,
97
+ row_count=row_count,
98
+ column_count=col_count,
99
+ ))
100
+
101
+ return tables
102
+
103
+ async def get_table_detail(self, table: str) -> TableDetail:
104
+ conn = self._ensure_conn()
105
+
106
+ col_rows = await conn.execute("""
107
+ SELECT c.column_name, c.data_type, c.is_nullable, c.column_default,
108
+ CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_pk
109
+ FROM information_schema.columns c
110
+ LEFT JOIN (
111
+ SELECT ku.column_name
112
+ FROM information_schema.table_constraints tc
113
+ JOIN information_schema.key_column_usage ku
114
+ ON tc.constraint_name = ku.constraint_name
115
+ WHERE tc.table_name = %s AND tc.constraint_type = 'PRIMARY KEY'
116
+ ) pk ON c.column_name = pk.column_name
117
+ WHERE c.table_schema = 'public' AND c.table_name = %s
118
+ ORDER BY c.ordinal_position
119
+ """, [table, table]).fetchall()
120
+
121
+ columns = []
122
+ pk_name = None
123
+ for c in col_rows:
124
+ is_pk = c[4]
125
+ if is_pk:
126
+ pk_name = c[0]
127
+ columns.append(ColumnInfo(
128
+ name=c[0],
129
+ type=c[1],
130
+ nullable=(c[2] == "YES"),
131
+ default=c[3],
132
+ is_primary_key=is_pk,
133
+ ))
134
+
135
+ fk_rows = await conn.execute("""
136
+ SELECT
137
+ kcu.column_name,
138
+ ccu.table_name AS foreign_table_name,
139
+ ccu.column_name AS foreign_column_name
140
+ FROM information_schema.table_constraints tc
141
+ JOIN information_schema.key_column_usage kcu
142
+ ON tc.constraint_name = kcu.constraint_name
143
+ JOIN information_schema.constraint_column_usage ccu
144
+ ON tc.constraint_name = ccu.constraint_name
145
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = %s
146
+ """, [table]).fetchall()
147
+
148
+ foreign_keys = []
149
+ fk_cols = set()
150
+ for fk in fk_rows:
151
+ foreign_keys.append(ForeignKeyInfo(
152
+ column=fk[0],
153
+ references_table=fk[1],
154
+ references_column=fk[2],
155
+ ))
156
+ fk_cols.add(fk[0])
157
+
158
+ for col in columns:
159
+ if col.name in fk_cols:
160
+ col.is_foreign_key = True
161
+ for fk in foreign_keys:
162
+ if fk.column == col.name:
163
+ col.foreign_key_table = fk.references_table
164
+ col.foreign_key_column = fk.references_column
165
+ break
166
+
167
+ idx_rows = await conn.execute("""
168
+ SELECT indexname, indexdef
169
+ FROM pg_indexes
170
+ WHERE tablename = %s AND schemaname = 'public'
171
+ """, [table]).fetchall()
172
+
173
+ indexes = []
174
+ for idx in idx_rows:
175
+ name = idx[0]
176
+ unique = "UNIQUE" in (idx[1] or "").upper()
177
+ indexes.append(IndexInfo(name=name, columns=[], unique=unique))
178
+
179
+ try:
180
+ sample = await conn.execute(f'SELECT * FROM "{table}" LIMIT 5').fetchall()
181
+ if sample:
182
+ cols_desc = sample[0].keys() if hasattr(sample[0], 'keys') else []
183
+ sample_data = [dict(row) for row in sample]
184
+ else:
185
+ sample_data = []
186
+ except Exception:
187
+ sample_data = []
188
+
189
+ stats = await self.get_table_stats(table)
190
+
191
+ return TableDetail(
192
+ name=table,
193
+ columns=columns,
194
+ indexes=indexes,
195
+ primary_key=pk_name,
196
+ foreign_keys=foreign_keys,
197
+ sample_data=sample_data,
198
+ stats=stats,
199
+ )
200
+
201
+ async def execute_query(self, sql: str, params: dict | None = None) -> QueryResult:
202
+ conn = self._ensure_conn()
203
+ start = time.monotonic()
204
+ try:
205
+ if params:
206
+ cur = await conn.execute(sql, params)
207
+ else:
208
+ cur = await conn.execute(sql)
209
+ rows = await cur.fetchall()
210
+ columns = [desc.name for desc in cur.description] if cur.description else []
211
+ elapsed = int((time.monotonic() - start) * 1000)
212
+ result_rows = [dict(row) for row in rows]
213
+ return QueryResult(
214
+ columns=columns,
215
+ rows=result_rows,
216
+ row_count=len(result_rows),
217
+ truncated=False,
218
+ execution_time_ms=elapsed,
219
+ sql=sql,
220
+ )
221
+ except Exception as e:
222
+ elapsed = int((time.monotonic() - start) * 1000)
223
+ return QueryResult(
224
+ sql=sql,
225
+ execution_time_ms=elapsed,
226
+ warning=f"Error: {str(e)}",
227
+ )
228
+
229
+ async def get_sample_data(self, table: str, limit: int = 5) -> QueryResult:
230
+ return await self.execute_query(f'SELECT * FROM "{table}" LIMIT {limit}')
231
+
232
+ async def get_table_stats(self, table: str) -> TableStats:
233
+ conn = self._ensure_conn()
234
+ try:
235
+ row = await conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()
236
+ row_count = row[0] if row else 0
237
+ except Exception:
238
+ row_count = 0
239
+
240
+ try:
241
+ row = await conn.execute(
242
+ "SELECT pg_size_pretty(pg_total_relation_size(%s))", [table]
243
+ ).fetchone()
244
+ total_size = row[0] if row else "unknown"
245
+ except Exception:
246
+ total_size = "unknown"
247
+
248
+ return TableStats(
249
+ row_count=row_count,
250
+ total_size=total_size,
251
+ )
252
+
253
+ def is_read_only(self) -> bool:
254
+ return self._read_only