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,390 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Sequence
4
+
5
+ from ..base import BaseContext, DiscoveryError, Explorer
6
+ from ..model.data_types import escape_like, quote
7
+ from ..model.id_types import LocalId, QualifiedId, SupportsQualifiedId
8
+ from .constraints import ForeignFactory, UniqueFactory
9
+ from .data_types import SqlDiscovery
10
+ from .object_types import Column, Constraint, ForeignConstraint, Namespace, ObjectFactory, Table, UniqueConstraint
11
+
12
+ LOGGER = logging.getLogger("pysqlsync")
13
+
14
+
15
+ @dataclass
16
+ class AnsiColumnMeta:
17
+ column_name: str
18
+ data_type: str
19
+ nullable: bool
20
+ column_default: str
21
+ character_maximum_length: Optional[int]
22
+ numeric_precision: Optional[int]
23
+ numeric_scale: Optional[int]
24
+ datetime_precision: Optional[int]
25
+
26
+
27
+ @dataclass
28
+ class AnsiUniqueMeta:
29
+ constraint_name: str
30
+ table_schema: str
31
+ table_name: str
32
+ column_name: str
33
+
34
+
35
+ @dataclass
36
+ class AnsiConstraintMeta:
37
+ fk_constraint_name: str
38
+ fk_table_schema: str
39
+ fk_table_name: str
40
+ fk_column_name: str
41
+ fk_ordinal_position: int
42
+ uq_constraint_name: str
43
+ uq_table_schema: str
44
+ uq_table_name: str
45
+ uq_column_name: str
46
+ uq_ordinal_position: int
47
+
48
+
49
+ class AnsiExplorer(Explorer):
50
+ discovery: SqlDiscovery
51
+ factory: ObjectFactory
52
+
53
+ _has_constraints: Optional[bool] = None
54
+ _has_column_extended_info: Optional[bool] = None
55
+
56
+ def __init__(self, conn: BaseContext, discovery: SqlDiscovery, factory: ObjectFactory) -> None:
57
+ super().__init__(conn)
58
+ self.discovery = discovery
59
+ self.factory = factory
60
+
61
+ async def has_constraints(self) -> bool:
62
+ "True if `information_schema` has tables to query for referential constraints and key/column usage."
63
+
64
+ if self._has_constraints is None:
65
+ try:
66
+ await self.conn.query_one(
67
+ int,
68
+ "SELECT COUNT(*) FROM information_schema.table_constraints",
69
+ )
70
+ await self.conn.query_one(
71
+ int,
72
+ "SELECT COUNT(*) FROM information_schema.referential_constraints",
73
+ )
74
+ await self.conn.query_one(
75
+ int,
76
+ "SELECT COUNT(*) FROM information_schema.key_column_usage",
77
+ )
78
+ LOGGER.info("explorer: PK/FK information available")
79
+ self._has_constraints = True
80
+ except Exception:
81
+ LOGGER.info("explorer: PK/FK information NOT available")
82
+ self._has_constraints = False
83
+
84
+ return self._has_constraints
85
+
86
+ async def has_column_extended_info(self) -> bool:
87
+ "True if extended information is available for columns in `information_schema`."
88
+
89
+ if self._has_column_extended_info is None:
90
+ try:
91
+ await self.conn.query_one(
92
+ tuple[int, int, int, int, int, int, int],
93
+ "SELECT\n"
94
+ " COUNT(column_name),\n"
95
+ " COUNT(data_type),\n"
96
+ " COUNT(is_nullable),\n"
97
+ " COUNT(character_maximum_length),\n"
98
+ " COUNT(numeric_precision),\n"
99
+ " COUNT(numeric_scale),\n"
100
+ " COUNT(datetime_precision)\n"
101
+ "FROM information_schema.columns",
102
+ )
103
+ LOGGER.info("explorer: extended column information available")
104
+ self._has_column_extended_info = True
105
+ except Exception:
106
+ LOGGER.info("explorer: extended column information NOT available")
107
+ self._has_column_extended_info = False
108
+
109
+ return self._has_column_extended_info
110
+
111
+ async def get_table_names(self) -> list[QualifiedId]:
112
+ records = await self.conn.query_all(
113
+ tuple[str, str],
114
+ "SELECT table_schema, table_name\n"
115
+ "FROM information_schema.tables\n"
116
+ "WHERE table_schema != 'information_schema' AND table_schema != 'pg_catalog'\n"
117
+ "ORDER BY table_name ASC",
118
+ )
119
+ return [QualifiedId(record[0], record[1]) for record in records]
120
+
121
+ def _where_table(self, table_id: SupportsQualifiedId, alias: str) -> str:
122
+ table_schema = (
123
+ quote(table_id.scope_id) if table_id.scope_id is not None else self.conn.connection.generator.get_current_schema_stmt()
124
+ )
125
+ table_name = quote(table_id.local_id)
126
+ conditions = [
127
+ f"{alias}.table_schema = {table_schema}",
128
+ f"{alias}.table_name = {table_name}",
129
+ ]
130
+ return " AND ".join(f"({c})" for c in conditions)
131
+
132
+ async def has_table(self, table_id: SupportsQualifiedId) -> bool:
133
+ "Checks if a table exists."
134
+
135
+ count = await self.conn.query_one(
136
+ int,
137
+ f"SELECT COUNT(*) FROM information_schema.tables AS tab WHERE {self._where_table(table_id, 'tab')}",
138
+ )
139
+ return count > 0
140
+
141
+ async def has_column(self, table_id: SupportsQualifiedId, column_id: LocalId) -> bool:
142
+ "Checks if a table has the specified column."
143
+
144
+ count = await self.conn.query_one(
145
+ int,
146
+ "SELECT COUNT(*)\n"
147
+ "FROM information_schema.columns AS col\n"
148
+ f"WHERE {self._where_table(table_id, 'col')} AND col.column_name = {quote(column_id.local_id)}",
149
+ )
150
+ return count > 0
151
+
152
+ async def get_columns(self, table_id: SupportsQualifiedId) -> Sequence[Column]:
153
+ if await self.has_column_extended_info():
154
+ return await self._get_columns_full(table_id)
155
+ else:
156
+ return await self._get_columns_limited(table_id)
157
+
158
+ async def _get_columns_limited(self, table_id: SupportsQualifiedId) -> Sequence[Column]:
159
+ column_meta = await self.conn.query_all(
160
+ tuple[str, str, bool],
161
+ "SELECT col.column_name, col.data_type, CASE WHEN col.is_nullable = 'YES' THEN 1 ELSE 0 END AS nullable\n"
162
+ "FROM information_schema.columns AS col\n"
163
+ f"WHERE {self._where_table(table_id, 'col')}\n"
164
+ "ORDER BY ordinal_position",
165
+ )
166
+
167
+ columns: list[Column] = []
168
+ for col in column_meta:
169
+ column_name, data_type, nullable = col
170
+ columns.append(
171
+ self.factory.column_class.create(
172
+ LocalId(column_name),
173
+ self.discovery.sql_data_type_from_spec(type_name=data_type),
174
+ nullable=bool(nullable),
175
+ )
176
+ )
177
+ return columns
178
+
179
+ async def _get_columns_full(self, table_id: SupportsQualifiedId) -> Sequence[Column]:
180
+ column_meta = await self.conn.query_all(
181
+ AnsiColumnMeta,
182
+ "SELECT\n"
183
+ " column_name AS column_name,\n"
184
+ " data_type AS data_type,\n"
185
+ " CASE WHEN is_nullable = 'YES' THEN 1 ELSE 0 END AS nullable,\n"
186
+ " column_default AS column_default,\n"
187
+ " character_maximum_length AS character_maximum_length,\n"
188
+ " numeric_precision AS numeric_precision,\n"
189
+ " numeric_scale AS numeric_scale,\n"
190
+ " datetime_precision AS datetime_precision\n"
191
+ "FROM information_schema.columns AS col\n"
192
+ f"WHERE {self._where_table(table_id, 'col')}\n"
193
+ "ORDER BY ordinal_position",
194
+ )
195
+
196
+ columns: list[Column] = []
197
+ for col in column_meta:
198
+ columns.append(
199
+ self.factory.column_class.create(
200
+ LocalId(col.column_name),
201
+ self.discovery.sql_data_type_from_spec(
202
+ type_name=col.data_type,
203
+ character_maximum_length=col.character_maximum_length,
204
+ numeric_precision=col.numeric_precision,
205
+ numeric_scale=col.numeric_scale,
206
+ datetime_precision=col.datetime_precision,
207
+ ),
208
+ nullable=bool(col.nullable),
209
+ default=col.column_default,
210
+ )
211
+ )
212
+ return columns
213
+
214
+ async def get_table_primary_key(self, table_id: SupportsQualifiedId) -> tuple[LocalId, ...]:
215
+ primary_meta = await self.conn.query_all(
216
+ str,
217
+ "SELECT\n"
218
+ " kcu.column_name\n"
219
+ "FROM information_schema.table_constraints AS tab\n"
220
+ " INNER JOIN information_schema.key_column_usage AS kcu\n"
221
+ " ON tab.constraint_catalog = kcu.constraint_catalog\n"
222
+ " AND tab.constraint_schema = kcu.constraint_schema\n"
223
+ " AND tab.constraint_name = kcu.constraint_name\n"
224
+ f"WHERE {self._where_table(table_id, 'tab')} AND {self._where_table(table_id, 'kcu')} AND\n"
225
+ " tab.constraint_type = 'PRIMARY KEY'\n"
226
+ "ORDER BY kcu.ordinal_position",
227
+ )
228
+
229
+ return tuple(LocalId(column) for column in primary_meta)
230
+
231
+ async def get_unique_constraints(self, table_id: SupportsQualifiedId) -> list[UniqueConstraint]:
232
+ constraint_meta = await self.conn.query_all(
233
+ AnsiUniqueMeta,
234
+ "SELECT\n"
235
+ " kcu.constraint_name AS constraint_name,\n"
236
+ " kcu.table_schema AS table_schema,\n"
237
+ " kcu.table_name AS table_name,\n"
238
+ " kcu.column_name AS column_name\n"
239
+ "FROM information_schema.table_constraints AS tab\n"
240
+ " INNER JOIN information_schema.key_column_usage AS kcu\n"
241
+ " ON tab.constraint_catalog = kcu.constraint_catalog\n"
242
+ " AND tab.constraint_schema = kcu.constraint_schema\n"
243
+ " AND tab.constraint_name = kcu.constraint_name\n"
244
+ f"WHERE {self._where_table(table_id, 'tab')} AND {self._where_table(table_id, 'kcu')} AND\n"
245
+ " tab.constraint_type = 'UNIQUE'\n"
246
+ "ORDER BY kcu.ordinal_position",
247
+ )
248
+
249
+ constraints = UniqueFactory()
250
+ for con in constraint_meta:
251
+ constraints.add(con.constraint_name, LocalId(con.column_name))
252
+ return constraints.fetch()
253
+
254
+ async def get_referential_constraints(self, table_id: SupportsQualifiedId) -> list[ForeignConstraint]:
255
+ constraint_meta = await self.conn.query_all(
256
+ AnsiConstraintMeta,
257
+ "SELECT\n"
258
+ " kcu1.constraint_name AS fk_constraint_name,\n"
259
+ " kcu1.table_schema AS fk_table_schema,\n"
260
+ " kcu1.table_name AS fk_table_name,\n"
261
+ " kcu1.column_name AS fk_column_name,\n"
262
+ " kcu1.ordinal_position AS fk_ordinal_position,\n"
263
+ " kcu2.constraint_name AS uq_constraint_name,\n"
264
+ " kcu2.table_schema AS uq_table_schema,\n"
265
+ " kcu2.table_name AS uq_table_name,\n"
266
+ " kcu2.column_name AS uq_column_name,\n"
267
+ " kcu2.ordinal_position AS uq_ordinal_position\n"
268
+ "FROM information_schema.referential_constraints AS ref\n"
269
+ " INNER JOIN information_schema.key_column_usage AS kcu1\n"
270
+ " ON kcu1.constraint_catalog = ref.constraint_catalog\n"
271
+ " AND kcu1.constraint_schema = ref.constraint_schema\n"
272
+ " AND kcu1.constraint_name = ref.constraint_name\n"
273
+ " INNER JOIN information_schema.key_column_usage AS kcu2\n"
274
+ " ON kcu2.constraint_catalog = ref.unique_constraint_catalog\n"
275
+ " AND kcu2.constraint_schema = ref.unique_constraint_schema\n"
276
+ " AND kcu2.constraint_name = ref.unique_constraint_name\n"
277
+ f"WHERE {self._where_table(table_id, 'kcu1')} AND\n"
278
+ " kcu1.ordinal_position = kcu2.ordinal_position\n"
279
+ "ORDER BY kcu1.ordinal_position",
280
+ )
281
+
282
+ constraints = ForeignFactory()
283
+ for con in constraint_meta:
284
+ constraints.add(
285
+ con.fk_constraint_name,
286
+ LocalId(con.fk_column_name),
287
+ QualifiedId(con.uq_table_schema, con.uq_table_name),
288
+ LocalId(con.uq_column_name),
289
+ )
290
+ return constraints.fetch()
291
+
292
+ async def get_table_description(self, table_id: SupportsQualifiedId) -> Optional[str]:
293
+ return None
294
+
295
+ async def get_table(self, table_id: SupportsQualifiedId) -> Table:
296
+ if not await self.has_table(table_id):
297
+ raise DiscoveryError(f"table not found: {table_id}")
298
+
299
+ columns = await self.get_columns(table_id)
300
+
301
+ if await self.has_constraints():
302
+ primary_key = await self.get_table_primary_key(table_id)
303
+ constraints: list[Constraint] = []
304
+ constraints.extend(await self.get_unique_constraints(table_id))
305
+ constraints.extend(await self.get_referential_constraints(table_id))
306
+ description = await self.get_table_description(table_id)
307
+ return self.factory.table_class(
308
+ name=table_id,
309
+ columns=columns,
310
+ primary_key=primary_key,
311
+ constraints=constraints or None,
312
+ description=description,
313
+ )
314
+ else:
315
+ # assume first column is the primary key
316
+ primary_key = (columns[0].name,)
317
+ return self.factory.table_class(name=table_id, columns=columns, primary_key=primary_key)
318
+
319
+ async def get_namespace_qualified(self, namespace_id: Optional[LocalId] = None) -> Namespace:
320
+ """
321
+ Constructs a database object model of a namespace (database schema).
322
+
323
+ To be invoked by database dialects with qualified name support.
324
+ """
325
+
326
+ tables: list[Table] = []
327
+
328
+ # create namespace using qualified IDs
329
+ if namespace_id is not None:
330
+ schema_expr = quote(namespace_id.local_id)
331
+ else:
332
+ schema_expr = self.conn.connection.generator.get_current_schema_stmt()
333
+ table_names = await self.conn.query_all(
334
+ str,
335
+ f"SELECT table_name\nFROM information_schema.tables\nWHERE table_schema = {schema_expr}\nORDER BY table_name ASC",
336
+ )
337
+ if table_names:
338
+ if namespace_id is not None:
339
+ scope_id = namespace_id.local_id
340
+ else:
341
+ scope_id = None
342
+
343
+ for table_name in table_names:
344
+ table = await self.get_table(self.get_qualified_id(scope_id, table_name))
345
+ tables.append(table)
346
+
347
+ return self.factory.namespace_class(name=LocalId(scope_id or ""), enums=[], structs=[], tables=tables)
348
+ else:
349
+ return self.factory.namespace_class()
350
+
351
+ async def get_namespace_flat(self, namespace_id: Optional[LocalId] = None) -> Namespace:
352
+ """
353
+ Constructs a database object model of a namespace (database schema).
354
+
355
+ To be invoked by database dialects without qualified name support.
356
+ """
357
+
358
+ tables: list[Table] = []
359
+
360
+ # create namespace using flat IDs
361
+ if namespace_id is not None:
362
+ schema_expr = f"'{escape_like(namespace_id.id, '~')}~_~_%'"
363
+ condition = f"table_name LIKE {schema_expr} ESCAPE '~'"
364
+ else:
365
+ condition = "table_name NOT LIKE '%~_~_%' ESCAPE '~'"
366
+ table_names = await self.conn.query_all(
367
+ str,
368
+ "SELECT table_name\n"
369
+ "FROM information_schema.tables\n"
370
+ f"WHERE table_schema = {self.conn.connection.generator.get_current_schema_stmt()} AND {condition}\n"
371
+ "ORDER BY table_name ASC",
372
+ )
373
+ if table_names:
374
+ if namespace_id is not None:
375
+ scope_id = namespace_id.local_id
376
+ else:
377
+ scope_id = None
378
+
379
+ for table_name in table_names:
380
+ if namespace_id is not None:
381
+ local_id = table_name.removeprefix(f"{namespace_id.local_id}__")
382
+ else:
383
+ local_id = table_name
384
+
385
+ table = await self.get_table(self.get_qualified_id(scope_id, local_id))
386
+ tables.append(table)
387
+
388
+ return self.factory.namespace_class(name=LocalId(""), enums=[], structs=[], tables=tables)
389
+ else:
390
+ return self.factory.namespace_class()
@@ -0,0 +1,179 @@
1
+ import datetime
2
+ import decimal
3
+ import inspect
4
+ import sys
5
+ import types
6
+ import uuid
7
+ from ipaddress import IPv4Address, IPv6Address
8
+ from typing import Any, Optional
9
+
10
+ from strong_typing.core import JsonType
11
+ from strong_typing.inspection import (
12
+ DataclassInstance,
13
+ TypeLike,
14
+ dataclass_fields,
15
+ evaluate_member_type,
16
+ is_dataclass_type,
17
+ is_type_enum,
18
+ is_type_optional,
19
+ is_type_union,
20
+ unwrap_annotated_type,
21
+ unwrap_optional_type,
22
+ unwrap_union_types,
23
+ )
24
+
25
+ from ..model.properties import get_field_properties, is_primary_key_type
26
+ from ..util.typing import TypeGuard
27
+
28
+
29
+ def is_simple_type(typ: Any) -> bool:
30
+ "True if the type is not a composite or user-defined type."
31
+
32
+ typ = unwrap_annotated_type(typ)
33
+
34
+ if (
35
+ typ is bool
36
+ or typ is int
37
+ or typ is float
38
+ or typ is str
39
+ or typ is decimal.Decimal
40
+ or typ is datetime.datetime
41
+ or typ is datetime.date
42
+ or typ is datetime.time
43
+ or typ is datetime.timedelta
44
+ or typ is uuid.UUID
45
+ or typ is JsonType
46
+ or typ is IPv4Address # PostgreSQL only
47
+ or typ is IPv6Address # PostgreSQL only
48
+ ):
49
+ return True
50
+ if is_type_enum(typ):
51
+ return True
52
+ if is_type_optional(typ):
53
+ typ = unwrap_optional_type(typ)
54
+ return is_simple_type(typ)
55
+ return False
56
+
57
+
58
+ def is_entity_type(typ: Any) -> TypeGuard[type[DataclassInstance]]:
59
+ "True for data-class types that have a primary key."
60
+
61
+ if not is_dataclass_type(typ):
62
+ return False
63
+
64
+ return dataclass_has_primary_key(typ)
65
+
66
+
67
+ def is_struct_type(typ: Any) -> TypeGuard[type[DataclassInstance]]:
68
+ "True for data-class types that have no primary key."
69
+
70
+ if not is_dataclass_type(typ):
71
+ return False
72
+
73
+ return not dataclass_has_primary_key(typ)
74
+
75
+
76
+ def is_reference_type(typ: Any, cls: type) -> bool:
77
+ """
78
+ True for an entity type (foreign key) or a union of entity types (discriminated key).
79
+
80
+ :param typ: The type to test.
81
+ :param cls: The context for evaluating forward references.
82
+ """
83
+
84
+ if is_entity_type(typ):
85
+ return True
86
+ elif is_type_union(typ):
87
+ member_types = [evaluate_member_type(t, cls) for t in unwrap_union_types(unwrap_annotated_type(typ))]
88
+ return all(is_entity_type(t) for t in member_types)
89
+ else:
90
+ return False
91
+
92
+
93
+ def is_ip_address_type(field_type: type) -> bool:
94
+ "Check if type is an IPv4 or IPv6 address type."
95
+
96
+ if field_type is IPv4Address or field_type is IPv6Address:
97
+ return True
98
+
99
+ # check if type is IPv4Address | IPv6Address
100
+ if is_type_union(field_type):
101
+ field_type = unwrap_annotated_type(field_type)
102
+ member_types = unwrap_union_types(field_type)
103
+ if len(member_types) != 2:
104
+ return False
105
+ if IPv4Address in member_types and IPv6Address in member_types:
106
+ return True
107
+
108
+ return False
109
+
110
+
111
+ def dataclass_has_primary_key(typ: type[DataclassInstance]) -> bool:
112
+ typ = unwrap_annotated_type(typ)
113
+ for field in dataclass_fields(typ):
114
+ if is_primary_key_type(field.type):
115
+ return True
116
+
117
+ return False
118
+
119
+
120
+ def dataclass_primary_key_name(typ: type[DataclassInstance]) -> str:
121
+ for field in dataclass_fields(typ):
122
+ if is_primary_key_type(field.type):
123
+ return field.name
124
+
125
+ raise TypeError(f"table {typ.__name__} lacks primary key")
126
+
127
+
128
+ def dataclass_primary_key_type(typ: type[DataclassInstance]) -> TypeLike:
129
+ """
130
+ Extracts the primary key data type from a dataclass.
131
+
132
+ This function returns the type of the primary key field without constraint annotations such as identity,
133
+ primary key, or unique. This may be a parameterized or annotated type, e.g. `Annotated[str, MaxLength(255)]`.
134
+ """
135
+
136
+ for field in dataclass_fields(typ):
137
+ props = get_field_properties(field.type)
138
+ if props.is_primary:
139
+ return props.field_type
140
+
141
+ raise TypeError(f"table {typ.__name__} lacks primary key")
142
+
143
+
144
+ def get_entity_types(modules: list[types.ModuleType]) -> list[type[DataclassInstance]]:
145
+ "Returns all entity types defined in one of the modules."
146
+
147
+ entity_types: list[type[DataclassInstance]] = []
148
+ for module in modules:
149
+ for _, obj in inspect.getmembers(module, is_entity_type):
150
+ if sys.modules[obj.__module__] in modules:
151
+ entity_types.append(obj)
152
+ return entity_types
153
+
154
+
155
+ def reference_to_key(typ: TypeLike, cls: type[DataclassInstance]) -> TypeLike:
156
+ """
157
+ Maps a foreign or discriminated reference type to the primary key type of the entity being referenced.
158
+
159
+ :param typ: A reference type, or a regular type.
160
+ :param cls: The entity context in which forward reference types are evaluated.
161
+ :returns: The primary key type of the referenced entity (or entities), or the original type.
162
+ """
163
+
164
+ data_type = evaluate_member_type(typ, cls)
165
+ plain_type = unwrap_annotated_type(data_type)
166
+ if is_type_optional(plain_type):
167
+ # nullable type
168
+ required_type = reference_to_key(unwrap_optional_type(plain_type), cls)
169
+ return Optional[required_type]
170
+ elif is_entity_type(plain_type):
171
+ # foreign key reference
172
+ return dataclass_primary_key_type(plain_type)
173
+ elif is_type_union(plain_type):
174
+ # discriminated key reference
175
+ union_types = tuple(evaluate_member_type(t, cls) for t in unwrap_union_types(plain_type) if t is not None)
176
+ if all(is_entity_type(t) for t in union_types):
177
+ return dataclass_primary_key_type(union_types[0])
178
+
179
+ return data_type