databao-context-engine 0.1.1__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 (135) hide show
  1. databao_context_engine/__init__.py +35 -0
  2. databao_context_engine/build_sources/__init__.py +0 -0
  3. databao_context_engine/build_sources/internal/__init__.py +0 -0
  4. databao_context_engine/build_sources/internal/build_runner.py +111 -0
  5. databao_context_engine/build_sources/internal/build_service.py +77 -0
  6. databao_context_engine/build_sources/internal/build_wiring.py +52 -0
  7. databao_context_engine/build_sources/internal/export_results.py +43 -0
  8. databao_context_engine/build_sources/internal/plugin_execution.py +74 -0
  9. databao_context_engine/build_sources/public/__init__.py +0 -0
  10. databao_context_engine/build_sources/public/api.py +4 -0
  11. databao_context_engine/cli/__init__.py +0 -0
  12. databao_context_engine/cli/add_datasource_config.py +130 -0
  13. databao_context_engine/cli/commands.py +256 -0
  14. databao_context_engine/cli/datasources.py +64 -0
  15. databao_context_engine/cli/info.py +32 -0
  16. databao_context_engine/config/__init__.py +0 -0
  17. databao_context_engine/config/log_config.yaml +16 -0
  18. databao_context_engine/config/logging.py +43 -0
  19. databao_context_engine/databao_context_project_manager.py +92 -0
  20. databao_context_engine/databao_engine.py +85 -0
  21. databao_context_engine/datasource_config/__init__.py +0 -0
  22. databao_context_engine/datasource_config/add_config.py +50 -0
  23. databao_context_engine/datasource_config/check_config.py +131 -0
  24. databao_context_engine/datasource_config/datasource_context.py +60 -0
  25. databao_context_engine/event_journal/__init__.py +0 -0
  26. databao_context_engine/event_journal/writer.py +29 -0
  27. databao_context_engine/generate_configs_schemas.py +92 -0
  28. databao_context_engine/init_project.py +18 -0
  29. databao_context_engine/introspection/__init__.py +0 -0
  30. databao_context_engine/introspection/property_extract.py +202 -0
  31. databao_context_engine/llm/__init__.py +0 -0
  32. databao_context_engine/llm/config.py +20 -0
  33. databao_context_engine/llm/descriptions/__init__.py +0 -0
  34. databao_context_engine/llm/descriptions/ollama.py +21 -0
  35. databao_context_engine/llm/descriptions/provider.py +10 -0
  36. databao_context_engine/llm/embeddings/__init__.py +0 -0
  37. databao_context_engine/llm/embeddings/ollama.py +37 -0
  38. databao_context_engine/llm/embeddings/provider.py +13 -0
  39. databao_context_engine/llm/errors.py +16 -0
  40. databao_context_engine/llm/factory.py +61 -0
  41. databao_context_engine/llm/install.py +227 -0
  42. databao_context_engine/llm/runtime.py +73 -0
  43. databao_context_engine/llm/service.py +159 -0
  44. databao_context_engine/main.py +19 -0
  45. databao_context_engine/mcp/__init__.py +0 -0
  46. databao_context_engine/mcp/all_results_tool.py +5 -0
  47. databao_context_engine/mcp/mcp_runner.py +16 -0
  48. databao_context_engine/mcp/mcp_server.py +63 -0
  49. databao_context_engine/mcp/retrieve_tool.py +22 -0
  50. databao_context_engine/pluginlib/__init__.py +0 -0
  51. databao_context_engine/pluginlib/build_plugin.py +107 -0
  52. databao_context_engine/pluginlib/config.py +37 -0
  53. databao_context_engine/pluginlib/plugin_utils.py +68 -0
  54. databao_context_engine/plugins/__init__.py +0 -0
  55. databao_context_engine/plugins/athena_db_plugin.py +12 -0
  56. databao_context_engine/plugins/base_db_plugin.py +45 -0
  57. databao_context_engine/plugins/clickhouse_db_plugin.py +15 -0
  58. databao_context_engine/plugins/databases/__init__.py +0 -0
  59. databao_context_engine/plugins/databases/athena_introspector.py +101 -0
  60. databao_context_engine/plugins/databases/base_introspector.py +144 -0
  61. databao_context_engine/plugins/databases/clickhouse_introspector.py +162 -0
  62. databao_context_engine/plugins/databases/database_chunker.py +69 -0
  63. databao_context_engine/plugins/databases/databases_types.py +114 -0
  64. databao_context_engine/plugins/databases/duckdb_introspector.py +325 -0
  65. databao_context_engine/plugins/databases/introspection_model_builder.py +270 -0
  66. databao_context_engine/plugins/databases/introspection_scope.py +74 -0
  67. databao_context_engine/plugins/databases/introspection_scope_matcher.py +103 -0
  68. databao_context_engine/plugins/databases/mssql_introspector.py +433 -0
  69. databao_context_engine/plugins/databases/mysql_introspector.py +338 -0
  70. databao_context_engine/plugins/databases/postgresql_introspector.py +428 -0
  71. databao_context_engine/plugins/databases/snowflake_introspector.py +287 -0
  72. databao_context_engine/plugins/duckdb_db_plugin.py +12 -0
  73. databao_context_engine/plugins/mssql_db_plugin.py +12 -0
  74. databao_context_engine/plugins/mysql_db_plugin.py +12 -0
  75. databao_context_engine/plugins/parquet_plugin.py +32 -0
  76. databao_context_engine/plugins/plugin_loader.py +110 -0
  77. databao_context_engine/plugins/postgresql_db_plugin.py +12 -0
  78. databao_context_engine/plugins/resources/__init__.py +0 -0
  79. databao_context_engine/plugins/resources/parquet_chunker.py +23 -0
  80. databao_context_engine/plugins/resources/parquet_introspector.py +154 -0
  81. databao_context_engine/plugins/snowflake_db_plugin.py +12 -0
  82. databao_context_engine/plugins/unstructured_files_plugin.py +68 -0
  83. databao_context_engine/project/__init__.py +0 -0
  84. databao_context_engine/project/datasource_discovery.py +141 -0
  85. databao_context_engine/project/info.py +44 -0
  86. databao_context_engine/project/init_project.py +102 -0
  87. databao_context_engine/project/layout.py +127 -0
  88. databao_context_engine/project/project_config.py +32 -0
  89. databao_context_engine/project/resources/examples/src/databases/example_postgres.yaml +7 -0
  90. databao_context_engine/project/resources/examples/src/files/documentation.md +30 -0
  91. databao_context_engine/project/resources/examples/src/files/notes.txt +20 -0
  92. databao_context_engine/project/runs.py +39 -0
  93. databao_context_engine/project/types.py +134 -0
  94. databao_context_engine/retrieve_embeddings/__init__.py +0 -0
  95. databao_context_engine/retrieve_embeddings/internal/__init__.py +0 -0
  96. databao_context_engine/retrieve_embeddings/internal/export_results.py +12 -0
  97. databao_context_engine/retrieve_embeddings/internal/retrieve_runner.py +34 -0
  98. databao_context_engine/retrieve_embeddings/internal/retrieve_service.py +68 -0
  99. databao_context_engine/retrieve_embeddings/internal/retrieve_wiring.py +29 -0
  100. databao_context_engine/retrieve_embeddings/public/__init__.py +0 -0
  101. databao_context_engine/retrieve_embeddings/public/api.py +3 -0
  102. databao_context_engine/serialisation/__init__.py +0 -0
  103. databao_context_engine/serialisation/yaml.py +35 -0
  104. databao_context_engine/services/__init__.py +0 -0
  105. databao_context_engine/services/chunk_embedding_service.py +104 -0
  106. databao_context_engine/services/embedding_shard_resolver.py +64 -0
  107. databao_context_engine/services/factories.py +88 -0
  108. databao_context_engine/services/models.py +12 -0
  109. databao_context_engine/services/persistence_service.py +61 -0
  110. databao_context_engine/services/run_name_policy.py +8 -0
  111. databao_context_engine/services/table_name_policy.py +15 -0
  112. databao_context_engine/storage/__init__.py +0 -0
  113. databao_context_engine/storage/connection.py +32 -0
  114. databao_context_engine/storage/exceptions/__init__.py +0 -0
  115. databao_context_engine/storage/exceptions/exceptions.py +6 -0
  116. databao_context_engine/storage/migrate.py +127 -0
  117. databao_context_engine/storage/migrations/V01__init.sql +63 -0
  118. databao_context_engine/storage/models.py +51 -0
  119. databao_context_engine/storage/repositories/__init__.py +0 -0
  120. databao_context_engine/storage/repositories/chunk_repository.py +130 -0
  121. databao_context_engine/storage/repositories/datasource_run_repository.py +136 -0
  122. databao_context_engine/storage/repositories/embedding_model_registry_repository.py +87 -0
  123. databao_context_engine/storage/repositories/embedding_repository.py +113 -0
  124. databao_context_engine/storage/repositories/factories.py +35 -0
  125. databao_context_engine/storage/repositories/run_repository.py +157 -0
  126. databao_context_engine/storage/repositories/vector_search_repository.py +63 -0
  127. databao_context_engine/storage/transaction.py +14 -0
  128. databao_context_engine/system/__init__.py +0 -0
  129. databao_context_engine/system/properties.py +13 -0
  130. databao_context_engine/templating/__init__.py +0 -0
  131. databao_context_engine/templating/renderer.py +29 -0
  132. databao_context_engine-0.1.1.dist-info/METADATA +186 -0
  133. databao_context_engine-0.1.1.dist-info/RECORD +135 -0
  134. databao_context_engine-0.1.1.dist-info/WHEEL +4 -0
  135. databao_context_engine-0.1.1.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,433 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ from mssql_python import connect # type: ignore[import-untyped]
