Skip to content

Commit 3153e6d

Browse files
GWealecopybara-github
authored andcommitted
feat: Add index to events table and update dependencies
Introduces a new index `idx_events_app_user_session_ts` on the `events` table in both V0 and V1 database schemas. This index covers `app_name`, `user_id`, `session_id`, and `timestamp` (descending) to improve query performance. Modifies `DatabaseSessionService` to automatically create any missing indexes defined in the SQLAlchemy metadata when preparing the database tables. Close #4827 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 888877174
1 parent 6c24ccc commit 3153e6d

4 files changed

Lines changed: 120 additions & 2 deletions

File tree

src/google/adk/sessions/database_session_service.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import logging
2222
from typing import Any
2323
from typing import AsyncIterator
24+
from typing import Optional
2425
from typing import TypeAlias
2526
from typing import TypeVar
2627

2728
from google.adk.platform import time as platform_time
2829
from sqlalchemy import delete
2930
from sqlalchemy import event
31+
from sqlalchemy import MetaData
3032
from sqlalchemy import select
33+
from sqlalchemy.engine import Connection
3134
from sqlalchemy.engine import make_url
3235
from sqlalchemy.exc import ArgumentError
3336
from sqlalchemy.ext.asyncio import async_sessionmaker
@@ -106,6 +109,22 @@ def _set_sqlite_pragma(dbapi_connection, connection_record):
106109
cursor.close()
107110

108111

112+
def _ensure_schema_indexes_exist(
113+
connection: Connection, metadata: MetaData
114+
) -> None:
115+
"""Ensures indexes declared in metadata exist for existing tables."""
116+
logger.debug("Ensuring schema indexes exist for metadata tables.")
117+
for table in metadata.sorted_tables:
118+
for index in sorted(table.indexes, key=lambda item: item.name or ""):
119+
index.create(bind=connection, checkfirst=True)
120+
121+
122+
def _setup_database_schema(connection: Connection, metadata: MetaData) -> None:
123+
"""Ensures tables and indexes declared in metadata exist."""
124+
metadata.create_all(bind=connection)
125+
_ensure_schema_indexes_exist(connection, metadata)
126+
127+
109128
def _merge_state(
110129
app_state: dict[str, Any],
111130
user_state: dict[str, Any],
@@ -301,11 +320,11 @@ async def _prepare_tables(self):
301320
# Uncomment to recreate DB every time
302321
# await conn.run_sync(BaseV1.metadata.drop_all)
303322
logger.debug("Using V1 schema tables...")
304-
await conn.run_sync(BaseV1.metadata.create_all)
323+
await conn.run_sync(_setup_database_schema, BaseV1.metadata)
305324
else:
306325
# await conn.run_sync(BaseV0.metadata.drop_all)
307326
logger.debug("Using V0 schema tables...")
308-
await conn.run_sync(BaseV0.metadata.create_all)
327+
await conn.run_sync(_setup_database_schema, BaseV0.metadata)
309328

310329
if self._db_schema_version == _schema_check_utils.LATEST_SCHEMA_VERSION:
311330
async with self._rollback_on_exception_session() as sql_session:

src/google/adk/sessions/schemas/v0.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
from google.adk.platform import uuid as platform_uuid
3737
from google.genai import types
3838
from sqlalchemy import Boolean
39+
from sqlalchemy import desc
3940
from sqlalchemy import ForeignKeyConstraint
4041
from sqlalchemy import func
42+
from sqlalchemy import Index
4143
from sqlalchemy import inspect
4244
from sqlalchemy import Text
4345
from sqlalchemy.dialects import mysql
@@ -256,6 +258,13 @@ class StorageEvent(Base):
256258
["sessions.app_name", "sessions.user_id", "sessions.id"],
257259
ondelete="CASCADE",
258260
),
261+
Index(
262+
"idx_events_app_user_session_ts",
263+
"app_name",
264+
"user_id",
265+
"session_id",
266+
desc("timestamp"),
267+
),
259268
)
260269

261270
@property

src/google/adk/sessions/schemas/v1.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
from typing import Any
2929

