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,318 @@
1
+ """SQLite 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.sqlite.config import SqliteConfig
12
+
13
+ logger = get_logger("adapters.sqlite.litestar.store")
14
+
15
+ SECONDS_PER_DAY = 86400.0
16
+ JULIAN_EPOCH = 2440587.5
17
+
18
+ __all__ = ("SQLiteStore",)
19
+
20
+
21
+ class SQLiteStore(BaseSQLSpecStore["SqliteConfig"]):
22
+ """SQLite session store using synchronous SQLite driver.
23
+
24
+ Implements server-side session storage for Litestar using SQLite
25
+ via the synchronous sqlite3 driver. Uses Litestar's sync_to_thread
26
+ utility to provide an async interface compatible with the Store protocol.
27
+
28
+ Provides efficient session management with:
29
+ - Sync operations wrapped for async compatibility
30
+ - INSERT OR REPLACE for UPSERT functionality
31
+ - Automatic expiration handling
32
+ - Efficient cleanup of expired sessions
33
+
34
+ Args:
35
+ config: SqliteConfig instance.
36
+
37
+ Example:
38
+ from sqlspec.adapters.sqlite import SqliteConfig
39
+ from sqlspec.adapters.sqlite.litestar.store import SQLiteStore
40
+
41
+ config = SqliteConfig(database=":memory:")
42
+ store = SQLiteStore(config)
43
+ await store.create_table()
44
+ """
45
+
46
+ __slots__ = ()
47
+
48
+ def __init__(self, config: "SqliteConfig") -> None:
49
+ """Initialize SQLite session store.
50
+
51
+ Args:
52
+ config: SqliteConfig instance.
53
+
54
+ Notes:
55
+ Table name is read from config.extension_config["litestar"]["session_table"].
56
+ """
57
+ super().__init__(config)
58
+
59
+ def _get_create_table_sql(self) -> str:
60
+ """Get SQLite CREATE TABLE SQL.
61
+
62
+ Returns:
63
+ SQL statement to create the sessions table with proper indexes.
64
+
65
+ Notes:
66
+ - Uses REAL type for expires_at (stores Julian Day number)
67
+ - Julian Day enables direct comparison with julianday('now')
68
+ - Partial index WHERE expires_at IS NOT NULL reduces index size
69
+ - This approach ensures the index is actually used by query optimizer
70
+ """
71
+ return f"""
72
+ CREATE TABLE IF NOT EXISTS {self._table_name} (
73
+ session_id TEXT PRIMARY KEY,
74
+ data BLOB NOT NULL,
75
+ expires_at REAL
76
+ );
77
+ CREATE INDEX IF NOT EXISTS idx_{self._table_name}_expires_at
78
+ ON {self._table_name}(expires_at) WHERE expires_at IS NOT NULL;
79
+ """
80
+
81
+ def _get_drop_table_sql(self) -> "list[str]":
82
+ """Get SQLite DROP TABLE SQL statements.
83
+
84
+ Returns:
85
+ List of SQL statements to drop indexes and table.
86
+ """
87
+ return [f"DROP INDEX IF EXISTS idx_{self._table_name}_expires_at", f"DROP TABLE IF EXISTS {self._table_name}"]
88
+
89
+ def _datetime_to_julian(self, dt: "datetime | None") -> "float | None":
90
+ """Convert datetime to Julian Day number for SQLite storage.
91
+
92
+ Args:
93
+ dt: Datetime to convert (must be UTC-aware).
94
+
95
+ Returns:
96
+ Julian Day number as REAL, or None if dt is None.
97
+
98
+ Notes:
99
+ Julian Day number is days since November 24, 4714 BCE (proleptic Gregorian).
100
+ This enables direct comparison with julianday('now') in SQL queries.
101
+ """
102
+ if dt is None:
103
+ return None
104
+
105
+ epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
106
+ delta_days = (dt - epoch).total_seconds() / SECONDS_PER_DAY
107
+ return JULIAN_EPOCH + delta_days
108
+
109
+ def _julian_to_datetime(self, julian: "float | None") -> "datetime | None":
110
+ """Convert Julian Day number back to datetime.
111
+
112
+ Args:
113
+ julian: Julian Day number.
114
+
115
+ Returns:
116
+ UTC-aware datetime, or None if julian is None.
117
+ """
118
+ if julian is None:
119
+ return None
120
+
121
+ days_since_epoch = julian - JULIAN_EPOCH
122
+ timestamp = days_since_epoch * SECONDS_PER_DAY
123
+ return datetime.fromtimestamp(timestamp, tz=timezone.utc)
124
+
125
+ def _create_table(self) -> None:
126
+ """Synchronous implementation of create_table."""
127
+ sql = self._get_create_table_sql()
128
+ with self._config.provide_session() as driver:
129
+ driver.execute_script(sql)
130
+ logger.debug("Created session table: %s", self._table_name)
131
+
132
+ async def create_table(self) -> None:
133
+ """Create the session table if it doesn't exist."""
134
+ await async_(self._create_table)()
135
+
136
+ def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
137
+ """Synchronous implementation of get."""
138
+ sql = f"""
139
+ SELECT data, expires_at FROM {self._table_name}
140
+ WHERE session_id = ?
141
+ AND (expires_at IS NULL OR julianday(expires_at) > julianday('now'))
142
+ """
143
+
144
+ with self._config.provide_connection() as conn:
145
+ cursor = conn.execute(sql, (key,))
146
+ row = cursor.fetchone()
147
+
148
+ if row is None:
149
+ return None
150
+
151
+ data, expires_at_julian = row
152
+
153
+ if renew_for is not None and expires_at_julian is not None:
154
+ new_expires_at = self._calculate_expires_at(renew_for)
155
+ new_expires_at_julian = self._datetime_to_julian(new_expires_at)
156
+ if new_expires_at_julian is not None:
157
+ update_sql = f"""
158
+ UPDATE {self._table_name}
159
+ SET expires_at = ?
160
+ WHERE session_id = ?
161
+ """
162
+ conn.execute(update_sql, (new_expires_at_julian, key))
163
+ conn.commit()
164
+
165
+ return bytes(data)
166
+
167
+ async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
168
+ """Get a session value by key.
169
+
170
+ Args:
171
+ key: Session ID to retrieve.
172
+ renew_for: If given, renew the expiry time for this duration.
173
+
174
+ Returns:
175
+ Session data as bytes if found and not expired, None otherwise.
176
+ """
177
+ return await async_(self._get)(key, renew_for)
178
+
179
+ def _set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
180
+ """Synchronous implementation of set.
181
+
182
+ Notes:
183
+ Stores expires_at as Julian Day number (REAL) for optimal index usage.
184
+ """
185
+ data = self._value_to_bytes(value)
186
+ expires_at = self._calculate_expires_at(expires_in)
187
+ expires_at_julian = self._datetime_to_julian(expires_at)
188
+
189
+ sql = f"""
190
+ INSERT OR REPLACE INTO {self._table_name} (session_id, data, expires_at)
191
+ VALUES (?, ?, ?)
192
+ """
193
+
194
+ with self._config.provide_connection() as conn:
195
+ conn.execute(sql, (key, data, expires_at_julian))
196
+ conn.commit()
197
+
198
+ async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
199
+ """Store a session value.
200
+
201
+ Args:
202
+ key: Session ID.
203
+ value: Session data.
204
+ expires_in: Time until expiration.
205
+ """
206
+ await async_(self._set)(key, value, expires_in)
207
+
208
+ def _delete(self, key: str) -> None:
209
+ """Synchronous implementation of delete."""
210
+ sql = f"DELETE FROM {self._table_name} WHERE session_id = ?"
211
+
212
+ with self._config.provide_connection() as conn:
213
+ conn.execute(sql, (key,))
214
+ conn.commit()
215
+
216
+ async def delete(self, key: str) -> None:
217
+ """Delete a session by key.
218
+
219
+ Args:
220
+ key: Session ID to delete.
221
+ """
222
+ await async_(self._delete)(key)
223
+
224
+ def _delete_all(self) -> None:
225
+ """Synchronous implementation of delete_all."""
226
+ sql = f"DELETE FROM {self._table_name}"
227
+
228
+ with self._config.provide_connection() as conn:
229
+ conn.execute(sql)
230
+ conn.commit()
231
+ logger.debug("Deleted all sessions from table: %s", self._table_name)
232
+
233
+ async def delete_all(self) -> None:
234
+ """Delete all sessions from the store."""
235
+ await async_(self._delete_all)()
236
+
237
+ def _exists(self, key: str) -> bool:
238
+ """Synchronous implementation of exists."""
239
+ sql = f"""
240
+ SELECT 1 FROM {self._table_name}
241
+ WHERE session_id = ?
242
+ AND (expires_at IS NULL OR julianday(expires_at) > julianday('now'))
243
+ """
244
+
245
+ with self._config.provide_connection() as conn:
246
+ cursor = conn.execute(sql, (key,))
247
+ result = cursor.fetchone()
248
+ return result is not None
249
+
250
+ async def exists(self, key: str) -> bool:
251
+ """Check if a session key exists and is not expired.
252
+
253
+ Args:
254
+ key: Session ID to check.
255
+
256
+ Returns:
257
+ True if the session exists and is not expired.
258
+ """
259
+ return await async_(self._exists)(key)
260
+
261
+ def _expires_in(self, key: str) -> "int | None":
262
+ """Synchronous implementation of expires_in."""
263
+ sql = f"""
264
+ SELECT expires_at FROM {self._table_name}
265
+ WHERE session_id = ?
266
+ """
267
+
268
+ with self._config.provide_connection() as conn:
269
+ cursor = conn.execute(sql, (key,))
270
+ row = cursor.fetchone()
271
+
272
+ if row is None or row[0] is None:
273
+ return None
274
+
275
+ expires_at_julian = row[0]
276
+ expires_at = self._julian_to_datetime(expires_at_julian)
277
+
278
+ if expires_at is None:
279
+ return None
280
+
281
+ now = datetime.now(timezone.utc)
282
+
283
+ if expires_at <= now:
284
+ return 0
285
+
286
+ delta = expires_at - now
287
+ return int(delta.total_seconds())
288
+
289
+ async def expires_in(self, key: str) -> "int | None":
290
+ """Get the time in seconds until the session expires.
291
+
292
+ Args:
293
+ key: Session ID to check.
294
+
295
+ Returns:
296
+ Seconds until expiration, or None if no expiry or key doesn't exist.
297
+ """
298
+ return await async_(self._expires_in)(key)
299
+
300
+ def _delete_expired(self) -> int:
301
+ """Synchronous implementation of delete_expired."""
302
+ sql = f"DELETE FROM {self._table_name} WHERE julianday(expires_at) <= julianday('now')"
303
+
304
+ with self._config.provide_connection() as conn:
305
+ cursor = conn.execute(sql)
306
+ conn.commit()
307
+ count = cursor.rowcount
308
+ if count > 0:
309
+ logger.debug("Cleaned up %d expired sessions", count)
310
+ return count
311
+
312
+ async def delete_expired(self) -> int:
313
+ """Delete all expired sessions.
314
+
315
+ Returns:
316
+ Number of sessions deleted.
317
+ """
318
+ return await async_(self._delete_expired)()
@@ -4,7 +4,7 @@ import contextlib
4
4
  import sqlite3
