sqlspec 0.46.2__py3-none-any.whl → 0.47.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.
Files changed (149) hide show
  1. sqlspec/_typing.py +16 -1
  2. sqlspec/adapters/adbc/adk/store.py +28 -6
  3. sqlspec/adapters/adbc/data_dictionary.py +21 -16
  4. sqlspec/adapters/aiomysql/adk/store.py +26 -3
  5. sqlspec/adapters/aiomysql/config.py +0 -4
  6. sqlspec/adapters/aiomysql/core.py +91 -53
  7. sqlspec/adapters/aiomysql/data_dictionary.py +2 -4
  8. sqlspec/adapters/aiomysql/driver.py +6 -1
  9. sqlspec/adapters/aiosqlite/adk/store.py +89 -51
  10. sqlspec/adapters/aiosqlite/config.py +3 -7
  11. sqlspec/adapters/aiosqlite/data_dictionary.py +3 -9
  12. sqlspec/adapters/aiosqlite/driver.py +15 -12
  13. sqlspec/adapters/asyncmy/adk/store.py +26 -3
  14. sqlspec/adapters/asyncmy/config.py +0 -4
  15. sqlspec/adapters/asyncmy/core.py +91 -53
  16. sqlspec/adapters/asyncmy/data_dictionary.py +2 -4
  17. sqlspec/adapters/asyncmy/driver.py +6 -1
  18. sqlspec/adapters/asyncpg/adk/store.py +25 -11
  19. sqlspec/adapters/asyncpg/config.py +0 -4
  20. sqlspec/adapters/asyncpg/core.py +61 -2
  21. sqlspec/adapters/asyncpg/data_dictionary.py +2 -7
  22. sqlspec/adapters/asyncpg/events/_hub.py +181 -0
  23. sqlspec/adapters/asyncpg/events/backend.py +49 -125
  24. sqlspec/adapters/bigquery/data_dictionary.py +7 -15
  25. sqlspec/adapters/bigquery/driver.py +3 -2
  26. sqlspec/adapters/cockroach_asyncpg/__init__.py +2 -0
  27. sqlspec/adapters/cockroach_asyncpg/adk/store.py +44 -19
  28. sqlspec/adapters/cockroach_asyncpg/config.py +15 -4
  29. sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +3 -0
  30. sqlspec/adapters/cockroach_psycopg/__init__.py +2 -1
  31. sqlspec/adapters/cockroach_psycopg/adk/store.py +34 -4
  32. sqlspec/adapters/cockroach_psycopg/config.py +1 -7
  33. sqlspec/adapters/cockroach_psycopg/data_dictionary.py +5 -0
  34. sqlspec/adapters/duckdb/adk/store.py +38 -17
  35. sqlspec/adapters/duckdb/core.py +91 -32
  36. sqlspec/adapters/mysqlconnector/adk/store.py +54 -8
  37. sqlspec/adapters/mysqlconnector/core.py +89 -52
  38. sqlspec/adapters/mysqlconnector/data_dictionary.py +3 -8
  39. sqlspec/adapters/mysqlconnector/driver.py +12 -2
  40. sqlspec/adapters/oracledb/__init__.py +8 -56
  41. sqlspec/adapters/oracledb/_typing.py +12 -0
  42. sqlspec/adapters/oracledb/adk/__init__.py +12 -1
  43. sqlspec/adapters/oracledb/adk/store.py +303 -104
  44. sqlspec/adapters/oracledb/config.py +0 -4
  45. sqlspec/adapters/oracledb/data_dictionary.py +51 -183
  46. sqlspec/adapters/oracledb/events/_hub.py +345 -0
  47. sqlspec/adapters/oracledb/events/backend.py +89 -136
  48. sqlspec/adapters/psqlpy/_typing.py +4 -1
  49. sqlspec/adapters/psqlpy/adk/store.py +18 -2
  50. sqlspec/adapters/psqlpy/config.py +0 -4
  51. sqlspec/adapters/psqlpy/core.py +71 -3
  52. sqlspec/adapters/psqlpy/data_dictionary.py +2 -7
  53. sqlspec/adapters/psqlpy/driver.py +11 -3
  54. sqlspec/adapters/psqlpy/events/_hub.py +204 -0
  55. sqlspec/adapters/psqlpy/events/backend.py +52 -152
  56. sqlspec/adapters/psycopg/adk/store.py +34 -4
  57. sqlspec/adapters/psycopg/core.py +55 -39
  58. sqlspec/adapters/psycopg/data_dictionary.py +3 -14
  59. sqlspec/adapters/psycopg/events/_hub.py +388 -0
  60. sqlspec/adapters/psycopg/events/backend.py +78 -138
  61. sqlspec/adapters/pymysql/adk/store.py +28 -5
  62. sqlspec/adapters/pymysql/core.py +91 -53
  63. sqlspec/adapters/pymysql/data_dictionary.py +2 -4
  64. sqlspec/adapters/pymysql/driver.py +6 -1
  65. sqlspec/adapters/spanner/adk/store.py +92 -56
  66. sqlspec/adapters/spanner/driver.py +11 -1
  67. sqlspec/adapters/sqlite/adk/store.py +84 -52
  68. sqlspec/adapters/sqlite/data_dictionary.py +3 -7
  69. sqlspec/adapters/sqlite/driver.py +16 -13
  70. sqlspec/base.py +228 -156
  71. sqlspec/builder/_base.py +4 -3
  72. sqlspec/builder/_dml.py +21 -0
  73. sqlspec/builder/_factory.py +34 -95
  74. sqlspec/builder/_insert.py +107 -30
  75. sqlspec/builder/_parsing_utils.py +2 -2
  76. sqlspec/config.py +44 -6
  77. sqlspec/core/__init__.py +1 -3
  78. sqlspec/core/cache.py +3 -13
  79. sqlspec/core/compiler.py +55 -88
  80. sqlspec/core/config_runtime.py +21 -3
  81. sqlspec/core/filters.py +92 -188
  82. sqlspec/core/parameters/__init__.py +1 -9
  83. sqlspec/core/parameters/_converter.py +10 -4
  84. sqlspec/core/parameters/_processor.py +87 -71
  85. sqlspec/core/result/__init__.py +0 -2
  86. sqlspec/core/result/_base.py +1 -4
  87. sqlspec/core/splitter.py +44 -5
  88. sqlspec/core/statement.py +3 -3
  89. sqlspec/data_dictionary/_loader.py +23 -8
  90. sqlspec/data_dictionary/dialects/bigquery.py +32 -0
  91. sqlspec/data_dictionary/dialects/cockroachdb.py +11 -0
  92. sqlspec/data_dictionary/dialects/mysql.py +11 -0
  93. sqlspec/data_dictionary/dialects/oracle.py +163 -0
  94. sqlspec/data_dictionary/dialects/postgres.py +23 -0
  95. sqlspec/data_dictionary/dialects/sqlite.py +17 -0
  96. sqlspec/driver/_async.py +29 -116
  97. sqlspec/driver/_common.py +113 -16
  98. sqlspec/driver/_exception_handler.py +3 -3
  99. sqlspec/driver/_storage_helpers.py +9 -0
  100. sqlspec/driver/_sync.py +8 -116
  101. sqlspec/exceptions.py +26 -7
  102. sqlspec/extensions/adk/__init__.py +1 -1
  103. sqlspec/extensions/adk/_config_utils.py +199 -0
  104. sqlspec/extensions/adk/artifact/__init__.py +2 -2
  105. sqlspec/extensions/adk/artifact/service.py +1 -2
  106. sqlspec/extensions/adk/artifact/store.py +10 -12
  107. sqlspec/extensions/adk/memory/__init__.py +1 -1
  108. sqlspec/extensions/adk/memory/service.py +2 -2
  109. sqlspec/extensions/adk/memory/store.py +18 -62
  110. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +14 -76
  111. sqlspec/extensions/adk/service.py +5 -7
  112. sqlspec/extensions/adk/store.py +15 -28
  113. sqlspec/extensions/events/_queue.py +1 -0
  114. sqlspec/extensions/fastapi/providers.py +431 -357
  115. sqlspec/extensions/litestar/plugin.py +149 -96
  116. sqlspec/extensions/litestar/providers.py +464 -281
  117. sqlspec/extensions/prometheus/__init__.py +1 -1
  118. sqlspec/loader.py +17 -5
  119. sqlspec/migrations/base.py +4 -0
  120. sqlspec/observability/_config.py +17 -12
  121. sqlspec/observability/_diagnostics.py +3 -3
  122. sqlspec/observability/_dispatcher.py +53 -7
  123. sqlspec/observability/_observer.py +1 -5
  124. sqlspec/observability/_runtime.py +29 -7
  125. sqlspec/observability/_spans.py +3 -3
  126. sqlspec/protocols.py +6 -2
  127. sqlspec/storage/__init__.py +1 -1
  128. sqlspec/storage/_arrow_payload.py +68 -0
  129. sqlspec/storage/_paths.py +58 -0
  130. sqlspec/storage/_utils.py +3 -83
  131. sqlspec/storage/backends/base.py +10 -10
  132. sqlspec/storage/backends/fsspec.py +9 -6
  133. sqlspec/storage/backends/local.py +4 -3
  134. sqlspec/storage/backends/obstore.py +6 -5
  135. sqlspec/storage/errors.py +2 -4
  136. sqlspec/storage/pipeline.py +104 -70
  137. sqlspec/typing.py +10 -0
  138. sqlspec/utils/arrow_helpers.py +37 -0
  139. sqlspec/utils/logging.py +2 -1
  140. sqlspec/utils/serializers/_json.py +34 -2
  141. sqlspec/utils/serializers/_schema.py +62 -54
  142. sqlspec/utils/sync_tools.py +1 -1
  143. sqlspec/utils/text.py +3 -2
  144. {sqlspec-0.46.2.dist-info → sqlspec-0.47.0.dist-info}/METADATA +3 -2
  145. {sqlspec-0.46.2.dist-info → sqlspec-0.47.0.dist-info}/RECORD +148 -142
  146. sqlspec/extensions/_filter_aliases.py +0 -112
  147. {sqlspec-0.46.2.dist-info → sqlspec-0.47.0.dist-info}/WHEEL +0 -0
  148. {sqlspec-0.46.2.dist-info → sqlspec-0.47.0.dist-info}/entry_points.txt +0 -0
  149. {sqlspec-0.46.2.dist-info → sqlspec-0.47.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/_typing.py CHANGED
@@ -1,5 +1,10 @@
1
1
  # ruff: noqa: RUF100, PLR0913, A002, DOC201, PLR6301, PLR0917, ARG004, ARG002, ARG001
2
- """Wrapper around library classes for compatibility when libraries are installed."""
2
+ """Private implementation for SQLSpec typing and optional dependency shims.
3
+
4
+ Public consumers should import from :mod:`sqlspec.typing`. This module is kept
5
+ private because it centralizes optional dependency fallbacks, compatibility
6
+ aliases, and mypyc-excluded type boundaries used by the package internals.
7
+ """
3
8
 
4
9
  import enum
5
10
  from collections.abc import Iterable, Mapping
@@ -150,6 +155,11 @@ def convert_stub( # noqa: PLR0913
150
155
  return {}
151
156
 
152
157
 
158
+ def msgspec_fields_stub(type_: Any, /) -> "tuple[Any, ...]": # noqa: ARG001
159
+ """Placeholder implementation."""
160
+ return ()
161
+
162
+
153
163
  class UnsetTypeStub(enum.Enum):
154
164
  UNSET = "UNSET"
155
165
 
@@ -162,16 +172,19 @@ try:
162
172
  from msgspec import Struct as _RealStruct
163
173
  from msgspec import UnsetType as _RealUnsetType
164
174
  from msgspec import convert as _real_convert
175
+ from msgspec.structs import fields as _real_msgspec_fields
165
176
 
166
177
  Struct = _RealStruct
167
178
  UnsetType = _RealUnsetType
168
179
  UNSET = _REAL_UNSET
169
180
  convert = _real_convert
181
+ msgspec_fields = _real_msgspec_fields
170
182
  except ImportError:
171
183
  Struct = StructStub # type: ignore[assignment,misc]
172
184
  UnsetType = UnsetTypeStub # type: ignore[assignment,misc]
173
185
  UNSET = UNSET_STUB # type: ignore[assignment] # pyright: ignore[reportConstantRedefinition]
174
186
  convert = convert_stub
187
+ msgspec_fields = msgspec_fields_stub # type: ignore[assignment]
175
188
 
176
189
 
177
190
  try:
@@ -695,5 +708,7 @@ __all__ = (
695
708
  "convert",
696
709
  "convert_stub",
697
710
  "module_available",
711
+ "msgspec_fields",
712
+ "msgspec_fields_stub",
698
713
  "trace",
699
714
  )
@@ -714,12 +714,14 @@ class AdbcADKStore(BaseAsyncADKStore["AdbcConfig"]):
714
714
 
715
715
  def _append_event_and_update_state(
716
716
  self, event_record: "EventRecord", session_id: str, state: "dict[str, Any]"
717
- ) -> None:
717
+ ) -> SessionRecord:
718
718
  """Atomically insert an event and update the session's durable state.
719
719
 
720
- The event insert and state update are executed within a single
721
- connection and committed together. If either statement fails the
722
- transaction is rolled back so the two writes remain consistent.
720
+ The event insert, state update, and refresh-SELECT are executed within
721
+ a single connection and committed together. ADBC drivers wrap a
722
+ variety of backends (Postgres, SQLite, DuckDB, ...) so we use a
723
+ SELECT-after-UPDATE rather than relying on RETURNING which not every
724
+ backend supports.
723
725
 
724
726
  Args:
725
727
  event_record: Event record to store.
@@ -737,6 +739,11 @@ class AdbcADKStore(BaseAsyncADKStore["AdbcConfig"]):
737
739
  SET state = ?, update_time = CURRENT_TIMESTAMP
738
740
  WHERE id = ?
739
741
  """
742
+ select_sql = f"""
743
+ SELECT id, app_name, user_id, state, create_time, update_time
744
+ FROM {self._session_table}
745
+ WHERE id = ?
746
+ """
740
747
  state_json = self._serialize_state(state)
741
748
  event_json = self._serialize_json_field(event_record["event_json"])
742
749
 
@@ -754,6 +761,8 @@ class AdbcADKStore(BaseAsyncADKStore["AdbcConfig"]):
754
761
  ),
