sqlserver-semantic-mcp 0.5.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.
Files changed (74) hide show
  1. sqlserver_semantic_mcp/__init__.py +1 -0
  2. sqlserver_semantic_mcp/config.py +78 -0
  3. sqlserver_semantic_mcp/domain/__init__.py +0 -0
  4. sqlserver_semantic_mcp/domain/enums.py +48 -0
  5. sqlserver_semantic_mcp/domain/models/__init__.py +0 -0
  6. sqlserver_semantic_mcp/domain/models/column.py +14 -0
  7. sqlserver_semantic_mcp/domain/models/object.py +13 -0
  8. sqlserver_semantic_mcp/domain/models/relationship.py +11 -0
  9. sqlserver_semantic_mcp/domain/models/table.py +29 -0
  10. sqlserver_semantic_mcp/infrastructure/__init__.py +0 -0
  11. sqlserver_semantic_mcp/infrastructure/background.py +59 -0
  12. sqlserver_semantic_mcp/infrastructure/cache/__init__.py +0 -0
  13. sqlserver_semantic_mcp/infrastructure/cache/semantic.py +132 -0
  14. sqlserver_semantic_mcp/infrastructure/cache/store.py +152 -0
  15. sqlserver_semantic_mcp/infrastructure/cache/structural.py +203 -0
  16. sqlserver_semantic_mcp/infrastructure/connection.py +78 -0
  17. sqlserver_semantic_mcp/infrastructure/queries/__init__.py +0 -0
  18. sqlserver_semantic_mcp/infrastructure/queries/comment_queries.py +18 -0
  19. sqlserver_semantic_mcp/infrastructure/queries/metadata_queries.py +70 -0
  20. sqlserver_semantic_mcp/infrastructure/queries/object_queries.py +15 -0
  21. sqlserver_semantic_mcp/main.py +90 -0
  22. sqlserver_semantic_mcp/policy/__init__.py +0 -0
  23. sqlserver_semantic_mcp/policy/analyzer.py +194 -0
  24. sqlserver_semantic_mcp/policy/enforcer.py +104 -0
  25. sqlserver_semantic_mcp/policy/intents/__init__.py +16 -0
  26. sqlserver_semantic_mcp/policy/intents/ast_analyzer.py +24 -0
  27. sqlserver_semantic_mcp/policy/intents/base.py +17 -0
  28. sqlserver_semantic_mcp/policy/intents/regex_analyzer.py +11 -0
  29. sqlserver_semantic_mcp/policy/intents/router.py +21 -0
  30. sqlserver_semantic_mcp/policy/loader.py +90 -0
  31. sqlserver_semantic_mcp/policy/models.py +43 -0
  32. sqlserver_semantic_mcp/server/__init__.py +0 -0
  33. sqlserver_semantic_mcp/server/app.py +125 -0
  34. sqlserver_semantic_mcp/server/compact.py +74 -0
  35. sqlserver_semantic_mcp/server/prompts/__init__.py +5 -0
  36. sqlserver_semantic_mcp/server/prompts/analysis.py +56 -0
  37. sqlserver_semantic_mcp/server/prompts/discovery.py +55 -0
  38. sqlserver_semantic_mcp/server/prompts/execution.py +64 -0
  39. sqlserver_semantic_mcp/server/prompts/registry.py +41 -0
  40. sqlserver_semantic_mcp/server/resources/__init__.py +1 -0
  41. sqlserver_semantic_mcp/server/resources/schema.py +144 -0
  42. sqlserver_semantic_mcp/server/tools/__init__.py +42 -0
  43. sqlserver_semantic_mcp/server/tools/cache.py +24 -0
  44. sqlserver_semantic_mcp/server/tools/metadata.py +167 -0
  45. sqlserver_semantic_mcp/server/tools/metrics.py +44 -0
  46. sqlserver_semantic_mcp/server/tools/object_tool.py +113 -0
  47. sqlserver_semantic_mcp/server/tools/policy.py +48 -0
  48. sqlserver_semantic_mcp/server/tools/query.py +159 -0
  49. sqlserver_semantic_mcp/server/tools/relationship.py +104 -0
  50. sqlserver_semantic_mcp/server/tools/semantic.py +112 -0
  51. sqlserver_semantic_mcp/server/tools/shape.py +204 -0
  52. sqlserver_semantic_mcp/server/tools/workflow.py +307 -0
  53. sqlserver_semantic_mcp/services/__init__.py +0 -0
  54. sqlserver_semantic_mcp/services/metadata_service.py +173 -0
  55. sqlserver_semantic_mcp/services/metrics_service.py +124 -0
  56. sqlserver_semantic_mcp/services/object_service.py +187 -0
  57. sqlserver_semantic_mcp/services/policy_service.py +59 -0
  58. sqlserver_semantic_mcp/services/query_service.py +321 -0
  59. sqlserver_semantic_mcp/services/relationship_service.py +160 -0
  60. sqlserver_semantic_mcp/services/semantic_service.py +277 -0
  61. sqlserver_semantic_mcp/workflows/__init__.py +26 -0
  62. sqlserver_semantic_mcp/workflows/bundle.py +157 -0
  63. sqlserver_semantic_mcp/workflows/contracts.py +64 -0
  64. sqlserver_semantic_mcp/workflows/discovery_flow.py +116 -0
  65. sqlserver_semantic_mcp/workflows/facade.py +117 -0
  66. sqlserver_semantic_mcp/workflows/query_flow.py +120 -0
  67. sqlserver_semantic_mcp/workflows/recommendations.py +161 -0
  68. sqlserver_semantic_mcp/workflows/router.py +59 -0
  69. sqlserver_semantic_mcp-0.5.0.dist-info/METADATA +679 -0
  70. sqlserver_semantic_mcp-0.5.0.dist-info/RECORD +74 -0
  71. sqlserver_semantic_mcp-0.5.0.dist-info/WHEEL +5 -0
  72. sqlserver_semantic_mcp-0.5.0.dist-info/entry_points.txt +2 -0
  73. sqlserver_semantic_mcp-0.5.0.dist-info/licenses/LICENSE +21 -0
  74. sqlserver_semantic_mcp-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,78 @@