5
5
  import threading
6
6
  from contextlib import contextmanager
7
- from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast
7
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
8
8
 
9
9
  from typing_extensions import NotRequired
10
10
 
@@ -14,15 +14,15 @@ if TYPE_CHECKING:
14
14
  from collections.abc import Generator
15
15
 
16
16
 
17
- class SqliteConnectionParams(TypedDict, total=False):
17
+ class SqliteConnectionParams(TypedDict):
18
18
  """SQLite connection parameters."""
19
19
 
20
20
  database: NotRequired[str]
21
21
  timeout: NotRequired[float]
22
22
  detect_types: NotRequired[int]
23
- isolation_level: "NotRequired[Optional[str]]"
23
+ isolation_level: "NotRequired[str | None]"
24
24
  check_same_thread: NotRequired[bool]
25
- factory: "NotRequired[Optional[type[SqliteConnection]]]"
25
+ factory: "NotRequired[type[SqliteConnection] | None]"
26
26
  cached_statements: NotRequired[int]
27
27
  uri: NotRequired[bool]
28
28
 
@@ -62,7 +62,7 @@ class SqliteConnectionPool:
62
62
 
63
63
  if self._enable_optimizations:
64
64
  database = self._connection_parameters.get("database", ":memory:")
65
- is_memory = database == ":memory:" or database.startswith("file::memory:")
65
+ is_memory = database == ":memory:" or "mode=memory" in database
66
66
 