755
762
  )
756
763
  cursor.execute(update_sql, (state_json, session_id))
764
+ cursor.execute(select_sql, (session_id,))
765
+ row = cursor.fetchone()
757
766
  conn.commit()
758
767
  except Exception:
759
768
  with contextlib.suppress(Exception):
@@ -762,11 +771,24 @@ class AdbcADKStore(BaseAsyncADKStore["AdbcConfig"]):
762
771
  finally:
763
772
  cursor.close()
764
773
 
774
+ if row is None:
775
+ msg = f"Session {session_id} not found during append_event_and_update_state."
776
+ raise ValueError(msg)
777
+
778
+ return SessionRecord(
779
+ id=row[0],
780
+ app_name=row[1],
781
+ user_id=row[2],
782
+ state=self._deserialize_state(row[3]),
783
+ create_time=row[4],
784
+ update_time=row[5],
785
+ )
786
+
765
787
  async def append_event_and_update_state(
766
788
  self, event_record: EventRecord, session_id: str, state: "dict[str, Any]"
767
- ) -> None:
789
+ ) -> SessionRecord:
768
790
  """Atomically append an event and update the session's durable state."""
769
- await async_(self._append_event_and_update_state)(event_record, session_id, state)
791
+ return await async_(self._append_event_and_update_state)(event_record, session_id, state)
770
792
 
