sqlspec 0.24.1__py3-none-any.whl → 0.26.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 (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,159 @@
1
+ """ADBC-specific type conversion with multi-dialect support.
2
+
3
+ Provides specialized type handling for ADBC adapters, including dialect-aware
4
+ type conversion for different database backends (PostgreSQL, SQLite, DuckDB,
5
+ MySQL, BigQuery, Snowflake).
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from sqlspec.core.type_conversion import BaseTypeConverter
11
+ from sqlspec.utils.serializers import to_json
12
+
13
+
14
+ class ADBCTypeConverter(BaseTypeConverter):
15
+ """ADBC-specific type converter with dialect awareness.
16
+
17
+ Extends the base BaseTypeConverter with ADBC multi-backend functionality
18
+ including dialect-specific type handling for different database systems.
19
+ """
20
+
21
+ __slots__ = ("dialect",)
22
+
23
+ def __init__(self, dialect: str) -> None:
24
+ """Initialize with dialect-specific configuration.
25
+
26
+ Args:
27
+ dialect: Target database dialect (postgres, sqlite, duckdb, etc.)
28
+ """
29
+ super().__init__()
30
+ self.dialect = dialect.lower()
31
+
32
+ def convert_if_detected(self, value: Any) -> Any:
33
+ """Convert value with dialect-specific handling.
34
+
35
+ Args:
36
+ value: Value to potentially convert.
37
+
38
+ Returns:
39
+ Converted value if special type detected, original value otherwise.
40
+ """
41
+ if not isinstance(value, str):
42
+ return value
43
+
44
+ if not any(c in value for c in ["{", "[", "-", ":", "T"]):
45
+ return value
46
+
47
+ detected_type = self.detect_type(value)
48
+ if detected_type:
49
+ try:
50
+ if self.dialect in {"postgres", "postgresql"}:
51
+ if detected_type in {"uuid", "interval"}:
52
+ return self.convert_value(value, detected_type)
53
+
54
+ elif self.dialect == "duckdb":
55
+ if detected_type == "uuid":
56
+ return self.convert_value(value, detected_type)
57
+
58
+ elif self.dialect == "sqlite":
59
+ if detected_type == "uuid":
60
+ return str(value)
61
+
62
+ elif self.dialect == "bigquery":
63
+ if detected_type == "uuid":
64
+ return self.convert_value(value, detected_type)
65
+
66
+ elif self.dialect in {"mysql", "snowflake"} and detected_type in {"uuid", "json"}:
67
+ return self.convert_value(value, detected_type)
68
+
69
+ return self.convert_value(value, detected_type)
70
+ except Exception:
71
+ return value
72
+
73
+ return value
74
+
75
+ def convert_dict(self, value: dict[str, Any]) -> Any:
76
+ """Convert dictionary values with dialect-specific handling.
77
+
78
+ Args:
79
+ value: Dictionary to convert.
80
+
81
+ Returns:
82
+ Converted value appropriate for the dialect.
83
+ """
84
+
85
+ # For dialects that cannot handle raw dicts (like ADBC PostgreSQL),
86
+ # convert to JSON strings
87
+ if self.dialect in {"postgres", "postgresql", "bigquery"}:
88
+ return to_json(value)
89
+
90
+ # For other dialects, pass through unchanged
91
+ return value
92
+
93
+ def supports_native_type(self, type_name: str) -> bool:
94
+ """Check if dialect supports native handling of a type.
95
+
96
+ Args:
97
+ type_name: Type name to check (e.g., 'uuid', 'json')
98
+
99
+ Returns:
100
+ True if dialect supports native handling, False otherwise.
101
+ """
102
+ native_support: dict[str, list[str]] = {
103
+ "postgres": ["uuid", "json", "interval", "pg_array"],
104
+ "postgresql": ["uuid", "json", "interval", "pg_array"],
105
+ "duckdb": ["uuid", "json"],
106
+ "bigquery": ["json"],
107
+ "sqlite": [], # Limited native type support
108
+ "mysql": ["json"],
109
+ "snowflake": ["json"],
110
+ }
111
+
112
+ return type_name in native_support.get(self.dialect, [])
113
+
114
+ def get_dialect_specific_converter(self, value: Any, target_type: str) -> Any:
115
+ """Apply dialect-specific conversion logic.
116
+
117
+ Args:
118
+ value: Value to convert.
119
+ target_type: Target type for conversion.
120
+
121
+ Returns:
122
+ Converted value according to dialect requirements.
123
+ """
124
+ if self.dialect in {"postgres", "postgresql"}:
125
+ if target_type in {"uuid", "json", "interval"}:
126
+ return self.convert_value(value, target_type)
127
+
128
+ elif self.dialect == "duckdb":
129
+ if target_type in {"uuid", "json"}:
130
+ return self.convert_value(value, target_type)
131
+
132
+ elif self.dialect == "sqlite":
133
+ if target_type == "uuid":
134
+ return str(value)
135
+ if target_type == "json":
136
+ return self.convert_value(value, target_type)
137
+
138
+ elif self.dialect == "bigquery":
139
+ if target_type == "uuid":
140
+ return str(self.convert_value(value, target_type))
141
+ if target_type == "json":
142
+ return self.convert_value(value, target_type)
143
+
144
+ return self.convert_value(value, target_type) if hasattr(self, "convert_value") else value
145
+
146
+
147
+ def get_adbc_type_converter(dialect: str) -> ADBCTypeConverter:
148
+ """Factory function to create dialect-specific ADBC type converter.
149
+
150
+ Args:
151
+ dialect: Database dialect name.
152
+
153
+ Returns:
154
+ Configured ADBCTypeConverter instance.
155
+ """
156
+ return ADBCTypeConverter(dialect)
157
+
158
+
159
+ __all__ = ("ADBCTypeConverter", "get_adbc_type_converter")
@@ -62,6 +62,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
62
62
  migration_config: "Optional[dict[str, Any]]" = None,
63
63
  statement_config: "Optional[StatementConfig]" = None,
64
64
  driver_features: "Optional[dict[str, Any]]" = None,
65
+ bind_key: "Optional[str]" = None,
65
66
  ) -> None:
66
67
  """Initialize AioSQLite configuration.
67
68
 
@@ -71,6 +72,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
71
72
  migration_config: Optional migration configuration.
72
73
  statement_config: Optional statement configuration.
73
74
  driver_features: Optional driver feature configuration.
75
+ bind_key: Optional unique identifier for this configuration.
74
76
  """
75
77
  config_dict = dict(pool_config) if pool_config else {}
76
78
 
@@ -84,6 +86,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
84
86
  migration_config=migration_config,
85
87
  statement_config=statement_config or aiosqlite_statement_config,
86
88
  driver_features=driver_features or {},
89
+ bind_key=bind_key,
87
90
  )
88
91
 
89
92
  def _get_pool_config_dict(self) -> "dict[str, Any]":
@@ -0,0 +1,117 @@
1
+ """SQLite-specific data dictionary for metadata queries via aiosqlite."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Callable, Optional, 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 sqlspec.adapters.aiosqlite.driver import AiosqliteDriver
11
+
12
+ logger = get_logger("adapters.aiosqlite.data_dictionary")
13
+
14
+ # Compiled regex patterns
15
+ SQLITE_VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)")
16
+
17
+ __all__ = ("AiosqliteAsyncDataDictionary",)
18
+
19
+
20
+ class AiosqliteAsyncDataDictionary(AsyncDataDictionaryBase):
21
+ """SQLite-specific async data dictionary via aiosqlite."""
22
+
23
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "Optional[VersionInfo]":
24
+ """Get SQLite database version information.
25
+
26
+ Args:
27
+ driver: Async database driver instance
28
+
29
+ Returns:
30
+ SQLite version information or None if detection fails
31
+ """
32
+ version_str = await cast("AiosqliteDriver", driver).select_value("SELECT sqlite_version()")
33
+ if not version_str:
34
+ logger.warning("No SQLite version information found")
35
+ return None
36
+
37
+ # Parse version like "3.45.0"
38
+ version_match = SQLITE_VERSION_PATTERN.match(str(version_str))
39
+ if not version_match:
40
+ logger.warning("Could not parse SQLite version: %s", version_str)
41
+ return None
42
+
43
+ major, minor, patch = map(int, version_match.groups())
44
+ version_info = VersionInfo(major, minor, patch)
45
+ logger.debug("Detected SQLite version: %s", version_info)
46
+ return version_info
47
+
48
+ async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
49
+ """Check if SQLite database supports a specific feature.
50
+
51
+ Args:
52
+ driver: AIOSQLite driver instance
53
+ feature: Feature name to check
54
+
55
+ Returns:
56
+ True if feature is supported, False otherwise
57
+ """
58
+ version_info = await self.get_version(driver)
59
+ if not version_info:
60
+ return False
61
+
62
+ feature_checks: dict[str, Callable[..., bool]] = {
63
+ "supports_json": lambda v: v >= VersionInfo(3, 38, 0),
64
+ "supports_returning": lambda v: v >= VersionInfo(3, 35, 0),
65
+ "supports_upsert": lambda v: v >= VersionInfo(3, 24, 0),
66
+ "supports_window_functions": lambda v: v >= VersionInfo(3, 25, 0),
67
+ "supports_cte": lambda v: v >= VersionInfo(3, 8, 3),
68
+ "supports_transactions": lambda _: True,
69
+ "supports_prepared_statements": lambda _: True,
70
+ "supports_schemas": lambda _: False, # SQLite has ATTACH but not schemas
71
+ "supports_arrays": lambda _: False,
72
+ "supports_uuid": lambda _: False,
73
+ }
74
+
75
+ if feature in feature_checks:
76
+ return bool(feature_checks[feature](version_info))
77
+
78
+ return False
79
+
80
+ async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
81
+ """Get optimal SQLite type for a category.
82
+
83
+ Args:
84
+ driver: AIOSQLite driver instance
85
+ type_category: Type category
86
+
87
+ Returns:
88
+ SQLite-specific type name
89
+ """
90
+ version_info = await self.get_version(driver)
91
+
92
+ if type_category == "json":
93
+ if version_info and version_info >= VersionInfo(3, 38, 0):
94
+ return "JSON"
95
+ return "TEXT"
96
+
97
+ type_map = {"uuid": "TEXT", "boolean": "INTEGER", "timestamp": "TIMESTAMP", "text": "TEXT", "blob": "BLOB"}
98
+ return type_map.get(type_category, "TEXT")
99
+
100
+ def list_available_features(self) -> "list[str]":
101
+ """List available SQLite feature flags.
102
+
103
+ Returns:
104
+ List of supported feature names
105
+ """
106
+ return [
107
+ "supports_json",
108
+ "supports_returning",
109
+ "supports_upsert",
110
+ "supports_window_functions",
111
+ "supports_cte",
112
+ "supports_transactions",
113
+ "supports_prepared_statements",
114
+ "supports_schemas",
115
+ "supports_arrays",
116
+ "supports_uuid",
117
+ ]
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
22
22
  from sqlspec.core.result import SQLResult
