sqlspec 0.26.0__py3-none-any.whl → 0.28.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 +7 -15
- sqlspec/_serialization.py +55 -25
- sqlspec/_typing.py +155 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +880 -0
- sqlspec/adapters/adbc/config.py +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +74 -2
- sqlspec/adapters/adbc/driver.py +226 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +536 -0
- sqlspec/adapters/aiosqlite/config.py +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +503 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +460 -0
- sqlspec/adapters/asyncpg/config.py +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +585 -0
- sqlspec/adapters/bigquery/config.py +36 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +489 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +563 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +225 -44
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1628 -0
- sqlspec/adapters/oracledb/config.py +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +475 -86
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +765 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +483 -0
- sqlspec/adapters/psqlpy/config.py +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
- sqlspec/adapters/psqlpy/driver.py +108 -41
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +962 -0
- sqlspec/adapters/psycopg/config.py +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +91 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +582 -0
- sqlspec/adapters/sqlite/config.py +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +331 -62
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +55 -47
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +234 -47
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +183 -160
- sqlspec/driver/_common.py +197 -109
- sqlspec/driver/_sync.py +189 -161
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +69 -61
- sqlspec/extensions/fastapi/__init__.py +21 -0
- sqlspec/extensions/fastapi/extension.py +331 -0
- sqlspec/extensions/fastapi/providers.py +543 -0
- sqlspec/extensions/flask/__init__.py +36 -0
- sqlspec/extensions/flask/_state.py +71 -0
- sqlspec/extensions/flask/_utils.py +40 -0
- sqlspec/extensions/flask/extension.py +389 -0
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +56 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +349 -224
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/extensions/starlette/__init__.py +10 -0
- sqlspec/extensions/starlette/_state.py +25 -0
- sqlspec/extensions/starlette/_utils.py +52 -0
- sqlspec/extensions/starlette/extension.py +254 -0
- sqlspec/extensions/starlette/middleware.py +154 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +106 -36
- sqlspec/storage/_utils.py +85 -0
- sqlspec/storage/backends/fsspec.py +133 -107
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +276 -168
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +30 -84
- sqlspec/utils/__init__.py +25 -4
- sqlspec/utils/arrow_helpers.py +81 -0
- sqlspec/utils/config_resolver.py +6 -6
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +205 -5
- sqlspec/utils/portal.py +311 -0
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +113 -4
- sqlspec/utils/sync_tools.py +36 -22
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +136 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
- sqlspec-0.28.0.dist-info/RECORD +221 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""DuckDB sync session store for Litestar integration."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sqlspec.extensions.litestar.store import BaseSQLSpecStore
|
|
7
|
+
from sqlspec.utils.logging import get_logger
|
|
8
|
+
from sqlspec.utils.sync_tools import async_
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqlspec.adapters.duckdb.config import DuckDBConfig
|
|
12
|
+
|
|
13
|
+
logger = get_logger("adapters.duckdb.litestar.store")
|
|
14
|
+
|
|
15
|
+
__all__ = ("DuckdbStore",)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DuckdbStore(BaseSQLSpecStore["DuckDBConfig"]):
|
|
19
|
+
"""DuckDB session store using synchronous DuckDB driver.
|
|
20
|
+
|
|
21
|
+
Implements server-side session storage for Litestar using DuckDB
|
|
22
|
+
via the synchronous duckdb driver. Uses Litestar's sync_to_thread
|
|
23
|
+
utility to provide an async interface compatible with the Store protocol.
|
|
24
|
+
|
|
25
|
+
Provides efficient session management with:
|
|
26
|
+
- Sync operations wrapped for async compatibility
|
|
27
|
+
- INSERT OR REPLACE for UPSERT functionality
|
|
28
|
+
- Native TIMESTAMP type support
|
|
29
|
+
- Automatic expiration handling
|
|
30
|
+
- Efficient cleanup of expired sessions
|
|
31
|
+
- Columnar storage optimized for analytical queries
|
|
32
|
+
|
|
33
|
+
Note:
|
|
34
|
+
DuckDB is primarily designed for analytical (OLAP) workloads.
|
|
35
|
+
For high-concurrency OLTP session stores, consider PostgreSQL adapters.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: DuckDBConfig instance.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
from sqlspec.adapters.duckdb import DuckDBConfig
|
|
42
|
+
from sqlspec.adapters.duckdb.litestar.store import DuckdbStore
|
|
43
|
+
|
|
44
|
+
config = DuckDBConfig()
|
|
45
|
+
store = DuckdbStore(config)
|
|
46
|
+
await store.create_table()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
__slots__ = ()
|
|
50
|
+
|
|
51
|
+
def __init__(self, config: "DuckDBConfig") -> None:
|
|
52
|
+
"""Initialize DuckDB session store.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: DuckDBConfig instance.
|
|
56
|
+
|
|
57
|
+
Notes:
|
|
58
|
+
Table name is read from config.extension_config["litestar"]["session_table"].
|
|
59
|
+
"""
|
|
60
|
+
super().__init__(config)
|
|
61
|
+
|
|
62
|
+
def _get_create_table_sql(self) -> str:
|
|
63
|
+
"""Get DuckDB CREATE TABLE SQL.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
SQL statement to create the sessions table with proper indexes.
|
|
67
|
+
|
|
68
|
+
Notes:
|
|
69
|
+
- Uses TIMESTAMP type for expires_at (DuckDB native datetime type)
|
|
70
|
+
- TIMESTAMP supports ISO 8601 format and direct comparisons
|
|
71
|
+
- Columnar storage makes this efficient for analytical queries
|
|
72
|
+
- DuckDB does not support partial indexes, so full index is created
|
|
73
|
+
"""
|
|
74
|
+
return f"""
|
|
75
|
+
CREATE TABLE IF NOT EXISTS {self._table_name} (
|
|
76
|
+
session_id VARCHAR PRIMARY KEY,
|
|
77
|
+
data BLOB NOT NULL,
|
|
78
|
+
expires_at TIMESTAMP,
|
|
79
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
80
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
81
|
+
);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_{self._table_name}_expires_at
|
|
83
|
+
ON {self._table_name}(expires_at);
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def _get_drop_table_sql(self) -> "list[str]":
|
|
87
|
+
"""Get DuckDB DROP TABLE SQL statements.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of SQL statements to drop indexes and table.
|
|
91
|
+
"""
|
|
92
|
+
return [f"DROP INDEX IF EXISTS idx_{self._table_name}_expires_at", f"DROP TABLE IF EXISTS {self._table_name}"]
|
|
93
|
+
|
|
94
|
+
def _datetime_to_timestamp(self, dt: "datetime | None") -> "str | None":
|
|
95
|
+
"""Convert datetime to ISO 8601 string for DuckDB TIMESTAMP storage.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
dt: Datetime to convert (must be UTC-aware).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
ISO 8601 formatted string, or None if dt is None.
|
|
102
|
+
|
|
103
|
+
Notes:
|
|
104
|
+
DuckDB's TIMESTAMP type accepts ISO 8601 format strings.
|
|
105
|
+
This enables efficient storage and comparison operations.
|
|
106
|
+
"""
|
|
107
|
+
if dt is None:
|
|
108
|
+
return None
|
|
109
|
+
return dt.isoformat()
|
|
110
|
+
|
|
111
|
+
def _timestamp_to_datetime(self, ts: "str | datetime | None") -> "datetime | None":
|
|
112
|
+
"""Convert TIMESTAMP string back to datetime.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
ts: ISO 8601 timestamp string or datetime object.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
UTC-aware datetime, or None if ts is None.
|
|
119
|
+
"""
|
|
120
|
+
if ts is None:
|
|
121
|
+
return None
|
|
122
|
+
if isinstance(ts, datetime):
|
|
123
|
+
if ts.tzinfo is None:
|
|
124
|
+
return ts.replace(tzinfo=timezone.utc)
|
|
125
|
+
return ts
|
|
126
|
+
dt = datetime.fromisoformat(ts)
|
|
127
|
+
if dt.tzinfo is None:
|
|
128
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
129
|
+
return dt
|
|
130
|
+
|
|
131
|
+
def _create_table(self) -> None:
|
|
132
|
+
"""Synchronous implementation of create_table."""
|
|
133
|
+
sql = self._get_create_table_sql()
|
|
134
|
+
with self._config.provide_session() as driver:
|
|
135
|
+
driver.execute_script(sql)
|
|
136
|
+
logger.debug("Created session table: %s", self._table_name)
|
|
137
|
+
|
|
138
|
+
async def create_table(self) -> None:
|
|
139
|
+
"""Create the session table if it doesn't exist."""
|
|
140
|
+
await async_(self._create_table)()
|
|
141
|
+
|
|
142
|
+
def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
|
|
143
|
+
"""Synchronous implementation of get."""
|
|
144
|
+
sql = f"""
|
|
145
|
+
SELECT data, expires_at FROM {self._table_name}
|
|
146
|
+
WHERE session_id = ?
|
|
147
|
+
AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
with self._config.provide_connection() as conn:
|
|
151
|
+
cursor = conn.execute(sql, (key,))
|
|
152
|
+
row = cursor.fetchone()
|
|
153
|
+
|
|
154
|
+
if row is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
data, expires_at_str = row
|
|
158
|
+
|
|
159
|
+
if renew_for is not None and expires_at_str is not None:
|
|
160
|
+
new_expires_at = self._calculate_expires_at(renew_for)
|
|
161
|
+
new_expires_at_str = self._datetime_to_timestamp(new_expires_at)
|
|
162
|
+
if new_expires_at_str is not None:
|
|
163
|
+
update_sql = f"""
|
|
164
|
+
UPDATE {self._table_name}
|
|
165
|
+
SET expires_at = ?, updated_at = NOW()
|
|
166
|
+
WHERE session_id = ?
|
|
167
|
+
"""
|
|
168
|
+
conn.execute(update_sql, (new_expires_at_str, key))
|
|
169
|
+
conn.commit()
|
|
170
|
+
|
|
171
|
+
return bytes(data)
|
|
172
|
+
|
|
173
|
+
async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
|
|
174
|
+
"""Get a session value by key.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
key: Session ID to retrieve.
|
|
178
|
+
renew_for: If given, renew the expiry time for this duration.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Session data as bytes if found and not expired, None otherwise.
|
|
182
|
+
"""
|
|
183
|
+
return await async_(self._get)(key, renew_for)
|
|
184
|
+
|
|
185
|
+
def _set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
|
|
186
|
+
"""Synchronous implementation of set.
|
|
187
|
+
|
|
188
|
+
Notes:
|
|
189
|
+
Stores expires_at as TIMESTAMP (ISO 8601 string) for DuckDB native support.
|
|
190
|
+
Uses INSERT ON CONFLICT instead of INSERT OR REPLACE to ensure all columns
|
|
191
|
+
are properly updated. created_at uses DEFAULT on insert, updated_at gets
|
|
192
|
+
current timestamp on both insert and update.
|
|
193
|
+
"""
|
|
194
|
+
data = self._value_to_bytes(value)
|
|
195
|
+
expires_at = self._calculate_expires_at(expires_in)
|
|
196
|
+
expires_at_str = self._datetime_to_timestamp(expires_at)
|
|
197
|
+
|
|
198
|
+
sql = f"""
|
|
199
|
+
INSERT INTO {self._table_name} (session_id, data, expires_at)
|
|
200
|
+
VALUES (?, ?, ?)
|
|
201
|
+
ON CONFLICT (session_id)
|
|
202
|
+
DO UPDATE SET
|
|
203
|
+
data = EXCLUDED.data,
|
|
204
|
+
expires_at = EXCLUDED.expires_at,
|
|
205
|
+
updated_at = NOW()
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
with self._config.provide_connection() as conn:
|
|
209
|
+
conn.execute(sql, (key, data, expires_at_str))
|
|
210
|
+
conn.commit()
|
|
211
|
+
|
|
212
|
+
async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
|
|
213
|
+
"""Store a session value.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
key: Session ID.
|
|
217
|
+
value: Session data.
|
|
218
|
+
expires_in: Time until expiration.
|
|
219
|
+
"""
|
|
220
|
+
await async_(self._set)(key, value, expires_in)
|
|
221
|
+
|
|
222
|
+
def _delete(self, key: str) -> None:
|
|
223
|
+
"""Synchronous implementation of delete."""
|
|
224
|
+
sql = f"DELETE FROM {self._table_name} WHERE session_id = ?"
|
|
225
|
+
|
|
226
|
+
with self._config.provide_connection() as conn:
|
|
227
|
+
conn.execute(sql, (key,))
|
|
228
|
+
conn.commit()
|
|
229
|
+
|
|
230
|
+
async def delete(self, key: str) -> None:
|
|
231
|
+
"""Delete a session by key.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
key: Session ID to delete.
|
|
235
|
+
"""
|
|
236
|
+
await async_(self._delete)(key)
|
|
237
|
+
|
|
238
|
+
def _delete_all(self) -> None:
|
|
239
|
+
"""Synchronous implementation of delete_all."""
|
|
240
|
+
sql = f"DELETE FROM {self._table_name}"
|
|
241
|
+
|
|
242
|
+
with self._config.provide_connection() as conn:
|
|
243
|
+
conn.execute(sql)
|
|
244
|
+
conn.commit()
|
|
245
|
+
logger.debug("Deleted all sessions from table: %s", self._table_name)
|
|
246
|
+
|
|
247
|
+
async def delete_all(self) -> None:
|
|
248
|
+
"""Delete all sessions from the store."""
|
|
249
|
+
await async_(self._delete_all)()
|
|
250
|
+
|
|
251
|
+
def _exists(self, key: str) -> bool:
|
|
252
|
+
"""Synchronous implementation of exists."""
|
|
253
|
+
sql = f"""
|
|
254
|
+
SELECT 1 FROM {self._table_name}
|
|
255
|
+
WHERE session_id = ?
|
|
256
|
+
AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
with self._config.provide_connection() as conn:
|
|
260
|
+
cursor = conn.execute(sql, (key,))
|
|
261
|
+
result = cursor.fetchone()
|
|
262
|
+
return result is not None
|
|
263
|
+
|
|
264
|
+
async def exists(self, key: str) -> bool:
|
|
265
|
+
"""Check if a session key exists and is not expired.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
key: Session ID to check.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if the session exists and is not expired.
|
|
272
|
+
"""
|
|
273
|
+
return await async_(self._exists)(key)
|
|
274
|
+
|
|
275
|
+
def _expires_in(self, key: str) -> "int | None":
|
|
276
|
+
"""Synchronous implementation of expires_in."""
|
|
277
|
+
sql = f"""
|
|
278
|
+
SELECT expires_at FROM {self._table_name}
|
|
279
|
+
WHERE session_id = ?
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
with self._config.provide_connection() as conn:
|
|
283
|
+
cursor = conn.execute(sql, (key,))
|
|
284
|
+
row = cursor.fetchone()
|
|
285
|
+
|
|
286
|
+
if row is None or row[0] is None:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
expires_at_str = row[0]
|
|
290
|
+
expires_at = self._timestamp_to_datetime(expires_at_str)
|
|
291
|
+
|
|
292
|
+
if expires_at is None:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
now = datetime.now(timezone.utc)
|
|
296
|
+
|
|
297
|
+
if expires_at <= now:
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
delta = expires_at - now
|
|
301
|
+
return int(delta.total_seconds())
|
|
302
|
+
|
|
303
|
+
async def expires_in(self, key: str) -> "int | None":
|
|
304
|
+
"""Get the time in seconds until the session expires.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
key: Session ID to check.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Seconds until expiration, or None if no expiry or key doesn't exist.
|
|
311
|
+
"""
|
|
312
|
+
return await async_(self._expires_in)(key)
|
|
313
|
+
|
|
314
|
+
def _delete_expired(self) -> int:
|
|
315
|
+
"""Synchronous implementation of delete_expired."""
|
|
316
|
+
sql = f"DELETE FROM {self._table_name} WHERE expires_at <= CURRENT_TIMESTAMP"
|
|
317
|
+
|
|
318
|
+
with self._config.provide_connection() as conn:
|
|
319
|
+
cursor = conn.execute(sql)
|
|
320
|
+
count = cursor.fetchone()
|
|
321
|
+
row_count = count[0] if count else 0
|
|
322
|
+
if row_count > 0:
|
|
323
|
+
logger.debug("Cleaned up %d expired sessions", row_count)
|
|
324
|
+
return row_count
|
|
325
|
+
|
|
326
|
+
async def delete_expired(self) -> int:
|
|
327
|
+
"""Delete all expired sessions.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Number of sessions deleted.
|
|
331
|
+
"""
|
|
332
|
+
return await async_(self._delete_expired)()
|
sqlspec/adapters/duckdb/pool.py
CHANGED
|
@@ -4,9 +4,9 @@ import logging
|
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
6
|
from contextlib import contextmanager, suppress
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Final,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
8
8
|
|
|
9
|
-
import duckdb
|
|
9
|
+
import duckdb
|
|
10
10
|
|
|
11
11
|
from sqlspec.adapters.duckdb._types import DuckDBConnection
|
|
12
12
|
|
|
@@ -51,9 +51,9 @@ class DuckDBConnectionPool:
|
|
|
51
51
|
self,
|
|
52
52
|
connection_config: "dict[str, Any]",
|
|
53
53
|
pool_recycle_seconds: int = POOL_RECYCLE,
|
|
54
|
-
extensions: "
|
|
55
|
-
secrets: "
|
|
56
|
-
on_connection_create: "
|
|
54
|
+
extensions: "list[dict[str, Any]] | None" = None,
|
|
55
|
+
secrets: "list[dict[str, Any]] | None" = None,
|
|
56
|
+
on_connection_create: "Callable[[DuckDBConnection], None] | None" = None,
|
|
57
57
|
**kwargs: Any,
|
|
58
58
|
) -> None:
|
|
59
59
|
"""Initialize the thread-local connection manager.
|
|
@@ -5,20 +5,63 @@ support and standardized datetime formatting.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import Any, Final
|
|
9
10
|
from uuid import UUID
|
|
10
11
|
|
|
11
12
|
from sqlspec.core.type_conversion import BaseTypeConverter, convert_uuid, format_datetime_rfc3339
|
|
12
13
|
|
|
14
|
+
DUCKDB_SPECIAL_CHARS: Final[frozenset[str]] = frozenset({"-", ":", "T", ".", "[", "{"})
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class DuckDBTypeConverter(BaseTypeConverter):
|
|
15
18
|
"""DuckDB-specific type conversion with native UUID support.
|
|
16
19
|
|
|
17
20
|
Extends the base TypeDetector with DuckDB-specific functionality
|
|
18
21
|
including native UUID handling and standardized datetime formatting.
|
|
22
|
+
Includes per-instance LRU cache for improved performance.
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
|
-
__slots__ = ()
|
|
25
|
+
__slots__ = ("_convert_cache", "_enable_uuid_conversion")
|
|
26
|
+
|
|
27
|
+
def __init__(self, cache_size: int = 5000, enable_uuid_conversion: bool = True) -> None:
|
|
28
|
+
"""Initialize converter with per-instance conversion cache.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
cache_size: Maximum number of string values to cache (default: 5000)
|
|
32
|
+
enable_uuid_conversion: Enable automatic UUID string conversion (default: True)
|
|
33
|
+
"""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self._enable_uuid_conversion = enable_uuid_conversion
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=cache_size)
|
|
38
|
+
def _cached_convert(value: str) -> Any:
|
|
39
|
+
if not value or not any(c in value for c in DUCKDB_SPECIAL_CHARS):
|
|
40
|
+
return value
|
|
41
|
+
detected_type = self.detect_type(value)
|
|
42
|
+
if detected_type:
|
|
43
|
+
if detected_type == "uuid" and not self._enable_uuid_conversion:
|
|
44
|
+
return value
|
|
45
|
+
try:
|
|
46
|
+
return self.convert_value(value, detected_type)
|
|
47
|
+
except Exception:
|
|
48
|
+
return value
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
self._convert_cache = _cached_convert
|
|
52
|
+
|
|
53
|
+
def convert_if_detected(self, value: Any) -> Any:
|
|
54
|
+
"""Convert string if special type detected (cached).
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: Value to potentially convert
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Converted value or original value
|
|
61
|
+
"""
|
|
62
|
+
if not isinstance(value, str):
|
|
63
|
+
return value
|
|
64
|
+
return self._convert_cache(value)
|
|
22
65
|
|
|
23
66
|
def handle_uuid(self, value: Any) -> Any:
|
|
24
67
|
"""Handle UUID conversion for DuckDB.
|
|
@@ -27,12 +70,12 @@ class DuckDBTypeConverter(BaseTypeConverter):
|
|
|
27
70
|
value: Value that might be a UUID.
|
|
28
71
|
|
|
29
72
|
Returns:
|
|
30
|
-
UUID object if value is UUID-like, original value otherwise.
|
|
73
|
+
UUID object if value is UUID-like and conversion enabled, original value otherwise.
|
|
31
74
|
"""
|
|
32
75
|
if isinstance(value, UUID):
|
|
33
|
-
return value
|
|
76
|
+
return value
|
|
34
77
|
|
|
35
|
-
if isinstance(value, str):
|
|
78
|
+
if isinstance(value, str) and self._enable_uuid_conversion:
|
|
36
79
|
detected_type = self.detect_type(value)
|
|
37
80
|
if detected_type == "uuid":
|
|
38
81
|
return convert_uuid(value)
|
|
@@ -59,23 +102,14 @@ class DuckDBTypeConverter(BaseTypeConverter):
|
|
|
59
102
|
Returns:
|
|
60
103
|
Converted value appropriate for DuckDB.
|
|
61
104
|
"""
|
|
62
|
-
# Handle UUIDs
|
|
63
105
|
if isinstance(value, (str, UUID)):
|
|
64
106
|
uuid_value = self.handle_uuid(value)
|
|
65
107
|
if isinstance(uuid_value, UUID):
|
|
66
108
|
return uuid_value
|
|
67
109
|
|
|
68
|
-
# Handle other string types
|
|
69
110
|
if isinstance(value, str):
|
|
70
|
-
|
|
71
|
-
if detected_type:
|
|
72
|
-
try:
|
|
73
|
-
return self.convert_value(value, detected_type)
|
|
74
|
-
except Exception:
|
|
75
|
-
# If conversion fails, return original value
|
|
76
|
-
return value
|
|
111
|
+
return self.convert_if_detected(value)
|
|
77
112
|
|
|
78
|
-
# Handle datetime formatting
|
|
79
113
|
if isinstance(value, datetime):
|
|
80
114
|
return self.format_datetime(value)
|
|
81
115
|
|
|
@@ -90,14 +124,10 @@ class DuckDBTypeConverter(BaseTypeConverter):
|
|
|
90
124
|
Returns:
|
|
91
125
|
Value ready for DuckDB parameter binding.
|
|
92
126
|
"""
|
|
93
|
-
# DuckDB can handle most Python types natively
|
|
94
127
|
converted = self.convert_duckdb_value(value)
|
|
95
|
-
|
|
96
|
-
# Ensure UUIDs are properly handled
|
|
97
128
|
if isinstance(converted, UUID):
|
|
98
|
-
return converted
|
|
99
|
-
|
|
129
|
+
return converted
|
|
100
130
|
return converted
|
|
101
131
|
|
|
102
132
|
|
|
103
|
-
__all__ = ("DuckDBTypeConverter"
|
|
133
|
+
__all__ = ("DUCKDB_SPECIAL_CHARS", "DuckDBTypeConverter")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Oracle NumPy vector type handlers for VECTOR data type support.
|
|
2
|
+
|
|
3
|
+
Provides automatic conversion between NumPy arrays and Oracle VECTOR types
|
|
4
|
+
via connection type handlers. Requires Oracle Database 23ai or higher.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import array
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from sqlspec.typing import NUMPY_INSTALLED
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from oracledb import AsyncConnection, AsyncCursor, Connection, Cursor
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"_input_type_handler",
|
|
18
|
+
"_output_type_handler",
|
|
19
|
+
"numpy_converter_in",
|
|
20
|
+
"numpy_converter_out",
|
|
21
|
+
"register_numpy_handlers",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DTYPE_TO_ARRAY_CODE: dict[str, str] = {"float64": "d", "float32": "f", "uint8": "B", "int8": "b"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def numpy_converter_in(value: Any) -> "array.array[Any]":
|
|
32
|
+
"""Convert NumPy array to Oracle array for VECTOR insertion.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
value: NumPy ndarray to convert.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Python array.array compatible with Oracle VECTOR type.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ImportError: If NumPy is not installed.
|
|
42
|
+
TypeError: If NumPy dtype is not supported for Oracle VECTOR.
|
|
43
|
+
"""
|
|
44
|
+
if not NUMPY_INSTALLED:
|
|
45
|
+
msg = "NumPy is not installed - cannot convert vectors"
|
|
46
|
+
raise ImportError(msg)
|
|
47
|
+
|
|
48
|
+
dtype_name = value.dtype.name
|
|
49
|
+
array_code = DTYPE_TO_ARRAY_CODE.get(dtype_name)
|
|
50
|
+
|
|
51
|
+
if not array_code:
|
|
52
|
+
supported = ", ".join(DTYPE_TO_ARRAY_CODE.keys())
|
|
53
|
+
msg = f"Unsupported NumPy dtype for Oracle VECTOR: {dtype_name}. Supported: {supported}"
|
|
54
|
+
raise TypeError(msg)
|
|
55
|
+
|
|
56
|
+
return array.array(array_code, value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def numpy_converter_out(value: "array.array[Any]") -> Any:
|
|
60
|
+
"""Convert Oracle array to NumPy array for VECTOR retrieval.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
value: Oracle array.array from VECTOR column.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
NumPy ndarray with appropriate dtype, or original value if NumPy not installed.
|
|
67
|
+
"""
|
|
68
|
+
if not NUMPY_INSTALLED:
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
import numpy as np
|
|
72
|
+
|
|
73
|
+
return np.array(value, copy=True, dtype=value.typecode)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _input_type_handler(cursor: "Cursor | AsyncCursor", value: Any, arraysize: int) -> Any:
|
|
77
|
+
"""Oracle input type handler for NumPy arrays.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
cursor: Oracle cursor (sync or async).
|
|
81
|
+
value: Value being inserted.
|
|
82
|
+
arraysize: Array size for the cursor variable.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Cursor variable with NumPy converter if value is ndarray, None otherwise.
|
|
86
|
+
"""
|
|
87
|
+
if not NUMPY_INSTALLED:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
import numpy as np
|
|
91
|
+
import oracledb
|
|
92
|
+
|
|
93
|
+
if isinstance(value, np.ndarray):
|
|
94
|
+
return cursor.var(oracledb.DB_TYPE_VECTOR, arraysize=arraysize, inconverter=numpy_converter_in)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _output_type_handler(cursor: "Cursor | AsyncCursor", metadata: Any) -> Any:
|
|
99
|
+
"""Oracle output type handler for VECTOR columns.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
cursor: Oracle cursor (sync or async).
|
|
103
|
+
metadata: Column metadata from Oracle.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Cursor variable with NumPy converter if column is VECTOR, None otherwise.
|
|
107
|
+
"""
|
|
108
|
+
if not NUMPY_INSTALLED:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
import oracledb
|
|
112
|
+
|
|
113
|
+
if metadata.type_code is oracledb.DB_TYPE_VECTOR:
|
|
114
|
+
return cursor.var(metadata.type_code, arraysize=cursor.arraysize, outconverter=numpy_converter_out)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def register_numpy_handlers(connection: "Connection | AsyncConnection") -> None:
|
|
119
|
+
"""Register NumPy type handlers on Oracle connection.
|
|
120
|
+
|
|
121
|
+
Enables automatic conversion between NumPy arrays and Oracle VECTOR types.
|
|
122
|
+
Works for both sync and async connections.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
connection: Oracle connection (sync or async).
|
|
126
|
+
"""
|
|
127
|
+
if not NUMPY_INSTALLED:
|
|
128
|
+
logger.debug("NumPy not installed - skipping vector type handlers")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
connection.inputtypehandler = _input_type_handler
|
|
132
|
+
connection.outputtypehandler = _output_type_handler
|
|
133
|
+
logger.debug("Registered NumPy vector type handlers on Oracle connection")
|
|
@@ -3,19 +3,37 @@ from typing import TYPE_CHECKING
|
|
|
3
3
|
from oracledb import AsyncConnection, Connection
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from oracledb import DB_TYPE_VECTOR
|
|
6
9
|
from oracledb.pool import AsyncConnectionPool, ConnectionPool
|
|
7
|
-
from typing_extensions import TypeAlias
|
|
8
10
|
|
|
9
11
|
OracleSyncConnection: TypeAlias = Connection
|
|
10
12
|
OracleAsyncConnection: TypeAlias = AsyncConnection
|
|
11
13
|
OracleSyncConnectionPool: TypeAlias = ConnectionPool
|
|
12
14
|
OracleAsyncConnectionPool: TypeAlias = AsyncConnectionPool
|
|
15
|
+
OracleVectorType: TypeAlias = int
|
|
13
16
|
else:
|
|
14
17
|
from oracledb.pool import AsyncConnectionPool, ConnectionPool
|
|
15
18
|
|
|
19
|
+
try:
|
|
20
|
+
from oracledb import DB_TYPE_VECTOR
|
|
21
|
+
|
|
22
|
+
OracleVectorType = int
|
|
23
|
+
except ImportError:
|
|
24
|
+
DB_TYPE_VECTOR = None
|
|
25
|
+
OracleVectorType = int
|
|
26
|
+
|
|
16
27
|
OracleSyncConnection = Connection
|
|
17
28
|
OracleAsyncConnection = AsyncConnection
|
|
18
29
|
OracleSyncConnectionPool = ConnectionPool
|
|
19
30
|
OracleAsyncConnectionPool = AsyncConnectionPool
|
|
20
31
|
|
|
21
|
-
__all__ = (
|
|
32
|
+
__all__ = (
|
|
33
|
+
"DB_TYPE_VECTOR",
|
|
34
|
+
"OracleAsyncConnection",
|
|
35
|
+
"OracleAsyncConnectionPool",
|
|
36
|
+
"OracleSyncConnection",
|
|
37
|
+
"OracleSyncConnectionPool",
|
|
38
|
+
"OracleVectorType",
|
|
39
|
+
)
|