6
+ from pydantic import Field
7
+
8
+ from databao_context_engine.plugins.base_db_plugin import BaseDatabaseConfigFile
9
+ from databao_context_engine.plugins.databases.base_introspector import BaseIntrospector, SQLQuery
10
+ from databao_context_engine.plugins.databases.databases_types import DatabaseSchema, DatabaseTable
11
+ from databao_context_engine.plugins.databases.introspection_model_builder import IntrospectionModelBuilder
12
+
13
+
14
+ class MSSQLConfigFile(BaseDatabaseConfigFile):
15
+ type: str = Field(default="databases/mssql")
16
+ connection: dict[str, Any] = Field(
17
+ description="Connection parameters for the Microsoft Server SQL database. It can contain any of the keys supported by the Microsoft Server connection library"
18
+ )
19
+
20
+
21
+ class MSSQLIntrospector(BaseIntrospector[MSSQLConfigFile]):
22
+ _IGNORED_SCHEMAS = {
23
+ "sys",
24
+ "information_schema",
25
+ "db_accessadmin",
26
+ "db_backupoperator",
27
+ "db_datareader",
28
+ "db_datawriter",
29
+ "db_ddladmin",
30
+ "db_denydatareader",
31
+ "db_denydatawriter",
32
+ "db_owner",
33
+ "db_securityadmin",
34
+ }
35
+ _IGNORED_CATALOGS = (
36
+ "master",
37
+ "model",
38
+ "msdb",
39
+ "tempdb",
40
+ )
41
+ supports_catalogs = True
42
+
43
+ def _connect(self, file_config: MSSQLConfigFile):
44
+ connection = file_config.connection
45
+ if not isinstance(connection, Mapping):
46
+ raise ValueError("Invalid YAML config: 'connection' must be a mapping of connection parameters")
47
+
48
+ connection_string = self._create_connection_string_for_config(connection)
49
+ return connect(connection_string)
50
+
51
+ def _connect_to_catalog(self, file_config: MSSQLConfigFile, catalog: str):
52
+ base_cfg = file_config.connection or {}
53
+ cfg_for_db: dict[str, Any] = dict(base_cfg)
54
+ cfg_for_db["database"] = catalog
55
+
56
+ connection_string = self._create_connection_string_for_config(cfg_for_db)
57
+ return connect(connection_string)
58
+
59
+ def _get_catalogs(self, connection, file_config: MSSQLConfigFile) -> list[str]:
60
+ database = file_config.connection.get("database")
61
+ if isinstance(database, str) and database:
62
+ return [database]
63
+
64
+ rows = self._fetchall_dicts(connection, "SELECT name FROM sys.databases", None)
65
+ all_catalogs = [row["name"] for row in rows]
66
+ return [catalog for catalog in all_catalogs if catalog not in self._IGNORED_CATALOGS]
67
+
68
+ def _sql_list_schemas(self, catalogs: list[str] | None) -> SQLQuery:
69
+ if not catalogs:
70
+ return SQLQuery("SELECT schema_name, catalog_name FROM information_schema.schemata", None)
71
+
72
+ parts = []
73
+ for catalog in catalogs:
74
+ parts.append(f"SELECT schema_name, catalog_name FROM {catalog}.information_schema.schemata")
75
+ return SQLQuery(" UNION ALL ".join(parts), None)
76
+
77
+ _USE_BATCH = True
78
+
79
+ def collect_catalog_model(self, connection, catalog: str, schemas: list[str]) -> list[DatabaseSchema] | None:
80
+ if not schemas:
81
+ return []
82
+
83
+ comps = self._component_queries()
84
+
85
+ values = ", ".join(f"({self._quote_literal(s)})" for s in schemas)
86
+ batch_prefix = "SET NOCOUNT ON; SET XACT_ABORT ON;"
87
+ schema_table = f"DECLARE @schemas TABLE (name sysname);\nINSERT INTO @schemas (name) VALUES {values};"
88
+
89
+ batch = (
90
+ batch_prefix
91
+ + "\n"
92
+ + schema_table
93
+ + "\n"
94
+ + ";\n".join(sql.strip().rstrip(";") for sql in comps.values())
95
+ + ";"
96
+ )
97
+
98
+ results: dict[str, list[dict]] = {name: [] for name in comps}
99
+ with connection.cursor() as cur:
100
+ cur.execute(batch)
101
+ for ix, name in enumerate(comps.keys(), start=1):
102
+ rows: list[dict] = []
103
+ if cur.description:
104
+ cols = [c[0].lower() for c in cur.description]
105
+ rows = [dict(zip(cols, r)) for r in cur.fetchall()]
106
+ results[name] = rows
107
+
108
+ if ix < len(comps):
109
+ ok = cur.nextset()
110
+ if not ok:
111
+ raise RuntimeError(f"Batch ended early after component #{ix} '{name}'")
112
+
113
+ return IntrospectionModelBuilder.build_schemas_from_components(
114
+ schemas=schemas,
115
+ rels=results.get("relations", []),
116
+ cols=results.get("columns", []),
117
+ pk_cols=results.get("pk", []),
118
+ uq_cols=results.get("uq", []),
119
+ checks=results.get("checks", []),
120
+ fk_cols=results.get("fks", []),
121
+ idx_cols=results.get("idx", []),
122
+ )
123
+
124
+ def collect_schema_model(self, connection, catalog: str, schema: str) -> list[DatabaseTable] | None:
125
+ comps = self._component_queries()
126
+
127
+ schema_lit = self._quote_literal(schema)
128
+ stmts = [sql.replace("{SCHEMA}", schema_lit) for sql in comps.values()]
129
+ batch_prefix = "SET NOCOUNT ON; SET XACT_ABORT ON;"
130
+ batch = batch_prefix + "\n" + ";\n".join(s.rstrip().rstrip(";") for s in stmts) + ";"
131
+
132
+ results: dict[str, list[dict]] = {name: [] for name in comps}
133
+ with connection.cursor() as cur:
134
+ cur.execute(batch)
135
+ for ix, name in enumerate(comps.keys(), start=1):
136
+ try:
137
+ rows: list[dict] = []
138
+ if cur.description:
139
+ cols = [c[0].lower() for c in cur.description]
140
+ rows = [dict(zip(cols, r)) for r in cur.fetchall()]
141
+ results[name] = rows
142
+ except Exception as e:
143
+ raise RuntimeError(f"Failed reading component #{ix} '{name}'") from e
144
+ if ix < len(comps):
145
+ ok = cur.nextset()
146
+ if not ok:
147
+ raise RuntimeError(f"Batch ended early after component #{ix} '{name}'")
148
+
149
+ return IntrospectionModelBuilder.build_tables_from_components(
150
+ rels=results.get("relations", []),
151
+ cols=results.get("columns", []),
152
+ pk_cols=results.get("pk", []),
153
+ uq_cols=results.get("uq", []),
154
+ checks=results.get("checks", []),
155
+ fk_cols=results.get("fks", []),
156
+ idx_cols=results.get("idx", []),
157
+ )
158
+
159
+ def _component_queries(self) -> dict[str, str]:
160
+ return {
161
+ "relations": self._sql_relations(),
162
+ "columns": self._sql_columns(),
163
+ "pk": self._sql_primary_keys(),
164
+ "uq": self._sql_uniques(),
165
+ "checks": self._sql_checks(),
166
+ "fks": self._sql_foreign_keys(),
167
+ "idx": self._sql_indexes(),
168
+ }
169
+
170
+ def _sql_relations(self) -> str:
171
+ return r"""
172
+ SELECT
173
+ s.name AS schema_name,
174
+ t.name AS table_name,
175
+ 'table' AS kind,
176
+ CAST(ep.value AS nvarchar(4000)) AS description
177
+ FROM
178
+ sys.tables t
179
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
180
+ LEFT JOIN sys.extended_properties ep
181
+ ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.class = 1 AND ep.name = 'MS_Description'
182
+ WHERE
183
+ s.name IN (SELECT name FROM @schemas)
184
+ UNION ALL
185
+ SELECT
186
+ s.name AS schema_name,
187
+ v.name AS table_name,
188
+ 'view' AS kind,
189
+ CAST(ep.value AS nvarchar(4000)) AS description
190
+ FROM
191
+ sys.views v
192
+ JOIN sys.schemas s ON s.schema_id = v.schema_id
193
+ LEFT JOIN sys.extended_properties ep
194
+ ON ep.major_id = v.object_id AND ep.minor_id = 0 AND ep.class = 1 AND ep.name = 'MS_Description'
195
+ WHERE
196
+ s.name IN (SELECT name FROM @schemas)
197
+ UNION ALL
198
+ SELECT
199
+ s.name AS schema_name,
200
+ et.name AS table_name,
201
+ 'external_table' AS kind,
202
+ CAST(ep.value AS nvarchar(4000)) AS description
203
+ FROM
204
+ sys.external_tables et
205
+ JOIN sys.schemas s ON s.schema_id = et.schema_id
206
+ LEFT JOIN sys.extended_properties ep
207
+ ON ep.major_id = et.object_id AND ep.minor_id = 0 AND ep.class = 1 AND ep.name = 'MS_Description'
208
+ WHERE
209
+ s.name IN (SELECT name FROM @schemas)
210
+ ORDER BY
211
+ table_name;
212
+ """
213
+
214
+ # TODO: simplify case when for datatype
215
+ def _sql_columns(self) -> str:
216
+ return r"""
217
+ SELECT
218
+ s.name AS schema_name,
219
+ o.name AS table_name,
220
+ c.name AS column_name,
221
+ c.column_id AS ordinal_position,
222
+ CASE
223
+ WHEN t.name IN ('varchar','char','varbinary','binary') THEN t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS varchar(10)) END + ')'
224
+ WHEN t.name IN ('nvarchar','nchar') THEN t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length / 2 AS varchar(10)) END + ')'
225
+ WHEN t.name IN ('decimal','numeric') THEN t.name + '(' + CAST(c.precision AS varchar(10)) + ',' + CAST(c.scale AS varchar(10)) + ')'
226
+ ELSE t.name
227
+ END AS data_type,
228
+ CAST(c.is_nullable AS bit) AS is_nullable,
229
+ CASE
230
+ WHEN cc.object_id IS NOT NULL THEN CAST(cc.definition AS nvarchar(4000))
231
+ ELSE CAST(dc.definition AS nvarchar(4000))
232
+ END AS default_expression,
233
+ CASE
234
+ WHEN c.is_identity = 1 THEN 'identity'
235
+ WHEN c.is_computed = 1 THEN 'computed'
236
+ END AS generated,
237
+ CAST(ep.value AS nvarchar(4000)) AS description
238
+ FROM
239
+ sys.columns c
240
+ JOIN sys.objects o ON o.object_id = c.object_id AND o.type IN ('U','V')
241
+ JOIN sys.schemas s ON s.schema_id = o.schema_id
242
+ JOIN sys.types t ON t.user_type_id = c.user_type_id
243
+ LEFT JOIN sys.computed_columns cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
244
+ LEFT JOIN sys.default_constraints dc ON dc.object_id = c.default_object_id
245
+ LEFT JOIN sys.extended_properties ep ON ep.class = 1 AND ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description'
246
+ WHERE
247
+ s.name IN (SELECT name FROM @schemas)
248
+ ORDER BY
249
+ o.name,
250
+ c.column_id;
251
+ """
252
+
253
+ def _sql_primary_keys(self) -> str:
254
+ return r"""
255
+ SELECT
256
+ s.name AS schema_name,
257
+ t.name AS table_name,
258
+ kc.name AS constraint_name,
259
+ c.name AS column_name,
260
+ ic.key_ordinal AS position
261
+ FROM
262
+ sys.key_constraints kc
263
+ JOIN sys.tables t ON t.object_id = kc.parent_object_id
264
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
265
+ JOIN sys.index_columns ic ON ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id AND ic.is_included_column = 0
266
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
267
+ WHERE
268
+ s.name IN (SELECT name FROM @schemas)
269
+ AND kc.type = 'PK'
270
+ ORDER BY
271
+ t.name,
272
+ kc.name,
273
+ ic.key_ordinal;
274
+ """
275
+
276
+ def _sql_uniques(self) -> str:
277
+ return r"""
278
+ SELECT
279
+ s.name AS schema_name,
280
+ t.name AS table_name,
281
+ kc.name AS constraint_name,
282
+ c.name AS column_name,
283
+ ic.key_ordinal AS position
284
+ FROM
285
+ sys.key_constraints kc
286
+ JOIN sys.tables t ON t.object_id = kc.parent_object_id
287
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
288
+ JOIN sys.index_columns ic ON ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id AND ic.is_included_column = 0
289
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
290
+ WHERE
291
+ s.name IN (SELECT name FROM @schemas)
292
+ AND kc.type = 'UQ'
293
+ ORDER BY
294
+ t.name,
295
+ kc.name,
296
+ ic.key_ordinal;
297
+ """
298
+
299
+ def _sql_checks(self) -> str:
300
+ return r"""
301
+ SELECT
302
+ s.name AS schema_name,
303
+ t.name AS table_name,
304
+ cc.name AS constraint_name,
305
+ CAST(cc.definition AS nvarchar(4000)) AS expression,
306
+ CAST(CASE
307
+ WHEN cc.is_not_trusted = 0 THEN 1
308
+ ELSE 0
309
+ END AS bit) AS validated
310
+ FROM
311
+ sys.check_constraints cc
312
+ JOIN sys.tables t ON t.object_id = cc.parent_object_id
313
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
314
+ WHERE
315
+ s.name IN (SELECT name FROM @schemas)
316
+ ORDER BY
317
+ t.name,
318
+ cc.name;
319
+ """
320
+
321
+ def _sql_foreign_keys(self) -> str:
322
+ return r"""
323
+ SELECT
324
+ s.name AS schema_name,
325
+ t.name AS table_name,
326
+ fk.name AS constraint_name,
327
+ fkc.constraint_column_id AS position,
328
+ pc.name AS from_column,
329
+ rs.name AS ref_schema,
330
+ rt.name AS ref_table,
331
+ rc.name AS to_column,
332
+ CAST(CASE WHEN fk.is_disabled = 0 THEN 1 ELSE 0 END AS bit) AS enforced,
333
+ CAST(CASE WHEN fk.is_not_trusted = 0 THEN 1 ELSE 0 END AS bit) AS validated,
334
+ LOWER(fk.update_referential_action_desc) AS on_update,
335
+ LOWER(fk.delete_referential_action_desc) AS on_delete
336
+ FROM
337
+ sys.foreign_keys fk
338
+ JOIN sys.tables t ON t.object_id = fk.parent_object_id
339
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
340
+ JOIN sys.tables rt ON rt.object_id = fk.referenced_object_id
341
+ JOIN sys.schemas rs ON rs.schema_id = rt.schema_id
342
+ JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
343
+ JOIN sys.columns pc ON pc.object_id = fkc.parent_object_id AND pc.column_id = fkc.parent_column_id
344
+ JOIN sys.columns rc ON rc.object_id = fkc.referenced_object_id AND rc.column_id = fkc.referenced_column_id
345
+ WHERE
346
+ s.name IN (SELECT name FROM @schemas)
347
+ ORDER BY
348
+ t.name,
349
+ fk.name,
350
+ fkc.constraint_column_id;
351
+ """
352
+
353
+ # TODO: case when is confusing
354
+ def _sql_indexes(self) -> str:
355
+ return r"""
356
+ SELECT
357
+ s.name AS schema_name,
358
+ t.name AS table_name,
359
+ i.name AS index_name,
360
+ ic.key_ordinal AS position,
361
+ CAST(
362
+ CASE
363
+ WHEN ic.is_descending_key = 1 THEN c.name + ' DESC'
364
+ ELSE c.name
365
+ END AS nvarchar(4000)
366
+ ) AS expr,
367
+ CAST(i.is_unique AS bit) AS is_unique,
368
+ LOWER(i.type_desc) AS method,
369
+ CAST(i.filter_definition AS nvarchar(4000)) AS predicate
370
+ FROM
371
+ sys.indexes i
372
+ JOIN sys.tables t ON t.object_id = i.object_id
373
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
374
+ JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
375
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
376
+ WHERE
377
+ s.name IN (SELECT name FROM @schemas)
378
+ AND i.is_primary_key = 0
379
+ AND i.is_unique_constraint = 0
380
+ AND ic.is_included_column = 0
381
+ AND ic.key_ordinal > 0
382
+ ORDER BY
383
+ t.name,
384
+ i.name,
385
+ ic.key_ordinal;
386
+ """
387
+
388
+ def _create_connection_string_for_config(self, file_config: Mapping[str, Any]) -> str:
389
+ def _escape_odbc_value(value: str) -> str:
390
+ return "{" + value.replace("}", "}}").replace("{", "{{") + "}"
391
+
392
+ host = file_config.get("host")
393
+ if not host:
394
+ raise ValueError("A host must be provided to connect to the MSSQL database.")
395
+
396
+ port = file_config.get("port", 1433)
397
+ instance = file_config.get("instanceName")
398
+ if instance:
399
+ server_part = f"{host}\\{instance}"
400
+ else:
401
+ server_part = f"{host},{port}"
402
+
403
+ database = file_config.get("database")
404
+ user = file_config.get("user")
405
+ password = file_config.get("password")
406
+
407
+ connection_parts = {
408
+ "server": _escape_odbc_value(server_part),
409
+ "database": _escape_odbc_value(str(database)) if database is not None else None,
410
+ "uid": _escape_odbc_value(str(user)) if user is not None else None,
411
+ "pwd": _escape_odbc_value(str(password)) if password is not None else None,
412
+ "encrypt": file_config.get("encrypt"),
413
+ "trust_server_certificate": "yes" if file_config.get("trust_server_certificate") else None,
414
+ }
415
+
416
+ connection_string = ";".join(f"{k}={v}" for k, v in connection_parts.items() if v is not None)
417
+ return connection_string
418
+
419
+ def _fetchall_dicts(self, connection, sql: str, params) -> list[dict]:
420
+ with connection.cursor() as cursor:
421
+ cursor.execute(sql, params or ())
422
+ if not cursor.description:
423
+ return []
424
+
425
+ columns = [col[0].lower() for col in cursor.description]
426
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
427
+
428
+ def _quote_literal(self, value: str) -> str:
429
+ return "'" + str(value).replace("'", "''") + "'"
430
+
431
+ def _sql_sample_rows(self, catalog: str, schema: str, table: str, limit: int) -> SQLQuery:
432
+ sql = f'SELECT TOP ({limit}) * FROM "{catalog}"."{schema}"."{table}"'
433
+ return SQLQuery(sql, [limit])