sqlspec 0.16.2__py3-none-any.whl → 0.17.1__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 (36) hide show
  1. sqlspec/__init__.py +11 -1
  2. sqlspec/_sql.py +152 -489
  3. sqlspec/adapters/aiosqlite/__init__.py +11 -1
  4. sqlspec/adapters/aiosqlite/config.py +137 -165
  5. sqlspec/adapters/aiosqlite/driver.py +21 -10
  6. sqlspec/adapters/aiosqlite/pool.py +492 -0
  7. sqlspec/adapters/duckdb/__init__.py +2 -0
  8. sqlspec/adapters/duckdb/config.py +11 -235
  9. sqlspec/adapters/duckdb/pool.py +243 -0
  10. sqlspec/adapters/sqlite/__init__.py +2 -0
  11. sqlspec/adapters/sqlite/config.py +4 -115
  12. sqlspec/adapters/sqlite/pool.py +140 -0
  13. sqlspec/base.py +147 -26
  14. sqlspec/builder/__init__.py +6 -0
  15. sqlspec/builder/_column.py +5 -1
  16. sqlspec/builder/_expression_wrappers.py +46 -0
  17. sqlspec/builder/_insert.py +1 -3
  18. sqlspec/builder/_parsing_utils.py +27 -0
  19. sqlspec/builder/_update.py +5 -5
  20. sqlspec/builder/mixins/_join_operations.py +115 -1
  21. sqlspec/builder/mixins/_order_limit_operations.py +16 -4
  22. sqlspec/builder/mixins/_select_operations.py +307 -3
  23. sqlspec/builder/mixins/_update_operations.py +4 -4
  24. sqlspec/builder/mixins/_where_clause.py +60 -11
  25. sqlspec/core/compiler.py +7 -5
  26. sqlspec/driver/_common.py +9 -1
  27. sqlspec/loader.py +27 -54
  28. sqlspec/protocols.py +10 -0
  29. sqlspec/storage/registry.py +2 -2
  30. sqlspec/typing.py +53 -99
  31. {sqlspec-0.16.2.dist-info → sqlspec-0.17.1.dist-info}/METADATA +1 -1
  32. {sqlspec-0.16.2.dist-info → sqlspec-0.17.1.dist-info}/RECORD +36 -32
  33. {sqlspec-0.16.2.dist-info → sqlspec-0.17.1.dist-info}/WHEEL +0 -0
  34. {sqlspec-0.16.2.dist-info → sqlspec-0.17.1.dist-info}/entry_points.txt +0 -0
  35. {sqlspec-0.16.2.dist-info → sqlspec-0.17.1.dist-info}/licenses/LICENSE +0 -0
  36. {sqlspec-0.16.2.dist-info → sqlspec-0.17.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,19 +1,29 @@
1
1
  from sqlspec.adapters.aiosqlite._types import AiosqliteConnection
2
- from sqlspec.adapters.aiosqlite.config import AiosqliteConfig, AiosqliteConnectionParams, AiosqliteConnectionPool
2
+ from sqlspec.adapters.aiosqlite.config import AiosqliteConfig, AiosqliteConnectionParams, AiosqlitePoolParams
3
3
  from sqlspec.adapters.aiosqlite.driver import (
4
4
  AiosqliteCursor,
5
5
  AiosqliteDriver,
6
6
  AiosqliteExceptionHandler,
7
7
  aiosqlite_statement_config,
8
8
  )
9
+ from sqlspec.adapters.aiosqlite.pool import (
10
+ AiosqliteConnectionPool,
11
+ AiosqliteConnectTimeoutError,
12
+ AiosqlitePoolClosedError,
13
+ AiosqlitePoolConnection,
14
+ )
9
15
 
10
16
  __all__ = (
11
17
  "AiosqliteConfig",
18
+ "AiosqliteConnectTimeoutError",
12
19
  "AiosqliteConnection",
13
20
  "AiosqliteConnectionParams",
14
21
  "AiosqliteConnectionPool",
15
22
  "AiosqliteCursor",
16
23
  "AiosqliteDriver",
17
24
  "AiosqliteExceptionHandler",
25
+ "AiosqlitePoolClosedError",
26
+ "AiosqlitePoolConnection",
27
+ "AiosqlitePoolParams",
18
28
  "aiosqlite_statement_config",
19
29
  )
@@ -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, Final, Optional, TypedDict
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", "AiosqliteConnectionPool")
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 AiosqliteConfig(AsyncDatabaseConfig):
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
- cursor_type: ClassVar[type[AiosqliteCursor]] = AiosqliteCursor
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
- connection_params = {}
149
- if pool_config:
150
- connection_params.update(pool_config)
151
- connection_params.update(kwargs)
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=connection_params,
84
+ pool_config=config_dict,
155
85
  pool_instance=pool_instance,
156
- migration_config=migration_config or {},
86
+ migration_config=migration_config,
157
87
  statement_config=statement_config or aiosqlite_statement_config,
88
+ driver_features={},
158
89
  )