23
23
  from sqlspec.core.statement import SQL
24
24
  from sqlspec.driver import ExecutionResult
25
+ from sqlspec.driver._async import AsyncDataDictionaryBase
25
26
 
26
27
  __all__ = ("AiosqliteCursor", "AiosqliteDriver", "AiosqliteExceptionHandler", "aiosqlite_statement_config")
27
28
 
@@ -66,8 +67,7 @@ class AiosqliteCursor:
66
67
  self.cursor = await self.connection.cursor()
67
68
  return self.cursor
68
69
 
69
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
70
- _ = (exc_type, exc_val, exc_tb)
70
+ async def __aexit__(self, *_: Any) -> None:
71
71
  if self.cursor is not None:
72
72
  with contextlib.suppress(Exception):
73
73
  await self.cursor.close()
@@ -120,7 +120,7 @@ class AiosqliteExceptionHandler:
120
120
  class AiosqliteDriver(AsyncDriverAdapterBase):
121
121
  """AIOSQLite driver for async SQLite database operations."""
122
122
 
123
- __slots__ = ()
123
+ __slots__ = ("_data_dictionary",)
124
124
  dialect = "sqlite"
125
125
 
126
126
  def __init__(
@@ -134,6 +134,7 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
134
134
  statement_config = aiosqlite_statement_config.replace(enable_caching=cache_config.compiled_cache_enabled)
135
135
 
136
136
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
137
+ self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
137
138
 
138
139
  def with_cursor(self, connection: "AiosqliteConnection") -> "AiosqliteCursor":
139
140
  """Create async context manager for AIOSQLite cursor."""
@@ -241,3 +242,16 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
241
242
  except aiosqlite.Error as e:
242
243
  msg = f"Failed to commit transaction: {e}"
243
244
  raise SQLSpecError(msg) from e
245
+
246
+ @property
247
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
248
+ """Get the data dictionary for this driver.
249
+
250
+ Returns:
251
+ Data dictionary instance for metadata queries
252
+ """
253
+ if self._data_dictionary is None:
254
+ from sqlspec.adapters.aiosqlite.data_dictionary import AiosqliteAsyncDataDictionary
255
+
256
+ self._data_dictionary = AiosqliteAsyncDataDictionary()
257
+ return self._data_dictionary
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from asyncmy import Connection
3
+ from asyncmy import Connection # pyright: ignore
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from typing_extensions import TypeAlias
@@ -6,8 +6,8 @@ from contextlib import asynccontextmanager
6
6
  from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union
7
7
 
8
8
  import asyncmy
9
- from asyncmy.cursors import Cursor, DictCursor
10
- from asyncmy.pool import Pool as AsyncmyPool
9
+ from asyncmy.cursors import Cursor, DictCursor # pyright: ignore
10
+ from asyncmy.pool import Pool as AsyncmyPool # pyright: ignore
11
11
  from typing_extensions import NotRequired
12
12
 
13
13
  from sqlspec.adapters.asyncmy._types import AsyncmyConnection
@@ -15,8 +15,8 @@ from sqlspec.adapters.asyncmy.driver import AsyncmyCursor, AsyncmyDriver, asyncm
15
15
  from sqlspec.config import AsyncDatabaseConfig
16
16
 
17
17
  if TYPE_CHECKING:
18
- from asyncmy.cursors import Cursor, DictCursor
19
- from asyncmy.pool import Pool
18
+ from asyncmy.cursors import Cursor, DictCursor # pyright: ignore
19
+ from asyncmy.pool import Pool # pyright: ignore
20
20
 
21
21
  from sqlspec.core.statement import StatementConfig
22
22
 
@@ -57,7 +57,7 @@ class AsyncmyPoolParams(AsyncmyConnectionParams, total=False):
57
57
  pool_recycle: NotRequired[int]
58
58
 
59
59
 
60
- class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "Pool", AsyncmyDriver]): # pyright: ignore
60
+ class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "AsyncmyPool", AsyncmyDriver]): # pyright: ignore
61
61
  """Configuration for Asyncmy database connections."""
