Lukynnnn commited on
Commit
1423444
·
verified ·
1 Parent(s): 40a5645

Upload mcp_database_universal/schema_inspector.py with huggingface_hub

Browse files
mcp_database_universal/schema_inspector.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Schema inspector — discovers tables, columns, relationships."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from mcp_database_universal.engines.base import BaseEngine
5
+
6
+
7
+ @dataclass
8
+ class Relationship:
9
+ from_table: str
10
+ from_column: str
11
+ to_table: str
12
+ to_column: str
13
+ cardinality: str # "one-to-one", "one-to-many", "many-to-many"
14
+ is_explicit: bool # from DB constraint or from naming convention
15
+
16
+
17
+ class SchemaInspector:
18
+ def __init__(self, engine: BaseEngine):
19
+ self.engine = engine
20
+
21
+ async def discover_relationships(self) -> list[Relationship]:
22
+ tables = await self.engine.get_tables()
23
+ relationships: list[Relationship] = []
24
+ explicit_pairs: set[tuple[str, str, str, str]] = set()
25
+
26
+ for table_info in tables:
27
+ detail = await self.engine.get_table_detail(table_info.name)
28
+ for fk in detail.foreign_keys:
29
+ relationships.append(Relationship(
30
+ from_table=table_info.name,
31
+ from_column=fk.column,
32
+ to_table=fk.references_table,
33
+ to_column=fk.references_column,
34
+ cardinality="many-to-one",
35
+ is_explicit=True,
36
+ ))
37
+ explicit_pairs.add((
38
+ table_info.name, fk.column,
39
+ fk.references_table, fk.references_column,
40
+ ))
41
+
42
+ inferred = self._infer_by_naming(tables, explicit_pairs)
43
+ relationships.extend(inferred)
44
+
45
+ return relationships
46
+
47
+ def _infer_by_naming(
48
+ self,
49
+ tables: list,
50
+ explicit_pairs: set[tuple[str, str, str, str]],
51
+ ) -> list[Relationship]:
52
+ table_names = {t.name.lower() for t in tables}
53
+ inferred: list[Relationship] = []
54
+
55
+ for table_info in tables:
56
+ detail = None
57
+ for col in (table_info.columns if hasattr(table_info, 'columns') else []):
58
+ col_lower = col.name.lower()
59
+ if col_lower.endswith("_id") and col_lower != "id":
60
+ candidate_table = col_lower[:-3]
61
+ if candidate_table in table_names:
62
+ pair = (table_info.name, col.name, candidate_table, "id")
63
+ if pair not in explicit_pairs:
64
+ inferred.append(Relationship(
65
+ from_table=table_info.name,
66
+ from_column=col.name,
67
+ to_table=candidate_table,
68
+ to_column="id",
69
+ cardinality="many-to-one",
70
+ is_explicit=False,
71
+ ))
72
+
73
+ return inferred
74
+
75
+ def detect_junction_tables(
76
+ self, relationships: list[Relationship]
77
+ ) -> list[str]:
78
+ fk_count: dict[str, list[Relationship]] = {}
79
+ for rel in relationships:
80
+ fk_count.setdefault(rel.from_table, []).append(rel)
81
+
82
+ junctions = []
83
+ for table, rels in fk_count.items():
84
+ unique_targets = set(r.to_table for r in rels)
85
+ if len(unique_targets) >= 2:
86
+ junctions.append(table)
87
+
88
+ return junctions
89
+
90
+ def generate_mermaid(self, relationships: list[Relationship]) -> str:
91
+ lines = ["erDiagram"]
92
+
93
+ seen_tables: set[str] = set()
94
+ for rel in relationships:
95
+ seen_tables.add(rel.from_table)
96
+ seen_tables.add(rel.to_table)
97
+
98
+ for table in sorted(seen_tables):
99
+ lines.append(f" {table} {{")
100
+
101
+ rels_from = [r for r in relationships if r.from_table == table]
102
+ for rel in rels_from:
103
+ lines.append(f" string {rel.from_column} FK")
104
+
105
+ lines.append(" }")
106
+
107
+ for rel in relationships:
108
+ if rel.cardinality == "many-to-one":
109
+ lines.append(" " + rel.to_table + " ||--o{ " + rel.from_table + " : has")
110
+ elif rel.cardinality == "one-to-one":
111
+ lines.append(" " + rel.to_table + " ||--|| " + rel.from_table + " : has")
112
+ elif rel.cardinality == "many-to-many":
113
+ lines.append(" " + rel.to_table + " }o--o{ " + rel.from_table + " : has")
114
+
115
+ return "\n".join(lines)