sqlspec 0.25.0__py3-none-any.whl → 0.27.0__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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""MySQL-specific data dictionary for metadata queries via asyncmy."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
|
+
|
|
6
|
+
from sqlspec.driver import AsyncDataDictionaryBase, AsyncDriverAdapterBase, VersionInfo
|
|
7
|
+
from sqlspec.utils.logging import get_logger
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from sqlspec.adapters.asyncmy.driver import AsyncmyDriver
|
|
13
|
+
|
|
14
|
+
logger = get_logger("adapters.asyncmy.data_dictionary")
|
|
15
|
+
|
|
16
|
+
# Compiled regex patterns
|
|
17
|
+
VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)")
|
|
18
|
+
|
|
19
|
+
__all__ = ("MySQLAsyncDataDictionary",)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MySQLAsyncDataDictionary(AsyncDataDictionaryBase):
|
|
23
|
+
"""MySQL-specific async data dictionary."""
|
|
24
|
+
|
|
25
|
+
async def get_version(self, driver: AsyncDriverAdapterBase) -> "VersionInfo | None":
|
|
26
|
+
"""Get MySQL database version information.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
driver: Async database driver instance
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
MySQL version information or None if detection fails
|
|
33
|
+
"""
|
|
34
|
+
result = await cast("AsyncmyDriver", driver).select_value_or_none("SELECT VERSION() as version")
|
|
35
|
+
if not result:
|
|
36
|
+
logger.warning("No MySQL version information found")
|
|
37
|
+
|
|
38
|
+
# Parse version like "8.0.33-0ubuntu0.22.04.2" or "5.7.42-log"
|
|
39
|
+
version_match = VERSION_PATTERN.search(str(result))
|
|
40
|
+
if not version_match:
|
|
41
|
+
logger.warning("Could not parse MySQL version: %s", result)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
major, minor, patch = map(int, version_match.groups())
|
|
45
|
+
version_info = VersionInfo(major, minor, patch)
|
|
46
|
+
logger.debug("Detected MySQL version: %s", version_info)
|
|
47
|
+
return version_info
|
|
48
|
+
|
|
49
|
+
async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
|
|
50
|
+
"""Check if MySQL database supports a specific feature.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
driver: MySQL async driver instance
|
|
54
|
+
feature: Feature name to check
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if feature is supported, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
version_info = await self.get_version(driver)
|
|
60
|
+
if not version_info:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
feature_checks: dict[str, Callable[..., bool]] = {
|
|
64
|
+
"supports_json": lambda v: v >= VersionInfo(5, 7, 8),
|
|
65
|
+
"supports_cte": lambda v: v >= VersionInfo(8, 0, 1),
|
|
66
|
+
"supports_window_functions": lambda v: v >= VersionInfo(8, 0, 2),
|
|
67
|
+
"supports_returning": lambda _: False, # MySQL doesn't have RETURNING
|
|
68
|
+
"supports_upsert": lambda _: True, # ON DUPLICATE KEY UPDATE available
|
|
69
|
+
"supports_transactions": lambda _: True,
|
|
70
|
+
"supports_prepared_statements": lambda _: True,
|
|
71
|
+
"supports_schemas": lambda _: True, # MySQL calls them databases
|
|
72
|
+
"supports_arrays": lambda _: False, # No array types
|
|
73
|
+
"supports_uuid": lambda _: False, # No native UUID, use VARCHAR(36)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if feature in feature_checks:
|
|
77
|
+
return bool(feature_checks[feature](version_info))
|
|
78
|
+
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
|
|
82
|
+
"""Get optimal MySQL type for a category.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
driver: MySQL async driver instance
|
|
86
|
+
type_category: Type category
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
MySQL-specific type name
|
|
90
|
+
"""
|
|
91
|
+
version_info = await self.get_version(driver)
|
|
92
|
+
|
|
93
|
+
if type_category == "json":
|
|
94
|
+
if version_info and version_info >= VersionInfo(5, 7, 8):
|
|
95
|
+
return "JSON"
|
|
96
|
+
return "TEXT"
|
|
97
|
+
|
|
98
|
+
type_map = {
|
|
99
|
+
"uuid": "VARCHAR(36)",
|
|
100
|
+
"boolean": "TINYINT(1)",
|
|
101
|
+
"timestamp": "TIMESTAMP",
|
|
102
|
+
"text": "TEXT",
|
|
103
|
+
"blob": "BLOB",
|
|
104
|
+
}
|
|
105
|
+
return type_map.get(type_category, "VARCHAR(255)")
|
|
106
|
+
|
|
107
|
+
async def get_columns(
|
|
108
|
+
self, driver: AsyncDriverAdapterBase, table: str, schema: "str | None" = None
|
|
109
|
+
) -> "list[dict[str, Any]]":
|
|
110
|
+
"""Get column information for a table using information_schema.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
driver: AsyncMy driver instance
|
|
114
|
+
table: Table name to query columns for
|
|
115
|
+
schema: Schema name (database name in MySQL)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of column metadata dictionaries with keys:
|
|
119
|
+
- column_name: Name of the column
|
|
120
|
+
- data_type: MySQL data type
|
|
121
|
+
- is_nullable: Whether column allows NULL (YES/NO)
|
|
122
|
+
- column_default: Default value if any
|
|
123
|
+
"""
|
|
124
|
+
asyncmy_driver = cast("AsyncmyDriver", driver)
|
|
125
|
+
|
|
126
|
+
if schema:
|
|
127
|
+
sql = f"""
|
|
128
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
129
|
+
FROM information_schema.columns
|
|
130
|
+
WHERE table_name = '{table}' AND table_schema = '{schema}'
|
|
131
|
+
ORDER BY ordinal_position
|
|
132
|
+
"""
|
|
133
|
+
else:
|
|
134
|
+
sql = f"""
|
|
135
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
136
|
+
FROM information_schema.columns
|
|
137
|
+
WHERE table_name = '{table}'
|
|
138
|
+
ORDER BY ordinal_position
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
result = await asyncmy_driver.execute(sql)
|
|
142
|
+
return result.data or []
|
|
143
|
+
|
|
144
|
+
def list_available_features(self) -> "list[str]":
|
|
145
|
+
"""List available MySQL feature flags.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of supported feature names
|
|
149
|
+
"""
|
|
150
|
+
return [
|
|
151
|
+
"supports_json",
|
|
152
|
+
"supports_cte",
|
|
153
|
+
"supports_window_functions",
|
|
154
|
+
"supports_returning",
|
|
155
|
+
"supports_upsert",
|
|
156
|
+
"supports_transactions",
|
|
157
|
+
"supports_prepared_statements",
|
|
158
|
+
"supports_schemas",
|
|
159
|
+
"supports_arrays",
|
|
160
|
+
"supports_uuid",
|
|
161
|
+
]
|
|
@@ -5,31 +5,50 @@ type coercion, error handling, and transaction management.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import TYPE_CHECKING, Any,
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
9
9
|
|
|
10
|
-
import asyncmy
|
|
11
|
-
|
|
12
|
-
from asyncmy.cursors import Cursor, DictCursor
|
|
10
|
+
import asyncmy.errors # pyright: ignore
|
|
11
|
+
from asyncmy.constants import FIELD_TYPE as ASYNC_MY_FIELD_TYPE # pyright: ignore
|
|
12
|
+
from asyncmy.cursors import Cursor, DictCursor # pyright: ignore
|
|
13
13
|
|
|
14
14
|
from sqlspec.core.cache import get_cache_config
|
|
15
15
|
from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
|
|
16
16
|
from sqlspec.core.statement import StatementConfig
|
|
17
17
|
from sqlspec.driver import AsyncDriverAdapterBase
|
|
18
|
-
from sqlspec.exceptions import
|
|
18
|
+
from sqlspec.exceptions import (
|
|
19
|
+
CheckViolationError,
|
|
20
|
+
DatabaseConnectionError,
|
|
21
|
+
DataError,
|
|
22
|
+
ForeignKeyViolationError,
|
|
23
|
+
IntegrityError,
|
|
24
|
+
NotNullViolationError,
|
|
25
|
+
SQLParsingError,
|
|
26
|
+
SQLSpecError,
|
|
27
|
+
TransactionError,
|
|
28
|
+
UniqueViolationError,
|
|
29
|
+
)
|
|
19
30
|
from sqlspec.utils.serializers import to_json
|
|
20
31
|
|
|
21
32
|
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable
|
|
22
34
|
from contextlib import AbstractAsyncContextManager
|
|
23
35
|
|
|
24
36
|
from sqlspec.adapters.asyncmy._types import AsyncmyConnection
|
|
25
37
|
from sqlspec.core.result import SQLResult
|
|
26
38
|
from sqlspec.core.statement import SQL
|
|
27
39
|
from sqlspec.driver import ExecutionResult
|
|
40
|
+
from sqlspec.driver._async import AsyncDataDictionaryBase
|
|
41
|
+
__all__ = ("AsyncmyCursor", "AsyncmyDriver", "AsyncmyExceptionHandler", "asyncmy_statement_config")
|
|
28
42
|
|
|
29
43
|
logger = logging.getLogger(__name__)
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
json_type_value = (
|
|
46
|
+
ASYNC_MY_FIELD_TYPE.JSON if ASYNC_MY_FIELD_TYPE is not None and hasattr(ASYNC_MY_FIELD_TYPE, "JSON") else None
|
|
47
|
+
)
|
|
48
|
+
ASYNCMY_JSON_TYPE_CODES: Final[set[int]] = {json_type_value} if json_type_value is not None else set()
|
|
49
|
+
MYSQL_ER_DUP_ENTRY = 1062
|
|
50
|
+
MYSQL_ER_NO_DEFAULT_FOR_FIELD = 1364
|
|
51
|
+
MYSQL_ER_CHECK_CONSTRAINT_VIOLATED = 3819
|
|
33
52
|
|
|
34
53
|
asyncmy_statement_config = StatementConfig(
|
|
35
54
|
dialect="mysql",
|
|
@@ -60,23 +79,22 @@ class AsyncmyCursor:
|
|
|
60
79
|
|
|
61
80
|
def __init__(self, connection: "AsyncmyConnection") -> None:
|
|
62
81
|
self.connection = connection
|
|
63
|
-
self.cursor:
|
|
82
|
+
self.cursor: Cursor | DictCursor | None = None
|
|
64
83
|
|
|
65
|
-
async def __aenter__(self) ->
|
|
84
|
+
async def __aenter__(self) -> Cursor | DictCursor:
|
|
66
85
|
self.cursor = self.connection.cursor()
|
|
67
86
|
return self.cursor
|
|
68
87
|
|
|
69
|
-
async def __aexit__(self,
|
|
70
|
-
_ = (exc_type, exc_val, exc_tb)
|
|
88
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
71
89
|
if self.cursor is not None:
|
|
72
90
|
await self.cursor.close()
|
|
73
91
|
|
|
74
92
|
|
|
75
93
|
class AsyncmyExceptionHandler:
|
|
76
|
-
"""
|
|
94
|
+
"""Async context manager for handling asyncmy (MySQL) database exceptions.
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
error
|
|
96
|
+
Maps MySQL error codes and SQLSTATE to specific SQLSpec exceptions
|
|
97
|
+
for better error handling in application code.
|
|
80
98
|
"""
|
|
81
99
|
|
|
82
100
|
__slots__ = ()
|
|
@@ -84,42 +102,118 @@ class AsyncmyExceptionHandler:
|
|
|
84
102
|
async def __aenter__(self) -> None:
|
|
85
103
|
return None
|
|
86
104
|
|
|
87
|
-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
105
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> "bool | None":
|
|
88
106
|
if exc_type is None:
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
if issubclass(exc_type, asyncmy.errors.IntegrityError):
|
|
92
|
-
e = exc_val
|
|
93
|
-
msg = f"AsyncMy MySQL integrity constraint violation: {e}"
|
|
94
|
-
raise SQLSpecError(msg) from e
|
|
95
|
-
if issubclass(exc_type, asyncmy.errors.ProgrammingError):
|
|
96
|
-
e = exc_val
|
|
97
|
-
error_msg = str(e).lower()
|
|
98
|
-
if "syntax" in error_msg or "parse" in error_msg:
|
|
99
|
-
msg = f"AsyncMy MySQL SQL syntax error: {e}"
|
|
100
|
-
raise SQLParsingError(msg) from e
|
|
101
|
-
msg = f"AsyncMy MySQL programming error: {e}"
|
|
102
|
-
raise SQLSpecError(msg) from e
|
|
103
|
-
if issubclass(exc_type, asyncmy.errors.OperationalError):
|
|
104
|
-
e = exc_val
|
|
105
|
-
msg = f"AsyncMy MySQL operational error: {e}"
|
|
106
|
-
raise SQLSpecError(msg) from e
|
|
107
|
-
if issubclass(exc_type, asyncmy.errors.DatabaseError):
|
|
108
|
-
e = exc_val
|
|
109
|
-
msg = f"AsyncMy MySQL database error: {e}"
|
|
110
|
-
raise SQLSpecError(msg) from e
|
|
107
|
+
return None
|
|
111
108
|
if issubclass(exc_type, asyncmy.errors.Error):
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
return self._map_mysql_exception(exc_val)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def _map_mysql_exception(self, e: Any) -> "bool | None":
|
|
113
|
+
"""Map MySQL exception to SQLSpec exception.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
e: MySQL error instance
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True to suppress migration-related errors, None otherwise
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
Specific SQLSpec exception based on error code
|
|
123
|
+
"""
|
|
124
|
+
error_code = None
|
|
125
|
+
sqlstate = None
|
|
126
|
+
|
|
127
|
+
if hasattr(e, "args") and len(e.args) >= 1 and isinstance(e.args[0], int):
|
|
128
|
+
error_code = e.args[0]
|
|
129
|
+
|
|
130
|
+
sqlstate = getattr(e, "sqlstate", None)
|
|
131
|
+
|
|
132
|
+
if error_code in {1061, 1091}:
|
|
133
|
+
logger.warning("AsyncMy MySQL expected migration error (ignoring): %s", e)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
if sqlstate == "23505" or error_code == MYSQL_ER_DUP_ENTRY:
|
|
137
|
+
self._raise_unique_violation(e, sqlstate, error_code)
|
|
138
|
+
elif sqlstate == "23503" or error_code in (1216, 1217, 1451, 1452):
|
|
139
|
+
self._raise_foreign_key_violation(e, sqlstate, error_code)
|
|
140
|
+
elif sqlstate == "23502" or error_code in (1048, MYSQL_ER_NO_DEFAULT_FOR_FIELD):
|
|
141
|
+
self._raise_not_null_violation(e, sqlstate, error_code)
|
|
142
|
+
elif sqlstate == "23514" or error_code == MYSQL_ER_CHECK_CONSTRAINT_VIOLATED:
|
|
143
|
+
self._raise_check_violation(e, sqlstate, error_code)
|
|
144
|
+
elif sqlstate and sqlstate.startswith("23"):
|
|
145
|
+
self._raise_integrity_error(e, sqlstate, error_code)
|
|
146
|
+
elif sqlstate and sqlstate.startswith("42"):
|
|
147
|
+
self._raise_parsing_error(e, sqlstate, error_code)
|
|
148
|
+
elif sqlstate and sqlstate.startswith("08"):
|
|
149
|
+
self._raise_connection_error(e, sqlstate, error_code)
|
|
150
|
+
elif sqlstate and sqlstate.startswith("40"):
|
|
151
|
+
self._raise_transaction_error(e, sqlstate, error_code)
|
|
152
|
+
elif sqlstate and sqlstate.startswith("22"):
|
|
153
|
+
self._raise_data_error(e, sqlstate, error_code)
|
|
154
|
+
elif error_code in {2002, 2003, 2005, 2006, 2013}:
|
|
155
|
+
self._raise_connection_error(e, sqlstate, error_code)
|
|
156
|
+
elif error_code in {1205, 1213}:
|
|
157
|
+
self._raise_transaction_error(e, sqlstate, error_code)
|
|
158
|
+
elif error_code in range(1064, 1100):
|
|
159
|
+
self._raise_parsing_error(e, sqlstate, error_code)
|
|
160
|
+
else:
|
|
161
|
+
self._raise_generic_error(e, sqlstate, error_code)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def _raise_unique_violation(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
165
|
+
code_str = f"[{sqlstate or code}]"
|
|
166
|
+
msg = f"MySQL unique constraint violation {code_str}: {e}"
|
|
167
|
+
raise UniqueViolationError(msg) from e
|
|
168
|
+
|
|
169
|
+
def _raise_foreign_key_violation(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
170
|
+
code_str = f"[{sqlstate or code}]"
|
|
171
|
+
msg = f"MySQL foreign key constraint violation {code_str}: {e}"
|
|
172
|
+
raise ForeignKeyViolationError(msg) from e
|
|
173
|
+
|
|
174
|
+
def _raise_not_null_violation(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
175
|
+
code_str = f"[{sqlstate or code}]"
|
|
176
|
+
msg = f"MySQL not-null constraint violation {code_str}: {e}"
|
|
177
|
+
raise NotNullViolationError(msg) from e
|
|
178
|
+
|
|
179
|
+
def _raise_check_violation(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
180
|
+
code_str = f"[{sqlstate or code}]"
|
|
181
|
+
msg = f"MySQL check constraint violation {code_str}: {e}"
|
|
182
|
+
raise CheckViolationError(msg) from e
|
|
183
|
+
|
|
184
|
+
def _raise_integrity_error(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
185
|
+
code_str = f"[{sqlstate or code}]"
|
|
186
|
+
msg = f"MySQL integrity constraint violation {code_str}: {e}"
|
|
187
|
+
raise IntegrityError(msg) from e
|
|
188
|
+
|
|
189
|
+
def _raise_parsing_error(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
190
|
+
code_str = f"[{sqlstate or code}]"
|
|
191
|
+
msg = f"MySQL SQL syntax error {code_str}: {e}"
|
|
192
|
+
raise SQLParsingError(msg) from e
|
|
193
|
+
|
|
194
|
+
def _raise_connection_error(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
195
|
+
code_str = f"[{sqlstate or code}]"
|
|
196
|
+
msg = f"MySQL connection error {code_str}: {e}"
|
|
197
|
+
raise DatabaseConnectionError(msg) from e
|
|
198
|
+
|
|
199
|
+
def _raise_transaction_error(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
200
|
+
code_str = f"[{sqlstate or code}]"
|
|
201
|
+
msg = f"MySQL transaction error {code_str}: {e}"
|
|
202
|
+
raise TransactionError(msg) from e
|
|
203
|
+
|
|
204
|
+
def _raise_data_error(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
205
|
+
code_str = f"[{sqlstate or code}]"
|
|
206
|
+
msg = f"MySQL data error {code_str}: {e}"
|
|
207
|
+
raise DataError(msg) from e
|
|
208
|
+
|
|
209
|
+
def _raise_generic_error(self, e: Any, sqlstate: "str | None", code: "int | None") -> None:
|
|
210
|
+
if sqlstate and code:
|
|
211
|
+
msg = f"MySQL database error [{sqlstate}:{code}]: {e}"
|
|
212
|
+
elif sqlstate or code:
|
|
213
|
+
msg = f"MySQL database error [{sqlstate or code}]: {e}"
|
|
214
|
+
else:
|
|
215
|
+
msg = f"MySQL database error: {e}"
|
|
216
|
+
raise SQLSpecError(msg) from e
|
|
123
217
|
|
|
124
218
|
|
|
125
219
|
class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
@@ -130,25 +224,100 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
130
224
|
and transaction management.
|
|
131
225
|
"""
|
|
132
226
|
|
|
133
|
-
__slots__ = ()
|
|
227
|
+
__slots__ = ("_data_dictionary",)
|
|
134
228
|
dialect = "mysql"
|
|
135
229
|
|
|
136
230
|
def __init__(
|
|
137
231
|
self,
|
|
138
232
|
connection: "AsyncmyConnection",
|
|
139
|
-
statement_config: "
|
|
140
|
-
driver_features: "
|
|
233
|
+
statement_config: "StatementConfig | None" = None,
|
|
234
|
+
driver_features: "dict[str, Any] | None" = None,
|
|
141
235
|
) -> None:
|
|
142
|
-
|
|
236
|
+
final_statement_config = statement_config
|
|
237
|
+
if final_statement_config is None:
|
|
143
238
|
cache_config = get_cache_config()
|
|
144
|
-
|
|
239
|
+
final_statement_config = asyncmy_statement_config.replace(
|
|
145
240
|
enable_caching=cache_config.compiled_cache_enabled,
|
|
146
241
|
enable_parsing=True,
|
|
147
242
|
enable_validation=True,
|
|
148
243
|
dialect="mysql",
|
|
149
244
|
)
|
|
150
245
|
|
|
151
|
-
|
|
246
|
+
final_statement_config = self._apply_json_serializer_feature(final_statement_config, driver_features)
|
|
247
|
+
|
|
248
|
+
super().__init__(
|
|
249
|
+
connection=connection, statement_config=final_statement_config, driver_features=driver_features
|
|
250
|
+
)
|
|
251
|
+
self._data_dictionary: AsyncDataDictionaryBase | None = None
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def _clone_parameter_config(
|
|
255
|
+
parameter_config: ParameterStyleConfig, type_coercion_map: "dict[type[Any], Callable[[Any], Any]]"
|
|
256
|
+
) -> ParameterStyleConfig:
|
|
257
|
+
"""Create a copy of the parameter configuration with updated coercion map.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
parameter_config: Existing parameter configuration to copy.
|
|
261
|
+
type_coercion_map: Updated coercion mapping for parameter serialization.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
ParameterStyleConfig with the updated type coercion map applied.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
supported_execution_styles = (
|
|
268
|
+
set(parameter_config.supported_execution_parameter_styles)
|
|
269
|
+
if parameter_config.supported_execution_parameter_styles is not None
|
|
270
|
+
else None
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return ParameterStyleConfig(
|
|
274
|
+
default_parameter_style=parameter_config.default_parameter_style,
|
|
275
|
+
supported_parameter_styles=set(parameter_config.supported_parameter_styles),
|
|
276
|
+
supported_execution_parameter_styles=supported_execution_styles,
|
|
277
|
+
default_execution_parameter_style=parameter_config.default_execution_parameter_style,
|
|
278
|
+
type_coercion_map=type_coercion_map,
|
|
279
|
+
has_native_list_expansion=parameter_config.has_native_list_expansion,
|
|
280
|
+
needs_static_script_compilation=parameter_config.needs_static_script_compilation,
|
|
281
|
+
allow_mixed_parameter_styles=parameter_config.allow_mixed_parameter_styles,
|
|
282
|
+
preserve_parameter_format=parameter_config.preserve_parameter_format,
|
|
283
|
+
preserve_original_params_for_many=parameter_config.preserve_original_params_for_many,
|
|
284
|
+
output_transformer=parameter_config.output_transformer,
|
|
285
|
+
ast_transformer=parameter_config.ast_transformer,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def _apply_json_serializer_feature(
|
|
290
|
+
statement_config: "StatementConfig", driver_features: "dict[str, Any] | None"
|
|
291
|
+
) -> "StatementConfig":
|
|
292
|
+
"""Apply driver-level JSON serializer customization to the statement config.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
statement_config: Base statement configuration for the driver.
|
|
296
|
+
driver_features: Driver feature mapping provided via configuration.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
StatementConfig with serializer adjustments applied when configured.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
if not driver_features:
|
|
303
|
+
return statement_config
|
|
304
|
+
|
|
305
|
+
serializer = driver_features.get("json_serializer")
|
|
306
|
+
if serializer is None:
|
|
307
|
+
return statement_config
|
|
308
|
+
|
|
309
|
+
parameter_config = statement_config.parameter_config
|
|
310
|
+
type_coercion_map = dict(parameter_config.type_coercion_map)
|
|
311
|
+
|
|
312
|
+
def serialize_tuple(value: Any) -> Any:
|
|
313
|
+
return serializer(list(value))
|
|
314
|
+
|
|
315
|
+
type_coercion_map[dict] = serializer
|
|
316
|
+
type_coercion_map[list] = serializer
|
|
317
|
+
type_coercion_map[tuple] = serialize_tuple
|
|
318
|
+
|
|
319
|
+
updated_parameter_config = AsyncmyDriver._clone_parameter_config(parameter_config, type_coercion_map)
|
|
320
|
+
return statement_config.replace(parameter_config=updated_parameter_config)
|
|
152
321
|
|
|
153
322
|
def with_cursor(self, connection: "AsyncmyConnection") -> "AsyncmyCursor":
|
|
154
323
|
"""Create cursor context manager for the connection.
|
|
@@ -169,7 +338,7 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
169
338
|
"""
|
|
170
339
|
return AsyncmyExceptionHandler()
|
|
171
340
|
|
|
172
|
-
async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "
|
|
341
|
+
async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
|
|
173
342
|
"""Handle AsyncMy-specific operations before standard execution.
|
|
174
343
|
|
|
175
344
|
Args:
|
|
@@ -182,6 +351,75 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
182
351
|
_ = (cursor, statement)
|
|
183
352
|
return None
|
|
184
353
|
|
|
354
|
+
def _detect_json_columns(self, cursor: Any) -> "list[int]":
|
|
355
|
+
"""Identify JSON column indexes from cursor metadata.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
cursor: Database cursor with description metadata available.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
List of index positions where JSON values are present.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
description = getattr(cursor, "description", None)
|
|
365
|
+
if not description or not ASYNCMY_JSON_TYPE_CODES:
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
json_indexes: list[int] = []
|
|
369
|
+
for index, column in enumerate(description):
|
|
370
|
+
type_code = getattr(column, "type_code", None)
|
|
371
|
+
if type_code is None and isinstance(column, (tuple, list)) and len(column) > 1:
|
|
372
|
+
type_code = column[1]
|
|
373
|
+
if type_code in ASYNCMY_JSON_TYPE_CODES:
|
|
374
|
+
json_indexes.append(index)
|
|
375
|
+
return json_indexes
|
|
376
|
+
|
|
377
|
+
def _deserialize_json_columns(
|
|
378
|
+
self, cursor: Any, column_names: "list[str]", rows: "list[dict[str, Any]]"
|
|
379
|
+
) -> "list[dict[str, Any]]":
|
|
380
|
+
"""Apply configured JSON deserializer to result rows.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
cursor: Database cursor used for the current result set.
|
|
384
|
+
column_names: Ordered column names from the cursor description.
|
|
385
|
+
rows: Result rows represented as dictionaries.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Rows with JSON columns decoded when a deserializer is configured.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
if not rows or not column_names:
|
|
392
|
+
return rows
|
|
393
|
+
|
|
394
|
+
deserializer = self.driver_features.get("json_deserializer")
|
|
395
|
+
if deserializer is None:
|
|
396
|
+
return rows
|
|
397
|
+
|
|
398
|
+
json_indexes = self._detect_json_columns(cursor)
|
|
399
|
+
if not json_indexes:
|
|
400
|
+
return rows
|
|
401
|
+
|
|
402
|
+
target_columns = [column_names[index] for index in json_indexes if index < len(column_names)]
|
|
403
|
+
if not target_columns:
|
|
404
|
+
return rows
|
|
405
|
+
|
|
406
|
+
for row in rows:
|
|
407
|
+
for column in target_columns:
|
|
408
|
+
if column not in row:
|
|
409
|
+
continue
|
|
410
|
+
raw_value = row[column]
|
|
411
|
+
if raw_value is None:
|
|
412
|
+
continue
|
|
413
|
+
if isinstance(raw_value, bytearray):
|
|
414
|
+
raw_value = bytes(raw_value)
|
|
415
|
+
if not isinstance(raw_value, (str, bytes)):
|
|
416
|
+
continue
|
|
417
|
+
try:
|
|
418
|
+
row[column] = deserializer(raw_value)
|
|
419
|
+
except Exception:
|
|
420
|
+
logger.debug("Failed to deserialize JSON column %s", column, exc_info=True)
|
|
421
|
+
return rows
|
|
422
|
+
|
|
185
423
|
async def _execute_script(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
186
424
|
"""Execute SQL script with statement splitting and parameter handling.
|
|
187
425
|
|
|
@@ -258,12 +496,16 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
258
496
|
column_names = [desc[0] for desc in cursor.description or []]
|
|
259
497
|
|
|
260
498
|
if fetched_data and not isinstance(fetched_data[0], dict):
|
|
261
|
-
|
|
499
|
+
rows = [dict(zip(column_names, row, strict=False)) for row in fetched_data]
|
|
500
|
+
elif fetched_data:
|
|
501
|
+
rows = [dict(row) for row in fetched_data]
|
|
262
502
|
else:
|
|
263
|
-
|
|
503
|
+
rows = []
|
|
504
|
+
|
|
505
|
+
rows = self._deserialize_json_columns(cursor, column_names, rows)
|
|
264
506
|
|
|
265
507
|
return self.create_execution_result(
|
|
266
|
-
cursor, selected_data=
|
|
508
|
+
cursor, selected_data=rows, column_names=column_names, data_row_count=len(rows), is_select_result=True
|
|
267
509
|
)
|
|
268
510
|
|
|
269
511
|
affected_rows = cursor.rowcount if cursor.rowcount is not None else -1
|
|
@@ -308,3 +550,16 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
308
550
|
except asyncmy.errors.MySQLError as e:
|
|
309
551
|
msg = f"Failed to commit MySQL transaction: {e}"
|
|
310
552
|
raise SQLSpecError(msg) from e
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def data_dictionary(self) -> "AsyncDataDictionaryBase":
|
|
556
|
+
"""Get the data dictionary for this driver.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Data dictionary instance for metadata queries
|
|
560
|
+
"""
|
|
561
|
+
if self._data_dictionary is None:
|
|
562
|
+
from sqlspec.adapters.asyncmy.data_dictionary import MySQLAsyncDataDictionary
|
|
563
|
+
|
|
564
|
+
self._data_dictionary = MySQLAsyncDataDictionary()
|
|
565
|
+
return self._data_dictionary
|