771
793
  def _get_events(
772
794
  self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
@@ -11,6 +11,14 @@ from sqlspec.data_dictionary import (
11
11
  list_registered_dialects,
12
12
  normalize_dialect_name,
13
13
  )
14
+ from sqlspec.data_dictionary.dialects.bigquery import (
15
+ format_bigquery_information_schema_tables,
16
+ format_bigquery_schema_prefix,
17
+ )
18
+ from sqlspec.data_dictionary.dialects.cockroachdb import resolve_cockroachdb_json_type
19
+ from sqlspec.data_dictionary.dialects.mysql import resolve_mysql_json_type
20
+ from sqlspec.data_dictionary.dialects.postgres import resolve_postgres_json_type
21
+ from sqlspec.data_dictionary.dialects.sqlite import resolve_sqlite_json_type
14
22
  from sqlspec.driver import SyncDataDictionaryBase
15
23
  from sqlspec.exceptions import SQLFileNotFoundError
16
24
  from sqlspec.typing import ColumnMetadata, ForeignKeyMetadata, IndexMetadata, TableMetadata, VersionInfo
@@ -138,8 +146,17 @@ class AdbcDataDictionary(SyncDataDictionaryBase):
138
146
  return self.get_default_type_mapping().get(type_category, "TEXT")
139
147
 
140
148
  if type_category == "json":
141
- json_version = config.get_feature_version("supports_json")
142
149
  version_info = self.get_version(driver)
150
+ if dialect == "postgres":
151
+ return resolve_postgres_json_type(version_info)
152
+ if dialect == "sqlite":
153
+ return resolve_sqlite_json_type(version_info)
154
+ if dialect == "mysql":
155
+ return resolve_mysql_json_type(version_info)
156
+ if dialect == "cockroachdb":
157
+ return resolve_cockroachdb_json_type(version_info)
158
+
159
+ json_version = config.get_feature_version("supports_json")
143
160
  if json_version and (version_info is None or version_info < json_version):
144
161
  return "TEXT"
145
162
 
@@ -152,14 +169,7 @@ class AdbcDataDictionary(SyncDataDictionaryBase):
152
169
  self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="tables")
