sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -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 +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  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 +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  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 +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +40 -11
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  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 +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,536 @@
1
+ """Base store classes for ADK session backend (sync and async)."""
2
+
3
+ import re
4
+ from abc import ABC, abstractmethod
5
+ from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar, cast
6
+
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from datetime import datetime
11
+
12
+ from sqlspec.extensions.adk._types import EventRecord, SessionRecord
13
+
14
+ ConfigT = TypeVar("ConfigT")
15
+
16
+ logger = get_logger("extensions.adk.store")
17
+
18
+ __all__ = ("BaseAsyncADKStore", "BaseSyncADKStore")
19
+
20
+ VALID_TABLE_NAME_PATTERN: Final = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
21
+ COLUMN_NAME_PATTERN: Final = re.compile(r"^(\w+)")
22
+ MAX_TABLE_NAME_LENGTH: Final = 63
23
+
24
+
25
+ def _parse_owner_id_column(owner_id_column_ddl: str) -> str:
26
+ """Extract column name from owner ID column DDL definition.
27
+
28
+ Args:
29
+ owner_id_column_ddl: Full column DDL string (e.g., "user_id INTEGER REFERENCES users(id)").
30
+
31
+ Returns:
32
+ Column name only (first word).
33
+
34
+ Raises:
35
+ ValueError: If DDL format is invalid.
36
+
37
+ Examples:
38
+ "account_id INTEGER NOT NULL" -> "account_id"
39
+ "user_id UUID REFERENCES users(id)" -> "user_id"
40
+ "tenant VARCHAR(64) DEFAULT 'public'" -> "tenant"
41
+
42
+ Notes:
43
+ Only the column name is parsed. The rest of the DDL is passed through
44
+ verbatim to CREATE TABLE statements.
45
+ """
46
+ match = COLUMN_NAME_PATTERN.match(owner_id_column_ddl.strip())
47
+ if not match:
48
+ msg = f"Invalid owner_id_column DDL: {owner_id_column_ddl!r}. Must start with column name."
49
+ raise ValueError(msg)
50
+
51
+ return match.group(1)
52
+
53
+
54
+ def _validate_table_name(table_name: str) -> None:
55
+ """Validate table name for SQL safety.
56
+
57
+ Args:
58
+ table_name: Table name to validate.
59
+
60
+ Raises:
61
+ ValueError: If table name is invalid.
62
+
63
+ Notes:
64
+ - Must start with letter or underscore
65
+ - Can only contain letters, numbers, and underscores
66
+ - Maximum length is 63 characters (PostgreSQL limit)
67
+ - Prevents SQL injection in table names
68
+ """
69
+ if not table_name:
70
+ msg = "Table name cannot be empty"
71
+ raise ValueError(msg)
72
+
73
+ if len(table_name) > MAX_TABLE_NAME_LENGTH:
74
+ msg = f"Table name too long: {len(table_name)} chars (max {MAX_TABLE_NAME_LENGTH})"
75
+ raise ValueError(msg)
76
+
77
+ if not VALID_TABLE_NAME_PATTERN.match(table_name):
78
+ msg = (
79
+ f"Invalid table name: {table_name!r}. "
80
+ "Must start with letter/underscore and contain only alphanumeric characters and underscores"
81
+ )
82
+ raise ValueError(msg)
83
+
84
+
85
+ class BaseAsyncADKStore(ABC, Generic[ConfigT]):
86
+ """Base class for async SQLSpec-backed ADK session stores.
87
+
88
+ Implements storage operations for Google ADK sessions and events using
89
+ SQLSpec database adapters with async/await.
90
+
91
+ This abstract base class provides common functionality for all database-specific
92
+ store implementations including:
93
+ - Connection management via SQLSpec configs
94
+ - Table name validation
95
+ - Session and event CRUD operations
96
+
97
+ Subclasses must implement dialect-specific SQL queries and will be created
98
+ in each adapter directory (e.g., sqlspec/adapters/asyncpg/adk/store.py).
99
+
100
+ Args:
101
+ config: SQLSpec database configuration with extension_config["adk"] settings.
102
+
103
+ Notes:
104
+ Configuration is read from config.extension_config["adk"]:
105
+ - session_table: Sessions table name (default: "adk_sessions")
106
+ - events_table: Events table name (default: "adk_events")
107
+ - owner_id_column: Optional owner FK column DDL (default: None)
108
+ """
109
+
110
+ __slots__ = ("_config", "_events_table", "_owner_id_column_ddl", "_owner_id_column_name", "_session_table")
111
+
112
+ def __init__(self, config: ConfigT) -> None:
113
+ """Initialize the ADK store.
114
+
115
+ Args:
116
+ config: SQLSpec database configuration.
117
+
118
+ Notes:
119
+ Reads configuration from config.extension_config["adk"]:
120
+ - session_table: Sessions table name (default: "adk_sessions")
121
+ - events_table: Events table name (default: "adk_events")
122
+ - owner_id_column: Optional owner FK column DDL (default: None)
123
+ """
124
+ self._config = config
125
+ store_config = self._get_store_config_from_extension()
126
+ self._session_table: str = str(store_config["session_table"])
127
+ self._events_table: str = str(store_config["events_table"])
128
+ self._owner_id_column_ddl: str | None = store_config.get("owner_id_column")
129
+ self._owner_id_column_name: str | None = (
130
+ _parse_owner_id_column(self._owner_id_column_ddl) if self._owner_id_column_ddl else None
131
+ )
132
+ _validate_table_name(self._session_table)
133
+ _validate_table_name(self._events_table)
134
+
135
+ def _get_store_config_from_extension(self) -> "dict[str, Any]":
136
+ """Extract ADK store configuration from config.extension_config.
137
+
138
+ Returns:
139
+ Dict with session_table, events_table, and optionally owner_id_column.
140
+ """
141
+ if hasattr(self._config, "extension_config"):
142
+ extension_config = cast("dict[str, dict[str, Any]]", self._config.extension_config) # pyright: ignore
143
+ adk_config: dict[str, Any] = extension_config.get("adk", {})
144
+ session_table = adk_config.get("session_table")
145
+ events_table = adk_config.get("events_table")
146
+ result: dict[str, Any] = {
147
+ "session_table": session_table if session_table is not None else "adk_sessions",
148
+ "events_table": events_table if events_table is not None else "adk_events",
149
+ }
150
+ owner_id = adk_config.get("owner_id_column")
151
+ if owner_id is not None:
152
+ result["owner_id_column"] = owner_id
153
+ return result
154
+ return {"session_table": "adk_sessions", "events_table": "adk_events"}
155
+
156
+ @property
157
+ def config(self) -> ConfigT:
158
+ """Return the database configuration."""
159
+ return self._config
160
+
161
+ @property
162
+ def session_table(self) -> str:
163
+ """Return the sessions table name."""
164
+ return self._session_table
165
+
166
+ @property
167
+ def events_table(self) -> str:
168
+ """Return the events table name."""
169
+ return self._events_table
170
+
171
+ @property
172
+ def owner_id_column_ddl(self) -> "str | None":
173
+ """Return the full owner ID column DDL (or None if not configured)."""
174
+ return self._owner_id_column_ddl
175
+
176
+ @property
177
+ def owner_id_column_name(self) -> "str | None":
178
+ """Return the owner ID column name only (or None if not configured)."""
179
+ return self._owner_id_column_name
180
+
181
+ @abstractmethod
182
+ async def create_session(
183
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
184
+ ) -> "SessionRecord":
185
+ """Create a new session.
186
+
187
+ Args:
188
+ session_id: Unique identifier for the session.
189
+ app_name: Name of the application.
190
+ user_id: ID of the user.
191
+ state: Session state dictionary.
192
+ owner_id: Optional owner ID value for owner_id_column (if configured).
193
+
194
+ Returns:
195
+ The created session record.
196
+ """
197
+ raise NotImplementedError
198
+
199
+ @abstractmethod
200
+ async def get_session(self, session_id: str) -> "SessionRecord | None":
201
+ """Get a session by ID.
202
+
203
+ Args:
204
+ session_id: Session identifier.
205
+
206
+ Returns:
207
+ Session record if found, None otherwise.
208
+ """
209
+ raise NotImplementedError
210
+
211
+ @abstractmethod
212
+ async def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
213
+ """Update session state.
214
+
215
+ Args:
216
+ session_id: Session identifier.
217
+ state: New state dictionary.
218
+ """
219
+ raise NotImplementedError
220
+
221
+ @abstractmethod
222
+ async def list_sessions(self, app_name: str, user_id: "str | None" = None) -> "list[SessionRecord]":
223
+ """List all sessions for an app, optionally filtered by user.
224
+
225
+ Args:
226
+ app_name: Name of the application.
227
+ user_id: ID of the user. If None, returns all sessions for the app.
228
+
229
+ Returns:
230
+ List of session records.
231
+ """
232
+ raise NotImplementedError
233
+
234
+ @abstractmethod
235
+ async def delete_session(self, session_id: str) -> None:
236
+ """Delete a session and its events.
237
+
238
+ Args:
239
+ session_id: Session identifier.
240
+ """
241
+ raise NotImplementedError
242
+
243
+ @abstractmethod
244
+ async def append_event(self, event_record: "EventRecord") -> None:
245
+ """Append an event to a session.
246
+
247
+ Args:
248
+ event_record: Event record to store.
249
+ """
250
+ raise NotImplementedError
251
+
252
+ @abstractmethod
253
+ async def get_events(
254
+ self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
255
+ ) -> "list[EventRecord]":
256
+ """Get events for a session.
257
+
258
+ Args:
259
+ session_id: Session identifier.
260
+ after_timestamp: Only return events after this time.
261
+ limit: Maximum number of events to return.
262
+
263
+ Returns:
264
+ List of event records ordered by timestamp ascending.
265
+ """
266
+ raise NotImplementedError
267
+
268
+ @abstractmethod
269
+ async def create_tables(self) -> None:
270
+ """Create the sessions and events tables if they don't exist."""
271
+ raise NotImplementedError
272
+
273
+ @abstractmethod
274
+ async def _get_create_sessions_table_sql(self) -> str:
275
+ """Get the CREATE TABLE SQL for the sessions table.
276
+
277
+ Returns:
278
+ SQL statement to create the sessions table.
279
+ """
280
+ raise NotImplementedError
281
+
282
+ @abstractmethod
283
+ async def _get_create_events_table_sql(self) -> str:
284
+ """Get the CREATE TABLE SQL for the events table.
285
+
286
+ Returns:
287
+ SQL statement to create the events table.
288
+ """
289
+ raise NotImplementedError
290
+
291
+ @abstractmethod
292
+ def _get_drop_tables_sql(self) -> "list[str]":
293
+ """Get the DROP TABLE SQL statements for this database dialect.
294
+
295
+ Returns:
296
+ List of SQL statements to drop the tables and all indexes.
297
+ Order matters: drop events table before sessions table due to FK.
298
+
299
+ Notes:
300
+ Should use IF EXISTS or dialect-specific error handling
301
+ to allow idempotent migrations.
302
+ """
303
+ raise NotImplementedError
304
+
305
+
306
+ class BaseSyncADKStore(ABC, Generic[ConfigT]):
307
+ """Base class for sync SQLSpec-backed ADK session stores.
308
+
309
+ Implements storage operations for Google ADK sessions and events using
310
+ SQLSpec database adapters with synchronous execution.
311
+
312
+ This abstract base class provides common functionality for sync database-specific
313
+ store implementations including:
314
+ - Connection management via SQLSpec configs
315
+ - Table name validation
316
+ - Session and event CRUD operations
317
+
318
+ Subclasses must implement dialect-specific SQL queries and will be created
319
+ in each adapter directory (e.g., sqlspec/adapters/sqlite/adk/store.py).
320
+
321
+ Args:
322
+ config: SQLSpec database configuration with extension_config["adk"] settings.
323
+
324
+ Notes:
325
+ Configuration is read from config.extension_config["adk"]:
326
+ - session_table: Sessions table name (default: "adk_sessions")
327
+ - events_table: Events table name (default: "adk_events")
328
+ - owner_id_column: Optional owner FK column DDL (default: None)
329
+ """
330
+
331
+ __slots__ = ("_config", "_events_table", "_owner_id_column_ddl", "_owner_id_column_name", "_session_table")
332
+
333
+ def __init__(self, config: ConfigT) -> None:
334
+ """Initialize the sync ADK store.
335
+
336
+ Args:
337
+ config: SQLSpec database configuration.
338
+
339
+ Notes:
340
+ Reads configuration from config.extension_config["adk"]:
341
+ - session_table: Sessions table name (default: "adk_sessions")
342
+ - events_table: Events table name (default: "adk_events")
343
+ - owner_id_column: Optional owner FK column DDL (default: None)
344
+ """
345
+ self._config = config
346
+ store_config = self._get_store_config_from_extension()
347
+ self._session_table: str = str(store_config["session_table"])
348
+ self._events_table: str = str(store_config["events_table"])
349
+ self._owner_id_column_ddl: str | None = store_config.get("owner_id_column")
350
+ self._owner_id_column_name: str | None = (
351
+ _parse_owner_id_column(self._owner_id_column_ddl) if self._owner_id_column_ddl else None
352
+ )
353
+ _validate_table_name(self._session_table)
354
+ _validate_table_name(self._events_table)
355
+
356
+ def _get_store_config_from_extension(self) -> "dict[str, Any]":
357
+ """Extract ADK store configuration from config.extension_config.
358
+
359
+ Returns:
360
+ Dict with session_table, events_table, and optionally owner_id_column.
361
+ """
362
+ if hasattr(self._config, "extension_config"):
363
+ extension_config = cast("dict[str, dict[str, Any]]", self._config.extension_config) # pyright: ignore
364
+ adk_config: dict[str, Any] = extension_config.get("adk", {})
365
+ session_table = adk_config.get("session_table")
366
+ events_table = adk_config.get("events_table")
367
+ result: dict[str, Any] = {
368
+ "session_table": session_table if session_table is not None else "adk_sessions",
369
+ "events_table": events_table if events_table is not None else "adk_events",
370
+ }
371
+ owner_id = adk_config.get("owner_id_column")
372
+ if owner_id is not None:
373
+ result["owner_id_column"] = owner_id
374
+ return result
375
+ return {"session_table": "adk_sessions", "events_table": "adk_events"}
376
+
377
+ @property
378
+ def config(self) -> ConfigT:
379
+ """Return the database configuration."""
380
+ return self._config
381
+
382
+ @property
383
+ def session_table(self) -> str:
384
+ """Return the sessions table name."""
385
+ return self._session_table
386
+
387
+ @property
388
+ def events_table(self) -> str:
389
+ """Return the events table name."""
390
+ return self._events_table
391
+
392
+ @property
393
+ def owner_id_column_ddl(self) -> "str | None":
394
+ """Return the full owner ID column DDL (or None if not configured)."""
395
+ return self._owner_id_column_ddl
396
+
397
+ @property
398
+ def owner_id_column_name(self) -> "str | None":
399
+ """Return the owner ID column name only (or None if not configured)."""
400
+ return self._owner_id_column_name
401
+
402
+ @abstractmethod
403
+ def create_session(
404
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
405
+ ) -> "SessionRecord":
406
+ """Create a new session.
407
+
408
+ Args:
409
+ session_id: Unique identifier for the session.
410
+ app_name: Name of the application.
411
+ user_id: ID of the user.
412
+ state: Session state dictionary.
413
+ owner_id: Optional owner ID value for owner_id_column (if configured).
414
+
415
+ Returns:
416
+ The created session record.
417
+ """
418
+ raise NotImplementedError
419
+
420
+ @abstractmethod
421
+ def get_session(self, session_id: str) -> "SessionRecord | None":
422
+ """Get a session by ID.
423
+
424
+ Args:
425
+ session_id: Session identifier.
426
+
427
+ Returns:
428
+ Session record if found, None otherwise.
429
+ """
430
+ raise NotImplementedError
431
+
432
+ @abstractmethod
433
+ def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
434
+ """Update session state.
435
+
436
+ Args:
437
+ session_id: Session identifier.
438
+ state: New state dictionary.
439
+ """
440
+ raise NotImplementedError
441
+
442
+ @abstractmethod
443
+ def list_sessions(self, app_name: str, user_id: "str | None" = None) -> "list[SessionRecord]":
444
+ """List all sessions for an app, optionally filtered by user.
445
+
446
+ Args:
447
+ app_name: Name of the application.
448
+ user_id: ID of the user. If None, returns all sessions for the app.
449
+
450
+ Returns:
451
+ List of session records.
452
+ """
453
+ raise NotImplementedError
454
+
455
+ @abstractmethod
456
+ def delete_session(self, session_id: str) -> None:
457
+ """Delete a session and its events.
458
+
459
+ Args:
460
+ session_id: Session identifier.
461
+ """
462
+ raise NotImplementedError
463
+
464
+ @abstractmethod
465
+ def create_event(
466
+ self,
467
+ event_id: str,
468
+ session_id: str,
469
+ app_name: str,
470
+ user_id: str,
471
+ author: "str | None" = None,
472
+ actions: "bytes | None" = None,
473
+ content: "dict[str, Any] | None" = None,
474
+ **kwargs: Any,
475
+ ) -> "EventRecord":
476
+ """Create a new event.
477
+
478
+ Args:
479
+ event_id: Unique event identifier.
480
+ session_id: Session identifier.
481
+ app_name: Application name.
482
+ user_id: User identifier.
483
+ author: Event author (user/assistant/system).
484
+ actions: Pickled actions object.
485
+ content: Event content (JSONB/JSON).
486
+ **kwargs: Additional optional fields.
487
+
488
+ Returns:
489
+ Created event record.
490
+ """
491
+ raise NotImplementedError
492
+
493
+ @abstractmethod
494
+ def list_events(self, session_id: str) -> "list[EventRecord]":
495
+ """List events for a session ordered by timestamp.
496
+
497
+ Args:
498
+ session_id: Session identifier.
499
+
500
+ Returns:
501
+ List of event records ordered by timestamp ASC.
502
+ """
503
+ raise NotImplementedError
504
+
505
+ @abstractmethod
506
+ def create_tables(self) -> None:
507
+ """Create both sessions and events tables if they don't exist."""
508
+ raise NotImplementedError
509
+
510
+ @abstractmethod
511
+ def _get_create_sessions_table_sql(self) -> str:
512
+ """Get SQL to create sessions table.
513
+
514
+ Returns:
515
+ SQL statement to create adk_sessions table with indexes.
516
+ """
517
+ raise NotImplementedError
518
+
519
+ @abstractmethod
520
+ def _get_create_events_table_sql(self) -> str:
521
+ """Get SQL to create events table.
522
+
523
+ Returns:
524
+ SQL statement to create adk_events table with indexes.
525
+ """
526
+ raise NotImplementedError
527
+
528
+ @abstractmethod
529
+ def _get_drop_tables_sql(self) -> "list[str]":
530
+ """Get SQL to drop tables.
531
+
532
+ Returns:
533
+ List of SQL statements to drop tables and indexes.
534
+ Order matters: drop events before sessions due to FK.
535
+ """
536
+ raise NotImplementedError