# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from google.adk.sessions.database_session_service import DatabaseSessionService from google.adk.sessions.migration import _schema_check_utils from google.adk.sessions.schemas import v0 import pytest from sqlalchemy import inspect from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine async def create_v0_db(db_path): db_url = f'sqlite+aiosqlite:///{db_path}' engine = create_async_engine(db_url) async with engine.begin() as conn: await conn.run_sync(v0.Base.metadata.create_all) await engine.dispose() # Use async context managers so DatabaseSessionService always closes. @pytest.mark.asyncio async def test_new_db_uses_latest_schema(tmp_path): db_path = tmp_path / 'new_db.db' db_url = f'sqlite+aiosqlite:///{db_path}' async with DatabaseSessionService(db_url) as session_service: assert session_service._db_schema_version is None await session_service.create_session(app_name='my_app', user_id='test_user') assert ( session_service._db_schema_version == _schema_check_utils.LATEST_SCHEMA_VERSION ) # Verify metadata table engine = create_async_engine(db_url) async with engine.connect() as conn: has_metadata_table = await conn.run_sync( lambda sync_conn: inspect(sync_conn).has_table('adk_internal_metadata') ) assert has_metadata_table def get_schema_version(sync_conn): inspector = inspect(sync_conn) key_col = inspector.dialect.identifier_preparer.quote('key') return sync_conn.execute( text( f'SELECT value FROM adk_internal_metadata WHERE {key_col} = :key' ), {'key': _schema_check_utils.SCHEMA_VERSION_KEY}, ).scalar_one_or_none() schema_version = await conn.run_sync(get_schema_version) assert schema_version == _schema_check_utils.LATEST_SCHEMA_VERSION # Verify events table columns for v1 event_cols = await conn.run_sync( lambda sync_conn: inspect(sync_conn).get_columns('events') ) event_col_names = {c['name'] for c in event_cols} assert 'event_data' in event_col_names assert 'actions' not in event_col_names event_indexes = await conn.run_sync( lambda sync_conn: inspect(sync_conn).get_indexes('events') ) assert any( index['name'] == 'idx_events_app_user_session_ts' and index['column_names'] == ['app_name', 'user_id', 'session_id', 'timestamp'] for index in event_indexes ) await engine.dispose() @pytest.mark.asyncio async def test_existing_v0_db_uses_v0_schema(tmp_path): db_path = tmp_path / 'v0_db.db' await create_v0_db(db_path) db_url = f'sqlite+aiosqlite:///{db_path}' async with DatabaseSessionService(db_url) as session_service: assert session_service._db_schema_version is None await session_service.create_session( app_name='my_app', user_id='test_user', session_id='s1' ) assert ( session_service._db_schema_version == _schema_check_utils.SCHEMA_VERSION_0_PICKLE ) session = await session_service.get_session( app_name='my_app', user_id='test_user', session_id='s1' ) assert session.id == 's1' # Verify schema tables engine = create_async_engine(db_url) async with engine.connect() as conn: has_metadata_table = await conn.run_sync( lambda sync_conn: inspect(sync_conn).has_table('adk_internal_metadata') ) assert not has_metadata_table # Verify events table columns for v0 event_cols = await conn.run_sync( lambda sync_conn: inspect(sync_conn).get_columns('events') ) event_col_names = {c['name'] for c in event_cols} assert 'event_data' not in event_col_names assert 'actions' in event_col_names await engine.dispose() @pytest.mark.asyncio async def test_existing_latest_db_uses_latest_schema(tmp_path): db_path = tmp_path / 'new_db.db' db_url = f'sqlite+aiosqlite:///{db_path}' # Create session service which creates db with latest schema async with DatabaseSessionService(db_url) as session_service1: await session_service1.create_session( app_name='my_app', user_id='test_user', session_id='s1' ) assert ( session_service1._db_schema_version == _schema_check_utils.LATEST_SCHEMA_VERSION ) # Create another session service on same db and check it detects latest schema async with DatabaseSessionService(db_url) as session_service2: await session_service2.create_session( app_name='my_app', user_id='test_user2', session_id='s2' ) assert ( session_service2._db_schema_version == _schema_check_utils.LATEST_SCHEMA_VERSION ) s2 = await session_service2.get_session( app_name='my_app', user_id='test_user2', session_id='s2' ) assert s2.id == 's2' s1 = await session_service2.get_session( app_name='my_app', user_id='test_user', session_id='s1' ) assert s1.id == 's1' list_sessions_response = await session_service2.list_sessions( app_name='my_app' ) assert len(list_sessions_response.sessions) == 2 # Verify schema tables engine = create_async_engine(db_url) async with engine.connect() as conn: has_metadata_table = await conn.run_sync( lambda sync_conn: inspect(sync_conn).has_table('adk_internal_metadata') ) assert has_metadata_table # Verify events table columns for v1 event_cols = await conn.run_sync( lambda sync_conn: inspect(sync_conn).get_columns('events') ) event_col_names = {c['name'] for c in event_cols} assert 'event_data' in event_col_names assert 'actions' not in event_col_names await engine.dispose() @pytest.mark.asyncio async def test_prepare_tables_recreates_missing_latest_events_index(tmp_path): db_path = tmp_path / 'missing_latest_index.db' db_url = f'sqlite+aiosqlite:///{db_path}' async with DatabaseSessionService(db_url) as session_service: await session_service.create_session( app_name='my_app', user_id='test_user', session_id='s1' ) engine = create_async_engine(db_url) async with engine.begin() as conn: await conn.execute(text('DROP INDEX idx_events_app_user_session_ts')) await engine.dispose() async with DatabaseSessionService(db_url) as session_service: session = await session_service.get_session( app_name='my_app', user_id='test_user', session_id='s1' ) assert session.id == 's1' engine = create_async_engine(db_url) async with engine.connect() as conn: event_indexes = await conn.run_sync( lambda sync_conn: inspect(sync_conn).get_indexes('events') ) await engine.dispose() assert any( index['name'] == 'idx_events_app_user_session_ts' and index['column_names'] == ['app_name', 'user_id', 'session_id', 'timestamp'] for index in event_indexes ) @pytest.mark.asyncio async def test_prepare_tables_recreates_missing_v0_events_index(tmp_path): db_path = tmp_path / 'missing_v0_index.db' await create_v0_db(db_path) db_url = f'sqlite+aiosqlite:///{db_path}' engine = create_async_engine(db_url) async with engine.begin() as conn: await conn.execute(text('DROP INDEX idx_events_app_user_session_ts')) await engine.dispose() async with DatabaseSessionService(db_url) as session_service: await session_service.create_session( app_name='my_app', user_id='test_user', session_id='s1' ) session = await session_service.get_session( app_name='my_app', user_id='test_user', session_id='s1' ) assert session.id == 's1' engine = create_async_engine(db_url) async with engine.connect() as conn: event_indexes = await conn.run_sync( lambda sync_conn: inspect(sync_conn).get_indexes('events') ) await engine.dispose() assert any( index['name'] == 'idx_events_app_user_session_ts' and index['column_names'] == ['app_name', 'user_id', 'session_id', 'timestamp'] for index in event_indexes )