sqlspec 0.25.0__py3-none-any.whl → 0.27.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 (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -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 +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -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 +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  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 +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  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 +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  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 +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  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 +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  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 +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  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 +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  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 +102 -0
  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 +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  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 +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -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 +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,296 @@
1
+ """AsyncMy session store for Litestar integration."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import TYPE_CHECKING, Final
5
+
6
+ from sqlspec.extensions.litestar.store import BaseSQLSpecStore
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.adapters.asyncmy.config import AsyncmyConfig
11
+
12
+ logger = get_logger("adapters.asyncmy.litestar.store")
13
+
14
+ __all__ = ("AsyncmyStore",)
15
+
16
+ MYSQL_TABLE_NOT_FOUND_ERROR: Final = 1146
17
+
18
+
19
+ class AsyncmyStore(BaseSQLSpecStore["AsyncmyConfig"]):
20
+ """MySQL/MariaDB session store using AsyncMy driver.
21
+
22
+ Implements server-side session storage for Litestar using MySQL/MariaDB
23
+ via the AsyncMy driver. Provides efficient session management with:
24
+ - Native async MySQL operations
25
+ - UPSERT support using ON DUPLICATE KEY UPDATE
26
+ - Automatic expiration handling
27
+ - Efficient cleanup of expired sessions
28
+ - Timezone-aware expiration (stored as UTC in DATETIME)
29
+
30
+ Args:
31
+ config: AsyncmyConfig instance.
32
+
33
+ Example:
34
+ from sqlspec.adapters.asyncmy import AsyncmyConfig
35
+ from sqlspec.adapters.asyncmy.litestar.store import AsyncmyStore
36
+
37
+ config = AsyncmyConfig(pool_config={"host": "localhost", ...})
38
+ store = AsyncmyStore(config)
39
+ await store.create_table()
40
+
41
+ Notes:
42
+ MySQL DATETIME is timezone-naive, so UTC datetimes are stored without
43
+ timezone info and timezone conversion is handled in Python layer.
44
+ """
45
+
46
+ __slots__ = ()
47
+
48
+ def __init__(self, config: "AsyncmyConfig") -> None:
49
+ """Initialize AsyncMy session store.
50
+
51
+ Args:
52
+ config: AsyncmyConfig 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 MySQL CREATE TABLE SQL with optimized schema.
61
+
62
+ Returns:
63
+ SQL statement to create the sessions table with proper indexes.
64
+
65
+ Notes:
66
+ - Uses DATETIME(6) for microsecond precision timestamps
67
+ - MySQL doesn't have TIMESTAMPTZ, so we store UTC as timezone-naive
68
+ - LONGBLOB for large session data support (up to 4GB)
69
+ - InnoDB engine for ACID compliance and proper transaction support
70
+ - UTF8MB4 for full Unicode support (including emoji)
71
+ - Index on expires_at for efficient cleanup queries
72
+ - Auto-update of updated_at timestamp on row modification
73
+ - Table name is internally controlled, not user input (S608 suppressed)
74
+ """
75
+ return f"""
76
+ CREATE TABLE IF NOT EXISTS {self._table_name} (
77
+ session_id VARCHAR(255) PRIMARY KEY,
78
+ data LONGBLOB NOT NULL,
79
+ expires_at DATETIME(6),
80
+ created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
81
+ updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
82
+ INDEX idx_{self._table_name}_expires_at (expires_at)
83
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
84
+ """
85
+
86
+ def _get_drop_table_sql(self) -> "list[str]":
87
+ """Get MySQL/MariaDB DROP TABLE SQL statements.
88
+
89
+ Returns:
90
+ List of SQL statements to drop indexes and table.
91
+ """
92
+ return [
93
+ f"DROP INDEX idx_{self._table_name}_expires_at ON {self._table_name}",
94
+ f"DROP TABLE IF EXISTS {self._table_name}",
95
+ ]
96
+
97
+ async def create_table(self) -> None:
98
+ """Create the session table if it doesn't exist."""
99
+ sql = self._get_create_table_sql()
100
+ async with self._config.provide_session() as driver:
101
+ await driver.execute_script(sql)
102
+ logger.debug("Created session table: %s", self._table_name)
103
+
104
+ async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
105
+ """Get a session value by key.
106
+
107
+ Args:
108
+ key: Session ID to retrieve.
109
+ renew_for: If given, renew the expiry time for this duration.
110
+
111
+ Returns:
112
+ Session data as bytes if found and not expired, None otherwise.
113
+
114
+ Notes:
115
+ Uses UTC_TIMESTAMP(6) for microsecond precision current time in MySQL.
116
+ Compares expires_at as UTC datetime (timezone-naive in MySQL).
117
+ """
118
+ import asyncmy
119
+
120
+ sql = f"""
121
+ SELECT data, expires_at FROM {self._table_name}
122
+ WHERE session_id = %s
123
+ AND (expires_at IS NULL OR expires_at > UTC_TIMESTAMP(6))
124
+ """
125
+
126
+ try:
127
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
128
+ await cursor.execute(sql, (key,))
129
+ row = await cursor.fetchone()
130
+
131
+ if row is None:
132
+ return None
133
+
134
+ data_value, expires_at = row
135
+
136
+ if renew_for is not None and expires_at is not None:
137
+ new_expires_at = self._calculate_expires_at(renew_for)
138
+ if new_expires_at is not None:
139
+ naive_expires_at = new_expires_at.replace(tzinfo=None)
140
+ update_sql = f"""
141
+ UPDATE {self._table_name}
142
+ SET expires_at = %s, updated_at = UTC_TIMESTAMP(6)
143
+ WHERE session_id = %s
144
+ """
145
+ await cursor.execute(update_sql, (naive_expires_at, key))
146
+ await conn.commit()
147
+
148
+ return bytes(data_value)
149
+ except asyncmy.errors.ProgrammingError as e: # pyright: ignore
150
+ if "doesn't exist" in str(e) or e.args[0] == MYSQL_TABLE_NOT_FOUND_ERROR:
151
+ return None
152
+ raise
153
+
154
+ async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
155
+ """Store a session value.
156
+
157
+ Args:
158
+ key: Session ID.
159
+ value: Session data.
160
+ expires_in: Time until expiration.
161
+
162
+ Notes:
163
+ Uses INSERT ... ON DUPLICATE KEY UPDATE for efficient UPSERT.
164
+ Stores UTC datetime as timezone-naive DATETIME in MySQL.
165
+ Uses alias syntax (AS new) instead of deprecated VALUES() function.
166
+ """
167
+ data = self._value_to_bytes(value)
168
+ expires_at = self._calculate_expires_at(expires_in)
169
+ naive_expires_at = expires_at.replace(tzinfo=None) if expires_at else None
170
+
171
+ sql = f"""
172
+ INSERT INTO {self._table_name} (session_id, data, expires_at)
173
+ VALUES (%s, %s, %s) AS new
174
+ ON DUPLICATE KEY UPDATE
175
+ data = new.data,
176
+ expires_at = new.expires_at,
177
+ updated_at = UTC_TIMESTAMP(6)
178
+ """
179
+
180
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
181
+ await cursor.execute(sql, (key, data, naive_expires_at))
182
+ await conn.commit()
183
+
184
+ async def delete(self, key: str) -> None:
185
+ """Delete a session by key.
186
+
187
+ Args:
188
+ key: Session ID to delete.
189
+ """
190
+ sql = f"DELETE FROM {self._table_name} WHERE session_id = %s"
191
+
192
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
193
+ await cursor.execute(sql, (key,))
194
+ await conn.commit()
195
+
196
+ async def delete_all(self) -> None:
197
+ """Delete all sessions from the store."""
198
+ import asyncmy
199
+
200
+ sql = f"DELETE FROM {self._table_name}"
201
+
202
+ try:
203
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
204
+ await cursor.execute(sql)
205
+ await conn.commit()
206
+ logger.debug("Deleted all sessions from table: %s", self._table_name)
207
+ except asyncmy.errors.ProgrammingError as e: # pyright: ignore
208
+ if "doesn't exist" in str(e) or e.args[0] == MYSQL_TABLE_NOT_FOUND_ERROR:
209
+ logger.debug("Table %s does not exist, skipping delete_all", self._table_name)
210
+ return
211
+ raise
212
+
213
+ async def exists(self, key: str) -> bool:
214
+ """Check if a session key exists and is not expired.
215
+
216
+ Args:
217
+ key: Session ID to check.
218
+
219
+ Returns:
220
+ True if the session exists and is not expired.
221
+
222
+ Notes:
223
+ Uses UTC_TIMESTAMP(6) for microsecond precision current time comparison.
224
+ """
225
+ import asyncmy
226
+
227
+ sql = f"""
228
+ SELECT 1 FROM {self._table_name}
229
+ WHERE session_id = %s
230
+ AND (expires_at IS NULL OR expires_at > UTC_TIMESTAMP(6))
231
+ """
232
+
233
+ try:
234
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
235
+ await cursor.execute(sql, (key,))
236
+ result = await cursor.fetchone()
237
+ return result is not None
238
+ except asyncmy.errors.ProgrammingError as e: # pyright: ignore
239
+ if "doesn't exist" in str(e) or e.args[0] == MYSQL_TABLE_NOT_FOUND_ERROR:
240
+ return False
241
+ raise
242
+
243
+ async def expires_in(self, key: str) -> "int | None":
244
+ """Get the time in seconds until the session expires.
245
+
246
+ Args:
247
+ key: Session ID to check.
248
+
249
+ Returns:
250
+ Seconds until expiration, or None if no expiry or key doesn't exist.
251
+
252
+ Notes:
253
+ MySQL DATETIME is timezone-naive, but we treat it as UTC.
254
+ Compare against UTC now in Python layer for accuracy.
255
+ """
256
+ sql = f"""
257
+ SELECT expires_at FROM {self._table_name}
258
+ WHERE session_id = %s
259
+ """
260
+
261
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
262
+ await cursor.execute(sql, (key,))
263
+ row = await cursor.fetchone()
264
+
265
+ if row is None or row[0] is None:
266
+ return None
267
+
268
+ expires_at_naive = row[0]
269
+ expires_at_utc = expires_at_naive.replace(tzinfo=timezone.utc)
270
+ now = datetime.now(timezone.utc)
271
+
272
+ if expires_at_utc <= now:
273
+ return 0
274
+
275
+ delta = expires_at_utc - now
276
+ return int(delta.total_seconds())
277
+
278
+ async def delete_expired(self) -> int:
279
+ """Delete all expired sessions.
280
+
281
+ Returns:
282
+ Number of sessions deleted.
283
+
284
+ Notes:
285
+ Uses UTC_TIMESTAMP(6) for microsecond precision current time comparison.
286
+ ROW_COUNT() returns the number of affected rows.
287
+ """
288
+ sql = f"DELETE FROM {self._table_name} WHERE expires_at <= UTC_TIMESTAMP(6)"
289
+
290
+ async with self._config.provide_connection() as conn, conn.cursor() as cursor:
291
+ await cursor.execute(sql)
292
+ await conn.commit()
293
+ count: int = cursor.rowcount
294
+ if count > 0:
295
+ logger.debug("Cleaned up %d expired sessions", count)
296
+ return count
@@ -1,6 +1,6 @@
1
1
  """AsyncPG adapter for SQLSpec."""
2
2
 
3
- from sqlspec.adapters.asyncpg._types import AsyncpgConnection
3
+ from sqlspec.adapters.asyncpg._types import AsyncpgConnection, AsyncpgPool
4
4
  from sqlspec.adapters.asyncpg.config import AsyncpgConfig, AsyncpgConnectionConfig, AsyncpgPoolConfig
5
5
  from sqlspec.adapters.asyncpg.driver import (
6
6
  AsyncpgCursor,
@@ -16,6 +16,7 @@ __all__ = (
16
16
  "AsyncpgCursor",
17
17
  "AsyncpgDriver",
18
18
  "AsyncpgExceptionHandler",
19
+ "AsyncpgPool",
19
20
  "AsyncpgPoolConfig",
20
21
  "asyncpg_statement_config",
21
22
  )
@@ -0,0 +1,71 @@
1
+ """AsyncPG type handlers for JSON and pgvector support.
2
+
3
+ Provides automatic registration of JSON codecs and pgvector extension
4
+ for asyncpg connections. Supports custom JSON serializers/deserializers
5
+ and optional vector type support.
6
+ """
7
+
8
+ import logging
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from sqlspec.typing import PGVECTOR_INSTALLED
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable
15
+
16
+ from sqlspec.adapters.asyncpg._types import AsyncpgConnection
17
+
18
+ __all__ = ("register_json_codecs", "register_pgvector_support")
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ async def register_json_codecs(
24
+ connection: "AsyncpgConnection", encoder: "Callable[[Any], str]", decoder: "Callable[[str], Any]"
25
+ ) -> None:
26
+ """Register JSON type codecs on asyncpg connection.
27
+
28
+ Configures both JSON and JSONB types with custom serializer/deserializer
29
+ functions. This allows using custom JSON libraries like orjson or msgspec
30
+ for better performance.
31
+
32
+ Args:
33
+ connection: AsyncPG connection instance.
34
+ encoder: Function to serialize Python objects to JSON strings.
35
+ decoder: Function to deserialize JSON strings to Python objects.
36
+ """
37
+ try:
38
+ await connection.set_type_codec("json", encoder=encoder, decoder=decoder, schema="pg_catalog")
39
+ await connection.set_type_codec("jsonb", encoder=encoder, decoder=decoder, schema="pg_catalog")
40
+ logger.debug("Registered JSON type codecs on asyncpg connection")
41
+ except Exception:
42
+ logger.exception("Failed to register JSON type codecs")
43
+
44
+
45
+ async def register_pgvector_support(connection: "AsyncpgConnection") -> None:
46
+ """Register pgvector extension support on asyncpg connection.
47
+
48
+ Enables automatic conversion between Python vector types and PostgreSQL
49
+ VECTOR columns when the pgvector library is installed. Gracefully skips
50
+ if pgvector is not available.
51
+
52
+ Args:
53
+ connection: AsyncPG connection instance.
54
+ """
55
+ if not PGVECTOR_INSTALLED:
56
+ logger.debug("pgvector not installed - skipping vector type support")
57
+ return
58
+
59
+ try:
60
+ import pgvector.asyncpg
61
+
62
+ await pgvector.asyncpg.register_vector(connection)
63
+ logger.debug("Registered pgvector support on asyncpg connection")
64
+ except ValueError as exc:
65
+ message = str(exc).lower()
66
+ if "unknown type" in message and "vector" in message:
67
+ logger.debug("Skipping pgvector registration because extension is unavailable")
68
+ return
69
+ logger.exception("Failed to register pgvector support")
70
+ except Exception:
71
+ logger.exception("Failed to register pgvector support")
@@ -1,17 +1,21 @@
1
- from typing import TYPE_CHECKING, Union
1
+ from typing import TYPE_CHECKING
2
2
 
3
- from asyncpg import Connection
4
3
  from asyncpg.pool import PoolConnectionProxy
5
4
 
6
5
  if TYPE_CHECKING:
7
- from asyncpg import Record
8
- from typing_extensions import TypeAlias
6
+ from typing import TypeAlias
7
+
8
+ from asyncpg import Connection, Pool, Record
9
9
 
10
10
 
11
11
  if TYPE_CHECKING:
12
- AsyncpgConnection: TypeAlias = Union[Connection[Record], PoolConnectionProxy[Record]]
12
+ AsyncpgConnection: TypeAlias = Connection[Record] | PoolConnectionProxy[Record]
13
+ AsyncpgPool: TypeAlias = Pool[Record]
13
14
  else:
14
- AsyncpgConnection = Union[Connection, PoolConnectionProxy]
15
+ from asyncpg import Pool
16
+
17
+ AsyncpgConnection = PoolConnectionProxy
18
+ AsyncpgPool = Pool
15
19
 
16
20
 
17
- __all__ = ("AsyncpgConnection",)
21
+ __all__ = ("AsyncpgConnection", "AsyncpgPool")
@@ -0,0 +1,5 @@
1
+ """AsyncPG ADK store module."""
2
+
3
+ from sqlspec.adapters.asyncpg.adk.store import AsyncpgADKStore
4
+
5
+ __all__ = ("AsyncpgADKStore",)