al-data-utils 0.3.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 (60) hide show
  1. al_data_utils/__init__.py +10 -0
  2. al_data_utils/airflow_utils/__init__.py +13 -0
  3. al_data_utils/airflow_utils/airflow_utils.py +83 -0
  4. al_data_utils/azure_as_utils/__init__.py +31 -0
  5. al_data_utils/azure_as_utils/auth.py +82 -0
  6. al_data_utils/azure_as_utils/azure_as_utils.py +223 -0
  7. al_data_utils/azure_as_utils/client.py +158 -0
  8. al_data_utils/azure_as_utils/errors.py +78 -0
  9. al_data_utils/azure_as_utils/models.py +265 -0
  10. al_data_utils/azure_as_utils/poller.py +121 -0
  11. al_data_utils/backup_utils/__init__.py +9 -0
  12. al_data_utils/backup_utils/mssql/dacpac/__init__.py +1 -0
  13. al_data_utils/backup_utils/mssql/dacpac/sqlpackage.py +207 -0
  14. al_data_utils/backup_utils/mssql/schema_snapshot/__init__.py +8 -0
  15. al_data_utils/backup_utils/mssql/schema_snapshot/config.py +150 -0
  16. al_data_utils/backup_utils/mssql/schema_snapshot/emit/__init__.py +11 -0
  17. al_data_utils/backup_utils/mssql/schema_snapshot/emit/foreign_keys.py +38 -0
  18. al_data_utils/backup_utils/mssql/schema_snapshot/emit/indexes.py +37 -0
  19. al_data_utils/backup_utils/mssql/schema_snapshot/emit/sql_objects.py +17 -0
  20. al_data_utils/backup_utils/mssql/schema_snapshot/emit/tables.py +139 -0
  21. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/__init__.py +19 -0
  22. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/foreign_keys.py +173 -0
  23. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/functions.py +133 -0
  24. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/indexes.py +190 -0
  25. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/sequences.py +180 -0
  26. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/stored_procedures.py +313 -0
  27. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/synonyms.py +135 -0
  28. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/tables.py +251 -0
  29. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/views.py +244 -0
  30. al_data_utils/backup_utils/mssql/schema_snapshot/io/__init__.py +10 -0
  31. al_data_utils/backup_utils/mssql/schema_snapshot/io/layout.py +28 -0
  32. al_data_utils/backup_utils/mssql/schema_snapshot/io/writer.py +40 -0
  33. al_data_utils/backup_utils/mssql/schema_snapshot/models/__init__.py +14 -0
  34. al_data_utils/backup_utils/mssql/schema_snapshot/models/foreign_key_def.py +19 -0
  35. al_data_utils/backup_utils/mssql/schema_snapshot/models/index_def.py +19 -0
  36. al_data_utils/backup_utils/mssql/schema_snapshot/models/sql_object_def.py +11 -0
  37. al_data_utils/backup_utils/mssql/schema_snapshot/models/table_column_def.py +27 -0
  38. al_data_utils/backup_utils/mssql/schema_snapshot/models/table_def.py +22 -0
  39. al_data_utils/backup_utils/mssql/schema_snapshot/snapshot_processor.py +754 -0
  40. al_data_utils/backup_utils/mssql/scripter/__init__.py +1 -0
  41. al_data_utils/config_utils/__init__.py +6 -0
  42. al_data_utils/config_utils/config_utils.py +31 -0
  43. al_data_utils/files_sync_utils/__init__.py +15 -0
  44. al_data_utils/files_sync_utils/files_sync_utils.py +567 -0
  45. al_data_utils/geo_utils/__init__.py +0 -0
  46. al_data_utils/git_utils/__init__.py +27 -0
  47. al_data_utils/git_utils/git_utils.py +142 -0
  48. al_data_utils/logging_utils/__init__.py +3 -0
  49. al_data_utils/logging_utils/logging_utils.py +39 -0
  50. al_data_utils/ones_api_utils/__init__.py +3 -0
  51. al_data_utils/ones_api_utils/ones_api_utils.py +61 -0
  52. al_data_utils/sql_utils/__init__.py +3 -0
  53. al_data_utils/sql_utils/sql_utils.py +158 -0
  54. al_data_utils/tg_utils/__init__.py +3 -0
  55. al_data_utils/tg_utils/tg_utils.py +45 -0
  56. al_data_utils-0.3.1.dist-info/METADATA +157 -0
  57. al_data_utils-0.3.1.dist-info/RECORD +60 -0
  58. al_data_utils-0.3.1.dist-info/WHEEL +5 -0
  59. al_data_utils-0.3.1.dist-info/licenses/LICENSE +21 -0
  60. al_data_utils-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,313 @@