67
67
  if not is_memory:
68
68
  connection.execute("PRAGMA journal_mode = DELETE")
sqlspec/base.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import atexit
3
3
  from collections.abc import Awaitable, Coroutine
4
- from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
4
+ from typing import TYPE_CHECKING, Any, Union, cast, overload
5
5
 
6
6
  from sqlspec.config import (
7
7
  AsyncConfigT,
@@ -42,11 +42,11 @@ class SQLSpec:
42
42
 
43
43
  __slots__ = ("_configs", "_instance_cache_config", "_sql_loader")
44
44
 
45
- def __init__(self, *, loader: "Optional[SQLFileLoader]" = None) -> None:
45
+ def __init__(self, *, loader: "SQLFileLoader | None" = None) -> None:
46
46
  self._configs: dict[Any, DatabaseConfigProtocol[Any, Any, Any]] = {}
47
47
  atexit.register(self._cleanup_sync_pools)
48
- self._instance_cache_config: Optional[CacheConfig] = None
49
- self._sql_loader: Optional[SQLFileLoader] = loader
48
+ self._instance_cache_config: CacheConfig | None = None
49
+ self._sql_loader: SQLFileLoader | None = loader
50
50
 
51
51
  @staticmethod
52
52
  def _get_config_name(obj: Any) -> str:
@@ -117,7 +117,7 @@ class SQLSpec:
117
117
  def add_config(self, config: "AsyncConfigT") -> "type[AsyncConfigT]": # pyright: ignore[reportInvalidTypeVarUse]
118
118
  ...
119
119
 
120
- def add_config(self, config: "Union[SyncConfigT, AsyncConfigT]") -> "type[Union[SyncConfigT, AsyncConfigT]]": # pyright: ignore[reportInvalidTypeVarUse]
120
+ def add_config(self, config: "SyncConfigT | AsyncConfigT") -> "type[SyncConfigT | AsyncConfigT]": # pyright: ignore[reportInvalidTypeVarUse]
121
121
  """Add a configuration instance to the registry.
122
122
 
123
123
  Args:
@@ -139,7 +139,7 @@ class SQLSpec:
139
139
  def get_config(self, name: "type[AsyncConfigT]") -> "AsyncConfigT": ...
140
140
 
141
141
  def get_config(
142
- self, name: "Union[type[DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]], Any]"
142
+ self, name: "type[DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]] | Any"
143
143
  ) -> "DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]":
144
144
  """Retrieve a configuration instance by its type or a key.
145
145
 
@@ -161,6 +161,15 @@ class SQLSpec:
161
161
  logger.debug("Retrieved configuration: %s", self._get_config_name(name))
162
162
  return config
163
163
 
164
+ @property
165
+ def configs(self) -> "dict[type, DatabaseConfigProtocol[Any, Any, Any]]":
166
+ """Access the registry of database configurations.
167
+
168
+ Returns:
169
+ Dictionary mapping config types to config instances.
170
+ """
171
+ return self._configs
172
+
164
173
  @overload
165
174
  def get_connection(
166
175
  self,
@@ -195,7 +204,7 @@ class SQLSpec:
195
204
  "NoPoolAsyncConfig[ConnectionT, DriverT]",
196
205
  "AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
197
206
  ],
198
- ) -> "Union[ConnectionT, Awaitable[ConnectionT]]":
207
+ ) -> "ConnectionT | Awaitable[ConnectionT]":
199
208
  """Get a database connection for the specified configuration.
200
209
 
201
210
  Args:
@@ -248,7 +257,7 @@ class SQLSpec:
248
257
  "SyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
249
258
  "AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
250
259
  ],
251
- ) -> "Union[DriverT, Awaitable[DriverT]]":
260
+ ) -> "DriverT | Awaitable[DriverT]":
252
261
  """Get a database session (driver adapter) for the specified configuration.
