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,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