nl2sql-engine 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.
- nl2sql/__init__.py +38 -0
- nl2sql/adapters/__init__.py +0 -0
- nl2sql/adapters/duckdb/__init__.py +0 -0
- nl2sql/adapters/duckdb/adapter.py +71 -0
- nl2sql/adapters/mssql/__init__.py +0 -0
- nl2sql/adapters/mssql/adapter.py +122 -0
- nl2sql/adapters/mysql/__init__.py +0 -0
- nl2sql/adapters/mysql/adapter.py +123 -0
- nl2sql/adapters/postgres/__init__.py +0 -0
- nl2sql/adapters/postgres/adapter.py +115 -0
- nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
- nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
- nl2sql/adapters/sqlalchemy_base/models.py +36 -0
- nl2sql/adapters/sqlite/__init__.py +0 -0
- nl2sql/adapters/sqlite/adapter.py +88 -0
- nl2sql/aggregation/__init__.py +3 -0
- nl2sql/aggregation/aggregator.py +98 -0
- nl2sql/aggregation/engines/__init__.py +3 -0
- nl2sql/aggregation/engines/polars_duckdb.py +125 -0
- nl2sql/api/__init__.py +0 -0
- nl2sql/api/auth_api.py +60 -0
- nl2sql/api/benchmark_api.py +114 -0
- nl2sql/api/datasource_api.py +132 -0
- nl2sql/api/indexing_api.py +59 -0
- nl2sql/api/llm_api.py +82 -0
- nl2sql/api/policy_api.py +135 -0
- nl2sql/api/query_api.py +138 -0
- nl2sql/api/result_api.py +24 -0
- nl2sql/api/settings_api.py +65 -0
- nl2sql/auth/__init__.py +8 -0
- nl2sql/auth/models.py +36 -0
- nl2sql/auth/rbac.py +25 -0
- nl2sql/cli/__init__.py +0 -0
- nl2sql/cli/checks.py +53 -0
- nl2sql/cli/commands/__init__.py +0 -0
- nl2sql/cli/commands/benchmark.py +34 -0
- nl2sql/cli/commands/doctor.py +49 -0
- nl2sql/cli/commands/indexing.py +126 -0
- nl2sql/cli/commands/info.py +25 -0
- nl2sql/cli/commands/install.py +27 -0
- nl2sql/cli/commands/policy.py +57 -0
- nl2sql/cli/commands/run.py +166 -0
- nl2sql/cli/commands/setup.py +415 -0
- nl2sql/cli/commands/visualize.py +34 -0
- nl2sql/cli/common/decorators.py +34 -0
- nl2sql/cli/config.py +24 -0
- nl2sql/cli/console.py +52 -0
- nl2sql/cli/demo/__init__.py +1 -0
- nl2sql/cli/demo/data.py +87 -0
- nl2sql/cli/demo/defaults.py +122 -0
- nl2sql/cli/demo/factory.py +289 -0
- nl2sql/cli/demo/manager.py +230 -0
- nl2sql/cli/demo/schemas.py +336 -0
- nl2sql/cli/demo/writers/__init__.py +0 -0
- nl2sql/cli/demo/writers/docker.py +182 -0
- nl2sql/cli/demo/writers/sqlite.py +88 -0
- nl2sql/cli/generators/datasources/__init__.py +3 -0
- nl2sql/cli/generators/datasources/generator.py +24 -0
- nl2sql/cli/generators/datasources/templates.py +7 -0
- nl2sql/cli/generators/env/__init__.py +3 -0
- nl2sql/cli/generators/env/generator.py +46 -0
- nl2sql/cli/generators/env/templates.py +25 -0
- nl2sql/cli/generators/llm/__init__.py +3 -0
- nl2sql/cli/generators/llm/generator.py +24 -0
- nl2sql/cli/generators/llm/templates.py +4 -0
- nl2sql/cli/generators/policies/__init__.py +3 -0
- nl2sql/cli/generators/policies/generator.py +20 -0
- nl2sql/cli/generators/policies/templates.py +2 -0
- nl2sql/cli/main.py +195 -0
- nl2sql/cli/reporting.py +878 -0
- nl2sql/cli/types.py +13 -0
- nl2sql/common/__init__.py +1 -0
- nl2sql/common/cancellation.py +25 -0
- nl2sql/common/context.py +5 -0
- nl2sql/common/errors.py +109 -0
- nl2sql/common/event_logger.py +88 -0
- nl2sql/common/exceptions.py +3 -0
- nl2sql/common/logger.py +119 -0
- nl2sql/common/metrics.py +50 -0
- nl2sql/common/resilience.py +59 -0
- nl2sql/common/settings.py +195 -0
- nl2sql/configs/__init__.py +6 -0
- nl2sql/configs/datasources.py +10 -0
- nl2sql/configs/llm.py +36 -0
- nl2sql/configs/manager.py +176 -0
- nl2sql/configs/policies.py +14 -0
- nl2sql/configs/sample_questions.py +11 -0
- nl2sql/configs/secrets.py +11 -0
- nl2sql/context.py +106 -0
- nl2sql/datasources/__init__.py +21 -0
- nl2sql/datasources/discovery.py +28 -0
- nl2sql/datasources/models.py +21 -0
- nl2sql/datasources/protocols.py +3 -0
- nl2sql/datasources/registry.py +172 -0
- nl2sql/evaluation/__init__.py +6 -0
- nl2sql/evaluation/benchmark_runner.py +320 -0
- nl2sql/evaluation/evaluator.py +134 -0
- nl2sql/evaluation/types.py +22 -0
- nl2sql/execution/__init__.py +4 -0
- nl2sql/execution/artifacts/__init__.py +3 -0
- nl2sql/execution/artifacts/parquet.py +41 -0
- nl2sql/execution/artifacts/store.py +165 -0
- nl2sql/execution/contracts.py +57 -0
- nl2sql/execution/execution_store.py +25 -0
- nl2sql/execution/executor/__init__.py +3 -0
- nl2sql/execution/executor/sql_executor.py +116 -0
- nl2sql/indexing/__init__.py +7 -0
- nl2sql/indexing/chunk_builder.py +227 -0
- nl2sql/indexing/embeddings.py +180 -0
- nl2sql/indexing/enrichment_service.py +316 -0
- nl2sql/indexing/models.py +209 -0
- nl2sql/indexing/orchestrator.py +90 -0
- nl2sql/indexing/vector_store.py +422 -0
- nl2sql/llm/__init__.py +8 -0
- nl2sql/llm/models.py +10 -0
- nl2sql/llm/registry.py +214 -0
- nl2sql/pipeline/__init__.py +1 -0
- nl2sql/pipeline/graph.py +73 -0
- nl2sql/pipeline/graph_utils.py +141 -0
- nl2sql/pipeline/nodes/__init__.py +25 -0
- nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
- nl2sql/pipeline/nodes/aggregator/node.py +55 -0
- nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
- nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
- nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
- nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
- nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
- nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
- nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
- nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
- nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
- nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
- nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
- nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
- nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
- nl2sql/pipeline/nodes/decomposer/node.py +219 -0
- nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
- nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
- nl2sql/pipeline/nodes/executor/__init__.py +3 -0
- nl2sql/pipeline/nodes/executor/node.py +107 -0
- nl2sql/pipeline/nodes/generator/__init__.py +4 -0
- nl2sql/pipeline/nodes/generator/node.py +267 -0
- nl2sql/pipeline/nodes/generator/schemas.py +13 -0
- nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/global_planner/node.py +186 -0
- nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
- nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
- nl2sql/pipeline/nodes/refiner/node.py +132 -0
- nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
- nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
- nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
- nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
- nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
- nl2sql/pipeline/nodes/validator/__init__.py +7 -0
- nl2sql/pipeline/nodes/validator/node.py +839 -0
- nl2sql/pipeline/nodes/validator/schemas.py +12 -0
- nl2sql/pipeline/pipeline_runner.py +72 -0
- nl2sql/pipeline/routes.py +72 -0
- nl2sql/pipeline/runtime.py +153 -0
- nl2sql/pipeline/state.py +92 -0
- nl2sql/pipeline/subgraphs/__init__.py +5 -0
- nl2sql/pipeline/subgraphs/schemas.py +23 -0
- nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
- nl2sql/public_api.py +199 -0
- nl2sql/schema/__init__.py +37 -0
- nl2sql/schema/in_memory_store.py +173 -0
- nl2sql/schema/protocol.py +88 -0
- nl2sql/schema/sqlite_store.py +233 -0
- nl2sql/schema/store.py +29 -0
- nl2sql/secrets/__init__.py +14 -0
- nl2sql/secrets/factory.py +85 -0
- nl2sql/secrets/interfaces.py +16 -0
- nl2sql/secrets/manager.py +139 -0
- nl2sql/secrets/models.py +56 -0
- nl2sql/secrets/providers/aws.py +30 -0
- nl2sql/secrets/providers/azure.py +49 -0
- nl2sql/secrets/providers/env.py +8 -0
- nl2sql/secrets/providers/hashi.py +46 -0
- nl2sql/services/__init__.py +0 -0
- nl2sql/services/callbacks/__init__.py +0 -0
- nl2sql/services/callbacks/monitor.py +84 -0
- nl2sql/services/callbacks/node_context.py +7 -0
- nl2sql/services/callbacks/node_handlers.py +187 -0
- nl2sql/services/callbacks/node_metrics.py +14 -0
- nl2sql/services/callbacks/presenter.py +12 -0
- nl2sql/services/callbacks/token_handler.py +56 -0
- nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
- nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
- nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
- nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
- nl2sql_engine-0.1.0.dist-info/top_level.txt +1 -0
nl2sql/public_api.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for NL2SQL Core Package
|
|
3
|
+
|
|
4
|
+
This module provides a clean, stable public interface to the NL2SQL core functionality.
|
|
5
|
+
It defines the official API boundaries and ensures backward compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pathlib
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from nl2sql.context import NL2SQLContext
|
|
15
|
+
from nl2sql.api.query_api import QueryAPI, QueryResult
|
|
16
|
+
from nl2sql.api.datasource_api import DatasourceAPI
|
|
17
|
+
from nl2sql.api.llm_api import LLM_API
|
|
18
|
+
from nl2sql.api.indexing_api import IndexingAPI
|
|
19
|
+
from nl2sql.api.auth_api import AuthAPI
|
|
20
|
+
from nl2sql.api.settings_api import SettingsAPI
|
|
21
|
+
from nl2sql.api.result_api import ResultAPI
|
|
22
|
+
from nl2sql.api.policy_api import PolicyAPI
|
|
23
|
+
from nl2sql.api.benchmark_api import BenchmarkAPI
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NL2SQL:
|
|
27
|
+
"""
|
|
28
|
+
Public API for NL2SQL Core Package
|
|
29
|
+
|
|
30
|
+
This class provides a clean, stable interface to the NL2SQL engine functionality.
|
|
31
|
+
It abstracts away the internal implementation details and provides a consistent
|
|
32
|
+
API for external consumers.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
ds_config_path: Optional[Union[str, pathlib.Path]] = None,
|
|
38
|
+
secrets_config_path: Optional[Union[str, pathlib.Path]] = None,
|
|
39
|
+
llm_config_path: Optional[Union[str, pathlib.Path]] = None,
|
|
40
|
+
vector_store_path: Optional[Union[str, pathlib.Path]] = None,
|
|
41
|
+
policies_config_path: Optional[Union[str, pathlib.Path]] = None,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize the NL2SQL engine with optional configuration paths.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ds_config_path: Path to datasource configuration file
|
|
48
|
+
secrets_config_path: Path to secrets configuration file
|
|
49
|
+
llm_config_path: Path to LLM configuration file
|
|
50
|
+
vector_store_path: Path to vector store directory
|
|
51
|
+
policies_config_path: Path to policies configuration file
|
|
52
|
+
"""
|
|
53
|
+
ds_config_path = pathlib.Path(ds_config_path) if ds_config_path else None
|
|
54
|
+
secrets_config_path = pathlib.Path(secrets_config_path) if secrets_config_path else None
|
|
55
|
+
llm_config_path = pathlib.Path(llm_config_path) if llm_config_path else None
|
|
56
|
+
vector_store_path = pathlib.Path(vector_store_path) if vector_store_path else None
|
|
57
|
+
policies_config_path = pathlib.Path(policies_config_path) if policies_config_path else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
self._ctx = NL2SQLContext(
|
|
61
|
+
ds_config_path=ds_config_path,
|
|
62
|
+
secrets_config_path=secrets_config_path,
|
|
63
|
+
llm_config_path=llm_config_path,
|
|
64
|
+
vector_store_path=vector_store_path,
|
|
65
|
+
policies_config_path=policies_config_path,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Initialize modular APIs
|
|
69
|
+
self.query = QueryAPI(self._ctx)
|
|
70
|
+
self.datasource = DatasourceAPI(self._ctx)
|
|
71
|
+
self.llm = LLM_API(self._ctx)
|
|
72
|
+
self.indexing = IndexingAPI(self._ctx)
|
|
73
|
+
self.auth = AuthAPI(self._ctx)
|
|
74
|
+
self.settings = SettingsAPI(self._ctx)
|
|
75
|
+
self.results = ResultAPI(self._ctx)
|
|
76
|
+
self.policy = PolicyAPI(self._ctx)
|
|
77
|
+
self.benchmark = BenchmarkAPI(self._ctx)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def context(self) -> NL2SQLContext:
|
|
81
|
+
"""Access to the underlying context (internal use only)."""
|
|
82
|
+
return self._ctx
|
|
83
|
+
|
|
84
|
+
# Convenience methods that delegate to the modular APIs
|
|
85
|
+
def run_query(
|
|
86
|
+
self,
|
|
87
|
+
natural_language: str,
|
|
88
|
+
datasource_id: Optional[str] = None,
|
|
89
|
+
execute: bool = True,
|
|
90
|
+
user_context=None,
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Execute a natural language query against the database.
|
|
94
|
+
"""
|
|
95
|
+
return self.query.run_query(
|
|
96
|
+
natural_language=natural_language,
|
|
97
|
+
datasource_id=datasource_id,
|
|
98
|
+
execute=execute,
|
|
99
|
+
user_context=user_context
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def add_datasource(self, config):
|
|
103
|
+
"""
|
|
104
|
+
Programmatically add a datasource to the engine.
|
|
105
|
+
"""
|
|
106
|
+
return self.datasource.add_datasource(config)
|
|
107
|
+
|
|
108
|
+
def add_datasource_from_config(self, config_path: Union[str, pathlib.Path]):
|
|
109
|
+
"""
|
|
110
|
+
Add datasources from a configuration file.
|
|
111
|
+
"""
|
|
112
|
+
return self.datasource.add_datasource_from_config(config_path)
|
|
113
|
+
|
|
114
|
+
def list_datasources(self) -> list:
|
|
115
|
+
"""
|
|
116
|
+
List all registered datasource IDs.
|
|
117
|
+
"""
|
|
118
|
+
return self.datasource.list_datasources()
|
|
119
|
+
|
|
120
|
+
def get_datasource_capabilities(self, datasource_id: str) -> dict:
|
|
121
|
+
"""
|
|
122
|
+
Get capabilities of a specific datasource.
|
|
123
|
+
"""
|
|
124
|
+
return self.datasource.get_capabilities(datasource_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def configure_llm(self, config):
|
|
128
|
+
"""
|
|
129
|
+
Programmatically configure an LLM.
|
|
130
|
+
"""
|
|
131
|
+
return self.llm.configure_llm(config)
|
|
132
|
+
|
|
133
|
+
def configure_llm_from_config(self, config_path: Union[str, pathlib.Path]):
|
|
134
|
+
"""
|
|
135
|
+
Configure LLMs from a configuration file.
|
|
136
|
+
"""
|
|
137
|
+
return self.llm.configure_llm_from_config(config_path)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def list_llms(self) -> dict:
|
|
141
|
+
"""
|
|
142
|
+
List all configured LLMs.
|
|
143
|
+
"""
|
|
144
|
+
return self.llm.list_llms()
|
|
145
|
+
|
|
146
|
+
def get_llm(self, llm_name: str) -> dict:
|
|
147
|
+
"""
|
|
148
|
+
Get details of a specific LLM.
|
|
149
|
+
"""
|
|
150
|
+
return self.llm.get_llm(llm_name)
|
|
151
|
+
|
|
152
|
+
def index_datasource(self, datasource_id: str):
|
|
153
|
+
"""
|
|
154
|
+
Index schema for a specific datasource.
|
|
155
|
+
"""
|
|
156
|
+
return self.indexing.index_datasource(datasource_id)
|
|
157
|
+
|
|
158
|
+
def index_all_datasources(self):
|
|
159
|
+
"""
|
|
160
|
+
Index schema for all registered datasources.
|
|
161
|
+
"""
|
|
162
|
+
return self.indexing.index_all_datasources()
|
|
163
|
+
|
|
164
|
+
def clear_index(self):
|
|
165
|
+
"""
|
|
166
|
+
Clear the vector store index.
|
|
167
|
+
"""
|
|
168
|
+
return self.indexing.clear_index()
|
|
169
|
+
|
|
170
|
+
# Auth API convenience methods
|
|
171
|
+
def check_permissions(self, user_context, datasource_id, table):
|
|
172
|
+
"""
|
|
173
|
+
Check if a user has permission to access a specific resource.
|
|
174
|
+
"""
|
|
175
|
+
return self.auth.check_permissions(user_context, datasource_id, table)
|
|
176
|
+
|
|
177
|
+
def get_allowed_resources(self, user_context):
|
|
178
|
+
"""
|
|
179
|
+
Get resources a user has access to.
|
|
180
|
+
"""
|
|
181
|
+
return self.auth.get_allowed_resources(user_context)
|
|
182
|
+
|
|
183
|
+
def get_current_settings(self):
|
|
184
|
+
"""
|
|
185
|
+
Get the current application settings.
|
|
186
|
+
"""
|
|
187
|
+
return self.settings.get_current_settings()
|
|
188
|
+
|
|
189
|
+
def get_setting(self, key):
|
|
190
|
+
"""
|
|
191
|
+
Get a specific setting value.
|
|
192
|
+
"""
|
|
193
|
+
return self.settings.get_setting(key)
|
|
194
|
+
|
|
195
|
+
def validate_configuration(self):
|
|
196
|
+
"""
|
|
197
|
+
Validate the current configuration.
|
|
198
|
+
"""
|
|
199
|
+
return self.settings.validate_configuration()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Core schema models and stores."""
|
|
2
|
+
|
|
3
|
+
from nl2sql_adapter_sdk.schema import (
|
|
4
|
+
TableRef,
|
|
5
|
+
ColumnStatistics,
|
|
6
|
+
ColumnMetadata,
|
|
7
|
+
ColumnContract,
|
|
8
|
+
ForeignKeyContract,
|
|
9
|
+
TableContract,
|
|
10
|
+
TableMetadata,
|
|
11
|
+
SchemaContract,
|
|
12
|
+
SchemaMetadata,
|
|
13
|
+
SchemaSnapshot,
|
|
14
|
+
)
|
|
15
|
+
from .protocol import SchemaStore
|
|
16
|
+
from .in_memory_store import SchemaContractStore, SchemaMetadataStore, InMemorySchemaStore
|
|
17
|
+
from .sqlite_store import SqliteSchemaStore
|
|
18
|
+
from .store import build_schema_store
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"TableRef",
|
|
22
|
+
"ColumnStatistics",
|
|
23
|
+
"ColumnMetadata",
|
|
24
|
+
"ColumnContract",
|
|
25
|
+
"ForeignKeyContract",
|
|
26
|
+
"TableContract",
|
|
27
|
+
"TableMetadata",
|
|
28
|
+
"SchemaContract",
|
|
29
|
+
"SchemaMetadata",
|
|
30
|
+
"SchemaSnapshot",
|
|
31
|
+
"SchemaContractStore",
|
|
32
|
+
"SchemaMetadataStore",
|
|
33
|
+
"SchemaStore",
|
|
34
|
+
"InMemorySchemaStore",
|
|
35
|
+
"SqliteSchemaStore",
|
|
36
|
+
"build_schema_store",
|
|
37
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict, defaultdict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from nl2sql_adapter_sdk.schema import (
|
|
9
|
+
SchemaContract,
|
|
10
|
+
SchemaMetadata,
|
|
11
|
+
SchemaSnapshot,
|
|
12
|
+
TableContract,
|
|
13
|
+
TableMetadata,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .protocol import generate_schema_fingerprint
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SchemaContractStore:
|
|
22
|
+
"""Registry for schema contracts by datasource/version."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, max_versions: int = 3):
|
|
25
|
+
self._registry: Dict[str, OrderedDict[str, SchemaContract]] = defaultdict(
|
|
26
|
+
OrderedDict
|
|
27
|
+
)
|
|
28
|
+
self._fingerprint_index: Dict[str, Dict[str, str]] = defaultdict(dict)
|
|
29
|
+
self._max_versions = max_versions
|
|
30
|
+
|
|
31
|
+
def register(self, schema: SchemaContract) -> tuple[str, List[str]]:
|
|
32
|
+
fingerprint = generate_schema_fingerprint(schema)
|
|
33
|
+
version = self._check_schema_exists(schema.datasource_id, fingerprint)
|
|
34
|
+
if version:
|
|
35
|
+
logger.info(
|
|
36
|
+
"Schema for %s already exists with version %s",
|
|
37
|
+
schema.datasource_id,
|
|
38
|
+
version,
|
|
39
|
+
)
|
|
40
|
+
return version, []
|
|
41
|
+
|
|
42
|
+
ts = datetime.utcnow().strftime("%Y%m%d%H%M%S")
|
|
43
|
+
schema_version = f"{ts}_{fingerprint[:8]}"
|
|
44
|
+
logger.info(
|
|
45
|
+
"Adding new schema for %s with version %s",
|
|
46
|
+
schema.datasource_id,
|
|
47
|
+
schema_version,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self._registry[schema.datasource_id][schema_version] = schema
|
|
51
|
+
self._fingerprint_index[schema.datasource_id][fingerprint] = schema_version
|
|
52
|
+
|
|
53
|
+
evicted_versions = self._evict_old_versions(schema.datasource_id)
|
|
54
|
+
return schema_version, evicted_versions
|
|
55
|
+
|
|
56
|
+
def get_all_versions(self, datasource_id: str) -> List[str]:
|
|
57
|
+
if datasource_id not in self._registry:
|
|
58
|
+
return []
|
|
59
|
+
return list(self._registry[datasource_id].keys())
|
|
60
|
+
|
|
61
|
+
def _check_schema_exists(self, datasource_id: str, fingerprint: str) -> Optional[str]:
|
|
62
|
+
if datasource_id not in self._fingerprint_index:
|
|
63
|
+
return None
|
|
64
|
+
return self._fingerprint_index[datasource_id].get(fingerprint)
|
|
65
|
+
|
|
66
|
+
def get(self, datasource_id: str, schema_version: str) -> Optional[SchemaContract]:
|
|
67
|
+
return self._registry.get(datasource_id, {}).get(schema_version)
|
|
68
|
+
|
|
69
|
+
def get_latest(self, datasource_id: str) -> Optional[SchemaContract]:
|
|
70
|
+
versions = self._registry.get(datasource_id)
|
|
71
|
+
if not versions:
|
|
72
|
+
return None
|
|
73
|
+
return next(reversed(versions.values()))
|
|
74
|
+
|
|
75
|
+
def get_latest_version(self, datasource_id: str) -> Optional[str]:
|
|
76
|
+
versions = self._registry.get(datasource_id)
|
|
77
|
+
if not versions:
|
|
78
|
+
return None
|
|
79
|
+
return next(reversed(versions.keys()))
|
|
80
|
+
|
|
81
|
+
def _evict_old_versions(self, datasource_id: str) -> List[str]:
|
|
82
|
+
versions = self._registry[datasource_id]
|
|
83
|
+
fp_index = self._fingerprint_index[datasource_id]
|
|
84
|
+
evicted_versions: List[str] = []
|
|
85
|
+
|
|
86
|
+
while len(versions) > self._max_versions:
|
|
87
|
+
evicted_version, evicted_schema = versions.popitem(last=False)
|
|
88
|
+
evicted_versions.append(evicted_version)
|
|
89
|
+
|
|
90
|
+
evicted_fp = generate_schema_fingerprint(evicted_schema)
|
|
91
|
+
fp_index.pop(evicted_fp, None)
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
"Evicted old schema version for %s: %s",
|
|
95
|
+
datasource_id,
|
|
96
|
+
evicted_version,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return evicted_versions
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SchemaMetadataStore:
|
|
103
|
+
def __init__(self):
|
|
104
|
+
self._store: Dict[str, Dict[str, SchemaMetadata]] = defaultdict(dict)
|
|
105
|
+
|
|
106
|
+
def get(self, datasource_id: str, schema_version: str) -> Optional[SchemaMetadata]:
|
|
107
|
+
return self._store.get(datasource_id, {}).get(schema_version)
|
|
108
|
+
|
|
109
|
+
def register(self, schema_version: str, schema: SchemaMetadata):
|
|
110
|
+
self._store[schema.datasource_id][schema_version] = schema
|
|
111
|
+
|
|
112
|
+
def delete(self, datasource_id: str, schema_version: str):
|
|
113
|
+
self._store.get(datasource_id, {}).pop(schema_version, None)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class InMemorySchemaStore:
|
|
117
|
+
"""In-memory schema store with versioning and per-table access."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, max_versions: int = 3):
|
|
120
|
+
self._contracts = SchemaContractStore(max_versions=max_versions)
|
|
121
|
+
self._metadata = SchemaMetadataStore()
|
|
122
|
+
|
|
123
|
+
def register_snapshot(self, snapshot: SchemaSnapshot) -> Tuple[str, List[str]]:
|
|
124
|
+
schema_version, evicted_versions = self._contracts.register(snapshot.contract)
|
|
125
|
+
self._metadata.register(schema_version, snapshot.metadata)
|
|
126
|
+
|
|
127
|
+
for evicted_version in evicted_versions:
|
|
128
|
+
self._metadata.delete(snapshot.contract.datasource_id, evicted_version)
|
|
129
|
+
|
|
130
|
+
return schema_version, evicted_versions
|
|
131
|
+
|
|
132
|
+
def get_snapshot(
|
|
133
|
+
self, datasource_id: str, schema_version: str
|
|
134
|
+
) -> Optional[SchemaSnapshot]:
|
|
135
|
+
contract = self._contracts.get(datasource_id, schema_version)
|
|
136
|
+
metadata = self._metadata.get(datasource_id, schema_version)
|
|
137
|
+
if not contract or not metadata:
|
|
138
|
+
return None
|
|
139
|
+
return SchemaSnapshot(contract=contract, metadata=metadata)
|
|
140
|
+
|
|
141
|
+
def get_latest_snapshot(self, datasource_id: str) -> Optional[SchemaSnapshot]:
|
|
142
|
+
latest_version = self._contracts.get_latest_version(datasource_id)
|
|
143
|
+
if not latest_version:
|
|
144
|
+
return None
|
|
145
|
+
return self.get_snapshot(datasource_id, latest_version)
|
|
146
|
+
|
|
147
|
+
def get_latest_version(self, datasource_id: str) -> Optional[str]:
|
|
148
|
+
return self._contracts.get_latest_version(datasource_id)
|
|
149
|
+
|
|
150
|
+
def list_versions(self, datasource_id: str) -> List[str]:
|
|
151
|
+
return self._contracts.get_all_versions(datasource_id)
|
|
152
|
+
|
|
153
|
+
def get_table_contract(
|
|
154
|
+
self,
|
|
155
|
+
datasource_id: str,
|
|
156
|
+
schema_version: str,
|
|
157
|
+
table_key: str,
|
|
158
|
+
) -> Optional[TableContract]:
|
|
159
|
+
contract = self._contracts.get(datasource_id, schema_version)
|
|
160
|
+
if not contract:
|
|
161
|
+
return None
|
|
162
|
+
return contract.tables.get(table_key)
|
|
163
|
+
|
|
164
|
+
def get_table_metadata(
|
|
165
|
+
self,
|
|
166
|
+
datasource_id: str,
|
|
167
|
+
schema_version: str,
|
|
168
|
+
table_key: str,
|
|
169
|
+
) -> Optional[TableMetadata]:
|
|
170
|
+
metadata = self._metadata.get(datasource_id, schema_version)
|
|
171
|
+
if not metadata:
|
|
172
|
+
return None
|
|
173
|
+
return metadata.tables.get(table_key)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import List, Optional, Protocol, Tuple
|
|
6
|
+
|
|
7
|
+
from nl2sql_adapter_sdk.schema import (
|
|
8
|
+
SchemaContract,
|
|
9
|
+
SchemaMetadata,
|
|
10
|
+
SchemaSnapshot,
|
|
11
|
+
TableContract,
|
|
12
|
+
TableMetadata,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_schema_fingerprint(schema: SchemaContract) -> str:
|
|
17
|
+
payload = {
|
|
18
|
+
"datasource_id": schema.datasource_id,
|
|
19
|
+
"engine_type": schema.engine_type,
|
|
20
|
+
"tables": {
|
|
21
|
+
table_key: {
|
|
22
|
+
"columns": [
|
|
23
|
+
{
|
|
24
|
+
"name": c.name,
|
|
25
|
+
"type": c.data_type,
|
|
26
|
+
"nullable": c.is_nullable,
|
|
27
|
+
"pk": c.is_primary_key,
|
|
28
|
+
}
|
|
29
|
+
for c in sorted(table.columns.values(), key=lambda c: c.name)
|
|
30
|
+
],
|
|
31
|
+
"fks": [
|
|
32
|
+
{
|
|
33
|
+
"cols": sorted(fk.constrained_columns),
|
|
34
|
+
"ref_table": fk.referred_table,
|
|
35
|
+
"ref_cols": sorted(fk.referred_columns),
|
|
36
|
+
}
|
|
37
|
+
for fk in sorted(
|
|
38
|
+
table.foreign_keys,
|
|
39
|
+
key=lambda fk: (
|
|
40
|
+
fk.referred_table,
|
|
41
|
+
sorted(fk.constrained_columns),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
for table_key, table in sorted(schema.tables.items())
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
raw = json.dumps(payload, sort_keys=True, default=str)
|
|
51
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SchemaStore(Protocol):
|
|
55
|
+
"""Unified interface for schema snapshot storage backends."""
|
|
56
|
+
|
|
57
|
+
def register_snapshot(self, snapshot: SchemaSnapshot) -> Tuple[str, List[str]]:
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def get_snapshot(
|
|
61
|
+
self, datasource_id: str, schema_version: str
|
|
62
|
+
) -> Optional[SchemaSnapshot]:
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def get_latest_snapshot(self, datasource_id: str) -> Optional[SchemaSnapshot]:
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def get_latest_version(self, datasource_id: str) -> Optional[str]:
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def list_versions(self, datasource_id: str) -> List[str]:
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def get_table_contract(
|
|
75
|
+
self,
|
|
76
|
+
datasource_id: str,
|
|
77
|
+
schema_version: str,
|
|
78
|
+
table_key: str,
|
|
79
|
+
) -> Optional[TableContract]:
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def get_table_metadata(
|
|
83
|
+
self,
|
|
84
|
+
datasource_id: str,
|
|
85
|
+
schema_version: str,
|
|
86
|
+
table_key: str,
|
|
87
|
+
) -> Optional[TableMetadata]:
|
|
88
|
+
...
|