253
262
 
254
263
  Args:
@@ -273,13 +282,23 @@ class SQLSpec:
273
282
  async def _create_driver_async() -> "DriverT":
274
283
  resolved_connection = await connection_obj # pyright: ignore
275
284
  return cast( # pyright: ignore
276
- "DriverT", config.driver_type(connection=resolved_connection)
285
+ "DriverT",
286
+ config.driver_type(
287
+ connection=resolved_connection,
288
+ statement_config=config.statement_config,
289
+ driver_features=config.driver_features,
290
+ ),
277
291
  )
278
292
 
279
293
  return _create_driver_async()
280
294
 
281
295
  return cast( # pyright: ignore
282
- "DriverT", config.driver_type(connection=connection_obj)
296
+ "DriverT",
297
+ config.driver_type(
298
+ connection=connection_obj,
299
+ statement_config=config.statement_config,
300
+ driver_features=config.driver_features,
301
+ ),
283
302
  )
284
303
 
285
304
  @overload
@@ -322,7 +341,7 @@ class SQLSpec:
322
341
  ],
323
342
  *args: Any,
324
343
  **kwargs: Any,
325
- ) -> "Union[AbstractContextManager[ConnectionT], AbstractAsyncContextManager[ConnectionT]]":
344
+ ) -> "AbstractContextManager[ConnectionT] | AbstractAsyncContextManager[ConnectionT]":
326
345
  """Create and provide a database connection from the specified configuration.
327
346
 
328
347
  Args:
@@ -383,7 +402,7 @@ class SQLSpec:
383
402
  ],
384
403
  *args: Any,
385
404
  **kwargs: Any,
386
- ) -> "Union[AbstractContextManager[DriverT], AbstractAsyncContextManager[DriverT]]":
405
+ ) -> "AbstractContextManager[DriverT] | AbstractAsyncContextManager[DriverT]":
387
406
  """Create and provide a database session from the specified configuration.