3030
from google.adk.platform import uuid as platform_uuid
31+
from sqlalchemy import desc
3132
from sqlalchemy import ForeignKeyConstraint
3233
from sqlalchemy import func
34+
from sqlalchemy import Index
3335
from sqlalchemy import inspect
3436
from sqlalchemy.ext.mutable import MutableDict
3537
from sqlalchemy.orm import DeclarativeBase
@@ -196,6 +198,13 @@ class StorageEvent(Base):
196198
["sessions.app_name", "sessions.user_id", "sessions.id"],
197199
ondelete="CASCADE",
198200
),
201+
Index(
202+
"idx_events_app_user_session_ts",
203+
"app_name",
204+
"user_id",
205+
"session_id",
206+
desc("timestamp"),
207+
),
199208
)
200209

201210
@classmethod

tests/unittests/sessions/migration/test_database_schema.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ def get_schema_version(sync_conn):
7272
event_col_names = {c['name'] for c in event_cols}
7373
assert 'event_data' in event_col_names
7474
assert 'actions' not in event_col_names
75+
76+
event_indexes = await conn.run_sync(
77+
lambda sync_conn: inspect(sync_conn).get_indexes('events')
78+
)
79+
assert any(
80+
index['name'] == 'idx_events_app_user_session_ts'
81+
and index['column_names']
82+
== ['app_name', 'user_id', 'session_id', 'timestamp']
83+
for index in event_indexes
84+
)
7585
await engine.dispose()
7686

7787

@@ -168,3 +178,74 @@ async def test_existing_latest_db_uses_latest_schema(tmp_path):
168178
assert 'event_data' in event_col_names
169179
assert 'actions' not in event_col_names
170180
await engine.dispose()
181+
182+
183+
@pytest.mark.asyncio
184+
async def test_prepare_tables_recreates_missing_latest_events_index(tmp_path):
185+
db_path = tmp_path / 'missing_latest_index.db'
186+
db_url = f'sqlite+aiosqlite:///{db_path}'
187+
188+
async with DatabaseSessionService(db_url) as session_service:
189+
await session_service.create_session(
190+
app_name='my_app', user_id='test_user', session_id='s1'
191+
)
192+
193+
engine = create_async_engine(db_url)
194+
async with engine.begin() as conn:
195+
await conn.execute(text('DROP INDEX idx_events_app_user_session_ts'))
196+
await engine.dispose()
197+
198+
async with DatabaseSessionService(db_url) as session_service:
199+
session = await session_service.get_session(
200+
app_name='my_app', user_id='test_user', session_id='s1'
201+
)
202+
assert session.id == 's1'
203+
204+
engine = create_async_engine(db_url)
205+
async with engine.connect() as conn:
206+
event_indexes = await conn.run_sync(
207+
lambda sync_conn: inspect(sync_conn).get_indexes('events')
208+
)
209+
await engine.dispose()
210+
211+
assert any(
212+
index['name'] == 'idx_events_app_user_session_ts'
213+
and index['column_names']
214+
== ['app_name', 'user_id', 'session_id', 'timestamp']
215+
for index in event_indexes
216+
)
217+
218+
219+
@pytest.mark.asyncio
220+
async def test_prepare_tables_recreates_missing_v0_events_index(tmp_path):
221+
db_path = tmp_path / 'missing_v0_index.db'
222+
await create_v0_db(db_path)
223+
db_url = f'sqlite+aiosqlite:///{db_path}'
224+
225+
engine = create_async_engine(db_url)
226+
async with engine.begin() as conn:
227+
await conn.execute(text('DROP INDEX idx_events_app_user_session_ts'))
228+
await engine.dispose()
229+
230+
async with DatabaseSessionService(db_url) as session_service:
231+
await session_service.create_session(
232+
app_name='my_app', user_id='test_user', session_id='s1'
233+
)
234+
session = await session_service.get_session(
235+
app_name='my_app', user_id='test_user', session_id='s1'
236+
)
237+
assert session.id == 's1'
238+
239+
engine = create_async_engine(db_url)
240+
async with engine.connect() as conn:
241+
event_indexes = await conn.run_sync(
242+
lambda sync_conn: inspect(sync_conn).get_indexes('events')
243+
)
244+
await engine.dispose()
245+
246+
assert any(
247+
index['name'] == 'idx_events_app_user_session_ts'
248+
and index['column_names']
249+
== ['app_name', 'user_id', 'session_id', 'timestamp']
250+
for index in event_indexes
251+
)

0 commit comments

Comments
 (0)