1
+ from typing import Literal, Optional
2
+ from pydantic import Field
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ DetailTier = Literal["brief", "standard", "full"]
7
+ ResponseMode = Literal["summary", "rows", "sample", "count_only"]
8
+ TokenBudgetHint = Literal["tiny", "low", "medium", "high"]
9
+ AffectedRowsPolicy = Literal["strict", "report"]
10
+ StartupMode = Literal["full", "cache_first"]
11
+
12
+
13
+ class Config(BaseSettings):
14
+ model_config = SettingsConfigDict(
15
+ env_prefix="SEMANTIC_MCP_",
16
+ env_file=".env",
17
+ env_file_encoding="utf-8",
18
+ extra="ignore",
19
+ )
20
+
21
+ # ---- DB connection ----
22
+ mssql_server: str
23
+ mssql_user: Optional[str] = None
24
+ mssql_password: Optional[str] = None
25
+ mssql_database: str
26
+ mssql_port: int = 1433
27
+ mssql_windows_auth: bool = False
28
+ mssql_encrypt: bool = False
29
+
30
+ # ---- Cache ----
31
+ cache_path: str = "./cache/semantic_mcp.db"
32
+ cache_enabled: bool = True
33
+ startup_mode: StartupMode = "cache_first"
34
+ background_batch_size: int = Field(default=5, ge=1)
35
+ background_interval_ms: int = Field(default=500, ge=0)
36
+
37
+ # ---- Policy ----
38
+ policy_file: Optional[str] = None
39
+ policy_profile: Optional[str] = None
40
+
41
+ # ---- Policy overrides ----
42
+ max_rows_returned: int = Field(default=1000, ge=1)
43
+ max_rows_affected: int = Field(default=100, ge=1)
44
+ query_timeout: int = Field(default=30, ge=1)
45
+
46
+ # ---- Tool surface ----
47
+ tool_profile: str = "all"
48
+
49
+ # ---- Metrics ----
50
+ metrics_enabled: bool = True
51
+
52
+ # ---- v0.5 agent-oriented defaults ----
53
+ default_detail: DetailTier = "brief"
54
+ default_response_mode: ResponseMode = "summary"
55
+ default_token_budget_hint: TokenBudgetHint = "low"
56
+ direct_execute_enabled: bool = True
57
+ strict_rows_affected_cap: bool = True
58
+ workflow_tools_enabled: bool = True
59
+
60
+ # ---- Analyzer router ----
61
+ # regex (default, regex-based) | ast (placeholder; falls back to regex)
62
+ intent_analyzer: Literal["regex", "ast"] = "regex"
63
+
64
+
65
+ _config: Optional[Config] = None
66
+
67
+
68
+ def get_config() -> Config:
69
+ global _config
70
+ if _config is None:
71
+ _config = Config()
72
+ return _config
73
+
74
+
75
+ def reset_config() -> None:
76
+ """Test helper only."""
77
+ global _config
78
+ _config = None
File without changes
@@ -0,0 +1,48 @@
1
+ from enum import Enum
2
+
3
+
4
+ class TableType(str, Enum):
5
+ FACT = "fact"
6
+ DIMENSION = "dimension"
7
+ LOOKUP = "lookup"
8
+ TRANSACTION = "transaction"
9
+ BRIDGE = "bridge"
10
+ CONFIG = "config"
11
+ AUDIT = "audit"
12
+ UNKNOWN = "unknown"
13
+
14
+
15
+ class ObjectType(str, Enum):
16
+ VIEW = "VIEW"
17
+ PROCEDURE = "PROCEDURE"
18
+ FUNCTION = "FUNCTION"
19
+
20
+
21
+ class CacheStatus(str, Enum):
22
+ PENDING = "pending"
23
+ COMPUTING = "computing"
24
+ READY = "ready"
25
+ DIRTY = "dirty"
26
+ ERROR = "error"
27
+
28
+
29
+ class RiskLevel(str, Enum):
30
+ LOW = "low"
31
+ MEDIUM = "medium"
32
+ HIGH = "high"
33
+ CRITICAL = "critical"
34
+
35
+
36
+ class SqlOperation(str, Enum):
37
+ SELECT = "SELECT"
38
+ INSERT = "INSERT"
39
+ UPDATE = "UPDATE"
40
+ DELETE = "DELETE"
41
+ TRUNCATE = "TRUNCATE"
42
+ CREATE = "CREATE"
43
+ ALTER = "ALTER"
44
+ DROP = "DROP"
45
+ EXECUTE = "EXECUTE"
46
+ EXEC = "EXEC"
47
+ MERGE = "MERGE"
48
+ UNKNOWN = "UNKNOWN"
File without changes
@@ -0,0 +1,14 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Column(BaseModel):
6
+ schema_name: str
7
+ table_name: str
8
+ column_name: str
9
+ data_type: str
10
+ max_length: Optional[int] = None
11
+ is_nullable: bool
12
+ column_default: Optional[str] = None
13
+ ordinal_position: int
14
+ description: Optional[str] = None
@@ -0,0 +1,13 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+ from ..enums import ObjectType
4
+
5
+
6
+ class DbObject(BaseModel):
7
+ schema_name: str
8
+ object_name: str
9
+ object_type: ObjectType
10
+ definition: Optional[str] = None
11
+ dependencies: list[str] = Field(default_factory=list)
12
+ affected_tables: list[str] = Field(default_factory=list)
13
+ description: Optional[str] = None
@@ -0,0 +1,11 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Relationship(BaseModel):
5
+ from_schema: str
6
+ from_table: str
7
+ to_schema: str
8
+ to_table: str
9
+ fk_column: str
10
+ ref_column: str
11
+ type: str # "many_to_one" | "one_to_one" | "self_ref"
@@ -0,0 +1,29 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+ from ..enums import TableType
4
+ from .column import Column
5
+
6
+
7
+ class ForeignKey(BaseModel):
8
+ column_name: str
9
+ ref_schema: str
10
+ ref_table: str
11
+ ref_column: str
12
+
13
+
14
+ class Index(BaseModel):
15
+ index_name: str
16
+ is_unique: bool
17
+ is_primary_key: bool
18
+ columns: list[str] = Field(default_factory=list)
19
+
20
+
21
+ class Table(BaseModel):
22
+ schema_name: str
23
+ table_name: str
24
+ columns: list[Column] = Field(default_factory=list)
25
+ primary_key: list[str] = Field(default_factory=list)
26
+ foreign_keys: list[ForeignKey] = Field(default_factory=list)
27
+ indexes: list[Index] = Field(default_factory=list)
28
+ description: Optional[str] = None
29
+ classification: Optional[TableType] = None
File without changes
@@ -0,0 +1,59 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Optional
4
+
5
+ from ..config import Config, get_config
6
+ from ..services import semantic_service
7
+ from .cache.semantic import list_pending_table_analyses
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ async def run_background_fill_once(cfg: Optional[Config] = None) -> int:
13
+ cfg = cfg or get_config()
14
+ pending = await list_pending_table_analyses(
15
+ cfg.cache_path, cfg.mssql_database, cfg.background_batch_size,
16
+ )
17
+ for (s, t) in pending:
18
+ try:
19
+ await semantic_service.classify_table(
20
+ cfg.cache_path, cfg.mssql_database, s, t, force=True,
21
+ )
22
+ except Exception:
23
+ logger.exception("Background classify failed for %s.%s", s, t)
24
+ return len(pending)
25
+
26
+
27
+ async def background_fill_loop(
28
+ cfg: Optional[Config] = None,
29
+ stop_event: Optional[asyncio.Event] = None,
30
+ ) -> None:
31
+ cfg = cfg or get_config()
32
+ stop_event = stop_event or asyncio.Event()
33
+ interval = cfg.background_interval_ms / 1000.0
34
+
35
+ consecutive_errors = 0
36
+ max_backoff = 60.0
37
+
38
+ while not stop_event.is_set():
39
+ try:
40
+ processed = await run_background_fill_once(cfg)
41
+ consecutive_errors = 0 # reset on any successful iteration
42
+ if processed == 0:
43
+ try:
44
+ await asyncio.wait_for(stop_event.wait(), timeout=10.0)
45
+ except asyncio.TimeoutError:
46
+ continue
47
+ continue
48
+ await asyncio.sleep(interval)
49
+ except Exception:
50
+ consecutive_errors += 1
51
+ backoff = min(max_backoff, 2.0 ** consecutive_errors)
52
+ logger.exception(
53
+ "Background fill iteration failed (attempt=%d, next retry in %.1fs)",
54
+ consecutive_errors, backoff,
55
+ )
56
+ try:
57
+ await asyncio.wait_for(stop_event.wait(), timeout=backoff)
58
+ except asyncio.TimeoutError:
59
+ continue
@@ -0,0 +1,132 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+ from typing import Optional
4
+ import aiosqlite
5
+
6
+
7
+ async def get_table_analysis(
8
+ db_path: str, database: str, schema: str, table: str,
9
+ ) -> Optional[dict]:
10
+ async with aiosqlite.connect(db_path) as db:
11
+ db.row_factory = aiosqlite.Row
12
+ cur = await db.execute(
13
+ "SELECT * FROM sem_table_analysis "
14
+ "WHERE database_name=? AND schema_name=? AND table_name=?",
15
+ (database, schema, table),
16
+ )
17
+ row = await cur.fetchone()
18
+ if not row:
19
+ return None
20
+ d = dict(row)
21
+ for k in ("classification", "column_analysis"):
22
+ if d.get(k):
23
+ d[k] = json.loads(d[k])
24
+ d["is_lookup"] = bool(d["is_lookup"]) if d["is_lookup"] is not None else None
25
+ return d
26
+
27
+
28
+ async def upsert_table_analysis(
29
+ db_path: str, database: str, schema: str, table: str,
30
+ *,
31
+ structural_hash: str,
32
+ status: str,
33
+ classification: Optional[dict] = None,
34
+ column_analysis: Optional[list] = None,
35
+ is_lookup: Optional[bool] = None,
36
+ error_message: Optional[str] = None,
37
+ ) -> None:
38
+ async with aiosqlite.connect(db_path) as db:
39
+ await db.execute(
40
+ "INSERT OR REPLACE INTO sem_table_analysis "
41
+ "(database_name, schema_name, table_name, structural_hash, status, "
42
+ " classification, column_analysis, is_lookup, computed_at, error_message) "
43
+ "VALUES (?,?,?,?,?,?,?,?,?,?)",
44
+ (
45
+ database, schema, table, structural_hash, status,
46
+ json.dumps(classification) if classification else None,
47
+ json.dumps(column_analysis) if column_analysis else None,
48
+ int(is_lookup) if is_lookup is not None else None,
49
+ datetime.now(timezone.utc).isoformat(),
50
+ error_message,
51
+ ),
52
+ )
53
+ await db.commit()
54
+
55
+
56
+ async def get_object_definition(
57
+ db_path: str, database: str, schema: str, obj_name: str, obj_type: str,
58
+ ) -> Optional[dict]:
59
+ async with aiosqlite.connect(db_path) as db:
60
+ db.row_factory = aiosqlite.Row
61
+ cur = await db.execute(
62
+ "SELECT * FROM sem_object_definitions "
63
+ "WHERE database_name=? AND schema_name=? "
64
+ "AND object_name=? AND object_type=?",
65
+ (database, schema, obj_name, obj_type),
66
+ )
67
+ row = await cur.fetchone()
68
+ if not row:
69
+ return None
70
+ d = dict(row)
71
+ for k in ("dependencies", "affected_tables"):
72
+ if d.get(k):
73
+ d[k] = json.loads(d[k])
74
+ return d
75
+
76
+
77
+ async def upsert_object_definition(
78
+ db_path: str, database: str, schema: str, obj_name: str, obj_type: str,
79
+ *,
80
+ object_hash: str,
81
+ status: str,
82
+ definition: Optional[str] = None,
83
+ dependencies: Optional[list] = None,
84
+ affected_tables: Optional[list] = None,
85
+ error_message: Optional[str] = None,
86
+ ) -> None:
87
+ async with aiosqlite.connect(db_path) as db:
88
+ await db.execute(
89
+ "INSERT OR REPLACE INTO sem_object_definitions "
90
+ "(database_name, schema_name, object_name, object_type, object_hash, "
91
+ " status, definition, dependencies, affected_tables, "
92
+ " computed_at, error_message) "
93
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
94
+ (
95
+ database, schema, obj_name, obj_type, object_hash, status,
96
+ definition,
97
+ json.dumps(dependencies) if dependencies else None,
98
+ json.dumps(affected_tables) if affected_tables else None,
99
+ datetime.now(timezone.utc).isoformat(),
100
+ error_message,
101
+ ),
102
+ )
103
+ await db.commit()
104
+
105
+
106
+ async def list_pending_table_analyses(
107
+ db_path: str, database: str, limit: int,
108
+ ) -> list[tuple[str, str]]:
109
+ async with aiosqlite.connect(db_path) as db:
110
+ db.row_factory = aiosqlite.Row
111
+ cur = await db.execute(
112
+ "SELECT schema_name, table_name FROM sem_table_analysis "
113
+ "WHERE database_name=? AND status IN ('pending','dirty') "
114
+ "ORDER BY schema_name, table_name LIMIT ?",
115
+ (database, limit),
116
+ )
117
+ return [(r["schema_name"], r["table_name"]) for r in await cur.fetchall()]
118
+
119
+
120
+ async def enqueue_all_tables(db_path: str, database: str, structural_hash: str) -> int:
121
+ async with aiosqlite.connect(db_path) as db:
122
+ await db.execute(
123
+ "INSERT OR IGNORE INTO sem_table_analysis "
124
+ "(database_name, schema_name, table_name, structural_hash, status) "
125
+ "SELECT ?, schema_name, table_name, ?, 'pending' "
126
+ "FROM sc_tables WHERE database_name=?",
127
+ (database, structural_hash, database),
128
+ )
129
+ await db.commit()
130
+ cur = await db.execute("SELECT changes()")
131
+ row = await cur.fetchone()
132
+ return int(row[0]) if row and row[0] is not None else 0
@@ -0,0 +1,152 @@
1
+ from pathlib import Path
2
+ import aiosqlite
3
+
4
+
5
+ SCHEMA_TABLES = [
6
+ "schema_version",
7
+ "sc_tables", "sc_columns", "sc_primary_keys", "sc_foreign_keys",
8
+ "sc_indexes", "sc_objects", "sc_comments",
9
+ "sem_table_analysis", "sem_object_definitions",
10
+ ]
11
+
12
+ SCHEMA_DDL = """
13
+ CREATE TABLE IF NOT EXISTS schema_version (
14
+ database_name TEXT PRIMARY KEY,
15
+ structural_hash TEXT NOT NULL,
16
+ object_hash TEXT NOT NULL,
17
+ comment_hash TEXT NOT NULL,
18
+ captured_at TEXT NOT NULL
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS sc_tables (
22
+ database_name TEXT NOT NULL,
23
+ schema_name TEXT NOT NULL,
24
+ table_name TEXT NOT NULL,
25
+ PRIMARY KEY (database_name, schema_name, table_name)
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS sc_columns (
29
+ database_name TEXT NOT NULL,
30
+ schema_name TEXT NOT NULL,
31
+ table_name TEXT NOT NULL,
32
+ column_name TEXT NOT NULL,
33
+ data_type TEXT NOT NULL,
34
+ max_length INTEGER,
35
+ is_nullable INTEGER NOT NULL,
36
+ column_default TEXT,
37
+ ordinal_position INTEGER NOT NULL,
38
+ PRIMARY KEY (database_name, schema_name, table_name, column_name)
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS sc_primary_keys (
42
+ database_name TEXT NOT NULL,
43
+ schema_name TEXT NOT NULL,
44
+ table_name TEXT NOT NULL,
45
+ column_name TEXT NOT NULL,
46
+ PRIMARY KEY (database_name, schema_name, table_name, column_name)
47
+ );
48
+
49
+ CREATE TABLE IF NOT EXISTS sc_foreign_keys (
50
+ database_name TEXT NOT NULL,
51
+ schema_name TEXT NOT NULL,
52
+ table_name TEXT NOT NULL,
53
+ column_name TEXT NOT NULL,
54
+ ref_schema TEXT NOT NULL,
55
+ ref_table TEXT NOT NULL,
56
+ ref_column TEXT NOT NULL
57
+ );
58
+ CREATE INDEX IF NOT EXISTS idx_fk_from
59
+ ON sc_foreign_keys (database_name, schema_name, table_name);
60
+ CREATE INDEX IF NOT EXISTS idx_fk_to
61
+ ON sc_foreign_keys (database_name, ref_schema, ref_table);
62
+
63
+ CREATE TABLE IF NOT EXISTS sc_indexes (
64
+ database_name TEXT NOT NULL,
65
+ schema_name TEXT NOT NULL,
66
+ table_name TEXT NOT NULL,
67
+ index_name TEXT NOT NULL,
68
+ is_unique INTEGER NOT NULL,
69
+ is_primary_key INTEGER NOT NULL,
70
+ columns TEXT NOT NULL,
71
+ PRIMARY KEY (database_name, schema_name, table_name, index_name)
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS sc_objects (
75
+ database_name TEXT NOT NULL,
76
+ schema_name TEXT NOT NULL,
77
+ object_name TEXT NOT NULL,
78
+ object_type TEXT NOT NULL,
79
+ PRIMARY KEY (database_name, schema_name, object_name, object_type)
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS sc_comments (
83
+ database_name TEXT NOT NULL,
84
+ schema_name TEXT NOT NULL,
85
+ object_name TEXT NOT NULL,
86
+ column_name TEXT NOT NULL DEFAULT '',
87
+ description TEXT NOT NULL,
88
+ PRIMARY KEY (database_name, schema_name, object_name, column_name)
89
+ );
90
+
91
+ CREATE TABLE IF NOT EXISTS sem_table_analysis (
92
+ database_name TEXT NOT NULL,
93
+ schema_name TEXT NOT NULL,
94
+ table_name TEXT NOT NULL,
95
+ structural_hash TEXT NOT NULL,
96
+ status TEXT NOT NULL DEFAULT 'pending',
97
+ classification TEXT,
98
+ column_analysis TEXT,
99
+ is_lookup INTEGER,
100
+ computed_at TEXT,
101
+ error_message TEXT,
102
+ PRIMARY KEY (database_name, schema_name, table_name)
103
+ );
104
+ CREATE INDEX IF NOT EXISTS idx_sem_table_status
105
+ ON sem_table_analysis (status);
106
+
107
+ CREATE TABLE IF NOT EXISTS sem_object_definitions (
108
+ database_name TEXT NOT NULL,
109
+ schema_name TEXT NOT NULL,
110
+ object_name TEXT NOT NULL,
111
+ object_type TEXT NOT NULL,
112
+ object_hash TEXT NOT NULL,
113
+ status TEXT NOT NULL DEFAULT 'pending',
114
+ definition TEXT,
115
+ dependencies TEXT,
116
+ affected_tables TEXT,
117
+ computed_at TEXT,
118
+ error_message TEXT,
119
+ PRIMARY KEY (database_name, schema_name, object_name, object_type)
120
+ );
121
+ CREATE INDEX IF NOT EXISTS idx_sem_obj_status
122
+ ON sem_object_definitions (status);
123
+
124
+ CREATE TABLE IF NOT EXISTS tool_metrics (
125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126
+ tool_name TEXT NOT NULL,
127
+ response_bytes INTEGER NOT NULL,
128
+ array_length INTEGER,
129
+ fields_returned INTEGER,
130
+ route_type TEXT,
131
+ detail TEXT,
132
+ response_mode TEXT,
133
+ token_budget_hint TEXT,
134
+ was_direct_execute INTEGER,
135
+ bundle_used INTEGER,
136
+ next_action TEXT,
137
+ recorded_at TEXT NOT NULL
138
+ );
139
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_name
140
+ ON tool_metrics (tool_name);
141
+ """
142
+
143
+
144
+ async def init_store(db_path: str) -> None:
145
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
146
+ async with aiosqlite.connect(db_path) as db:
147
+ await db.executescript(SCHEMA_DDL)
148
+ await db.commit()
149
+
150
+
151
+ def connection(db_path: str):
152
+ return aiosqlite.connect(db_path)