388
407
 
389
408
  Args:
@@ -407,17 +426,17 @@ class SQLSpec:
407
426
  @overload
408
427
  def get_pool(
409
428
  self,
410
- name: "Union[type[Union[NoPoolSyncConfig[ConnectionT, DriverT], NoPoolAsyncConfig[ConnectionT, DriverT]]], NoPoolSyncConfig[ConnectionT, DriverT], NoPoolAsyncConfig[ConnectionT, DriverT]]",
429
+ name: "type[NoPoolSyncConfig[ConnectionT, DriverT] | NoPoolAsyncConfig[ConnectionT, DriverT]] | NoPoolSyncConfig[ConnectionT, DriverT] | NoPoolAsyncConfig[ConnectionT, DriverT]",
411
430
  ) -> "None": ...
412
431
  @overload
413
432
  def get_pool(
414
433
  self,
415
- name: "Union[type[SyncDatabaseConfig[ConnectionT, PoolT, DriverT]], SyncDatabaseConfig[ConnectionT, PoolT, DriverT]]",
434
+ name: "type[SyncDatabaseConfig[ConnectionT, PoolT, DriverT]] | SyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
416
435
  ) -> "type[PoolT]": ...
417
436
  @overload
418
437
  def get_pool(
419
438
  self,
420
- name: "Union[type[AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]],AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]]",
439
+ name: "type[AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]] | AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
421
440
  ) -> "Awaitable[type[PoolT]]": ...
