sqlspec 0.26.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.

Files changed (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.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, Callable, Optional, cast
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) -> "Optional[VersionInfo]":
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, Optional, Union
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 SQLParsingError, SQLSpecError
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
- __all__ = ("AsyncmyCursor", "AsyncmyDriver", "AsyncmyExceptionHandler", "asyncmy_statement_config")
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: Optional[Union[Cursor, DictCursor]] = None
82
+ self.cursor: Cursor | DictCursor | None = None
65
83
 
66
- async def __aenter__(self) -> Union[Cursor, DictCursor]:
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
- """Context manager for AsyncMy database exception handling.
94
+ """Async context manager for handling asyncmy (MySQL) database exceptions.
77
95
 
78
- Converts AsyncMy-specific exceptions to SQLSpec exceptions with appropriate
79
- error categorization and context preservation.
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) -> "Optional[bool]":
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
- e = exc_val
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: "Optional[StatementConfig]" = None,
150
- driver_features: "Optional[dict[str, Any]]" = None,
233
+ statement_config: "StatementConfig | None" = None,
234
+ driver_features: "dict[str, Any] | None" = None,
151
235
  ) -> None:
152
- if statement_config is None:
236
+ final_statement_config = statement_config
237
+ if final_statement_config is None:
153
238
  cache_config = get_cache_config()
154
- statement_config = asyncmy_statement_config.replace(
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
- super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
162
- self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
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") -> "Optional[SQLResult]":
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
- data = [dict(zip(column_names, row)) for row in fetched_data]
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
- data = fetched_data
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=data, column_names=column_names, data_row_count=len(data), is_select_result=True
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
@@ -0,0 +1,5 @@
1
+ """Litestar integration for AsyncMy adapter."""
2
+
3
+ from sqlspec.adapters.asyncmy.litestar.store import AsyncmyStore
4
+
5
+ __all__ = ("AsyncmyStore",)