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.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
8
|
-
|
|
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 =
|
|
12
|
+
AsyncpgConnection: TypeAlias = Connection[Record] | PoolConnectionProxy[Record]
|
|
13
|
+
AsyncpgPool: TypeAlias = Pool[Record]
|
|
13
14
|
else:
|
|
14
|
-
|
|
15
|
+
from asyncpg import Pool
|
|
16
|
+
|
|
17
|
+
AsyncpgConnection = PoolConnectionProxy
|
|
18
|
+
AsyncpgPool = Pool
|
|
15
19
|
|
|
16
20
|
|
|
17
|
-
__all__ = ("AsyncpgConnection",)
|
|
21
|
+
__all__ = ("AsyncpgConnection", "AsyncpgPool")
|