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,38 +1,24 @@
|
|
|
1
1
|
"""DuckDB database configuration with connection pooling."""
|
|
2
|
-
# ruff: noqa: D107 W293 RUF100 S110 PLR0913 FA100 BLE001 UP037 COM812 ARG002
|
|
3
2
|
|
|
4
|
-
import logging
|
|
5
|
-
import threading
|
|
6
|
-
import time
|
|
7
3
|
from collections.abc import Sequence
|
|
8
|
-
from contextlib import contextmanager
|
|
9
|
-
from typing import TYPE_CHECKING, Any,
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union, cast
|
|
10
6
|
|
|
11
|
-
import duckdb
|
|
12
7
|
from typing_extensions import NotRequired
|
|
13
8
|
|
|
14
9
|
from sqlspec.adapters.duckdb._types import DuckDBConnection
|
|
15
10
|
from sqlspec.adapters.duckdb.driver import DuckDBCursor, DuckDBDriver, duckdb_statement_config
|
|
11
|
+
from sqlspec.adapters.duckdb.pool import DuckDBConnectionPool
|
|
16
12
|
from sqlspec.config import SyncDatabaseConfig
|
|
17
13
|
|
|
18
14
|
if TYPE_CHECKING:
|
|
19
|
-
from collections.abc import Generator
|
|
20
|
-
from typing import Callable, ClassVar, Union
|
|
15
|
+
from collections.abc import Callable, Generator
|
|
21
16
|
|
|
22
17
|
from sqlspec.core.statement import StatementConfig
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
DEFAULT_MIN_POOL: Final[int] = 1
|
|
28
|
-
DEFAULT_MAX_POOL: Final[int] = 4
|
|
29
|
-
POOL_TIMEOUT: Final[float] = 30.0
|
|
30
|
-
POOL_RECYCLE: Final[int] = 86400
|
|
31
|
-
|
|
32
19
|
__all__ = (
|
|
33
20
|
"DuckDBConfig",
|
|
34
21
|
"DuckDBConnectionParams",
|
|
35
|
-
"DuckDBConnectionPool",
|
|
36
22
|
"DuckDBDriverFeatures",
|
|
37
23
|
"DuckDBExtensionConfig",
|
|
38
24
|
"DuckDBPoolParams",
|
|
@@ -40,210 +26,6 @@ __all__ = (
|
|
|
40
26
|
)
|
|
41
27
|
|
|
42
28
|
|
|
43
|
-
class DuckDBConnectionPool:
|
|
44
|
-
"""Thread-local connection manager for DuckDB with performance optimizations.
|
|
45
|
-
|
|
46
|
-
Uses thread-local storage to ensure each thread gets its own DuckDB connection,
|
|
47
|
-
preventing the thread-safety issues that cause segmentation faults when
|
|
48
|
-
multiple cursors share the same connection concurrently.
|
|
49
|
-
|
|
50
|
-
This design trades traditional pooling for thread safety, which is essential
|
|
51
|
-
for DuckDB since connections and cursors are not thread-safe.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
__slots__ = (
|
|
55
|
-
"_connection_config",
|
|
56
|
-
"_connection_times",
|
|
57
|
-
"_created_connections",
|
|
58
|
-
"_extensions",
|
|
59
|
-
"_lock",
|
|
60
|
-
"_on_connection_create",
|
|
61
|
-
"_recycle",
|
|
62
|
-
"_secrets",
|
|
63
|
-
"_thread_local",
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
def __init__( # noqa: PLR0913
|
|
67
|
-
self,
|
|
68
|
-
connection_config: "dict[str, Any]", # noqa: UP037
|
|
69
|
-
pool_min_size: int = DEFAULT_MIN_POOL,
|
|
70
|
-
pool_max_size: int = DEFAULT_MAX_POOL,
|
|
71
|
-
pool_timeout: float = POOL_TIMEOUT,
|
|
72
|
-
pool_recycle_seconds: int = POOL_RECYCLE,
|
|
73
|
-
extensions: "Optional[list[dict[str, Any]]]" = None, # noqa: FA100, UP037
|
|
74
|
-
secrets: "Optional[list[dict[str, Any]]]" = None, # noqa: FA100, UP037
|
|
75
|
-
on_connection_create: "Optional[Callable[[DuckDBConnection], None]]" = None, # noqa: FA100
|
|
76
|
-
) -> None:
|
|
77
|
-
"""Initialize the thread-local connection manager."""
|
|
78
|
-
self._connection_config = connection_config
|
|
79
|
-
self._recycle = pool_recycle_seconds
|
|
80
|
-
self._extensions = extensions or []
|
|
81
|
-
self._secrets = secrets or []
|
|
82
|
-
self._on_connection_create = on_connection_create
|
|
83
|
-
self._thread_local = threading.local()
|
|
84
|
-
self._lock = threading.RLock()
|
|
85
|
-
self._created_connections = 0
|
|
86
|
-
self._connection_times: "dict[int, float]" = {}
|
|
87
|
-
|
|
88
|
-
def _create_connection(self) -> DuckDBConnection:
|
|
89
|
-
"""Create a new DuckDB connection with extensions and secrets."""
|
|
90
|
-
connect_parameters = {}
|
|
91
|
-
config_dict = {}
|
|
92
|
-
|
|
93
|
-
for key, value in self._connection_config.items():
|
|
94
|
-
if key in {"database", "read_only"}:
|
|
95
|
-
connect_parameters[key] = value
|
|
96
|
-
else:
|
|
97
|
-
config_dict[key] = value
|
|
98
|
-
|
|
99
|
-
if config_dict:
|
|
100
|
-
connect_parameters["config"] = config_dict
|
|
101
|
-
|
|
102
|
-
connection = duckdb.connect(**connect_parameters)
|
|
103
|
-
|
|
104
|
-
for ext_config in self._extensions:
|
|
105
|
-
ext_name = ext_config.get("name")
|
|
106
|
-
if not ext_name:
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
install_kwargs = {}
|
|
110
|
-
if "version" in ext_config:
|
|
111
|
-
install_kwargs["version"] = ext_config["version"]
|
|
112
|
-
if "repository" in ext_config:
|
|
113
|
-
install_kwargs["repository"] = ext_config["repository"]
|
|
114
|
-
if ext_config.get("force_install", False):
|
|
115
|
-
install_kwargs["force_install"] = True
|
|
116
|
-
|
|
117
|
-
try:
|
|
118
|
-
if install_kwargs:
|
|
119
|
-
connection.install_extension(ext_name, **install_kwargs)
|
|
120
|
-
connection.load_extension(ext_name)
|
|
121
|
-
except Exception: # noqa: BLE001, S110
|
|
122
|
-
pass
|
|
123
|
-
|
|
124
|
-
for secret_config in self._secrets:
|
|
125
|
-
secret_type = secret_config.get("secret_type")
|
|
126
|
-
secret_name = secret_config.get("name")
|
|
127
|
-
secret_value = secret_config.get("value")
|
|
128
|
-
|
|
129
|
-
if not (secret_type and secret_name and secret_value):
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
value_pairs = []
|
|
133
|
-
for key, value in secret_value.items():
|
|
134
|
-
escaped_value = str(value).replace("'", "''")
|
|
135
|
-
value_pairs.append(f"'{key}' = '{escaped_value}'")
|
|
136
|
-
value_string = ", ".join(value_pairs)
|
|
137
|
-
scope_clause = ""
|
|
138
|
-
if "scope" in secret_config:
|
|
139
|
-
scope_clause = f" SCOPE '{secret_config['scope']}'"
|
|
140
|
-
|
|
141
|
-
sql = f""" # noqa: S608
|
|
142
|
-
CREATE SECRET {secret_name} (
|
|
143
|
-
TYPE {secret_type},
|
|
144
|
-
{value_string}
|
|
145
|
-
){scope_clause}
|
|
146
|
-
"""
|
|
147
|
-
with suppress(Exception):
|
|
148
|
-
connection.execute(sql)
|
|
149
|
-
|
|
150
|
-
if self._on_connection_create:
|
|
151
|
-
with suppress(Exception):
|
|
152
|
-
self._on_connection_create(connection)
|
|
153
|
-
|
|
154
|
-
conn_id = id(connection)
|
|
155
|
-
with self._lock:
|
|
156
|
-
self._created_connections += 1
|
|
157
|
-
self._connection_times[conn_id] = time.time()
|
|
158
|
-
|
|
159
|
-
return connection
|
|
160
|
-
|
|
161
|
-
def _get_thread_connection(self) -> DuckDBConnection:
|
|
162
|
-
"""Get or create a connection for the current thread.
|
|
163
|
-
|
|
164
|
-
Each thread gets its own dedicated DuckDB connection to prevent
|
|
165
|
-
thread-safety issues with concurrent cursor operations.
|
|
166
|
-
"""
|
|
167
|
-
if not hasattr(self._thread_local, "connection"):
|
|
168
|
-
self._thread_local.connection = self._create_connection()
|
|
169
|
-
self._thread_local.created_at = time.time()
|
|
170
|
-
|
|
171
|
-
# Check if connection needs recycling
|
|
172
|
-
if self._recycle > 0 and time.time() - self._thread_local.created_at > self._recycle:
|
|
173
|
-
with suppress(Exception):
|
|
174
|
-
self._thread_local.connection.close()
|
|
175
|
-
self._thread_local.connection = self._create_connection()
|
|
176
|
-
self._thread_local.created_at = time.time()
|
|
177
|
-
|
|
178
|
-
return cast("DuckDBConnection", self._thread_local.connection)
|
|
179
|
-
|
|
180
|
-
def _close_thread_connection(self) -> None:
|
|
181
|
-
"""Close the connection for the current thread."""
|
|
182
|
-
if hasattr(self._thread_local, "connection"):
|
|
183
|
-
with suppress(Exception):
|
|
184
|
-
self._thread_local.connection.close()
|
|
185
|
-
del self._thread_local.connection
|
|
186
|
-
if hasattr(self._thread_local, "created_at"):
|
|
187
|
-
del self._thread_local.created_at
|
|
188
|
-
|
|
189
|
-
def _is_connection_alive(self, connection: DuckDBConnection) -> bool:
|
|
190
|
-
"""Check if a connection is still alive and usable.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
connection: Connection to check
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
True if connection is alive, False otherwise
|
|
197
|
-
"""
|
|
198
|
-
try:
|
|
199
|
-
cursor = connection.cursor()
|
|
200
|
-
cursor.close()
|
|
201
|
-
except Exception:
|
|
202
|
-
return False
|
|
203
|
-
return True
|
|
204
|
-
|
|
205
|
-
@contextmanager
|
|
206
|
-
def get_connection(self) -> "Generator[DuckDBConnection, None, None]":
|
|
207
|
-
"""Get a thread-local connection.
|
|
208
|
-
|
|
209
|
-
Each thread gets its own dedicated DuckDB connection to prevent
|
|
210
|
-
thread-safety issues with concurrent cursor operations.
|
|
211
|
-
|
|
212
|
-
Yields:
|
|
213
|
-
DuckDBConnection: A thread-local connection.
|
|
214
|
-
"""
|
|
215
|
-
connection = self._get_thread_connection()
|
|
216
|
-
try:
|
|
217
|
-
yield connection
|
|
218
|
-
except Exception:
|
|
219
|
-
# On error, close and recreate connection for this thread
|
|
220
|
-
self._close_thread_connection()
|
|
221
|
-
raise
|
|
222
|
-
|
|
223
|
-
def close(self) -> None:
|
|
224
|
-
"""Close the thread-local connection if it exists."""
|
|
225
|
-
self._close_thread_connection()
|
|
226
|
-
|
|
227
|
-
def size(self) -> int:
|
|
228
|
-
"""Get current pool size (always 1 for thread-local)."""
|
|
229
|
-
return 1 if hasattr(self._thread_local, "connection") else 0
|
|
230
|
-
|
|
231
|
-
def checked_out(self) -> int:
|
|
232
|
-
"""Get number of checked out connections (always 0 for thread-local)."""
|
|
233
|
-
return 0
|
|
234
|
-
|
|
235
|
-
def acquire(self) -> DuckDBConnection:
|
|
236
|
-
"""Acquire a thread-local connection.
|
|
237
|
-
|
|
238
|
-
Each thread gets its own dedicated DuckDB connection to prevent
|
|
239
|
-
thread-safety issues with concurrent cursor operations.
|
|
240
|
-
|
|
241
|
-
Returns:
|
|
242
|
-
DuckDBConnection: A thread-local connection
|
|
243
|
-
"""
|
|
244
|
-
return self._get_thread_connection()
|
|
245
|
-
|
|
246
|
-
|
|
247
29
|
class DuckDBConnectionParams(TypedDict, total=False):
|
|
248
30
|
"""DuckDB connection parameters."""
|
|
249
31
|
|
|
@@ -398,16 +180,9 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
|
|
|
398
180
|
and k not in {"pool_min_size", "pool_max_size", "pool_timeout", "pool_recycle_seconds", "extra"}
|
|
399
181
|
}
|
|
400
182
|
|
|
401
|
-
def _get_pool_config_dict(self) -> "dict[str, Any]":
|
|
402
|
-
"""Get pool configuration as plain dict for pool creation."""
|
|
403
|
-
return {
|
|
404
|
-
k: v
|
|
405
|
-
for k, v in self.pool_config.items()
|
|
406
|
-
if v is not None and k in {"pool_min_size", "pool_max_size", "pool_timeout", "pool_recycle_seconds"}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
183
|
def _create_pool(self) -> DuckDBConnectionPool:
|
|
410
|
-
"""Create
|
|
184
|
+
"""Create connection pool from configuration."""
|
|
185
|
+
connection_config = self._get_connection_config_dict()
|
|
411
186
|
|
|
412
187
|
extensions = self.driver_features.get("extensions", None)
|
|
413
188
|
secrets = self.driver_features.get("secrets", None)
|
|
@@ -423,12 +198,13 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
|
|
|
423
198
|
on_connection_create(conn)
|
|
424
199
|
|
|
425
200
|
pool_callback = wrapped_callback
|
|
426
|
-
conf = {"extensions": extensions_dicts, "secrets": secrets_dicts, "on_connection_create": pool_callback}
|
|
427
201
|
|
|
428
202
|
return DuckDBConnectionPool(
|
|
429
|
-
connection_config=
|
|
430
|
-
|
|
431
|
-
|
|
203
|
+
connection_config=connection_config,
|
|
204
|
+
extensions=extensions_dicts,
|
|
205
|
+
secrets=secrets_dicts,
|
|
206
|
+
on_connection_create=pool_callback,
|
|
207
|
+
**self.pool_config, # Pass all pool_config as kwargs to be filtered by the pool
|
|
432
208
|
)
|
|
433
209
|
|
|
434
210
|
def _close_pool(self) -> None:
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""DuckDB connection pool with thread-local connections."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from contextlib import contextmanager, suppress
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Final, Optional, cast
|
|
8
|
+
|
|
9
|
+
import duckdb
|
|
10
|
+
|
|
11
|
+
from sqlspec.adapters.duckdb._types import DuckDBConnection
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable, Generator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
DEFAULT_MIN_POOL: Final[int] = 1
|
|
20
|
+
DEFAULT_MAX_POOL: Final[int] = 4
|
|
21
|
+
POOL_TIMEOUT: Final[float] = 30.0
|
|
22
|
+
POOL_RECYCLE: Final[int] = 86400
|
|
23
|
+
|
|
24
|
+
__all__ = ("DuckDBConnectionPool",)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DuckDBConnectionPool:
|
|
28
|
+
"""Thread-local connection manager for DuckDB with performance optimizations.
|
|
29
|
+
|
|
30
|
+
Uses thread-local storage to ensure each thread gets its own DuckDB connection,
|
|
31
|
+
preventing the thread-safety issues that cause segmentation faults when
|
|
32
|
+
multiple cursors share the same connection concurrently.
|
|
33
|
+
|
|
34
|
+
This design trades traditional pooling for thread safety, which is essential
|
|
35
|
+
for DuckDB since connections and cursors are not thread-safe.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = (
|
|
39
|
+
"_connection_config",
|
|
40
|
+
"_connection_times",
|
|
41
|
+
"_created_connections",
|
|
42
|
+
"_extensions",
|
|
43
|
+
"_lock",
|
|
44
|
+
"_on_connection_create",
|
|
45
|
+
"_recycle",
|
|
46
|
+
"_secrets",
|
|
47
|
+
"_thread_local",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
connection_config: "dict[str, Any]",
|
|
53
|
+
pool_recycle_seconds: int = POOL_RECYCLE,
|
|
54
|
+
extensions: "Optional[list[dict[str, Any]]]" = None,
|
|
55
|
+
secrets: "Optional[list[dict[str, Any]]]" = None,
|
|
56
|
+
on_connection_create: "Optional[Callable[[DuckDBConnection], None]]" = None,
|
|
57
|
+
**kwargs: Any, # Accept and ignore additional parameters for compatibility
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize the thread-local connection manager.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
connection_config: DuckDB connection configuration
|
|
63
|
+
pool_recycle_seconds: Connection recycle time in seconds
|
|
64
|
+
extensions: List of extensions to install/load
|
|
65
|
+
secrets: List of secrets to create
|
|
66
|
+
on_connection_create: Callback executed when connection is created
|
|
67
|
+
**kwargs: Additional parameters ignored for compatibility
|
|
68
|
+
"""
|
|
69
|
+
self._connection_config = connection_config
|
|
70
|
+
self._recycle = pool_recycle_seconds
|
|
71
|
+
self._extensions = extensions or []
|
|
72
|
+
self._secrets = secrets or []
|
|
73
|
+
self._on_connection_create = on_connection_create
|
|
74
|
+
self._thread_local = threading.local()
|
|
75
|
+
self._lock = threading.RLock()
|
|
76
|
+
self._created_connections = 0
|
|
77
|
+
self._connection_times: dict[int, float] = {}
|
|
78
|
+
|
|
79
|
+
def _create_connection(self) -> DuckDBConnection:
|
|
80
|
+
"""Create a new DuckDB connection with extensions and secrets."""
|
|
81
|
+
connect_parameters = {}
|
|
82
|
+
config_dict = {}
|
|
83
|
+
|
|
84
|
+
for key, value in self._connection_config.items():
|
|
85
|
+
if key in {"database", "read_only"}:
|
|
86
|
+
connect_parameters[key] = value
|
|
87
|
+
else:
|
|
88
|
+
config_dict[key] = value
|
|
89
|
+
|
|
90
|
+
if config_dict:
|
|
91
|
+
connect_parameters["config"] = config_dict
|
|
92
|
+
|
|
93
|
+
connection = duckdb.connect(**connect_parameters)
|
|
94
|
+
|
|
95
|
+
for ext_config in self._extensions:
|
|
96
|
+
ext_name = ext_config.get("name")
|
|
97
|
+
if not ext_name:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
install_kwargs = {}
|
|
101
|
+
if "version" in ext_config:
|
|
102
|
+
install_kwargs["version"] = ext_config["version"]
|
|
103
|
+
if "repository" in ext_config:
|
|
104
|
+
install_kwargs["repository"] = ext_config["repository"]
|
|
105
|
+
if ext_config.get("force_install", False):
|
|
106
|
+
install_kwargs["force_install"] = True
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
if install_kwargs:
|
|
110
|
+
connection.install_extension(ext_name, **install_kwargs)
|
|
111
|
+
connection.load_extension(ext_name)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.debug("Failed to load DuckDB extension %s: %s", ext_name, e)
|
|
114
|
+
|
|
115
|
+
for secret_config in self._secrets:
|
|
116
|
+
secret_type = secret_config.get("secret_type")
|
|
117
|
+
secret_name = secret_config.get("name")
|
|
118
|
+
secret_value = secret_config.get("value")
|
|
119
|
+
|
|
120
|
+
if not (secret_type and secret_name and secret_value):
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
value_pairs = []
|
|
124
|
+
for key, value in secret_value.items():
|
|
125
|
+
escaped_value = str(value).replace("'", "''")
|
|
126
|
+
value_pairs.append(f"'{key}' = '{escaped_value}'")
|
|
127
|
+
value_string = ", ".join(value_pairs)
|
|
128
|
+
scope_clause = ""
|
|
129
|
+
if "scope" in secret_config:
|
|
130
|
+
scope_clause = f" SCOPE '{secret_config['scope']}'"
|
|
131
|
+
|
|
132
|
+
sql = f"""
|
|
133
|
+
CREATE SECRET {secret_name} (
|
|
134
|
+
TYPE {secret_type},
|
|
135
|
+
{value_string}
|
|
136
|
+
){scope_clause}
|
|
137
|
+
"""
|
|
138
|
+
with suppress(Exception):
|
|
139
|
+
connection.execute(sql)
|
|
140
|
+
|
|
141
|
+
if self._on_connection_create:
|
|
142
|
+
with suppress(Exception):
|
|
143
|
+
self._on_connection_create(connection)
|
|
144
|
+
|
|
145
|
+
conn_id = id(connection)
|
|
146
|
+
with self._lock:
|
|
147
|
+
self._created_connections += 1
|
|
148
|
+
self._connection_times[conn_id] = time.time()
|
|
149
|
+
|
|
150
|
+
return connection
|
|
151
|
+
|
|
152
|
+
def _get_thread_connection(self) -> DuckDBConnection:
|
|
153
|
+
"""Get or create a connection for the current thread.
|
|
154
|
+
|
|
155
|
+
Each thread gets its own dedicated DuckDB connection to prevent
|
|
156
|
+
thread-safety issues with concurrent cursor operations.
|
|
157
|
+
"""
|
|
158
|
+
if not hasattr(self._thread_local, "connection"):
|
|
159
|
+
self._thread_local.connection = self._create_connection()
|
|
160
|
+
self._thread_local.created_at = time.time()
|
|
161
|
+
|
|
162
|
+
# Check if connection needs recycling
|
|
163
|
+
if self._recycle > 0 and time.time() - self._thread_local.created_at > self._recycle:
|
|
164
|
+
with suppress(Exception):
|
|
165
|
+
self._thread_local.connection.close()
|
|
166
|
+
self._thread_local.connection = self._create_connection()
|
|
167
|
+
self._thread_local.created_at = time.time()
|
|
168
|
+
|
|
169
|
+
return cast("DuckDBConnection", self._thread_local.connection)
|
|
170
|
+
|
|
171
|
+
def _close_thread_connection(self) -> None:
|
|
172
|
+
"""Close the connection for the current thread."""
|
|
173
|
+
if hasattr(self._thread_local, "connection"):
|
|
174
|
+
with suppress(Exception):
|
|
175
|
+
self._thread_local.connection.close()
|
|
176
|
+
del self._thread_local.connection
|
|
177
|
+
if hasattr(self._thread_local, "created_at"):
|
|
178
|
+
del self._thread_local.created_at
|
|
179
|
+
|
|
180
|
+
def _is_connection_alive(self, connection: DuckDBConnection) -> bool:
|
|
181
|
+
"""Check if a connection is still alive and usable.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
connection: Connection to check
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
True if connection is alive, False otherwise
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
cursor = connection.cursor()
|
|
191
|
+
cursor.close()
|
|
192
|
+
except Exception:
|
|
193
|
+
return False
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
@contextmanager
|
|
197
|
+
def get_connection(self) -> "Generator[DuckDBConnection, None, None]":
|
|
198
|
+
"""Get a thread-local connection.
|
|
199
|
+
|
|
200
|
+
Each thread gets its own dedicated DuckDB connection to prevent
|
|
201
|
+
thread-safety issues with concurrent cursor operations.
|
|
202
|
+
|
|
203
|
+
Yields:
|
|
204
|
+
DuckDBConnection: A thread-local connection.
|
|
205
|
+
"""
|
|
206
|
+
connection = self._get_thread_connection()
|
|
207
|
+
try:
|
|
208
|
+
yield connection
|
|
209
|
+
except Exception:
|
|
210
|
+
# On error, close and recreate connection for this thread
|
|
211
|
+
self._close_thread_connection()
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
def close(self) -> None:
|
|
215
|
+
"""Close the thread-local connection if it exists."""
|
|
216
|
+
self._close_thread_connection()
|
|
217
|
+
|
|
218
|
+
def size(self) -> int:
|
|
219
|
+
"""Get current pool size (always 1 for thread-local)."""
|
|
220
|
+
return 1 if hasattr(self._thread_local, "connection") else 0
|
|
221
|
+
|
|
222
|
+
def checked_out(self) -> int:
|
|
223
|
+
"""Get number of checked out connections (always 0 for thread-local)."""
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
def acquire(self) -> DuckDBConnection:
|
|
227
|
+
"""Acquire a thread-local connection.
|
|
228
|
+
|
|
229
|
+
Each thread gets its own dedicated DuckDB connection to prevent
|
|
230
|
+
thread-safety issues with concurrent cursor operations.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
DuckDBConnection: A thread-local connection
|
|
234
|
+
"""
|
|
235
|
+
return self._get_thread_connection()
|
|
236
|
+
|
|
237
|
+
def release(self, connection: DuckDBConnection) -> None:
|
|
238
|
+
"""Release a connection (no-op for thread-local connections).
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
connection: The connection to release (ignored)
|
|
242
|
+
"""
|
|
243
|
+
# No-op: thread-local connections are managed per-thread
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
from sqlspec.adapters.sqlite._types import SqliteConnection
|
|
4
4
|
from sqlspec.adapters.sqlite.config import SqliteConfig, SqliteConnectionParams
|
|
5
5
|
from sqlspec.adapters.sqlite.driver import SqliteCursor, SqliteDriver, SqliteExceptionHandler, sqlite_statement_config
|
|
6
|
+
from sqlspec.adapters.sqlite.pool import SqliteConnectionPool
|
|
6
7
|
|
|
7
8
|
__all__ = (
|
|
8
9
|
"SqliteConfig",
|
|
9
10
|
"SqliteConnection",
|
|
10
11
|
"SqliteConnectionParams",
|
|
12
|
+
"SqliteConnectionPool",
|
|
11
13
|
"SqliteCursor",
|
|
12
14
|
"SqliteDriver",
|
|
13
15
|
"SqliteExceptionHandler",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""SQLite database configuration with thread-local connections."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import threading
|
|
3
|
+
import uuid
|
|
5
4
|
from contextlib import contextmanager
|
|
6
5
|
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union, cast
|
|
7
6
|
|
|
@@ -9,6 +8,7 @@ from typing_extensions import NotRequired
|
|
|
9
8
|
|
|
10
9
|
from sqlspec.adapters.sqlite._types import SqliteConnection
|
|
11
10
|
from sqlspec.adapters.sqlite.driver import SqliteCursor, SqliteDriver, sqlite_statement_config
|
|
11
|
+
from sqlspec.adapters.sqlite.pool import SqliteConnectionPool
|
|
12
12
|
from sqlspec.config import SyncDatabaseConfig
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -30,118 +30,7 @@ class SqliteConnectionParams(TypedDict, total=False):
|
|
|
30
30
|
uri: NotRequired[bool]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
__all__ = ("SqliteConfig", "SqliteConnectionParams"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class SqliteConnectionPool:
|
|
37
|
-
"""Thread-local connection manager for SQLite.
|
|
38
|
-
|
|
39
|
-
SQLite connections aren't thread-safe, so we use thread-local storage
|
|
40
|
-
to ensure each thread has its own connection. This is simpler and more
|
|
41
|
-
efficient than a traditional pool for SQLite's constraints.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
__slots__ = ("_connection_parameters", "_enable_optimizations", "_thread_local")
|
|
45
|
-
|
|
46
|
-
def __init__(
|
|
47
|
-
self,
|
|
48
|
-
connection_parameters: "dict[str, Any]",
|
|
49
|
-
enable_optimizations: bool = True,
|
|
50
|
-
**kwargs: Any, # Accept and ignore pool parameters for compatibility
|
|
51
|
-
) -> None:
|
|
52
|
-
"""Initialize the thread-local connection manager.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
connection_parameters: SQLite connection parameters
|
|
56
|
-
enable_optimizations: Whether to apply performance PRAGMAs
|
|
57
|
-
**kwargs: Ignored pool parameters for compatibility
|
|
58
|
-
"""
|
|
59
|
-
self._connection_parameters = connection_parameters
|
|
60
|
-
self._thread_local = threading.local()
|
|
61
|
-
self._enable_optimizations = enable_optimizations
|
|
62
|
-
|
|
63
|
-
def _create_connection(self) -> SqliteConnection:
|
|
64
|
-
"""Create a new SQLite connection with optimizations."""
|
|
65
|
-
connection = sqlite3.connect(**self._connection_parameters)
|
|
66
|
-
|
|
67
|
-
# Only apply optimizations if requested and not in-memory
|
|
68
|
-
if self._enable_optimizations:
|
|
69
|
-
database = self._connection_parameters.get("database", ":memory:")
|
|
70
|
-
is_memory = database == ":memory:" or database.startswith("file::memory:")
|
|
71
|
-
|
|
72
|
-
if not is_memory:
|
|
73
|
-
# WAL mode doesn't work with in-memory databases
|
|
74
|
-
connection.execute("PRAGMA journal_mode = WAL")
|
|
75
|
-
# Set busy timeout for better concurrent access
|
|
76
|
-
connection.execute("PRAGMA busy_timeout = 5000")
|
|
77
|
-
connection.execute("PRAGMA optimize")
|
|
78
|
-
# These work for all database types
|
|
79
|
-
connection.execute("PRAGMA foreign_keys = ON")
|
|
80
|
-
connection.execute("PRAGMA synchronous = NORMAL")
|
|
81
|
-
|
|
82
|
-
return connection # type: ignore[no-any-return]
|
|
83
|
-
|
|
84
|
-
def _get_thread_connection(self) -> SqliteConnection:
|
|
85
|
-
"""Get or create a connection for the current thread."""
|
|
86
|
-
try:
|
|
87
|
-
return cast("SqliteConnection", self._thread_local.connection)
|
|
88
|
-
except AttributeError:
|
|
89
|
-
# Connection doesn't exist for this thread yet
|
|
90
|
-
connection = self._create_connection()
|
|
91
|
-
self._thread_local.connection = connection
|
|
92
|
-
return connection
|
|
93
|
-
|
|
94
|
-
def _close_thread_connection(self) -> None:
|
|
95
|
-
"""Close the connection for the current thread."""
|
|
96
|
-
try:
|
|
97
|
-
connection = self._thread_local.connection
|
|
98
|
-
connection.close()
|
|
99
|
-
del self._thread_local.connection
|
|
100
|
-
except AttributeError:
|
|
101
|
-
# No connection for this thread
|
|
102
|
-
pass
|
|
103
|
-
|
|
104
|
-
@contextmanager
|
|
105
|
-
def get_connection(self) -> "Generator[SqliteConnection, None, None]":
|
|
106
|
-
"""Get a thread-local connection.
|
|
107
|
-
|
|
108
|
-
Yields:
|
|
109
|
-
SqliteConnection: A thread-local connection.
|
|
110
|
-
"""
|
|
111
|
-
yield self._get_thread_connection()
|
|
112
|
-
|
|
113
|
-
def close(self) -> None:
|
|
114
|
-
"""Close the thread-local connection if it exists."""
|
|
115
|
-
self._close_thread_connection()
|
|
116
|
-
|
|
117
|
-
def acquire(self) -> SqliteConnection:
|
|
118
|
-
"""Acquire a thread-local connection.
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
SqliteConnection: A thread-local connection
|
|
122
|
-
"""
|
|
123
|
-
return self._get_thread_connection()
|
|
124
|
-
|
|
125
|
-
def release(self, connection: SqliteConnection) -> None:
|
|
126
|
-
"""Release a connection (no-op for thread-local connections).
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
connection: The connection to release (ignored)
|
|
130
|
-
"""
|
|
131
|
-
# No-op: thread-local connections are managed per-thread
|
|
132
|
-
|
|
133
|
-
# Compatibility methods that return dummy values
|
|
134
|
-
def size(self) -> int:
|
|
135
|
-
"""Get pool size (always 1 for thread-local)."""
|
|
136
|
-
try:
|
|
137
|
-
_ = self._thread_local.connection
|
|
138
|
-
except AttributeError:
|
|
139
|
-
return 0
|
|
140
|
-
return 1
|
|
141
|
-
|
|
142
|
-
def checked_out(self) -> int:
|
|
143
|
-
"""Get number of checked out connections (always 0)."""
|
|
144
|
-
return 0
|
|
33
|
+
__all__ = ("SqliteConfig", "SqliteConnectionParams")
|
|
145
34
|
|
|
146
35
|
|
|
147
36
|
class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, SqliteDriver]):
|
|
@@ -169,7 +58,7 @@ class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, Sq
|
|
|
169
58
|
if pool_config is None:
|
|
170
59
|
pool_config = {}
|
|
171
60
|
if "database" not in pool_config or pool_config["database"] == ":memory:":
|
|
172
|
-
pool_config["database"] = "file
|
|
61
|
+
pool_config["database"] = f"file:memory_{uuid.uuid4().hex}?mode=memory&cache=private"
|
|
173
62
|
pool_config["uri"] = True
|
|
174
63
|
|
|
175
64
|
super().__init__(
|