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.
- instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
- pysqlsync/__init__.py +15 -0
- pysqlsync/base.py +1386 -0
- pysqlsync/connection.py +101 -0
- pysqlsync/data/__init__.py +0 -0
- pysqlsync/data/exchange.py +184 -0
- pysqlsync/data/generator.py +305 -0
- pysqlsync/dialect/README.md +35 -0
- pysqlsync/dialect/__init__.py +0 -0
- pysqlsync/dialect/delta/__init__.py +0 -0
- pysqlsync/dialect/delta/data_types.py +51 -0
- pysqlsync/dialect/delta/dependency.py +7 -0
- pysqlsync/dialect/delta/engine.py +18 -0
- pysqlsync/dialect/delta/generator.py +74 -0
- pysqlsync/dialect/delta/object_types.py +87 -0
- pysqlsync/dialect/mssql/README.md +23 -0
- pysqlsync/dialect/mssql/__init__.py +0 -0
- pysqlsync/dialect/mssql/connection.py +146 -0
- pysqlsync/dialect/mssql/data_types.py +113 -0
- pysqlsync/dialect/mssql/dependency.py +9 -0
- pysqlsync/dialect/mssql/discovery.py +99 -0
- pysqlsync/dialect/mssql/engine.py +20 -0
- pysqlsync/dialect/mssql/generator.py +88 -0
- pysqlsync/dialect/mssql/mutation.py +46 -0
- pysqlsync/dialect/mssql/object_types.py +135 -0
- pysqlsync/dialect/mysql/__init__.py +0 -0
- pysqlsync/dialect/mysql/connection.py +126 -0
- pysqlsync/dialect/mysql/data_types.py +40 -0
- pysqlsync/dialect/mysql/dependency.py +10 -0
- pysqlsync/dialect/mysql/discovery.py +145 -0
- pysqlsync/dialect/mysql/engine.py +20 -0
- pysqlsync/dialect/mysql/generator.py +140 -0
- pysqlsync/dialect/mysql/mutation.py +65 -0
- pysqlsync/dialect/mysql/object_types.py +75 -0
- pysqlsync/dialect/oracle/README.md +22 -0
- pysqlsync/dialect/oracle/__init__.py +0 -0
- pysqlsync/dialect/oracle/connection.py +141 -0
- pysqlsync/dialect/oracle/data_types.py +104 -0
- pysqlsync/dialect/oracle/dependency.py +9 -0
- pysqlsync/dialect/oracle/discovery.py +231 -0
- pysqlsync/dialect/oracle/engine.py +20 -0
- pysqlsync/dialect/oracle/generator.py +93 -0
- pysqlsync/dialect/oracle/mutation.py +37 -0
- pysqlsync/dialect/oracle/object_types.py +59 -0
- pysqlsync/dialect/postgresql/__init__.py +0 -0
- pysqlsync/dialect/postgresql/connection.py +133 -0
- pysqlsync/dialect/postgresql/data_types.py +6 -0
- pysqlsync/dialect/postgresql/dependency.py +9 -0
- pysqlsync/dialect/postgresql/discovery.py +392 -0
- pysqlsync/dialect/postgresql/engine.py +20 -0
- pysqlsync/dialect/postgresql/generator.py +99 -0
- pysqlsync/dialect/postgresql/mutation.py +82 -0
- pysqlsync/dialect/postgresql/object_types.py +99 -0
- pysqlsync/dialect/redshift/__init__.py +0 -0
- pysqlsync/dialect/redshift/connection.py +156 -0
- pysqlsync/dialect/redshift/data_types.py +10 -0
- pysqlsync/dialect/redshift/dependency.py +9 -0
- pysqlsync/dialect/redshift/engine.py +20 -0
- pysqlsync/dialect/redshift/generator.py +82 -0
- pysqlsync/dialect/snowflake/__init__.py +0 -0
- pysqlsync/dialect/snowflake/data_types.py +18 -0
- pysqlsync/dialect/snowflake/dependency.py +9 -0
- pysqlsync/dialect/snowflake/engine.py +18 -0
- pysqlsync/dialect/snowflake/generator.py +64 -0
- pysqlsync/dialect/snowflake/object_types.py +84 -0
- pysqlsync/dialect/test/__init__.py +0 -0
- pysqlsync/dialect/test/dependency.py +10 -0
- pysqlsync/dialect/trino/__init__.py +0 -0
- pysqlsync/dialect/trino/connection.py +86 -0
- pysqlsync/dialect/trino/dependency.py +9 -0
- pysqlsync/dialect/trino/discovery.py +21 -0
- pysqlsync/dialect/trino/engine.py +20 -0
- pysqlsync/dialect/trino/generator.py +31 -0
- pysqlsync/factory.py +169 -0
- pysqlsync/formation/__init__.py +0 -0
- pysqlsync/formation/constraints.py +78 -0
- pysqlsync/formation/data_types.py +192 -0
- pysqlsync/formation/discovery.py +390 -0
- pysqlsync/formation/inspection.py +179 -0
- pysqlsync/formation/mutation.py +320 -0
- pysqlsync/formation/object_dict.py +79 -0
- pysqlsync/formation/object_types.py +872 -0
- pysqlsync/formation/py_to_sql.py +1134 -0
- pysqlsync/formation/sql_to_py.py +194 -0
- pysqlsync/model/__init__.py +0 -0
- pysqlsync/model/data_types.py +397 -0
- pysqlsync/model/entity_types.py +68 -0
- pysqlsync/model/id_types.py +193 -0
- pysqlsync/model/key_types.py +38 -0
- pysqlsync/model/properties.py +234 -0
- pysqlsync/py.typed +0 -0
- pysqlsync/python_types.py +192 -0
- pysqlsync/resultset.py +106 -0
- pysqlsync/util/__init__.py +0 -0
- pysqlsync/util/dataclasses.py +80 -0
- pysqlsync/util/dispatch.py +28 -0
- 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
|