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,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