sqlspec 0.26.0__py3-none-any.whl → 0.28.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 +55 -25
- sqlspec/_typing.py +155 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +880 -0
- sqlspec/adapters/adbc/config.py +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +74 -2
- sqlspec/adapters/adbc/driver.py +226 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +536 -0
- sqlspec/adapters/aiosqlite/config.py +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- 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 +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +503 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- 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 +460 -0
- sqlspec/adapters/asyncpg/config.py +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- 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 +585 -0
- sqlspec/adapters/bigquery/config.py +36 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +489 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +563 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +225 -44
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- 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 +1628 -0
- sqlspec/adapters/oracledb/config.py +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +475 -86
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +765 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- 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 +483 -0
- sqlspec/adapters/psqlpy/config.py +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
- sqlspec/adapters/psqlpy/driver.py +108 -41
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- 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 +962 -0
- sqlspec/adapters/psycopg/config.py +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +91 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- 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 +582 -0
- sqlspec/adapters/sqlite/config.py +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +331 -62
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +55 -47
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +234 -47
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +183 -160
- sqlspec/driver/_common.py +197 -109
- sqlspec/driver/_sync.py +189 -161
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -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 +69 -61
- sqlspec/extensions/fastapi/__init__.py +21 -0
- sqlspec/extensions/fastapi/extension.py +331 -0
- sqlspec/extensions/fastapi/providers.py +543 -0
- sqlspec/extensions/flask/__init__.py +36 -0
- sqlspec/extensions/flask/_state.py +71 -0
- sqlspec/extensions/flask/_utils.py +40 -0
- sqlspec/extensions/flask/extension.py +389 -0
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +56 -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 +349 -224
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/extensions/starlette/__init__.py +10 -0
- sqlspec/extensions/starlette/_state.py +25 -0
- sqlspec/extensions/starlette/_utils.py +52 -0
- sqlspec/extensions/starlette/extension.py +254 -0
- sqlspec/extensions/starlette/middleware.py +154 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +106 -36
- sqlspec/storage/_utils.py +85 -0
- sqlspec/storage/backends/fsspec.py +133 -107
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +276 -168
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +30 -84
- sqlspec/utils/__init__.py +25 -4
- sqlspec/utils/arrow_helpers.py +81 -0
- sqlspec/utils/config_resolver.py +6 -6
- 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 +205 -5
- sqlspec/utils/portal.py +311 -0
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +113 -4
- sqlspec/utils/sync_tools.py +36 -22
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +136 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
- sqlspec-0.28.0.dist-info/RECORD +221 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""MySQL-specific data dictionary for metadata queries via asyncmy."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from typing import TYPE_CHECKING,
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
5
|
|
|
6
6
|
from sqlspec.driver import AsyncDataDictionaryBase, AsyncDriverAdapterBase, VersionInfo
|
|
7
7
|
from sqlspec.utils.logging import get_logger
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
10
12
|
from sqlspec.adapters.asyncmy.driver import AsyncmyDriver
|
|
11
13
|
|
|
12
14
|
logger = get_logger("adapters.asyncmy.data_dictionary")
|
|
@@ -20,7 +22,7 @@ __all__ = ("MySQLAsyncDataDictionary",)
|
|
|
20
22
|
class MySQLAsyncDataDictionary(AsyncDataDictionaryBase):
|
|
21
23
|
"""MySQL-specific async data dictionary."""
|
|
22
24
|
|
|
23
|
-
async def get_version(self, driver: AsyncDriverAdapterBase) -> "
|
|
25
|
+
async def get_version(self, driver: AsyncDriverAdapterBase) -> "VersionInfo | None":
|
|
24
26
|
"""Get MySQL database version information.
|
|
25
27
|
|
|
26
28
|
Args:
|
|
@@ -102,6 +104,43 @@ class MySQLAsyncDataDictionary(AsyncDataDictionaryBase):
|
|
|
102
104
|
}
|
|
103
105
|
return type_map.get(type_category, "VARCHAR(255)")
|
|
104
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
|
+
|
|
105
144
|
def list_available_features(self) -> "list[str]":
|
|
106
145
|
"""List available MySQL feature flags.
|
|
107
146
|
|
|
@@ -5,20 +5,32 @@ 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
10
|
import asyncmy.errors # pyright: ignore
|
|
11
|
+
from asyncmy.constants import FIELD_TYPE as ASYNC_MY_FIELD_TYPE # pyright: ignore
|
|
12
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
|
|
@@ -26,11 +38,17 @@ if TYPE_CHECKING:
|
|
|
26
38
|
from sqlspec.core.statement import SQL
|
|
27
39
|
from sqlspec.driver import ExecutionResult
|
|
28
40
|
from sqlspec.driver._async import AsyncDataDictionaryBase
|
|
41
|
+
__all__ = ("AsyncmyCursor", "AsyncmyDriver", "AsyncmyExceptionHandler", "asyncmy_statement_config")
|
|
29
42
|
|
|
30
43
|
logger = logging.getLogger(__name__)
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
52
|
|
|
35
53
|
asyncmy_statement_config = StatementConfig(
|
|
36
54
|
dialect="mysql",
|
|
@@ -61,9 +79,9 @@ class AsyncmyCursor:
|
|
|
61
79
|
|
|
62
80
|
def __init__(self, connection: "AsyncmyConnection") -> None:
|
|
63
81
|
self.connection = connection
|
|
64
|
-
self.cursor:
|
|
82
|
+
self.cursor: Cursor | DictCursor | None = None
|
|
65
83
|
|
|
66
|
-
async def __aenter__(self) ->
|
|
84
|
+
async def __aenter__(self) -> Cursor | DictCursor:
|
|
67
85
|
self.cursor = self.connection.cursor()
|
|
68
86
|
return self.cursor
|
|
69
87
|
|
|
@@ -73,10 +91,10 @@ class AsyncmyCursor:
|
|
|
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,53 +102,119 @@ 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) -> "
|
|
105
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> "bool | None":
|
|
88
106
|
if exc_type is None:
|
|
89
107
|
return None
|
|
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
|
-
# Handle specific MySQL errors that are expected in migrations
|
|
106
|
-
if hasattr(e, "args") and len(e.args) >= 1 and isinstance(e.args[0], int):
|
|
107
|
-
error_code = e.args[0]
|
|
108
|
-
# Error 1061: Duplicate key name (index already exists)
|
|
109
|
-
# Error 1091: Can't DROP index that doesn't exist
|
|
110
|
-
if error_code in {1061, 1091}:
|
|
111
|
-
# These are acceptable during migrations - log and continue
|
|
112
|
-
logger.warning("AsyncMy MySQL expected migration error (ignoring): %s", e)
|
|
113
|
-
return True # Suppress the exception by returning True
|
|
114
|
-
msg = f"AsyncMy MySQL operational error: {e}"
|
|
115
|
-
raise SQLSpecError(msg) from e
|
|
116
|
-
if issubclass(exc_type, asyncmy.errors.DatabaseError):
|
|
117
|
-
e = exc_val
|
|
118
|
-
msg = f"AsyncMy MySQL database error: {e}"
|
|
119
|
-
raise SQLSpecError(msg) from e
|
|
120
108
|
if issubclass(exc_type, asyncmy.errors.Error):
|
|
121
|
-
|
|
122
|
-
msg = f"AsyncMy MySQL error: {e}"
|
|
123
|
-
raise SQLSpecError(msg) from e
|
|
124
|
-
if issubclass(exc_type, Exception):
|
|
125
|
-
e = exc_val
|
|
126
|
-
error_msg = str(e).lower()
|
|
127
|
-
if "parse" in error_msg or "syntax" in error_msg:
|
|
128
|
-
msg = f"SQL parsing failed: {e}"
|
|
129
|
-
raise SQLParsingError(msg) from e
|
|
130
|
-
msg = f"Unexpected async database operation error: {e}"
|
|
131
|
-
raise SQLSpecError(msg) from e
|
|
109
|
+
return self._map_mysql_exception(exc_val)
|
|
132
110
|
return None
|
|
133
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
|
|
217
|
+
|
|
134
218
|
|
|
135
219
|
class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
136
220
|
"""MySQL/MariaDB database driver using AsyncMy client library.
|
|
@@ -146,20 +230,94 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
146
230
|
def __init__(
|
|
147
231
|
self,
|
|
148
232
|
connection: "AsyncmyConnection",
|
|
149
|
-
statement_config: "
|
|
150
|
-
driver_features: "
|
|
233
|
+
statement_config: "StatementConfig | None" = None,
|
|
234
|
+
driver_features: "dict[str, Any] | None" = None,
|
|
151
235
|
) -> None:
|
|
152
|
-
|
|
236
|
+
final_statement_config = statement_config
|
|
237
|
+
if final_statement_config is None:
|
|
153
238
|
cache_config = get_cache_config()
|
|
154
|
-
|
|
239
|
+
final_statement_config = asyncmy_statement_config.replace(
|
|
155
240
|
enable_caching=cache_config.compiled_cache_enabled,
|
|
156
241
|
enable_parsing=True,
|
|
157
242
|
enable_validation=True,
|
|
158
243
|
dialect="mysql",
|
|
159
244
|
)
|
|
160
245
|
|
|
161
|
-
|
|
162
|
-
|
|
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)
|
|
163
321
|
|
|
164
322
|
def with_cursor(self, connection: "AsyncmyConnection") -> "AsyncmyCursor":
|
|
165
323
|
"""Create cursor context manager for the connection.
|
|
@@ -180,7 +338,7 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
180
338
|
"""
|
|
181
339
|
return AsyncmyExceptionHandler()
|
|
182
340
|
|
|
183
|
-
async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "
|
|
341
|
+
async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
|
|
184
342
|
"""Handle AsyncMy-specific operations before standard execution.
|
|
185
343
|
|
|
186
344
|
Args:
|
|
@@ -193,6 +351,75 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
193
351
|
_ = (cursor, statement)
|
|
194
352
|
return None
|
|
195
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
|
+
|
|
196
423
|
async def _execute_script(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
197
424
|
"""Execute SQL script with statement splitting and parameter handling.
|
|
198
425
|
|
|
@@ -269,12 +496,16 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
|
|
|
269
496
|
column_names = [desc[0] for desc in cursor.description or []]
|
|
270
497
|
|
|
271
498
|
if fetched_data and not isinstance(fetched_data[0], dict):
|
|
272
|
-
|
|
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]
|
|
273
502
|
else:
|
|
274
|
-
|
|
503
|
+
rows = []
|
|
504
|
+
|
|
505
|
+
rows = self._deserialize_json_columns(cursor, column_names, rows)
|
|
275
506
|
|
|
276
507
|
return self.create_execution_result(
|
|
277
|
-
cursor, selected_data=
|
|
508
|
+
cursor, selected_data=rows, column_names=column_names, data_row_count=len(rows), is_select_result=True
|
|
278
509
|
)
|
|
279
510
|
|
|
280
511
|
affected_rows = cursor.rowcount if cursor.rowcount is not None else -1
|