Skip to content

Commit 2da09e6

Browse files
abhiramArisecopybara-github
authored andcommitted
fix: handle sqlite float timestamps in PreciseTimestamp
Merge #6355 Closes: #6352 PiperOrigin-RevId: 949070334
1 parent 065234e commit 2da09e6

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
# limitations under the License.
1414
from __future__ import annotations
1515

16+
import datetime
1617
import json
18+
from typing import Any
19+
from typing import Callable
1720

1821
from sqlalchemy import Dialect
1922
from sqlalchemy import Text
@@ -65,3 +68,19 @@ def load_dialect_impl(self, dialect):
6568
if dialect.name == "mysql":
6669
return dialect.type_descriptor(mysql.DATETIME(fsp=6))
6770
return self.impl
71+
72+
def result_processor(
73+
self, dialect: Dialect, coltype: object
74+
) -> Callable[[Any], Any]: # Any: database values can be of any type
75+
impl_processor = self.impl.result_processor(dialect, coltype)
76+
77+
def process(value: Any) -> Any: # Any: database values can be of any type
78+
if value is None:
79+
return None
80+
if isinstance(value, (int, float)):
81+
return datetime.datetime.fromtimestamp(value, datetime.timezone.utc)
82+
if impl_processor:
83+
value = impl_processor(value)
84+
return value
85+
86+
return process

tests/unittests/sessions/test_session_service.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from datetime import timezone
1919
import enum
2020
import sqlite3
21+
import time
2122
from unittest import mock
2223

2324
from google.adk.errors.already_exists_error import AlreadyExistsError
@@ -2052,3 +2053,59 @@ async def test_database_session_service_requires_one_argument():
20522053
DatabaseSessionService(
20532054
db_url='sqlite+aiosqlite:///:memory:', db_engine=engine
20542055
)
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

Comments
 (0)