153
170
 
154
171
  if dialect == "bigquery":
155
- if schema_name:
156
- tables_table = f"`{schema_name}.INFORMATION_SCHEMA.TABLES`"
157
- kcu_table = f"`{schema_name}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE`"
158
- rc_table = f"`{schema_name}.INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`"
159
- else:
160
- tables_table = "INFORMATION_SCHEMA.TABLES"
161
- kcu_table = "INFORMATION_SCHEMA.KEY_COLUMN_USAGE"
162
- rc_table = "INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS"
172
+ tables_table, kcu_table, rc_table = format_bigquery_information_schema_tables(schema_name)
163
173
  query_text = self._get_query_text(dialect, "tables_by_schema").format(
164
174
  tables_table=tables_table, kcu_table=kcu_table, rc_table=rc_table
165
175
  )
@@ -186,7 +196,7 @@ class AdbcDataDictionary(SyncDataDictionaryBase):
186
196
  self._log_table_describe(driver, schema_name=schema_name, table_name=table, operation="columns")
187
197
 
188
198
  if dialect == "bigquery":
189
- schema_prefix = f"`{schema_name}`." if schema_name else ""
199
+ schema_prefix = format_bigquery_schema_prefix(schema_name)
190
200
  if table is None:
191
201
  query_text = self._get_query_text(dialect, "columns_by_schema").format(schema_prefix=schema_prefix)
