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.
- al_data_utils/__init__.py +10 -0
- al_data_utils/airflow_utils/__init__.py +13 -0
- al_data_utils/airflow_utils/airflow_utils.py +83 -0
- al_data_utils/azure_as_utils/__init__.py +31 -0
- al_data_utils/azure_as_utils/auth.py +82 -0
- al_data_utils/azure_as_utils/azure_as_utils.py +223 -0
- al_data_utils/azure_as_utils/client.py +158 -0
- al_data_utils/azure_as_utils/errors.py +78 -0
- al_data_utils/azure_as_utils/models.py +265 -0
- al_data_utils/azure_as_utils/poller.py +121 -0
- al_data_utils/backup_utils/__init__.py +9 -0
- al_data_utils/backup_utils/mssql/dacpac/__init__.py +1 -0
- al_data_utils/backup_utils/mssql/dacpac/sqlpackage.py +207 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/__init__.py +8 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/config.py +150 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/emit/__init__.py +11 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/emit/foreign_keys.py +38 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/emit/indexes.py +37 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/emit/sql_objects.py +17 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/emit/tables.py +139 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/__init__.py +19 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/foreign_keys.py +173 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/functions.py +133 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/indexes.py +190 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/sequences.py +180 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/stored_procedures.py +313 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/synonyms.py +135 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/tables.py +251 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/introspect/views.py +244 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/io/__init__.py +10 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/io/layout.py +28 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/io/writer.py +40 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/models/__init__.py +14 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/models/foreign_key_def.py +19 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/models/index_def.py +19 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/models/sql_object_def.py +11 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/models/table_column_def.py +27 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/models/table_def.py +22 -0
- al_data_utils/backup_utils/mssql/schema_snapshot/snapshot_processor.py +754 -0
- al_data_utils/backup_utils/mssql/scripter/__init__.py +1 -0
- al_data_utils/config_utils/__init__.py +6 -0
- al_data_utils/config_utils/config_utils.py +31 -0
- al_data_utils/files_sync_utils/__init__.py +15 -0
- al_data_utils/files_sync_utils/files_sync_utils.py +567 -0
- al_data_utils/geo_utils/__init__.py +0 -0
- al_data_utils/git_utils/__init__.py +27 -0
- al_data_utils/git_utils/git_utils.py +142 -0
- al_data_utils/logging_utils/__init__.py +3 -0
- al_data_utils/logging_utils/logging_utils.py +39 -0
- al_data_utils/ones_api_utils/__init__.py +3 -0
- al_data_utils/ones_api_utils/ones_api_utils.py +61 -0
- al_data_utils/sql_utils/__init__.py +3 -0
- al_data_utils/sql_utils/sql_utils.py +158 -0
- al_data_utils/tg_utils/__init__.py +3 -0
- al_data_utils/tg_utils/tg_utils.py +45 -0
- al_data_utils-0.3.1.dist-info/METADATA +157 -0
- al_data_utils-0.3.1.dist-info/RECORD +60 -0
- al_data_utils-0.3.1.dist-info/WHEEL +5 -0
- al_data_utils-0.3.1.dist-info/licenses/LICENSE +21 -0
- al_data_utils-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from al_data_utils import files_sync_utils
|
|
11
|
+
from al_data_utils.logging_utils import info
|
|
12
|
+
from al_data_utils.sql_utils.sql_utils import UnifiedSQLClient, SQLDialect
|
|
13
|
+
|
|
14
|
+
from .config import SnapshotConfig
|
|
15
|
+
from .emit import emit_index_ddl, emit_table_ddl, emit_sql_object_ddl
|
|
16
|
+
from .introspect import (
|
|
17
|
+
get_indexes,
|
|
18
|
+
get_foreign_keys,
|
|
19
|
+
get_tables,
|
|
20
|
+
get_views,
|
|
21
|
+
get_stored_procedures,
|
|
22
|
+
get_functions,
|
|
23
|
+
get_sequences,
|
|
24
|
+
get_synonyms,
|
|
25
|
+
)
|
|
26
|
+
from .io import (
|
|
27
|
+
table_file_path,
|
|
28
|
+
object_file_path,
|
|
29
|
+
write_text_atomic,
|
|
30
|
+
set_mtime_like,
|
|
31
|
+
read_text_if_exists,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run_id() -> str:
|
|
36
|
+
return datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _safe_path_token(value: str) -> str:
|
|
40
|
+
# Keep only characters that are safe and portable across Windows/Linux filesystems.
|
|
41
|
+
token = re.sub(r'[^A-Za-z0-9._-]+', '_', value.strip())
|
|
42
|
+
token = token.strip('._-')
|
|
43
|
+
return token[:80] if token else 'default'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_folder_name(cfg: SnapshotConfig) -> str:
|
|
47
|
+
rid = _run_id()
|
|
48
|
+
if cfg.staging_include_run_guid:
|
|
49
|
+
return f'{rid}_{uuid4().hex[:8]}'
|
|
50
|
+
return rid
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _run_root(*, staging_root: Path, cfg: SnapshotConfig) -> Path:
|
|
54
|
+
base = staging_root
|
|
55
|
+
if cfg.staging_group_by_database and cfg.staging_database_name:
|
|
56
|
+
base = base / _safe_path_token(cfg.staging_database_name)
|
|
57
|
+
return base / _run_folder_name(cfg)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _default_staging_root(output_root: Path) -> Path:
|
|
61
|
+
return Path(str(output_root) + '__staging')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _batch_sep(cfg: SnapshotConfig) -> str:
|
|
65
|
+
return cfg.batch_separator.strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _join_blocks(blocks: list[str], cfg: SnapshotConfig) -> str:
|
|
69
|
+
sep = _batch_sep(cfg)
|
|
70
|
+
nl = cfg.newline
|
|
71
|
+
if not blocks:
|
|
72
|
+
return ''
|
|
73
|
+
if sep:
|
|
74
|
+
return (f'{nl}{sep}{nl}{nl}').join(blocks) + f'{nl}{sep}{nl}'
|
|
75
|
+
return (f'{nl}{nl}').join(blocks) + nl
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_template_override(
|
|
79
|
+
*, cfg: SnapshotConfig, file_name: str, object_folder: str | None = None
|
|
80
|
+
) -> str | None:
|
|
81
|
+
if not cfg.prefer_template_files:
|
|
82
|
+
return None
|
|
83
|
+
root = cfg.datagrip_templates_root
|
|
84
|
+
if root is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
folder_candidates: list[str] = []
|
|
88
|
+
if object_folder:
|
|
89
|
+
folder_candidates.append(object_folder)
|
|
90
|
+
# Backward-compat alias between old and new naming.
|
|
91
|
+
if object_folder == 'routines':
|
|
92
|
+
folder_candidates.append('stored_procedures')
|
|
93
|
+
if object_folder == 'stored_procedures':
|
|
94
|
+
folder_candidates.append('routines')
|
|
95
|
+
|
|
96
|
+
# 1) Exact folder + file path.
|
|
97
|
+
for folder in folder_candidates:
|
|
98
|
+
candidate = root / folder / file_name
|
|
99
|
+
if candidate.exists() and candidate.is_file():
|
|
100
|
+
return candidate.read_text(encoding=cfg.encoding)
|
|
101
|
+
|
|
102
|
+
# 2) Legacy flat root path.
|
|
103
|
+
candidate = root / file_name
|
|
104
|
+
if candidate.exists() and candidate.is_file():
|
|
105
|
+
return candidate.read_text(encoding=cfg.encoding)
|
|
106
|
+
|
|
107
|
+
lower_name = file_name.lower()
|
|
108
|
+
try:
|
|
109
|
+
# 3) Recursive search, folder-scoped first.
|
|
110
|
+
for folder in folder_candidates:
|
|
111
|
+
folder_path = root / folder
|
|
112
|
+
if not folder_path.exists() or not folder_path.is_dir():
|
|
113
|
+
continue
|
|
114
|
+
for p in folder_path.rglob('*.sql'):
|
|
115
|
+
if p.name.lower() == lower_name:
|
|
116
|
+
return p.read_text(encoding=cfg.encoding)
|
|
117
|
+
|
|
118
|
+
# 4) Recursive fallback across template root.
|
|
119
|
+
for p in root.rglob('*.sql'):
|
|
120
|
+
if p.name.lower() == lower_name:
|
|
121
|
+
return p.read_text(encoding=cfg.encoding)
|
|
122
|
+
except Exception:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _iter_template_folder_files(
|
|
129
|
+
*, cfg: SnapshotConfig, object_folder: str
|
|
130
|
+
) -> list[Path]:
|
|
131
|
+
root = cfg.datagrip_templates_root
|
|
132
|
+
if root is None:
|
|
133
|
+
return []
|
|
134
|
+
folder = root / object_folder
|
|
135
|
+
if not folder.exists() or not folder.is_dir():
|
|
136
|
+
return []
|
|
137
|
+
return sorted(folder.glob('*.sql'))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_schema_object_from_file_name(file_name: str) -> tuple[str, str] | None:
|
|
141
|
+
name = file_name.strip()
|
|
142
|
+
if not name.lower().endswith('.sql'):
|
|
143
|
+
return None
|
|
144
|
+
stem = name[:-4]
|
|
145
|
+
if '.' not in stem:
|
|
146
|
+
return None
|
|
147
|
+
schema, object_name = stem.split('.', 1)
|
|
148
|
+
schema = schema.strip()
|
|
149
|
+
object_name = object_name.strip()
|
|
150
|
+
if not schema or not object_name:
|
|
151
|
+
return None
|
|
152
|
+
return schema, object_name
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _render_table_file(
|
|
156
|
+
*, schema: str, table: str, table_ddl: str, indexes: list[str], cfg: SnapshotConfig
|
|
157
|
+
) -> str:
|
|
158
|
+
nl = cfg.newline
|
|
159
|
+
lines: list[str] = []
|
|
160
|
+
|
|
161
|
+
if cfg.header_comment:
|
|
162
|
+
lines.append(cfg.header_comment)
|
|
163
|
+
if cfg.section_comments:
|
|
164
|
+
lines.append(f'-- table: {schema}.{table}')
|
|
165
|
+
lines.append('')
|
|
166
|
+
|
|
167
|
+
if table_ddl:
|
|
168
|
+
if cfg.section_comments:
|
|
169
|
+
lines.append('-- === table ===')
|
|
170
|
+
lines.append(table_ddl.strip())
|
|
171
|
+
lines.append('')
|
|
172
|
+
|
|
173
|
+
if indexes:
|
|
174
|
+
if cfg.section_comments:
|
|
175
|
+
lines.append('-- === indexes ===')
|
|
176
|
+
lines.append(_join_blocks(indexes, cfg).rstrip(nl))
|
|
177
|
+
lines.append('')
|
|
178
|
+
|
|
179
|
+
return nl.join(lines).strip() + nl
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _render_sql_object_file(
|
|
183
|
+
*,
|
|
184
|
+
schema: str,
|
|
185
|
+
object_name: str,
|
|
186
|
+
object_kind: str,
|
|
187
|
+
object_ddl: str,
|
|
188
|
+
cfg: SnapshotConfig,
|
|
189
|
+
) -> str:
|
|
190
|
+
nl = cfg.newline
|
|
191
|
+
lines: list[str] = []
|
|
192
|
+
|
|
193
|
+
if cfg.header_comment:
|
|
194
|
+
lines.append(cfg.header_comment)
|
|
195
|
+
if cfg.section_comments:
|
|
196
|
+
lines.append(f'-- {object_kind}: {schema}.{object_name}')
|
|
197
|
+
lines.append('')
|
|
198
|
+
lines.append(object_ddl.strip())
|
|
199
|
+
|
|
200
|
+
sep = _batch_sep(cfg)
|
|
201
|
+
if sep:
|
|
202
|
+
lines.append('')
|
|
203
|
+
lines.append(sep)
|
|
204
|
+
|
|
205
|
+
return nl.join(lines).strip() + nl
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _write_object_file(
|
|
209
|
+
*, text: str, staging_path: Path, dest_path: Path, cfg: SnapshotConfig
|
|
210
|
+
) -> None:
|
|
211
|
+
write_text_atomic(staging_path, text, encoding=cfg.encoding, newline=cfg.newline)
|
|
212
|
+
|
|
213
|
+
# keep mtime aligned when content matches so files_sync_utils doesn't "update"
|
|
214
|
+
dest_text = read_text_if_exists(dest_path, encoding=cfg.encoding)
|
|
215
|
+
if dest_text is not None:
|
|
216
|
+
norm_dest = dest_text.replace('\r\n', '\n').replace('\r', '\n')
|
|
217
|
+
norm_new = text.replace('\r\n', '\n').replace('\r', '\n')
|
|
218
|
+
if norm_dest == norm_new:
|
|
219
|
+
set_mtime_like(staging_path, dest_path)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def snapshot_schema(
|
|
223
|
+
*, sql_client: UnifiedSQLClient, config: SnapshotConfig
|
|
224
|
+
) -> list[Path]:
|
|
225
|
+
cfg = config
|
|
226
|
+
_info = info if cfg.verbose else (lambda _: None)
|
|
227
|
+
output_root = cfg.output_root
|
|
228
|
+
staging_root = cfg.staging_root or _default_staging_root(output_root)
|
|
229
|
+
run_root = _run_root(staging_root=staging_root, cfg=cfg)
|
|
230
|
+
|
|
231
|
+
_info(f'[schema_snapshot..] run root: {run_root}')
|
|
232
|
+
_info(
|
|
233
|
+
'[schema_snapshot..] collecting metadata (tables, indexes, foreign keys, views, procedures)...'
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if cfg.include_table_definitions:
|
|
237
|
+
_info('[schema_snapshot..] metadata step: fetching tables (filtered SQL query)')
|
|
238
|
+
table_defs = get_tables(
|
|
239
|
+
sql_client,
|
|
240
|
+
include_schemas=cfg.include_schemas,
|
|
241
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
242
|
+
include_tables=cfg.include_tables,
|
|
243
|
+
exclude_tables=cfg.exclude_tables,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
table_defs = []
|
|
247
|
+
|
|
248
|
+
if cfg.include_indexes:
|
|
249
|
+
_info('[schema_snapshot..] metadata step: fetching indexes (filtered SQL query)')
|
|
250
|
+
indexes = get_indexes(
|
|
251
|
+
sql_client,
|
|
252
|
+
include_schemas=cfg.include_schemas,
|
|
253
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
254
|
+
include_tables=cfg.include_tables,
|
|
255
|
+
exclude_tables=cfg.exclude_tables,
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
indexes = []
|
|
259
|
+
|
|
260
|
+
if cfg.include_foreign_keys:
|
|
261
|
+
_info(
|
|
262
|
+
'[schema_snapshot..] metadata step: fetching foreign keys (filtered SQL query)'
|
|
263
|
+
)
|
|
264
|
+
fks = get_foreign_keys(
|
|
265
|
+
sql_client,
|
|
266
|
+
include_schemas=cfg.include_schemas,
|
|
267
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
268
|
+
include_tables=cfg.include_tables,
|
|
269
|
+
exclude_tables=cfg.exclude_tables,
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
fks = []
|
|
273
|
+
|
|
274
|
+
if cfg.include_views:
|
|
275
|
+
_info('[schema_snapshot..] metadata step: fetching views (filtered SQL query)')
|
|
276
|
+
views = get_views(
|
|
277
|
+
sql_client,
|
|
278
|
+
include_schemas=cfg.include_schemas,
|
|
279
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
280
|
+
include_views=cfg.include_views_objects,
|
|
281
|
+
exclude_views=cfg.exclude_views_objects,
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
views = []
|
|
285
|
+
|
|
286
|
+
if cfg.include_stored_procedures:
|
|
287
|
+
_info(
|
|
288
|
+
'[schema_snapshot..] metadata step: fetching stored procedures '
|
|
289
|
+
'(filtered SQL query/per-object definitions)'
|
|
290
|
+
)
|
|
291
|
+
sprocs = get_stored_procedures(
|
|
292
|
+
sql_client,
|
|
293
|
+
include_schemas=cfg.include_schemas,
|
|
294
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
295
|
+
include_objects=cfg.include_stored_procedures_objects,
|
|
296
|
+
exclude_objects=cfg.exclude_stored_procedures_objects,
|
|
297
|
+
verbose=cfg.verbose,
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
sprocs = []
|
|
301
|
+
|
|
302
|
+
if cfg.include_functions:
|
|
303
|
+
_info(
|
|
304
|
+
'[schema_snapshot..] metadata step: fetching functions (filtered SQL query)'
|
|
305
|
+
)
|
|
306
|
+
funcs = get_functions(
|
|
307
|
+
sql_client,
|
|
308
|
+
include_schemas=cfg.include_schemas,
|
|
309
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
310
|
+
include_objects=cfg.include_functions_objects,
|
|
311
|
+
exclude_objects=cfg.exclude_functions_objects,
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
funcs = []
|
|
315
|
+
|
|
316
|
+
if cfg.include_sequences:
|
|
317
|
+
_info(
|
|
318
|
+
'[schema_snapshot..] metadata step: fetching sequences (filtered SQL query)'
|
|
319
|
+
)
|
|
320
|
+
sequences = get_sequences(
|
|
321
|
+
sql_client,
|
|
322
|
+
include_schemas=cfg.include_schemas,
|
|
323
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
324
|
+
include_objects=cfg.include_sequences_objects,
|
|
325
|
+
exclude_objects=cfg.exclude_sequences_objects,
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
sequences = []
|
|
329
|
+
|
|
330
|
+
if cfg.include_synonyms:
|
|
331
|
+
_info(
|
|
332
|
+
'[schema_snapshot..] metadata step: fetching synonyms (filtered SQL query)'
|
|
333
|
+
)
|
|
334
|
+
synonyms = get_synonyms(
|
|
335
|
+
sql_client,
|
|
336
|
+
include_schemas=cfg.include_schemas,
|
|
337
|
+
exclude_schemas=cfg.exclude_schemas,
|
|
338
|
+
include_objects=cfg.include_synonyms_objects,
|
|
339
|
+
exclude_objects=cfg.exclude_synonyms_objects,
|
|
340
|
+
)
|
|
341
|
+
else:
|
|
342
|
+
synonyms = []
|
|
343
|
+
|
|
344
|
+
_info(
|
|
345
|
+
'[schema_snapshot..] metadata counts: '
|
|
346
|
+
f'tables={len(table_defs)}, indexes={len(indexes)}, '
|
|
347
|
+
f'foreign_keys={len(fks)}, views={len(views)}, '
|
|
348
|
+
f'procedures={len(sprocs)}, functions={len(funcs)}, '
|
|
349
|
+
f'sequences={len(sequences)}, synonyms={len(synonyms)}'
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
idx_by_table: dict[tuple[str, str], list[str]] = defaultdict(list)
|
|
353
|
+
for idx in indexes:
|
|
354
|
+
if idx.is_hypothetical:
|
|
355
|
+
continue
|
|
356
|
+
if not cfg.is_schema_allowed(idx.schema_name):
|
|
357
|
+
continue
|
|
358
|
+
if not cfg.is_table_allowed(idx.schema_name, idx.table_name):
|
|
359
|
+
continue
|
|
360
|
+
idx_by_table[(idx.schema_name, idx.table_name)].append(emit_index_ddl(idx))
|
|
361
|
+
|
|
362
|
+
fk_by_table: dict[tuple[str, str], list] = defaultdict(list)
|
|
363
|
+
for fk in fks:
|
|
364
|
+
if not cfg.is_schema_allowed(fk.parent_schema):
|
|
365
|
+
continue
|
|
366
|
+
if not cfg.is_table_allowed(fk.parent_schema, fk.parent_table):
|
|
367
|
+
continue
|
|
368
|
+
fk_by_table[(fk.parent_schema, fk.parent_table)].append(fk)
|
|
369
|
+
|
|
370
|
+
tables_by_key = {
|
|
371
|
+
(t.schema_name, t.table_name): t
|
|
372
|
+
for t in table_defs
|
|
373
|
+
if cfg.is_schema_allowed(t.schema_name)
|
|
374
|
+
and cfg.is_table_allowed(t.schema_name, t.table_name)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
all_tables = sorted(
|
|
378
|
+
set(tables_by_key.keys()) | set(idx_by_table.keys()) | set(fk_by_table.keys()),
|
|
379
|
+
key=lambda x: (x[0].lower(), x[1].lower()),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
written_paths: list[Path] = []
|
|
383
|
+
|
|
384
|
+
_info(f'[schema_snapshot..] writing table files: {len(all_tables)}')
|
|
385
|
+
|
|
386
|
+
for schema, table in all_tables:
|
|
387
|
+
_info(f'[schema_snapshot..] extracting table: {schema}.{table}')
|
|
388
|
+
text = _read_template_override(
|
|
389
|
+
cfg=cfg, file_name=f'{schema}.{table}.sql', object_folder='tables'
|
|
390
|
+
)
|
|
391
|
+
if text is None:
|
|
392
|
+
table_def = tables_by_key.get((schema, table))
|
|
393
|
+
table_ddl = (
|
|
394
|
+
emit_table_ddl(
|
|
395
|
+
table_def, foreign_keys=fk_by_table.get((schema, table), [])
|
|
396
|
+
)
|
|
397
|
+
if table_def is not None
|
|
398
|
+
else ''
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
idx_list = sorted(idx_by_table.get((schema, table), []))
|
|
402
|
+
|
|
403
|
+
text = _render_table_file(
|
|
404
|
+
schema=schema,
|
|
405
|
+
table=table,
|
|
406
|
+
table_ddl=table_ddl,
|
|
407
|
+
indexes=idx_list,
|
|
408
|
+
cfg=cfg,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
staging_path = table_file_path(
|
|
412
|
+
root=run_root,
|
|
413
|
+
root_folder_name=cfg.root_folder_name,
|
|
414
|
+
schema=schema,
|
|
415
|
+
table=table,
|
|
416
|
+
)
|
|
417
|
+
dest_path = table_file_path(
|
|
418
|
+
root=output_root,
|
|
419
|
+
root_folder_name=cfg.root_folder_name,
|
|
420
|
+
schema=schema,
|
|
421
|
+
table=table,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
_write_object_file(
|
|
425
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
426
|
+
)
|
|
427
|
+
_info(f'[schema_snapshot..] wrote table file: {dest_path}')
|
|
428
|
+
written_paths.append(dest_path)
|
|
429
|
+
|
|
430
|
+
filtered_views = [
|
|
431
|
+
v
|
|
432
|
+
for v in views
|
|
433
|
+
if cfg.is_schema_allowed(v.schema_name)
|
|
434
|
+
and cfg.is_view_allowed(v.schema_name, v.object_name)
|
|
435
|
+
]
|
|
436
|
+
_info(f'[schema_snapshot..] writing view files: {len(filtered_views)}')
|
|
437
|
+
for v in sorted(
|
|
438
|
+
filtered_views, key=lambda x: (x.schema_name.lower(), x.object_name.lower())
|
|
439
|
+
):
|
|
440
|
+
_info(f'[schema_snapshot..] extracting view: {v.schema_name}.{v.object_name}')
|
|
441
|
+
text = _read_template_override(
|
|
442
|
+
cfg=cfg,
|
|
443
|
+
file_name=f'{v.schema_name}.{v.object_name}.sql',
|
|
444
|
+
object_folder='views',
|
|
445
|
+
)
|
|
446
|
+
if text is None:
|
|
447
|
+
text = _render_sql_object_file(
|
|
448
|
+
schema=v.schema_name,
|
|
449
|
+
object_name=v.object_name,
|
|
450
|
+
object_kind='view',
|
|
451
|
+
object_ddl=emit_sql_object_ddl(v),
|
|
452
|
+
cfg=cfg,
|
|
453
|
+
)
|
|
454
|
+
staging_path = object_file_path(
|
|
455
|
+
root=run_root,
|
|
456
|
+
root_folder_name=cfg.root_folder_name,
|
|
457
|
+
schema=v.schema_name,
|
|
458
|
+
object_folder='views',
|
|
459
|
+
object_name=v.object_name,
|
|
460
|
+
)
|
|
461
|
+
dest_path = object_file_path(
|
|
462
|
+
root=output_root,
|
|
463
|
+
root_folder_name=cfg.root_folder_name,
|
|
464
|
+
schema=v.schema_name,
|
|
465
|
+
object_folder='views',
|
|
466
|
+
object_name=v.object_name,
|
|
467
|
+
)
|
|
468
|
+
_write_object_file(
|
|
469
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
470
|
+
)
|
|
471
|
+
_info(f'[schema_snapshot..] wrote view file: {dest_path}')
|
|
472
|
+
written_paths.append(dest_path)
|
|
473
|
+
|
|
474
|
+
filtered_sprocs = [
|
|
475
|
+
p
|
|
476
|
+
for p in sprocs
|
|
477
|
+
if cfg.is_schema_allowed(p.schema_name)
|
|
478
|
+
and cfg.is_stored_procedure_allowed(p.schema_name, p.object_name)
|
|
479
|
+
]
|
|
480
|
+
filtered_funcs = [
|
|
481
|
+
f
|
|
482
|
+
for f in funcs
|
|
483
|
+
if cfg.is_schema_allowed(f.schema_name)
|
|
484
|
+
and cfg.is_function_allowed(f.schema_name, f.object_name)
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
total_routines = len(filtered_sprocs) + len(filtered_funcs)
|
|
488
|
+
_info(f'[schema_snapshot..] writing routines files: {total_routines}')
|
|
489
|
+
|
|
490
|
+
for f in sorted(
|
|
491
|
+
filtered_funcs, key=lambda x: (x.schema_name.lower(), x.object_name.lower())
|
|
492
|
+
):
|
|
493
|
+
_info(f'[schema_snapshot..] extracting routine: {f.schema_name}.{f.object_name}')
|
|
494
|
+
text = _read_template_override(
|
|
495
|
+
cfg=cfg,
|
|
496
|
+
file_name=f'{f.schema_name}.{f.object_name}.sql',
|
|
497
|
+
object_folder='routines',
|
|
498
|
+
)
|
|
499
|
+
if text is None:
|
|
500
|
+
text = _render_sql_object_file(
|
|
501
|
+
schema=f.schema_name,
|
|
502
|
+
object_name=f.object_name,
|
|
503
|
+
object_kind='routine',
|
|
504
|
+
object_ddl=emit_sql_object_ddl(f),
|
|
505
|
+
cfg=cfg,
|
|
506
|
+
)
|
|
507
|
+
staging_path = object_file_path(
|
|
508
|
+
root=run_root,
|
|
509
|
+
root_folder_name=cfg.root_folder_name,
|
|
510
|
+
schema=f.schema_name,
|
|
511
|
+
object_folder='routines',
|
|
512
|
+
object_name=f.object_name,
|
|
513
|
+
)
|
|
514
|
+
dest_path = object_file_path(
|
|
515
|
+
root=output_root,
|
|
516
|
+
root_folder_name=cfg.root_folder_name,
|
|
517
|
+
schema=f.schema_name,
|
|
518
|
+
object_folder='routines',
|
|
519
|
+
object_name=f.object_name,
|
|
520
|
+
)
|
|
521
|
+
_write_object_file(
|
|
522
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
523
|
+
)
|
|
524
|
+
_info(f'[schema_snapshot..] wrote routine file: {dest_path}')
|
|
525
|
+
written_paths.append(dest_path)
|
|
526
|
+
|
|
527
|
+
for p in sorted(
|
|
528
|
+
filtered_sprocs, key=lambda x: (x.schema_name.lower(), x.object_name.lower())
|
|
529
|
+
):
|
|
530
|
+
_info(f'[schema_snapshot..] extracting routine: {p.schema_name}.{p.object_name}')
|
|
531
|
+
text = _read_template_override(
|
|
532
|
+
cfg=cfg,
|
|
533
|
+
file_name=f'{p.schema_name}.{p.object_name}.sql',
|
|
534
|
+
object_folder='routines',
|
|
535
|
+
)
|
|
536
|
+
if text is None:
|
|
537
|
+
text = _render_sql_object_file(
|
|
538
|
+
schema=p.schema_name,
|
|
539
|
+
object_name=p.object_name,
|
|
540
|
+
object_kind='stored_procedure',
|
|
541
|
+
object_ddl=emit_sql_object_ddl(p),
|
|
542
|
+
cfg=cfg,
|
|
543
|
+
)
|
|
544
|
+
staging_path = object_file_path(
|
|
545
|
+
root=run_root,
|
|
546
|
+
root_folder_name=cfg.root_folder_name,
|
|
547
|
+
schema=p.schema_name,
|
|
548
|
+
object_folder='routines',
|
|
549
|
+
object_name=p.object_name,
|
|
550
|
+
)
|
|
551
|
+
dest_path = object_file_path(
|
|
552
|
+
root=output_root,
|
|
553
|
+
root_folder_name=cfg.root_folder_name,
|
|
554
|
+
schema=p.schema_name,
|
|
555
|
+
object_folder='routines',
|
|
556
|
+
object_name=p.object_name,
|
|
557
|
+
)
|
|
558
|
+
_write_object_file(
|
|
559
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
560
|
+
)
|
|
561
|
+
_info(f'[schema_snapshot..] wrote routine file: {dest_path}')
|
|
562
|
+
written_paths.append(dest_path)
|
|
563
|
+
|
|
564
|
+
filtered_sequences = [
|
|
565
|
+
s
|
|
566
|
+
for s in sequences
|
|
567
|
+
if cfg.is_schema_allowed(s.schema_name)
|
|
568
|
+
and cfg.is_sequence_allowed(s.schema_name, s.object_name)
|
|
569
|
+
]
|
|
570
|
+
_info(f'[schema_snapshot..] writing sequences files: {len(filtered_sequences)}')
|
|
571
|
+
for s in sorted(
|
|
572
|
+
filtered_sequences, key=lambda x: (x.schema_name.lower(), x.object_name.lower())
|
|
573
|
+
):
|
|
574
|
+
_info(
|
|
575
|
+
f'[schema_snapshot..] extracting sequence: {s.schema_name}.{s.object_name}'
|
|
576
|
+
)
|
|
577
|
+
text = _read_template_override(
|
|
578
|
+
cfg=cfg,
|
|
579
|
+
file_name=f'{s.schema_name}.{s.object_name}.sql',
|
|
580
|
+
object_folder='sequences',
|
|
581
|
+
)
|
|
582
|
+
if text is None:
|
|
583
|
+
text = _render_sql_object_file(
|
|
584
|
+
schema=s.schema_name,
|
|
585
|
+
object_name=s.object_name,
|
|
586
|
+
object_kind='sequence',
|
|
587
|
+
object_ddl=emit_sql_object_ddl(s),
|
|
588
|
+
cfg=cfg,
|
|
589
|
+
)
|
|
590
|
+
staging_path = object_file_path(
|
|
591
|
+
root=run_root,
|
|
592
|
+
root_folder_name=cfg.root_folder_name,
|
|
593
|
+
schema=s.schema_name,
|
|
594
|
+
object_folder='sequences',
|
|
595
|
+
object_name=s.object_name,
|
|
596
|
+
)
|
|
597
|
+
dest_path = object_file_path(
|
|
598
|
+
root=output_root,
|
|
599
|
+
root_folder_name=cfg.root_folder_name,
|
|
600
|
+
schema=s.schema_name,
|
|
601
|
+
object_folder='sequences',
|
|
602
|
+
object_name=s.object_name,
|
|
603
|
+
)
|
|
604
|
+
_write_object_file(
|
|
605
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
606
|
+
)
|
|
607
|
+
_info(f'[schema_snapshot..] wrote sequence file: {dest_path}')
|
|
608
|
+
written_paths.append(dest_path)
|
|
609
|
+
|
|
610
|
+
filtered_synonyms = [
|
|
611
|
+
s
|
|
612
|
+
for s in synonyms
|
|
613
|
+
if cfg.is_schema_allowed(s.schema_name)
|
|
614
|
+
and cfg.is_synonym_allowed(s.schema_name, s.object_name)
|
|
615
|
+
]
|
|
616
|
+
_info(f'[schema_snapshot..] writing synonyms files: {len(filtered_synonyms)}')
|
|
617
|
+
for s in sorted(
|
|
618
|
+
filtered_synonyms, key=lambda x: (x.schema_name.lower(), x.object_name.lower())
|
|
619
|
+
):
|
|
620
|
+
_info(f'[schema_snapshot..] extracting synonym: {s.schema_name}.{s.object_name}')
|
|
621
|
+
text = _read_template_override(
|
|
622
|
+
cfg=cfg,
|
|
623
|
+
file_name=f'{s.schema_name}.{s.object_name}.sql',
|
|
624
|
+
object_folder='synonyms',
|
|
625
|
+
)
|
|
626
|
+
if text is None:
|
|
627
|
+
text = _render_sql_object_file(
|
|
628
|
+
schema=s.schema_name,
|
|
629
|
+
object_name=s.object_name,
|
|
630
|
+
object_kind='synonym',
|
|
631
|
+
object_ddl=emit_sql_object_ddl(s),
|
|
632
|
+
cfg=cfg,
|
|
633
|
+
)
|
|
634
|
+
staging_path = object_file_path(
|
|
635
|
+
root=run_root,
|
|
636
|
+
root_folder_name=cfg.root_folder_name,
|
|
637
|
+
schema=s.schema_name,
|
|
638
|
+
object_folder='synonyms',
|
|
639
|
+
object_name=s.object_name,
|
|
640
|
+
)
|
|
641
|
+
dest_path = object_file_path(
|
|
642
|
+
root=output_root,
|
|
643
|
+
root_folder_name=cfg.root_folder_name,
|
|
644
|
+
schema=s.schema_name,
|
|
645
|
+
object_folder='synonyms',
|
|
646
|
+
object_name=s.object_name,
|
|
647
|
+
)
|
|
648
|
+
_write_object_file(
|
|
649
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
650
|
+
)
|
|
651
|
+
_info(f'[schema_snapshot..] wrote synonym file: {dest_path}')
|
|
652
|
+
written_paths.append(dest_path)
|
|
653
|
+
|
|
654
|
+
# Optional passthrough: copy template-only SQL files from dedicated folders.
|
|
655
|
+
# This allows extra object types (e.g., sequences/synonyms/routines functions)
|
|
656
|
+
# to be produced even when they are not introspected from the database yet.
|
|
657
|
+
if cfg.prefer_template_files and cfg.datagrip_templates_root is not None:
|
|
658
|
+
written_set = {str(p).lower() for p in written_paths}
|
|
659
|
+
for folder_name in ('tables', 'views', 'routines', 'sequences', 'synonyms'):
|
|
660
|
+
for template_path in _iter_template_folder_files(
|
|
661
|
+
cfg=cfg, object_folder=folder_name
|
|
662
|
+
):
|
|
663
|
+
parsed = _parse_schema_object_from_file_name(template_path.name)
|
|
664
|
+
if parsed is None:
|
|
665
|
+
continue
|
|
666
|
+
|
|
667
|
+
schema, object_name = parsed
|
|
668
|
+
if folder_name == 'tables':
|
|
669
|
+
dest_path = table_file_path(
|
|
670
|
+
root=output_root,
|
|
671
|
+
root_folder_name=cfg.root_folder_name,
|
|
672
|
+
schema=schema,
|
|
673
|
+
table=object_name,
|
|
674
|
+
)
|
|
675
|
+
staging_path = table_file_path(
|
|
676
|
+
root=run_root,
|
|
677
|
+
root_folder_name=cfg.root_folder_name,
|
|
678
|
+
schema=schema,
|
|
679
|
+
table=object_name,
|
|
680
|
+
)
|
|
681
|
+
else:
|
|
682
|
+
dest_path = object_file_path(
|
|
683
|
+
root=output_root,
|
|
684
|
+
root_folder_name=cfg.root_folder_name,
|
|
685
|
+
schema=schema,
|
|
686
|
+
object_folder=folder_name,
|
|
687
|
+
object_name=object_name,
|
|
688
|
+
)
|
|
689
|
+
staging_path = object_file_path(
|
|
690
|
+
root=run_root,
|
|
691
|
+
root_folder_name=cfg.root_folder_name,
|
|
692
|
+
schema=schema,
|
|
693
|
+
object_folder=folder_name,
|
|
694
|
+
object_name=object_name,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
key = str(dest_path).lower()
|
|
698
|
+
text = template_path.read_text(encoding=cfg.encoding)
|
|
699
|
+
_write_object_file(
|
|
700
|
+
text=text, staging_path=staging_path, dest_path=dest_path, cfg=cfg
|
|
701
|
+
)
|
|
702
|
+
_info(
|
|
703
|
+
f'[schema_snapshot..] wrote template passthrough file: {dest_path}'
|
|
704
|
+
)
|
|
705
|
+
written_paths.append(dest_path)
|
|
706
|
+
written_set.add(key)
|
|
707
|
+
|
|
708
|
+
# sync staging -> destination using existing module (unchanged)
|
|
709
|
+
sync_cfg = files_sync_utils.SyncConfig(
|
|
710
|
+
source_paths=[str(run_root)],
|
|
711
|
+
target_path=str(output_root),
|
|
712
|
+
log_folder=str(cfg.sync_log_folder or (output_root / 'sync_logs')),
|
|
713
|
+
copy_folders_names_to_skip=[],
|
|
714
|
+
copy_files_names_to_skip=[],
|
|
715
|
+
delete_folders_names_to_skip=[],
|
|
716
|
+
delete_files_names_to_skip=[],
|
|
717
|
+
delete_folders_names_to_try=[],
|
|
718
|
+
delete_files_names_to_try=[],
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
plan_df = files_sync_utils.build_plan_df(sync_cfg)
|
|
722
|
+
_info('[schema_snapshot..] syncing staging -> output...')
|
|
723
|
+
executed_df, summary = files_sync_utils.execute_plan(plan_df)
|
|
724
|
+
files_sync_utils.log_activity(executed_df, summary, sync_cfg.log_folder)
|
|
725
|
+
if cfg.sync_log_retention_days is not None and cfg.sync_log_retention_days >= 0:
|
|
726
|
+
files_sync_utils.log_folder_retention(
|
|
727
|
+
sync_cfg.log_folder, retain_days=cfg.sync_log_retention_days
|
|
728
|
+
)
|
|
729
|
+
_info('[schema_snapshot..] sync completed')
|
|
730
|
+
|
|
731
|
+
if cfg.cleanup_staging:
|
|
732
|
+
try:
|
|
733
|
+
import shutil
|
|
734
|
+
|
|
735
|
+
shutil.rmtree(run_root, ignore_errors=True)
|
|
736
|
+
_info('[schema_snapshot..] staging cleaned up')
|
|
737
|
+
except Exception:
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
_info(f'[schema_snapshot..] done. written files: {len(written_paths)}')
|
|
741
|
+
return written_paths
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def snapshot_schema_from_params(
|
|
745
|
+
*, mssql_params: dict, config: SnapshotConfig
|
|
746
|
+
) -> list[Path]:
|
|
747
|
+
cfg = config
|
|
748
|
+
if not cfg.staging_database_name:
|
|
749
|
+
db_name = mssql_params.get('database')
|
|
750
|
+
if isinstance(db_name, str) and db_name.strip():
|
|
751
|
+
cfg = replace(cfg, staging_database_name=db_name.strip())
|
|
752
|
+
|
|
753
|
+
sql_client = UnifiedSQLClient(SQLDialect.MSSQL, mssql_params)
|
|
754
|
+
return snapshot_schema(sql_client=sql_client, config=cfg)
|