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.
- sqlserver_semantic_mcp/__init__.py +1 -0
- sqlserver_semantic_mcp/config.py +78 -0
- sqlserver_semantic_mcp/domain/__init__.py +0 -0
- sqlserver_semantic_mcp/domain/enums.py +48 -0
- sqlserver_semantic_mcp/domain/models/__init__.py +0 -0
- sqlserver_semantic_mcp/domain/models/column.py +14 -0
- sqlserver_semantic_mcp/domain/models/object.py +13 -0
- sqlserver_semantic_mcp/domain/models/relationship.py +11 -0
- sqlserver_semantic_mcp/domain/models/table.py +29 -0
- sqlserver_semantic_mcp/infrastructure/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/background.py +59 -0
- sqlserver_semantic_mcp/infrastructure/cache/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/cache/semantic.py +132 -0
- sqlserver_semantic_mcp/infrastructure/cache/store.py +152 -0
- sqlserver_semantic_mcp/infrastructure/cache/structural.py +203 -0
- sqlserver_semantic_mcp/infrastructure/connection.py +78 -0
- sqlserver_semantic_mcp/infrastructure/queries/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/queries/comment_queries.py +18 -0
- sqlserver_semantic_mcp/infrastructure/queries/metadata_queries.py +70 -0
- sqlserver_semantic_mcp/infrastructure/queries/object_queries.py +15 -0
- sqlserver_semantic_mcp/main.py +90 -0
- sqlserver_semantic_mcp/policy/__init__.py +0 -0
- sqlserver_semantic_mcp/policy/analyzer.py +194 -0
- sqlserver_semantic_mcp/policy/enforcer.py +104 -0
- sqlserver_semantic_mcp/policy/intents/__init__.py +16 -0
- sqlserver_semantic_mcp/policy/intents/ast_analyzer.py +24 -0
- sqlserver_semantic_mcp/policy/intents/base.py +17 -0
- sqlserver_semantic_mcp/policy/intents/regex_analyzer.py +11 -0
- sqlserver_semantic_mcp/policy/intents/router.py +21 -0
- sqlserver_semantic_mcp/policy/loader.py +90 -0
- sqlserver_semantic_mcp/policy/models.py +43 -0
- sqlserver_semantic_mcp/server/__init__.py +0 -0
- sqlserver_semantic_mcp/server/app.py +125 -0
- sqlserver_semantic_mcp/server/compact.py +74 -0
- sqlserver_semantic_mcp/server/prompts/__init__.py +5 -0
- sqlserver_semantic_mcp/server/prompts/analysis.py +56 -0
- sqlserver_semantic_mcp/server/prompts/discovery.py +55 -0
- sqlserver_semantic_mcp/server/prompts/execution.py +64 -0
- sqlserver_semantic_mcp/server/prompts/registry.py +41 -0
- sqlserver_semantic_mcp/server/resources/__init__.py +1 -0
- sqlserver_semantic_mcp/server/resources/schema.py +144 -0
- sqlserver_semantic_mcp/server/tools/__init__.py +42 -0
- sqlserver_semantic_mcp/server/tools/cache.py +24 -0
- sqlserver_semantic_mcp/server/tools/metadata.py +167 -0
- sqlserver_semantic_mcp/server/tools/metrics.py +44 -0
- sqlserver_semantic_mcp/server/tools/object_tool.py +113 -0
- sqlserver_semantic_mcp/server/tools/policy.py +48 -0
- sqlserver_semantic_mcp/server/tools/query.py +159 -0
- sqlserver_semantic_mcp/server/tools/relationship.py +104 -0
- sqlserver_semantic_mcp/server/tools/semantic.py +112 -0
- sqlserver_semantic_mcp/server/tools/shape.py +204 -0
- sqlserver_semantic_mcp/server/tools/workflow.py +307 -0
- sqlserver_semantic_mcp/services/__init__.py +0 -0
- sqlserver_semantic_mcp/services/metadata_service.py +173 -0
- sqlserver_semantic_mcp/services/metrics_service.py +124 -0
- sqlserver_semantic_mcp/services/object_service.py +187 -0
- sqlserver_semantic_mcp/services/policy_service.py +59 -0
- sqlserver_semantic_mcp/services/query_service.py +321 -0
- sqlserver_semantic_mcp/services/relationship_service.py +160 -0
- sqlserver_semantic_mcp/services/semantic_service.py +277 -0
- sqlserver_semantic_mcp/workflows/__init__.py +26 -0
- sqlserver_semantic_mcp/workflows/bundle.py +157 -0
- sqlserver_semantic_mcp/workflows/contracts.py +64 -0
- sqlserver_semantic_mcp/workflows/discovery_flow.py +116 -0
- sqlserver_semantic_mcp/workflows/facade.py +117 -0
- sqlserver_semantic_mcp/workflows/query_flow.py +120 -0
- sqlserver_semantic_mcp/workflows/recommendations.py +161 -0
- sqlserver_semantic_mcp/workflows/router.py +59 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/METADATA +679 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/RECORD +74 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/WHEEL +5 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/entry_points.txt +2 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/licenses/LICENSE +21 -0
- 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,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
|
|
File without changes
|
|
@@ -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)
|