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.

Files changed (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {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)()
@@ -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, Optional, cast
7
+ from typing import TYPE_CHECKING, Any, Final, cast
8
8
 
9
- import duckdb # type: ignore[import-untyped]
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: "Optional[list[dict[str, Any]]]" = None,
55
- secrets: "Optional[list[dict[str, Any]]]" = None,
56
- on_connection_create: "Optional[Callable[[DuckDBConnection], None]]" = None,
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 typing import Any
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 # DuckDB supports UUID natively
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
- detected_type = self.detect_type(value)
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 # DuckDB native UUID support
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__ = ("OracleAsyncConnection", "OracleAsyncConnectionPool", "OracleSyncConnection", "OracleSyncConnectionPool")
32
+ __all__ = (
33
+ "DB_TYPE_VECTOR",
34
+ "OracleAsyncConnection",
35
+ "OracleAsyncConnectionPool",
36
+ "OracleSyncConnection",
37
+ "OracleSyncConnectionPool",
38
+ "OracleVectorType",
39
+ )
@@ -0,0 +1,5 @@
1
+ """Oracle ADK extension integration."""
2
+
3
+ from sqlspec.adapters.oracledb.adk.store import OracleAsyncADKStore, OracleSyncADKStore
4
+
5
+ __all__ = ("OracleAsyncADKStore", "OracleSyncADKStore")