422
441
 
423
442
  def get_pool(
@@ -432,7 +451,7 @@ class SQLSpec:
432
451
  "SyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
433
452
  "AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
434
453
  ],
435
- ) -> "Union[type[PoolT], Awaitable[type[PoolT]], None]":
454
+ ) -> "type[PoolT] | Awaitable[type[PoolT]] | None":
436
455
  """Get the connection pool for the specified configuration.
437
456
 
438
457
  Args:
@@ -450,7 +469,7 @@ class SQLSpec:
450
469
 
451
470
  if config.supports_connection_pooling:
452
471
  logger.debug("Getting pool for config: %s", config_name, extra={"config_type": config_name})
453
- return cast("Union[type[PoolT], Awaitable[type[PoolT]]]", config.create_pool())
472
+ return cast("type[PoolT] | Awaitable[type[PoolT]]", config.create_pool())
454
473
 
455
474
  logger.debug("Config %s does not support connection pooling", config_name)
456
475
  return None
@@ -489,7 +508,7 @@ class SQLSpec:
489
508
  "NoPoolAsyncConfig[ConnectionT, DriverT]",
490
509
  "AsyncDatabaseConfig[ConnectionT, PoolT, DriverT]",
491
510
  ],
492
- ) -> "Optional[Awaitable[None]]":
511
+ ) -> "Awaitable[None] | None":
493
512
  """Close the connection pool for the specified configuration.
494
513
 
495
514
  Args:
@@ -552,12 +571,12 @@ class SQLSpec:
552
571
  @staticmethod
553
572
  def configure_cache(
554
573
  *,
555
- sql_cache_size: Optional[int] = None,
556
- fragment_cache_size: Optional[int] = None,
557
- optimized_cache_size: Optional[int] = None,
558
- sql_cache_enabled: Optional[bool] = None,
559
- fragment_cache_enabled: Optional[bool] = None,
560
- optimized_cache_enabled: Optional[bool] = None,
574
+ sql_cache_size: int | None = None,
575
+ fragment_cache_size: int | None = None,
576
+ optimized_cache_size: int | None = None,
577
+ sql_cache_enabled: bool | None = None,
578
+ fragment_cache_enabled: bool | None = None,
579
+ optimized_cache_enabled: bool | None = None,
561
580
  ) -> None:
562
581
  """Update cache configuration with partial values.
563
582
 
@@ -591,7 +610,7 @@ class SQLSpec:
591
610
  )
592
611
  )
593
612
 
594
- def load_sql_files(self, *paths: "Union[str, Path]") -> None:
613
+ def load_sql_files(self, *paths: "str | Path") -> None:
595
614
  """Load SQL files from paths or directories.
596
615
 
597
616
  Args:
@@ -605,7 +624,7 @@ class SQLSpec:
605
624
  self._sql_loader.load_sql(*paths)
606
625
  logger.debug("Loaded SQL files: %s", paths)
607
626
 