192
202
  return driver.select(query_text, schema_name=schema_name, schema_type=ColumnMetadata)
@@ -293,12 +303,7 @@ class AdbcDataDictionary(SyncDataDictionaryBase):
293
303
  self._log_table_describe(driver, schema_name=schema_name, table_name=table, operation="foreign_keys")
294
304
 
295
305
  if dialect == "bigquery":
296
- if schema_name:
297
- kcu_table = f"`{schema_name}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE`"
298
- rc_table = f"`{schema_name}.INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`"
299
- else:
300
- kcu_table = "INFORMATION_SCHEMA.KEY_COLUMN_USAGE"
301
- rc_table = "INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS"
306
+ _, kcu_table, rc_table = format_bigquery_information_schema_tables(schema_name)
302
307
  if table is None:
303
308
  query_text = self._get_query_text(dialect, "foreign_keys_by_schema").format(
304
309
  kcu_table=kcu_table, rc_table=rc_table
@@ -346,11 +346,12 @@ class AiomysqlADKStore(BaseAsyncADKStore["AiomysqlConfig"]):
346
346
 
347
347
  async def append_event_and_update_state(
348
348
  self, event_record: EventRecord, session_id: str, state: "dict[str, Any]"
349
- ) -> None:
349
+ ) -> SessionRecord:
350
350
  """Atomically append an event and update the session's durable state.
351
351
 
352
- The event insert and state update succeed together or fail together
353
- within a single transaction.
352
+ MySQL doesn't support UPDATE...RETURNING; we follow the UPDATE with a
353
+ SELECT inside the same transaction so callers get the refreshed row
354
+ in a single round-trip pair (no separate connection acquisition).
354
355
 
355
356
  Args:
356
357
  event_record: Event record to store.
@@ -373,6 +374,12 @@ class AiomysqlADKStore(BaseAsyncADKStore["AiomysqlConfig"]):
373
374
  WHERE id = %s
374
375
  """
375
376
 
377
+ select_sql = f"""
378
+ SELECT id, app_name, user_id, state, create_time, update_time
379
+ FROM {self._session_table}
380
+ WHERE id = %s
381
+ """
382
+
376
383
  async with (
377
384
  self._config.provide_connection() as conn,
378
385
  AiomysqlCursor(conn, cursor_class=AiomysqlRawCursor) as cursor,
@@ -388,8 +395,24 @@ class AiomysqlADKStore(BaseAsyncADKStore["AiomysqlConfig"]):
388
395
  ),
389
396
  )
390
397
  await cursor.execute(update_sql, (state_json, session_id))
398
+ await cursor.execute(select_sql, (session_id,))
399
+ row = await cursor.fetchone()
391
400
  await conn.commit()
392
401
 
402
+ if row is None:
403
+ msg = f"Session {session_id} not found during append_event_and_update_state."
404
+ raise ValueError(msg)
405
+
406
+ state_value = row[3]
407
+ return SessionRecord(
408
+ id=row[0],
409
+ app_name=row[1],
410
+ user_id=row[2],
411
+ state=from_json(state_value) if isinstance(state_value, str) else state_value,
412
+ create_time=row[4],
413
+ update_time=row[5],
414
+ )
415
+
393
416
  async def get_events(
394
417
  self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
395
418
  ) -> "list[EventRecord]":
