instructure-pysqlsync 0.8.5.dev0__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 (102) hide show
  1. instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
  2. instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
  3. instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
  4. instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
  5. instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
  6. instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
  7. pysqlsync/__init__.py +15 -0
  8. pysqlsync/base.py +1386 -0
  9. pysqlsync/connection.py +101 -0
  10. pysqlsync/data/__init__.py +0 -0
  11. pysqlsync/data/exchange.py +184 -0
  12. pysqlsync/data/generator.py +305 -0
  13. pysqlsync/dialect/README.md +35 -0
  14. pysqlsync/dialect/__init__.py +0 -0
  15. pysqlsync/dialect/delta/__init__.py +0 -0
  16. pysqlsync/dialect/delta/data_types.py +51 -0
  17. pysqlsync/dialect/delta/dependency.py +7 -0
  18. pysqlsync/dialect/delta/engine.py +18 -0
  19. pysqlsync/dialect/delta/generator.py +74 -0
  20. pysqlsync/dialect/delta/object_types.py +87 -0
  21. pysqlsync/dialect/mssql/README.md +23 -0
  22. pysqlsync/dialect/mssql/__init__.py +0 -0
  23. pysqlsync/dialect/mssql/connection.py +146 -0
  24. pysqlsync/dialect/mssql/data_types.py +113 -0
  25. pysqlsync/dialect/mssql/dependency.py +9 -0
  26. pysqlsync/dialect/mssql/discovery.py +99 -0
  27. pysqlsync/dialect/mssql/engine.py +20 -0
  28. pysqlsync/dialect/mssql/generator.py +88 -0
  29. pysqlsync/dialect/mssql/mutation.py +46 -0
  30. pysqlsync/dialect/mssql/object_types.py +135 -0
  31. pysqlsync/dialect/mysql/__init__.py +0 -0
  32. pysqlsync/dialect/mysql/connection.py +126 -0
  33. pysqlsync/dialect/mysql/data_types.py +40 -0
  34. pysqlsync/dialect/mysql/dependency.py +10 -0
  35. pysqlsync/dialect/mysql/discovery.py +145 -0
  36. pysqlsync/dialect/mysql/engine.py +20 -0
  37. pysqlsync/dialect/mysql/generator.py +140 -0
  38. pysqlsync/dialect/mysql/mutation.py +65 -0
  39. pysqlsync/dialect/mysql/object_types.py +75 -0
  40. pysqlsync/dialect/oracle/README.md +22 -0
  41. pysqlsync/dialect/oracle/__init__.py +0 -0
  42. pysqlsync/dialect/oracle/connection.py +141 -0
  43. pysqlsync/dialect/oracle/data_types.py +104 -0
  44. pysqlsync/dialect/oracle/dependency.py +9 -0
  45. pysqlsync/dialect/oracle/discovery.py +231 -0
  46. pysqlsync/dialect/oracle/engine.py +20 -0
  47. pysqlsync/dialect/oracle/generator.py +93 -0
  48. pysqlsync/dialect/oracle/mutation.py +37 -0
  49. pysqlsync/dialect/oracle/object_types.py +59 -0
  50. pysqlsync/dialect/postgresql/__init__.py +0 -0
  51. pysqlsync/dialect/postgresql/connection.py +133 -0
  52. pysqlsync/dialect/postgresql/data_types.py +6 -0
  53. pysqlsync/dialect/postgresql/dependency.py +9 -0
  54. pysqlsync/dialect/postgresql/discovery.py +392 -0
  55. pysqlsync/dialect/postgresql/engine.py +20 -0
  56. pysqlsync/dialect/postgresql/generator.py +99 -0
  57. pysqlsync/dialect/postgresql/mutation.py +82 -0
  58. pysqlsync/dialect/postgresql/object_types.py +99 -0
  59. pysqlsync/dialect/redshift/__init__.py +0 -0
  60. pysqlsync/dialect/redshift/connection.py +156 -0
  61. pysqlsync/dialect/redshift/data_types.py +10 -0
  62. pysqlsync/dialect/redshift/dependency.py +9 -0
  63. pysqlsync/dialect/redshift/engine.py +20 -0
  64. pysqlsync/dialect/redshift/generator.py +82 -0
  65. pysqlsync/dialect/snowflake/__init__.py +0 -0
  66. pysqlsync/dialect/snowflake/data_types.py +18 -0
  67. pysqlsync/dialect/snowflake/dependency.py +9 -0
  68. pysqlsync/dialect/snowflake/engine.py +18 -0
  69. pysqlsync/dialect/snowflake/generator.py +64 -0
  70. pysqlsync/dialect/snowflake/object_types.py +84 -0
  71. pysqlsync/dialect/test/__init__.py +0 -0
  72. pysqlsync/dialect/test/dependency.py +10 -0
  73. pysqlsync/dialect/trino/__init__.py +0 -0
  74. pysqlsync/dialect/trino/connection.py +86 -0
  75. pysqlsync/dialect/trino/dependency.py +9 -0
  76. pysqlsync/dialect/trino/discovery.py +21 -0
  77. pysqlsync/dialect/trino/engine.py +20 -0
  78. pysqlsync/dialect/trino/generator.py +31 -0
  79. pysqlsync/factory.py +169 -0
  80. pysqlsync/formation/__init__.py +0 -0
  81. pysqlsync/formation/constraints.py +78 -0
  82. pysqlsync/formation/data_types.py +192 -0
  83. pysqlsync/formation/discovery.py +390 -0
  84. pysqlsync/formation/inspection.py +179 -0
  85. pysqlsync/formation/mutation.py +320 -0
  86. pysqlsync/formation/object_dict.py +79 -0
  87. pysqlsync/formation/object_types.py +872 -0
  88. pysqlsync/formation/py_to_sql.py +1134 -0
  89. pysqlsync/formation/sql_to_py.py +194 -0
  90. pysqlsync/model/__init__.py +0 -0
  91. pysqlsync/model/data_types.py +397 -0
  92. pysqlsync/model/entity_types.py +68 -0
  93. pysqlsync/model/id_types.py +193 -0
  94. pysqlsync/model/key_types.py +38 -0
  95. pysqlsync/model/properties.py +234 -0
  96. pysqlsync/py.typed +0 -0
  97. pysqlsync/python_types.py +192 -0
  98. pysqlsync/resultset.py +106 -0
  99. pysqlsync/util/__init__.py +0 -0
  100. pysqlsync/util/dataclasses.py +80 -0
  101. pysqlsync/util/dispatch.py +28 -0
  102. pysqlsync/util/typing.py +13 -0
