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,872 @@
1
+ import abc
2
+ import copy
3
+ from dataclasses import dataclass
4
+ from typing import Any, Iterable, Optional, overload
5
+
6
+ from strong_typing.inspection import is_dataclass_instance
7
+ from strong_typing.topological import topological_sort
8
+
9
+ from ..model.data_types import SqlDataType, SqlUserDefinedType, constant
10
+ from ..model.id_types import LocalId, SupportsQualifiedId
11
+ from .object_dict import ObjectDict
12
+
13
+
14
+ def deleted(name: str) -> str:
15
+ "Name for a soft-deleted object."
16
+
17
+ return f"pysqlsync${name}"
18
+
19
+
20
+ class StatementList(list[str]):
21
+ def append(self, __object: Optional[str]) -> None:
22
+ if __object is None:
23
+ return
24
+ if isinstance(__object, str) and not __object.strip(): # pyright: ignore[reportUnnecessaryIsInstance]
25
+ raise ValueError("empty statement")
26
+ return super().append(__object)
27
+
28
+ def extend(self, __iterable: Iterable[Optional[str]]) -> None:
29
+ for item in __iterable:
30
+ self.append(item)
31
+
32
+
33
+ def join(statements: Iterable[Optional[str]]) -> str:
34
+ return "\n".join(s for s in statements if s is not None)
35
+
36
+
37
+ def join_or_none(statements: Iterable[Optional[str]]) -> Optional[str]:
38
+ return join(statements) or None
39
+
40
+
41
+ class MappingError(RuntimeError):
42
+ "Raised when a Python class cannot map to a database entity."
43
+
44
+
45
+ class FormationError(RuntimeError):
46
+ "Raised when a source state cannot mutate into a target state."
47
+
48
+
49
+ class ColumnFormationError(FormationError):
50
+ "Raised when a column cannot mutate into another column."
51
+
52
+ column: LocalId
53
+
54
+ def __init__(self, cause: str, column: LocalId) -> None:
55
+ super().__init__(cause)
56
+ self.column = column
57
+
58
+ def __str__(self) -> str:
59
+ return f"column {self.column}: {self.args[0]}"
60
+
61
+
62
+ class TableFormationError(FormationError):
63
+ """
64
+ Raised when a table cannot mutate into another table.
65
+
66
+ Details on why the mutation failed are available in the stack trace.
67
+ """
68
+
69
+ table: SupportsQualifiedId
70
+
71
+ def __init__(self, cause: str, table: SupportsQualifiedId) -> None:
72
+ super().__init__(cause)
73
+ self.table = table
74
+
75
+ def __str__(self) -> str:
76
+ return f"table {self.table}: {self.args[0]}"
77
+
78
+
79
+ @dataclass
80
+ class QualifiedObject(abc.ABC):
81
+ name: SupportsQualifiedId
82
+
83
+
84
+ class DatabaseObject(abc.ABC):
85
+ @abc.abstractmethod
86
+ def create_stmt(self) -> str: ...
87
+
88
+ @abc.abstractmethod
89
+ def drop_stmt(self) -> str: ...
90
+
91
+
92
+ @dataclass
93
+ class EnumType(DatabaseObject, QualifiedObject):
94
+ values: list[str]
95
+
96
+ def __init__(self, enum_id: SupportsQualifiedId, values: list[str]) -> None:
97
+ super().__init__(enum_id)
98
+ self.values = values
99
+
100
+ def create_stmt(self) -> str:
101
+ vals = ", ".join(constant(val) for val in self.values)
102
+ return f"CREATE TYPE {self.name} AS ENUM ({vals});"
103
+
104
+ def drop_stmt(self) -> str:
105
+ return f"DROP TYPE {self.name};"
106
+
107
+ def soft_drop_stmt(self) -> str:
108
+ "Causes the enumeration type to become inaccessible by obfuscating its name."
109
+
110
+ return self.rename_stmt(deleted(self.name.local_id))
111
+
112
+ def hard_drop_stmt(self) -> str:
113
+ "Removes the enumeration type that had previously been soft-deleted."
114
+
115
+ return f"DROP TYPE {self.name.rename(deleted(self.name.local_id))};"
116
+
117
+ def rename_stmt(self, name: str) -> str:
118
+ return f"ALTER TYPE {self.name} RENAME TO {LocalId(name)};"
119
+
120
+ def __str__(self) -> str:
121
+ return self.create_stmt()
122
+
123
+
124
+ @dataclass
125
+ class StructMember:
126
+ """
127
+ A member (a.k.a. field) of a composite (a.k.a. struct) type.
128
+
129
+ :param name: Name of the field.
130
+ :param data_type: Type of the field.
131
+ :param description: Human-readable description of the field.
132
+ """
133
+
134
+ name: LocalId
135
+ data_type: SqlDataType
136
+ description: Optional[str] = None
137
+
138
+ def __str__(self) -> str:
139
+ return f"{self.name} {self.data_type}"
140
+
141
+
142
+ @dataclass
143
+ class StructType(DatabaseObject, QualifiedObject):
144
+ """
145
+ A composite (a.k.a. struct) type, i.e. a nested type without a primary key.
146
+
147
+ :param members: Members (a.k.a. fields) of the composite type.
148
+ :param description: Human-readable description of the composite type.
149
+ """
150
+
151
+ members: ObjectDict[StructMember]
152
+ description: Optional[str]
153
+
154
+ def __init__(
155
+ self,
156
+ name: SupportsQualifiedId,
157
+ members: list[StructMember],
158
+ description: Optional[str] = None,
159
+ ) -> None:
160
+ super().__init__(name)
161
+ self.members = ObjectDict(members)
162
+ self.description = description
163
+
164
+ def get_referenced_types(self) -> set[SupportsQualifiedId]:
165
+ return set(m.data_type.ref for m in self.members.values() if isinstance(m.data_type, SqlUserDefinedType))
166
+
167
+ def create_stmt(self) -> str:
168
+ members = ",\n".join(str(m) for m in self.members.values())
169
+ return f"CREATE TYPE {self.name} AS (\n{members}\n);"
170
+
171
+ def drop_stmt(self) -> str:
172
+ return f"DROP TYPE {self.name};"
173
+
174
+ def __str__(self) -> str:
175
+ return self.create_stmt()
176
+
177
+
178
+ @dataclass(eq=True)
179
+ class Column(DatabaseObject):
180
+ """
181
+ A column in a database table.
182
+
183
+ :param name: The name of the column within its host table.
184
+ :param data_type: The SQL data type of the column.
185
+ :param nullable: True if the column can take the value NULL.
186
+ :param default: The default value the column takes if no explicit value is set. Must be a valid SQL expression.
187
+ :param identity: Whether the column is an identity column.
188
+ :param description: The textual description of the column.
189
+ """
190
+
191
+ name: LocalId
192
+ data_type: SqlDataType
193
+ nullable: bool
194
+ default: Optional[str]
195
+ identity: bool
196
+ description: Optional[str]
197
+
198
+ @classmethod
199
+ def create(
200
+ cls,
201
+ name: LocalId,
202
+ data_type: SqlDataType,
203
+ *,
204
+ nullable: bool,
205
+ default: Optional[str] = None,
206
+ identity: bool = False,
207
+ description: Optional[str] = None,
208
+ ) -> "Column":
209
+ return cls(
210
+ name,
211
+ data_type,
212
+ nullable=nullable,
213
+ default=default,
214
+ identity=identity,
215
+ description=description,
216
+ )
217
+
218
+ def __str__(self) -> str:
219
+ return self.column_spec
220
+
221
+ @property
222
+ def column_spec(self) -> str:
223
+ return f"{self.name} {self.data_spec}"
224
+
225
+ @property
226
+ def data_spec(self) -> str:
227
+ nullable = " NOT NULL" if not self.nullable and not self.identity else ""
228
+ default = f" DEFAULT {self.default}" if self.default is not None else ""
229
+ identity = " GENERATED BY DEFAULT AS IDENTITY" if self.identity else ""
230
+ return f"{self.data_type}{nullable}{default}{identity}"
231
+
232
+ def create_stmt(self) -> str:
233
+ "Creates a column as part of an ALTER TABLE statement."
234
+
235
+ return f"ADD COLUMN {self.column_spec}"
236
+
237
+ def drop_stmt(self) -> str:
238
+ "Removes a column as part of an ALTER TABLE statement."
239
+
240
+ return f"DROP COLUMN {self.name}"
241
+
242
+ def soft_drop_stmt(self) -> str:
243
+ "Causes the column to become inaccessible by obfuscating its name."
244
+
245
+ return self.rename_stmt(deleted(self.name.local_id))
246
+
247
+ def hard_drop_stmt(self) -> str:
248
+ "Removes the column that had previously been soft-deleted."
249
+
250
+ return f"DROP COLUMN {LocalId(deleted(self.name.local_id))}"
251
+
252
+ def rename_stmt(self, name: str) -> str:
253
+ return f"RENAME COLUMN {self.name} TO {LocalId(name)}"
254
+
255
+
256
+ @dataclass
257
+ class ConstraintReference:
258
+ """
259
+ A reference that a constraint points to.
260
+
261
+ :param table: The table that the constraint points to.
262
+ :param columns: The columns in the table that the constraint points to.
263
+ """
264
+
265
+ table: SupportsQualifiedId
266
+ columns: tuple[LocalId, ...]
267
+
268
+
269
+ @dataclass
270
+ class Constraint(abc.ABC):
271
+ """
272
+ A table constraint, such as a primary, foreign or check constraint.
273
+
274
+ :param name: The name of the constraint.
275
+ """
276
+
277
+ name: LocalId
278
+
279
+ @property
280
+ @abc.abstractmethod
281
+ def spec(self) -> str: ...
282
+
283
+ def is_alter_table(self) -> bool:
284
+ "True if the constraint is to be applied with an ALTER TABLE statement."
285
+
286
+ return False
287
+
288
+ def __str__(self) -> str:
289
+ return f"CONSTRAINT {self.spec}"
290
+
291
+
292
+ @dataclass
293
+ class UniqueConstraint(Constraint):
294
+ """
295
+ A unique constraint.
296
+
297
+ :param unique_columns: Names of columns that comprise the constraint.
298
+ """
299
+
300
+ unique_columns: tuple[LocalId, ...]
301
+
302
+ def is_alter_table(self) -> bool:
303
+ return True
304
+
305
+ @property
306
+ def spec(self) -> str:
307
+ columns = ", ".join(str(column) for column in self.unique_columns)
308
+ return f"{self.name} UNIQUE ({columns})"
309
+
310
+
311
+ @dataclass
312
+ class ReferenceConstraint(Constraint):
313
+ """
314
+ A constraint that references another table, such as a foreign or discriminated key constraint.
315
+
316
+ :param foreign_columns: Names of columns that comprise the reference constraint.
317
+ """
318
+
319
+ foreign_columns: tuple[LocalId, ...]
320
+
321
+
322
+ @dataclass
323
+ class ForeignConstraint(ReferenceConstraint):
324
+ """
325
+ A foreign key constraint.
326
+
327
+ :param reference: The reference this foreign constraint incorporates.
328
+ """
329
+
330
+ reference: ConstraintReference
331
+
332
+ def is_alter_table(self) -> bool:
333
+ return True
334
+
335
+ @property
336
+ def spec(self) -> str:
337
+ source_columns = ", ".join(str(column) for column in self.foreign_columns)
338
+ target_columns = ", ".join(str(column) for column in self.reference.columns)
339
+ return f"{self.name} FOREIGN KEY ({source_columns}) REFERENCES {self.reference.table} ({target_columns})"
340
+
341
+
342
+ @dataclass
343
+ class DiscriminatedConstraint(ReferenceConstraint):
344
+ """
345
+ A discriminated key constraint whose value references one of several tables.
346
+
347
+ :param references: The list of tables either of which the constraint can point to.
348
+ """
349
+
350
+ references: list[ConstraintReference]
351
+
352
+ @property
353
+ def spec(self) -> str:
354
+ raise NotImplementedError("cannot represent discriminated constraint in SQL DDL")
355
+
356
+
357
+ @dataclass
358
+ class CheckConstraint(Constraint):
359
+ """
360
+ A check constraint.
361
+
362
+ :param condition: A Boolean expression in SQL that must hold true for the column values.
363
+ """
364
+
365
+ condition: str
366
+
367
+ def is_alter_table(self) -> bool:
368
+ return True
369
+
370
+ @property
371
+ def spec(self) -> str:
372
+ return f"{self.name} CHECK ({self.condition})"
373
+
374
+
375
+ @dataclass
376
+ class Table(DatabaseObject, QualifiedObject):
377
+ """
378
+ A database table.
379
+
380
+ :param columns: The columns that the table consists of.
381
+ :param primary_key: The primary key column(s) of the table.
382
+ :param constraints: Any constraints applied to the table.
383
+ :param description: A textual description of the table.
384
+ """
385
+
386
+ columns: ObjectDict[Column]
387
+ primary_key: tuple[LocalId, ...]
388
+ constraints: ObjectDict[Constraint]
389
+ description: Optional[str]
390
+
391
+ def __init__(
392
+ self,
393
+ name: SupportsQualifiedId,
394
+ columns: Iterable[Column],
395
+ *,
396
+ primary_key: tuple[LocalId, ...],
397
+ constraints: Optional[Iterable[Constraint]] = None,
398
+ description: Optional[str] = None,
399
+ ) -> None:
400
+ super().__init__(name)
401
+ self.columns = ObjectDict(columns)
402
+ self.primary_key = primary_key
403
+ self.constraints = ObjectDict(constraints or ())
404
+ self.description = description
405
+
406
+ def __str__(self) -> str:
407
+ defs: list[str] = []
408
+ defs.extend(str(c) for c in self.columns.values())
409
+ keys = ", ".join(str(key) for key in self.primary_key)
410
+ defs.append(f"CONSTRAINT {self.primary_key_constraint_id} PRIMARY KEY ({keys})")
411
+ defs.extend(str(c) for c in self.constraints.values())
412
+ definition = ",\n".join(defs)
413
+ return f"CREATE TABLE {self.name} (\n{definition}\n);"
414
+
415
+ @property
416
+ def primary_key_constraint_id(self) -> LocalId:
417
+ return LocalId(f"pk_{self.name.compact_id.replace('.', '_')}")
418
+
419
+ def get_columns(self, field_names: Optional[tuple[str, ...]] = None) -> list[Column]:
420
+ "Returns columns of a table in a desired order."
421
+
422
+ if field_names is not None:
423
+ columns: list[Column] = []
424
+
425
+ for field_name in field_names:
426
+ column = self.columns.get(field_name)
427
+ if column is None:
428
+ raise ValueError(f"column {LocalId(field_name)} not found in table {self.name}")
429
+ columns.append(column)
430
+
431
+ return columns
432
+ else:
433
+ return list(self.columns.values())
434
+
435
+ def check_primary(self) -> None:
436
+ if len(self.primary_key) == 0:
437
+ raise KeyError(f"no primary key defined in table: {self.name}")
438
+ if len(self.primary_key) > 1:
439
+ raise KeyError(f"composite primary defined in table: {self.name}")
440
+
441
+ def is_primary_column(self, column_id: LocalId) -> bool:
442
+ "True if the specified column is a primary key."
443
+
444
+ self.check_primary()
445
+ return column_id in self.primary_key
446
+
447
+ def get_primary_column(self) -> Column:
448
+ "Returns the primary key column."
449
+
450
+ self.check_primary()
451
+ for column in self.columns.values():
452
+ if column.name in self.primary_key:
453
+ return column
454
+
455
+ raise KeyError(f"no primary column in table: {self.name}")
456
+
457
+ def get_value_columns(self, field_names: Optional[tuple[str, ...]] = None) -> list[Column]:
458
+ """
459
+ Returns columns that have to be supplied an explicit value for a newly inserted row.
460
+
461
+ This constitutes all columns that not part of the primary key and not generated by identity.
462
+ """
463
+
464
+ return [column for column in self.get_columns(field_names) if column.name not in self.primary_key and not column.identity]
465
+
466
+ def is_unique_column(self, column: Column) -> bool:
467
+ "True if a unique constraint is applied to the specified column."
468
+
469
+ for constraint in self.constraints.values():
470
+ if not isinstance(constraint, UniqueConstraint):
471
+ continue
472
+ if len(constraint.unique_columns) > 1:
473
+ continue
474
+ if column.name not in constraint.unique_columns:
475
+ continue
476
+ return True
477
+
478
+ return False
479
+
480
+ def is_lookup_column(self, column: Column) -> bool:
481
+ "True if the column may be used to look up a record by its value."
482
+
483
+ return self.is_primary_column(column.name) or self.is_unique_column(column)
484
+
485
+ def is_lookup_table(self) -> bool:
486
+ "Checks whether the table maps a primary key to a unique value."
487
+
488
+ if len(self.columns) != 2:
489
+ return False
490
+
491
+ for column in self.columns.values():
492
+ if self.is_primary_column(column.name):
493
+ continue
494
+ if self.is_unique_column(column):
495
+ continue
496
+ return False
497
+
498
+ return True
499
+
500
+ def is_relation(self, column: Column) -> bool:
501
+ "Checks whether the column is a foreign key relation."
502
+
503
+ for constraint in self.constraints.values():
504
+ if not isinstance(constraint, ForeignConstraint):
505
+ continue
506
+ if len(constraint.foreign_columns) > 1:
507
+ continue
508
+ if column.name not in constraint.foreign_columns:
509
+ continue
510
+ return True
511
+
512
+ return False
513
+
514
+ def get_constraint(self, column_id: LocalId) -> ConstraintReference:
515
+ "Returns a reference that a column points to."
516
+
517
+ for constraint in self.constraints.values():
518
+ if not isinstance(constraint, ForeignConstraint):
519
+ continue
520
+ if len(constraint.foreign_columns) > 1:
521
+ continue
522
+ if column_id not in constraint.foreign_columns:
523
+ continue
524
+ return constraint.reference
525
+
526
+ raise KeyError(f"foreign constraint not found for column: {column_id}")
527
+
528
+ def get_referenced_types(self) -> set[SupportsQualifiedId]:
529
+ return set(c.data_type.ref for c in self.columns.values() if isinstance(c.data_type, SqlUserDefinedType))
530
+
531
+ def get_referenced_tables(self) -> set[SupportsQualifiedId]:
532
+ items: set[SupportsQualifiedId] = set()
533
+ for c in self.constraints.values():
534
+ if isinstance(c, ForeignConstraint):
535
+ items.add(c.reference.table)
536
+ elif isinstance(c, DiscriminatedConstraint):
537
+ items.update(r.table for r in c.references)
538
+ return items
539
+
540
+ def create_keys(self) -> str:
541
+ keys = ", ".join(str(key) for key in self.primary_key)
542
+ return f"CONSTRAINT {self.primary_key_constraint_id} PRIMARY KEY ({keys})"
543
+
544
+ def create_stmt(self) -> str:
545
+ defs: list[str] = []
546
+ defs.extend(str(c) for c in self.columns.values())
547
+ defs.append(self.create_keys())
548
+ definition = ",\n".join(defs)
549
+ return f"CREATE TABLE {self.name} (\n{definition}\n);"
550
+
551
+ def drop_stmt(self) -> str:
552
+ return f"DROP TABLE {self.name};"
553
+
554
+ def drop_if_exists_stmt(self) -> str:
555
+ return f"DROP TABLE IF EXISTS {self.name};"
556
+
557
+ def alter_table_stmt(self, statements: list[str]) -> str:
558
+ return f"ALTER TABLE {self.name}\n" + ",\n".join(statements) + ";"
559
+
560
+ def add_constraints_stmt(self) -> Optional[str]:
561
+ if self.table_constraints:
562
+ return self.alter_table_stmt([f"ADD CONSTRAINT {c.spec}" for c in self.table_constraints])
563
+ else:
564
+ return None
565
+
566
+ def drop_constraints_stmt(self) -> Optional[str]:
567
+ if self.table_constraints:
568
+ return f"ALTER TABLE {self.name}\n" + ",\n".join(f"DROP CONSTRAINT {c.name}" for c in self.table_constraints) + "\n;"
569
+ else:
570
+ return None
571
+
572
+ @property
573
+ def table_constraints(self) -> list[Constraint]:
574
+ return [c for c in self.constraints.values() if c.is_alter_table()]
575
+
576
+
577
+ class EnumTable(Table):
578
+ values: list[Any]
579
+
580
+ def __init__(
581
+ self,
582
+ name: SupportsQualifiedId,
583
+ columns: list[Column],
584
+ *,
585
+ values: list[Any],
586
+ primary_key: tuple[LocalId, ...],
587
+ constraints: Optional[list[Constraint]] = None,
588
+ description: Optional[str] = None,
589
+ ) -> None:
590
+ super().__init__(
591
+ name,
592
+ columns,
593
+ primary_key=primary_key,
594
+ constraints=constraints,
595
+ description=description,
596
+ )
597
+ self.values = values
598
+
599
+ def create_stmt(self) -> str:
600
+ statements: list[str] = []
601
+ statements.append(super().create_stmt())
602
+ column_list = ", ".join(str(col.name) for col in self.get_value_columns())
603
+ values: list[str] = []
604
+ for value in self.values:
605
+ if isinstance(value, tuple) or is_dataclass_instance(value):
606
+ values.append(constant(value))
607
+ else:
608
+ values.append(f"({constant(value)})")
609
+ value_list = ", ".join(values)
610
+
611
+ statements.append(f"INSERT INTO {self.name} ({column_list}) VALUES {value_list};")
612
+ return "\n".join(statements)
613
+
614
+
615
+ @dataclass
616
+ class Namespace(DatabaseObject):
617
+ """
618
+ A namespace that multiple objects can share. Typically corresponds to a database schema.
619
+
620
+ :param name: Unqualified name for the namespace (schema).
621
+ :param enums: Enumeration types defined in the namespace (if the database engine supports them).
622
+ :param structs: Composite types defined in the namespace (if the database engine supports them).
623
+ :param tables: Tables defined in the namespace.
624
+ """
625
+
626
+ name: LocalId
627
+ enums: ObjectDict[EnumType]
628
+ structs: ObjectDict[StructType]
629
+ tables: ObjectDict[Table]
630
+
631
+ @overload
632
+ def __init__(self) -> None: ...
633
+
634
+ @overload
635
+ def __init__(self, name: LocalId) -> None: ...
636
+
637
+ @overload
638
+ def __init__(
639
+ self,
640
+ *,
641
+ enums: list[EnumType],
642
+ structs: list[StructType],
643
+ tables: list[Table],
644
+ ) -> None: ...
645
+
646
+ @overload
647
+ def __init__(
648
+ self,
649
+ name: LocalId,
650
+ *,
651
+ enums: list[EnumType],
652
+ structs: list[StructType],
653
+ tables: list[Table],
654
+ ) -> None: ...
655
+
656
+ def __init__(
657
+ self,
658
+ name: Optional[LocalId] = None,
659
+ *,
660
+ enums: Optional[list[EnumType]] = None,
661
+ structs: Optional[list[StructType]] = None,
662
+ tables: Optional[list[Table]] = None,
663
+ ) -> None:
664
+ self.name = name or LocalId("")
665
+ self.enums = ObjectDict(enums or [])
666
+ self.structs = ObjectDict(structs or [])
667
+ self.tables = ObjectDict(tables or [])
668
+
669
+ def __bool__(self) -> bool:
670
+ return len(self.enums) > 0 or len(self.structs) > 0 or len(self.tables) > 0
671
+
672
+ def get_referenced_namespaces(self) -> set[LocalId]:
673
+ items: set[SupportsQualifiedId] = set()
674
+ for s in self.structs.values():
675
+ items.update(s.get_referenced_types())
676
+ for t in self.tables.values():
677
+ items.update(t.get_referenced_types())
678
+ items.update(t.get_referenced_tables())
679
+ return set(LocalId(i.scope_id) for i in items if i.scope_id is not None)
680
+
681
+ def create_schema_stmt(self) -> Optional[str]:
682
+ if self.name.local_id:
683
+ return f"CREATE SCHEMA {self.name};"
684
+ else:
685
+ return None
686
+
687
+ def drop_schema_stmt(self) -> Optional[str]:
688
+ if self.name.local_id:
689
+ return f"DROP SCHEMA {self.name};"
690
+ else:
691
+ return None
692
+
693
+ def create_stmt(self) -> str:
694
+ return join([self.create_schema_stmt(), self.create_objects_stmt()])
695
+
696
+ def drop_stmt(self) -> str:
697
+ return join([self.drop_objects_stmt(), self.drop_schema_stmt()])
698
+
699
+ def create_objects_stmt(self) -> Optional[str]:
700
+ items: list[str] = []
701
+ items.extend(str(e) for e in self.enums.values())
702
+ items.extend(str(s) for s in self.structs.values())
703
+ items.extend(t.create_stmt() for t in self.tables.values())
704
+ return join_or_none(items)
705
+
706
+ def drop_objects_stmt(self) -> Optional[str]:
707
+ items: list[str] = []
708
+ items.extend(t.drop_stmt() for t in reversed(list(self.tables.values())))
709
+ items.extend(s.drop_stmt() for s in reversed(list(self.structs.values())))
710
+ items.extend(e.drop_stmt() for e in reversed(list(self.enums.values())))
711
+ return join_or_none(items)
712
+
713
+ def add_constraints_stmt(self) -> Optional[str]:
714
+ return join_or_none(table.add_constraints_stmt() for table in self.tables.values())
715
+
716
+ def drop_constraints_stmt(self) -> Optional[str]:
717
+ return join_or_none(table.drop_constraints_stmt() for table in self.tables.values())
718
+
719
+ def merge(self, op: "Namespace") -> None:
720
+ "Merges the contents of two objects."
721
+
722
+ for enum_name, op_enum in op.enums.items():
723
+ self_enum = self.enums.get(enum_name)
724
+ if self_enum is not None:
725
+ if self_enum != op_enum:
726
+ raise FormationError(f"different definitions for {self_enum.name} and {op_enum.name}")
727
+ else:
728
+ self.enums.add(copy.deepcopy(op_enum))
729
+
730
+ for struct_name, op_struct in op.structs.items():
731
+ self_struct = self.structs.get(struct_name)
732
+ if self_struct is not None:
733
+ if self_struct != op_struct:
734
+ raise FormationError(f"different definitions for {self_struct.name} and {op_struct.name}")
735
+ else:
736
+ self.structs.add(copy.deepcopy(op_struct))
737
+
738
+ for table_name, op_table in op.tables.items():
739
+ self_table = self.tables.get(table_name)
740
+ if self_table is not None:
741
+ if self_table != op_table:
742
+ raise FormationError(f"different definitions for {self_table.name} and {op_table.name}")
743
+ else:
744
+ self.tables.add(copy.deepcopy(op_table))
745
+
746
+ def __str__(self) -> str:
747
+ return self.create_stmt()
748
+
749
+
750
+ @dataclass
751
+ class Catalog(DatabaseObject):
752
+ """
753
+ A collection of database objects. Typically corresponds to a complete database.
754
+
755
+ For databases without namespace (schema) support, the only member in the collection is the empty string (`""`).
756
+
757
+ :param namespaces: Collection of namespaces (schemas) defined in the database.
758
+ """
759
+
760
+ namespaces: ObjectDict[Namespace]
761
+
762
+ def __init__(
763
+ self,
764
+ namespaces: Optional[list[Namespace]] = None,
765
+ ) -> None:
766
+ self.namespaces = ObjectDict(namespaces or [])
767
+
768
+ def __bool__(self) -> bool:
769
+ return len(self.namespaces) > 0
770
+
771
+ def get_table(self, table_id: SupportsQualifiedId) -> Table:
772
+ """
773
+ Looks up a table by its qualified name.
774
+
775
+ :param table_id: Identifies the table in the catalog.
776
+ :returns: The table identified by the qualified name.
777
+ """
778
+
779
+ if not self.namespaces:
780
+ raise MappingError("empty namespace")
781
+
782
+ return self.namespaces[table_id.scope_id or ""].tables[table_id.local_id]
783
+
784
+ def get_referenced_table(self, table_id: SupportsQualifiedId, column_id: LocalId) -> Table:
785
+ """
786
+ Looks up a table referenced by a foreign key column.
787
+
788
+ :param table_id: Identifies the table in the catalog.
789
+ :param column_id: Identifies the foreign key column.
790
+ :returns: The table in which the referenced primary key is.
791
+ """
792
+
793
+ table = self.get_table(table_id)
794
+ reference = table.get_constraint(column_id)
795
+ return self.get_table(reference.table)
796
+
797
+ def create_stmt(self) -> str:
798
+ self.sort()
799
+ items: list[Optional[str]] = []
800
+ items.extend(n.create_schema_stmt() for n in self.namespaces.values())
801
+ items.extend(n.create_objects_stmt() for n in self.namespaces.values())
802
+ return join(items)
803
+
804
+ def add_constraints_stmt(self) -> Optional[str]:
805
+ return join_or_none(n.add_constraints_stmt() for n in self.namespaces.values())
806
+
807
+ def drop_stmt(self) -> str:
808
+ self.sort()
809
+ items: list[Optional[str]] = []
810
+ items.extend(n.drop_objects_stmt() for n in reversed(list(self.namespaces.values())))
811
+ items.extend(n.drop_schema_stmt() for n in reversed(list(self.namespaces.values())))
812
+ return join(items)
813
+
814
+ def sort(self) -> None:
815
+ """
816
+ Sort namespaces in topological order of reference.
817
+
818
+ Namespaces that have no external references come first. The next group contains namespaces that reference
819
+ items only in the first group of namespaces, etc.
820
+ """
821
+
822
+ graph = {ns.name: ns.get_referenced_namespaces() for ns in self.namespaces.values()}
823
+ for ref_ns in graph.values():
824
+ ref_ns.intersection_update(graph.keys())
825
+ order = [name.local_id for name in topological_sort(graph)]
826
+ self.namespaces.reorder(order)
827
+
828
+ def merge(self, op: "Catalog") -> None:
829
+ "Merges the contents of two objects."
830
+
831
+ for name, op_ns in op.namespaces.items():
832
+ if name in self.namespaces:
833
+ self.namespaces[name].merge(op_ns)
834
+ else:
835
+ self.namespaces.add(copy.deepcopy(op_ns))
836
+
837
+ def __str__(self) -> str:
838
+ return join([self.create_stmt(), self.add_constraints_stmt()])
839
+
840
+
841
+ class ObjectFactory:
842
+ "Creates new column, table, struct and namespace instances."
843
+
844
+ @property
845
+ def column_class(self) -> type[Column]:
846
+ "The object type instantiated for table columns."
847
+
848
+ return Column
849
+
850
+ @property
851
+ def table_class(self) -> type[Table]:
852
+ "The object type instantiated for tables."
853
+
854
+ return Table
855
+
856
+ @property
857
+ def enum_table_class(self) -> type[EnumTable]:
858
+ "The object type instantiated for tables that store enumeration values."
859
+
860
+ return EnumTable
861
+
862
+ @property
863
+ def struct_class(self) -> type[StructType]:
864
+ "The object type instantiated for struct types."
865
+
866
+ return StructType
867
+
868
+ @property
869
+ def namespace_class(self) -> type[Namespace]:
870
+ "The object type instantiated for namespaces (schemas)."
871
+
872
+ return Namespace