@@ -253,10 +253,6 @@ class AiomysqlConfig(AsyncDatabaseConfig[AiomysqlConnection, "AiomysqlPool", Aio
253
253
  await self.connection_instance.wait_closed()
254
254
  self.connection_instance = None
255
255
 
256
- async def close_pool(self) -> None:
257
- """Close the connection pool."""
258
- await self._close_pool()
259
-
260
256
  async def create_connection(self) -> AiomysqlConnection:
261
257
  """Create a single async connection (not from pool).
262
258
 
@@ -22,7 +22,7 @@ from sqlspec.exceptions import (
22
22
  )
23
23
  from sqlspec.utils.serializers import from_json, to_json
24
24
  from sqlspec.utils.type_converters import build_uuid_coercions
25
- from sqlspec.utils.type_guards import has_cursor_metadata, has_lastrowid, has_rowcount, has_sqlstate
25
+ from sqlspec.utils.type_guards import has_cursor_metadata, has_lastrowid, has_rowcount
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from collections.abc import Mapping, Sequence
@@ -67,6 +67,50 @@ MYSQL_CR_CONN_HOST_ERROR = 2003
67
67
  MYSQL_CR_UNKNOWN_HOST = 2005
68
68
  MYSQL_CR_SERVER_GONE_ERROR = 2006
69
69
  MYSQL_CR_SERVER_LOST = 2013
70
+ MYSQL_SYNTAX_ERROR_MIN = 1064
71
+ MYSQL_SYNTAX_ERROR_MAX_EXCLUSIVE = 1100
72
+
73
+ _MYSQL_MIGRATION_ERROR_CODES = frozenset((1061, 1091))
74
+ _MYSQL_SQLSTATE_EXACT_DISPATCH: dict[str, tuple[type[SQLSpecError], str]] = {
75
+ "23505": (UniqueViolationError, "unique constraint violation"),
76
+ "23503": (ForeignKeyViolationError, "foreign key constraint violation"),
77
+ "23502": (NotNullViolationError, "not-null constraint violation"),
78
+ "23514": (CheckViolationError, "check constraint violation"),
79
+ }
80
+ _MYSQL_SQLSTATE_PREFIX_DISPATCH: dict[str, tuple[type[SQLSpecError], str]] = {
81
+ "23": (IntegrityError, "integrity constraint violation"),
82
+ "28": (PermissionDeniedError, "authorization error"),
83
+ "40": (TransactionError, "transaction error"),
84
+ "42": (SQLParsingError, "SQL syntax error"),
85
+ "08": (DatabaseConnectionError, "connection error"),
86
+ "22": (DataError, "data error"),
87
+ }
88
+ _MYSQL_CONSTRAINT_ERROR_DISPATCH: dict[int, tuple[type[SQLSpecError], str]] = {
89
+ MYSQL_ER_DUP_ENTRY: (UniqueViolationError, "unique constraint violation"),
90
+ 1216: (ForeignKeyViolationError, "foreign key constraint violation"),
91
+ 1217: (ForeignKeyViolationError, "foreign key constraint violation"),
92
+ 1451: (ForeignKeyViolationError, "foreign key constraint violation"),
93
+ 1452: (ForeignKeyViolationError, "foreign key constraint violation"),
94
+ 1048: (NotNullViolationError, "not-null constraint violation"),
95
+ MYSQL_ER_NO_DEFAULT_FOR_FIELD: (NotNullViolationError, "not-null constraint violation"),
96
+ MYSQL_ER_CHECK_CONSTRAINT_VIOLATED: (CheckViolationError, "check constraint violation"),
97
+ }
98
+ _MYSQL_ACCESS_ERROR_DISPATCH: dict[int, tuple[type[SQLSpecError], str]] = {
99
+ MYSQL_ER_DBACCESS_DENIED: (PermissionDeniedError, "access denied"),
100
+ MYSQL_ER_ACCESS_DENIED: (PermissionDeniedError, "access denied"),
101
+ MYSQL_ER_TABLEACCESS_DENIED: (PermissionDeniedError, "access denied"),
102
+ }
103
+ _MYSQL_TRANSACTION_ERROR_DISPATCH: dict[int, tuple[type[SQLSpecError], str]] = {
104
+ MYSQL_ER_LOCK_DEADLOCK: (DeadlockError, "deadlock detected"),
105
+ MYSQL_ER_LOCK_WAIT_TIMEOUT: (QueryTimeoutError, "lock wait timeout"),
106
+ }
107
+ _MYSQL_CONNECTION_ERROR_DISPATCH: dict[int, tuple[type[SQLSpecError], str]] = {
108
+ MYSQL_CR_SERVER_LOST: (ConnectionTimeoutError, "connection lost"),
109
+ MYSQL_CR_CONNECTION_ERROR: (DatabaseConnectionError, "connection error"),
110
+ MYSQL_CR_CONN_HOST_ERROR: (DatabaseConnectionError, "connection error"),
111
+ MYSQL_CR_UNKNOWN_HOST: (DatabaseConnectionError, "connection error"),
112
+ MYSQL_CR_SERVER_GONE_ERROR: (DatabaseConnectionError, "connection error"),
113
+ }
70
114
 
71
115
 
72
116
  def _bool_to_int(value: bool) -> int:
@@ -210,66 +254,60 @@ def create_mapped_exception(error: Any, *, logger: Any | None = None) -> "SQLSpe
210
254
  Returns:
211
255
  True to suppress expected migration errors, or a SQLSpec exception
212
256
  """
213
- error_code = error.args[0] if len(error.args) >= 1 and isinstance(error.args[0], int) else None
214
- sqlstate_attr = error.sqlstate if has_sqlstate(error) else None
215
- sqlstate = sqlstate_attr if sqlstate_attr is not None else None
257
+ error_args = getattr(error, "args", ())
258
+ error_code = error_args[0] if error_args and isinstance(error_args[0], int) else None
259
+ sqlstate_attr = getattr(error, "sqlstate", None)
260
+ sqlstate = sqlstate_attr if isinstance(sqlstate_attr, str) else None
261
+ sqlstate_prefix = sqlstate[:2] if isinstance(sqlstate, str) and sqlstate else None
216
262
 
217
263
  # Migration-specific errors to suppress
218
- if error_code in {1061, 1091}:
264
+ if error_code in _MYSQL_MIGRATION_ERROR_CODES:
219
265
  if logger is not None:
220
266
  logger.warning("aiomysql MySQL expected migration error (ignoring): %s", error)
221
267
  return True
222
268
 
223
- # Integrity constraint violations
224
- if sqlstate == "23505" or error_code == MYSQL_ER_DUP_ENTRY:
225
- return _create_mysql_error(error, sqlstate, error_code, UniqueViolationError, "unique constraint violation")
226
- if sqlstate == "23503" or error_code in {1216, 1217, 1451, 1452}:
227
- return _create_mysql_error(
228
- error, sqlstate, error_code, ForeignKeyViolationError, "foreign key constraint violation"
229
- )
230
- if sqlstate == "23502" or error_code in {1048, MYSQL_ER_NO_DEFAULT_FOR_FIELD}:
231
- return _create_mysql_error(error, sqlstate, error_code, NotNullViolationError, "not-null constraint violation")
232
- if sqlstate == "23514" or error_code == MYSQL_ER_CHECK_CONSTRAINT_VIOLATED:
233
- return _create_mysql_error(error, sqlstate, error_code, CheckViolationError, "check constraint violation")
234
- if sqlstate and sqlstate.startswith("23"):
235
- return _create_mysql_error(error, sqlstate, error_code, IntegrityError, "integrity constraint violation")
236
-
237
- # Permission/access errors (check specific codes first)
238
- if error_code in {MYSQL_ER_DBACCESS_DENIED, MYSQL_ER_ACCESS_DENIED, MYSQL_ER_TABLEACCESS_DENIED}:
239
- return _create_mysql_error(error, sqlstate, error_code, PermissionDeniedError, "access denied")
240
- if sqlstate and sqlstate.startswith("28"):
241
- return _create_mysql_error(error, sqlstate, error_code, PermissionDeniedError, "authorization error")
242
-
243
- # Transaction errors (deadlock vs lock wait timeout)
244
- if error_code == MYSQL_ER_LOCK_DEADLOCK:
245
- return _create_mysql_error(error, sqlstate, error_code, DeadlockError, "deadlock detected")
246
- if error_code == MYSQL_ER_LOCK_WAIT_TIMEOUT:
247
- return _create_mysql_error(error, sqlstate, error_code, QueryTimeoutError, "lock wait timeout")
248
- if sqlstate and sqlstate.startswith("40"):
249
- return _create_mysql_error(error, sqlstate, error_code, TransactionError, "transaction error")
250
-
251
- # SQL syntax errors
252
- if sqlstate and sqlstate.startswith("42"):
253
- return _create_mysql_error(error, sqlstate, error_code, SQLParsingError, "SQL syntax error")
254
- if error_code in range(1064, 1100):
269
+ dispatch = _MYSQL_SQLSTATE_EXACT_DISPATCH.get(sqlstate) if sqlstate is not None else None
270
+ if dispatch is not None:
271
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
272
+
273
+ dispatch = _MYSQL_CONSTRAINT_ERROR_DISPATCH.get(error_code) if error_code is not None else None
274
+ if dispatch is not None:
275
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
276
+
277
+ if sqlstate_prefix == "23":
278
+ dispatch = _MYSQL_SQLSTATE_PREFIX_DISPATCH["23"]
279
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
280
+
281
+ dispatch = _MYSQL_ACCESS_ERROR_DISPATCH.get(error_code) if error_code is not None else None
282
+ if dispatch is not None:
283
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
284
+ if sqlstate_prefix == "28":
285
+ dispatch = _MYSQL_SQLSTATE_PREFIX_DISPATCH["28"]
286
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
287
+
288
+ dispatch = _MYSQL_TRANSACTION_ERROR_DISPATCH.get(error_code) if error_code is not None else None
289
+ if dispatch is not None:
290
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
291
+ if sqlstate_prefix == "40":
292
+ dispatch = _MYSQL_SQLSTATE_PREFIX_DISPATCH["40"]
293
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
294
+
295
+ if sqlstate_prefix == "42":
296
+ dispatch = _MYSQL_SQLSTATE_PREFIX_DISPATCH["42"]
297
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
298
+ if isinstance(error_code, int) and MYSQL_SYNTAX_ERROR_MIN <= error_code < MYSQL_SYNTAX_ERROR_MAX_EXCLUSIVE:
255
299
  return _create_mysql_error(error, sqlstate, error_code, SQLParsingError, "SQL syntax error")
256
300
 
257
- # Connection errors
258
- if sqlstate and sqlstate.startswith("08"):
259
- return _create_mysql_error(error, sqlstate, error_code, DatabaseConnectionError, "connection error")
260
- if error_code == MYSQL_CR_SERVER_LOST:
261
- return _create_mysql_error(error, sqlstate, error_code, ConnectionTimeoutError, "connection lost")
262
- if error_code in {
263
- MYSQL_CR_CONNECTION_ERROR,
264
- MYSQL_CR_CONN_HOST_ERROR,
265
- MYSQL_CR_UNKNOWN_HOST,
266
- MYSQL_CR_SERVER_GONE_ERROR,
267
- }:
268
- return _create_mysql_error(error, sqlstate, error_code, DatabaseConnectionError, "connection error")
269
-
270
- # Data errors
271
- if sqlstate and sqlstate.startswith("22"):
272
- return _create_mysql_error(error, sqlstate, error_code, DataError, "data error")
301
+ if sqlstate_prefix == "08":
302
+ dispatch = _MYSQL_SQLSTATE_PREFIX_DISPATCH["08"]
303
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
304
+ dispatch = _MYSQL_CONNECTION_ERROR_DISPATCH.get(error_code) if error_code is not None else None
305
+ if dispatch is not None:
306
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
307
+
308
+ if sqlstate_prefix == "22":
309
+ dispatch = _MYSQL_SQLSTATE_PREFIX_DISPATCH["22"]
310
+ return _create_mysql_error(error, sqlstate, error_code, dispatch[0], dispatch[1])
273
311
 
274
312
  return _create_mysql_error(error, sqlstate, error_code, SQLSpecError, "database error")
275
313
 
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, ClassVar
4
4
 
5
5
  from mypy_extensions import mypyc_attr
6
6
 
7
+ from sqlspec.data_dictionary.dialects.mysql import resolve_mysql_json_type
7
8
  from sqlspec.driver import AsyncDataDictionaryBase
8
9
  from sqlspec.typing import ColumnMetadata, ForeignKeyMetadata, IndexMetadata, TableMetadata, VersionInfo
9
10
 
@@ -56,10 +57,7 @@ class AiomysqlDataDictionary(AsyncDataDictionaryBase):
56
57
  version_info = await self.get_version(driver)
57
58
 
58
59
  if type_category == "json":
59
- json_version = config.get_feature_version("supports_json")
60
- if version_info and json_version and version_info >= json_version:
61
- return "JSON"
62
- return "TEXT"
60
+ return resolve_mysql_json_type(version_info)
63
61
 
64
62
  return config.get_optimal_type(type_category)
65
63
 
@@ -306,9 +306,14 @@ class AiomysqlDriver(AsyncDriverAdapterBase):
306
306
  columns, records = self._arrow_table_to_rows(arrow_table)
307
307
  if records:
308
308
  insert_sql = build_insert_statement(table, columns)
309
+ prepared_records = (
310
+ self.prepare_driver_parameters(records, self.statement_config, is_many=True)
311
+ if self._arrow_table_needs_parameter_preparation(arrow_table)
312
+ else records
313
+ )
309
314
  exc_handler = self.handle_database_exceptions()
310
315
  async with exc_handler, self.with_cursor(self.connection) as cursor:
311
- await cursor.executemany(insert_sql, records)
316
+ await cursor.executemany(insert_sql, prepared_records)
312
317
  if exc_handler.pending_exception is not None:
313
318
  raise exc_handler.pending_exception from None
314
319