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,572 @@
|
|
|
1
|
+
"""SQLite sync ADK store for Google Agent Development Kit session/event storage."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from sqlspec.extensions.adk import BaseAsyncADKStore, EventRecord, SessionRecord
|
|
7
|
+
from sqlspec.utils.logging import get_logger
|
|
8
|
+
from sqlspec.utils.serializers import from_json, to_json
|
|
9
|
+
from sqlspec.utils.sync_tools import async_
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sqlspec.adapters.sqlite.config import SqliteConfig
|
|
13
|
+
|
|
14
|
+
logger = get_logger("adapters.sqlite.adk.store")
|
|
15
|
+
|
|
16
|
+
SECONDS_PER_DAY = 86400.0
|
|
17
|
+
JULIAN_EPOCH = 2440587.5
|
|
18
|
+
|
|
19
|
+
__all__ = ("SqliteADKStore",)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _datetime_to_julian(dt: datetime) -> float:
|
|
23
|
+
"""Convert datetime to Julian Day number for SQLite storage.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
dt: Datetime to convert (must be UTC-aware).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Julian Day number as REAL.
|
|
30
|
+
|
|
31
|
+
Notes:
|
|
32
|
+
Julian Day number is days since November 24, 4714 BCE (proleptic Gregorian).
|
|
33
|
+
This enables direct comparison with julianday('now') in SQL queries.
|
|
34
|
+
"""
|
|
35
|
+
if dt.tzinfo is None:
|
|
36
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
37
|
+
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
38
|
+
delta_days = (dt - epoch).total_seconds() / SECONDS_PER_DAY
|
|
39
|
+
return JULIAN_EPOCH + delta_days
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _julian_to_datetime(julian: float) -> datetime:
|
|
43
|
+
"""Convert Julian Day number back to datetime.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
julian: Julian Day number.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
UTC-aware datetime.
|
|
50
|
+
"""
|
|
51
|
+
days_since_epoch = julian - JULIAN_EPOCH
|
|
52
|
+
timestamp = days_since_epoch * SECONDS_PER_DAY
|
|
53
|
+
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _to_sqlite_bool(value: "bool | None") -> "int | None":
|
|
57
|
+
"""Convert Python bool to SQLite INTEGER.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: Boolean value or None.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
1 for True, 0 for False, None for None.
|
|
64
|
+
"""
|
|
65
|
+
if value is None:
|
|
66
|
+
return None
|
|
67
|
+
return 1 if value else 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _from_sqlite_bool(value: "int | None") -> "bool | None":
|
|
71
|
+
"""Convert SQLite INTEGER to Python bool.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
value: Integer value (0/1) or None.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True for 1, False for 0, None for None.
|
|
78
|
+
"""
|
|
79
|
+
if value is None:
|
|
80
|
+
return None
|
|
81
|
+
return bool(value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SqliteADKStore(BaseAsyncADKStore["SqliteConfig"]):
|
|
85
|
+
"""SQLite ADK store using synchronous SQLite driver.
|
|
86
|
+
|
|
87
|
+
Implements session and event storage for Google Agent Development Kit
|
|
88
|
+
using SQLite via the synchronous sqlite3 driver. Uses Litestar's sync_to_thread
|
|
89
|
+
utility to provide an async interface compatible with the Store protocol.
|
|
90
|
+
|
|
91
|
+
Provides:
|
|
92
|
+
- Session state management with JSON storage (as TEXT)
|
|
93
|
+
- Event history tracking with BLOB-serialized actions
|
|
94
|
+
- Julian Day timestamps (REAL) for efficient date operations
|
|
95
|
+
- Foreign key constraints with cascade delete
|
|
96
|
+
- Efficient upserts using INSERT OR REPLACE
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
config: SqliteConfig instance with extension_config["adk"] settings.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
from sqlspec.adapters.sqlite import SqliteConfig
|
|
103
|
+
from sqlspec.adapters.sqlite.adk import SqliteADKStore
|
|
104
|
+
|
|
105
|
+
config = SqliteConfig(
|
|
106
|
+
database=":memory:",
|
|
107
|
+
extension_config={
|
|
108
|
+
"adk": {
|
|
109
|
+
"session_table": "my_sessions",
|
|
110
|
+
"events_table": "my_events",
|
|
111
|
+
"owner_id_column": "tenant_id INTEGER REFERENCES tenants(id) ON DELETE CASCADE"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
store = SqliteADKStore(config)
|
|
116
|
+
await store.create_tables()
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
- JSON stored as TEXT with SQLSpec serializers (msgspec/orjson/stdlib)
|
|
120
|
+
- BOOLEAN as INTEGER (0/1, with None for NULL)
|
|
121
|
+
- Timestamps as REAL (Julian day: julianday('now'))
|
|
122
|
+
- BLOB for pre-serialized actions from Google ADK
|
|
123
|
+
- PRAGMA foreign_keys = ON (enable per connection)
|
|
124
|
+
- Configuration is read from config.extension_config["adk"]
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
__slots__ = ()
|
|
128
|
+
|
|
129
|
+
def __init__(self, config: "SqliteConfig") -> None:
|
|
130
|
+
"""Initialize SQLite ADK store.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
config: SqliteConfig instance.
|
|
134
|
+
|
|
135
|
+
Notes:
|
|
136
|
+
Configuration is read from config.extension_config["adk"]:
|
|
137
|
+
- session_table: Sessions table name (default: "adk_sessions")
|
|
138
|
+
- events_table: Events table name (default: "adk_events")
|
|
139
|
+
- owner_id_column: Optional owner FK column DDL (default: None)
|
|
140
|
+
"""
|
|
141
|
+
super().__init__(config)
|
|
142
|
+
|
|
143
|
+
def _get_create_sessions_table_sql(self) -> str:
|
|
144
|
+
"""Get SQLite CREATE TABLE SQL for sessions.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
SQL statement to create adk_sessions table with indexes.
|
|
148
|
+
|
|
149
|
+
Notes:
|
|
150
|
+
- TEXT for IDs, names, and JSON state
|
|
151
|
+
- REAL for Julian Day timestamps
|
|
152
|
+
- Optional owner ID column for multi-tenant scenarios
|
|
153
|
+
- Composite index on (app_name, user_id)
|
|
154
|
+
- Index on update_time DESC for recent session queries
|
|
155
|
+
"""
|
|
156
|
+
owner_id_line = ""
|
|
157
|
+
if self._owner_id_column_ddl:
|
|
158
|
+
owner_id_line = f",\n {self._owner_id_column_ddl}"
|
|
159
|
+
|
|
160
|
+
return f"""
|
|
161
|
+
CREATE TABLE IF NOT EXISTS {self._session_table} (
|
|
162
|
+
id TEXT PRIMARY KEY,
|
|
163
|
+
app_name TEXT NOT NULL,
|
|
164
|
+
user_id TEXT NOT NULL{owner_id_line},
|
|
165
|
+
state TEXT NOT NULL DEFAULT '{{}}',
|
|
166
|
+
create_time REAL NOT NULL,
|
|
167
|
+
update_time REAL NOT NULL
|
|
168
|
+
);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_{self._session_table}_app_user
|
|
170
|
+
ON {self._session_table}(app_name, user_id);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_{self._session_table}_update_time
|
|
172
|
+
ON {self._session_table}(update_time DESC);
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def _get_create_events_table_sql(self) -> str:
|
|
176
|
+
"""Get SQLite CREATE TABLE SQL for events.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
SQL statement to create adk_events table with indexes.
|
|
180
|
+
|
|
181
|
+
Notes:
|
|
182
|
+
- TEXT for IDs, strings, and JSON content
|
|
183
|
+
- BLOB for pickled actions
|
|
184
|
+
- INTEGER for booleans (0/1/NULL)
|
|
185
|
+
- REAL for Julian Day timestamps
|
|
186
|
+
- Foreign key to sessions with CASCADE delete
|
|
187
|
+
- Index on (session_id, timestamp ASC)
|
|
188
|
+
"""
|
|
189
|
+
return f"""
|
|
190
|
+
CREATE TABLE IF NOT EXISTS {self._events_table} (
|
|
191
|
+
id TEXT PRIMARY KEY,
|
|
192
|
+
session_id TEXT NOT NULL,
|
|
193
|
+
app_name TEXT NOT NULL,
|
|
194
|
+
user_id TEXT NOT NULL,
|
|
195
|
+
invocation_id TEXT NOT NULL,
|
|
196
|
+
author TEXT NOT NULL,
|
|
197
|
+
actions BLOB NOT NULL,
|
|
198
|
+
long_running_tool_ids_json TEXT,
|
|
199
|
+
branch TEXT,
|
|
200
|
+
timestamp REAL NOT NULL,
|
|
201
|
+
content TEXT,
|
|
202
|
+
grounding_metadata TEXT,
|
|
203
|
+
custom_metadata TEXT,
|
|
204
|
+
partial INTEGER,
|
|
205
|
+
turn_complete INTEGER,
|
|
206
|
+
interrupted INTEGER,
|
|
207
|
+
error_code TEXT,
|
|
208
|
+
error_message TEXT,
|
|
209
|
+
FOREIGN KEY (session_id) REFERENCES {self._session_table}(id) ON DELETE CASCADE
|
|
210
|
+
);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_{self._events_table}_session
|
|
212
|
+
ON {self._events_table}(session_id, timestamp ASC);
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def _get_drop_tables_sql(self) -> "list[str]":
|
|
216
|
+
"""Get SQLite DROP TABLE SQL statements.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of SQL statements to drop tables and indexes.
|
|
220
|
+
|
|
221
|
+
Notes:
|
|
222
|
+
Order matters: drop events table (child) before sessions (parent).
|
|
223
|
+
SQLite automatically drops indexes when dropping tables.
|
|
224
|
+
"""
|
|
225
|
+
return [f"DROP TABLE IF EXISTS {self._events_table}", f"DROP TABLE IF EXISTS {self._session_table}"]
|
|
226
|
+
|
|
227
|
+
def _enable_foreign_keys(self, connection: Any) -> None:
|
|
228
|
+
"""Enable foreign key constraints for this connection.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
connection: SQLite connection.
|
|
232
|
+
|
|
233
|
+
Notes:
|
|
234
|
+
SQLite requires PRAGMA foreign_keys = ON per connection.
|
|
235
|
+
"""
|
|
236
|
+
connection.execute("PRAGMA foreign_keys = ON")
|
|
237
|
+
|
|
238
|
+
def _create_tables(self) -> None:
|
|
239
|
+
"""Synchronous implementation of create_tables."""
|
|
240
|
+
with self._config.provide_connection() as conn:
|
|
241
|
+
self._enable_foreign_keys(conn)
|
|
242
|
+
conn.executescript(self._get_create_sessions_table_sql())
|
|
243
|
+
conn.executescript(self._get_create_events_table_sql())
|
|
244
|
+
logger.debug("Created ADK tables: %s, %s", self._session_table, self._events_table)
|
|
245
|
+
|
|
246
|
+
async def create_tables(self) -> None:
|
|
247
|
+
"""Create both sessions and events tables if they don't exist."""
|
|
248
|
+
await async_(self._create_tables)()
|
|
249
|
+
|
|
250
|
+
def _create_session(
|
|
251
|
+
self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
|
|
252
|
+
) -> SessionRecord:
|
|
253
|
+
"""Synchronous implementation of create_session."""
|
|
254
|
+
now = datetime.now(timezone.utc)
|
|
255
|
+
now_julian = _datetime_to_julian(now)
|
|
256
|
+
state_json = to_json(state) if state else None
|
|
257
|
+
|
|
258
|
+
params: tuple[Any, ...]
|
|
259
|
+
if self._owner_id_column_name:
|
|
260
|
+
sql = f"""
|
|
261
|
+
INSERT INTO {self._session_table}
|
|
262
|
+
(id, app_name, user_id, {self._owner_id_column_name}, state, create_time, update_time)
|
|
263
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
264
|
+
"""
|
|
265
|
+
params = (session_id, app_name, user_id, owner_id, state_json, now_julian, now_julian)
|
|
266
|
+
else:
|
|
267
|
+
sql = f"""
|
|
268
|
+
INSERT INTO {self._session_table} (id, app_name, user_id, state, create_time, update_time)
|
|
269
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
270
|
+
"""
|
|
271
|
+
params = (session_id, app_name, user_id, state_json, now_julian, now_julian)
|
|
272
|
+
|
|
273
|
+
with self._config.provide_connection() as conn:
|
|
274
|
+
self._enable_foreign_keys(conn)
|
|
275
|
+
conn.execute(sql, params)
|
|
276
|
+
conn.commit()
|
|
277
|
+
|
|
278
|
+
return SessionRecord(
|
|
279
|
+
id=session_id, app_name=app_name, user_id=user_id, state=state, create_time=now, update_time=now
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
async def create_session(
|
|
283
|
+
self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
|
|
284
|
+
) -> SessionRecord:
|
|
285
|
+
"""Create a new session.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
session_id: Unique session identifier.
|
|
289
|
+
app_name: Application name.
|
|
290
|
+
user_id: User identifier.
|
|
291
|
+
state: Initial session state.
|
|
292
|
+
owner_id: Optional owner ID value for owner ID column.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Created session record.
|
|
296
|
+
|
|
297
|
+
Notes:
|
|
298
|
+
Uses Julian Day for create_time and update_time.
|
|
299
|
+
State is JSON-serialized before insertion.
|
|
300
|
+
If owner_id_column is configured, owner_id is inserted into that column.
|
|
301
|
+
"""
|
|
302
|
+
return await async_(self._create_session)(session_id, app_name, user_id, state, owner_id)
|
|
303
|
+
|
|
304
|
+
def _get_session(self, session_id: str) -> "SessionRecord | None":
|
|
305
|
+
"""Synchronous implementation of get_session."""
|
|
306
|
+
sql = f"""
|
|
307
|
+
SELECT id, app_name, user_id, state, create_time, update_time
|
|
308
|
+
FROM {self._session_table}
|
|
309
|
+
WHERE id = ?
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
with self._config.provide_connection() as conn:
|
|
313
|
+
self._enable_foreign_keys(conn)
|
|
314
|
+
cursor = conn.execute(sql, (session_id,))
|
|
315
|
+
row = cursor.fetchone()
|
|
316
|
+
|
|
317
|
+
if row is None:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
return SessionRecord(
|
|
321
|
+
id=row[0],
|
|
322
|
+
app_name=row[1],
|
|
323
|
+
user_id=row[2],
|
|
324
|
+
state=from_json(row[3]) if row[3] else {},
|
|
325
|
+
create_time=_julian_to_datetime(row[4]),
|
|
326
|
+
update_time=_julian_to_datetime(row[5]),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def get_session(self, session_id: str) -> "SessionRecord | None":
|
|
330
|
+
"""Get session by ID.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
session_id: Session identifier.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Session record or None if not found.
|
|
337
|
+
|
|
338
|
+
Notes:
|
|
339
|
+
SQLite returns Julian Day (REAL) for timestamps.
|
|
340
|
+
JSON is parsed from TEXT storage.
|
|
341
|
+
"""
|
|
342
|
+
return await async_(self._get_session)(session_id)
|
|
343
|
+
|
|
344
|
+
def _update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
|
|
345
|
+
"""Synchronous implementation of update_session_state."""
|
|
346
|
+
now_julian = _datetime_to_julian(datetime.now(timezone.utc))
|
|
347
|
+
state_json = to_json(state) if state else None
|
|
348
|
+
|
|
349
|
+
sql = f"""
|
|
350
|
+
UPDATE {self._session_table}
|
|
351
|
+
SET state = ?, update_time = ?
|
|
352
|
+
WHERE id = ?
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
with self._config.provide_connection() as conn:
|
|
356
|
+
self._enable_foreign_keys(conn)
|
|
357
|
+
conn.execute(sql, (state_json, now_julian, session_id))
|
|
358
|
+
conn.commit()
|
|
359
|
+
|
|
360
|
+
async def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
|
|
361
|
+
"""Update session state.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
session_id: Session identifier.
|
|
365
|
+
state: New state dictionary (replaces existing state).
|
|
366
|
+
|
|
367
|
+
Notes:
|
|
368
|
+
This replaces the entire state dictionary.
|
|
369
|
+
Updates update_time to current Julian Day.
|
|
370
|
+
"""
|
|
371
|
+
await async_(self._update_session_state)(session_id, state)
|
|
372
|
+
|
|
373
|
+
def _list_sessions(self, app_name: str, user_id: str) -> "list[SessionRecord]":
|
|
374
|
+
"""Synchronous implementation of list_sessions."""
|
|
375
|
+
sql = f"""
|
|
376
|
+
SELECT id, app_name, user_id, state, create_time, update_time
|
|
377
|
+
FROM {self._session_table}
|
|
378
|
+
WHERE app_name = ? AND user_id = ?
|
|
379
|
+
ORDER BY update_time DESC
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
with self._config.provide_connection() as conn:
|
|
383
|
+
self._enable_foreign_keys(conn)
|
|
384
|
+
cursor = conn.execute(sql, (app_name, user_id))
|
|
385
|
+
rows = cursor.fetchall()
|
|
386
|
+
|
|
387
|
+
return [
|
|
388
|
+
SessionRecord(
|
|
389
|
+
id=row[0],
|
|
390
|
+
app_name=row[1],
|
|
391
|
+
user_id=row[2],
|
|
392
|
+
state=from_json(row[3]) if row[3] else {},
|
|
393
|
+
create_time=_julian_to_datetime(row[4]),
|
|
394
|
+
update_time=_julian_to_datetime(row[5]),
|
|
395
|
+
)
|
|
396
|
+
for row in rows
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
async def list_sessions(self, app_name: str, user_id: str) -> "list[SessionRecord]":
|
|
400
|
+
"""List all sessions for a user in an app.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
app_name: Application name.
|
|
404
|
+
user_id: User identifier.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
List of session records ordered by update_time DESC.
|
|
408
|
+
|
|
409
|
+
Notes:
|
|
410
|
+
Uses composite index on (app_name, user_id).
|
|
411
|
+
"""
|
|
412
|
+
return await async_(self._list_sessions)(app_name, user_id)
|
|
413
|
+
|
|
414
|
+
def _delete_session(self, session_id: str) -> None:
|
|
415
|
+
"""Synchronous implementation of delete_session."""
|
|
416
|
+
sql = f"DELETE FROM {self._session_table} WHERE id = ?"
|
|
417
|
+
|
|
418
|
+
with self._config.provide_connection() as conn:
|
|
419
|
+
self._enable_foreign_keys(conn)
|
|
420
|
+
conn.execute(sql, (session_id,))
|
|
421
|
+
conn.commit()
|
|
422
|
+
|
|
423
|
+
async def delete_session(self, session_id: str) -> None:
|
|
424
|
+
"""Delete session and all associated events (cascade).
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
session_id: Session identifier.
|
|
428
|
+
|
|
429
|
+
Notes:
|
|
430
|
+
Foreign key constraint ensures events are cascade-deleted.
|
|
431
|
+
"""
|
|
432
|
+
await async_(self._delete_session)(session_id)
|
|
433
|
+
|
|
434
|
+
def _append_event(self, event_record: EventRecord) -> None:
|
|
435
|
+
"""Synchronous implementation of append_event."""
|
|
436
|
+
timestamp_julian = _datetime_to_julian(event_record["timestamp"])
|
|
437
|
+
|
|
438
|
+
content_json = to_json(event_record.get("content")) if event_record.get("content") else None
|
|
439
|
+
grounding_metadata_json = (
|
|
440
|
+
to_json(event_record.get("grounding_metadata")) if event_record.get("grounding_metadata") else None
|
|
441
|
+
)
|
|
442
|
+
custom_metadata_json = (
|
|
443
|
+
to_json(event_record.get("custom_metadata")) if event_record.get("custom_metadata") else None
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
partial_int = _to_sqlite_bool(event_record.get("partial"))
|
|
447
|
+
turn_complete_int = _to_sqlite_bool(event_record.get("turn_complete"))
|
|
448
|
+
interrupted_int = _to_sqlite_bool(event_record.get("interrupted"))
|
|
449
|
+
|
|
450
|
+
sql = f"""
|
|
451
|
+
INSERT INTO {self._events_table} (
|
|
452
|
+
id, session_id, app_name, user_id, invocation_id, author, actions,
|
|
453
|
+
long_running_tool_ids_json, branch, timestamp, content,
|
|
454
|
+
grounding_metadata, custom_metadata, partial, turn_complete,
|
|
455
|
+
interrupted, error_code, error_message
|
|
456
|
+
) VALUES (
|
|
457
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
458
|
+
)
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
with self._config.provide_connection() as conn:
|
|
462
|
+
self._enable_foreign_keys(conn)
|
|
463
|
+
conn.execute(
|
|
464
|
+
sql,
|
|
465
|
+
(
|
|
466
|
+
event_record["id"],
|
|
467
|
+
event_record["session_id"],
|
|
468
|
+
event_record["app_name"],
|
|
469
|
+
event_record["user_id"],
|
|
470
|
+
event_record["invocation_id"],
|
|
471
|
+
event_record["author"],
|
|
472
|
+
event_record["actions"],
|
|
473
|
+
event_record.get("long_running_tool_ids_json"),
|
|
474
|
+
event_record.get("branch"),
|
|
475
|
+
timestamp_julian,
|
|
476
|
+
content_json,
|
|
477
|
+
grounding_metadata_json,
|
|
478
|
+
custom_metadata_json,
|
|
479
|
+
partial_int,
|
|
480
|
+
turn_complete_int,
|
|
481
|
+
interrupted_int,
|
|
482
|
+
event_record.get("error_code"),
|
|
483
|
+
event_record.get("error_message"),
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
conn.commit()
|
|
487
|
+
|
|
488
|
+
async def append_event(self, event_record: EventRecord) -> None:
|
|
489
|
+
"""Append an event to a session.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
event_record: Event record to store.
|
|
493
|
+
|
|
494
|
+
Notes:
|
|
495
|
+
Uses Julian Day for timestamp.
|
|
496
|
+
JSON fields are serialized to TEXT.
|
|
497
|
+
Boolean fields converted to INTEGER (0/1/NULL).
|
|
498
|
+
"""
|
|
499
|
+
await async_(self._append_event)(event_record)
|
|
500
|
+
|
|
501
|
+
def _get_events(
|
|
502
|
+
self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
|
|
503
|
+
) -> "list[EventRecord]":
|
|
504
|
+
"""Synchronous implementation of get_events."""
|
|
505
|
+
where_clauses = ["session_id = ?"]
|
|
506
|
+
params: list[Any] = [session_id]
|
|
507
|
+
|
|
508
|
+
if after_timestamp is not None:
|
|
509
|
+
where_clauses.append("timestamp > ?")
|
|
510
|
+
params.append(_datetime_to_julian(after_timestamp))
|
|
511
|
+
|
|
512
|
+
where_clause = " AND ".join(where_clauses)
|
|
513
|
+
limit_clause = f" LIMIT {limit}" if limit else ""
|
|
514
|
+
|
|
515
|
+
sql = f"""
|
|
516
|
+
SELECT id, session_id, app_name, user_id, invocation_id, author, actions,
|
|
517
|
+
long_running_tool_ids_json, branch, timestamp, content,
|
|
518
|
+
grounding_metadata, custom_metadata, partial, turn_complete,
|
|
519
|
+
interrupted, error_code, error_message
|
|
520
|
+
FROM {self._events_table}
|
|
521
|
+
WHERE {where_clause}
|
|
522
|
+
ORDER BY timestamp ASC{limit_clause}
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
with self._config.provide_connection() as conn:
|
|
526
|
+
self._enable_foreign_keys(conn)
|
|
527
|
+
cursor = conn.execute(sql, params)
|
|
528
|
+
rows = cursor.fetchall()
|
|
529
|
+
|
|
530
|
+
return [
|
|
531
|
+
EventRecord(
|
|
532
|
+
id=row[0],
|
|
533
|
+
session_id=row[1],
|
|
534
|
+
app_name=row[2],
|
|
535
|
+
user_id=row[3],
|
|
536
|
+
invocation_id=row[4],
|
|
537
|
+
author=row[5],
|
|
538
|
+
actions=bytes(row[6]),
|
|
539
|
+
long_running_tool_ids_json=row[7],
|
|
540
|
+
branch=row[8],
|
|
541
|
+
timestamp=_julian_to_datetime(row[9]),
|
|
542
|
+
content=from_json(row[10]) if row[10] else None,
|
|
543
|
+
grounding_metadata=from_json(row[11]) if row[11] else None,
|
|
544
|
+
custom_metadata=from_json(row[12]) if row[12] else None,
|
|
545
|
+
partial=_from_sqlite_bool(row[13]),
|
|
546
|
+
turn_complete=_from_sqlite_bool(row[14]),
|
|
547
|
+
interrupted=_from_sqlite_bool(row[15]),
|
|
548
|
+
error_code=row[16],
|
|
549
|
+
error_message=row[17],
|
|
550
|
+
)
|
|
551
|
+
for row in rows
|
|
552
|
+
]
|
|
553
|
+
|
|
554
|
+
async def get_events(
|
|
555
|
+
self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
|
|
556
|
+
) -> "list[EventRecord]":
|
|
557
|
+
"""Get events for a session.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
session_id: Session identifier.
|
|
561
|
+
after_timestamp: Only return events after this time.
|
|
562
|
+
limit: Maximum number of events to return.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
List of event records ordered by timestamp ASC.
|
|
566
|
+
|
|
567
|
+
Notes:
|
|
568
|
+
Uses index on (session_id, timestamp ASC).
|
|
569
|
+
Parses JSON fields and converts BLOB actions to bytes.
|
|
570
|
+
Converts INTEGER booleans back to bool/None.
|
|
571
|
+
"""
|
|
572
|
+
return await async_(self._get_events)(session_id, after_timestamp, limit)
|