|
18 | 18 | from datetime import timezone |
19 | 19 | import enum |
20 | 20 | import sqlite3 |
| 21 | +import time |
21 | 22 | from unittest import mock |
22 | 23 |
|
23 | 24 | from google.adk.errors.already_exists_error import AlreadyExistsError |
@@ -2052,3 +2053,59 @@ async def test_database_session_service_requires_one_argument(): |
2052 | 2053 | DatabaseSessionService( |
2053 | 2054 | db_url='sqlite+aiosqlite:///:memory:', db_engine=engine |
2054 | 2055 | ) |
| 2056 | + |
| 2057 | + |
| 2058 | +@pytest.mark.asyncio |
| 2059 | +async def test_database_session_service_sqlite_file_timestamp_read_after_reopen( |
| 2060 | + tmp_path, |
| 2061 | +): |
| 2062 | + """Regression test for #6352 (SQLite REAL-affinity timestamp reads).""" |
| 2063 | + # SQLite REAL-affinity columns can end up storing raw Unix epoch floats |
| 2064 | + # instead of the text format SQLAlchemy's DateTime type normally writes |
| 2065 | + # (for example, if the row was written by a different code path than the |
| 2066 | + # SQLAlchemy ORM). Reading such a row back must not raise TypeError, since |
| 2067 | + # TypeDecorator.process_result_value runs after the impl's own result |
| 2068 | + # processor, which chokes on a float before process_result_value can run. |
| 2069 | + # This test forces that condition directly via raw SQL. |
| 2070 | + db_path = tmp_path / 'timestamp_regression.db' |
| 2071 | + db_url = f'sqlite+aiosqlite:///{db_path}' |
| 2072 | + app_name = 'my_app' |
| 2073 | + user_id = 'user' |
| 2074 | + |
| 2075 | + service = DatabaseSessionService(db_url) |
| 2076 | + try: |
| 2077 | + session = await service.create_session(app_name=app_name, user_id=user_id) |
| 2078 | + event = Event(author='user', timestamp=time.time()) |
| 2079 | + await service.append_event(session, event) |
| 2080 | + finally: |
| 2081 | + await service.close() |
| 2082 | + |
| 2083 | + # Directly overwrite the stored timestamp with a raw float via raw SQL, |
| 2084 | + # simulating a REAL-affinity column value that bypassed SQLAlchemy's |
| 2085 | + # normal text-based DateTime serialization. |
| 2086 | + raw_epoch_float = time.time() |
| 2087 | + conn = sqlite3.connect(str(db_path)) |
| 2088 | + try: |
| 2089 | + conn.execute( |
| 2090 | + 'UPDATE events SET timestamp = ? WHERE session_id = ?', |
| 2091 | + (raw_epoch_float, session.id), |
| 2092 | + ) |
| 2093 | + conn.commit() |
| 2094 | + finally: |
| 2095 | + conn.close() |
| 2096 | + |
| 2097 | + # Read it back with a fresh service instance; this must not raise |
| 2098 | + # TypeError: fromisoformat: argument must be str. |
| 2099 | + service2 = DatabaseSessionService(db_url) |
| 2100 | + try: |
| 2101 | + retrieved_session = await service2.get_session( |
| 2102 | + app_name=app_name, user_id=user_id, session_id=session.id |
| 2103 | + ) |
| 2104 | + finally: |
| 2105 | + await service2.close() |
| 2106 | + |
| 2107 | + assert retrieved_session is not None |
| 2108 | + assert len(retrieved_session.events) == 1 |
| 2109 | + assert retrieved_session.events[0].timestamp == pytest.approx( |
| 2110 | + raw_epoch_float, abs=1.0 |
| 2111 | + ) |
0 commit comments