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,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class SnapshotConfig:
|
|
9
|
+
"""Configuration for MSSQL schema snapshot.
|
|
10
|
+
|
|
11
|
+
Notes:
|
|
12
|
+
- output_root: final destination folder.
|
|
13
|
+
- staging_root: if None, staging folder is created as sibling: f"{output_root}__staging".
|
|
14
|
+
- snapshot writes into staging first, then syncs staging -> output_root using files_sync_utils.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
output_root: Path
|
|
18
|
+
staging_root: Path | None = None
|
|
19
|
+
cleanup_staging: bool = True
|
|
20
|
+
staging_database_name: str | None = None
|
|
21
|
+
staging_group_by_database: bool = True
|
|
22
|
+
staging_include_run_guid: bool = True
|
|
23
|
+
sync_log_folder: Path | None = None
|
|
24
|
+
sync_log_retention_days: int | None = 30
|
|
25
|
+
verbose: bool = True
|
|
26
|
+
|
|
27
|
+
include_indexes: bool = True
|
|
28
|
+
include_foreign_keys: bool = True
|
|
29
|
+
include_table_definitions: bool = True
|
|
30
|
+
include_views: bool = True
|
|
31
|
+
include_stored_procedures: bool = True
|
|
32
|
+
include_functions: bool = True
|
|
33
|
+
include_sequences: bool = True
|
|
34
|
+
include_synonyms: bool = True
|
|
35
|
+
|
|
36
|
+
root_folder_name: str = 'schemas'
|
|
37
|
+
|
|
38
|
+
batch_separator: str = 'go'
|
|
39
|
+
header_comment: str = ''
|
|
40
|
+
section_comments: bool = False
|
|
41
|
+
newline: str = '\n'
|
|
42
|
+
encoding: str = 'utf-8'
|
|
43
|
+
|
|
44
|
+
datagrip_templates_root: Path | None = None
|
|
45
|
+
prefer_template_files: bool = True
|
|
46
|
+
|
|
47
|
+
include_schemas: set[str] | None = None
|
|
48
|
+
exclude_schemas: set[str] | None = None
|
|
49
|
+
|
|
50
|
+
include_tables: set[str] | None = None
|
|
51
|
+
exclude_tables: set[str] | None = None
|
|
52
|
+
|
|
53
|
+
include_views_objects: set[str] | None = None
|
|
54
|
+
exclude_views_objects: set[str] | None = None
|
|
55
|
+
|
|
56
|
+
include_stored_procedures_objects: set[str] | None = None
|
|
57
|
+
exclude_stored_procedures_objects: set[str] | None = None
|
|
58
|
+
|
|
59
|
+
include_functions_objects: set[str] | None = None
|
|
60
|
+
exclude_functions_objects: set[str] | None = None
|
|
61
|
+
|
|
62
|
+
include_sequences_objects: set[str] | None = None
|
|
63
|
+
exclude_sequences_objects: set[str] | None = None
|
|
64
|
+
|
|
65
|
+
include_synonyms_objects: set[str] | None = None
|
|
66
|
+
exclude_synonyms_objects: set[str] | None = None
|
|
67
|
+
|
|
68
|
+
def is_schema_allowed(self, schema: str) -> bool:
|
|
69
|
+
s = schema.lower()
|
|
70
|
+
if self.include_schemas is not None and s not in {
|
|
71
|
+
x.lower() for x in self.include_schemas
|
|
72
|
+
}:
|
|
73
|
+
return False
|
|
74
|
+
if self.exclude_schemas is not None and s in {
|
|
75
|
+
x.lower() for x in self.exclude_schemas
|
|
76
|
+
}:
|
|
77
|
+
return False
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def is_table_allowed(self, schema: str, table: str) -> bool:
|
|
81
|
+
key = f'{schema}.{table}'.lower()
|
|
82
|
+
if self.include_tables is not None and key not in {
|
|
83
|
+
x.lower() for x in self.include_tables
|
|
84
|
+
}:
|
|
85
|
+
return False
|
|
86
|
+
if self.exclude_tables is not None and key in {
|
|
87
|
+
x.lower() for x in self.exclude_tables
|
|
88
|
+
}:
|
|
89
|
+
return False
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
def is_view_allowed(self, schema: str, view_name: str) -> bool:
|
|
93
|
+
key = f'{schema}.{view_name}'.lower()
|
|
94
|
+
if self.include_views_objects is not None and key not in {
|
|
95
|
+
x.lower() for x in self.include_views_objects
|
|
96
|
+
}:
|
|
97
|
+
return False
|
|
98
|
+
if self.exclude_views_objects is not None and key in {
|
|
99
|
+
x.lower() for x in self.exclude_views_objects
|
|
100
|
+
}:
|
|
101
|
+
return False
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def is_stored_procedure_allowed(self, schema: str, procedure_name: str) -> bool:
|
|
105
|
+
key = f'{schema}.{procedure_name}'.lower()
|
|
106
|
+
if self.include_stored_procedures_objects is not None and key not in {
|
|
107
|
+
x.lower() for x in self.include_stored_procedures_objects
|
|
108
|
+
}:
|
|
109
|
+
return False
|
|
110
|
+
if self.exclude_stored_procedures_objects is not None and key in {
|
|
111
|
+
x.lower() for x in self.exclude_stored_procedures_objects
|
|
112
|
+
}:
|
|
113
|
+
return False
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def is_function_allowed(self, schema: str, function_name: str) -> bool:
|
|
117
|
+
key = f'{schema}.{function_name}'.lower()
|
|
118
|
+
if self.include_functions_objects is not None and key not in {
|
|
119
|
+
x.lower() for x in self.include_functions_objects
|
|
120
|
+
}:
|
|
121
|
+
return False
|
|
122
|
+
if self.exclude_functions_objects is not None and key in {
|
|
123
|
+
x.lower() for x in self.exclude_functions_objects
|
|
124
|
+
}:
|
|
125
|
+
return False
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def is_sequence_allowed(self, schema: str, sequence_name: str) -> bool:
|
|
129
|
+
key = f'{schema}.{sequence_name}'.lower()
|
|
130
|
+
if self.include_sequences_objects is not None and key not in {
|
|
131
|
+
x.lower() for x in self.include_sequences_objects
|
|
132
|
+
}:
|
|
133
|
+
return False
|
|
134
|
+
if self.exclude_sequences_objects is not None and key in {
|
|
135
|
+
x.lower() for x in self.exclude_sequences_objects
|
|
136
|
+
}:
|
|
137
|
+
return False
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
def is_synonym_allowed(self, schema: str, synonym_name: str) -> bool:
|
|
141
|
+
key = f'{schema}.{synonym_name}'.lower()
|
|
142
|
+
if self.include_synonyms_objects is not None and key not in {
|
|
143
|
+
x.lower() for x in self.include_synonyms_objects
|
|
144
|
+
}:
|
|
145
|
+
return False
|
|
146
|
+
if self.exclude_synonyms_objects is not None and key in {
|
|
147
|
+
x.lower() for x in self.exclude_synonyms_objects
|
|
148
|
+
}:
|
|
149
|
+
return False
|
|
150
|
+
return True
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .indexes import emit_index_ddl
|
|
2
|
+
from .foreign_keys import emit_foreign_key_ddl
|
|
3
|
+
from .tables import emit_table_ddl
|
|
4
|
+
from .sql_objects import emit_sql_object_ddl
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'emit_index_ddl',
|
|
8
|
+
'emit_foreign_key_ddl',
|
|
9
|
+
'emit_table_ddl',
|
|
10
|
+
'emit_sql_object_ddl',
|
|
11
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..models import ForeignKeyDef
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _emit_action(action_desc: str) -> str:
|
|
7
|
+
a = (action_desc or "").upper()
|
|
8
|
+
if a in ("NO_ACTION", ""):
|
|
9
|
+
return ""
|
|
10
|
+
if a == "CASCADE":
|
|
11
|
+
return "cascade"
|
|
12
|
+
if a == "SET_NULL":
|
|
13
|
+
return "set null"
|
|
14
|
+
if a == "SET_DEFAULT":
|
|
15
|
+
return "set default"
|
|
16
|
+
return action_desc.lower()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def emit_foreign_key_ddl(fk: ForeignKeyDef) -> str:
|
|
20
|
+
parent = f"{fk.parent_schema}.{fk.parent_table}"
|
|
21
|
+
ref = f"{fk.referenced_schema}.{fk.referenced_table}"
|
|
22
|
+
|
|
23
|
+
ddl = (
|
|
24
|
+
f"alter table {parent}\n"
|
|
25
|
+
f" add constraint {fk.fk_name}\n"
|
|
26
|
+
f" foreign key ({fk.parent_columns})\n"
|
|
27
|
+
f" references {ref} ({fk.referenced_columns})"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
on_delete = _emit_action(fk.on_delete)
|
|
31
|
+
if on_delete:
|
|
32
|
+
ddl += f"\n on delete {on_delete}"
|
|
33
|
+
|
|
34
|
+
on_update = _emit_action(fk.on_update)
|
|
35
|
+
if on_update:
|
|
36
|
+
ddl += f"\n on update {on_update}"
|
|
37
|
+
|
|
38
|
+
return ddl
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..models import IndexDef
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _strip_brackets(expr: str) -> str:
|
|
9
|
+
return re.sub(r'\[([^\]]+)\]', r'\1', expr or '')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _create_index_prefix(idx: IndexDef) -> str:
|
|
13
|
+
t = (idx.type_desc or '').strip().lower()
|
|
14
|
+
if idx.is_unique and t and t != 'nonclustered':
|
|
15
|
+
return f'create unique {t} index'
|
|
16
|
+
if idx.is_unique:
|
|
17
|
+
return 'create unique index'
|
|
18
|
+
if t and t != 'nonclustered':
|
|
19
|
+
return f'create {t} index'
|
|
20
|
+
return 'create index'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def emit_index_ddl(idx: IndexDef) -> str:
|
|
24
|
+
schema_table = f'{idx.schema_name}.{idx.table_name}'
|
|
25
|
+
key_columns = _strip_brackets(idx.key_columns)
|
|
26
|
+
include_columns = _strip_brackets(idx.include_columns)
|
|
27
|
+
|
|
28
|
+
ddl = f'{_create_index_prefix(idx)} {idx.index_name}\n'
|
|
29
|
+
ddl += f' on {schema_table} ({key_columns})'
|
|
30
|
+
|
|
31
|
+
if include_columns:
|
|
32
|
+
ddl += f' include ({include_columns})'
|
|
33
|
+
|
|
34
|
+
if idx.has_filter and idx.filter_definition:
|
|
35
|
+
ddl += f'\n where {idx.filter_definition}'
|
|
36
|
+
|
|
37
|
+
return ddl
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..models import SqlObjectDef
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def emit_sql_object_ddl(obj: SqlObjectDef) -> str:
|
|
7
|
+
blocks: list[str] = []
|
|
8
|
+
definition = (obj.definition or '').strip()
|
|
9
|
+
if definition:
|
|
10
|
+
blocks.append(definition)
|
|
11
|
+
|
|
12
|
+
for stmt in obj.permissions_sql or []:
|
|
13
|
+
s = (stmt or '').strip()
|
|
14
|
+
if s:
|
|
15
|
+
blocks.append(s)
|
|
16
|
+
|
|
17
|
+
return '\n\ngo\n\n'.join(blocks).strip()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..models import TableDef, TableColumnDef, ForeignKeyDef
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _q(name: str) -> str:
|
|
9
|
+
n = name.strip()
|
|
10
|
+
if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', n):
|
|
11
|
+
return n
|
|
12
|
+
return '[' + n.replace(']', ']]') + ']'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _strip_brackets(expr: str) -> str:
|
|
16
|
+
return re.sub(r'\[([^\]]+)\]', r'\1', expr or '')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sql_type(col: TableColumnDef) -> str:
|
|
20
|
+
t = col.data_type.lower()
|
|
21
|
+
|
|
22
|
+
if t in {'nvarchar', 'nchar'}:
|
|
23
|
+
if col.max_length is None:
|
|
24
|
+
return t
|
|
25
|
+
if col.max_length == -1:
|
|
26
|
+
return f'{t}(max)'
|
|
27
|
+
return f'{t}({int(col.max_length / 2)})'
|
|
28
|
+
|
|
29
|
+
if t in {'varchar', 'char', 'varbinary', 'binary'}:
|
|
30
|
+
if col.max_length is None:
|
|
31
|
+
return t
|
|
32
|
+
if col.max_length == -1:
|
|
33
|
+
return f'{t}(max)'
|
|
34
|
+
return f'{t}({col.max_length})'
|
|
35
|
+
|
|
36
|
+
if t in {'decimal', 'numeric'}:
|
|
37
|
+
p = col.precision if col.precision is not None else 18
|
|
38
|
+
s = col.scale if col.scale is not None else 0
|
|
39
|
+
return f'{t}({p},{s})'
|
|
40
|
+
|
|
41
|
+
if t in {'datetime2', 'datetimeoffset', 'time'}:
|
|
42
|
+
if col.scale is None:
|
|
43
|
+
return t
|
|
44
|
+
return f'{t}({col.scale})'
|
|
45
|
+
|
|
46
|
+
return t
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _emit_column(col: TableColumnDef) -> str:
|
|
50
|
+
name = _q(col.column_name)
|
|
51
|
+
|
|
52
|
+
if col.is_computed:
|
|
53
|
+
expr = col.computed_definition or ''
|
|
54
|
+
return f'{name} as {expr}'
|
|
55
|
+
|
|
56
|
+
line = f'{name} {_sql_type(col)}'
|
|
57
|
+
|
|
58
|
+
if col.collation_name and col.data_type.lower() in {
|
|
59
|
+
'char',
|
|
60
|
+
'varchar',
|
|
61
|
+
'nchar',
|
|
62
|
+
'nvarchar',
|
|
63
|
+
}:
|
|
64
|
+
line += f' collate {col.collation_name}'
|
|
65
|
+
|
|
66
|
+
if col.is_identity:
|
|
67
|
+
seed = col.identity_seed if col.identity_seed is not None else 1
|
|
68
|
+
inc = col.identity_increment if col.identity_increment is not None else 1
|
|
69
|
+
line += f' identity({seed},{inc})'
|
|
70
|
+
|
|
71
|
+
if col.default_definition:
|
|
72
|
+
if col.default_constraint_name:
|
|
73
|
+
line += f' constraint {_q(col.default_constraint_name)} default {col.default_definition}'
|
|
74
|
+
else:
|
|
75
|
+
line += f' default {col.default_definition}'
|
|
76
|
+
|
|
77
|
+
if not col.is_nullable:
|
|
78
|
+
line += ' not null'
|
|
79
|
+
return line
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _emit_fk_action(action_desc: str) -> str:
|
|
83
|
+
a = (action_desc or '').upper()
|
|
84
|
+
if a in {'', 'NO_ACTION'}:
|
|
85
|
+
return ''
|
|
86
|
+
if a == 'CASCADE':
|
|
87
|
+
return 'cascade'
|
|
88
|
+
if a == 'SET_NULL':
|
|
89
|
+
return 'set null'
|
|
90
|
+
if a == 'SET_DEFAULT':
|
|
91
|
+
return 'set default'
|
|
92
|
+
return a.lower()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _emit_inline_fk_constraint(fk: ForeignKeyDef) -> str:
|
|
96
|
+
ref_table = f'{_q(fk.referenced_schema)}.{_q(fk.referenced_table)}'
|
|
97
|
+
parent_cols = _strip_brackets(fk.parent_columns)
|
|
98
|
+
referenced_cols = _strip_brackets(fk.referenced_columns)
|
|
99
|
+
line = (
|
|
100
|
+
f'CONSTRAINT {_q(fk.fk_name)} FOREIGN KEY ({parent_cols}) '
|
|
101
|
+
f'REFERENCES {ref_table} ({referenced_cols})'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
on_delete = _emit_fk_action(fk.on_delete)
|
|
105
|
+
if on_delete:
|
|
106
|
+
line += f' ON DELETE {on_delete}'
|
|
107
|
+
|
|
108
|
+
on_update = _emit_fk_action(fk.on_update)
|
|
109
|
+
if on_update:
|
|
110
|
+
line += f' ON UPDATE {on_update}'
|
|
111
|
+
|
|
112
|
+
return line
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def emit_table_ddl(
|
|
116
|
+
table: TableDef, *, foreign_keys: list[ForeignKeyDef] | None = None
|
|
117
|
+
) -> str:
|
|
118
|
+
full_name = f'{_q(table.schema_name)}.{_q(table.table_name)}'
|
|
119
|
+
|
|
120
|
+
lines: list[str] = [f'create table {full_name} (']
|
|
121
|
+
|
|
122
|
+
body: list[str] = [f' {_emit_column(col)}' for col in table.columns]
|
|
123
|
+
|
|
124
|
+
if table.primary_key and table.primary_key.key_columns:
|
|
125
|
+
pk_type = table.primary_key.type_desc.lower().replace('_', ' ')
|
|
126
|
+
pk_cols = _strip_brackets(table.primary_key.key_columns)
|
|
127
|
+
pk_cols = re.sub(r'\s+asc\b', '', pk_cols, flags=re.IGNORECASE)
|
|
128
|
+
body.append(
|
|
129
|
+
f' CONSTRAINT {_q(table.primary_key.constraint_name)} PRIMARY KEY {pk_type} ({pk_cols})'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if foreign_keys:
|
|
133
|
+
for fk in sorted(foreign_keys, key=lambda x: x.fk_name.lower()):
|
|
134
|
+
body.append(f' {_emit_inline_fk_constraint(fk)}')
|
|
135
|
+
|
|
136
|
+
lines.append(',\n'.join(body))
|
|
137
|
+
lines.append(')')
|
|
138
|
+
|
|
139
|
+
return '\n'.join(lines)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .indexes import get_indexes
|
|
2
|
+
from .foreign_keys import get_foreign_keys
|
|
3
|
+
from .tables import get_tables
|
|
4
|
+
from .views import get_views
|
|
5
|
+
from .stored_procedures import get_stored_procedures
|
|
6
|
+
from .functions import get_functions
|
|
7
|
+
from .sequences import get_sequences
|
|
8
|
+
from .synonyms import get_synonyms
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'get_indexes',
|
|
12
|
+
'get_foreign_keys',
|
|
13
|
+
'get_tables',
|
|
14
|
+
'get_views',
|
|
15
|
+
'get_stored_procedures',
|
|
16
|
+
'get_functions',
|
|
17
|
+
'get_sequences',
|
|
18
|
+
'get_synonyms',
|
|
19
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from al_data_utils.sql_utils.sql_utils import UnifiedSQLClient
|
|
4
|
+
from ..models import ForeignKeyDef
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_SQL_FOREIGN_KEYS = r"""
|
|
8
|
+
select
|
|
9
|
+
ps.name as parent_schema,
|
|
10
|
+
pt.name as parent_table,
|
|
11
|
+
fk.name as fk_name,
|
|
12
|
+
rs.name as referenced_schema,
|
|
13
|
+
rt.name as referenced_table,
|
|
14
|
+
fk.delete_referential_action_desc as on_delete,
|
|
15
|
+
fk.update_referential_action_desc as on_update,
|
|
16
|
+
cast(fk.is_not_for_replication as bit) as is_not_for_replication,
|
|
17
|
+
cast(fk.is_disabled as bit) as is_disabled,
|
|
18
|
+
cast(fk.is_not_trusted as bit) as is_not_trusted,
|
|
19
|
+
|
|
20
|
+
stuff((
|
|
21
|
+
select
|
|
22
|
+
', ' + quotename(pc.name)
|
|
23
|
+
from sys.foreign_key_columns fkc
|
|
24
|
+
inner join sys.columns pc
|
|
25
|
+
on pc.object_id = fkc.parent_object_id
|
|
26
|
+
and pc.column_id = fkc.parent_column_id
|
|
27
|
+
where fkc.constraint_object_id = fk.object_id
|
|
28
|
+
order by fkc.constraint_column_id
|
|
29
|
+
for xml path(''), type
|
|
30
|
+
).value('.', 'nvarchar(max)'), 1, 2, '') as parent_columns,
|
|
31
|
+
|
|
32
|
+
stuff((
|
|
33
|
+
select
|
|
34
|
+
', ' + quotename(rc.name)
|
|
35
|
+
from sys.foreign_key_columns fkc
|
|
36
|
+
inner join sys.columns rc
|
|
37
|
+
on rc.object_id = fkc.referenced_object_id
|
|
38
|
+
and rc.column_id = fkc.referenced_column_id
|
|
39
|
+
where fkc.constraint_object_id = fk.object_id
|
|
40
|
+
order by fkc.constraint_column_id
|
|
41
|
+
for xml path(''), type
|
|
42
|
+
).value('.', 'nvarchar(max)'), 1, 2, '') as referenced_columns
|
|
43
|
+
|
|
44
|
+
from sys.foreign_keys fk
|
|
45
|
+
inner join sys.tables pt on pt.object_id = fk.parent_object_id
|
|
46
|
+
inner join sys.schemas ps on ps.schema_id = pt.schema_id
|
|
47
|
+
inner join sys.tables rt on rt.object_id = fk.referenced_object_id
|
|
48
|
+
inner join sys.schemas rs on rs.schema_id = rt.schema_id
|
|
49
|
+
where pt.is_ms_shipped = 0
|
|
50
|
+
order by ps.name, pt.name, fk.name;
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalized_set(values: set[str] | None) -> set[str]:
|
|
55
|
+
if not values:
|
|
56
|
+
return set()
|
|
57
|
+
return {str(v).strip().lower() for v in values if str(v).strip()}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _split_qualified(values: set[str]) -> list[tuple[str, str]]:
|
|
61
|
+
pairs: list[tuple[str, str]] = []
|
|
62
|
+
for v in sorted(values):
|
|
63
|
+
if '.' not in v:
|
|
64
|
+
continue
|
|
65
|
+
schema, table = v.split('.', 1)
|
|
66
|
+
schema = schema.strip().lower()
|
|
67
|
+
table = table.strip().lower()
|
|
68
|
+
if schema and table:
|
|
69
|
+
pairs.append((schema, table))
|
|
70
|
+
return pairs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_filter_sql_and_params(
|
|
74
|
+
*,
|
|
75
|
+
include_schemas: set[str] | None = None,
|
|
76
|
+
exclude_schemas: set[str] | None = None,
|
|
77
|
+
include_tables: set[str] | None = None,
|
|
78
|
+
exclude_tables: set[str] | None = None,
|
|
79
|
+
) -> tuple[str, dict[str, str]]:
|
|
80
|
+
inc_schemas = sorted(_normalized_set(include_schemas))
|
|
81
|
+
exc_schemas = sorted(_normalized_set(exclude_schemas))
|
|
82
|
+
inc_tables = _split_qualified(_normalized_set(include_tables))
|
|
83
|
+
exc_tables = _split_qualified(_normalized_set(exclude_tables))
|
|
84
|
+
|
|
85
|
+
params: dict[str, str] = {}
|
|
86
|
+
clauses: list[str] = []
|
|
87
|
+
|
|
88
|
+
if inc_schemas:
|
|
89
|
+
names: list[str] = []
|
|
90
|
+
for i, schema in enumerate(inc_schemas):
|
|
91
|
+
key = f'inc_schema_{i}'
|
|
92
|
+
params[key] = schema
|
|
93
|
+
names.append(f':{key}')
|
|
94
|
+
clauses.append(f'lower(ps.name) in ({", ".join(names)})')
|
|
95
|
+
|
|
96
|
+
if exc_schemas:
|
|
97
|
+
names = []
|
|
98
|
+
for i, schema in enumerate(exc_schemas):
|
|
99
|
+
key = f'exc_schema_{i}'
|
|
100
|
+
params[key] = schema
|
|
101
|
+
names.append(f':{key}')
|
|
102
|
+
clauses.append(f'lower(ps.name) not in ({", ".join(names)})')
|
|
103
|
+
|
|
104
|
+
if inc_tables:
|
|
105
|
+
parts: list[str] = []
|
|
106
|
+
for i, (schema, table) in enumerate(inc_tables):
|
|
107
|
+
sk = f'inc_tbl_schema_{i}'
|
|
108
|
+
tk = f'inc_tbl_name_{i}'
|
|
109
|
+
params[sk] = schema
|
|
110
|
+
params[tk] = table
|
|
111
|
+
parts.append(f'(lower(ps.name) = :{sk} and lower(pt.name) = :{tk})')
|
|
112
|
+
clauses.append(f'({" or ".join(parts)})')
|
|
113
|
+
|
|
114
|
+
if exc_tables:
|
|
115
|
+
parts = []
|
|
116
|
+
for i, (schema, table) in enumerate(exc_tables):
|
|
117
|
+
sk = f'exc_tbl_schema_{i}'
|
|
118
|
+
tk = f'exc_tbl_name_{i}'
|
|
119
|
+
params[sk] = schema
|
|
120
|
+
params[tk] = table
|
|
121
|
+
parts.append(f'(lower(ps.name) = :{sk} and lower(pt.name) = :{tk})')
|
|
122
|
+
clauses.append(f'not ({" or ".join(parts)})')
|
|
123
|
+
|
|
124
|
+
if not clauses:
|
|
125
|
+
return '', {}
|
|
126
|
+
|
|
127
|
+
return '\n and ' + '\n and '.join(clauses), params
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _inject_filters(sql: str, filters_sql: str) -> str:
|
|
131
|
+
if not filters_sql:
|
|
132
|
+
return sql
|
|
133
|
+
return sql.replace(
|
|
134
|
+
'where pt.is_ms_shipped = 0', f'where pt.is_ms_shipped = 0{filters_sql}'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_foreign_keys(
|
|
139
|
+
sql_client: UnifiedSQLClient,
|
|
140
|
+
*,
|
|
141
|
+
include_schemas: set[str] | None = None,
|
|
142
|
+
exclude_schemas: set[str] | None = None,
|
|
143
|
+
include_tables: set[str] | None = None,
|
|
144
|
+
exclude_tables: set[str] | None = None,
|
|
145
|
+
) -> list[ForeignKeyDef]:
|
|
146
|
+
filters_sql, params = _build_filter_sql_and_params(
|
|
147
|
+
include_schemas=include_schemas,
|
|
148
|
+
exclude_schemas=exclude_schemas,
|
|
149
|
+
include_tables=include_tables,
|
|
150
|
+
exclude_tables=exclude_tables,
|
|
151
|
+
)
|
|
152
|
+
rows = sql_client.execute_command_with_result_to_dicts(
|
|
153
|
+
_inject_filters(_SQL_FOREIGN_KEYS, filters_sql), params
|
|
154
|
+
)
|
|
155
|
+
result: list[ForeignKeyDef] = []
|
|
156
|
+
for r in rows:
|
|
157
|
+
result.append(
|
|
158
|
+
ForeignKeyDef(
|
|
159
|
+
parent_schema=str(r['parent_schema']),
|
|
160
|
+
parent_table=str(r['parent_table']),
|
|
161
|
+
fk_name=str(r['fk_name']),
|
|
162
|
+
referenced_schema=str(r['referenced_schema']),
|
|
163
|
+
referenced_table=str(r['referenced_table']),
|
|
164
|
+
on_delete=str(r['on_delete']),
|
|
165
|
+
on_update=str(r['on_update']),
|
|
166
|
+
is_not_for_replication=bool(r['is_not_for_replication']),
|
|
167
|
+
is_disabled=bool(r['is_disabled']),
|
|
168
|
+
is_not_trusted=bool(r['is_not_trusted']),
|
|
169
|
+
parent_columns=str(r['parent_columns'] or ''),
|
|
170
|
+
referenced_columns=str(r['referenced_columns'] or ''),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return result
|