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.
Files changed (192) hide show
  1. nl2sql/__init__.py +38 -0
  2. nl2sql/adapters/__init__.py +0 -0
  3. nl2sql/adapters/duckdb/__init__.py +0 -0
  4. nl2sql/adapters/duckdb/adapter.py +71 -0
  5. nl2sql/adapters/mssql/__init__.py +0 -0
  6. nl2sql/adapters/mssql/adapter.py +122 -0
  7. nl2sql/adapters/mysql/__init__.py +0 -0
  8. nl2sql/adapters/mysql/adapter.py +123 -0
  9. nl2sql/adapters/postgres/__init__.py +0 -0
  10. nl2sql/adapters/postgres/adapter.py +115 -0
  11. nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
  12. nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
  13. nl2sql/adapters/sqlalchemy_base/models.py +36 -0
  14. nl2sql/adapters/sqlite/__init__.py +0 -0
  15. nl2sql/adapters/sqlite/adapter.py +88 -0
  16. nl2sql/aggregation/__init__.py +3 -0
  17. nl2sql/aggregation/aggregator.py +98 -0
  18. nl2sql/aggregation/engines/__init__.py +3 -0
  19. nl2sql/aggregation/engines/polars_duckdb.py +125 -0
  20. nl2sql/api/__init__.py +0 -0
  21. nl2sql/api/auth_api.py +60 -0
  22. nl2sql/api/benchmark_api.py +114 -0
  23. nl2sql/api/datasource_api.py +132 -0
  24. nl2sql/api/indexing_api.py +59 -0
  25. nl2sql/api/llm_api.py +82 -0
  26. nl2sql/api/policy_api.py +135 -0
  27. nl2sql/api/query_api.py +138 -0
  28. nl2sql/api/result_api.py +24 -0
  29. nl2sql/api/settings_api.py +65 -0
  30. nl2sql/auth/__init__.py +8 -0
  31. nl2sql/auth/models.py +36 -0
  32. nl2sql/auth/rbac.py +25 -0
  33. nl2sql/cli/__init__.py +0 -0
  34. nl2sql/cli/checks.py +53 -0
  35. nl2sql/cli/commands/__init__.py +0 -0
  36. nl2sql/cli/commands/benchmark.py +34 -0
  37. nl2sql/cli/commands/doctor.py +49 -0
  38. nl2sql/cli/commands/indexing.py +126 -0
  39. nl2sql/cli/commands/info.py +25 -0
  40. nl2sql/cli/commands/install.py +27 -0
  41. nl2sql/cli/commands/policy.py +57 -0
  42. nl2sql/cli/commands/run.py +166 -0
  43. nl2sql/cli/commands/setup.py +415 -0
  44. nl2sql/cli/commands/visualize.py +34 -0
  45. nl2sql/cli/common/decorators.py +34 -0
  46. nl2sql/cli/config.py +24 -0
  47. nl2sql/cli/console.py +52 -0
  48. nl2sql/cli/demo/__init__.py +1 -0
  49. nl2sql/cli/demo/data.py +87 -0
  50. nl2sql/cli/demo/defaults.py +122 -0
  51. nl2sql/cli/demo/factory.py +289 -0
  52. nl2sql/cli/demo/manager.py +230 -0
  53. nl2sql/cli/demo/schemas.py +336 -0
  54. nl2sql/cli/demo/writers/__init__.py +0 -0
  55. nl2sql/cli/demo/writers/docker.py +182 -0
  56. nl2sql/cli/demo/writers/sqlite.py +88 -0
  57. nl2sql/cli/generators/datasources/__init__.py +3 -0
  58. nl2sql/cli/generators/datasources/generator.py +24 -0
  59. nl2sql/cli/generators/datasources/templates.py +7 -0
  60. nl2sql/cli/generators/env/__init__.py +3 -0
  61. nl2sql/cli/generators/env/generator.py +46 -0
  62. nl2sql/cli/generators/env/templates.py +25 -0
  63. nl2sql/cli/generators/llm/__init__.py +3 -0
  64. nl2sql/cli/generators/llm/generator.py +24 -0
  65. nl2sql/cli/generators/llm/templates.py +4 -0
  66. nl2sql/cli/generators/policies/__init__.py +3 -0
  67. nl2sql/cli/generators/policies/generator.py +20 -0
  68. nl2sql/cli/generators/policies/templates.py +2 -0
  69. nl2sql/cli/main.py +195 -0
  70. nl2sql/cli/reporting.py +878 -0
  71. nl2sql/cli/types.py +13 -0
  72. nl2sql/common/__init__.py +1 -0
  73. nl2sql/common/cancellation.py +25 -0
  74. nl2sql/common/context.py +5 -0
  75. nl2sql/common/errors.py +109 -0
  76. nl2sql/common/event_logger.py +88 -0
  77. nl2sql/common/exceptions.py +3 -0
  78. nl2sql/common/logger.py +119 -0
  79. nl2sql/common/metrics.py +50 -0
  80. nl2sql/common/resilience.py +59 -0
  81. nl2sql/common/settings.py +195 -0
  82. nl2sql/configs/__init__.py +6 -0
  83. nl2sql/configs/datasources.py +10 -0
  84. nl2sql/configs/llm.py +36 -0
  85. nl2sql/configs/manager.py +176 -0
  86. nl2sql/configs/policies.py +14 -0
  87. nl2sql/configs/sample_questions.py +11 -0
  88. nl2sql/configs/secrets.py +11 -0
  89. nl2sql/context.py +106 -0
  90. nl2sql/datasources/__init__.py +21 -0
  91. nl2sql/datasources/discovery.py +28 -0
  92. nl2sql/datasources/models.py +21 -0
  93. nl2sql/datasources/protocols.py +3 -0
  94. nl2sql/datasources/registry.py +172 -0
  95. nl2sql/evaluation/__init__.py +6 -0
  96. nl2sql/evaluation/benchmark_runner.py +320 -0
  97. nl2sql/evaluation/evaluator.py +134 -0
  98. nl2sql/evaluation/types.py +22 -0
  99. nl2sql/execution/__init__.py +4 -0
  100. nl2sql/execution/artifacts/__init__.py +3 -0
  101. nl2sql/execution/artifacts/parquet.py +41 -0
  102. nl2sql/execution/artifacts/store.py +165 -0
  103. nl2sql/execution/contracts.py +57 -0
  104. nl2sql/execution/execution_store.py +25 -0
  105. nl2sql/execution/executor/__init__.py +3 -0
  106. nl2sql/execution/executor/sql_executor.py +116 -0
  107. nl2sql/indexing/__init__.py +7 -0
  108. nl2sql/indexing/chunk_builder.py +227 -0
  109. nl2sql/indexing/embeddings.py +180 -0
  110. nl2sql/indexing/enrichment_service.py +316 -0
  111. nl2sql/indexing/models.py +209 -0
  112. nl2sql/indexing/orchestrator.py +90 -0
  113. nl2sql/indexing/vector_store.py +422 -0
  114. nl2sql/llm/__init__.py +8 -0
  115. nl2sql/llm/models.py +10 -0
  116. nl2sql/llm/registry.py +214 -0
  117. nl2sql/pipeline/__init__.py +1 -0
  118. nl2sql/pipeline/graph.py +73 -0
  119. nl2sql/pipeline/graph_utils.py +141 -0
  120. nl2sql/pipeline/nodes/__init__.py +25 -0
  121. nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
  122. nl2sql/pipeline/nodes/aggregator/node.py +55 -0
  123. nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
  124. nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
  125. nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
  126. nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
  127. nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
  128. nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
  129. nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
  130. nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
  131. nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
  132. nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
  133. nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
  134. nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
  135. nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
  136. nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
  137. nl2sql/pipeline/nodes/decomposer/node.py +219 -0
  138. nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
  139. nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
  140. nl2sql/pipeline/nodes/executor/__init__.py +3 -0
  141. nl2sql/pipeline/nodes/executor/node.py +107 -0
  142. nl2sql/pipeline/nodes/generator/__init__.py +4 -0
  143. nl2sql/pipeline/nodes/generator/node.py +267 -0
  144. nl2sql/pipeline/nodes/generator/schemas.py +13 -0
  145. nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
  146. nl2sql/pipeline/nodes/global_planner/node.py +186 -0
  147. nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
  148. nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
  149. nl2sql/pipeline/nodes/refiner/node.py +132 -0
  150. nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
  151. nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
  152. nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
  153. nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
  154. nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
  155. nl2sql/pipeline/nodes/validator/__init__.py +7 -0
  156. nl2sql/pipeline/nodes/validator/node.py +839 -0
  157. nl2sql/pipeline/nodes/validator/schemas.py +12 -0
  158. nl2sql/pipeline/pipeline_runner.py +72 -0
  159. nl2sql/pipeline/routes.py +72 -0
  160. nl2sql/pipeline/runtime.py +153 -0
  161. nl2sql/pipeline/state.py +92 -0
  162. nl2sql/pipeline/subgraphs/__init__.py +5 -0
  163. nl2sql/pipeline/subgraphs/schemas.py +23 -0
  164. nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
  165. nl2sql/public_api.py +199 -0
  166. nl2sql/schema/__init__.py +37 -0
  167. nl2sql/schema/in_memory_store.py +173 -0
  168. nl2sql/schema/protocol.py +88 -0
  169. nl2sql/schema/sqlite_store.py +233 -0
  170. nl2sql/schema/store.py +29 -0
  171. nl2sql/secrets/__init__.py +14 -0
  172. nl2sql/secrets/factory.py +85 -0
  173. nl2sql/secrets/interfaces.py +16 -0
  174. nl2sql/secrets/manager.py +139 -0
  175. nl2sql/secrets/models.py +56 -0
  176. nl2sql/secrets/providers/aws.py +30 -0
  177. nl2sql/secrets/providers/azure.py +49 -0
  178. nl2sql/secrets/providers/env.py +8 -0
  179. nl2sql/secrets/providers/hashi.py +46 -0
  180. nl2sql/services/__init__.py +0 -0
  181. nl2sql/services/callbacks/__init__.py +0 -0
  182. nl2sql/services/callbacks/monitor.py +84 -0
  183. nl2sql/services/callbacks/node_context.py +7 -0
  184. nl2sql/services/callbacks/node_handlers.py +187 -0
  185. nl2sql/services/callbacks/node_metrics.py +14 -0
  186. nl2sql/services/callbacks/presenter.py +12 -0
  187. nl2sql/services/callbacks/token_handler.py +56 -0
  188. nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
  189. nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
  190. nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
  191. nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
  192. 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
+ ...