62
62
 
63
63
  driver_type: ClassVar[type[AsyncmyDriver]] = AsyncmyDriver
@@ -67,10 +67,11 @@ class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "Pool", AsyncmyDriver
67
67
  self,
68
68
  *,
69
69
  pool_config: "Optional[Union[AsyncmyPoolParams, dict[str, Any]]]" = None,
70
- pool_instance: "Optional[Pool]" = None,
70
+ pool_instance: "Optional[AsyncmyPool]" = None,
71
71
  migration_config: Optional[dict[str, Any]] = None,
72
72
  statement_config: "Optional[StatementConfig]" = None,
73
73
  driver_features: "Optional[dict[str, Any]]" = None,
74
+ bind_key: "Optional[str]" = None,
74
75
  ) -> None:
75
76
  """Initialize Asyncmy configuration.
76
77
 
@@ -80,6 +81,7 @@ class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "Pool", AsyncmyDriver
80
81
  migration_config: Migration configuration
81
82
  statement_config: Statement configuration override
82
83
  driver_features: Driver feature configuration
84
+ bind_key: Optional unique identifier for this configuration
83
85
  """
84
86
  processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {}
85
87
  if "extra" in processed_pool_config:
@@ -100,11 +102,12 @@ class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "Pool", AsyncmyDriver
100
102
  migration_config=migration_config,
