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.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -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 +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  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 +153 -0
  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 +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  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 +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  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 +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  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 +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  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 +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  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 +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  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 +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  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 +102 -0
  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 +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  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 +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -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/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {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, Optional, Union
8
+ from typing import TYPE_CHECKING, Any, Final
9
9
 
10
- import asyncmy
11
- import asyncmy.errors
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 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
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
- __all__ = ("AsyncmyCursor", "AsyncmyDriver", "AsyncmyExceptionHandler", "asyncmy_statement_config")
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: Optional[Union[Cursor, DictCursor]] = None
82
+ self.cursor: Cursor | DictCursor | None = None
64
83
 
65
- async def __aenter__(self) -> Union[Cursor, DictCursor]:
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, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
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
- """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,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
- e = exc_val
113
- msg = f"AsyncMy MySQL error: {e}"
114
- raise SQLSpecError(msg) from e
115
- if issubclass(exc_type, Exception):
116
- e = exc_val
117
- error_msg = str(e).lower()
118
- if "parse" in error_msg or "syntax" in error_msg:
119
- msg = f"SQL parsing failed: {e}"
120
- raise SQLParsingError(msg) from e
121
- msg = f"Unexpected async database operation error: {e}"
122
- raise SQLSpecError(msg) from e
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: "Optional[StatementConfig]" = None,
140
- driver_features: "Optional[dict[str, Any]]" = None,
233
+ statement_config: "StatementConfig | None" = None,
234
+ driver_features: "dict[str, Any] | None" = None,
141
235
  ) -> None:
142
- if statement_config is None:
236
+ final_statement_config = statement_config
237
+ if final_statement_config is None:
143
238
  cache_config = get_cache_config()
144
- statement_config = asyncmy_statement_config.replace(
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
- super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
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") -> "Optional[SQLResult]":
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
- 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]
262
502
  else:
263
- data = fetched_data
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=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
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
@@ -0,0 +1,5 @@
1
+ """Litestar integration for AsyncMy adapter."""
2
+
3
+ from sqlspec.adapters.asyncmy.litestar.store import AsyncmyStore
4
+
5
+ __all__ = ("AsyncmyStore",)