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,1134 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import dataclasses
|
|
3
|
+
import datetime
|
|
4
|
+
import decimal
|
|
5
|
+
import enum
|
|
6
|
+
import inspect
|
|
7
|
+
import ipaddress
|
|
8
|
+
import sys
|
|
9
|
+
import types
|
|
10
|
+
import typing
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Annotated, Any, Callable, Iterable, Optional, TypeVar
|
|
13
|
+
|
|
14
|
+
from strong_typing.auxiliary import MaxLength, float32, float64, int8, int16, int32, int64, uint8, uint16, uint32, uint64
|
|
15
|
+
from strong_typing.core import JsonType
|
|
16
|
+
from strong_typing.docstring import Docstring, parse_type
|
|
17
|
+
from strong_typing.inspection import (
|
|
18
|
+
DataclassField,
|
|
19
|
+
DataclassInstance,
|
|
20
|
+
TypeLike,
|
|
21
|
+
dataclass_fields,
|
|
22
|
+
enum_value_types,
|
|
23
|
+
evaluate_member_type,
|
|
24
|
+
get_referenced_types,
|
|
25
|
+
is_dataclass_type,
|
|
26
|
+
is_generic_list,
|
|
27
|
+
is_generic_set,
|
|
28
|
+
is_type_enum,
|
|
29
|
+
is_type_literal,
|
|
30
|
+
is_type_optional,
|
|
31
|
+
is_type_union,
|
|
32
|
+
unwrap_annotated_type,
|
|
33
|
+
unwrap_generic_list,
|
|
34
|
+
unwrap_generic_set,
|
|
35
|
+
unwrap_literal_types,
|
|
36
|
+
unwrap_literal_values,
|
|
37
|
+
unwrap_optional_type,
|
|
38
|
+
unwrap_union_types,
|
|
39
|
+
)
|
|
40
|
+
from strong_typing.topological import type_topological_sort
|
|
41
|
+
|
|
42
|
+
from ..model.data_types import (
|
|
43
|
+
CompatibilityError,
|
|
44
|
+
SqlArrayType,
|
|
45
|
+
SqlBooleanType,
|
|
46
|
+
SqlDataType,
|
|
47
|
+
SqlDateType,
|
|
48
|
+
SqlDecimalType,
|
|
49
|
+
SqlDoubleType,
|
|
50
|
+
SqlEnumType,
|
|
51
|
+
SqlFixedCharacterType,
|
|
52
|
+
SqlFloatType,
|
|
53
|
+
SqlIntegerType,
|
|
54
|
+
SqlIntervalType,
|
|
55
|
+
SqlJsonType,
|
|
56
|
+
SqlRealType,
|
|
57
|
+
SqlStructMember,
|
|
58
|
+
SqlStructType,
|
|
59
|
+
SqlTimestampType,
|
|
60
|
+
SqlTimeType,
|
|
61
|
+
SqlUserDefinedType,
|
|
62
|
+
SqlUuidType,
|
|
63
|
+
SqlVariableCharacterType,
|
|
64
|
+
compatible_type,
|
|
65
|
+
constant,
|
|
66
|
+
)
|
|
67
|
+
from ..model.id_types import GlobalId, LocalId, PrefixedId, QualifiedId, SupportsQualifiedId
|
|
68
|
+
from ..model.properties import get_field_properties
|
|
69
|
+
from .inspection import (
|
|
70
|
+
dataclass_primary_key_name,
|
|
71
|
+
dataclass_primary_key_type,
|
|
72
|
+
get_entity_types,
|
|
73
|
+
is_entity_type,
|
|
74
|
+
is_reference_type,
|
|
75
|
+
is_simple_type,
|
|
76
|
+
is_struct_type,
|
|
77
|
+
reference_to_key,
|
|
78
|
+
)
|
|
79
|
+
from .object_types import (
|
|
80
|
+
Catalog,
|
|
81
|
+
CheckConstraint,
|
|
82
|
+
Column,
|
|
83
|
+
Constraint,
|
|
84
|
+
ConstraintReference,
|
|
85
|
+
DiscriminatedConstraint,
|
|
86
|
+
EnumType,
|
|
87
|
+
ForeignConstraint,
|
|
88
|
+
ObjectFactory,
|
|
89
|
+
StructMember,
|
|
90
|
+
StructType,
|
|
91
|
+
Table,
|
|
92
|
+
UniqueConstraint,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
T = TypeVar("T")
|
|
96
|
+
|
|
97
|
+
ENUM_NAME_LENGTH: int = 64
|
|
98
|
+
"Maximum length for an enumeration string value."
|
|
99
|
+
|
|
100
|
+
ENUM_LABEL_TYPE = Annotated[str, MaxLength(ENUM_NAME_LENGTH)]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def enum_value_type(enum_type: type[enum.Enum]) -> type:
|
|
104
|
+
value_types = enum_value_types(enum_type)
|
|
105
|
+
if len(value_types) > 1:
|
|
106
|
+
raise TypeError(f"inconsistent enumeration value types for type {enum_type.__name__}: {value_types}")
|
|
107
|
+
value_type = value_types.pop()
|
|
108
|
+
return value_type if value_type is not str else ENUM_LABEL_TYPE
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_type_enum_list(typ: TypeLike) -> bool:
|
|
112
|
+
if not is_generic_list(typ):
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
elem_type = unwrap_generic_list(typ)
|
|
116
|
+
return is_type_enum(elem_type)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_type_enum_set(typ: TypeLike) -> bool:
|
|
120
|
+
if not is_generic_set(typ):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
elem_type = unwrap_generic_set(typ)
|
|
124
|
+
return is_type_enum(elem_type)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_extensible_enum_type(typ: TypeLike, cls: type) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
True if the type represents an extensible enumeration type.
|
|
130
|
+
|
|
131
|
+
:param typ: A type to check.
|
|
132
|
+
:param cls: Context in which to evaluate the type (e.g. a class).
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if not is_type_union(typ):
|
|
136
|
+
return False
|
|
137
|
+
typ = unwrap_annotated_type(typ)
|
|
138
|
+
member_types = [evaluate_member_type(t, cls) for t in unwrap_union_types(typ)]
|
|
139
|
+
for member_type in member_types:
|
|
140
|
+
if member_type is None:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
member_type = unwrap_annotated_type(member_type)
|
|
144
|
+
if member_type is not str and not is_type_enum(member_type):
|
|
145
|
+
return False
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def unwrap_extensible_enum_type(typ: TypeLike, cls: type) -> type[enum.Enum]:
|
|
150
|
+
if is_type_union(typ):
|
|
151
|
+
member_types = [evaluate_member_type(t, cls) for t in unwrap_union_types(typ)]
|
|
152
|
+
for member_type in member_types:
|
|
153
|
+
if is_type_enum(member_type):
|
|
154
|
+
return member_type
|
|
155
|
+
raise TypeError("extensible enum types are of the form `E | str` where `E` is a subclass of `enum.Enum`")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def dataclass_fields_as_required(
|
|
159
|
+
cls: type[DataclassInstance], pred: Optional[Callable[[TypeLike], bool]] = None
|
|
160
|
+
) -> Iterable[DataclassField]:
|
|
161
|
+
for field in dataclass_fields(cls):
|
|
162
|
+
props = get_field_properties(field.type)
|
|
163
|
+
ref = evaluate_member_type(props.field_type, cls)
|
|
164
|
+
if pred is None or pred(ref):
|
|
165
|
+
yield DataclassField(field.name, ref, field.default)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _topological_sort(class_types: list[type]) -> list[type]:
|
|
169
|
+
"""
|
|
170
|
+
Returns a list of types in topological order.
|
|
171
|
+
|
|
172
|
+
Types that don't depend on other types (i.e. fundamental types) are first. Types on which no other types depend are last.
|
|
173
|
+
|
|
174
|
+
:param types: A list of types (simple or composite).
|
|
175
|
+
:returns: A list of the same types but in topological order.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
return [t for t in type_topological_sort(class_types) if t in class_types]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclasses.dataclass
|
|
182
|
+
class DataclassEnumField:
|
|
183
|
+
name: str
|
|
184
|
+
type: type[enum.Enum]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class NamespaceMapping:
|
|
188
|
+
"""
|
|
189
|
+
Associates Python modules with SQL namespaces (schemas).
|
|
190
|
+
|
|
191
|
+
:param dictionary: Maps a Python module to a SQL namespace, or `None` to use the default namespace.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
dictionary: dict[types.ModuleType, Optional[str]]
|
|
195
|
+
|
|
196
|
+
def __init__(self, dictionary: Optional[dict[types.ModuleType, Optional[str]]] = None) -> None:
|
|
197
|
+
if dictionary is not None:
|
|
198
|
+
if len(set(dictionary.values())) != len(dictionary):
|
|
199
|
+
raise ValueError("expected: a one-to-one mapping between Python modules and database schema names")
|
|
200
|
+
self.dictionary = dictionary
|
|
201
|
+
else:
|
|
202
|
+
self.dictionary = {}
|
|
203
|
+
|
|
204
|
+
def get(self, name: str) -> Optional[str]:
|
|
205
|
+
module = sys.modules[name]
|
|
206
|
+
if module in self.dictionary:
|
|
207
|
+
return self.dictionary[module] # include special return value `None`
|
|
208
|
+
else:
|
|
209
|
+
raise ValueError(f"expected: database schema name mapping for module: {module.__name__}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@enum.unique
|
|
213
|
+
class EnumMode(enum.Enum):
|
|
214
|
+
"Determines whether enumeration types are converted as SQL ENUM, a foreign/primary key relation, or a CHECK constraint."
|
|
215
|
+
|
|
216
|
+
TYPE = "type"
|
|
217
|
+
"Enumeration types are converted to SQL ENUM types with CREATE TYPE ... AS ENUM ( ... )."
|
|
218
|
+
|
|
219
|
+
INLINE = "inline"
|
|
220
|
+
"Enumeration types are converted into inline SQL ENUM definitions."
|
|
221
|
+
|
|
222
|
+
RELATION = "relation"
|
|
223
|
+
"Enumeration types are converted into foreign/primary key relations (reference constraint) with a lookup table."
|
|
224
|
+
|
|
225
|
+
CHECK = "check"
|
|
226
|
+
"Enumeration types are converted into their value type, with a possible CHECK constraint on the column."
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@enum.unique
|
|
230
|
+
class StructMode(enum.Enum):
|
|
231
|
+
"Determines how to convert structure types that are not mapped into SQL tables."
|
|
232
|
+
|
|
233
|
+
TYPE = "type"
|
|
234
|
+
"Dataclass types are converted into SQL types with CREATE TYPE ... AS ( ... )."
|
|
235
|
+
|
|
236
|
+
INLINE = "inline"
|
|
237
|
+
"Dataclass types are converted into embedded nested type with STRUCT < ... >."
|
|
238
|
+
|
|
239
|
+
JSON = "json"
|
|
240
|
+
"Dataclass types are converted into SQL JSON type (or text)."
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@enum.unique
|
|
244
|
+
class ArrayMode(enum.Enum):
|
|
245
|
+
"Determines how to treat sequence types."
|
|
246
|
+
|
|
247
|
+
ARRAY = "array"
|
|
248
|
+
"Convert Python list type as SQL array type."
|
|
249
|
+
|
|
250
|
+
JSON = "json"
|
|
251
|
+
"Convert Python list type to SQL JSON type (or its representation type)."
|
|
252
|
+
|
|
253
|
+
RELATION = "relation"
|
|
254
|
+
"Flatten Python list type to a separate table."
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclasses.dataclass
|
|
258
|
+
class DataclassConverterOptions:
|
|
259
|
+
"""
|
|
260
|
+
Configuration options for generating a SQL table definition from a Python dataclass.
|
|
261
|
+
|
|
262
|
+
:param enum_mode: Conversion mode for enumeration types.
|
|
263
|
+
:param struct_mode: Conversion mode for structure types that are not entities.
|
|
264
|
+
:param array_mode: Conversion mode for array types.
|
|
265
|
+
:param extra_numeric_types: Whether to use extra numeric types like `int1` or `uint4`.
|
|
266
|
+
:param qualified_names: Whether to use fully qualified names (True) or string-prefixed names (False).
|
|
267
|
+
:param unique_constraint_names: Whether to generate constraint names that are globally unique.
|
|
268
|
+
:param namespaces: Maps Python modules to SQL namespaces (schemas).
|
|
269
|
+
:param foreign_constraints: Whether to create foreign/primary key relationships between tables.
|
|
270
|
+
:param check_constraints: Whether to create check constraints for table columns with a restricted set of values.
|
|
271
|
+
:param initialize_tables: Whether to populate special tables (e.g. enumerations) with data.
|
|
272
|
+
:param substitutions: SQL type to be substituted for a specific Python type.
|
|
273
|
+
:param factory: Creates new column, table, struct and namespace instances.
|
|
274
|
+
:param skip_annotations: Annotation classes to ignore on table column types.
|
|
275
|
+
:param auto_default: Automatically assign a default value to non-nullable types.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
enum_mode: EnumMode = EnumMode.TYPE
|
|
279
|
+
struct_mode: StructMode = StructMode.TYPE
|
|
280
|
+
array_mode: ArrayMode = ArrayMode.ARRAY
|
|
281
|
+
extra_numeric_types: bool = False
|
|
282
|
+
qualified_names: bool = True
|
|
283
|
+
unique_constraint_names: bool = True
|
|
284
|
+
namespaces: NamespaceMapping = dataclasses.field(default_factory=NamespaceMapping)
|
|
285
|
+
foreign_constraints: bool = True
|
|
286
|
+
check_constraints: bool = True
|
|
287
|
+
initialize_tables: bool = False
|
|
288
|
+
substitutions: dict[TypeLike, SqlDataType] = dataclasses.field(default_factory=dict[TypeLike, SqlDataType])
|
|
289
|
+
factory: ObjectFactory = dataclasses.field(default_factory=ObjectFactory)
|
|
290
|
+
skip_annotations: tuple[type, ...] = ()
|
|
291
|
+
auto_default: bool = False
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class DataclassConverter:
|
|
295
|
+
options: DataclassConverterOptions
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
options: Optional[DataclassConverterOptions] = None,
|
|
301
|
+
):
|
|
302
|
+
if options is not None:
|
|
303
|
+
self.options = options
|
|
304
|
+
else:
|
|
305
|
+
self.options = DataclassConverterOptions()
|
|
306
|
+
|
|
307
|
+
def create_qualified_id(self, module_name: str, object_name: str) -> SupportsQualifiedId:
|
|
308
|
+
mapped_name = self.options.namespaces.get(module_name)
|
|
309
|
+
if self.options.qualified_names:
|
|
310
|
+
return QualifiedId(mapped_name, object_name)
|
|
311
|
+
else:
|
|
312
|
+
return PrefixedId(mapped_name, object_name)
|
|
313
|
+
|
|
314
|
+
def create_qualified_prefix(self, cls: type) -> str:
|
|
315
|
+
if self.options.unique_constraint_names:
|
|
316
|
+
id = self.create_qualified_id(cls.__module__, cls.__name__)
|
|
317
|
+
return id.compact_id.replace(".", "_")
|
|
318
|
+
else:
|
|
319
|
+
return cls.__name__
|
|
320
|
+
|
|
321
|
+
def create_foreign_key_name(self, cls: type, field: str) -> str:
|
|
322
|
+
return f"fk_{self.create_qualified_prefix(cls)}_{field}"
|
|
323
|
+
|
|
324
|
+
def create_discriminated_key_name(self, cls: type, field: str) -> str:
|
|
325
|
+
return f"dk_{self.create_qualified_prefix(cls)}_{field}"
|
|
326
|
+
|
|
327
|
+
def create_unique_key_name(self, cls: type, field: str) -> str:
|
|
328
|
+
return f"uq_{self.create_qualified_prefix(cls)}_{field}"
|
|
329
|
+
|
|
330
|
+
def create_check_name(self, cls: type, field: str) -> str:
|
|
331
|
+
return f"ch_{self.create_qualified_prefix(cls)}_{field}"
|
|
332
|
+
|
|
333
|
+
def simple_type_to_sql_data_type(self, typ: TypeLike) -> SqlDataType:
|
|
334
|
+
substitute = self.options.substitutions.get(typ)
|
|
335
|
+
if substitute is not None:
|
|
336
|
+
return substitute
|
|
337
|
+
|
|
338
|
+
if typ is bool:
|
|
339
|
+
return SqlBooleanType()
|
|
340
|
+
if typ is int:
|
|
341
|
+
return SqlIntegerType(8)
|
|
342
|
+
if typ is float:
|
|
343
|
+
return SqlDoubleType()
|
|
344
|
+
if typ is int8:
|
|
345
|
+
if self.options.extra_numeric_types:
|
|
346
|
+
return SqlUserDefinedType(GlobalId("int1"))
|
|
347
|
+
else:
|
|
348
|
+
return SqlIntegerType(2)
|
|
349
|
+
if typ is int16:
|
|
350
|
+
return SqlIntegerType(2)
|
|
351
|
+
if typ is int32:
|
|
352
|
+
return SqlIntegerType(4)
|
|
353
|
+
if typ is int64:
|
|
354
|
+
return SqlIntegerType(8)
|
|
355
|
+
if typ is uint8:
|
|
356
|
+
if self.options.extra_numeric_types:
|
|
357
|
+
return SqlUserDefinedType(GlobalId("uint1"))
|
|
358
|
+
else:
|
|
359
|
+
return SqlIntegerType(2)
|
|
360
|
+
if typ is uint16:
|
|
361
|
+
if self.options.extra_numeric_types:
|
|
362
|
+
return SqlUserDefinedType(GlobalId("uint2"))
|
|
363
|
+
else:
|
|
364
|
+
return SqlIntegerType(2)
|
|
365
|
+
if typ is uint32:
|
|
366
|
+
if self.options.extra_numeric_types:
|
|
367
|
+
return SqlUserDefinedType(GlobalId("uint4"))
|
|
368
|
+
else:
|
|
369
|
+
return SqlIntegerType(4)
|
|
370
|
+
if typ is uint64:
|
|
371
|
+
if self.options.extra_numeric_types:
|
|
372
|
+
return SqlUserDefinedType(GlobalId("uint8"))
|
|
373
|
+
else:
|
|
374
|
+
return SqlIntegerType(8)
|
|
375
|
+
if typ is float32:
|
|
376
|
+
return SqlRealType()
|
|
377
|
+
if typ is float64:
|
|
378
|
+
return SqlDoubleType()
|
|
379
|
+
if typ is str:
|
|
380
|
+
return SqlVariableCharacterType()
|
|
381
|
+
if typ is decimal.Decimal:
|
|
382
|
+
return SqlDecimalType()
|
|
383
|
+
if typ is datetime.datetime:
|
|
384
|
+
return SqlTimestampType()
|
|
385
|
+
if typ is datetime.date:
|
|
386
|
+
return SqlDateType()
|
|
387
|
+
if typ is datetime.time:
|
|
388
|
+
return SqlTimeType()
|
|
389
|
+
if typ is datetime.timedelta:
|
|
390
|
+
return SqlIntervalType()
|
|
391
|
+
if typ is uuid.UUID:
|
|
392
|
+
return SqlUuidType()
|
|
393
|
+
if typ is JsonType:
|
|
394
|
+
return SqlJsonType()
|
|
395
|
+
if typ is ipaddress.IPv4Address or typ is ipaddress.IPv6Address:
|
|
396
|
+
return SqlUserDefinedType(GlobalId("inet")) # PostgreSQL only
|
|
397
|
+
|
|
398
|
+
metadata = getattr(typ, "__metadata__", None)
|
|
399
|
+
if metadata:
|
|
400
|
+
unadorned_type = unwrap_annotated_type(typ)
|
|
401
|
+
substitute = self.options.substitutions.get(unadorned_type)
|
|
402
|
+
if substitute is not None:
|
|
403
|
+
sql_type = copy.copy(substitute)
|
|
404
|
+
elif unadorned_type is int:
|
|
405
|
+
sql_type = SqlIntegerType(8)
|
|
406
|
+
elif unadorned_type is str:
|
|
407
|
+
sql_type = SqlVariableCharacterType()
|
|
408
|
+
elif unadorned_type is float:
|
|
409
|
+
sql_type = SqlFloatType()
|
|
410
|
+
elif unadorned_type is decimal.Decimal:
|
|
411
|
+
sql_type = SqlDecimalType()
|
|
412
|
+
elif unadorned_type is datetime.datetime:
|
|
413
|
+
sql_type = SqlTimestampType()
|
|
414
|
+
elif unadorned_type is datetime.time:
|
|
415
|
+
sql_type = SqlTimeType()
|
|
416
|
+
else:
|
|
417
|
+
# avoid error when only skipped annotations are applied to a custom type
|
|
418
|
+
sql_type = None
|
|
419
|
+
|
|
420
|
+
for meta in metadata:
|
|
421
|
+
if isinstance(meta, self.options.skip_annotations):
|
|
422
|
+
continue
|
|
423
|
+
if sql_type is None:
|
|
424
|
+
raise TypeError(f"unsupported annotated Python type: {typ}")
|
|
425
|
+
sql_type.parse_meta(meta)
|
|
426
|
+
|
|
427
|
+
if sql_type:
|
|
428
|
+
return sql_type
|
|
429
|
+
|
|
430
|
+
typ = unadorned_type
|
|
431
|
+
|
|
432
|
+
if is_type_enum(typ):
|
|
433
|
+
value_type = enum_value_type(typ)
|
|
434
|
+
unadorned_value_type = unwrap_annotated_type(value_type)
|
|
435
|
+
|
|
436
|
+
if unadorned_value_type is int or unadorned_value_type is str:
|
|
437
|
+
if self.options.enum_mode is EnumMode.TYPE:
|
|
438
|
+
return SqlUserDefinedType(self.create_qualified_id(typ.__module__, typ.__name__))
|
|
439
|
+
elif self.options.enum_mode is EnumMode.INLINE:
|
|
440
|
+
return SqlEnumType([str(e.value) for e in typ])
|
|
441
|
+
elif self.options.enum_mode is EnumMode.RELATION:
|
|
442
|
+
return self._enumeration_key_type()
|
|
443
|
+
elif self.options.enum_mode is EnumMode.CHECK:
|
|
444
|
+
return self.simple_type_to_sql_data_type(value_type)
|
|
445
|
+
else:
|
|
446
|
+
raise NotImplementedError(f"match not exhaustive: {EnumMode}")
|
|
447
|
+
else:
|
|
448
|
+
if self.options.enum_mode is EnumMode.RELATION:
|
|
449
|
+
return self._enumeration_key_type()
|
|
450
|
+
else:
|
|
451
|
+
raise TypeError(
|
|
452
|
+
f"enumeration mode {self.options.enum_mode} not valid for complex member types in enumeration `{typ.__name__}`"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
raise TypeError(f"not a simple type: {typ}")
|
|
456
|
+
|
|
457
|
+
def _enumeration_key_type(self) -> SqlDataType:
|
|
458
|
+
"Key type for storing enumeration values in a dedicated table."
|
|
459
|
+
|
|
460
|
+
return self.simple_type_to_sql_data_type(int32)
|
|
461
|
+
|
|
462
|
+
def _get_relationship(self, field_type: TypeLike) -> Optional[tuple[type, TypeLike]]:
|
|
463
|
+
"""
|
|
464
|
+
Returns relationship type and field type if the field expands into a separate relation (join table).
|
|
465
|
+
|
|
466
|
+
Join tables hold many-to-many relationships. For example, assume a table `Student` has a field `guardians`
|
|
467
|
+
of type `list[Person]`. This would expand into a separate table where each record holds:
|
|
468
|
+
|
|
469
|
+
1. a unique identifier
|
|
470
|
+
2. a foreign key to the originating table `Student`
|
|
471
|
+
3. a foreign key to the table `Person`
|
|
472
|
+
|
|
473
|
+
Likewise, a field of type `list[enum.Enum]` with options `ArrayMode.RELATION` and `EnumMode.RELATION` would
|
|
474
|
+
expand to a table of
|
|
475
|
+
|
|
476
|
+
1. a unique identifier
|
|
477
|
+
2. a foreign key to the originating table
|
|
478
|
+
3. a foreign key to the lookup table with all possible enumeration values
|
|
479
|
+
|
|
480
|
+
:param field_type: A type to check.
|
|
481
|
+
:returns: A tuple of host relationship type (e.g. list) and host field type.
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
if is_type_optional(field_type):
|
|
485
|
+
field_type = unwrap_optional_type(field_type)
|
|
486
|
+
|
|
487
|
+
field_type = unwrap_annotated_type(field_type)
|
|
488
|
+
|
|
489
|
+
# relations must be one-to-many
|
|
490
|
+
relation_type: type
|
|
491
|
+
elem_type: TypeLike
|
|
492
|
+
if is_generic_list(field_type):
|
|
493
|
+
relation_type = list
|
|
494
|
+
elem_type = unwrap_generic_list(field_type)
|
|
495
|
+
elif is_generic_set(field_type):
|
|
496
|
+
relation_type = set
|
|
497
|
+
elem_type = unwrap_generic_set(field_type)
|
|
498
|
+
else:
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
if self.options.array_mode is ArrayMode.RELATION or is_entity_type(elem_type):
|
|
502
|
+
return relation_type, elem_type
|
|
503
|
+
else:
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
def member_to_sql_data_type(self, typ: TypeLike, cls: type) -> SqlDataType:
|
|
507
|
+
"""
|
|
508
|
+
Maps a native Python type into a SQL data type.
|
|
509
|
+
|
|
510
|
+
:param typ: The type to convert, typically a dataclass member type.
|
|
511
|
+
:param cls: The context for the type, typically the dataclass in which the member is defined.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
substitute = self.options.substitutions.get(typ)
|
|
515
|
+
if substitute is not None:
|
|
516
|
+
return substitute
|
|
517
|
+
|
|
518
|
+
if is_simple_type(typ):
|
|
519
|
+
return self.simple_type_to_sql_data_type(typ)
|
|
520
|
+
if is_entity_type(typ):
|
|
521
|
+
# a many-to-one relationship to another entity class
|
|
522
|
+
key_field_type = dataclass_primary_key_type(typ)
|
|
523
|
+
return self.member_to_sql_data_type(key_field_type, typ)
|
|
524
|
+
if is_dataclass_type(typ):
|
|
525
|
+
# an embedded user-defined type
|
|
526
|
+
if self.options.struct_mode is StructMode.TYPE:
|
|
527
|
+
return SqlUserDefinedType(self.create_qualified_id(typ.__module__, typ.__name__))
|
|
528
|
+
elif self.options.struct_mode is StructMode.INLINE:
|
|
529
|
+
inline_types: list[SqlStructMember] = []
|
|
530
|
+
for field in dataclass_fields(unwrap_annotated_type(typ)):
|
|
531
|
+
inline_props = get_field_properties(field.type)
|
|
532
|
+
inline_type = self.member_to_sql_data_type(inline_props.field_type, typ)
|
|
533
|
+
inline_types.append(SqlStructMember(LocalId(field.name), inline_type, inline_props.nullable))
|
|
534
|
+
return SqlStructType(inline_types)
|
|
535
|
+
elif self.options.struct_mode is StructMode.JSON:
|
|
536
|
+
return self.member_to_sql_data_type(JsonType, cls)
|
|
537
|
+
if isinstance(typ, typing.ForwardRef):
|
|
538
|
+
return self.member_to_sql_data_type(evaluate_member_type(typ, cls), cls)
|
|
539
|
+
if is_type_literal(typ):
|
|
540
|
+
literal_types = unwrap_literal_types(typ)
|
|
541
|
+
if not all(t is str for t in literal_types):
|
|
542
|
+
sql_literal_types = [self.member_to_sql_data_type(t, cls) for t in literal_types]
|
|
543
|
+
else:
|
|
544
|
+
literal_values = unwrap_literal_values(typ)
|
|
545
|
+
sql_literal_types = [SqlFixedCharacterType(limit=len(v)) for v in literal_values]
|
|
546
|
+
return compatible_type(sql_literal_types)
|
|
547
|
+
if is_generic_list(typ) or is_generic_set(typ):
|
|
548
|
+
if is_generic_set(typ):
|
|
549
|
+
item_type = unwrap_generic_set(typ)
|
|
550
|
+
elif is_generic_list(typ):
|
|
551
|
+
item_type = unwrap_generic_list(typ)
|
|
552
|
+
else:
|
|
553
|
+
raise NotImplementedError("match not exhaustive: list | set")
|
|
554
|
+
if is_simple_type(item_type):
|
|
555
|
+
if self.options.array_mode is ArrayMode.ARRAY:
|
|
556
|
+
return SqlArrayType(self.simple_type_to_sql_data_type(item_type))
|
|
557
|
+
elif self.options.array_mode is ArrayMode.JSON:
|
|
558
|
+
return self.member_to_sql_data_type(JsonType, cls)
|
|
559
|
+
elif self.options.array_mode is ArrayMode.RELATION:
|
|
560
|
+
raise TypeError("use a join table for flattening a list of values into a separate table")
|
|
561
|
+
if is_entity_type(item_type):
|
|
562
|
+
# list of entity type (i.e. type with primary key) cannot become SQL array (only list of struct type can)
|
|
563
|
+
raise TypeError(f"use a join table; cannot convert list of entity type to SQL array: {typ}")
|
|
564
|
+
if isinstance(item_type, type):
|
|
565
|
+
if self.options.array_mode is ArrayMode.ARRAY:
|
|
566
|
+
return SqlArrayType(SqlUserDefinedType(self.create_qualified_id(item_type.__module__, item_type.__name__)))
|
|
567
|
+
elif self.options.array_mode is ArrayMode.JSON:
|
|
568
|
+
return self.member_to_sql_data_type(JsonType, cls)
|
|
569
|
+
elif self.options.array_mode is ArrayMode.RELATION:
|
|
570
|
+
raise TypeError("use a join table for flattening a list of struct type into a separate table")
|
|
571
|
+
|
|
572
|
+
raise TypeError(f"unsupported array data type: {item_type}")
|
|
573
|
+
if is_extensible_enum_type(typ, cls):
|
|
574
|
+
return self._enumeration_key_type()
|
|
575
|
+
if is_type_union(typ):
|
|
576
|
+
member_types = [evaluate_member_type(t, cls) for t in unwrap_union_types(unwrap_annotated_type(typ))]
|
|
577
|
+
if all(is_entity_type(t) for t in member_types):
|
|
578
|
+
# discriminated union type
|
|
579
|
+
primary_key_types = set(dataclass_primary_key_type(t) for t in member_types)
|
|
580
|
+
if len(primary_key_types) > 1:
|
|
581
|
+
raise TypeError(f"mismatching key types in discriminated union of: {member_types}")
|
|
582
|
+
common_key_type = primary_key_types.pop()
|
|
583
|
+
return self.member_to_sql_data_type(common_key_type, cls)
|
|
584
|
+
|
|
585
|
+
sql_member_types = [self.member_to_sql_data_type(t, cls) for t in member_types]
|
|
586
|
+
try:
|
|
587
|
+
return compatible_type(sql_member_types)
|
|
588
|
+
except CompatibilityError:
|
|
589
|
+
raise TypeError(f"inconsistent union data types: {member_types}") from None
|
|
590
|
+
if isinstance(typ, type):
|
|
591
|
+
return SqlUserDefinedType(self.create_qualified_id(typ.__module__, typ.__name__))
|
|
592
|
+
|
|
593
|
+
raise TypeError(f"unsupported data type: {typ}")
|
|
594
|
+
|
|
595
|
+
def member_to_column(self, field: DataclassField, cls: type[DataclassInstance], doc: Docstring) -> Column:
|
|
596
|
+
"Converts a data-class field into a SQL table column."
|
|
597
|
+
|
|
598
|
+
props = get_field_properties(field.type)
|
|
599
|
+
data_type = self.member_to_sql_data_type(props.field_type, cls)
|
|
600
|
+
|
|
601
|
+
if field.default is not dataclasses.MISSING and field.default is not None:
|
|
602
|
+
default_value = data_type.value_to_sql_literal(field.default)
|
|
603
|
+
elif self.options.auto_default and not props.nullable and not props.is_primary and not props.is_identity:
|
|
604
|
+
if is_reference_type(props.plain_type, cls):
|
|
605
|
+
plain_type = reference_to_key(props.plain_type, cls)
|
|
606
|
+
elif is_type_union(props.plain_type):
|
|
607
|
+
member_types = [evaluate_member_type(t, cls) for t in unwrap_union_types(props.plain_type)]
|
|
608
|
+
for member_type in member_types:
|
|
609
|
+
if is_simple_type(member_type):
|
|
610
|
+
plain_type = member_type
|
|
611
|
+
break
|
|
612
|
+
else:
|
|
613
|
+
plain_type = None
|
|
614
|
+
elif (
|
|
615
|
+
not is_struct_type(props.plain_type)
|
|
616
|
+
and not is_generic_list(props.plain_type)
|
|
617
|
+
and not is_generic_set(props.plain_type)
|
|
618
|
+
and not is_type_union(props.plain_type)
|
|
619
|
+
):
|
|
620
|
+
plain_type = props.plain_type
|
|
621
|
+
else:
|
|
622
|
+
# unable to infer default value for type
|
|
623
|
+
plain_type = None
|
|
624
|
+
|
|
625
|
+
if plain_type is not None:
|
|
626
|
+
default_expr: Any
|
|
627
|
+
if plain_type is datetime.datetime:
|
|
628
|
+
default_expr = datetime.datetime.min
|
|
629
|
+
elif plain_type is datetime.date:
|
|
630
|
+
default_expr = datetime.date.min
|
|
631
|
+
elif plain_type is datetime.time:
|
|
632
|
+
default_expr = datetime.time.min
|
|
633
|
+
elif is_type_enum(plain_type):
|
|
634
|
+
default_expr = next(member.value for member in plain_type)
|
|
635
|
+
else:
|
|
636
|
+
default_expr = plain_type() # type: ignore
|
|
637
|
+
default_value = constant(default_expr)
|
|
638
|
+
else:
|
|
639
|
+
default_value = None
|
|
640
|
+
else:
|
|
641
|
+
default_value = None
|
|
642
|
+
|
|
643
|
+
description = doc.params[field.name].description if field.name in doc.params else None
|
|
644
|
+
|
|
645
|
+
return self.options.factory.column_class(
|
|
646
|
+
name=LocalId(field.name),
|
|
647
|
+
data_type=data_type,
|
|
648
|
+
nullable=props.nullable,
|
|
649
|
+
default=default_value,
|
|
650
|
+
identity=props.is_identity,
|
|
651
|
+
description=description,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def dataclass_to_table(self, cls: type[DataclassInstance]) -> Table:
|
|
655
|
+
"Converts a data-class with a primary key into a SQL table type."
|
|
656
|
+
|
|
657
|
+
if not is_dataclass_type(cls):
|
|
658
|
+
raise TypeError(f"expected: dataclass type; got: {cls}")
|
|
659
|
+
|
|
660
|
+
doc = parse_type(cls)
|
|
661
|
+
|
|
662
|
+
try:
|
|
663
|
+
columns = [
|
|
664
|
+
self.member_to_column(field, cls, doc) for field in dataclass_fields(cls) if self._get_relationship(field.type) is None
|
|
665
|
+
]
|
|
666
|
+
except TypeError as e:
|
|
667
|
+
raise TypeError(f"error processing data-class: {cls}") from e
|
|
668
|
+
|
|
669
|
+
# foreign/primary key constraints
|
|
670
|
+
constraints: list[Constraint] = []
|
|
671
|
+
if self.options.foreign_constraints:
|
|
672
|
+
constraints.extend(self.dataclass_to_constraints(cls))
|
|
673
|
+
|
|
674
|
+
# relationships for extensible enumeration types ignore foreign constraints option and always create a foreign key
|
|
675
|
+
for enum_field in dataclass_fields_as_required(cls, lambda t: is_extensible_enum_type(t, cls)):
|
|
676
|
+
enum_type = unwrap_extensible_enum_type(enum_field.type, cls)
|
|
677
|
+
constraints.append(
|
|
678
|
+
ForeignConstraint(
|
|
679
|
+
LocalId(self.create_foreign_key_name(cls, enum_field.name)),
|
|
680
|
+
(LocalId(enum_field.name),),
|
|
681
|
+
ConstraintReference(
|
|
682
|
+
self.create_qualified_id(enum_type.__module__, enum_type.__name__),
|
|
683
|
+
(LocalId("id"),),
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# relationships for enumeration types ignore foreign constraints option and always create a foreign key
|
|
689
|
+
if self.options.enum_mode is EnumMode.RELATION:
|
|
690
|
+
for enum_field in dataclass_fields_as_required(cls, is_type_enum):
|
|
691
|
+
constraints.append(
|
|
692
|
+
ForeignConstraint(
|
|
693
|
+
LocalId(self.create_foreign_key_name(cls, enum_field.name)),
|
|
694
|
+
(LocalId(enum_field.name),),
|
|
695
|
+
ConstraintReference(
|
|
696
|
+
self.create_qualified_id(enum_field.type.__module__, enum_field.type.__name__),
|
|
697
|
+
(LocalId("id"),),
|
|
698
|
+
),
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if self.options.check_constraints and self.options.enum_mode is EnumMode.CHECK:
|
|
703
|
+
for enum_field in dataclass_fields_as_required(cls, is_type_enum):
|
|
704
|
+
enum_values = ", ".join(constant(e.value) for e in enum_field.type)
|
|
705
|
+
constraints.append(
|
|
706
|
+
CheckConstraint(
|
|
707
|
+
LocalId(self.create_check_name(cls, enum_field.name)),
|
|
708
|
+
f"{LocalId(enum_field.name)} IN ({enum_values})",
|
|
709
|
+
),
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
for column in columns:
|
|
713
|
+
check_constraint = self.create_check_constraint(column)
|
|
714
|
+
if check_constraint:
|
|
715
|
+
constraints.append(
|
|
716
|
+
CheckConstraint(
|
|
717
|
+
LocalId(self.create_check_name(cls, column.name.id)),
|
|
718
|
+
check_constraint,
|
|
719
|
+
),
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
return self.options.factory.table_class(
|
|
723
|
+
name=self.create_qualified_id(cls.__module__, cls.__name__),
|
|
724
|
+
columns=columns,
|
|
725
|
+
primary_key=(LocalId(dataclass_primary_key_name(cls)),),
|
|
726
|
+
constraints=constraints or None,
|
|
727
|
+
description=doc.full_description,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
def create_check_constraint(self, column: Column) -> Optional[str]:
|
|
731
|
+
conditions: list[str] = []
|
|
732
|
+
data_type = column.data_type
|
|
733
|
+
if isinstance(data_type, SqlIntegerType):
|
|
734
|
+
if data_type.minimum is not None:
|
|
735
|
+
conditions.append(f"{column.name} >= {data_type.minimum}")
|
|
736
|
+
if data_type.maximum is not None:
|
|
737
|
+
conditions.append(f"{column.name} <= {data_type.maximum}")
|
|
738
|
+
if conditions:
|
|
739
|
+
return " AND ".join(f"({c})" for c in conditions)
|
|
740
|
+
else:
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
def dataclass_to_constraints(self, cls: type[DataclassInstance]) -> list[Constraint]:
|
|
744
|
+
"Extracts the foreign/primary key relationships from a data-class."
|
|
745
|
+
|
|
746
|
+
constraints: list[Constraint] = []
|
|
747
|
+
|
|
748
|
+
for field in dataclass_fields(cls):
|
|
749
|
+
props = get_field_properties(field.type)
|
|
750
|
+
if props.is_unique:
|
|
751
|
+
constraints.append(
|
|
752
|
+
UniqueConstraint(
|
|
753
|
+
LocalId(self.create_unique_key_name(cls, field.name)),
|
|
754
|
+
(LocalId(field.name),),
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
if self.options.foreign_constraints:
|
|
759
|
+
for field in dataclass_fields_as_required(cls):
|
|
760
|
+
if is_entity_type(field.type):
|
|
761
|
+
# foreign keys
|
|
762
|
+
constraints.append(
|
|
763
|
+
ForeignConstraint(
|
|
764
|
+
LocalId(self.create_foreign_key_name(cls, field.name)),
|
|
765
|
+
(LocalId(field.name),),
|
|
766
|
+
ConstraintReference(
|
|
767
|
+
self.create_qualified_id(field.type.__module__, field.type.__name__),
|
|
768
|
+
(LocalId(dataclass_primary_key_name(field.type)),),
|
|
769
|
+
),
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
if is_type_union(field.type):
|
|
774
|
+
member_types = [evaluate_member_type(t, cls) for t in unwrap_union_types(unwrap_annotated_type(field.type))]
|
|
775
|
+
if all(is_entity_type(t) for t in member_types):
|
|
776
|
+
# discriminated keys
|
|
777
|
+
constraints.append(
|
|
778
|
+
DiscriminatedConstraint(
|
|
779
|
+
LocalId(self.create_discriminated_key_name(cls, field.name)),
|
|
780
|
+
(LocalId(field.name),),
|
|
781
|
+
[
|
|
782
|
+
ConstraintReference(
|
|
783
|
+
self.create_qualified_id(t.__module__, t.__name__),
|
|
784
|
+
(LocalId(dataclass_primary_key_name(t)),),
|
|
785
|
+
)
|
|
786
|
+
for t in member_types
|
|
787
|
+
],
|
|
788
|
+
)
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
return constraints
|
|
792
|
+
|
|
793
|
+
def member_to_field(self, field: DataclassField, cls: type[DataclassInstance], doc: Docstring) -> StructMember:
|
|
794
|
+
"Converts a data-class field into a SQL struct (composite type) field."
|
|
795
|
+
|
|
796
|
+
props = get_field_properties(field.type)
|
|
797
|
+
description = doc.params[field.name].description if field.name in doc.params else None
|
|
798
|
+
|
|
799
|
+
return StructMember(
|
|
800
|
+
name=LocalId(field.name),
|
|
801
|
+
data_type=self.member_to_sql_data_type(props.field_type, cls),
|
|
802
|
+
description=description,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
def dataclass_to_struct(self, cls: type[DataclassInstance]) -> StructType:
|
|
806
|
+
"Converts a data-class without a primary key into a SQL struct type."
|
|
807
|
+
|
|
808
|
+
if not is_dataclass_type(cls):
|
|
809
|
+
raise TypeError(f"expected: dataclass type; got: {cls}")
|
|
810
|
+
|
|
811
|
+
doc = parse_type(cls)
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
members = [self.member_to_field(field, cls, doc) for field in dataclass_fields(cls)]
|
|
815
|
+
except TypeError as e:
|
|
816
|
+
raise TypeError(f"error processing data-class: {cls}") from e
|
|
817
|
+
|
|
818
|
+
return self.options.factory.struct_class(
|
|
819
|
+
name=self.create_qualified_id(cls.__module__, cls.__name__),
|
|
820
|
+
members=members,
|
|
821
|
+
description=doc.full_description,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
def _enum_table(self, enum_type: type[enum.Enum]) -> Table:
|
|
825
|
+
enum_table_name = enum_type.__name__
|
|
826
|
+
id = self.create_qualified_id(enum_type.__module__, enum_table_name)
|
|
827
|
+
columns = [
|
|
828
|
+
self.options.factory.column_class.create(
|
|
829
|
+
LocalId("id"),
|
|
830
|
+
self._enumeration_key_type(),
|
|
831
|
+
nullable=False,
|
|
832
|
+
identity=True,
|
|
833
|
+
)
|
|
834
|
+
]
|
|
835
|
+
primary_key = (LocalId("id"),)
|
|
836
|
+
constraints: list[Constraint] = []
|
|
837
|
+
|
|
838
|
+
enum_type = unwrap_annotated_type(enum_type)
|
|
839
|
+
enum_member_types: set[type] = set(type(e.value) for e in enum_type)
|
|
840
|
+
if len(enum_member_types) > 1:
|
|
841
|
+
raise TypeError(
|
|
842
|
+
f"inconsistent member types in enumeration `{enum_type.__name__}`: {sorted(list(e.__name__ for e in enum_member_types))}"
|
|
843
|
+
)
|
|
844
|
+
enum_member_type = enum_member_types.pop()
|
|
845
|
+
unadorned_member_type = unwrap_annotated_type(enum_member_type)
|
|
846
|
+
|
|
847
|
+
if unadorned_member_type is int or unadorned_member_type is str:
|
|
848
|
+
columns.append(
|
|
849
|
+
self.options.factory.column_class.create(
|
|
850
|
+
LocalId("value"),
|
|
851
|
+
self.member_to_sql_data_type(ENUM_LABEL_TYPE, type(None)),
|
|
852
|
+
nullable=False,
|
|
853
|
+
)
|
|
854
|
+
)
|
|
855
|
+
constraints.append(
|
|
856
|
+
UniqueConstraint(
|
|
857
|
+
LocalId(f"uq_{enum_table_name}"),
|
|
858
|
+
(LocalId("value"),),
|
|
859
|
+
)
|
|
860
|
+
)
|
|
861
|
+
elif is_dataclass_type(unadorned_member_type):
|
|
862
|
+
columns.extend(
|
|
863
|
+
self.member_to_column(field, enum_member_type, parse_type(unadorned_member_type))
|
|
864
|
+
for field in dataclass_fields(unadorned_member_type)
|
|
865
|
+
)
|
|
866
|
+
else:
|
|
867
|
+
raise TypeError(f"unsupported member type in enumeration `{enum_type.__name__}`: {enum_member_type}")
|
|
868
|
+
|
|
869
|
+
if self.options.initialize_tables:
|
|
870
|
+
return self.options.factory.enum_table_class(
|
|
871
|
+
id,
|
|
872
|
+
columns,
|
|
873
|
+
values=[e.value for e in enum_type],
|
|
874
|
+
primary_key=primary_key,
|
|
875
|
+
constraints=constraints,
|
|
876
|
+
)
|
|
877
|
+
else:
|
|
878
|
+
return self.options.factory.table_class(
|
|
879
|
+
id,
|
|
880
|
+
columns,
|
|
881
|
+
primary_key=primary_key,
|
|
882
|
+
constraints=constraints,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def dataclasses_to_catalog(self, entity_types: list[type[DataclassInstance]]) -> Catalog:
|
|
886
|
+
"Converts a list of Python data-class types into a database object catalog."
|
|
887
|
+
|
|
888
|
+
# omit abstract base classes
|
|
889
|
+
entity_types = [t for t in entity_types if not inspect.isabstract(t)]
|
|
890
|
+
|
|
891
|
+
# collect all dependent types
|
|
892
|
+
referenced_types: set[type] = set(entity_types)
|
|
893
|
+
for entity_type in entity_types:
|
|
894
|
+
for ref_type in get_referenced_types(entity_type):
|
|
895
|
+
referenced_types.add(ref_type)
|
|
896
|
+
|
|
897
|
+
# collect all enumeration types in alphabetical order
|
|
898
|
+
enums: dict[str, list[EnumType]] = {}
|
|
899
|
+
if self.options.enum_mode is EnumMode.TYPE:
|
|
900
|
+
enum_types = [obj for obj in referenced_types if is_type_enum(obj)]
|
|
901
|
+
enum_types.sort(key=lambda e: e.__name__)
|
|
902
|
+
for enum_type in enum_types:
|
|
903
|
+
enum_values = [str(e.value) for e in enum_type]
|
|
904
|
+
if len(enum_values) < 2:
|
|
905
|
+
# enumerations with too few members expand into extensible enumeration tables
|
|
906
|
+
continue
|
|
907
|
+
|
|
908
|
+
enum_defs = enums.setdefault(enum_type.__module__, [])
|
|
909
|
+
enum_defs.append(
|
|
910
|
+
EnumType(
|
|
911
|
+
QualifiedId(
|
|
912
|
+
self.options.namespaces.get(enum_type.__module__),
|
|
913
|
+
enum_type.__name__,
|
|
914
|
+
),
|
|
915
|
+
enum_values,
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
# collect all struct types in alphabetical order
|
|
920
|
+
structs: dict[str, list[StructType]] = {}
|
|
921
|
+
if self.options.struct_mode is StructMode.TYPE:
|
|
922
|
+
struct_types = [obj for obj in referenced_types if is_struct_type(obj)]
|
|
923
|
+
struct_types.sort(key=lambda s: s.__name__)
|
|
924
|
+
struct_types = _topological_sort(struct_types)
|
|
925
|
+
for struct_type in struct_types:
|
|
926
|
+
struct_defs = structs.setdefault(struct_type.__module__, [])
|
|
927
|
+
struct_defs.append(self.dataclass_to_struct(struct_type))
|
|
928
|
+
|
|
929
|
+
# create tables
|
|
930
|
+
tables: dict[str, list[Table]] = {}
|
|
931
|
+
table_types = [obj for obj in referenced_types if is_entity_type(obj)]
|
|
932
|
+
table_types.sort(key=lambda t: t.__name__)
|
|
933
|
+
for table_type in table_types:
|
|
934
|
+
table_defs = tables.setdefault(table_type.__module__, [])
|
|
935
|
+
table_defs.append(self.dataclass_to_table(table_type))
|
|
936
|
+
|
|
937
|
+
# discover regular enumerations
|
|
938
|
+
regular_enum_types: set[type[enum.Enum]] = set()
|
|
939
|
+
if self.options.enum_mode is EnumMode.RELATION:
|
|
940
|
+
for entity in table_types:
|
|
941
|
+
# field of type E
|
|
942
|
+
for enum_field in dataclass_fields_as_required(entity, is_type_enum):
|
|
943
|
+
regular_enum_types.add(enum_field.type)
|
|
944
|
+
|
|
945
|
+
# field of type list[E]
|
|
946
|
+
for enum_field in dataclass_fields_as_required(entity, is_type_enum_list):
|
|
947
|
+
regular_enum_types.add(unwrap_generic_list(enum_field.type))
|
|
948
|
+
|
|
949
|
+
# field of type set[E]
|
|
950
|
+
for enum_field in dataclass_fields_as_required(entity, is_type_enum_set):
|
|
951
|
+
regular_enum_types.add(unwrap_generic_set(enum_field.type))
|
|
952
|
+
|
|
953
|
+
for enum_type in sorted(list(regular_enum_types), key=lambda e: e.__name__):
|
|
954
|
+
table_defs = tables.setdefault(enum_type.__module__, [])
|
|
955
|
+
table_defs.append(self._enum_table(enum_type))
|
|
956
|
+
|
|
957
|
+
# discover extensible enumerations
|
|
958
|
+
extensible_enum_types: set[type[enum.Enum]] = set()
|
|
959
|
+
for entity in table_types:
|
|
960
|
+
for enum_field in dataclass_fields_as_required(
|
|
961
|
+
entity,
|
|
962
|
+
lambda t: is_extensible_enum_type(t, entity), # noqa: B023
|
|
963
|
+
):
|
|
964
|
+
enum_type = unwrap_extensible_enum_type(enum_field.type, entity)
|
|
965
|
+
extensible_enum_types.add(enum_type)
|
|
966
|
+
for enum_type in sorted(list(extensible_enum_types), key=lambda e: e.__name__):
|
|
967
|
+
table_defs = tables.setdefault(enum_type.__module__, [])
|
|
968
|
+
table_defs.append(self._enum_table(enum_type))
|
|
969
|
+
|
|
970
|
+
# create join tables for many-to-many relationships
|
|
971
|
+
for entity in table_types:
|
|
972
|
+
for field in dataclass_fields(entity):
|
|
973
|
+
relationship = self._get_relationship(field.type)
|
|
974
|
+
if relationship is None:
|
|
975
|
+
continue
|
|
976
|
+
|
|
977
|
+
relationship_type, item_type = relationship
|
|
978
|
+
|
|
979
|
+
# "primary" refers to the primary key name/type of the tables participating in the many-to-many relationship
|
|
980
|
+
# "join" refers to the join table column names
|
|
981
|
+
primary_left_name = LocalId(dataclass_primary_key_name(entity))
|
|
982
|
+
if is_entity_type(item_type):
|
|
983
|
+
primary_right_name = LocalId(dataclass_primary_key_name(item_type))
|
|
984
|
+
primary_right_type = self.member_to_sql_data_type(dataclass_primary_key_type(item_type), item_type)
|
|
985
|
+
elif self.options.enum_mode is EnumMode.RELATION and is_type_enum(item_type):
|
|
986
|
+
# use the same name and type as used when generating the enumeration table
|
|
987
|
+
primary_right_name = LocalId("id")
|
|
988
|
+
primary_right_type = self._enumeration_key_type()
|
|
989
|
+
else:
|
|
990
|
+
# related type must be an entity, or (depending on options) an enumeration type
|
|
991
|
+
raise TypeError(f"unrecognized join relation type: {item_type}")
|
|
992
|
+
|
|
993
|
+
column_left_name = f"{entity.__name__}_{field.name}"
|
|
994
|
+
column_right_name = f"{item_type.__name__}_{primary_right_name.id}"
|
|
995
|
+
table_name = f"{column_left_name}_{item_type.__name__}"
|
|
996
|
+
join_left_name = f"{self.create_qualified_prefix(entity)}_{field.name}"
|
|
997
|
+
join_right_name = f"{join_left_name}_{item_type.__name__}"
|
|
998
|
+
|
|
999
|
+
constraints: list[Constraint] = [
|
|
1000
|
+
ForeignConstraint(
|
|
1001
|
+
LocalId(f"jk_{join_left_name}"),
|
|
1002
|
+
(LocalId(column_left_name),),
|
|
1003
|
+
ConstraintReference(
|
|
1004
|
+
self.create_qualified_id(
|
|
1005
|
+
entity.__module__,
|
|
1006
|
+
entity.__name__,
|
|
1007
|
+
),
|
|
1008
|
+
(primary_left_name,),
|
|
1009
|
+
),
|
|
1010
|
+
),
|
|
1011
|
+
ForeignConstraint(
|
|
1012
|
+
LocalId(f"jk_{join_right_name}"),
|
|
1013
|
+
(LocalId(column_right_name),),
|
|
1014
|
+
ConstraintReference(
|
|
1015
|
+
self.create_qualified_id(
|
|
1016
|
+
item_type.__module__,
|
|
1017
|
+
item_type.__name__,
|
|
1018
|
+
),
|
|
1019
|
+
(primary_right_name,),
|
|
1020
|
+
),
|
|
1021
|
+
),
|
|
1022
|
+
]
|
|
1023
|
+
if relationship_type is set:
|
|
1024
|
+
constraints.append(
|
|
1025
|
+
UniqueConstraint(
|
|
1026
|
+
LocalId(f"uq_{join_right_name}"),
|
|
1027
|
+
(LocalId(column_right_name),),
|
|
1028
|
+
)
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
table_defs = tables.setdefault(entity.__module__, [])
|
|
1032
|
+
table_defs.append(
|
|
1033
|
+
self.options.factory.table_class(
|
|
1034
|
+
self.create_qualified_id(
|
|
1035
|
+
entity.__module__,
|
|
1036
|
+
table_name,
|
|
1037
|
+
),
|
|
1038
|
+
[
|
|
1039
|
+
self.options.factory.column_class.create(
|
|
1040
|
+
LocalId("uuid"),
|
|
1041
|
+
self.member_to_sql_data_type(uuid.UUID, entity),
|
|
1042
|
+
nullable=False,
|
|
1043
|
+
),
|
|
1044
|
+
self.options.factory.column_class.create(
|
|
1045
|
+
LocalId(column_left_name),
|
|
1046
|
+
self.member_to_sql_data_type(dataclass_primary_key_type(entity), entity),
|
|
1047
|
+
nullable=False,
|
|
1048
|
+
),
|
|
1049
|
+
self.options.factory.column_class.create(
|
|
1050
|
+
LocalId(column_right_name),
|
|
1051
|
+
primary_right_type,
|
|
1052
|
+
nullable=False,
|
|
1053
|
+
),
|
|
1054
|
+
],
|
|
1055
|
+
primary_key=(LocalId("uuid"),),
|
|
1056
|
+
constraints=constraints,
|
|
1057
|
+
)
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
if self.options.qualified_names:
|
|
1061
|
+
namespaces = [
|
|
1062
|
+
self.options.factory.namespace_class(
|
|
1063
|
+
name=LocalId(self.options.namespaces.get(module_name) or ""),
|
|
1064
|
+
enums=enums.get(module_name, []),
|
|
1065
|
+
structs=structs.get(module_name, []),
|
|
1066
|
+
tables=table_defs,
|
|
1067
|
+
)
|
|
1068
|
+
for module_name, table_defs in tables.items()
|
|
1069
|
+
]
|
|
1070
|
+
else:
|
|
1071
|
+
namespaces = [
|
|
1072
|
+
self.options.factory.namespace_class(
|
|
1073
|
+
name=LocalId(""),
|
|
1074
|
+
enums=[item for enum_list in enums.values() for item in enum_list],
|
|
1075
|
+
structs=[item for struct_list in structs.values() for item in struct_list],
|
|
1076
|
+
tables=[item for table_list in tables.values() for item in table_list],
|
|
1077
|
+
)
|
|
1078
|
+
]
|
|
1079
|
+
return Catalog(namespaces=namespaces)
|
|
1080
|
+
|
|
1081
|
+
def modules_to_catalog(self, modules: list[types.ModuleType]) -> Catalog:
|
|
1082
|
+
"Converts a list of Python modules into a database object catalog."
|
|
1083
|
+
|
|
1084
|
+
return self.dataclasses_to_catalog(get_entity_types(modules))
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def dataclass_to_table(
|
|
1088
|
+
cls: type[DataclassInstance],
|
|
1089
|
+
*,
|
|
1090
|
+
options: Optional[DataclassConverterOptions] = None,
|
|
1091
|
+
) -> Table:
|
|
1092
|
+
"Converts a data-class with a primary key into a SQL table type."
|
|
1093
|
+
|
|
1094
|
+
if options is None:
|
|
1095
|
+
options = DataclassConverterOptions()
|
|
1096
|
+
|
|
1097
|
+
converter = DataclassConverter(options=options)
|
|
1098
|
+
return converter.dataclass_to_table(cls)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def dataclass_to_struct(
|
|
1102
|
+
cls: type[DataclassInstance],
|
|
1103
|
+
*,
|
|
1104
|
+
options: Optional[DataclassConverterOptions] = None,
|
|
1105
|
+
) -> StructType:
|
|
1106
|
+
"Converts a data-class without a primary key into a SQL struct type."
|
|
1107
|
+
|
|
1108
|
+
if options is None:
|
|
1109
|
+
options = DataclassConverterOptions()
|
|
1110
|
+
|
|
1111
|
+
converter = DataclassConverter(options=options)
|
|
1112
|
+
return converter.dataclass_to_struct(cls)
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def module_to_catalog(
|
|
1116
|
+
module: types.ModuleType,
|
|
1117
|
+
*,
|
|
1118
|
+
options: Optional[DataclassConverterOptions] = None,
|
|
1119
|
+
) -> Catalog:
|
|
1120
|
+
return modules_to_catalog([module], options=options)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def modules_to_catalog(
|
|
1124
|
+
modules: list[types.ModuleType],
|
|
1125
|
+
*,
|
|
1126
|
+
options: Optional[DataclassConverterOptions] = None,
|
|
1127
|
+
) -> Catalog:
|
|
1128
|
+
"Converts the entire contents of a Python module into a SQL namespace (schema)."
|
|
1129
|
+
|
|
1130
|
+
if options is None:
|
|
1131
|
+
options = DataclassConverterOptions()
|
|
1132
|
+
|
|
1133
|
+
converter = DataclassConverter(options=options)
|
|
1134
|
+
return converter.modules_to_catalog(modules)
|