instructure-pysqlsync 0.8.5.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
  2. instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
  3. instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
  4. instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
  5. instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
  6. instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
  7. pysqlsync/__init__.py +15 -0
  8. pysqlsync/base.py +1386 -0
  9. pysqlsync/connection.py +101 -0
  10. pysqlsync/data/__init__.py +0 -0
  11. pysqlsync/data/exchange.py +184 -0
  12. pysqlsync/data/generator.py +305 -0
  13. pysqlsync/dialect/README.md +35 -0
  14. pysqlsync/dialect/__init__.py +0 -0
  15. pysqlsync/dialect/delta/__init__.py +0 -0
  16. pysqlsync/dialect/delta/data_types.py +51 -0
  17. pysqlsync/dialect/delta/dependency.py +7 -0
  18. pysqlsync/dialect/delta/engine.py +18 -0
  19. pysqlsync/dialect/delta/generator.py +74 -0
  20. pysqlsync/dialect/delta/object_types.py +87 -0
  21. pysqlsync/dialect/mssql/README.md +23 -0
  22. pysqlsync/dialect/mssql/__init__.py +0 -0
  23. pysqlsync/dialect/mssql/connection.py +146 -0
  24. pysqlsync/dialect/mssql/data_types.py +113 -0
  25. pysqlsync/dialect/mssql/dependency.py +9 -0
  26. pysqlsync/dialect/mssql/discovery.py +99 -0
  27. pysqlsync/dialect/mssql/engine.py +20 -0
  28. pysqlsync/dialect/mssql/generator.py +88 -0
  29. pysqlsync/dialect/mssql/mutation.py +46 -0
  30. pysqlsync/dialect/mssql/object_types.py +135 -0
  31. pysqlsync/dialect/mysql/__init__.py +0 -0
  32. pysqlsync/dialect/mysql/connection.py +126 -0
  33. pysqlsync/dialect/mysql/data_types.py +40 -0
  34. pysqlsync/dialect/mysql/dependency.py +10 -0
  35. pysqlsync/dialect/mysql/discovery.py +145 -0
  36. pysqlsync/dialect/mysql/engine.py +20 -0
  37. pysqlsync/dialect/mysql/generator.py +140 -0
  38. pysqlsync/dialect/mysql/mutation.py +65 -0
  39. pysqlsync/dialect/mysql/object_types.py +75 -0
  40. pysqlsync/dialect/oracle/README.md +22 -0
  41. pysqlsync/dialect/oracle/__init__.py +0 -0
  42. pysqlsync/dialect/oracle/connection.py +141 -0
  43. pysqlsync/dialect/oracle/data_types.py +104 -0
  44. pysqlsync/dialect/oracle/dependency.py +9 -0
  45. pysqlsync/dialect/oracle/discovery.py +231 -0
  46. pysqlsync/dialect/oracle/engine.py +20 -0
  47. pysqlsync/dialect/oracle/generator.py +93 -0
  48. pysqlsync/dialect/oracle/mutation.py +37 -0
  49. pysqlsync/dialect/oracle/object_types.py +59 -0
  50. pysqlsync/dialect/postgresql/__init__.py +0 -0
  51. pysqlsync/dialect/postgresql/connection.py +133 -0
  52. pysqlsync/dialect/postgresql/data_types.py +6 -0
  53. pysqlsync/dialect/postgresql/dependency.py +9 -0
  54. pysqlsync/dialect/postgresql/discovery.py +392 -0
  55. pysqlsync/dialect/postgresql/engine.py +20 -0
  56. pysqlsync/dialect/postgresql/generator.py +99 -0
  57. pysqlsync/dialect/postgresql/mutation.py +82 -0
  58. pysqlsync/dialect/postgresql/object_types.py +99 -0
  59. pysqlsync/dialect/redshift/__init__.py +0 -0
  60. pysqlsync/dialect/redshift/connection.py +156 -0
  61. pysqlsync/dialect/redshift/data_types.py +10 -0
  62. pysqlsync/dialect/redshift/dependency.py +9 -0
  63. pysqlsync/dialect/redshift/engine.py +20 -0
  64. pysqlsync/dialect/redshift/generator.py +82 -0
  65. pysqlsync/dialect/snowflake/__init__.py +0 -0
  66. pysqlsync/dialect/snowflake/data_types.py +18 -0
  67. pysqlsync/dialect/snowflake/dependency.py +9 -0
  68. pysqlsync/dialect/snowflake/engine.py +18 -0
  69. pysqlsync/dialect/snowflake/generator.py +64 -0
  70. pysqlsync/dialect/snowflake/object_types.py +84 -0
  71. pysqlsync/dialect/test/__init__.py +0 -0
  72. pysqlsync/dialect/test/dependency.py +10 -0
  73. pysqlsync/dialect/trino/__init__.py +0 -0
  74. pysqlsync/dialect/trino/connection.py +86 -0
  75. pysqlsync/dialect/trino/dependency.py +9 -0
  76. pysqlsync/dialect/trino/discovery.py +21 -0
  77. pysqlsync/dialect/trino/engine.py +20 -0
  78. pysqlsync/dialect/trino/generator.py +31 -0
  79. pysqlsync/factory.py +169 -0
  80. pysqlsync/formation/__init__.py +0 -0
  81. pysqlsync/formation/constraints.py +78 -0
  82. pysqlsync/formation/data_types.py +192 -0
  83. pysqlsync/formation/discovery.py +390 -0
  84. pysqlsync/formation/inspection.py +179 -0
  85. pysqlsync/formation/mutation.py +320 -0
  86. pysqlsync/formation/object_dict.py +79 -0
  87. pysqlsync/formation/object_types.py +872 -0
  88. pysqlsync/formation/py_to_sql.py +1134 -0
  89. pysqlsync/formation/sql_to_py.py +194 -0
  90. pysqlsync/model/__init__.py +0 -0
  91. pysqlsync/model/data_types.py +397 -0
  92. pysqlsync/model/entity_types.py +68 -0
  93. pysqlsync/model/id_types.py +193 -0
  94. pysqlsync/model/key_types.py +38 -0
  95. pysqlsync/model/properties.py +234 -0
  96. pysqlsync/py.typed +0 -0
  97. pysqlsync/python_types.py +192 -0
  98. pysqlsync/resultset.py +106 -0
  99. pysqlsync/util/__init__.py +0 -0
  100. pysqlsync/util/dataclasses.py +80 -0
  101. pysqlsync/util/dispatch.py +28 -0
  102. pysqlsync/util/typing.py +13 -0
@@ -0,0 +1,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)