101
103
  statement_config=statement_config,
102
104
  driver_features=driver_features or {},
105
+ bind_key=bind_key,
103
106
  )
104
107
 
105
- async def _create_pool(self) -> "Pool": # pyright: ignore
108
+ async def _create_pool(self) -> "AsyncmyPool": # pyright: ignore
106
109
  """Create the actual async connection pool."""
107
- return await asyncmy.create_pool(**dict(self.pool_config))
110
+ return await asyncmy.create_pool(**dict(self.pool_config)) # pyright: ignore
108
111
 
109
112
  async def _close_pool(self) -> None:
110
113
  """Close the actual async connection pool."""
@@ -0,0 +1,122 @@
1
+ """MySQL-specific data dictionary for metadata queries via asyncmy."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Callable, Optional, 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 sqlspec.adapters.asyncmy.driver import AsyncmyDriver
11
+
12
+ logger = get_logger("adapters.asyncmy.data_dictionary")
13
+
14
+ # Compiled regex patterns
15
+ VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)")
16
+
17
+ __all__ = ("MySQLAsyncDataDictionary",)
18
+
19
+
20
+ class MySQLAsyncDataDictionary(AsyncDataDictionaryBase):
21
+ """MySQL-specific async data dictionary."""
22
+
23
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "Optional[VersionInfo]":
24
+ """Get MySQL database version information.
25
+
26
+ Args:
27
+ driver: Async database driver instance
28
+
29
+ Returns:
30
+ MySQL version information or None if detection fails
31
+ """
32
+ result = await cast("AsyncmyDriver", driver).select_value_or_none("SELECT VERSION() as version")
33
+ if not result:
34
+ logger.warning("No MySQL version information found")
35
+
36
+ # Parse version like "8.0.33-0ubuntu0.22.04.2" or "5.7.42-log"
37
+ version_match = VERSION_PATTERN.search(str(result))
38
+ if not version_match:
39
+ logger.warning("Could not parse MySQL version: %s", result)
40
+ return None
41
+
42
+ major, minor, patch = map(int, version_match.groups())
43
+ version_info = VersionInfo(major, minor, patch)
44
+ logger.debug("Detected MySQL version: %s", version_info)
45
+ return version_info
46
+
47
+ async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
48
+ """Check if MySQL database supports a specific feature.
49
+
50
+ Args:
51
+ driver: MySQL async driver instance
52
+ feature: Feature name to check
53
+
54
+ Returns:
55
+ True if feature is supported, False otherwise
56
+ """
57
+ version_info = await self.get_version(driver)
58
+ if not version_info:
59
+ return False
60
+
61
+ feature_checks: dict[str, Callable[..., bool]] = {
62
+ "supports_json": lambda v: v >= VersionInfo(5, 7, 8),
63
+ "supports_cte": lambda v: v >= VersionInfo(8, 0, 1),
64
+ "supports_window_functions": lambda v: v >= VersionInfo(8, 0, 2),
65
+ "supports_returning": lambda _: False, # MySQL doesn't have RETURNING
66
+ "supports_upsert": lambda _: True, # ON DUPLICATE KEY UPDATE available
67
+ "supports_transactions": lambda _: True,
68
+ "supports_prepared_statements": lambda _: True,
69
+ "supports_schemas": lambda _: True, # MySQL calls them databases
70
+ "supports_arrays": lambda _: False, # No array types
71
+ "supports_uuid": lambda _: False, # No native UUID, use VARCHAR(36)
72
+ }
73
+
74
+ if feature in feature_checks:
75
+ return bool(feature_checks[feature](version_info))
76
+
77
+ return False
78
+
79
+ async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
80
+ """Get optimal MySQL type for a category.
81
+
82
+ Args:
83
+ driver: MySQL async driver instance
84
+ type_category: Type category
85
+
86
+ Returns:
87
+ MySQL-specific type name
88
+ """
89
+ version_info = await self.get_version(driver)
90
+
91
+ if type_category == "json":
92
+ if version_info and version_info >= VersionInfo(5, 7, 8):
93
+ return "JSON"
94
+ return "TEXT"
95
+
96
+ type_map = {
97
+ "uuid": "VARCHAR(36)",
98
+ "boolean": "TINYINT(1)",
99
+ "timestamp": "TIMESTAMP",
100
+ "text": "TEXT",
101
+ "blob": "BLOB",
102
+ }
103
+ return type_map.get(type_category, "VARCHAR(255)")
104
+
105
+ def list_available_features(self) -> "list[str]":
106
+ """List available MySQL feature flags.
107
+
108
+ Returns:
109
+ List of supported feature names
110
+ """
111
+ return [
112
+ "supports_json",
113
+ "supports_cte",
114
+ "supports_window_functions",
115
+ "supports_returning",
116
+ "supports_upsert",
117
+ "supports_transactions",
118
+ "supports_prepared_statements",
119
+ "supports_schemas",
120
+ "supports_arrays",
121
+ "supports_uuid",
122
+ ]
@@ -8,8 +8,8 @@ import logging
8
8
  from typing import TYPE_CHECKING, Any, Optional, Union
9
9
 
10
10
  import asyncmy
11
- import asyncmy.errors
12
- from asyncmy.cursors import Cursor, DictCursor
11
+ import asyncmy.errors # 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
@@ -25,6 +25,7 @@ if TYPE_CHECKING:
25
25
  from sqlspec.core.result import SQLResult
26
26
  from sqlspec.core.statement import SQL
27
27
  from sqlspec.driver import ExecutionResult
28
+ from sqlspec.driver._async import AsyncDataDictionaryBase
28
29
 
29
30
  logger = logging.getLogger(__name__)
30
31
 
@@ -66,8 +67,7 @@ class AsyncmyCursor:
66
67
  self.cursor = self.connection.cursor()
67
68
  return self.cursor
68
69
 
69
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
70
- _ = (exc_type, exc_val, exc_tb)
70
+ async def __aexit__(self, *_: Any) -> None:
71
71
  if self.cursor is not None:
72
72
  await self.cursor.close()
73
73
 
@@ -84,9 +84,9 @@ class AsyncmyExceptionHandler:
84
84
  async def __aenter__(self) -> None:
85
85
  return None
86
86
 
87
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
87
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> "Optional[bool]":
88
88
  if exc_type is None:
89
- return
89
+ return None
90
90
 
91
91
  if issubclass(exc_type, asyncmy.errors.IntegrityError):
92
92
  e = exc_val
@@ -102,6 +102,15 @@ class AsyncmyExceptionHandler:
102
102
  raise SQLSpecError(msg) from e
103
103
  if issubclass(exc_type, asyncmy.errors.OperationalError):
104
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
105
114
  msg = f"AsyncMy MySQL operational error: {e}"
106
115
  raise SQLSpecError(msg) from e
107
116
  if issubclass(exc_type, asyncmy.errors.DatabaseError):
@@ -120,6 +129,7 @@ class AsyncmyExceptionHandler:
120
129
  raise SQLParsingError(msg) from e
121
130
  msg = f"Unexpected async database operation error: {e}"
122
131
  raise SQLSpecError(msg) from e
132
+ return None
123
133
 
124
134
 
125
135
  class AsyncmyDriver(AsyncDriverAdapterBase):
@@ -130,7 +140,7 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
130
140
  and transaction management.
131
141
  """
