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,553 @@
|
|
|
1
|
+
"""DuckDB ADK store for Google Agent Development Kit.
|
|
2
|
+
|
|
3
|
+
DuckDB is an OLAP database optimized for analytical queries. This adapter provides:
|
|
4
|
+
- Embedded session storage with zero-configuration setup
|
|
5
|
+
- Excellent performance for analytical queries on session data
|
|
6
|
+
- Native JSON type support for flexible state storage
|
|
7
|
+
- Perfect for development, testing, and analytical workloads
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
DuckDB is optimized for OLAP workloads and analytical queries. For highly
|
|
11
|
+
concurrent DML operations (frequent inserts/updates/deletes), consider
|
|
12
|
+
PostgreSQL or other OLTP-optimized databases.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
17
|
+
|
|
18
|
+
from sqlspec.extensions.adk import BaseSyncADKStore, EventRecord, SessionRecord
|
|
19
|
+
from sqlspec.utils.logging import get_logger
|
|
20
|
+
from sqlspec.utils.serializers import from_json, to_json
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sqlspec.adapters.duckdb.config import DuckDBConfig
|
|
24
|
+
|
|
25
|
+
logger = get_logger("adapters.duckdb.adk.store")
|
|
26
|
+
|
|
27
|
+
__all__ = ("DuckdbADKStore",)
|
|
28
|
+
|
|
29
|
+
DUCKDB_TABLE_NOT_FOUND_ERROR: Final = "does not exist"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DuckdbADKStore(BaseSyncADKStore["DuckDBConfig"]):
|
|
33
|
+
"""DuckDB ADK store for Google Agent Development Kit.
|
|
34
|
+
|
|
35
|
+
Implements session and event storage for Google Agent Development Kit
|
|
36
|
+
using DuckDB's synchronous driver. Provides:
|
|
37
|
+
- Session state management with native JSON type
|
|
38
|
+
- Event history tracking with BLOB-serialized actions
|
|
39
|
+
- Native TIMESTAMP type support
|
|
40
|
+
- Foreign key constraints (manual cascade in delete_session)
|
|
41
|
+
- Columnar storage for analytical queries
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: DuckDBConfig with extension_config["adk"] settings.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
from sqlspec.adapters.duckdb import DuckDBConfig
|
|
48
|
+
from sqlspec.adapters.duckdb.adk import DuckdbADKStore
|
|
49
|
+
|
|
50
|
+
config = DuckDBConfig(
|
|
51
|
+
database="sessions.ddb",
|
|
52
|
+
extension_config={
|
|
53
|
+
"adk": {
|
|
54
|
+
"session_table": "my_sessions",
|
|
55
|
+
"events_table": "my_events",
|
|
56
|
+
"owner_id_column": "tenant_id INTEGER REFERENCES tenants(id)"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
store = DuckdbADKStore(config)
|
|
61
|
+
store.create_tables()
|
|
62
|
+
|
|
63
|
+
session = store.create_session(
|
|
64
|
+
session_id="session-123",
|
|
65
|
+
app_name="my-app",
|
|
66
|
+
user_id="user-456",
|
|
67
|
+
state={"context": "conversation"}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Notes:
|
|
71
|
+
- Uses DuckDB native JSON type (not JSONB)
|
|
72
|
+
- TIMESTAMP for date/time storage with microsecond precision
|
|
73
|
+
- BLOB for binary actions data
|
|
74
|
+
- BOOLEAN native type support
|
|
75
|
+
- Columnar storage provides excellent analytical query performance
|
|
76
|
+
- DuckDB doesn't support CASCADE in foreign keys (manual cascade required)
|
|
77
|
+
- Optimized for OLAP workloads; for high-concurrency writes use PostgreSQL
|
|
78
|
+
- Configuration is read from config.extension_config["adk"]
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
__slots__ = ()
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: "DuckDBConfig") -> None:
|
|
84
|
+
"""Initialize DuckDB ADK store.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: DuckDBConfig instance.
|
|
88
|
+
|
|
89
|
+
Notes:
|
|
90
|
+
Configuration is read from config.extension_config["adk"]:
|
|
91
|
+
- session_table: Sessions table name (default: "adk_sessions")
|
|
92
|
+
- events_table: Events table name (default: "adk_events")
|
|
93
|
+
- owner_id_column: Optional owner FK column DDL (default: None)
|
|
94
|
+
"""
|
|
95
|
+
super().__init__(config)
|
|
96
|
+
|
|
97
|
+
def _get_create_sessions_table_sql(self) -> str:
|
|
98
|
+
"""Get DuckDB CREATE TABLE SQL for sessions.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
SQL statement to create adk_sessions table with indexes.
|
|
102
|
+
|
|
103
|
+
Notes:
|
|
104
|
+
- VARCHAR for IDs and names
|
|
105
|
+
- JSON type for state storage (DuckDB native)
|
|
106
|
+
- TIMESTAMP for create_time and update_time
|
|
107
|
+
- CURRENT_TIMESTAMP for defaults
|
|
108
|
+
- Optional owner ID column for multi-tenant scenarios
|
|
109
|
+
- Composite index on (app_name, user_id) for listing
|
|
110
|
+
- Index on update_time DESC for recent session queries
|
|
111
|
+
"""
|
|
112
|
+
owner_id_line = ""
|
|
113
|
+
if self._owner_id_column_ddl:
|
|
114
|
+
owner_id_line = f",\n {self._owner_id_column_ddl}"
|
|
115
|
+
|
|
116
|
+
return f"""
|
|
117
|
+
CREATE TABLE IF NOT EXISTS {self._session_table} (
|
|
118
|
+
id VARCHAR PRIMARY KEY,
|
|
119
|
+
app_name VARCHAR NOT NULL,
|
|
120
|
+
user_id VARCHAR NOT NULL{owner_id_line},
|
|
121
|
+
state JSON NOT NULL,
|
|
122
|
+
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
123
|
+
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
124
|
+
);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_{self._session_table}_app_user ON {self._session_table}(app_name, user_id);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_{self._session_table}_update_time ON {self._session_table}(update_time DESC);
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def _get_create_events_table_sql(self) -> str:
|
|
130
|
+
"""Get DuckDB CREATE TABLE SQL for events.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
SQL statement to create adk_events table with indexes.
|
|
134
|
+
|
|
135
|
+
Notes:
|
|
136
|
+
- VARCHAR for string fields
|
|
137
|
+
- BLOB for pickled actions
|
|
138
|
+
- JSON for content, grounding_metadata, custom_metadata, long_running_tool_ids_json
|
|
139
|
+
- BOOLEAN for flags
|
|
140
|
+
- Foreign key constraint (DuckDB doesn't support CASCADE)
|
|
141
|
+
- Index on (session_id, timestamp ASC) for ordered event retrieval
|
|
142
|
+
- Manual cascade delete required in delete_session method
|
|
143
|
+
"""
|
|
144
|
+
return f"""
|
|
145
|
+
CREATE TABLE IF NOT EXISTS {self._events_table} (
|
|
146
|
+
id VARCHAR PRIMARY KEY,
|
|
147
|
+
session_id VARCHAR NOT NULL,
|
|
148
|
+
app_name VARCHAR NOT NULL,
|
|
149
|
+
user_id VARCHAR NOT NULL,
|
|
150
|
+
invocation_id VARCHAR,
|
|
151
|
+
author VARCHAR,
|
|
152
|
+
actions BLOB,
|
|
153
|
+
long_running_tool_ids_json JSON,
|
|
154
|
+
branch VARCHAR,
|
|
155
|
+
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
156
|
+
content JSON,
|
|
157
|
+
grounding_metadata JSON,
|
|
158
|
+
custom_metadata JSON,
|
|
159
|
+
partial BOOLEAN,
|
|
160
|
+
turn_complete BOOLEAN,
|
|
161
|
+
interrupted BOOLEAN,
|
|
162
|
+
error_code VARCHAR,
|
|
163
|
+
error_message VARCHAR,
|
|
164
|
+
FOREIGN KEY (session_id) REFERENCES {self._session_table}(id)
|
|
165
|
+
);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_{self._events_table}_session ON {self._events_table}(session_id, timestamp ASC);
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def _get_drop_tables_sql(self) -> "list[str]":
|
|
170
|
+
"""Get DuckDB DROP TABLE SQL statements.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of SQL statements to drop tables and indexes.
|
|
174
|
+
|
|
175
|
+
Notes:
|
|
176
|
+
Order matters: drop events table (child) before sessions (parent).
|
|
177
|
+
DuckDB automatically drops indexes when dropping tables.
|
|
178
|
+
"""
|
|
179
|
+
return [f"DROP TABLE IF EXISTS {self._events_table}", f"DROP TABLE IF EXISTS {self._session_table}"]
|
|
180
|
+
|
|
181
|
+
def create_tables(self) -> None:
|
|
182
|
+
"""Create both sessions and events tables if they don't exist."""
|
|
183
|
+
with self._config.provide_connection() as conn:
|
|
184
|
+
conn.execute(self._get_create_sessions_table_sql())
|
|
185
|
+
conn.execute(self._get_create_events_table_sql())
|
|
186
|
+
logger.debug("Created ADK tables: %s, %s", self._session_table, self._events_table)
|
|
187
|
+
|
|
188
|
+
def create_session(
|
|
189
|
+
self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
|
|
190
|
+
) -> SessionRecord:
|
|
191
|
+
"""Create a new session.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
session_id: Unique session identifier.
|
|
195
|
+
app_name: Application name.
|
|
196
|
+
user_id: User identifier.
|
|
197
|
+
state: Initial session state.
|
|
198
|
+
owner_id: Optional owner ID value for owner_id_column (if configured).
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Created session record.
|
|
202
|
+
|
|
203
|
+
Notes:
|
|
204
|
+
Uses current UTC timestamp for create_time and update_time.
|
|
205
|
+
State is JSON-serialized using SQLSpec serializers.
|
|
206
|
+
"""
|
|
207
|
+
now = datetime.now(timezone.utc)
|
|
208
|
+
state_json = to_json(state)
|
|
209
|
+
|
|
210
|
+
params: tuple[Any, ...]
|
|
211
|
+
if self._owner_id_column_name:
|
|
212
|
+
sql = f"""
|
|
213
|
+
INSERT INTO {self._session_table}
|
|
214
|
+
(id, app_name, user_id, {self._owner_id_column_name}, state, create_time, update_time)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
216
|
+
"""
|
|
217
|
+
params = (session_id, app_name, user_id, owner_id, state_json, now, now)
|
|
218
|
+
else:
|
|
219
|
+
sql = f"""
|
|
220
|
+
INSERT INTO {self._session_table} (id, app_name, user_id, state, create_time, update_time)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
222
|
+
"""
|
|
223
|
+
params = (session_id, app_name, user_id, state_json, now, now)
|
|
224
|
+
|
|
225
|
+
with self._config.provide_connection() as conn:
|
|
226
|
+
conn.execute(sql, params)
|
|
227
|
+
conn.commit()
|
|
228
|
+
|
|
229
|
+
return SessionRecord(
|
|
230
|
+
id=session_id, app_name=app_name, user_id=user_id, state=state, create_time=now, update_time=now
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def get_session(self, session_id: str) -> "SessionRecord | None":
|
|
234
|
+
"""Get session by ID.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
session_id: Session identifier.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Session record or None if not found.
|
|
241
|
+
|
|
242
|
+
Notes:
|
|
243
|
+
DuckDB returns datetime objects for TIMESTAMP columns.
|
|
244
|
+
JSON is parsed from database storage.
|
|
245
|
+
"""
|
|
246
|
+
sql = f"""
|
|
247
|
+
SELECT id, app_name, user_id, state, create_time, update_time
|
|
248
|
+
FROM {self._session_table}
|
|
249
|
+
WHERE id = ?
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
with self._config.provide_connection() as conn:
|
|
254
|
+
cursor = conn.execute(sql, (session_id,))
|
|
255
|
+
row = cursor.fetchone()
|
|
256
|
+
|
|
257
|
+
if row is None:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
session_id_val, app_name, user_id, state_data, create_time, update_time = row
|
|
261
|
+
|
|
262
|
+
state = from_json(state_data) if state_data else {}
|
|
263
|
+
|
|
264
|
+
return SessionRecord(
|
|
265
|
+
id=session_id_val,
|
|
266
|
+
app_name=app_name,
|
|
267
|
+
user_id=user_id,
|
|
268
|
+
state=state,
|
|
269
|
+
create_time=create_time,
|
|
270
|
+
update_time=update_time,
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
if DUCKDB_TABLE_NOT_FOUND_ERROR in str(e):
|
|
274
|
+
return None
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
|
|
278
|
+
"""Update session state.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
session_id: Session identifier.
|
|
282
|
+
state: New state dictionary (replaces existing state).
|
|
283
|
+
|
|
284
|
+
Notes:
|
|
285
|
+
This replaces the entire state dictionary.
|
|
286
|
+
Update time is automatically set to current UTC timestamp.
|
|
287
|
+
"""
|
|
288
|
+
now = datetime.now(timezone.utc)
|
|
289
|
+
state_json = to_json(state)
|
|
290
|
+
|
|
291
|
+
sql = f"""
|
|
292
|
+
UPDATE {self._session_table}
|
|
293
|
+
SET state = ?, update_time = ?
|
|
294
|
+
WHERE id = ?
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
with self._config.provide_connection() as conn:
|
|
298
|
+
conn.execute(sql, (state_json, now, session_id))
|
|
299
|
+
conn.commit()
|
|
300
|
+
|
|
301
|
+
def delete_session(self, session_id: str) -> None:
|
|
302
|
+
"""Delete session and all associated events.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
session_id: Session identifier.
|
|
306
|
+
|
|
307
|
+
Notes:
|
|
308
|
+
DuckDB doesn't support CASCADE in foreign keys, so we manually delete events first.
|
|
309
|
+
"""
|
|
310
|
+
delete_events_sql = f"DELETE FROM {self._events_table} WHERE session_id = ?"
|
|
311
|
+
delete_session_sql = f"DELETE FROM {self._session_table} WHERE id = ?"
|
|
312
|
+
|
|
313
|
+
with self._config.provide_connection() as conn:
|
|
314
|
+
conn.execute(delete_events_sql, (session_id,))
|
|
315
|
+
conn.execute(delete_session_sql, (session_id,))
|
|
316
|
+
conn.commit()
|
|
317
|
+
|
|
318
|
+
def list_sessions(self, app_name: str, user_id: str) -> "list[SessionRecord]":
|
|
319
|
+
"""List all sessions for a user in an app.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
app_name: Application name.
|
|
323
|
+
user_id: User identifier.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
List of session records ordered by update_time DESC.
|
|
327
|
+
|
|
328
|
+
Notes:
|
|
329
|
+
Uses composite index on (app_name, user_id).
|
|
330
|
+
"""
|
|
331
|
+
sql = f"""
|
|
332
|
+
SELECT id, app_name, user_id, state, create_time, update_time
|
|
333
|
+
FROM {self._session_table}
|
|
334
|
+
WHERE app_name = ? AND user_id = ?
|
|
335
|
+
ORDER BY update_time DESC
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
with self._config.provide_connection() as conn:
|
|
340
|
+
cursor = conn.execute(sql, (app_name, user_id))
|
|
341
|
+
rows = cursor.fetchall()
|
|
342
|
+
|
|
343
|
+
return [
|
|
344
|
+
SessionRecord(
|
|
345
|
+
id=row[0],
|
|
346
|
+
app_name=row[1],
|
|
347
|
+
user_id=row[2],
|
|
348
|
+
state=from_json(row[3]) if row[3] else {},
|
|
349
|
+
create_time=row[4],
|
|
350
|
+
update_time=row[5],
|
|
351
|
+
)
|
|
352
|
+
for row in rows
|
|
353
|
+
]
|
|
354
|
+
except Exception as e:
|
|
355
|
+
if DUCKDB_TABLE_NOT_FOUND_ERROR in str(e):
|
|
356
|
+
return []
|
|
357
|
+
raise
|
|
358
|
+
|
|
359
|
+
def create_event(
|
|
360
|
+
self,
|
|
361
|
+
event_id: str,
|
|
362
|
+
session_id: str,
|
|
363
|
+
app_name: str,
|
|
364
|
+
user_id: str,
|
|
365
|
+
author: "str | None" = None,
|
|
366
|
+
actions: "bytes | None" = None,
|
|
367
|
+
content: "dict[str, Any] | None" = None,
|
|
368
|
+
**kwargs: Any,
|
|
369
|
+
) -> EventRecord:
|
|
370
|
+
"""Create a new event.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
event_id: Unique event identifier.
|
|
374
|
+
session_id: Session identifier.
|
|
375
|
+
app_name: Application name.
|
|
376
|
+
user_id: User identifier.
|
|
377
|
+
author: Event author (user/assistant/system).
|
|
378
|
+
actions: Pickled actions object.
|
|
379
|
+
content: Event content (JSON).
|
|
380
|
+
**kwargs: Additional optional fields.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Created event record.
|
|
384
|
+
|
|
385
|
+
Notes:
|
|
386
|
+
Uses current UTC timestamp if not provided in kwargs.
|
|
387
|
+
JSON fields are serialized using SQLSpec serializers.
|
|
388
|
+
"""
|
|
389
|
+
timestamp = kwargs.get("timestamp", datetime.now(timezone.utc))
|
|
390
|
+
content_json = to_json(content) if content else None
|
|
391
|
+
grounding_metadata = kwargs.get("grounding_metadata")
|
|
392
|
+
grounding_metadata_json = to_json(grounding_metadata) if grounding_metadata else None
|
|
393
|
+
custom_metadata = kwargs.get("custom_metadata")
|
|
394
|
+
custom_metadata_json = to_json(custom_metadata) if custom_metadata else None
|
|
395
|
+
|
|
396
|
+
sql = f"""
|
|
397
|
+
INSERT INTO {self._events_table} (
|
|
398
|
+
id, session_id, app_name, user_id, invocation_id, author, actions,
|
|
399
|
+
long_running_tool_ids_json, branch, timestamp, content,
|
|
400
|
+
grounding_metadata, custom_metadata, partial, turn_complete,
|
|
401
|
+
interrupted, error_code, error_message
|
|
402
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
with self._config.provide_connection() as conn:
|
|
406
|
+
conn.execute(
|
|
407
|
+
sql,
|
|
408
|
+
(
|
|
409
|
+
event_id,
|
|
410
|
+
session_id,
|
|
411
|
+
app_name,
|
|
412
|
+
user_id,
|
|
413
|
+
kwargs.get("invocation_id"),
|
|
414
|
+
author,
|
|
415
|
+
actions,
|
|
416
|
+
kwargs.get("long_running_tool_ids_json"),
|
|
417
|
+
kwargs.get("branch"),
|
|
418
|
+
timestamp,
|
|
419
|
+
content_json,
|
|
420
|
+
grounding_metadata_json,
|
|
421
|
+
custom_metadata_json,
|
|
422
|
+
kwargs.get("partial"),
|
|
423
|
+
kwargs.get("turn_complete"),
|
|
424
|
+
kwargs.get("interrupted"),
|
|
425
|
+
kwargs.get("error_code"),
|
|
426
|
+
kwargs.get("error_message"),
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
conn.commit()
|
|
430
|
+
|
|
431
|
+
return EventRecord(
|
|
432
|
+
id=event_id,
|
|
433
|
+
session_id=session_id,
|
|
434
|
+
app_name=app_name,
|
|
435
|
+
user_id=user_id,
|
|
436
|
+
invocation_id=kwargs.get("invocation_id", ""),
|
|
437
|
+
author=author or "",
|
|
438
|
+
actions=actions or b"",
|
|
439
|
+
long_running_tool_ids_json=kwargs.get("long_running_tool_ids_json"),
|
|
440
|
+
branch=kwargs.get("branch"),
|
|
441
|
+
timestamp=timestamp,
|
|
442
|
+
content=content,
|
|
443
|
+
grounding_metadata=grounding_metadata,
|
|
444
|
+
custom_metadata=custom_metadata,
|
|
445
|
+
partial=kwargs.get("partial"),
|
|
446
|
+
turn_complete=kwargs.get("turn_complete"),
|
|
447
|
+
interrupted=kwargs.get("interrupted"),
|
|
448
|
+
error_code=kwargs.get("error_code"),
|
|
449
|
+
error_message=kwargs.get("error_message"),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def get_event(self, event_id: str) -> "EventRecord | None":
|
|
453
|
+
"""Get event by ID.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
event_id: Event identifier.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Event record or None if not found.
|
|
460
|
+
"""
|
|
461
|
+
sql = f"""
|
|
462
|
+
SELECT id, session_id, app_name, user_id, invocation_id, author, actions,
|
|
463
|
+
long_running_tool_ids_json, branch, timestamp, content,
|
|
464
|
+
grounding_metadata, custom_metadata, partial, turn_complete,
|
|
465
|
+
interrupted, error_code, error_message
|
|
466
|
+
FROM {self._events_table}
|
|
467
|
+
WHERE id = ?
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
with self._config.provide_connection() as conn:
|
|
472
|
+
cursor = conn.execute(sql, (event_id,))
|
|
473
|
+
row = cursor.fetchone()
|
|
474
|
+
|
|
475
|
+
if row is None:
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
return EventRecord(
|
|
479
|
+
id=row[0],
|
|
480
|
+
session_id=row[1],
|
|
481
|
+
app_name=row[2],
|
|
482
|
+
user_id=row[3],
|
|
483
|
+
invocation_id=row[4],
|
|
484
|
+
author=row[5],
|
|
485
|
+
actions=bytes(row[6]) if row[6] else b"",
|
|
486
|
+
long_running_tool_ids_json=row[7],
|
|
487
|
+
branch=row[8],
|
|
488
|
+
timestamp=row[9],
|
|
489
|
+
content=from_json(row[10]) if row[10] else None,
|
|
490
|
+
grounding_metadata=from_json(row[11]) if row[11] else None,
|
|
491
|
+
custom_metadata=from_json(row[12]) if row[12] else None,
|
|
492
|
+
partial=row[13],
|
|
493
|
+
turn_complete=row[14],
|
|
494
|
+
interrupted=row[15],
|
|
495
|
+
error_code=row[16],
|
|
496
|
+
error_message=row[17],
|
|
497
|
+
)
|
|
498
|
+
except Exception as e:
|
|
499
|
+
if DUCKDB_TABLE_NOT_FOUND_ERROR in str(e):
|
|
500
|
+
return None
|
|
501
|
+
raise
|
|
502
|
+
|
|
503
|
+
def list_events(self, session_id: str) -> "list[EventRecord]":
|
|
504
|
+
"""List events for a session ordered by timestamp.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
session_id: Session identifier.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
List of event records ordered by timestamp ASC.
|
|
511
|
+
"""
|
|
512
|
+
sql = f"""
|
|
513
|
+
SELECT id, session_id, app_name, user_id, invocation_id, author, actions,
|
|
514
|
+
long_running_tool_ids_json, branch, timestamp, content,
|
|
515
|
+
grounding_metadata, custom_metadata, partial, turn_complete,
|
|
516
|
+
interrupted, error_code, error_message
|
|
517
|
+
FROM {self._events_table}
|
|
518
|
+
WHERE session_id = ?
|
|
519
|
+
ORDER BY timestamp ASC
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
with self._config.provide_connection() as conn:
|
|
524
|
+
cursor = conn.execute(sql, (session_id,))
|
|
525
|
+
rows = cursor.fetchall()
|
|
526
|
+
|
|
527
|
+
return [
|
|
528
|
+
EventRecord(
|
|
529
|
+
id=row[0],
|
|
530
|
+
session_id=row[1],
|
|
531
|
+
app_name=row[2],
|
|
532
|
+
user_id=row[3],
|
|
533
|
+
invocation_id=row[4],
|
|
534
|
+
author=row[5],
|
|
535
|
+
actions=bytes(row[6]) if row[6] else b"",
|
|
536
|
+
long_running_tool_ids_json=row[7],
|
|
537
|
+
branch=row[8],
|
|
538
|
+
timestamp=row[9],
|
|
539
|
+
content=from_json(row[10]) if row[10] else None,
|
|
540
|
+
grounding_metadata=from_json(row[11]) if row[11] else None,
|
|
541
|
+
custom_metadata=from_json(row[12]) if row[12] else None,
|
|
542
|
+
partial=row[13],
|
|
543
|
+
turn_complete=row[14],
|
|
544
|
+
interrupted=row[15],
|
|
545
|
+
error_code=row[16],
|
|
546
|
+
error_message=row[17],
|
|
547
|
+
)
|
|
548
|
+
for row in rows
|
|
549
|
+
]
|
|
550
|
+
except Exception as e:
|
|
551
|
+
if DUCKDB_TABLE_NOT_FOUND_ERROR in str(e):
|
|
552
|
+
return []
|
|
553
|
+
raise
|