1
+ from __future__ import annotations
2
+
3
+ from al_data_utils.logging_utils import warning, info
4
+ from al_data_utils.sql_utils.sql_utils import UnifiedSQLClient
5
+ from ..models import SqlObjectDef
6
+
7
+
8
+ _SQL_STORED_PROCEDURE_NAMES = r"""
9
+ select
10
+ s.name as schema_name,
11
+ p.name as object_name
12
+ from sys.procedures p
13
+ inner join sys.schemas s
14
+ on s.schema_id = p.schema_id
15
+ where p.is_ms_shipped = 0
16
+ order by s.name, p.name;
17
+ """
18
+
19
+
20
+ _SQL_STORED_PROCEDURES_BULK = r"""
21
+ select
22
+ s.name as schema_name,
23
+ p.name as object_name,
24
+ convert(nvarchar(max), m.definition) as definition
25
+ from sys.procedures p
26
+ inner join sys.schemas s
27
+ on s.schema_id = p.schema_id
28
+ inner join sys.sql_modules m
29
+ on m.object_id = p.object_id
30
+ where p.is_ms_shipped = 0
31
+ order by s.name, p.name;
32
+ """
33
+
34
+
35
+ _SQL_STORED_PROCEDURE_DEFINITION = r"""
36
+ select
37
+ convert(nvarchar(max), object_definition(object_id(quotename(?) + '.' + quotename(?)))) as definition
38
+ """
39
+
40
+
41
+ def _normalized_set(values: set[str] | None) -> set[str]:
42
+ if not values:
43
+ return set()
44
+ return {str(v).strip().lower() for v in values if str(v).strip()}
45
+
46
+
47
+ def _split_qualified(values: set[str]) -> list[tuple[str, str]]:
48
+ pairs: list[tuple[str, str]] = []
49
+ for v in sorted(values):
50
+ if '.' not in v:
51
+ continue
52
+ schema, name = v.split('.', 1)
53
+ schema = schema.strip().lower()
54
+ name = name.strip().lower()
55
+ if schema and name:
56
+ pairs.append((schema, name))
57
+ return pairs
58
+
59
+
60
+ def _build_filter_sql_and_params(
61
+ *,
62
+ include_schemas: set[str] | None = None,
63
+ exclude_schemas: set[str] | None = None,
64
+ include_objects: set[str] | None = None,
65
+ exclude_objects: set[str] | None = None,
66
+ ) -> tuple[str, dict[str, str]]:
67
+ inc_schemas = sorted(_normalized_set(include_schemas))
68
+ exc_schemas = sorted(_normalized_set(exclude_schemas))
69
+ inc_objects = _split_qualified(_normalized_set(include_objects))
70
+ exc_objects = _split_qualified(_normalized_set(exclude_objects))
71
+
72
+ params: dict[str, str] = {}
73
+ clauses: list[str] = []
74
+
75
+ if inc_schemas:
76
+ names: list[str] = []
77
+ for i, schema in enumerate(inc_schemas):
78
+ key = f'inc_schema_{i}'
79
+ params[key] = schema
80
+ names.append(f':{key}')
81
+ clauses.append(f'lower(s.name) in ({", ".join(names)})')
82
+
83
+ if exc_schemas:
84
+ names = []
85
+ for i, schema in enumerate(exc_schemas):
86
+ key = f'exc_schema_{i}'
87
+ params[key] = schema
88
+ names.append(f':{key}')
89
+ clauses.append(f'lower(s.name) not in ({", ".join(names)})')
90
+
91
+ if inc_objects:
92
+ parts: list[str] = []
93
+ for i, (schema, name) in enumerate(inc_objects):
94
+ sk = f'inc_obj_schema_{i}'
95
+ nk = f'inc_obj_name_{i}'
96
+ params[sk] = schema
97
+ params[nk] = name
98
+ parts.append(f'(lower(s.name) = :{sk} and lower(p.name) = :{nk})')
99
+ clauses.append(f'({" or ".join(parts)})')
100
+
101
+ if exc_objects:
102
+ parts = []
103
+ for i, (schema, name) in enumerate(exc_objects):
104
+ sk = f'exc_obj_schema_{i}'
105
+ nk = f'exc_obj_name_{i}'
106
+ params[sk] = schema
107
+ params[nk] = name
108
+ parts.append(f'(lower(s.name) = :{sk} and lower(p.name) = :{nk})')
109
+ clauses.append(f'not ({" or ".join(parts)})')
110
+
111
+ if not clauses:
112
+ return '', {}
113
+
114
+ return '\n and ' + '\n and '.join(clauses), params
115
+
116
+
117
+ def _inject_filters(sql: str, filters_sql: str) -> str:
118
+ if not filters_sql:
119
+ return sql
120
+ return sql.replace(
121
+ 'where p.is_ms_shipped = 0', f'where p.is_ms_shipped = 0{filters_sql}'
122
+ )
123
+
124
+
125
+ def _execute_direct(
126
+ sql_client: UnifiedSQLClient, query: str, params: tuple | None = None
127
+ ) -> list[dict[str, object]]:
128
+ """Execute a query via raw DBAPI cursor and return rows as dicts.
129
+
130
+ This path avoids SQLAlchemy transaction state issues after low-level TDS errors.
131
+ """
132
+ conn = sql_client.engine.raw_connection()
133
+ cursor = conn.cursor()
134
+ try:
135
+ if params:
136
+ cursor.execute(query, params)
137
+ else:
138
+ cursor.execute(query)
139
+
140
+ columns = [c[0] for c in cursor.description or []]
141
+ rows = cursor.fetchall() if columns else []
142
+ return [dict(zip(columns, row)) for row in rows]
143
+ finally:
144
+ try:
145
+ cursor.close()
146
+ except Exception:
147
+ pass
148
+ try:
149
+ conn.close()
150
+ except Exception:
151
+ pass
152
+
153
+
154
+ def get_stored_procedures(
155
+ sql_client: UnifiedSQLClient,
156
+ *,
157
+ include_schemas: set[str] | None = None,
158
+ exclude_schemas: set[str] | None = None,
159
+ include_objects: set[str] | None = None,
160
+ exclude_objects: set[str] | None = None,
161
+ verbose: bool = True,
162
+ ) -> list[SqlObjectDef]:
163
+ _info = info if verbose else (lambda _: None)
164
+
165
+ filters_sql, params = _build_filter_sql_and_params(
166
+ include_schemas=include_schemas,
167
+ exclude_schemas=exclude_schemas,
168
+ include_objects=include_objects,
169
+ exclude_objects=exclude_objects,
170
+ )
171
+
172
+ include_pairs = _split_qualified(_normalized_set(include_objects))
173
+
174
+ # Exact include-list mode: fetch only requested procedure definitions.
175
+ if include_pairs:
176
+ total = len(include_pairs)
177
+ _info(
178
+ '[schema_snapshot..] stored procedures discovered: '
179
+ f'{total} (targeted include mode)'
180
+ )
181
+ result: list[SqlObjectDef] = []
182
+ for i, (schema_name, object_name) in enumerate(include_pairs, start=1):
183
+ _info(
184
+ f'[schema_snapshot..] fetching stored procedure definition [{i}/{total}]: '
185
+ f'{schema_name}.{object_name}'
186
+ )
187
+
188
+ try:
189
+ def_rows = _execute_direct(
190
+ sql_client,
191
+ _SQL_STORED_PROCEDURE_DEFINITION,
192
+ params=(schema_name, object_name),
193
+ )
194
+ definition = ''
195
+ if def_rows:
196
+ definition = str(def_rows[0].get('definition') or '')
197
+
198
+ if not definition:
199
+ warning(
200
+ '[schema_snapshot..] stored procedure definition is empty '
201
+ f'for {schema_name}.{object_name} (not found or encrypted); skipping'
202
+ )
203
+ continue
204
+
205
+ result.append(
206
+ SqlObjectDef(
207
+ schema_name=schema_name,
208
+ object_name=object_name,
209
+ definition=definition,
210
+ )
211
+ )
212
+ except Exception as one_err:
213
+ warning(
214
+ '[schema_snapshot..] failed to extract stored procedure '
215
+ f'[{i}/{total}] {schema_name}.{object_name}; skipping ({one_err})'
216
+ )
217
+
218
+ return result
219
+
220
+ # Fast path: single bulk query for all procedure definitions.
221
+ # Falls back to per-procedure extraction only if the bulk query fails.
222
+ try:
223
+ rows = sql_client.execute_command_with_result_to_dicts(
224
+ _inject_filters(_SQL_STORED_PROCEDURES_BULK, filters_sql), params
225
+ )
226
+ total = len(rows)
227
+ _info(f'[schema_snapshot..] stored procedures discovered: {total} (bulk mode)')
228
+
229
+ result: list[SqlObjectDef] = []
230
+ for i, r in enumerate(rows, start=1):
231
+ schema_name = str(r['schema_name'])
232
+ object_name = str(r['object_name'])
233
+ definition = str(r.get('definition') or '')
234
+
235
+ if not definition:
236
+ warning(
237
+ '[schema_snapshot..] stored procedure definition is empty '
238
+ f'for {schema_name}.{object_name} (possibly encrypted); skipping'
239
+ )
240
+ continue
241
+
242
+ result.append(
243
+ SqlObjectDef(
244
+ schema_name=schema_name,
245
+ object_name=object_name,
246
+ definition=definition,
247
+ )
248
+ )
249
+
250
+ _info(
251
+ f'[schema_snapshot..] extracted stored procedure [{i}/{total}]: '
252
+ f'{schema_name}.{object_name}'
253
+ )
254
+
255
+ return result
256
+ except Exception as bulk_err:
257
+ warning(
258
+ '[schema_snapshot..] bulk stored procedure extraction failed; '
259
+ f'falling back to per-procedure mode ({bulk_err})'
260
+ )
261
+
262
+ # Fetch names first, then definition one by one.
263
+ # This avoids failing the whole snapshot because of one problematic procedure.
264
+ result: list[SqlObjectDef] = []
265
+ name_rows = sql_client.execute_command_with_result_to_dicts(
266
+ _inject_filters(_SQL_STORED_PROCEDURE_NAMES, filters_sql), params
267
+ )
268
+ total = len(name_rows)
269
+ _info(
270
+ f'[schema_snapshot..] stored procedures discovered: {total} (per-procedure mode)'
271
+ )
272
+ for i, r in enumerate(name_rows, start=1):
273
+ schema_name = str(r['schema_name'])
274
+ object_name = str(r['object_name'])
275
+ _info(
276
+ f'[schema_snapshot..] fetching stored procedure definition [{i}/{total}]: '
277
+ f'{schema_name}.{object_name}'
278
+ )
279
+ try:
280
+ def_rows = _execute_direct(
281
+ sql_client,
282
+ _SQL_STORED_PROCEDURE_DEFINITION,
283
+ params=(schema_name, object_name),
284
+ )
285
+ definition = ''
286
+ if def_rows:
287
+ definition = str(def_rows[0].get('definition') or '')
288
+
289
+ if not definition:
290
+ warning(
291
+ '[schema_snapshot..] stored procedure definition is empty '
292
+ f'for {schema_name}.{object_name} (possibly encrypted); skipping'
293
+ )
294
+ continue
295
+
296
+ result.append(
297
+ SqlObjectDef(
298
+ schema_name=schema_name,
299
+ object_name=object_name,
300
+ definition=definition,
301
+ )
302
+ )
303
+ _info(
304
+ f'[schema_snapshot..] extracted stored procedure [{i}/{total}]: '
305
+ f'{schema_name}.{object_name}'
306
+ )
307
+ except Exception as one_err:
308
+ warning(
309
+ '[schema_snapshot..] failed to extract stored procedure '
310
+ f'[{i}/{total}] {schema_name}.{object_name}; skipping ({one_err})'
311
+ )
312
+
313
+ return result
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ from al_data_utils.sql_utils.sql_utils import UnifiedSQLClient
4
+ from ..models import SqlObjectDef
5
+
6
+
7
+ _SQL_SYNONYMS = r"""
8
+ select
9
+ s.name as schema_name,
10
+ sn.name as object_name,
11
+ sn.base_object_name
12
+ from sys.synonyms sn
13
+ inner join sys.schemas s
14
+ on s.schema_id = sn.schema_id
15
+ order by s.name, sn.name;
16
+ """
17
+
18
+
19
+ def _normalized_set(values: set[str] | None) -> set[str]:
20
+ if not values:
21
+ return set()
22
+ return {str(v).strip().lower() for v in values if str(v).strip()}
23
+
24
+
25
+ def _split_qualified(values: set[str]) -> list[tuple[str, str]]:
26
+ pairs: list[tuple[str, str]] = []
27
+ for v in sorted(values):
28
+ if '.' not in v:
29
+ continue
30
+ schema, name = v.split('.', 1)
31
+ schema = schema.strip().lower()
32
+ name = name.strip().lower()
33
+ if schema and name:
34
+ pairs.append((schema, name))
35
+ return pairs
36
+
37
+
38
+ def _build_filter_sql_and_params(
39
+ *,
40
+ include_schemas: set[str] | None = None,
41
+ exclude_schemas: set[str] | None = None,
42
+ include_objects: set[str] | None = None,
43
+ exclude_objects: set[str] | None = None,
44
+ ) -> tuple[str, dict[str, str]]:
45
+ inc_schemas = sorted(_normalized_set(include_schemas))
46
+ exc_schemas = sorted(_normalized_set(exclude_schemas))
47
+ inc_objects = _split_qualified(_normalized_set(include_objects))
48
+ exc_objects = _split_qualified(_normalized_set(exclude_objects))
49
+
50
+ params: dict[str, str] = {}
51
+ clauses: list[str] = []
52
+
53
+ if inc_schemas:
54
+ names: list[str] = []
55
+ for i, schema in enumerate(inc_schemas):
56
+ key = f'inc_schema_{i}'
57
+ params[key] = schema
58
+ names.append(f':{key}')
59
+ clauses.append(f'lower(s.name) in ({", ".join(names)})')
60
+
61
+ if exc_schemas:
62
+ names = []
63
+ for i, schema in enumerate(exc_schemas):
64
+ key = f'exc_schema_{i}'
65
+ params[key] = schema
66
+ names.append(f':{key}')
67
+ clauses.append(f'lower(s.name) not in ({", ".join(names)})')
68
+
69
+ if inc_objects:
70
+ parts: list[str] = []
71
+ for i, (schema, name) in enumerate(inc_objects):
72
+ sk = f'inc_obj_schema_{i}'
73
+ nk = f'inc_obj_name_{i}'
74
+ params[sk] = schema
75
+ params[nk] = name
76
+ parts.append(f'(lower(s.name) = :{sk} and lower(sn.name) = :{nk})')
77
+ clauses.append(f'({" or ".join(parts)})')
78
+
79
+ if exc_objects:
80
+ parts = []
81
+ for i, (schema, name) in enumerate(exc_objects):
82
+ sk = f'exc_obj_schema_{i}'
83
+ nk = f'exc_obj_name_{i}'
84
+ params[sk] = schema
85
+ params[nk] = name
86
+ parts.append(f'(lower(s.name) = :{sk} and lower(sn.name) = :{nk})')
87
+ clauses.append(f'not ({" or ".join(parts)})')
88
+
89
+ if not clauses:
90
+ return '', {}
91
+
92
+ return '\nwhere ' + '\n and '.join(clauses), params
93
+
94
+
95
+ def _inject_filters(sql: str, filters_sql: str) -> str:
96
+ if not filters_sql:
97
+ return sql
98
+ return sql.replace(
99
+ 'order by s.name, sn.name;', f'{filters_sql}\norder by s.name, sn.name;'
100
+ )
101
+
102
+
103
+ def get_synonyms(
104
+ sql_client: UnifiedSQLClient,
105
+ *,
106
+ include_schemas: set[str] | None = None,
107
+ exclude_schemas: set[str] | None = None,
108
+ include_objects: set[str] | None = None,
109
+ exclude_objects: set[str] | None = None,
110
+ ) -> list[SqlObjectDef]:
111
+ filters_sql, params = _build_filter_sql_and_params(
112
+ include_schemas=include_schemas,
113
+ exclude_schemas=exclude_schemas,
114
+ include_objects=include_objects,
115
+ exclude_objects=exclude_objects,
116
+ )
117
+
118
+ rows = sql_client.execute_command_with_result_to_dicts(
119
+ _inject_filters(_SQL_SYNONYMS, filters_sql), params
120
+ )
121
+
122
+ result: list[SqlObjectDef] = []
123
+ for r in rows:
124
+ schema_name = str(r['schema_name'])
125
+ object_name = str(r['object_name'])
126
+ base_object_name = str(r.get('base_object_name') or '')
127
+ definition = (
128
+ f'create synonym {schema_name}.{object_name} for {base_object_name}'
129
+ )
130
+ result.append(
131
+ SqlObjectDef(
132
+ schema_name=schema_name, object_name=object_name, definition=definition
133
+ )
134
+ )
135
+ return result
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+ from al_data_utils.sql_utils.sql_utils import UnifiedSQLClient
6
+ from ..models import TableColumnDef, TableDef, PrimaryKeyDef
7
+
8
+
9
+ _SQL_TABLE_COLUMNS = r"""
10
+ select
11
+ s.name as schema_name,
12
+ t.name as table_name,
13
+ c.name as column_name,
14
+ c.column_id as ordinal_position,
15
+ ty.name as data_type,
16
+ cast(c.max_length as int) as max_length,
17
+ cast(c.precision as int) as precision,
18
+ cast(c.scale as int) as scale,
19
+ cast(c.is_nullable as bit) as is_nullable,
20
+ cast(c.is_identity as bit) as is_identity,
21
+ cast(ic.seed_value as int) as identity_seed,
22
+ cast(ic.increment_value as int) as identity_increment,
23
+ cast(c.is_computed as bit) as is_computed,
24
+ cc.definition as computed_definition,
25
+ c.collation_name,
26
+ dc.name as default_constraint_name,
27
+ dc.definition as default_definition
28
+ from sys.tables t
29
+ inner join sys.schemas s
30
+ on s.schema_id = t.schema_id
31
+ inner join sys.columns c
32
+ on c.object_id = t.object_id
33
+ inner join sys.types ty
34
+ on ty.user_type_id = c.user_type_id
35
+ left outer join sys.identity_columns ic
36
+ on ic.object_id = c.object_id
37
+ and ic.column_id = c.column_id
38
+ left outer join sys.computed_columns cc
39
+ on cc.object_id = c.object_id
40
+ and cc.column_id = c.column_id
41
+ left outer join sys.default_constraints dc
42
+ on dc.parent_object_id = c.object_id
43
+ and dc.parent_column_id = c.column_id
44
+ where t.is_ms_shipped = 0
45
+ order by s.name, t.name, c.column_id;
46
+ """
47
+
48
+
49
+ _SQL_PRIMARY_KEYS = r"""
50
+ select
51
+ s.name as schema_name,
52
+ t.name as table_name,
53
+ kc.name as constraint_name,
54
+ i.type_desc,
55
+ stuff((
56
+ select
57
+ ', ' + quotename(c.name) + case when ic2.is_descending_key = 1 then ' desc' else ' asc' end
58
+ from sys.index_columns ic2
59
+ inner join sys.columns c
60
+ on c.object_id = ic2.object_id and c.column_id = ic2.column_id
61
+ where ic2.object_id = i.object_id
62
+ and ic2.index_id = i.index_id
63
+ and ic2.key_ordinal > 0
64
+ order by ic2.key_ordinal
65
+ for xml path(''), type
66
+ ).value('.', 'nvarchar(max)'), 1, 2, '') as key_columns
67
+ from sys.key_constraints kc
68
+ inner join sys.tables t
69
+ on t.object_id = kc.parent_object_id
70
+ inner join sys.schemas s
71
+ on s.schema_id = t.schema_id
72
+ inner join sys.indexes i
73
+ on i.object_id = kc.parent_object_id
74
+ and i.index_id = kc.unique_index_id
75
+ where kc.type = 'PK'
76
+ order by s.name, t.name;
77
+ """
78
+
79
+
80
+ def _normalized_set(values: set[str] | None) -> set[str]:
81
+ if not values:
82
+ return set()
83
+ return {str(v).strip().lower() for v in values if str(v).strip()}
84
+
85
+
86
+ def _split_qualified(values: set[str]) -> list[tuple[str, str]]:
87
+ pairs: list[tuple[str, str]] = []
88
+ for v in sorted(values):
89
+ if '.' not in v:
90
+ continue
91
+ schema, table = v.split('.', 1)
92
+ schema = schema.strip().lower()
93
+ table = table.strip().lower()
94
+ if schema and table:
95
+ pairs.append((schema, table))
96
+ return pairs
97
+
98
+
99
+ def _build_table_filter_sql_and_params(
100
+ *,
101
+ include_schemas: set[str] | None = None,
102
+ exclude_schemas: set[str] | None = None,
103
+ include_tables: set[str] | None = None,
104
+ exclude_tables: set[str] | None = None,
105
+ ) -> tuple[str, dict[str, str]]:
106
+ inc_schemas = sorted(_normalized_set(include_schemas))
107
+ exc_schemas = sorted(_normalized_set(exclude_schemas))
108
+ inc_tables = _split_qualified(_normalized_set(include_tables))
109
+ exc_tables = _split_qualified(_normalized_set(exclude_tables))
110
+
111
+ params: dict[str, str] = {}
112
+ clauses: list[str] = []
113
+
114
+ if inc_schemas:
115
+ names: list[str] = []
116
+ for i, schema in enumerate(inc_schemas):
117
+ key = f'inc_schema_{i}'
118
+ params[key] = schema
119
+ names.append(f':{key}')
120
+ clauses.append(f'lower(s.name) in ({", ".join(names)})')
121
+
122
+ if exc_schemas:
123
+ names = []
124
+ for i, schema in enumerate(exc_schemas):
125
+ key = f'exc_schema_{i}'
126
+ params[key] = schema
127
+ names.append(f':{key}')
128
+ clauses.append(f'lower(s.name) not in ({", ".join(names)})')
129
+
130
+ if inc_tables:
131
+ parts: list[str] = []
132
+ for i, (schema, table) in enumerate(inc_tables):
133
+ sk = f'inc_tbl_schema_{i}'
134
+ tk = f'inc_tbl_name_{i}'
135
+ params[sk] = schema
136
+ params[tk] = table
137
+ parts.append(f'(lower(s.name) = :{sk} and lower(t.name) = :{tk})')
138
+ clauses.append(f'({" or ".join(parts)})')
139
+
140
+ if exc_tables:
141
+ parts = []
142
+ for i, (schema, table) in enumerate(exc_tables):
143
+ sk = f'exc_tbl_schema_{i}'
144
+ tk = f'exc_tbl_name_{i}'
145
+ params[sk] = schema
146
+ params[tk] = table
147
+ parts.append(f'(lower(s.name) = :{sk} and lower(t.name) = :{tk})')
148
+ clauses.append(f'not ({" or ".join(parts)})')
149
+
150
+ if not clauses:
151
+ return '', {}
152
+
153
+ return '\n and ' + '\n and '.join(clauses), params
154
+
155
+
156
+ def _inject_table_filters(sql: str, filters_sql: str) -> str:
157
+ if not filters_sql:
158
+ return sql
159
+ if 'where t.is_ms_shipped = 0' in sql:
160
+ return sql.replace(
161
+ 'where t.is_ms_shipped = 0', f'where t.is_ms_shipped = 0{filters_sql}'
162
+ )
163
+ if "where kc.type = 'PK'" in sql:
164
+ return sql.replace("where kc.type = 'PK'", f"where kc.type = 'PK'{filters_sql}")
165
+ return sql
166
+
167
+
168
+ def get_tables(
169
+ sql_client: UnifiedSQLClient,
170
+ *,
171
+ include_schemas: set[str] | None = None,
172
+ exclude_schemas: set[str] | None = None,
173
+ include_tables: set[str] | None = None,
174
+ exclude_tables: set[str] | None = None,
175
+ ) -> list[TableDef]:
176
+ filters_sql, params = _build_table_filter_sql_and_params(
177
+ include_schemas=include_schemas,
178
+ exclude_schemas=exclude_schemas,
179
+ include_tables=include_tables,
180
+ exclude_tables=exclude_tables,
181
+ )
182
+
183
+ col_rows = sql_client.execute_command_with_result_to_dicts(
184
+ _inject_table_filters(_SQL_TABLE_COLUMNS, filters_sql), params
185
+ )
186
+ pk_rows = sql_client.execute_command_with_result_to_dicts(
187
+ _inject_table_filters(_SQL_PRIMARY_KEYS, filters_sql), params
188
+ )
189
+
190
+ columns_by_table: dict[tuple[str, str], list[TableColumnDef]] = defaultdict(list)
191
+ for r in col_rows:
192
+ col = TableColumnDef(
193
+ schema_name=str(r['schema_name']),
194
+ table_name=str(r['table_name']),
195
+ column_name=str(r['column_name']),
196
+ ordinal_position=int(r['ordinal_position']),
197
+ data_type=str(r['data_type']),
198
+ max_length=None if r['max_length'] is None else int(r['max_length']),
199
+ precision=None if r['precision'] is None else int(r['precision']),
200
+ scale=None if r['scale'] is None else int(r['scale']),
201
+ is_nullable=bool(r['is_nullable']),
202
+ is_identity=bool(r['is_identity']),
203
+ identity_seed=None
204
+ if r['identity_seed'] is None
205
+ else int(r['identity_seed']),
206
+ identity_increment=None
207
+ if r['identity_increment'] is None
208
+ else int(r['identity_increment']),
209
+ is_computed=bool(r['is_computed']),
210
+ computed_definition=None
211
+ if r['computed_definition'] is None
212
+ else str(r['computed_definition']),
213
+ collation_name=None
214
+ if r['collation_name'] is None
215
+ else str(r['collation_name']),
216
+ default_constraint_name=None
217
+ if r.get('default_constraint_name') is None
218
+ else str(r.get('default_constraint_name')),
219
+ default_definition=None
220
+ if r['default_definition'] is None
221
+ else str(r['default_definition']),
222
+ )
223
+ columns_by_table[(col.schema_name, col.table_name)].append(col)
224
+
225
+ pk_by_table: dict[tuple[str, str], PrimaryKeyDef] = {}
226
+ for r in pk_rows:
227
+ pk = PrimaryKeyDef(
228
+ schema_name=str(r['schema_name']),
229
+ table_name=str(r['table_name']),
230
+ constraint_name=str(r['constraint_name']),
231
+ key_columns=str(r['key_columns'] or ''),
232
+ type_desc=str(r['type_desc'] or ''),
233
+ )
234
+ pk_by_table[(pk.schema_name, pk.table_name)] = pk
235
+
236
+ result: list[TableDef] = []
237
+ for key in sorted(
238
+ columns_by_table.keys(), key=lambda x: (x[0].lower(), x[1].lower())
239
+ ):
240
+ schema_name, table_name = key
241
+ cols = sorted(columns_by_table[key], key=lambda c: c.ordinal_position)
242
+ result.append(
243
+ TableDef(
244
+ schema_name=schema_name,
245
+ table_name=table_name,
246
+ columns=cols,
247
+ primary_key=pk_by_table.get((schema_name, table_name)),
248
+ )
249
+ )
250
+
251
+ return result