@@ -0,0 +1,320 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ from ..model.data_types import SqlEnumType, SqlIntegerType, SqlVariableCharacterType, constant, quote
5
+ from ..model.id_types import SupportsName
6
+ from .object_types import (
7
+ Catalog,
8
+ Column,
9
+ ColumnFormationError,
10
+ EnumType,
11
+ FormationError,
12
+ Namespace,
13
+ StatementList,
14
+ StructType,
15
+ Table,
16
+ TableFormationError,
17
+ join_or_none,
18
+ )
19
+
20
+
21
+ @dataclass
22
+ class MutatorOptions:
23
+ """
24
+ Options for synchronizing an existing source state with a desired target state.
25
+
26
+ :param allow_drop_enum: Permit dropping enumeration types.
27
+ :param allow_drop_struct: Permit dropping (non-table) structure types.
28
+ :param allow_drop_table: Permit dropping tables.
29
+ :param allow_drop_namespace: Permit dropping namespaces (database schemas) and all objects within.
30
+ """
31
+
32
+ allow_drop_enum: bool = True
33
+ allow_drop_struct: bool = True
34
+ allow_drop_table: bool = True
35
+ allow_drop_namespace: bool = True
36
+
37
+
38
+ class Mutator:
39
+ "Morphs an existing source state into a desired target state."
40
+
41
+ options: MutatorOptions
42
+
43
+ def __init__(self, options: Optional[MutatorOptions] = None) -> None:
44
+ self.options = options or MutatorOptions()
45
+
46
+ def check_identity(self, source: SupportsName, target: SupportsName) -> None:
47
+ if source.name != target.name:
48
+ raise FormationError(f"object mismatch: {source.name} != {target.name}")
49
+
50
+ def migrate_enum_stmt(self, enum_type: EnumType, table: Table) -> Optional[str]:
51
+ enum_values = ", ".join(f"({quote(v)})" for v in enum_type.values)
52
+ return f'INSERT INTO {table.name} ("value") VALUES {enum_values};'
53
+
54
+ def mutate_enum_stmt(self, source: EnumType, target: EnumType) -> Optional[str]:
55
+ self.check_identity(source, target)
56
+
57
+ removed_values = [value for value in source.values if value not in target.values]
58
+ if removed_values:
59
+ raise FormationError(f"operation not permitted; cannot drop values in an enumeration: {''.join(removed_values)}")
60
+
61
+ added_values = [value for value in target.values if value not in source.values]
62
+ if added_values:
63
+ return "\n".join(f"ALTER TYPE {source.name} ADD VALUE {constant(v)};" for v in added_values)
64
+ else:
65
+ return None
66
+
67
+ def mutate_struct_stmt(self, source: StructType, target: StructType) -> Optional[str]:
68
+ self.check_identity(source, target)
69
+
70
+ statements: list[str] = []
71
+ statements.extend(f"DROP ATTRIBUTE {member.name}" for member in source.members.difference(target.members))
72
+ for source_member, target_member in source.members.intersection(target.members):
73
+ if source_member != target_member:
74
+ statements.append(f"ALTER ATTRIBUTE {source_member.name} SET DATA TYPE {target_member.data_type}")
75
+ statements.extend(f"ADD ATTRIBUTE {member}" for member in target.members.difference(source.members))
76
+ if statements:
77
+ return f"ALTER TYPE {source.name}\n" + ",\n".join(statements) + ";\n"
78
+ else:
79
+ return None
80
+
81
+ def migrate_column_stmt(self, source_table: Table, source: Column, target_table: Table, target: Column) -> Optional[str]:
82
+ return None
83
+
84
+ def is_column_migrated(self, source: Column, target: Column) -> Optional[bool]:
85
+ """
86
+ True if the column requires data migration, false if no migration is needed.
87
+
88
+ :param source: The source column to convert data from.
89
+ :param target: The target column to convert data to.
90
+ :returns: True/False, or None if subclass method is to decide.
91
+ """
92
+
93
+ # no migration is needed if column data type is unchanged
94
+ if source == target or source.data_type == target.data_type:
95
+ return False
96
+
97
+ # check if target data type is a primary key type
98
+ if isinstance(target.data_type, SqlIntegerType):
99
+ if isinstance(source.data_type, SqlEnumType):
100
+ return True
101
+ # some database engines represent enumerations as `varchar`
102
+ elif isinstance(source.data_type, SqlVariableCharacterType):
103
+ return True
104
+
105
+ return None # undecided (converts to False in a Boolean expression)
106
+
107
+ def mutate_column_type(self, source: Column, target: Column) -> Optional[str]:
108
+ if source.data_type != target.data_type:
109
+ return f"SET DATA TYPE {target.data_type}"
110
+ else:
111
+ return None
112
+
113
+ def mutate_column_stmt(self, source: Column, target: Column) -> Optional[str]:
114
+ if source == target:
115
+ return None
116
+
117
+ statements: list[str] = []
118
+
119
+ type_stmt = self.mutate_column_type(source, target)
120
+ if type_stmt:
121
+ statements.append(type_stmt)
122
+
123
+ if source.nullable and not target.nullable:
124
+ statements.append("SET NOT NULL")
125
+ elif not source.nullable and target.nullable:
126
+ statements.append("DROP NOT NULL")
127
+
128
+ if source.default is not None and target.default is None:
129
+ statements.append("DROP DEFAULT")
130
+ elif source.default != target.default:
131
+ statements.append(f"SET DEFAULT {target.default}")
132
+
133
+ if source.identity and not target.identity:
134
+ statements.append("DROP IDENTITY")
135
+ elif not source.identity and target.identity:
136
+ statements.append("ADD GENERATED BY DEFAULT AS IDENTITY")
137
+
138
+ if statements:
139
+ return ",\n".join(f"ALTER COLUMN {source.name} {s}" for s in statements)
140
+ else:
141
+ return None
142
+
143
+ def mutate_table_stmt(self, source: Table, target: Table) -> Optional[str]:
144
+ self.check_identity(source, target)
145
+
146
+ statements: list[Optional[str]] = []
147
+
148
+ create_columns = list(target.columns.difference(source.columns))
149
+ common_columns = list(source.columns.intersection(target.columns))
150
+ drop_columns = list(source.columns.difference(target.columns))
151
+ migrated_columns: list[tuple[Column, Column]] = []
152
+
153
+ # accumulate object creation statements
154
+ create_stmts: list[str] = []
155
+
156
+ # rename conflicting columns
157
+ try:
158
+ for pair in common_columns:
159
+ source_column, target_column = pair
160
+ if self.is_column_migrated(source_column, target_column):
161
+ statements.append(source.alter_table_stmt([source_column.soft_drop_stmt()]))
162
+ create_stmts.append(target_column.create_stmt())
163
+ migrated_columns.append(pair)
164
+ except ColumnFormationError as e:
165
+ raise TableFormationError("failed to migrate columns in table", target.name) from e
166
+
167
+ # create new columns
168
+ try:
169
+ create_stmts.extend(column.create_stmt() for column in create_columns)
170
+ except ColumnFormationError as e:
171
+ raise TableFormationError("failed to create columns in table", target.name) from e
172
+
173
+ if create_stmts:
174
+ statements.append(source.alter_table_stmt(create_stmts))
175
+
176
+ # accumulate object mutation statements
177
+ alter_stmts: list[Optional[str]] = []
178
+
179
+ # migrate data when data type changes
180
+ try:
181
+ for source_column, target_column in migrated_columns:
182
+ statements.append(self.migrate_column_stmt(source, source_column, target, target_column))
183
+ alter_stmts.append(source_column.hard_drop_stmt())
184
+ except ColumnFormationError as e:
185
+ raise TableFormationError("failed to migrate columns in table", target.name) from e
186
+
187
+ # mutate existing columns as necessary
188
+ try:
189
+ for source_column, target_column in common_columns:
190
+ alter_stmts.append(self.mutate_column_stmt(source_column, target_column))
191
+ except ColumnFormationError as e:
192
+ raise TableFormationError("failed to update columns in table", target.name) from e
193
+
194
+ # remove deleted columns
195
+ try:
196
+ alter_stmts.extend(column.drop_stmt() for column in drop_columns)
197
+ except ColumnFormationError as e:
198
+ raise TableFormationError("failed to drop columns in table", target.name) from e
199
+
200
+ alter_stmts.extend(
201
+ f"DROP CONSTRAINT {constraint.name}"
202
+ for constraint in source.constraints.difference(target.constraints)
203
+ if constraint.is_alter_table()
204
+ )
205
+
206
+ # remove outdated constraints
207
+ common_constraints = source.constraints.intersection(target.constraints)
208
+ for source_constraint, target_constraint in common_constraints:
209
+ if source_constraint != target_constraint:
210
+ raise TableFormationError(
211
+ f"failed to mutate constraint `{source_constraint.name}`",
212
+ target.name,
213
+ )
214
+
215
+ # add new constraints
216
+ alter_stmts.extend(
217
+ f"ADD CONSTRAINT {constraint.spec}"
218
+ for constraint in target.constraints.difference(source.constraints)
219
+ if constraint.is_alter_table()
220
+ )
221
+
222
+ stmts = [s for s in alter_stmts if s is not None]
223
+ if stmts:
224
+ statements.append(source.alter_table_stmt(stmts))
225
+
226
+ return join_or_none(statements)
227
+
228
+ def mutate_namespace_stmt(self, source: Namespace, target: Namespace) -> Optional[str]:
229
+ self.check_identity(source, target)
230
+
231
+ statements: StatementList = StatementList()
232
+
233
+ # identify objects to create
234
+ enum_create = list(target.enums.difference(source.enums))
235
+ struct_create = list(target.structs.difference(source.structs))
236
+ table_create = list(target.tables.difference(source.tables))
237
+
238
+ # identify objects to drop
239
+ enum_drop = list(source.enums.difference(target.enums))
240
+ struct_drop = list(source.structs.difference(target.structs))
241
+ table_drop = list(source.tables.difference(target.tables))
242
+ enum_soft_deleted: list[EnumType] = []
243
+
244
+ # create new objects
245
+ statements.extend(enum.create_stmt() for enum in enum_create)
246
+ statements.extend(struct.create_stmt() for struct in struct_create)
247
+
248
+ for enum_type in enum_drop:
249
+ for table in table_create:
250
+ if enum_type.name != table.name:
251
+ continue
252
+ statements.append(enum_type.soft_drop_stmt())
253
+ enum_soft_deleted.append(enum_type)
254
+
255
+ statements.extend(table.create_stmt() for table in table_create)
256
+ statements.extend(table.add_constraints_stmt() for table in table_create)
257
+
258
+ # populate new objects with data
259
+ for enum_type in enum_drop:
260
+ for table in table_create:
261
+ if enum_type.name != table.name:
262
+ continue
263
+
264
+ statements.append(self.migrate_enum_stmt(enum_type, table))
265
+
266
+ # mutate existing object
267
+ enum_mutate = list(source.enums.intersection(target.enums))
268
+ statements.extend(self.mutate_enum_stmt(source_enum, target_enum) for source_enum, target_enum in enum_mutate)
269
+
270
+ struct_mutate = list(source.structs.intersection(target.structs))
271
+ statements.extend(self.mutate_struct_stmt(source_struct, target_struct) for source_struct, target_struct in struct_mutate)
272
+
273
+ table_mutate = list(source.tables.intersection(target.tables))
274
+ statements.extend(self.mutate_table_stmt(source_table, target_table) for source_table, target_table in table_mutate)
275
+
276
+ # drop old objects
277
+ if self.options.allow_drop_table:
278
+ statements.extend(table.drop_stmt() for table in table_drop)
279
+
280
+ if self.options.allow_drop_struct:
281
+ statements.extend(struct.drop_stmt() for struct in struct_drop)
282
+
283
+ if self.options.allow_drop_enum:
284
+ for enum_type in enum_drop:
285
+ statements.append(enum_type.hard_drop_stmt() if enum_type in enum_soft_deleted else enum_type.drop_stmt())
286
+
287
+ return join_or_none(statements)
288
+
289
+ def mutate_catalog_stmt(self, source: Catalog, target: Catalog) -> Optional[str]:
290
+ statements: StatementList = StatementList()
291
+
292
+ source.sort()
293
+ target.sort()
294
+
295
+ ns_create = list(target.namespaces.difference(source.namespaces))
296
+ ns_drop = list(source.namespaces.difference(target.namespaces))
297
+ ns_mutate = list(source.namespaces.intersection(target.namespaces))
298
+
299
+ # create new namespaces
300
+ statements.extend(namespace.create_schema_stmt() for namespace in ns_create)
301
+
302
+ # create objects in each namespace added
303
+ statements.extend(namespace.create_objects_stmt() for namespace in ns_create)
304
+
305
+ # add new constraints
306
+ statements.extend(namespace.add_constraints_stmt() for namespace in ns_create)
307
+
308
+ # mutate existing namespaces
309
+ statements.extend(self.mutate_namespace_stmt(source_ns, target_ns) for source_ns, target_ns in ns_mutate)
310
+
311
+ # remove old constraints
312
+ statements.extend(namespace.drop_constraints_stmt() for namespace in ns_drop)
313
+ if self.options.allow_drop_namespace:
314
+ # drop objects in each namespace removed
315
+ statements.extend(namespace.drop_objects_stmt() for namespace in ns_drop)
316
+
317
+ # drop old namespaces
318
+ statements.extend(namespace.drop_schema_stmt() for namespace in ns_drop)
319
+
320
+ return join_or_none(statements)
@@ -0,0 +1,79 @@
1
+ from collections.abc import ItemsView, KeysView, Mapping, ValuesView
2
+ from typing import Generic, Iterable, Iterator, Optional, TypeVar, Union, overload
3
+
4
+ from ..model.id_types import SupportsName
5
+
6
+ T = TypeVar("T")
7
+ ObjectItem = TypeVar("ObjectItem", bound=SupportsName)
8
+
9
+
10
+ class ObjectDict(Generic[ObjectItem], Mapping[str, ObjectItem]):
11
+ _items: dict[str, ObjectItem]
12
+
13
+ def __init__(self, items: Iterable[ObjectItem]) -> None:
14
+ self._items = {item.name.local_id: item for item in items}
15
+
16
+ def __contains__(self, key: object) -> bool:
17
+ return key in self._items
18
+
19
+ def __eq__(self, value: object) -> bool:
20
+ if not isinstance(value, ObjectDict):
21
+ return False
22
+
23
+ return self._items == value._items # pyright: ignore[reportUnknownMemberType]
24
+
25
+ def __getitem__(self, key: str) -> ObjectItem:
26
+ return self._items[key]
27
+
28
+ def __iter__(self) -> Iterator[str]:
29
+ return iter(self._items)
30
+
31
+ def __len__(self) -> int:
32
+ return len(self._items)
33
+
34
+ def __repr__(self) -> str:
35
+ return repr(list(self._items.values()))
36
+
37
+ @overload
38
+ def get(self, key: str, /) -> Optional[ObjectItem]: ...
39
+
40
+ @overload
41
+ def get(self, key: str, /, default: Union[ObjectItem, T]) -> Union[ObjectItem, T]: ...
42
+
43
+ def get(self, key: str, /, default: Optional[T] = None) -> Union[None, ObjectItem, T]:
44
+ return self._items.get(key, default)
45
+
46
+ def add(self, item: ObjectItem) -> None:
47
+ if item.name.local_id in self._items:
48
+ raise ValueError(f"item already in collection: {item.name}")
49
+
50
+ self._items[item.name.local_id] = item
51
+
52
+ def remove(self, key: str) -> None:
53
+ self._items.pop(key)
54
+
55
+ def keys(self) -> KeysView[str]:
56
+ return self._items.keys()
57
+
58
+ def values(self) -> ValuesView[ObjectItem]:
59
+ return self._items.values()
60
+
61
+ def items(self) -> ItemsView[str, ObjectItem]:
62
+ return self._items.items()
63
+
64
+ def difference(self, op: "ObjectDict[ObjectItem]") -> Iterable["ObjectItem"]:
65
+ for key, item in self.items():
66
+ if key not in op:
67
+ yield item
68
+
69
+ def intersection(self, op: "ObjectDict[ObjectItem]") -> Iterable[tuple["ObjectItem", "ObjectItem"]]:
70
+ for key, item in self.items():
71
+ op_item = op.get(key)
72
+ if op_item is not None:
73
+ yield (item, op_item)
74
+
75
+ def sort(self) -> None:
76
+ self._items = dict(sorted(self._items.items()))
77
+
78
+ def reorder(self, order: list[str]) -> None:
79
+ self._items = {key: self._items[key] for key in order}