159
90
 
160
- self._connection_parameters = self._parse_connection_parameters(connection_params)
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
- Processed connection parameters dict.
95
+ Dictionary with pool parameters, filtering out None values.
173
96
  """
174
- result = params.copy()
175
-
176
- if "database" not in result:
177
- # Default to in-memory database
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
- for pool_param in ["pool_min_size", "pool_max_size", "pool_timeout", "pool_recycle_seconds"]:
186
- result.pop(pool_param, None)
102
+ def _get_connection_config_dict(self) -> "dict[str, Any]":
103
+ """Get connection configuration as plain dict for pool creation.
187
104
 
188
- return result
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 a database connection.
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
- AiosqliteConnection: Database connection instance.
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, *args: Any, statement_config: "Optional[StatementConfig]" = None, **kwargs: Any
140
+ self, *_args: Any, statement_config: "Optional[StatementConfig]" = None, **_kwargs: Any
203
141
  ) -> "AsyncGenerator[AiosqliteDriver, None]":
204
- """Provide an async database session.
142
+ """Provide an async driver session context manager.
205
143
 
206
144
  Args:
207
- *args: Additional positional arguments.
145
+ *_args: Additional arguments.
208
146
  statement_config: Optional statement configuration override.
209
- **kwargs: Additional keyword arguments.
147
+ **_kwargs: Additional keyword arguments.
210
148
 
211
149
  Yields:
212
- AiosqliteDriver: Database session instance.
150
+ An AiosqliteDriver instance.
213
151
  """
214
- _ = args, kwargs
215
- effective_statement_config = statement_config or self.statement_config
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 _get_connection_config_dict(self) -> "dict[str, Any]":
229
- """Get connection configuration dictionary.
155
+ async def _create_pool(self) -> AiosqliteConnectionPool:
156
+ """Create the connection pool instance.
230
157
 
231
158
  Returns:
232
- Connection parameters for creating connections.
159
+ AiosqliteConnectionPool: The connection pool instance.
233
160
  """
234
- return self._connection_parameters.copy()
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 _create_pool(self) -> "AiosqliteConnectionPool":
237
- """Create the connection manager instance.
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
- AiosqliteConnectionPool: The connection manager instance.
184
+ An aiosqlite connection instance.
241
185
  """
242
186
  if self.pool_instance is None:
243
- self.pool_instance = AiosqliteConnectionPool(self._connection_parameters)
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
- async def _close_pool(self) -> None:
247
- """Close the connection manager."""
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
- async def close_pool(self) -> None:
252
- """Close the connection pool (delegates to _close_pool)."""
253
- await self._close_pool()
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
- enhanced_config = aiosqlite_statement_config.replace(
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
- await self.connection.execute("BEGIN")
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
- msg = f"Failed to begin transaction: {e}"
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: