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,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
|