sqlspec 0.16.1__cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1780 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +256 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +122 -0
- sqlspec/builder/mixins/_merge_operations.py +476 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +603 -0
- sqlspec/builder/mixins/_update_operations.py +187 -0
- sqlspec/builder/mixins/_where_clause.py +621 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.1.dist-info/METADATA +365 -0
- sqlspec-0.16.1.dist-info/RECORD +148 -0
- sqlspec-0.16.1.dist-info/WHEEL +7 -0
- sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""DuckDB database configuration with connection pooling."""
|
|
2
|
+
# ruff: noqa: D107 W293 RUF100 S110 PLR0913 FA100 BLE001 UP037 COM812 ARG002
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from contextlib import contextmanager, suppress
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Final, Optional, TypedDict, cast
|
|
10
|
+
|
|
11
|
+
import duckdb
|
|
12
|
+
from typing_extensions import NotRequired
|
|
13
|
+
|
|
14
|
+
from sqlspec.adapters.duckdb._types import DuckDBConnection
|
|
15
|
+
from sqlspec.adapters.duckdb.driver import DuckDBCursor, DuckDBDriver, duckdb_statement_config
|
|
16
|
+
from sqlspec.config import SyncDatabaseConfig
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Generator
|
|
20
|
+
from typing import Callable, ClassVar, Union
|
|
21
|
+
|
|
22
|
+
from sqlspec.core.statement import StatementConfig
|
|
23
|
+
|
|
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
|
+
__all__ = (
|
|
33
|
+
"DuckDBConfig",
|
|
34
|
+
"DuckDBConnectionParams",
|
|
35
|
+
"DuckDBConnectionPool",
|
|
36
|
+
"DuckDBDriverFeatures",
|
|
37
|
+
"DuckDBExtensionConfig",
|
|
38
|
+
"DuckDBPoolParams",
|
|
39
|
+
"DuckDBSecretConfig",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
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
|
+
class DuckDBConnectionParams(TypedDict, total=False):
|
|
248
|
+
"""DuckDB connection parameters."""
|
|
249
|
+
|
|
250
|
+
database: NotRequired[str]
|
|
251
|
+
read_only: NotRequired[bool]
|
|
252
|
+
config: NotRequired[dict[str, Any]]
|
|
253
|
+
memory_limit: NotRequired[str]
|
|
254
|
+
threads: NotRequired[int]
|
|
255
|
+
temp_directory: NotRequired[str]
|
|
256
|
+
max_temp_directory_size: NotRequired[str]
|
|
257
|
+
autoload_known_extensions: NotRequired[bool]
|
|
258
|
+
autoinstall_known_extensions: NotRequired[bool]
|
|
259
|
+
allow_community_extensions: NotRequired[bool]
|
|
260
|
+
allow_unsigned_extensions: NotRequired[bool]
|
|
261
|
+
extension_directory: NotRequired[str]
|
|
262
|
+
custom_extension_repository: NotRequired[str]
|
|
263
|
+
autoinstall_extension_repository: NotRequired[str]
|
|
264
|
+
allow_persistent_secrets: NotRequired[bool]
|
|
265
|
+
enable_external_access: NotRequired[bool]
|
|
266
|
+
secret_directory: NotRequired[str]
|
|
267
|
+
enable_object_cache: NotRequired[bool]
|
|
268
|
+
parquet_metadata_cache: NotRequired[str]
|
|
269
|
+
enable_external_file_cache: NotRequired[bool]
|
|
270
|
+
checkpoint_threshold: NotRequired[str]
|
|
271
|
+
enable_progress_bar: NotRequired[bool]
|
|
272
|
+
progress_bar_time: NotRequired[float]
|
|
273
|
+
enable_logging: NotRequired[bool]
|
|
274
|
+
log_query_path: NotRequired[str]
|
|
275
|
+
logging_level: NotRequired[str]
|
|
276
|
+
preserve_insertion_order: NotRequired[bool]
|
|
277
|
+
default_null_order: NotRequired[str]
|
|
278
|
+
default_order: NotRequired[str]
|
|
279
|
+
ieee_floating_point_ops: NotRequired[bool]
|
|
280
|
+
binary_as_string: NotRequired[bool]
|
|
281
|
+
arrow_large_buffer_size: NotRequired[bool]
|
|
282
|
+
errors_as_json: NotRequired[bool]
|
|
283
|
+
extra: NotRequired[dict[str, Any]]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class DuckDBPoolParams(DuckDBConnectionParams, total=False):
|
|
287
|
+
"""Complete pool configuration for DuckDB adapter.
|
|
288
|
+
|
|
289
|
+
Combines standardized pool parameters with DuckDB-specific connection parameters.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
# Standardized pool parameters (consistent across ALL adapters)
|
|
293
|
+
pool_min_size: NotRequired[int]
|
|
294
|
+
pool_max_size: NotRequired[int]
|
|
295
|
+
pool_timeout: NotRequired[float]
|
|
296
|
+
pool_recycle_seconds: NotRequired[int]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class DuckDBExtensionConfig(TypedDict, total=False):
|
|
300
|
+
"""DuckDB extension configuration for auto-management."""
|
|
301
|
+
|
|
302
|
+
name: str
|
|
303
|
+
"""Name of the extension to install/load."""
|
|
304
|
+
|
|
305
|
+
version: NotRequired[str]
|
|
306
|
+
"""Specific version of the extension."""
|
|
307
|
+
|
|
308
|
+
repository: NotRequired[str]
|
|
309
|
+
"""Repository for the extension (core, community, or custom URL)."""
|
|
310
|
+
|
|
311
|
+
force_install: NotRequired[bool]
|
|
312
|
+
"""Force reinstallation of the extension."""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class DuckDBSecretConfig(TypedDict, total=False):
|
|
316
|
+
"""DuckDB secret configuration for AI/API integrations."""
|
|
317
|
+
|
|
318
|
+
secret_type: str
|
|
319
|
+
"""Type of secret (e.g., 'openai', 'aws', 'azure', 'gcp')."""
|
|
320
|
+
|
|
321
|
+
name: str
|
|
322
|
+
"""Name of the secret."""
|
|
323
|
+
|
|
324
|
+
value: dict[str, Any]
|
|
325
|
+
"""Secret configuration values."""
|
|
326
|
+
|
|
327
|
+
scope: NotRequired[str]
|
|
328
|
+
"""Scope of the secret (LOCAL or PERSISTENT)."""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class DuckDBDriverFeatures(TypedDict, total=False):
|
|
332
|
+
"""TypedDict for DuckDB driver features configuration."""
|
|
333
|
+
|
|
334
|
+
extensions: NotRequired[Sequence[DuckDBExtensionConfig]]
|
|
335
|
+
"""List of extensions to install/load on connection creation."""
|
|
336
|
+
secrets: NotRequired[Sequence[DuckDBSecretConfig]]
|
|
337
|
+
"""List of secrets to create for AI/API integrations."""
|
|
338
|
+
on_connection_create: NotRequired["Callable[[DuckDBConnection], Optional[DuckDBConnection]]"]
|
|
339
|
+
"""Callback executed when connection is created."""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, DuckDBDriver]):
|
|
343
|
+
"""Enhanced DuckDB configuration with connection pooling and intelligent features.
|
|
344
|
+
|
|
345
|
+
This configuration supports all of DuckDB's unique features including:
|
|
346
|
+
|
|
347
|
+
- Connection pooling optimized for DuckDB's architecture
|
|
348
|
+
- Extension auto-management and installation
|
|
349
|
+
- Secret management for API integrations
|
|
350
|
+
- Intelligent auto configuration settings
|
|
351
|
+
- High-performance Arrow integration
|
|
352
|
+
- Direct file querying capabilities
|
|
353
|
+
- Performance optimizations for analytics workloads
|
|
354
|
+
|
|
355
|
+
DuckDB Connection Pool Best Practices:
|
|
356
|
+
- DuckDB performs best with long-lived connections that maintain cache
|
|
357
|
+
- Default pool size is 1-4 connections (DuckDB is optimized for single connection)
|
|
358
|
+
- Connection recycling is set to 24 hours by default (set to 0 to disable)
|
|
359
|
+
- Shared memory databases use `:memory:shared_db` for proper concurrency
|
|
360
|
+
- Health checks are minimized to reduce overhead
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
driver_type: "ClassVar[type[DuckDBDriver]]" = DuckDBDriver
|
|
364
|
+
connection_type: "ClassVar[type[DuckDBConnection]]" = DuckDBConnection
|
|
365
|
+
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
*,
|
|
369
|
+
pool_config: "Optional[Union[DuckDBPoolParams, dict[str, Any]]]" = None,
|
|
370
|
+
migration_config: Optional[dict[str, Any]] = None,
|
|
371
|
+
pool_instance: "Optional[DuckDBConnectionPool]" = None,
|
|
372
|
+
statement_config: "Optional[StatementConfig]" = None,
|
|
373
|
+
driver_features: "Optional[Union[DuckDBDriverFeatures, dict[str, Any]]]" = None,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Initialize DuckDB configuration with intelligent features."""
|
|
376
|
+
if pool_config is None:
|
|
377
|
+
pool_config = {}
|
|
378
|
+
if "database" not in pool_config:
|
|
379
|
+
pool_config["database"] = ":memory:shared_db"
|
|
380
|
+
|
|
381
|
+
if pool_config.get("database") in {":memory:", ""}:
|
|
382
|
+
pool_config["database"] = ":memory:shared_db"
|
|
383
|
+
|
|
384
|
+
super().__init__(
|
|
385
|
+
pool_config=dict(pool_config),
|
|
386
|
+
pool_instance=pool_instance,
|
|
387
|
+
migration_config=migration_config,
|
|
388
|
+
statement_config=statement_config or duckdb_statement_config,
|
|
389
|
+
driver_features=cast("dict[str, Any]", driver_features),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def _get_connection_config_dict(self) -> "dict[str, Any]":
|
|
393
|
+
"""Get connection configuration as plain dict for pool creation."""
|
|
394
|
+
return {
|
|
395
|
+
k: v
|
|
396
|
+
for k, v in self.pool_config.items()
|
|
397
|
+
if v is not None
|
|
398
|
+
and k not in {"pool_min_size", "pool_max_size", "pool_timeout", "pool_recycle_seconds", "extra"}
|
|
399
|
+
}
|
|
400
|
+
|
|
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
|
+
def _create_pool(self) -> DuckDBConnectionPool:
|
|
410
|
+
"""Create the DuckDB connection pool."""
|
|
411
|
+
|
|
412
|
+
extensions = self.driver_features.get("extensions", None)
|
|
413
|
+
secrets = self.driver_features.get("secrets", None)
|
|
414
|
+
on_connection_create = self.driver_features.get("on_connection_create", None)
|
|
415
|
+
|
|
416
|
+
extensions_dicts = [dict(ext) for ext in extensions] if extensions else None
|
|
417
|
+
secrets_dicts = [dict(secret) for secret in secrets] if secrets else None
|
|
418
|
+
|
|
419
|
+
pool_callback = None
|
|
420
|
+
if on_connection_create:
|
|
421
|
+
|
|
422
|
+
def wrapped_callback(conn: DuckDBConnection) -> None:
|
|
423
|
+
on_connection_create(conn)
|
|
424
|
+
|
|
425
|
+
pool_callback = wrapped_callback
|
|
426
|
+
conf = {"extensions": extensions_dicts, "secrets": secrets_dicts, "on_connection_create": pool_callback}
|
|
427
|
+
|
|
428
|
+
return DuckDBConnectionPool(
|
|
429
|
+
connection_config=self._get_connection_config_dict(),
|
|
430
|
+
**conf, # type: ignore[arg-type]
|
|
431
|
+
**self._get_pool_config_dict(),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def _close_pool(self) -> None:
|
|
435
|
+
"""Close the connection pool."""
|
|
436
|
+
if self.pool_instance:
|
|
437
|
+
self.pool_instance.close()
|
|
438
|
+
|
|
439
|
+
def create_connection(self) -> DuckDBConnection:
|
|
440
|
+
"""Get a DuckDB connection from the pool.
|
|
441
|
+
|
|
442
|
+
This method ensures the pool is created and returns a connection
|
|
443
|
+
from the pool. The connection is checked out from the pool and must
|
|
444
|
+
be properly managed by the caller.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
DuckDBConnection: A connection from the pool
|
|
448
|
+
|
|
449
|
+
Note:
|
|
450
|
+
For automatic connection management, prefer using provide_connection()
|
|
451
|
+
or provide_session() which handle returning connections to the pool.
|
|
452
|
+
The caller is responsible for returning the connection to the pool
|
|
453
|
+
using pool.release(connection) when done.
|
|
454
|
+
"""
|
|
455
|
+
pool = self.provide_pool()
|
|
456
|
+
|
|
457
|
+
return pool.acquire()
|
|
458
|
+
|
|
459
|
+
@contextmanager
|
|
460
|
+
def provide_connection(self, *args: Any, **kwargs: Any) -> "Generator[DuckDBConnection, None, None]":
|
|
461
|
+
"""Provide a pooled DuckDB connection context manager.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
*args: Additional arguments.
|
|
465
|
+
**kwargs: Additional keyword arguments.
|
|
466
|
+
|
|
467
|
+
Yields:
|
|
468
|
+
A DuckDB connection instance.
|
|
469
|
+
"""
|
|
470
|
+
pool = self.provide_pool()
|
|
471
|
+
with pool.get_connection() as connection:
|
|
472
|
+
yield connection
|
|
473
|
+
|
|
474
|
+
@contextmanager
|
|
475
|
+
def provide_session(
|
|
476
|
+
self, *args: Any, statement_config: "Optional[StatementConfig]" = None, **kwargs: Any
|
|
477
|
+
) -> "Generator[DuckDBDriver, None, None]":
|
|
478
|
+
"""Provide a DuckDB driver session context manager.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
*args: Additional arguments.
|
|
482
|
+
statement_config: Optional statement configuration override.
|
|
483
|
+
**kwargs: Additional keyword arguments.
|
|
484
|
+
|
|
485
|
+
Yields:
|
|
486
|
+
A context manager that yields a DuckDBDriver instance.
|
|
487
|
+
"""
|
|
488
|
+
with self.provide_connection(*args, **kwargs) as connection:
|
|
489
|
+
driver = self.driver_type(connection=connection, statement_config=statement_config or self.statement_config)
|
|
490
|
+
yield driver
|
|
491
|
+
|
|
492
|
+
def get_signature_namespace(self) -> "dict[str, type[Any]]":
|
|
493
|
+
"""Get the signature namespace for DuckDB types.
|
|
494
|
+
|
|
495
|
+
This provides all DuckDB-specific types that Litestar needs to recognize
|
|
496
|
+
to avoid serialization attempts.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Dictionary mapping type names to types.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
namespace = super().get_signature_namespace()
|
|
503
|
+
namespace.update({"DuckDBConnection": DuckDBConnection, "DuckDBCursor": DuckDBCursor})
|
|
504
|
+
return namespace
|