d365fo-client 0.1.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.
- d365fo_client/__init__.py +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,555 @@
|
|
1
|
+
"""Database resource handler for MCP server."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import Any, Dict, List
|
8
|
+
|
9
|
+
from mcp.types import Resource
|
10
|
+
|
11
|
+
from ..client_manager import D365FOClientManager
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class DatabaseResourceHandler:
|
17
|
+
"""Handles database resources for the MCP server."""
|
18
|
+
|
19
|
+
def __init__(self, client_manager: D365FOClientManager):
|
20
|
+
"""Initialize the database resource handler.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
client_manager: D365FO client manager instance
|
24
|
+
"""
|
25
|
+
self.client_manager = client_manager
|
26
|
+
|
27
|
+
async def list_resources(self) -> List[Resource]:
|
28
|
+
"""List available database resources.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
List of database resources
|
32
|
+
"""
|
33
|
+
resources = [
|
34
|
+
Resource(
|
35
|
+
uri="d365fo://database/schema",
|
36
|
+
name="Database Schema",
|
37
|
+
description="Complete database schema with tables, columns, indexes, and relationships",
|
38
|
+
mimeType="application/json",
|
39
|
+
),
|
40
|
+
Resource(
|
41
|
+
uri="d365fo://database/statistics",
|
42
|
+
name="Database Statistics",
|
43
|
+
description="Database performance statistics, table sizes, and utilization metrics",
|
44
|
+
mimeType="application/json",
|
45
|
+
),
|
46
|
+
Resource(
|
47
|
+
uri="d365fo://database/tables",
|
48
|
+
name="Database Tables",
|
49
|
+
description="List of all database tables with basic information",
|
50
|
+
mimeType="application/json",
|
51
|
+
),
|
52
|
+
Resource(
|
53
|
+
uri="d365fo://database/indexes",
|
54
|
+
name="Database Indexes",
|
55
|
+
description="All database indexes and their characteristics",
|
56
|
+
mimeType="application/json",
|
57
|
+
),
|
58
|
+
Resource(
|
59
|
+
uri="d365fo://database/relationships",
|
60
|
+
name="Database Relationships",
|
61
|
+
description="Foreign key relationships between tables",
|
62
|
+
mimeType="application/json",
|
63
|
+
),
|
64
|
+
]
|
65
|
+
|
66
|
+
# Add individual table resources
|
67
|
+
try:
|
68
|
+
# Get list of tables from database
|
69
|
+
client = await self.client_manager.get_client()
|
70
|
+
if hasattr(client, 'metadata_cache') and client.metadata_cache:
|
71
|
+
table_names = await self._get_table_names(client.metadata_cache.db_path)
|
72
|
+
|
73
|
+
for table_name in table_names:
|
74
|
+
resources.append(
|
75
|
+
Resource(
|
76
|
+
uri=f"d365fo://database/tables/{table_name}",
|
77
|
+
name=f"Table: {table_name}",
|
78
|
+
description=f"Detailed schema and information for table {table_name}",
|
79
|
+
mimeType="application/json",
|
80
|
+
)
|
81
|
+
)
|
82
|
+
except Exception as e:
|
83
|
+
logger.warning(f"Could not load table list for resources: {e}")
|
84
|
+
|
85
|
+
logger.info(f"Listed {len(resources)} database resources")
|
86
|
+
return resources
|
87
|
+
|
88
|
+
async def read_resource(self, uri: str) -> str:
|
89
|
+
"""Read specific database resource.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
uri: Resource URI
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
JSON string with database resource content
|
96
|
+
"""
|
97
|
+
try:
|
98
|
+
if uri == "d365fo://database/schema":
|
99
|
+
return await self._get_complete_schema()
|
100
|
+
elif uri == "d365fo://database/statistics":
|
101
|
+
return await self._get_database_statistics()
|
102
|
+
elif uri == "d365fo://database/tables":
|
103
|
+
return await self._get_tables_list()
|
104
|
+
elif uri == "d365fo://database/indexes":
|
105
|
+
return await self._get_indexes_info()
|
106
|
+
elif uri == "d365fo://database/relationships":
|
107
|
+
return await self._get_relationships_info()
|
108
|
+
elif uri.startswith("d365fo://database/tables/"):
|
109
|
+
table_name = uri.split("/")[-1]
|
110
|
+
return await self._get_table_details(table_name)
|
111
|
+
else:
|
112
|
+
raise ValueError(f"Unknown database resource URI: {uri}")
|
113
|
+
except Exception as e:
|
114
|
+
logger.error(f"Failed to read database resource {uri}: {e}")
|
115
|
+
error_content = {
|
116
|
+
"error": str(e),
|
117
|
+
"uri": uri,
|
118
|
+
"timestamp": datetime.utcnow().isoformat(),
|
119
|
+
"resource_type": "database"
|
120
|
+
}
|
121
|
+
return json.dumps(error_content, indent=2)
|
122
|
+
|
123
|
+
async def _get_table_names(self, db_path: str) -> List[str]:
|
124
|
+
"""Get list of table names from database."""
|
125
|
+
import aiosqlite
|
126
|
+
|
127
|
+
async with aiosqlite.connect(db_path) as db:
|
128
|
+
cursor = await db.execute(
|
129
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
130
|
+
)
|
131
|
+
return [row[0] for row in await cursor.fetchall()]
|
132
|
+
|
133
|
+
async def _get_complete_schema(self) -> str:
|
134
|
+
"""Get complete database schema resource."""
|
135
|
+
try:
|
136
|
+
client = await self.client_manager.get_client()
|
137
|
+
if not hasattr(client, 'metadata_cache') or not client.metadata_cache:
|
138
|
+
raise ValueError("No metadata database available")
|
139
|
+
|
140
|
+
db_path = str(client.metadata_cache.db_path)
|
141
|
+
|
142
|
+
import aiosqlite
|
143
|
+
|
144
|
+
async with aiosqlite.connect(db_path) as db:
|
145
|
+
schema_info = {
|
146
|
+
"database_path": db_path,
|
147
|
+
"generated_at": datetime.utcnow().isoformat(),
|
148
|
+
"tables": {},
|
149
|
+
"summary": {
|
150
|
+
"total_tables": 0,
|
151
|
+
"total_columns": 0,
|
152
|
+
"total_indexes": 0,
|
153
|
+
"total_foreign_keys": 0
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
# Get all tables
|
158
|
+
cursor = await db.execute(
|
159
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
160
|
+
)
|
161
|
+
table_names = [row[0] for row in await cursor.fetchall()]
|
162
|
+
schema_info["summary"]["total_tables"] = len(table_names)
|
163
|
+
|
164
|
+
# Get detailed info for each table
|
165
|
+
for table_name in table_names:
|
166
|
+
table_info = await self._get_table_schema_info(db, table_name)
|
167
|
+
schema_info["tables"][table_name] = table_info
|
168
|
+
|
169
|
+
# Update summary counters
|
170
|
+
schema_info["summary"]["total_columns"] += len(table_info["columns"])
|
171
|
+
schema_info["summary"]["total_indexes"] += len(table_info["indexes"])
|
172
|
+
schema_info["summary"]["total_foreign_keys"] += len(table_info["foreign_keys"])
|
173
|
+
|
174
|
+
return json.dumps(schema_info, indent=2)
|
175
|
+
|
176
|
+
except Exception as e:
|
177
|
+
logger.error(f"Failed to get database schema: {e}")
|
178
|
+
raise
|
179
|
+
|
180
|
+
async def _get_table_schema_info(self, db, table_name: str) -> Dict[str, Any]:
|
181
|
+
"""Get schema information for a specific table."""
|
182
|
+
table_info = {"name": table_name}
|
183
|
+
|
184
|
+
# Get column information
|
185
|
+
cursor = await db.execute(f"PRAGMA table_info({table_name})")
|
186
|
+
columns = await cursor.fetchall()
|
187
|
+
table_info["columns"] = [
|
188
|
+
{
|
189
|
+
"cid": col[0],
|
190
|
+
"name": col[1],
|
191
|
+
"type": col[2],
|
192
|
+
"not_null": bool(col[3]),
|
193
|
+
"default_value": col[4],
|
194
|
+
"primary_key": bool(col[5])
|
195
|
+
}
|
196
|
+
for col in columns
|
197
|
+
]
|
198
|
+
|
199
|
+
# Get row count
|
200
|
+
try:
|
201
|
+
cursor = await db.execute(f"SELECT COUNT(*) FROM {table_name}")
|
202
|
+
table_info["row_count"] = (await cursor.fetchone())[0]
|
203
|
+
except Exception:
|
204
|
+
table_info["row_count"] = 0
|
205
|
+
|
206
|
+
# Get indexes
|
207
|
+
cursor = await db.execute(f"PRAGMA index_list({table_name})")
|
208
|
+
indexes = await cursor.fetchall()
|
209
|
+
table_info["indexes"] = []
|
210
|
+
for idx in indexes:
|
211
|
+
index_info = {
|
212
|
+
"name": idx[1],
|
213
|
+
"unique": bool(idx[2]),
|
214
|
+
"origin": idx[3]
|
215
|
+
}
|
216
|
+
# Get index columns
|
217
|
+
try:
|
218
|
+
cursor = await db.execute(f"PRAGMA index_info({idx[1]})")
|
219
|
+
index_columns = await cursor.fetchall()
|
220
|
+
index_info["columns"] = [col[2] for col in index_columns]
|
221
|
+
except Exception:
|
222
|
+
index_info["columns"] = []
|
223
|
+
table_info["indexes"].append(index_info)
|
224
|
+
|
225
|
+
# Get foreign keys
|
226
|
+
cursor = await db.execute(f"PRAGMA foreign_key_list({table_name})")
|
227
|
+
foreign_keys = await cursor.fetchall()
|
228
|
+
table_info["foreign_keys"] = [
|
229
|
+
{
|
230
|
+
"id": fk[0],
|
231
|
+
"seq": fk[1],
|
232
|
+
"table": fk[2],
|
233
|
+
"from": fk[3],
|
234
|
+
"to": fk[4],
|
235
|
+
"on_update": fk[5],
|
236
|
+
"on_delete": fk[6],
|
237
|
+
"match": fk[7]
|
238
|
+
}
|
239
|
+
for fk in foreign_keys
|
240
|
+
]
|
241
|
+
|
242
|
+
return table_info
|
243
|
+
|
244
|
+
async def _get_database_statistics(self) -> str:
|
245
|
+
"""Get database statistics resource."""
|
246
|
+
try:
|
247
|
+
client = await self.client_manager.get_client()
|
248
|
+
if not hasattr(client, 'metadata_cache') or not client.metadata_cache:
|
249
|
+
raise ValueError("No metadata database available")
|
250
|
+
|
251
|
+
# Use existing database statistics method
|
252
|
+
if hasattr(client.metadata_cache, 'database') and hasattr(client.metadata_cache.database, 'get_database_statistics'):
|
253
|
+
stats = await client.metadata_cache.database.get_database_statistics()
|
254
|
+
stats["generated_at"] = datetime.utcnow().isoformat()
|
255
|
+
stats["resource_type"] = "database_statistics"
|
256
|
+
return json.dumps(stats, indent=2)
|
257
|
+
else:
|
258
|
+
# Fallback to basic statistics
|
259
|
+
db_path = str(client.metadata_cache.db_path)
|
260
|
+
stats = await self._get_basic_statistics(db_path)
|
261
|
+
return json.dumps(stats, indent=2)
|
262
|
+
|
263
|
+
except Exception as e:
|
264
|
+
logger.error(f"Failed to get database statistics: {e}")
|
265
|
+
raise
|
266
|
+
|
267
|
+
async def _get_basic_statistics(self, db_path: str) -> Dict[str, Any]:
|
268
|
+
"""Get basic database statistics."""
|
269
|
+
import aiosqlite
|
270
|
+
import os
|
271
|
+
|
272
|
+
stats = {
|
273
|
+
"generated_at": datetime.utcnow().isoformat(),
|
274
|
+
"database_path": db_path,
|
275
|
+
"resource_type": "database_statistics"
|
276
|
+
}
|
277
|
+
|
278
|
+
# File size
|
279
|
+
try:
|
280
|
+
stats["database_size_bytes"] = os.path.getsize(db_path)
|
281
|
+
stats["database_size_mb"] = round(stats["database_size_bytes"] / (1024 * 1024), 2)
|
282
|
+
except Exception:
|
283
|
+
stats["database_size_bytes"] = None
|
284
|
+
stats["database_size_mb"] = None
|
285
|
+
|
286
|
+
async with aiosqlite.connect(db_path) as db:
|
287
|
+
# Table counts
|
288
|
+
cursor = await db.execute(
|
289
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
290
|
+
)
|
291
|
+
table_names = [row[0] for row in await cursor.fetchall()]
|
292
|
+
stats["table_count"] = len(table_names)
|
293
|
+
|
294
|
+
# Row counts by table
|
295
|
+
table_stats = {}
|
296
|
+
total_rows = 0
|
297
|
+
for table_name in table_names:
|
298
|
+
try:
|
299
|
+
cursor = await db.execute(f"SELECT COUNT(*) FROM {table_name}")
|
300
|
+
row_count = (await cursor.fetchone())[0]
|
301
|
+
table_stats[table_name] = row_count
|
302
|
+
total_rows += row_count
|
303
|
+
except Exception:
|
304
|
+
table_stats[table_name] = 0
|
305
|
+
|
306
|
+
stats["table_statistics"] = table_stats
|
307
|
+
stats["total_rows"] = total_rows
|
308
|
+
|
309
|
+
# Database page information
|
310
|
+
try:
|
311
|
+
cursor = await db.execute("PRAGMA page_count")
|
312
|
+
page_count = (await cursor.fetchone())[0]
|
313
|
+
|
314
|
+
cursor = await db.execute("PRAGMA page_size")
|
315
|
+
page_size = (await cursor.fetchone())[0]
|
316
|
+
|
317
|
+
stats["page_statistics"] = {
|
318
|
+
"page_count": page_count,
|
319
|
+
"page_size_bytes": page_size,
|
320
|
+
"calculated_size_bytes": page_count * page_size
|
321
|
+
}
|
322
|
+
except Exception:
|
323
|
+
stats["page_statistics"] = None
|
324
|
+
|
325
|
+
return stats
|
326
|
+
|
327
|
+
async def _get_tables_list(self) -> str:
|
328
|
+
"""Get tables list resource."""
|
329
|
+
try:
|
330
|
+
client = await self.client_manager.get_client()
|
331
|
+
if not hasattr(client, 'metadata_cache') or not client.metadata_cache:
|
332
|
+
raise ValueError("No metadata database available")
|
333
|
+
|
334
|
+
db_path = str(client.metadata_cache.db_path)
|
335
|
+
|
336
|
+
import aiosqlite
|
337
|
+
|
338
|
+
async with aiosqlite.connect(db_path) as db:
|
339
|
+
tables_info = {
|
340
|
+
"generated_at": datetime.utcnow().isoformat(),
|
341
|
+
"resource_type": "tables_list",
|
342
|
+
"tables": []
|
343
|
+
}
|
344
|
+
|
345
|
+
# Get table information
|
346
|
+
cursor = await db.execute(
|
347
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
348
|
+
)
|
349
|
+
table_names = [row[0] for row in await cursor.fetchall()]
|
350
|
+
|
351
|
+
for table_name in table_names:
|
352
|
+
table_info = {"name": table_name}
|
353
|
+
|
354
|
+
# Get row count
|
355
|
+
try:
|
356
|
+
cursor = await db.execute(f"SELECT COUNT(*) FROM {table_name}")
|
357
|
+
table_info["row_count"] = (await cursor.fetchone())[0]
|
358
|
+
except Exception:
|
359
|
+
table_info["row_count"] = 0
|
360
|
+
|
361
|
+
# Get column count
|
362
|
+
cursor = await db.execute(f"PRAGMA table_info({table_name})")
|
363
|
+
columns = await cursor.fetchall()
|
364
|
+
table_info["column_count"] = len(columns)
|
365
|
+
|
366
|
+
# Get primary key columns
|
367
|
+
pk_columns = [col[1] for col in columns if col[5]] # col[5] is primary key flag
|
368
|
+
table_info["primary_key_columns"] = pk_columns
|
369
|
+
|
370
|
+
tables_info["tables"].append(table_info)
|
371
|
+
|
372
|
+
tables_info["total_tables"] = len(tables_info["tables"])
|
373
|
+
return json.dumps(tables_info, indent=2)
|
374
|
+
|
375
|
+
except Exception as e:
|
376
|
+
logger.error(f"Failed to get tables list: {e}")
|
377
|
+
raise
|
378
|
+
|
379
|
+
async def _get_indexes_info(self) -> str:
|
380
|
+
"""Get indexes information resource."""
|
381
|
+
try:
|
382
|
+
client = await self.client_manager.get_client()
|
383
|
+
if not hasattr(client, 'metadata_cache') or not client.metadata_cache:
|
384
|
+
raise ValueError("No metadata database available")
|
385
|
+
|
386
|
+
db_path = str(client.metadata_cache.db_path)
|
387
|
+
|
388
|
+
import aiosqlite
|
389
|
+
|
390
|
+
async with aiosqlite.connect(db_path) as db:
|
391
|
+
indexes_info = {
|
392
|
+
"generated_at": datetime.utcnow().isoformat(),
|
393
|
+
"resource_type": "indexes_info",
|
394
|
+
"indexes": []
|
395
|
+
}
|
396
|
+
|
397
|
+
# Get all tables
|
398
|
+
cursor = await db.execute(
|
399
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
400
|
+
)
|
401
|
+
table_names = [row[0] for row in await cursor.fetchall()]
|
402
|
+
|
403
|
+
for table_name in table_names:
|
404
|
+
# Get indexes for this table
|
405
|
+
cursor = await db.execute(f"PRAGMA index_list({table_name})")
|
406
|
+
indexes = await cursor.fetchall()
|
407
|
+
|
408
|
+
for idx in indexes:
|
409
|
+
index_info = {
|
410
|
+
"table": table_name,
|
411
|
+
"name": idx[1],
|
412
|
+
"unique": bool(idx[2]),
|
413
|
+
"origin": idx[3]
|
414
|
+
}
|
415
|
+
|
416
|
+
# Get index columns
|
417
|
+
try:
|
418
|
+
cursor = await db.execute(f"PRAGMA index_info({idx[1]})")
|
419
|
+
index_columns = await cursor.fetchall()
|
420
|
+
index_info["columns"] = [
|
421
|
+
{"seq": col[0], "column": col[2]} for col in index_columns
|
422
|
+
]
|
423
|
+
except Exception:
|
424
|
+
index_info["columns"] = []
|
425
|
+
|
426
|
+
indexes_info["indexes"].append(index_info)
|
427
|
+
|
428
|
+
indexes_info["total_indexes"] = len(indexes_info["indexes"])
|
429
|
+
return json.dumps(indexes_info, indent=2)
|
430
|
+
|
431
|
+
except Exception as e:
|
432
|
+
logger.error(f"Failed to get indexes info: {e}")
|
433
|
+
raise
|
434
|
+
|
435
|
+
async def _get_relationships_info(self) -> str:
|
436
|
+
"""Get relationships information resource."""
|
437
|
+
try:
|
438
|
+
client = await self.client_manager.get_client()
|
439
|
+
if not hasattr(client, 'metadata_cache') or not client.metadata_cache:
|
440
|
+
raise ValueError("No metadata database available")
|
441
|
+
|
442
|
+
db_path = str(client.metadata_cache.db_path)
|
443
|
+
|
444
|
+
import aiosqlite
|
445
|
+
|
446
|
+
async with aiosqlite.connect(db_path) as db:
|
447
|
+
relationships_info = {
|
448
|
+
"generated_at": datetime.utcnow().isoformat(),
|
449
|
+
"resource_type": "relationships_info",
|
450
|
+
"foreign_keys": [],
|
451
|
+
"relationship_summary": {}
|
452
|
+
}
|
453
|
+
|
454
|
+
# Get all tables
|
455
|
+
cursor = await db.execute(
|
456
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
457
|
+
)
|
458
|
+
table_names = [row[0] for row in await cursor.fetchall()]
|
459
|
+
|
460
|
+
relationship_summary = {}
|
461
|
+
|
462
|
+
for table_name in table_names:
|
463
|
+
# Get foreign keys for this table
|
464
|
+
cursor = await db.execute(f"PRAGMA foreign_key_list({table_name})")
|
465
|
+
foreign_keys = await cursor.fetchall()
|
466
|
+
|
467
|
+
table_relationships = {
|
468
|
+
"references": [], # Tables this table references
|
469
|
+
"referenced_by": [] # Will be filled in second pass
|
470
|
+
}
|
471
|
+
|
472
|
+
for fk in foreign_keys:
|
473
|
+
fk_info = {
|
474
|
+
"from_table": table_name,
|
475
|
+
"from_column": fk[3],
|
476
|
+
"to_table": fk[2],
|
477
|
+
"to_column": fk[4],
|
478
|
+
"on_update": fk[5],
|
479
|
+
"on_delete": fk[6]
|
480
|
+
}
|
481
|
+
relationships_info["foreign_keys"].append(fk_info)
|
482
|
+
table_relationships["references"].append({
|
483
|
+
"table": fk[2],
|
484
|
+
"via_column": fk[3]
|
485
|
+
})
|
486
|
+
|
487
|
+
relationship_summary[table_name] = table_relationships
|
488
|
+
|
489
|
+
# Second pass to find reverse relationships
|
490
|
+
for fk in relationships_info["foreign_keys"]:
|
491
|
+
to_table = fk["to_table"]
|
492
|
+
if to_table in relationship_summary:
|
493
|
+
relationship_summary[to_table]["referenced_by"].append({
|
494
|
+
"table": fk["from_table"],
|
495
|
+
"via_column": fk["to_column"]
|
496
|
+
})
|
497
|
+
|
498
|
+
relationships_info["relationship_summary"] = relationship_summary
|
499
|
+
relationships_info["total_foreign_keys"] = len(relationships_info["foreign_keys"])
|
500
|
+
|
501
|
+
return json.dumps(relationships_info, indent=2)
|
502
|
+
|
503
|
+
except Exception as e:
|
504
|
+
logger.error(f"Failed to get relationships info: {e}")
|
505
|
+
raise
|
506
|
+
|
507
|
+
async def _get_table_details(self, table_name: str) -> str:
|
508
|
+
"""Get detailed information for a specific table."""
|
509
|
+
try:
|
510
|
+
client = await self.client_manager.get_client()
|
511
|
+
if not hasattr(client, 'metadata_cache') or not client.metadata_cache:
|
512
|
+
raise ValueError("No metadata database available")
|
513
|
+
|
514
|
+
db_path = str(client.metadata_cache.db_path)
|
515
|
+
|
516
|
+
import aiosqlite
|
517
|
+
|
518
|
+
async with aiosqlite.connect(db_path) as db:
|
519
|
+
# Verify table exists
|
520
|
+
cursor = await db.execute(
|
521
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
522
|
+
(table_name,)
|
523
|
+
)
|
524
|
+
if not await cursor.fetchone():
|
525
|
+
raise ValueError(f"Table '{table_name}' does not exist")
|
526
|
+
|
527
|
+
table_details = {
|
528
|
+
"generated_at": datetime.utcnow().isoformat(),
|
529
|
+
"resource_type": "table_details",
|
530
|
+
"table_name": table_name
|
531
|
+
}
|
532
|
+
|
533
|
+
# Get complete table schema info
|
534
|
+
table_info = await self._get_table_schema_info(db, table_name)
|
535
|
+
table_details.update(table_info)
|
536
|
+
|
537
|
+
# Add sample data (first 3 rows)
|
538
|
+
try:
|
539
|
+
cursor = await db.execute(f"SELECT * FROM {table_name} LIMIT 3")
|
540
|
+
sample_rows = await cursor.fetchall()
|
541
|
+
if sample_rows:
|
542
|
+
column_names = [desc[0] for desc in cursor.description]
|
543
|
+
table_details["sample_data"] = {
|
544
|
+
"columns": column_names,
|
545
|
+
"rows": [list(row) for row in sample_rows],
|
546
|
+
"note": "Limited to first 3 rows for preview"
|
547
|
+
}
|
548
|
+
except Exception as e:
|
549
|
+
table_details["sample_data_error"] = str(e)
|
550
|
+
|
551
|
+
return json.dumps(table_details, indent=2)
|
552
|
+
|
553
|
+
except Exception as e:
|
554
|
+
logger.error(f"Failed to get table details for {table_name}: {e}")
|
555
|
+
raise
|