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.
- sqlspec/_serialization.py +223 -21
- sqlspec/_sql.py +20 -62
- sqlspec/_typing.py +11 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +129 -20
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +68 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +8 -4
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/base.py +3 -4
- sqlspec/builder/_base.py +130 -48
- sqlspec/builder/_column.py +66 -24
- sqlspec/builder/_ddl.py +91 -41
- sqlspec/builder/_insert.py +40 -58
- sqlspec/builder/_parsing_utils.py +127 -12
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
- sqlspec/builder/mixins/_delete_operations.py +12 -7
- sqlspec/builder/mixins/_insert_operations.py +50 -36
- sqlspec/builder/mixins/_join_operations.py +15 -30
- sqlspec/builder/mixins/_merge_operations.py +210 -78
- sqlspec/builder/mixins/_order_limit_operations.py +4 -10
- sqlspec/builder/mixins/_pivot_operations.py +1 -0
- sqlspec/builder/mixins/_select_operations.py +44 -22
- sqlspec/builder/mixins/_update_operations.py +30 -37
- sqlspec/builder/mixins/_where_clause.py +52 -70
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/__init__.py +3 -2
- sqlspec/core/cache.py +298 -352
- sqlspec/core/compiler.py +61 -4
- sqlspec/core/filters.py +246 -213
- sqlspec/core/hashing.py +9 -11
- sqlspec/core/parameters.py +27 -10
- sqlspec/core/statement.py +72 -12
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +108 -5
- sqlspec/driver/_common.py +186 -17
- sqlspec/driver/_sync.py +108 -5
- sqlspec/driver/mixins/_result_tools.py +60 -7
- sqlspec/exceptions.py +5 -0
- sqlspec/loader.py +8 -9
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/storage/backends/fsspec.py +1 -0
- sqlspec/typing.py +4 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.24.1.dist-info/RECORD +0 -139
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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
|
|
@@ -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, "
|
|
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[
|
|
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) -> "
|
|
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,
|
|
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) ->
|
|
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]":
|