Skip to content

Commit efc1aa5

Browse files
authored
feat: async migrations and callable config support (#87)
Implements async migration support and callable config functions as outlined in the async migrations enhancement plan. This enables Python migrations to use `async def up/down` functions and allows the CLI to accept callable configuration functions. Async Migration Support - Enhanced `MigrationRunner` to seamlessly handle both sync and async migrations - Added `MigrationContext` with async execution awareness and metadata tracking - Removed redundant async methods, unified the API around main methods that auto-detect driver types Callable Config Support - Added `config_resolver.py` to resolve both direct config instances and callable functions - Enhanced CLI to support sync/async callable config functions CLI Usage: ```bash # Existing patterns (still supported) sqlspec --config myapp.config.database_configs upgrade head # New callable config support sqlspec --config myapp.config.get_database_configs upgrade head sqlspec --config myapp.config.async_get_database_configs upgrade head ```
1 parent 90c54af commit efc1aa5

File tree

21 files changed

+1632
-555
lines changed

21 files changed

+1632
-555
lines changed

sqlspec/adapters/asyncmy/_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import TYPE_CHECKING
22

3-
from asyncmy import Connection
3+
from asyncmy import Connection # pyright: ignore
44

55
if TYPE_CHECKING:
66
from typing_extensions import TypeAlias

sqlspec/adapters/asyncmy/config.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union
77

88
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
1111
from typing_extensions import NotRequired
1212

1313
from sqlspec.adapters.asyncmy._types import AsyncmyConnection
1414
from sqlspec.adapters.asyncmy.driver import AsyncmyCursor, AsyncmyDriver, asyncmy_statement_config
1515
from sqlspec.config import AsyncDatabaseConfig
1616

1717
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
2020

2121
from sqlspec.core.statement import StatementConfig
2222

@@ -57,7 +57,7 @@ class AsyncmyPoolParams(AsyncmyConnectionParams, total=False):
5757
pool_recycle: NotRequired[int]
5858

5959

60-
class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "Pool", AsyncmyDriver]): # pyright: ignore
60+
class AsyncmyConfig(AsyncDatabaseConfig[AsyncmyConnection, "AsyncmyPool", AsyncmyDriver]): # pyright: ignore
6161
"""Configuration for Asyncmy database connections."""
6262

6363
driver_type: ClassVar[type[AsyncmyDriver]] = AsyncmyDriver
@@ -67,7 +67,7 @@ def __init__(
6767
self,
6868
*,
6969
pool_config: "Optional[Union[AsyncmyPoolParams, dict[str, Any]]]" = None,
70-
pool_instance: "Optional[Pool]" = None,
70+
pool_instance: "Optional[AsyncmyPool]" = None,
7171
migration_config: Optional[dict[str, Any]] = None,
7272
statement_config: "Optional[StatementConfig]" = None,
7373
driver_features: "Optional[dict[str, Any]]" = None,
@@ -102,9 +102,9 @@ def __init__(
102102
driver_features=driver_features or {},
103103
)
104104

105-
async def _create_pool(self) -> "Pool": # pyright: ignore
105+
async def _create_pool(self) -> "AsyncmyPool": # pyright: ignore
106106
"""Create the actual async connection pool."""
107-
return await asyncmy.create_pool(**dict(self.pool_config))
107+
return await asyncmy.create_pool(**dict(self.pool_config)) # pyright: ignore
108108

109109
async def _close_pool(self) -> None:
110110
"""Close the actual async connection pool."""

sqlspec/adapters/sqlite/pool.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""SQLite database configuration with thread-local connections."""
22

3+
import contextlib
34
import sqlite3
45
import threading
56
from contextlib import contextmanager
@@ -49,6 +50,8 @@ def __init__(
4950
enable_optimizations: Whether to apply performance PRAGMAs
5051
**kwargs: Ignored pool parameters for compatibility
5152
"""
53+
if "check_same_thread" not in connection_parameters:
54+
connection_parameters = {**connection_parameters, "check_same_thread": False}
5255
self._connection_parameters = connection_parameters
5356
self._thread_local = threading.local()
5457
self._enable_optimizations = enable_optimizations
@@ -62,8 +65,7 @@ def _create_connection(self) -> SqliteConnection:
6265
is_memory = database == ":memory:" or database.startswith("file::memory:")
6366

6467
if not is_memory:
65-
connection.execute("PRAGMA journal_mode = WAL")
66-
68+
connection.execute("PRAGMA journal_mode = DELETE")
6769
connection.execute("PRAGMA busy_timeout = 5000")
6870
connection.execute("PRAGMA optimize")
6971

@@ -97,7 +99,13 @@ def get_connection(self) -> "Generator[SqliteConnection, None, None]":
9799
Yields:
98100
SqliteConnection: A thread-local connection.
99101
"""
100-
yield self._get_thread_connection()
102+
connection = self._get_thread_connection()
103+
try:
104+
yield connection
105+
finally:
106+
with contextlib.suppress(Exception):
107+
if connection.in_transaction:
108+
connection.commit()
101109

102110
def close(self) -> None:
103111
"""Close the thread-local connection if it exists."""
@@ -124,7 +132,8 @@ def size(self) -> int:
124132
_ = self._thread_local.connection
125133
except AttributeError:
126134
return 0
127-
return 1
135+
else:
136+
return 1
128137

129138
def checked_out(self) -> int:
130139
"""Get number of checked out connections (always 0)."""

0 commit comments

Comments
 (0)