sqlspec 0.16.2__py3-none-any.whl → 0.17.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/__init__.py +11 -1
- sqlspec/_sql.py +16 -412
- sqlspec/adapters/aiosqlite/__init__.py +11 -1
- sqlspec/adapters/aiosqlite/config.py +137 -165
- sqlspec/adapters/aiosqlite/driver.py +21 -10
- sqlspec/adapters/aiosqlite/pool.py +492 -0
- sqlspec/adapters/duckdb/__init__.py +2 -0
- sqlspec/adapters/duckdb/config.py +11 -235
- sqlspec/adapters/duckdb/pool.py +243 -0
- sqlspec/adapters/sqlite/__init__.py +2 -0
- sqlspec/adapters/sqlite/config.py +4 -115
- sqlspec/adapters/sqlite/pool.py +140 -0
- sqlspec/base.py +147 -26
- sqlspec/builder/__init__.py +6 -0
- sqlspec/builder/_parsing_utils.py +27 -0
- sqlspec/builder/mixins/_join_operations.py +115 -1
- sqlspec/builder/mixins/_select_operations.py +307 -3
- sqlspec/builder/mixins/_where_clause.py +60 -11
- sqlspec/core/compiler.py +7 -5
- sqlspec/driver/_common.py +9 -1
- sqlspec/loader.py +27 -54
- sqlspec/storage/registry.py +2 -2
- sqlspec/typing.py +53 -99
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/METADATA +1 -1
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/RECORD +29 -26
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.16.2.dist-info → sqlspec-0.17.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,116 +1,33 @@
|
|
|
1
1
|
"""Aiosqlite database configuration with optimized connection management."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import logging
|
|
5
4
|
from contextlib import asynccontextmanager
|
|
6
|
-
from typing import TYPE_CHECKING, Any, ClassVar,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union
|
|
7
6
|
|
|
8
|
-
import aiosqlite
|
|
9
7
|
from typing_extensions import NotRequired
|
|
10
8
|
|
|
9
|
+
from sqlspec.adapters.aiosqlite._types import AiosqliteConnection
|
|
11
10
|
from sqlspec.adapters.aiosqlite.driver import AiosqliteCursor, AiosqliteDriver, aiosqlite_statement_config
|
|
11
|
+
from sqlspec.adapters.aiosqlite.pool import (
|
|
12
|
+
AiosqliteConnectionPool,
|
|
13
|
+
AiosqliteConnectTimeoutError,
|
|
14
|
+
AiosqlitePoolClosedError,
|
|
15
|
+
AiosqlitePoolConnection,
|
|
16
|
+
)
|
|
12
17
|
from sqlspec.config import AsyncDatabaseConfig
|
|
13
18
|
|
|
14
19
|
if TYPE_CHECKING:
|
|
15
20
|
from collections.abc import AsyncGenerator
|
|
16
21
|
|
|
17
|
-
from sqlspec.adapters.aiosqlite._types import AiosqliteConnection
|
|
18
22
|
from sqlspec.core.statement import StatementConfig
|
|
19
23
|
|
|
20
|
-
__all__ = ("AiosqliteConfig", "AiosqliteConnectionParams", "
|
|
24
|
+
__all__ = ("AiosqliteConfig", "AiosqliteConnectionParams", "AiosqlitePoolParams")
|
|
21
25
|
|
|
22
26
|
logger = logging.getLogger(__name__)
|
|
23
27
|
|
|
24
|
-
# Core PRAGMAs for SQLite performance optimization
|
|
25
|
-
WAL_PRAGMA_SQL: Final[str] = "PRAGMA journal_mode = WAL"
|
|
26
|
-
FOREIGN_KEYS_SQL: Final[str] = "PRAGMA foreign_keys = ON"
|
|
27
|
-
SYNC_NORMAL_SQL: Final[str] = "PRAGMA synchronous = NORMAL"
|
|
28
|
-
BUSY_TIMEOUT_SQL: Final[str] = "PRAGMA busy_timeout = 5000" # 5 seconds
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class AiosqliteConnectionPool:
|
|
32
|
-
"""Connection pool for Aiosqlite using a single shared connection approach.
|
|
33
|
-
|
|
34
|
-
Uses a single shared connection per database file since aiosqlite internally
|
|
35
|
-
handles queuing and serialization of operations.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
__slots__ = ("_closed", "_connection", "_connection_parameters", "_lock")
|
|
39
|
-
|
|
40
|
-
def __init__(self, connection_parameters: "dict[str, Any]") -> None:
|
|
41
|
-
"""Initialize connection manager.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
connection_parameters: SQLite connection parameters
|
|
45
|
-
"""
|
|
46
|
-
self._connection: Optional[AiosqliteConnection] = None
|
|
47
|
-
self._connection_parameters = connection_parameters
|
|
48
|
-
self._lock = asyncio.Lock()
|
|
49
|
-
self._closed = False
|
|
50
|
-
|
|
51
|
-
async def _ensure_connection(self) -> "AiosqliteConnection":
|
|
52
|
-
"""Ensure we have a valid connection, creating one if needed."""
|
|
53
|
-
async with self._lock:
|
|
54
|
-
if self._connection is None or self._closed:
|
|
55
|
-
self._connection = await aiosqlite.connect(**self._connection_parameters)
|
|
56
|
-
|
|
57
|
-
await self._connection.execute(WAL_PRAGMA_SQL)
|
|
58
|
-
await self._connection.execute(FOREIGN_KEYS_SQL)
|
|
59
|
-
await self._connection.execute(SYNC_NORMAL_SQL)
|
|
60
|
-
await self._connection.execute(BUSY_TIMEOUT_SQL)
|
|
61
|
-
await self._connection.commit()
|
|
62
|
-
|
|
63
|
-
self._closed = False
|
|
64
|
-
logger.debug("Created new aiosqlite connection")
|
|
65
|
-
|
|
66
|
-
return self._connection
|
|
67
|
-
|
|
68
|
-
@asynccontextmanager
|
|
69
|
-
async def get_connection(self) -> "AsyncGenerator[AiosqliteConnection, None]":
|
|
70
|
-
"""Get the shared connection.
|
|
71
|
-
|
|
72
|
-
Yields:
|
|
73
|
-
The shared Aiosqlite connection instance.
|
|
74
|
-
"""
|
|
75
|
-
connection = await self._ensure_connection()
|
|
76
|
-
yield connection
|
|
77
|
-
|
|
78
|
-
async def close(self) -> None:
|
|
79
|
-
"""Close the shared connection."""
|
|
80
|
-
async with self._lock:
|
|
81
|
-
if self._connection is not None and not self._closed:
|
|
82
|
-
await self._connection.close()
|
|
83
|
-
self._connection = None
|
|
84
|
-
self._closed = True
|
|
85
|
-
logger.debug("Closed aiosqlite connection")
|
|
86
|
-
|
|
87
|
-
def size(self) -> int:
|
|
88
|
-
"""Get connection count."""
|
|
89
|
-
return 0 if self._closed or self._connection is None else 1
|
|
90
|
-
|
|
91
|
-
def checked_out(self) -> int:
|
|
92
|
-
"""Get number of checked out connections."""
|
|
93
|
-
return 0
|
|
94
|
-
|
|
95
|
-
async def acquire(self) -> "AiosqliteConnection":
|
|
96
|
-
"""Get the shared connection directly.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
The shared connection instance.
|
|
100
|
-
"""
|
|
101
|
-
return await self._ensure_connection()
|
|
102
|
-
|
|
103
|
-
async def release(self, connection: "AiosqliteConnection") -> None:
|
|
104
|
-
"""No-op release for compatibility.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
connection: Connection to release (ignored)
|
|
108
|
-
"""
|
|
109
|
-
_ = connection
|
|
110
|
-
|
|
111
28
|
|
|
112
29
|
class AiosqliteConnectionParams(TypedDict, total=False):
|
|
113
|
-
"""aiosqlite connection parameters."""
|
|
30
|
+
"""TypedDict for aiosqlite connection parameters."""
|
|
114
31
|
|
|
115
32
|
database: NotRequired[str]
|
|
116
33
|
timeout: NotRequired[float]
|
|
@@ -121,17 +38,27 @@ class AiosqliteConnectionParams(TypedDict, total=False):
|
|
|
121
38
|
uri: NotRequired[bool]
|
|
122
39
|
|
|
123
40
|
|
|
124
|
-
class
|
|
41
|
+
class AiosqlitePoolParams(AiosqliteConnectionParams, total=False):
|
|
42
|
+
"""TypedDict for aiosqlite pool parameters, inheriting connection parameters."""
|
|
43
|
+
|
|
44
|
+
pool_size: NotRequired[int]
|
|
45
|
+
connect_timeout: NotRequired[float]
|
|
46
|
+
idle_timeout: NotRequired[float]
|
|
47
|
+
operation_timeout: NotRequired[float]
|
|
48
|
+
extra: NotRequired[dict[str, Any]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnectionPool, AiosqliteDriver]):
|
|
125
52
|
"""Database configuration for AioSQLite engine."""
|
|
126
53
|
|
|
127
|
-
driver_type: ClassVar[type[AiosqliteDriver]] = AiosqliteDriver
|
|
128
|
-
|
|
54
|
+
driver_type: "ClassVar[type[AiosqliteDriver]]" = AiosqliteDriver
|
|
55
|
+
connection_type: "ClassVar[type[AiosqliteConnection]]" = AiosqliteConnection
|
|
129
56
|
|
|
130
57
|
def __init__(
|
|
131
58
|
self,
|
|
132
59
|
*,
|
|
60
|
+
pool_config: "Optional[Union[AiosqlitePoolParams, dict[str, Any]]]" = None,
|
|
133
61
|
pool_instance: "Optional[AiosqliteConnectionPool]" = None,
|
|
134
|
-
pool_config: "Optional[dict[str, Any]]" = None,
|
|
135
62
|
migration_config: "Optional[dict[str, Any]]" = None,
|
|
136
63
|
statement_config: "Optional[StatementConfig]" = None,
|
|
137
64
|
**kwargs: Any,
|
|
@@ -139,115 +66,160 @@ class AiosqliteConfig(AsyncDatabaseConfig):
|
|
|
139
66
|
"""Initialize AioSQLite configuration.
|
|
140
67
|
|
|
141
68
|
Args:
|
|
69
|
+
pool_config: Pool configuration parameters (TypedDict or dict)
|
|
142
70
|
pool_instance: Optional pre-configured connection pool instance.
|
|
143
|
-
pool_config: Optional pool configuration dict (AiosqliteConnectionParams).
|
|
144
71
|
migration_config: Optional migration configuration.
|
|
145
72
|
statement_config: Optional statement configuration.
|
|
146
|
-
**kwargs: Additional connection parameters.
|
|
73
|
+
**kwargs: Additional connection parameters that override pool_config.
|
|
147
74
|
"""
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
75
|
+
config_dict = dict(pool_config) if pool_config else {}
|
|
76
|
+
config_dict.update(kwargs) # Allow kwargs to override pool_config values
|
|
77
|
+
|
|
78
|
+
# Handle memory database URI conversion - test expectation is different than sqlite pattern
|
|
79
|
+
if "database" not in config_dict or config_dict["database"] == ":memory:":
|
|
80
|
+
config_dict["database"] = "file::memory:?cache=shared"
|
|
81
|
+
config_dict["uri"] = True
|
|
152
82
|
|
|
153
83
|
super().__init__(
|
|
154
|
-
pool_config=
|
|
84
|
+
pool_config=config_dict,
|
|
155
85
|
pool_instance=pool_instance,
|
|
156
|
-
migration_config=migration_config
|
|
86
|
+
migration_config=migration_config,
|
|
157
87
|
statement_config=statement_config or aiosqlite_statement_config,
|
|
88
|
+
driver_features={},
|
|
158
89
|
)
|
|
159
90
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if pool_instance is None:
|
|
163
|
-
self.pool_instance: AiosqliteConnectionPool = AiosqliteConnectionPool(self._connection_parameters)
|
|
164
|
-
|
|
165
|
-
def _parse_connection_parameters(self, params: "dict[str, Any]") -> "dict[str, Any]":
|
|
166
|
-
"""Parse connection parameters for AioSQLite.
|
|
167
|
-
|
|
168
|
-
Args:
|
|
169
|
-
params: Connection parameters dict.
|
|
91
|
+
def _get_pool_config_dict(self) -> "dict[str, Any]":
|
|
92
|
+
"""Get pool configuration as plain dict for external library.
|
|
170
93
|
|
|
171
94
|
Returns:
|
|
172
|
-
|
|
95
|
+
Dictionary with pool parameters, filtering out None values.
|
|
173
96
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
result["database"] = ":memory:"
|
|
179
|
-
|
|
180
|
-
# Convert regular :memory: to shared memory for multi-connection access
|
|
181
|
-
if result.get("database") == ":memory:":
|
|
182
|
-
result["database"] = "file::memory:?cache=shared"
|
|
183
|
-
result["uri"] = True
|
|
97
|
+
config: dict[str, Any] = dict(self.pool_config)
|
|
98
|
+
extras = config.pop("extra", {})
|
|
99
|
+
config.update(extras)
|
|
100
|
+
return {k: v for k, v in config.items() if v is not None}
|
|
184
101
|
|
|
185
|
-
|
|
186
|
-
|
|
102
|
+
def _get_connection_config_dict(self) -> "dict[str, Any]":
|
|
103
|
+
"""Get connection configuration as plain dict for pool creation.
|
|
187
104
|
|
|
188
|
-
|
|
105
|
+
Returns:
|
|
106
|
+
Dictionary with connection parameters for creating connections.
|
|
107
|
+
"""
|
|
108
|
+
# Filter out all pool-specific parameters that aiosqlite.connect() doesn't accept
|
|
109
|
+
excluded_keys = {
|
|
110
|
+
"pool_size",
|
|
111
|
+
"connect_timeout",
|
|
112
|
+
"idle_timeout",
|
|
113
|
+
"operation_timeout",
|
|
114
|
+
"extra",
|
|
115
|
+
"pool_min_size",
|
|
116
|
+
"pool_max_size",
|
|
117
|
+
"pool_timeout",
|
|
118
|
+
"pool_recycle_seconds",
|
|
119
|
+
}
|
|
120
|
+
return {k: v for k, v in self.pool_config.items() if k not in excluded_keys}
|
|
189
121
|
|
|
190
122
|
@asynccontextmanager
|
|
191
|
-
async def provide_connection(self) -> "AsyncGenerator[AiosqliteConnection, None]":
|
|
192
|
-
"""Provide
|
|
123
|
+
async def provide_connection(self, *args: Any, **kwargs: Any) -> "AsyncGenerator[AiosqliteConnection, None]":
|
|
124
|
+
"""Provide an async connection context manager.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
*args: Additional arguments.
|
|
128
|
+
**kwargs: Additional keyword arguments.
|
|
193
129
|
|
|
194
130
|
Yields:
|
|
195
|
-
|
|
131
|
+
An aiosqlite connection instance.
|
|
196
132
|
"""
|
|
133
|
+
if self.pool_instance is None:
|
|
134
|
+
self.pool_instance = await self._create_pool()
|
|
197
135
|
async with self.pool_instance.get_connection() as connection:
|
|
198
136
|
yield connection
|
|
199
137
|
|
|
200
138
|
@asynccontextmanager
|
|
201
139
|
async def provide_session(
|
|
202
|
-
self, *
|
|
140
|
+
self, *_args: Any, statement_config: "Optional[StatementConfig]" = None, **_kwargs: Any
|
|
203
141
|
) -> "AsyncGenerator[AiosqliteDriver, None]":
|
|
204
|
-
"""Provide an async
|
|
142
|
+
"""Provide an async driver session context manager.
|
|
205
143
|
|
|
206
144
|
Args:
|
|
207
|
-
*
|
|
145
|
+
*_args: Additional arguments.
|
|
208
146
|
statement_config: Optional statement configuration override.
|
|
209
|
-
**
|
|
147
|
+
**_kwargs: Additional keyword arguments.
|
|
210
148
|
|
|
211
149
|
Yields:
|
|
212
|
-
AiosqliteDriver
|
|
150
|
+
An AiosqliteDriver instance.
|
|
213
151
|
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
async with self.pool_instance.get_connection() as connection:
|
|
217
|
-
session = self.driver_type(connection, statement_config=effective_statement_config)
|
|
218
|
-
try:
|
|
219
|
-
yield session
|
|
220
|
-
finally:
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
|
-
async def close(self) -> None:
|
|
224
|
-
"""Close the connection manager."""
|
|
225
|
-
if self.pool_instance:
|
|
226
|
-
await self.pool_instance.close()
|
|
152
|
+
async with self.provide_connection(*_args, **_kwargs) as connection:
|
|
153
|
+
yield self.driver_type(connection=connection, statement_config=statement_config or self.statement_config)
|
|
227
154
|
|
|
228
|
-
def
|
|
229
|
-
"""
|
|
155
|
+
async def _create_pool(self) -> AiosqliteConnectionPool:
|
|
156
|
+
"""Create the connection pool instance.
|
|
230
157
|
|
|
231
158
|
Returns:
|
|
232
|
-
|
|
159
|
+
AiosqliteConnectionPool: The connection pool instance.
|
|
233
160
|
"""
|
|
234
|
-
|
|
161
|
+
config = self._get_pool_config_dict()
|
|
162
|
+
pool_size = config.pop("pool_size", 5)
|
|
163
|
+
connect_timeout = config.pop("connect_timeout", 30.0)
|
|
164
|
+
idle_timeout = config.pop("idle_timeout", 24 * 60 * 60)
|
|
165
|
+
operation_timeout = config.pop("operation_timeout", 10.0)
|
|
166
|
+
|
|
167
|
+
return AiosqliteConnectionPool(
|
|
168
|
+
connection_parameters=self._get_connection_config_dict(),
|
|
169
|
+
pool_size=pool_size,
|
|
170
|
+
connect_timeout=connect_timeout,
|
|
171
|
+
idle_timeout=idle_timeout,
|
|
172
|
+
operation_timeout=operation_timeout,
|
|
173
|
+
)
|
|
235
174
|
|
|
236
|
-
async def
|
|
237
|
-
"""
|
|
175
|
+
async def close_pool(self) -> None:
|
|
176
|
+
"""Close the connection pool."""
|
|
177
|
+
if self.pool_instance and not self.pool_instance.is_closed:
|
|
178
|
+
await self.pool_instance.close()
|
|
179
|
+
|
|
180
|
+
async def create_connection(self) -> "AiosqliteConnection":
|
|
181
|
+
"""Create a single async connection from the pool.
|
|
238
182
|
|
|
239
183
|
Returns:
|
|
240
|
-
|
|
184
|
+
An aiosqlite connection instance.
|
|
241
185
|
"""
|
|
242
186
|
if self.pool_instance is None:
|
|
243
|
-
self.pool_instance =
|
|
187
|
+
self.pool_instance = await self._create_pool()
|
|
188
|
+
pool_connection = await self.pool_instance.acquire()
|
|
189
|
+
return pool_connection.connection
|
|
190
|
+
|
|
191
|
+
async def provide_pool(self) -> AiosqliteConnectionPool:
|
|
192
|
+
"""Provide async pool instance.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The async connection pool.
|
|
196
|
+
"""
|
|
197
|
+
if not self.pool_instance:
|
|
198
|
+
self.pool_instance = await self.create_pool()
|
|
244
199
|
return self.pool_instance
|
|
245
200
|
|
|
246
|
-
|
|
247
|
-
"""
|
|
248
|
-
if self.pool_instance:
|
|
249
|
-
await self.pool_instance.close()
|
|
201
|
+
def get_signature_namespace(self) -> "dict[str, type[Any]]":
|
|
202
|
+
"""Get the signature namespace for aiosqlite types.
|
|
250
203
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
204
|
+
This provides all aiosqlite-specific types that Litestar needs to recognize
|
|
205
|
+
to avoid serialization attempts.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dictionary mapping type names to types.
|
|
209
|
+
"""
|
|
210
|
+
namespace = super().get_signature_namespace()
|
|
211
|
+
namespace.update(
|
|
212
|
+
{
|
|
213
|
+
"AiosqliteConnection": AiosqliteConnection,
|
|
214
|
+
"AiosqliteConnectionPool": AiosqliteConnectionPool,
|
|
215
|
+
"AiosqliteConnectTimeoutError": AiosqliteConnectTimeoutError,
|
|
216
|
+
"AiosqliteCursor": AiosqliteCursor,
|
|
217
|
+
"AiosqlitePoolClosedError": AiosqlitePoolClosedError,
|
|
218
|
+
"AiosqlitePoolConnection": AiosqlitePoolConnection,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
return namespace
|
|
222
|
+
|
|
223
|
+
async def _close_pool(self) -> None:
|
|
224
|
+
"""Close the connection pool."""
|
|
225
|
+
await self.close_pool()
|
|
@@ -7,6 +7,7 @@ Provides async SQLite database connectivity with:
|
|
|
7
7
|
- SQLite-specific optimizations
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import asyncio
|
|
10
11
|
import contextlib
|
|
11
12
|
import datetime
|
|
12
13
|
from decimal import Decimal
|
|
@@ -143,13 +144,7 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
|
|
|
143
144
|
) -> None:
|
|
144
145
|
if statement_config is None:
|
|
145
146
|
cache_config = get_cache_config()
|
|
146
|
-
|
|
147
|
-
enable_caching=cache_config.compiled_cache_enabled,
|
|
148
|
-
enable_parsing=True,
|
|
149
|
-
enable_validation=True,
|
|
150
|
-
dialect="sqlite",
|
|
151
|
-
)
|
|
152
|
-
statement_config = enhanced_config
|
|
147
|
+
statement_config = aiosqlite_statement_config.replace(enable_caching=cache_config.compiled_cache_enabled)
|
|
153
148
|
|
|
154
149
|
super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
|
|
155
150
|
|
|
@@ -223,12 +218,28 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
|
|
|
223
218
|
return self.create_execution_result(cursor, rowcount_override=affected_rows)
|
|
224
219
|
|
|
225
220
|
async def begin(self) -> None:
|
|
226
|
-
"""Begin a database transaction."""
|
|
221
|
+
"""Begin a database transaction with appropriate locking strategy."""
|
|
227
222
|
try:
|
|
228
223
|
if not self.connection.in_transaction:
|
|
229
|
-
|
|
224
|
+
# For shared cache databases, use IMMEDIATE to reduce lock contention
|
|
225
|
+
# For other databases, BEGIN IMMEDIATE is also safer for concurrent access
|
|
226
|
+
await self.connection.execute("BEGIN IMMEDIATE")
|
|
230
227
|
except aiosqlite.Error as e:
|
|
231
|
-
|
|
228
|
+
# If IMMEDIATE fails due to lock, try with exponential backoff
|
|
229
|
+
import random
|
|
230
|
+
|
|
231
|
+
max_retries = 3
|
|
232
|
+
for attempt in range(max_retries):
|
|
233
|
+
delay = 0.01 * (2**attempt) + random.uniform(0, 0.01) # noqa: S311
|
|
234
|
+
await asyncio.sleep(delay)
|
|
235
|
+
try:
|
|
236
|
+
await self.connection.execute("BEGIN IMMEDIATE")
|
|
237
|
+
except aiosqlite.Error:
|
|
238
|
+
if attempt == max_retries - 1: # Last attempt
|
|
239
|
+
break
|
|
240
|
+
else:
|
|
241
|
+
return
|
|
242
|
+
msg = f"Failed to begin transaction after retries: {e}"
|
|
232
243
|
raise SQLSpecError(msg) from e
|
|
233
244
|
|
|
234
245
|
async def rollback(self) -> None:
|