alchemy-kit 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.gitignore +1 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/PKG-INFO +1 -1
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/__init__.py +2 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/_conn_builders/mssql.py +21 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/info_builder.py +38 -4
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model_def/_extractor.py +77 -7
- alchemy_kit-0.2.1/alchemy_kit/model/_model_def/_sql/mssql_check_constraints.sql +9 -0
- alchemy_kit-0.2.1/alchemy_kit/model/_model_def/_sql/mssql_unique_constraints.sql +14 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/_settings.py +2 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/_base.py +10 -4
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/mariadb.py +8 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/mssql.py +8 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/mysql.py +8 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/oracle.py +9 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/postgresql.py +9 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/sqlite.py +8 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/_driver_types.py +6 -3
- alchemy_kit-0.2.1/docs/builder/base-builder.md +0 -0
- alchemy_kit-0.2.1/docs/builder/insert-builder.md +0 -0
- alchemy_kit-0.2.1/docs/builder/select-builder.md +0 -0
- alchemy_kit-0.2.1/docs/builder/temp-builder.md +0 -0
- alchemy_kit-0.2.1/docs/connect/connection-info.md +0 -0
- alchemy_kit-0.2.1/docs/connect/engine-handler.md +0 -0
- alchemy_kit-0.2.1/docs/connect/engine-manager.md +0 -0
- alchemy_kit-0.2.1/docs/connect/info-builder.md +0 -0
- alchemy_kit-0.2.1/docs/model/base-model.md +0 -0
- alchemy_kit-0.2.1/docs/model/build.md +0 -0
- alchemy_kit-0.2.1/docs/model/schema-config.md +0 -0
- alchemy_kit-0.2.1/docs/model/units.md +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/pyproject.toml +1 -1
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/model/test_builder.py +10 -1
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/model/test_inspector_def.py +90 -0
- alchemy_kit-0.2.1/tests/resources/test_reflected_type_facts.py +82 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/uv.lock +289 -3
- alchemy_kit-0.2.0/builder.md +0 -222
- alchemy_kit-0.2.0/tests/resources/test_reflected_type_facts.py +0 -41
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.claude/settings.local.json +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.grimp_cache/.gitignore +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.grimp_cache/CACHEDIR.TAG +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.grimp_cache/alchemy_kit.meta.json +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.grimp_cache/b436f6e8205b1418cbee6b7347a3c7b0d1be876b.data.json +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.import_linter_cache/.gitignore +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.import_linter_cache/CACHEDIR.TAG +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.import_linter_cache/alchemy_kit.meta.json +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.import_linter_cache/b436f6e8205b1418cbee6b7347a3c7b0d1be876b.data.json +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/.python-version +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/LICENSE +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/README.md +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/builder/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/builder/_base.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/builder/_insert.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/builder/_select.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/builder/_temp.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/_conn_builders/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/_drivers.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/_engine_handler.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/_engine_manager.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/connect/_info.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builder.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/_build_py_file.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/_build_stub_file.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/_check_parser.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/_column_metadata.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/_svg.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/py_template_file.txt +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_builders/stub_template_file.txt +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model/column_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model/constraint_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model/db_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model/object_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model/schema_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model_def/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_model_def/_frames.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_schema_config.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/_utils.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/base_model.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/units/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/units/_column_unit.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/model/units/_object_unit.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/_better_logger.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/_identifiers.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/_sql.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dialect_map/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/resources/dir_helpers.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/_sql_parameters.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/_sql_utilities.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/api_types.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/auth_types.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/dialect_types.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/errors/__init__.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/errors/_model_validation_error.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/generic_path.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/py_type_parameters.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/sql_type_parameters.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/alchemy_kit/types/sql_types.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/builder/test_builders.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/connect/test_connect.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/connect/test_engine_handler.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/dotenv_test +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/model/test_check_codegen.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/model/test_check_parser.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/model/test_fk_validation.py +0 -0
- {alchemy_kit-0.2.0 → alchemy_kit-0.2.1}/tests/test_json.json +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
from pydantic import SecretStr
|
|
2
4
|
import sqlalchemy
|
|
3
5
|
|
|
@@ -5,6 +7,18 @@ from ...types import _driver_types
|
|
|
5
7
|
from ...types.api_types import SqlServerApi
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
def _tls_query(
|
|
11
|
+
encrypt: Optional[bool],
|
|
12
|
+
trust_server_certificate: Optional[bool],
|
|
13
|
+
) -> dict[str, str]:
|
|
14
|
+
query: dict[str, str] = {}
|
|
15
|
+
if encrypt is not None:
|
|
16
|
+
query["Encrypt"] = "yes" if encrypt else "no"
|
|
17
|
+
if trust_server_certificate is not None:
|
|
18
|
+
query["TrustServerCertificate"] = "yes" if trust_server_certificate else "no"
|
|
19
|
+
return query
|
|
20
|
+
|
|
21
|
+
|
|
8
22
|
def sql_auth(
|
|
9
23
|
driver: _driver_types.SqlServerDrivers,
|
|
10
24
|
server: str,
|
|
@@ -12,6 +26,8 @@ def sql_auth(
|
|
|
12
26
|
user_name: str,
|
|
13
27
|
user_pwd: SecretStr,
|
|
14
28
|
api: SqlServerApi,
|
|
29
|
+
encrypt: Optional[bool] = None,
|
|
30
|
+
trust_server_certificate: Optional[bool] = None,
|
|
15
31
|
) -> sqlalchemy.URL:
|
|
16
32
|
|
|
17
33
|
if isinstance(driver, _driver_types.SqlServerODBC):
|
|
@@ -25,6 +41,7 @@ def sql_auth(
|
|
|
25
41
|
"driver": driver,
|
|
26
42
|
"ApplicationIntent": "ReadWrite",
|
|
27
43
|
"MultiSubnetFailover": "yes",
|
|
44
|
+
**_tls_query(encrypt, trust_server_certificate),
|
|
28
45
|
}
|
|
29
46
|
)
|
|
30
47
|
|
|
@@ -37,6 +54,7 @@ def sql_auth(
|
|
|
37
54
|
database=database,
|
|
38
55
|
query={
|
|
39
56
|
"driver": driver,
|
|
57
|
+
**_tls_query(encrypt, trust_server_certificate),
|
|
40
58
|
}
|
|
41
59
|
)
|
|
42
60
|
|
|
@@ -46,6 +64,8 @@ def microsoft_auth(
|
|
|
46
64
|
server: str,
|
|
47
65
|
database: str,
|
|
48
66
|
api: SqlServerApi,
|
|
67
|
+
encrypt: Optional[bool] = None,
|
|
68
|
+
trust_server_certificate: Optional[bool] = None,
|
|
49
69
|
) -> sqlalchemy.URL:
|
|
50
70
|
return sqlalchemy.URL.create(
|
|
51
71
|
drivername=f"mssql+{api}",
|
|
@@ -56,5 +76,6 @@ def microsoft_auth(
|
|
|
56
76
|
"Trusted_Connection": "yes",
|
|
57
77
|
"ApplicationIntent": "ReadWrite",
|
|
58
78
|
"MultiSubnetFailover": "yes",
|
|
79
|
+
**_tls_query(encrypt, trust_server_certificate),
|
|
59
80
|
}
|
|
60
81
|
)
|
|
@@ -84,6 +84,20 @@ def from_env(env_path: GenericPath) -> ConnectionInfo:
|
|
|
84
84
|
|
|
85
85
|
return _match_dialect(dialect, env_data.get(Settings.FileExtraction.unique_id_marker), env_data)
|
|
86
86
|
|
|
87
|
+
_TRUE_VALUES = {"yes", "true", "1", "on"}
|
|
88
|
+
_FALSE_VALUES = {"no", "false", "0", "off"}
|
|
89
|
+
|
|
90
|
+
def _parse_optional_bool(value: Optional[str]) -> Optional[bool]:
|
|
91
|
+
"""Parse a connection-file flag into a tri-state bool (``None`` when unset)."""
|
|
92
|
+
if value is None or value == "":
|
|
93
|
+
return None
|
|
94
|
+
normalized = value.strip().lower()
|
|
95
|
+
if normalized in _TRUE_VALUES:
|
|
96
|
+
return True
|
|
97
|
+
if normalized in _FALSE_VALUES:
|
|
98
|
+
return False
|
|
99
|
+
raise ValueError(f"Expected a boolean-like value, got {value!r}")
|
|
100
|
+
|
|
87
101
|
def _match_dialect(dialect: DialectTypes, unique_id: Optional[str], conn_data: dict[str, str]) -> ConnectionInfo: # CONTINUE
|
|
88
102
|
"""Dispatch a parsed connection definition to the right dialect builder.
|
|
89
103
|
|
|
@@ -109,6 +123,10 @@ def _match_dialect(dialect: DialectTypes, unique_id: Optional[str], conn_data: d
|
|
|
109
123
|
case DialectTypes.MSSQL:
|
|
110
124
|
driver = _driver_types.parse_sql_server_driver(conn_data[Settings.FileExtraction.driver_marker])
|
|
111
125
|
api = SqlServerApi(conn_data.get(Settings.FileExtraction.api_marker, SqlServerApi.PYODBC))
|
|
126
|
+
encrypt = _parse_optional_bool(conn_data.get(Settings.FileExtraction.encrypt_marker))
|
|
127
|
+
trust_server_certificate = _parse_optional_bool(
|
|
128
|
+
conn_data.get(Settings.FileExtraction.trust_server_certificate_marker)
|
|
129
|
+
)
|
|
112
130
|
|
|
113
131
|
if auth == AuthType.SQL_AUTH:
|
|
114
132
|
return from_values_mssql(
|
|
@@ -119,7 +137,9 @@ def _match_dialect(dialect: DialectTypes, unique_id: Optional[str], conn_data: d
|
|
|
119
137
|
user_name=conn_data[Settings.FileExtraction.user_name_marker],
|
|
120
138
|
user_pwd=SecretStr(conn_data[Settings.FileExtraction.user_pwd_marker]),
|
|
121
139
|
unique_id=unique_id,
|
|
122
|
-
api=cast(SqlServerApi, api)
|
|
140
|
+
api=cast(SqlServerApi, api),
|
|
141
|
+
encrypt=encrypt,
|
|
142
|
+
trust_server_certificate=trust_server_certificate,
|
|
123
143
|
)
|
|
124
144
|
elif auth == AuthType.MICROSOFT_AUTH:
|
|
125
145
|
return from_values_mssql(
|
|
@@ -128,7 +148,9 @@ def _match_dialect(dialect: DialectTypes, unique_id: Optional[str], conn_data: d
|
|
|
128
148
|
conn_data[Settings.FileExtraction.server_marker],
|
|
129
149
|
conn_data[Settings.FileExtraction.database_marker],
|
|
130
150
|
unique_id=unique_id,
|
|
131
|
-
api=api
|
|
151
|
+
api=api,
|
|
152
|
+
encrypt=encrypt,
|
|
153
|
+
trust_server_certificate=trust_server_certificate,
|
|
132
154
|
)
|
|
133
155
|
else:
|
|
134
156
|
raise ValueError(f"'{auth}' is not a supported authentication method")
|
|
@@ -157,6 +179,8 @@ def from_values_mssql(
|
|
|
157
179
|
*,
|
|
158
180
|
unique_id: Optional[str] = None,
|
|
159
181
|
api: SqlServerApi = SqlServerApi.PYODBC,
|
|
182
|
+
encrypt: Optional[bool] = None,
|
|
183
|
+
trust_server_certificate: Optional[bool] = None,
|
|
160
184
|
) -> ConnectionInfo: ...
|
|
161
185
|
@overload
|
|
162
186
|
def from_values_mssql(
|
|
@@ -169,6 +193,8 @@ def from_values_mssql(
|
|
|
169
193
|
user_pwd: SecretStr,
|
|
170
194
|
unique_id: Optional[str] = None,
|
|
171
195
|
api: SqlServerApi = SqlServerApi.PYODBC,
|
|
196
|
+
encrypt: Optional[bool] = None,
|
|
197
|
+
trust_server_certificate: Optional[bool] = None,
|
|
172
198
|
) -> ConnectionInfo: ...
|
|
173
199
|
|
|
174
200
|
def from_values_mssql(
|
|
@@ -181,6 +207,8 @@ def from_values_mssql(
|
|
|
181
207
|
user_pwd: Optional[SecretStr] = None,
|
|
182
208
|
unique_id: Optional[str] = None,
|
|
183
209
|
api: SqlServerApi = SqlServerApi.PYODBC,
|
|
210
|
+
encrypt: Optional[bool] = None,
|
|
211
|
+
trust_server_certificate: Optional[bool] = None,
|
|
184
212
|
) -> ConnectionInfo:
|
|
185
213
|
"""Build a SQL Server connection from individual values.
|
|
186
214
|
|
|
@@ -214,10 +242,16 @@ def from_values_mssql(
|
|
|
214
242
|
case AuthType.SQL_AUTH:
|
|
215
243
|
if user_name is None or user_pwd is None:
|
|
216
244
|
raise ValueError("SQL Authentication needs user_name and 'user_pwd' to be 'passed'")
|
|
217
|
-
conn_url = mssql.sql_auth(
|
|
245
|
+
conn_url = mssql.sql_auth(
|
|
246
|
+
driver, server, database, user_name, user_pwd, api,
|
|
247
|
+
encrypt, trust_server_certificate,
|
|
248
|
+
)
|
|
218
249
|
|
|
219
250
|
case AuthType.MICROSOFT_AUTH:
|
|
220
|
-
conn_url = mssql.microsoft_auth(
|
|
251
|
+
conn_url = mssql.microsoft_auth(
|
|
252
|
+
driver, server, database, api,
|
|
253
|
+
encrypt, trust_server_certificate,
|
|
254
|
+
)
|
|
221
255
|
|
|
222
256
|
return ConnectionInfo(conn_url, unique_id)
|
|
223
257
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
from typing import Any, Mapping, Sequence
|
|
3
4
|
|
|
4
5
|
import pandas as pd
|
|
5
6
|
from pandera.typing import DataFrame
|
|
7
|
+
from sqlalchemy.engine.interfaces import ReflectedCheckConstraint, ReflectedUniqueConstraint
|
|
6
8
|
from sqlalchemy.exc import NoSuchTableError
|
|
7
9
|
|
|
8
10
|
from ...connect._engine_handler import EngineHandler
|
|
11
|
+
from ...resources._sql import SQL
|
|
9
12
|
from ...resources.dialect_map import DialectMap, ReflectedTypeFacts, get_map
|
|
10
13
|
from ...types.dialect_types import DialectTypes
|
|
11
14
|
from .._schema_config import SchemaConfig
|
|
@@ -35,6 +38,16 @@ _SYSTEM_SCHEMA_PREFIXES: dict[DialectTypes, tuple[str, ...]] = {
|
|
|
35
38
|
DialectTypes.POSTGRESQL: ("pg_",),
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
_FALLBACK_SQL_DIR = Path(__file__).parent / "_sql"
|
|
42
|
+
|
|
43
|
+
_UNIQUE_CONSTRAINTS_FALLBACK_SQL: dict[DialectTypes, str] = {
|
|
44
|
+
DialectTypes.MSSQL: "mssql_unique_constraints",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_CHECK_CONSTRAINTS_FALLBACK_SQL: dict[DialectTypes, str] = {
|
|
48
|
+
DialectTypes.MSSQL: "mssql_check_constraints",
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
|
|
39
52
|
class MetadataExtractor:
|
|
40
53
|
"""Database metadata extraction backed by ``sqlalchemy.Inspector``.
|
|
@@ -166,7 +179,7 @@ class MetadataExtractor:
|
|
|
166
179
|
index_type="PRIMARY KEY",
|
|
167
180
|
)
|
|
168
181
|
|
|
169
|
-
for constraint in self.
|
|
182
|
+
for constraint in self._get_unique_constraints(schema_name, object_name):
|
|
170
183
|
add_cluster(
|
|
171
184
|
constraint.get("name") or f"uq_{object_name}",
|
|
172
185
|
constraint["column_names"],
|
|
@@ -217,11 +230,7 @@ class MetadataExtractor:
|
|
|
217
230
|
return ListForeignKeys.validate(df)
|
|
218
231
|
|
|
219
232
|
def list_check_constraints(self, schema_name: str, object_name: str) -> DataFrame[ListCheckConstraints]:
|
|
220
|
-
|
|
221
|
-
checks = self._inspector.get_check_constraints(object_name, schema=schema_name)
|
|
222
|
-
except NotImplementedError:
|
|
223
|
-
checks = []
|
|
224
|
-
|
|
233
|
+
checks = self._get_check_constraints(schema_name, object_name)
|
|
225
234
|
column_names = [column["name"] for column in self._get_columns(schema_name, object_name)]
|
|
226
235
|
|
|
227
236
|
rows = []
|
|
@@ -261,10 +270,71 @@ class MetadataExtractor:
|
|
|
261
270
|
pk = self._inspector.get_pk_constraint(object_name, schema=schema_name)
|
|
262
271
|
return list(pk.get("constrained_columns") or []) if pk else []
|
|
263
272
|
|
|
273
|
+
def _get_unique_constraints(self, schema_name: str, object_name: str) -> list[ReflectedUniqueConstraint]:
|
|
274
|
+
try:
|
|
275
|
+
return self._inspector.get_unique_constraints(object_name, schema=schema_name)
|
|
276
|
+
except NotImplementedError:
|
|
277
|
+
return self._unique_constraints_fallback(schema_name, object_name)
|
|
278
|
+
|
|
279
|
+
def _unique_constraints_fallback(self, schema_name: str, object_name: str) -> list[ReflectedUniqueConstraint]:
|
|
280
|
+
data = self._run_fallback_sql(_UNIQUE_CONSTRAINTS_FALLBACK_SQL, schema_name, object_name)
|
|
281
|
+
if data is None:
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
grouped: dict[str, list[str]] = {}
|
|
285
|
+
for row in data.itertuples(index=False):
|
|
286
|
+
grouped.setdefault(str(row.constraint_name), []).append(str(row.column_name))
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
ReflectedUniqueConstraint(
|
|
290
|
+
name=name,
|
|
291
|
+
column_names=columns,
|
|
292
|
+
comment=None,
|
|
293
|
+
duplicates_index=None,
|
|
294
|
+
dialect_options={},
|
|
295
|
+
)
|
|
296
|
+
for name, columns in grouped.items()
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
def _get_check_constraints(self, schema_name: str, object_name: str) -> list[ReflectedCheckConstraint]:
|
|
300
|
+
try:
|
|
301
|
+
return self._inspector.get_check_constraints(object_name, schema=schema_name)
|
|
302
|
+
except NotImplementedError:
|
|
303
|
+
return self._check_constraints_fallback(schema_name, object_name)
|
|
304
|
+
|
|
305
|
+
def _check_constraints_fallback(self, schema_name: str, object_name: str) -> list[ReflectedCheckConstraint]:
|
|
306
|
+
data = self._run_fallback_sql(_CHECK_CONSTRAINTS_FALLBACK_SQL, schema_name, object_name)
|
|
307
|
+
if data is None:
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
return [
|
|
311
|
+
ReflectedCheckConstraint(
|
|
312
|
+
name=str(row.constraint_name),
|
|
313
|
+
sqltext=str(row.sqltext),
|
|
314
|
+
comment=None,
|
|
315
|
+
dialect_options={},
|
|
316
|
+
)
|
|
317
|
+
for row in data.itertuples(index=False)
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
def _run_fallback_sql(self, sql_names: Mapping[DialectTypes, str], schema_name: str, object_name: str) -> pd.DataFrame | None:
|
|
321
|
+
sql_name = sql_names.get(self._dialect)
|
|
322
|
+
if sql_name is None:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
sql = SQL(
|
|
326
|
+
sql_path=sql_name,
|
|
327
|
+
query_parameters={"schema_name": schema_name, "object_name": object_name},
|
|
328
|
+
)
|
|
329
|
+
sql.set_script_dir(_FALLBACK_SQL_DIR)
|
|
330
|
+
|
|
331
|
+
_, data = self._handler.run_sql(sql)
|
|
332
|
+
return data
|
|
333
|
+
|
|
264
334
|
def _get_single_column_uniques(self, schema_name: str, object_name: str) -> set[str]:
|
|
265
335
|
unique_columns: set[str] = set()
|
|
266
336
|
|
|
267
|
-
for constraint in self.
|
|
337
|
+
for constraint in self._get_unique_constraints(schema_name, object_name):
|
|
268
338
|
if len(constraint["column_names"]) == 1:
|
|
269
339
|
unique_columns.add(constraint["column_names"][0])
|
|
270
340
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
cc.name AS constraint_name,
|
|
3
|
+
cc.definition AS sqltext
|
|
4
|
+
FROM sys.check_constraints cc
|
|
5
|
+
JOIN sys.tables t ON t.object_id = cc.parent_object_id
|
|
6
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
7
|
+
WHERE s.name = @schema_name
|
|
8
|
+
AND t.name = @object_name
|
|
9
|
+
ORDER BY cc.name
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
kc.name AS constraint_name,
|
|
3
|
+
c.name AS column_name
|
|
4
|
+
FROM sys.key_constraints kc
|
|
5
|
+
JOIN sys.tables t ON t.object_id = kc.parent_object_id
|
|
6
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
7
|
+
JOIN sys.index_columns ic ON ic.object_id = kc.parent_object_id
|
|
8
|
+
AND ic.index_id = kc.unique_index_id
|
|
9
|
+
JOIN sys.columns c ON c.object_id = ic.object_id
|
|
10
|
+
AND c.column_id = ic.column_id
|
|
11
|
+
WHERE kc.type = 'UQ'
|
|
12
|
+
AND s.name = @schema_name
|
|
13
|
+
AND t.name = @object_name
|
|
14
|
+
ORDER BY kc.name, ic.key_ordinal
|
|
@@ -54,6 +54,7 @@ class DialectMap[_TypeParameters: str](ABC):
|
|
|
54
54
|
dialect_paramaters: ClassVar[frozenset[str]]
|
|
55
55
|
py_types: ClassVar[dict[str, PyTypeParameters]]
|
|
56
56
|
sa_types: ClassVar[dict[str, SaTypeFactory]]
|
|
57
|
+
_reflected_synonyms: ClassVar[dict[str, str]] = {}
|
|
57
58
|
|
|
58
59
|
_quote_open: ClassVar[str] = '"'
|
|
59
60
|
_quote_close: ClassVar[str] = '"'
|
|
@@ -108,9 +109,13 @@ class DialectMap[_TypeParameters: str](ABC):
|
|
|
108
109
|
|
|
109
110
|
The type name is the lowercased class name, which for dialect-specific
|
|
110
111
|
reflected types matches the dialect's SQL type name (``NVARCHAR`` ->
|
|
111
|
-
``nvarchar``).
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
``nvarchar``). Dialects sometimes reflect a SQL type as a class whose
|
|
113
|
+
name differs from the type's own name (MSSQL ``int`` ->
|
|
114
|
+
``sqltypes.INTEGER``); ``_reflected_synonyms`` maps those class names
|
|
115
|
+
back to the dialect's canonical type name. A string type whose
|
|
116
|
+
``length`` is ``None`` is unbounded and reported as ``-1``; parameters
|
|
117
|
+
a type does not carry are ``0``. Dialects with different conventions
|
|
118
|
+
override this.
|
|
114
119
|
"""
|
|
115
120
|
length = getattr(sa_type, "length", None)
|
|
116
121
|
if hasattr(sa_type, "length") and length is None:
|
|
@@ -118,8 +123,9 @@ class DialectMap[_TypeParameters: str](ABC):
|
|
|
118
123
|
else:
|
|
119
124
|
max_length = length or 0
|
|
120
125
|
|
|
126
|
+
sql_type = type(sa_type).__name__.lower()
|
|
121
127
|
return ReflectedTypeFacts(
|
|
122
|
-
sql_type=
|
|
128
|
+
sql_type=cls._reflected_synonyms.get(sql_type, sql_type),
|
|
123
129
|
max_length=max_length,
|
|
124
130
|
precision=getattr(sa_type, "precision", None) or 0,
|
|
125
131
|
scale=getattr(sa_type, "scale", None) or 0,
|
|
@@ -140,6 +140,13 @@ class MariadbMap(DialectMap[MariadbTypeParameters]):
|
|
|
140
140
|
"geometrycollection": NullType,
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
# Reflected class name -> dialect type name (NCHAR/NVARCHAR are national
|
|
144
|
+
# charset variants MariaDB stores as char/varchar)
|
|
145
|
+
_REFLECTED_SYNONYMS: dict[str, MariadbTypeParameters] = {
|
|
146
|
+
"nchar": "char",
|
|
147
|
+
"nvarchar": "varchar",
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
# SQL type name buckets (MariaDB lengths are in characters)
|
|
144
151
|
_length_types_char: set[MariadbTypeParameters] = {"char", "varchar"}
|
|
145
152
|
_length_types_bin: set[MariadbTypeParameters] = {"binary", "varbinary"}
|
|
@@ -150,6 +157,7 @@ class MariadbMap(DialectMap[MariadbTypeParameters]):
|
|
|
150
157
|
# Parent parameters
|
|
151
158
|
py_types = cast(dict[str, PyTypeParameters], _PY_TYPES)
|
|
152
159
|
sa_types = cast(dict[str, SaTypeFactory], _SA_TYPES)
|
|
160
|
+
_reflected_synonyms = cast(dict[str, str], _REFLECTED_SYNONYMS)
|
|
153
161
|
dialect = DialectTypes.MARIADB
|
|
154
162
|
dialect_paramaters = frozenset(get_args(MariadbTypeParameters))
|
|
155
163
|
_quote_open = "`"
|
|
@@ -95,6 +95,13 @@ class MssqlMap(DialectMap[MssqlTypeParameters]):
|
|
|
95
95
|
"sql_variant": sa_mssql.SQL_VARIANT,
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
# Reflected class name -> dialect type name ("int" reflects as
|
|
99
|
+
# sqltypes.INTEGER; "double precision" is an alias of float(53))
|
|
100
|
+
_REFLECTED_SYNONYMS: dict[str, MssqlTypeParameters] = {
|
|
101
|
+
"integer": "int",
|
|
102
|
+
"double_precision": "float",
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
# SQL type name buckets
|
|
99
106
|
_length_types_char: set[MssqlTypeParameters] = {"char", "varchar"} # 1 byte/char
|
|
100
107
|
_length_types_nchar: set[MssqlTypeParameters] = {"nchar", "nvarchar"} # 2 bytes/char (Unicode)
|
|
@@ -106,6 +113,7 @@ class MssqlMap(DialectMap[MssqlTypeParameters]):
|
|
|
106
113
|
# Parent parameters
|
|
107
114
|
py_types = cast(dict[str, PyTypeParameters], _PY_TYPES)
|
|
108
115
|
sa_types = cast(dict[str, SaTypeFactory], _SA_TYPES)
|
|
116
|
+
_reflected_synonyms = cast(dict[str, str], _REFLECTED_SYNONYMS)
|
|
109
117
|
dialect = DialectTypes.MSSQL
|
|
110
118
|
dialect_paramaters = frozenset(get_args(MssqlTypeParameters))
|
|
111
119
|
_quote_open = "["
|
|
@@ -130,6 +130,13 @@ class MysqlMap(DialectMap[MysqlTypeParameters]):
|
|
|
130
130
|
"geometrycollection": NullType,
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
# Reflected class name -> dialect type name (NCHAR/NVARCHAR are national
|
|
134
|
+
# charset variants MySQL stores as char/varchar)
|
|
135
|
+
_REFLECTED_SYNONYMS: dict[str, MysqlTypeParameters] = {
|
|
136
|
+
"nchar": "char",
|
|
137
|
+
"nvarchar": "varchar",
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
# SQL type name buckets (MySQL lengths are in characters)
|
|
134
141
|
_length_types_char: set[MysqlTypeParameters] = {"char", "varchar"}
|
|
135
142
|
_length_types_bin: set[MysqlTypeParameters] = {"binary", "varbinary"}
|
|
@@ -140,6 +147,7 @@ class MysqlMap(DialectMap[MysqlTypeParameters]):
|
|
|
140
147
|
# Parent parameters
|
|
141
148
|
py_types = cast(dict[str, PyTypeParameters], _PY_TYPES)
|
|
142
149
|
sa_types = cast(dict[str, SaTypeFactory], _SA_TYPES)
|
|
150
|
+
_reflected_synonyms = cast(dict[str, str], _REFLECTED_SYNONYMS)
|
|
143
151
|
dialect = DialectTypes.MYSQL
|
|
144
152
|
dialect_paramaters = frozenset(get_args(MysqlTypeParameters))
|
|
145
153
|
_quote_open = "`"
|
|
@@ -96,6 +96,14 @@ class OracleMap(DialectMap[OracleTypeParameters]):
|
|
|
96
96
|
"xmltype": NullType,
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
# Reflected class name -> dialect type name (NVARCHAR2 reflects as the
|
|
100
|
+
# generic NVARCHAR class; REAL and DOUBLE PRECISION are FLOAT aliases)
|
|
101
|
+
_REFLECTED_SYNONYMS: dict[str, OracleTypeParameters] = {
|
|
102
|
+
"nvarchar": "nvarchar2",
|
|
103
|
+
"real": "float",
|
|
104
|
+
"double_precision": "float",
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
# SQL type name buckets (Oracle lengths reported in characters/bytes as-is)
|
|
100
108
|
_length_types_char: set[OracleTypeParameters] = {
|
|
101
109
|
"char", "nchar", "varchar", "varchar2", "nvarchar2",
|
|
@@ -112,6 +120,7 @@ class OracleMap(DialectMap[OracleTypeParameters]):
|
|
|
112
120
|
# Parent parameters
|
|
113
121
|
py_types = cast(dict[str, PyTypeParameters], _PY_TYPES)
|
|
114
122
|
sa_types = cast(dict[str, SaTypeFactory], _SA_TYPES)
|
|
123
|
+
_reflected_synonyms = cast(dict[str, str], _REFLECTED_SYNONYMS)
|
|
115
124
|
dialect = DialectTypes.ORACLE
|
|
116
125
|
dialect_paramaters = frozenset(get_args(OracleTypeParameters))
|
|
117
126
|
|
|
@@ -190,6 +190,14 @@ class PostgresqlMap(DialectMap[PostgresqlTypeParameters]):
|
|
|
190
190
|
"array": NullType,
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
# Reflected class name -> dialect type name (catalog types '"char"' and
|
|
194
|
+
# 'name' reflect as the generic sqltypes.String; FLOAT is stored as
|
|
195
|
+
# double precision)
|
|
196
|
+
_REFLECTED_SYNONYMS: dict[str, PostgresqlTypeParameters] = {
|
|
197
|
+
"string": "text",
|
|
198
|
+
"float": "double precision",
|
|
199
|
+
}
|
|
200
|
+
|
|
193
201
|
# SQL type name buckets (PostgreSQL lengths are in characters)
|
|
194
202
|
_length_types_char: set[PostgresqlTypeParameters] = {
|
|
195
203
|
"character", "char", "character varying", "varchar", "bpchar",
|
|
@@ -204,6 +212,7 @@ class PostgresqlMap(DialectMap[PostgresqlTypeParameters]):
|
|
|
204
212
|
# Parent parameters
|
|
205
213
|
py_types = cast(dict[str, PyTypeParameters], _PY_TYPES)
|
|
206
214
|
sa_types = cast(dict[str, SaTypeFactory], _SA_TYPES)
|
|
215
|
+
_reflected_synonyms = cast(dict[str, str], _REFLECTED_SYNONYMS)
|
|
207
216
|
dialect = DialectTypes.POSTGRESQL
|
|
208
217
|
dialect_paramaters = frozenset(get_args(PostgresqlTypeParameters))
|
|
209
218
|
|
|
@@ -82,6 +82,13 @@ class SqliteMap(DialectMap[SqliteTypeParameters]):
|
|
|
82
82
|
"blob": sa_sqlite.BLOB,
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
# Reflected class name -> dialect type name (a declared CHAR reflects as
|
|
86
|
+
# the generic CHAR class; TIMESTAMP behaves as datetime)
|
|
87
|
+
_REFLECTED_SYNONYMS: dict[str, SqliteTypeParameters] = {
|
|
88
|
+
"char": "character",
|
|
89
|
+
"timestamp": "datetime",
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
# SQL type name buckets (SQLite ignores length constraints, but declared
|
|
86
93
|
# lengths are preserved for rendering and validation)
|
|
87
94
|
_length_types_char: set[SqliteTypeParameters] = {
|
|
@@ -93,6 +100,7 @@ class SqliteMap(DialectMap[SqliteTypeParameters]):
|
|
|
93
100
|
# Parent parameters
|
|
94
101
|
py_types = cast(dict[str, PyTypeParameters], _PY_TYPES)
|
|
95
102
|
sa_types = cast(dict[str, SaTypeFactory], _SA_TYPES)
|
|
103
|
+
_reflected_synonyms = cast(dict[str, str], _REFLECTED_SYNONYMS)
|
|
96
104
|
dialect = DialectTypes.SQLITE
|
|
97
105
|
dialect_paramaters = frozenset(get_args(SqliteTypeParameters))
|
|
98
106
|
|
|
@@ -2,9 +2,12 @@ from typing import TypeAlias, get_args
|
|
|
2
2
|
from enum import StrEnum
|
|
3
3
|
|
|
4
4
|
class SqlServerODBC(StrEnum):
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
DRIVER_18 = "ODBC Driver 18 for SQL Server"
|
|
6
|
+
DRIVER_17 = "ODBC Driver 17 for SQL Server"
|
|
7
|
+
DRIVER_13_1 = "ODBC Driver 13.1 for SQL Server"
|
|
8
|
+
DRIVER_13 = "ODBC Driver 13 for SQL Server"
|
|
9
|
+
DRIVER_11 = "ODBC Driver 11 for SQL Server"
|
|
10
|
+
SQL_SERVER = "SQL Server"
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class SqlServerNative(StrEnum):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -5,17 +5,20 @@ from typing import cast
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from alchemy_kit.connect._info import ConnectionInfo
|
|
8
|
+
from alchemy_kit.connect import info_builder
|
|
8
9
|
from alchemy_kit.model import build
|
|
9
10
|
from alchemy_kit.model import _builder
|
|
10
11
|
from alchemy_kit.model._model.column_model import ColumnModel, ForeignKeyModel
|
|
11
12
|
from alchemy_kit.model._model.constraint_model import CheckConstraintModel, FilteredUniqueIndexModel
|
|
13
|
+
from alchemy_kit.model import SchemaConfig
|
|
12
14
|
from alchemy_kit.model._model.db_model import DBModel
|
|
13
15
|
from alchemy_kit.model._model.object_model import ObjectModel
|
|
14
16
|
from alchemy_kit.model._model.schema_model import SchemaModel
|
|
15
17
|
from alchemy_kit.types.dialect_types import DialectTypes
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
21
|
+
PREVIEW_DIR = ROOT / "secret_model_preview"
|
|
19
22
|
|
|
20
23
|
def _column(name: str, column_type: str, *, nullable: bool = False, fk_ref: ForeignKeyModel | None = None) -> ColumnModel:
|
|
21
24
|
return ColumnModel(
|
|
@@ -117,3 +120,9 @@ def test_build_generates_package(monkeypatch: pytest.MonkeyPatch):
|
|
|
117
120
|
for path in result_dir.rglob("*.py*"):
|
|
118
121
|
compile(path.read_text(), str(path), "exec")
|
|
119
122
|
|
|
123
|
+
def test_mssql_local_build():
|
|
124
|
+
conn = info_builder.from_env(ROOT/".env-mssql")
|
|
125
|
+
config = SchemaConfig()
|
|
126
|
+
config.include_schema("uploader")
|
|
127
|
+
build(conn, ROOT/"secret_local_mssql", schema_config=config, clear_result_dir=True)
|
|
128
|
+
|