608
- def add_named_sql(self, name: str, sql: str, dialect: "Optional[str]" = None) -> None:
627
+ def add_named_sql(self, name: str, sql: str, dialect: "str | None" = None) -> None:
609
628
  """Add a named SQL query directly.
610
629
 
611
630
  Args:
@@ -24,21 +24,64 @@ from sqlspec.builder._ddl import (
24
24
  Truncate,
25
25
  )
26
26
  from sqlspec.builder._delete import Delete
27
+ from sqlspec.builder._dml import (
28
+ DeleteFromClauseMixin,
29
+ InsertFromSelectMixin,
30
+ InsertIntoClauseMixin,
31
+ InsertValuesMixin,
32
+ UpdateFromClauseMixin,
33
+ UpdateSetClauseMixin,
34
+ UpdateTableClauseMixin,
35
+ )
36
+ from sqlspec.builder._expression_wrappers import (
37
+ AggregateExpression,
38
+ ConversionExpression,
39
+ FunctionExpression,
40
+ MathExpression,
41
+ StringExpression,
42
+ )
43
+ from sqlspec.builder._factory import SQLFactory, sql
27
44
  from sqlspec.builder._insert import Insert
45
+ from sqlspec.builder._join import JoinBuilder
28
46
  from sqlspec.builder._merge import Merge
29
- from sqlspec.builder._select import Select
47
+ from sqlspec.builder._parsing_utils import (
48
+ extract_expression,
49
+ parse_column_expression,
50
+ parse_condition_expression,
51
+ parse_order_expression,
52
+ parse_table_expression,
53
+ to_expression,
54
+ )
55
+ from sqlspec.builder._select import (
56
+ Case,
57
+ CaseBuilder,
58
+ CommonTableExpressionMixin,
59
+ HavingClauseMixin,
60
+ LimitOffsetClauseMixin,
61
+ OrderByClauseMixin,
62
+ PivotClauseMixin,
63
+ ReturningClauseMixin,
64
+ Select,
65
+ SelectClauseMixin,
66
+ SetOperationMixin,
67
+ SubqueryBuilder,
68
+ UnpivotClauseMixin,
69
+ WhereClauseMixin,
70
+ WindowFunctionBuilder,
71
+ )
30
72
  from sqlspec.builder._update import Update
31
- from sqlspec.builder.mixins import WhereClauseMixin
32
- from sqlspec.builder.mixins._join_operations import JoinBuilder
33
- from sqlspec.builder.mixins._select_operations import Case, SubqueryBuilder, WindowFunctionBuilder
34
73
  from sqlspec.exceptions import SQLBuilderError
35
74
 
36
75
  __all__ = (
76
+ "AggregateExpression",
37
77
  "AlterTable",
38
78
  "Case",
79
+ "CaseBuilder",
39
80
  "Column",
40
81
  "ColumnExpression",
41
82
  "CommentOn",
83
+ "CommonTableExpressionMixin",
84
+ "ConversionExpression",
42
85
  "CreateIndex",
43
86
  "CreateMaterializedView",
44
87
  "CreateSchema",
@@ -47,22 +90,48 @@ __all__ = (
47
90
  "CreateView",
48
91
  "DDLBuilder",
49
92
  "Delete",
93
+ "DeleteFromClauseMixin",
50
94
  "DropIndex",
51
95
  "DropSchema",
52
96
  "DropTable",
53
97
  "DropView",
54
98
  "FunctionColumn",
99
+ "FunctionExpression",
100
+ "HavingClauseMixin",
55
101
  "Insert",
102
+ "InsertFromSelectMixin",
103
+ "InsertIntoClauseMixin",
104
+ "InsertValuesMixin",
56
105
  "JoinBuilder",
106
+ "LimitOffsetClauseMixin",
107
+ "MathExpression",
57
108
  "Merge",
109
+ "OrderByClauseMixin",
110
+ "PivotClauseMixin",
58
111
  "QueryBuilder",
59
112
  "RenameTable",
113
+ "ReturningClauseMixin",
60
114
  "SQLBuilderError",
115
+ "SQLFactory",
61
116
  "SafeQuery",
62
117
  "Select",
118
+ "SelectClauseMixin",
119
+ "SetOperationMixin",
120
+ "StringExpression",
63
121
  "SubqueryBuilder",
64
122
  "Truncate",
123
+ "UnpivotClauseMixin",
65
124
  "Update",
125
+ "UpdateFromClauseMixin",
126
+ "UpdateSetClauseMixin",
127
+ "UpdateTableClauseMixin",
66
128
  "WhereClauseMixin",
67
129
  "WindowFunctionBuilder",
130
+ "extract_expression",
131
+ "parse_column_expression",
132
+ "parse_condition_expression",
133
+ "parse_order_expression",
134
+ "parse_table_expression",
135
+ "sql",
136
+ "to_expression",
68
137
  )