132
142
 
133
- __slots__ = ()
143
+ __slots__ = ("_data_dictionary",)
134
144
  dialect = "mysql"
135
145
 
136
146
  def __init__(
@@ -149,6 +159,7 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
149
159
  )
150
160
 
151
161
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
162
+ self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
152
163
 
153
164
  def with_cursor(self, connection: "AsyncmyConnection") -> "AsyncmyCursor":
154
165
  """Create cursor context manager for the connection.
@@ -308,3 +319,16 @@ class AsyncmyDriver(AsyncDriverAdapterBase):
308
319
  except asyncmy.errors.MySQLError as e:
309
320
  msg = f"Failed to commit MySQL transaction: {e}"
310
321
  raise SQLSpecError(msg) from e
322
+
323
+ @property
324
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
325
+ """Get the data dictionary for this driver.
326
+
327
+ Returns:
328
+ Data dictionary instance for metadata queries
329
+ """
330
+ if self._data_dictionary is None:
331
+ from sqlspec.adapters.asyncmy.data_dictionary import MySQLAsyncDataDictionary
332
+
333
+ self._data_dictionary = MySQLAsyncDataDictionary()
334
+ return self._data_dictionary
@@ -84,6 +84,7 @@ class AsyncpgConfig(AsyncDatabaseConfig[AsyncpgConnection, "Pool[Record]", Async
84
84
  migration_config: "Optional[dict[str, Any]]" = None,
85
85
  statement_config: "Optional[StatementConfig]" = None,
86
86
  driver_features: "Optional[Union[AsyncpgDriverFeatures, dict[str, Any]]]" = None,
87
+ bind_key: "Optional[str]" = None,
87
88
  ) -> None:
88
89
  """Initialize AsyncPG configuration.
89
90
 
@@ -93,6 +94,7 @@ class AsyncpgConfig(AsyncDatabaseConfig[AsyncpgConnection, "Pool[Record]", Async
93
94
  migration_config: Migration configuration
94
95
  statement_config: Statement configuration override
95
96
  driver_features: Driver features configuration (TypedDict or dict)
97
+ bind_key: Optional unique identifier for this configuration
96
98
  """
97
99
  features_dict: dict[str, Any] = dict(driver_features) if driver_features else {}
98
100
 
@@ -106,6 +108,7 @@ class AsyncpgConfig(AsyncDatabaseConfig[AsyncpgConnection, "Pool[Record]", Async
106
108
  migration_config=migration_config,
107
109
  statement_config=statement_config or asyncpg_statement_config,
108
110
  driver_features=features_dict,
111
+ bind_key=bind_key,
109
112
  )
110
113
 
111
114
  def _get_pool_config_dict(self) -> "dict[str, Any]":