Skip to content

Commit 13787a5

Browse files
donggyun112copybara-github
authored andcommitted
fix: keep SQLite session state numbers exact across updates
Merge #7426 PiperOrigin-RevId: 996043876
1 parent 4d31fa9 commit 13787a5

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

‎src/google/adk/sessions/sqlite_session_service.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,22 @@
5252
# Merges {delta} into {state} with dict.update() semantics: keys in the delta
5353
# always win with their delta value (including SQL NULL / JSON null), unlike
5454
# json_patch() which deep-merges dict values and treats null as "delete key".
55+
#
56+
# json_group_object writes a REAL with 15 significant digits, which rounds
57+
# floats such as 0.1 + 0.2 for every key in the row. A REAL is written with 17
58+
# digits when 15 do not read back as the same value, so floats round-trip.
5559
_MERGE_STATE_SQL = """
5660
SELECT json_group_object(
5761
key,
5862
CASE
5963
WHEN type IN ('object','array') THEN json(value)
6064
WHEN type IN ('true','false') THEN json(type)
65+
WHEN type = 'real' THEN json(
66+
CASE
67+
WHEN CAST(printf('%!.15g', value) AS REAL) = value
68+
THEN printf('%!.15g', value)
69+
ELSE printf('%!.17g', value)
70+
END)
6171
ELSE value
6272
END)
6373
FROM (

‎tests/unittests/sessions/test_session_service.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,63 @@ async def test_user_state_none_valued_delta_is_stored_not_dropped(
12711271
assert session1.state.get('user:pref') is None
12721272

12731273

1274+
# Floats that need more than 15 significant digits to read back unchanged.
1275+
_EXACT_NUMBERS = {
1276+
'ratio': 0.1 + 0.2,
1277+
'epoch_seconds': 1727500000.1234567,
1278+
}
1279+
1280+
1281+
@pytest.mark.asyncio
1282+
@pytest.mark.parametrize('prefix', ['', 'user:', 'app:'])
1283+
async def test_numbers_survive_unrelated_state_delta(session_service, prefix):
1284+
"""Stored numbers read back unchanged after an unrelated state_delta."""
1285+
numbers = {prefix + key: value for key, value in _EXACT_NUMBERS.items()}
1286+
session = await session_service.create_session(
1287+
app_name='my_app', user_id='u1', session_id='s1', state=numbers
1288+
)
1289+
event = Event(
1290+
invocation_id='inv1',
1291+
author='user',
1292+
actions=EventActions(state_delta={prefix + 'other': 1}),
1293+
)
1294+
await session_service.append_event(session=session, event=event)
1295+
1296+
reloaded = await session_service.get_session(
1297+
app_name='my_app', user_id='u1', session_id='s1'
1298+
)
1299+
1300+
assert {key: reloaded.state[key] for key in numbers} == numbers
1301+
1302+
1303+
@pytest.mark.asyncio
1304+
@pytest.mark.parametrize('prefix', ['', 'user:', 'app:'])
1305+
async def test_number_valued_state_delta_is_stored_exactly(
1306+
session_service, prefix
1307+
):
1308+
"""Numbers written by a state_delta read back unchanged."""
1309+
numbers = {prefix + key: value for key, value in _EXACT_NUMBERS.items()}
1310+
# Seed the scope so the delta is merged into existing state.
1311+
session = await session_service.create_session(
1312+
app_name='my_app',
1313+
user_id='u1',
1314+
session_id='s1',
1315+
state={prefix + 'seed': 1},
1316+
)
1317+
event = Event(
1318+
invocation_id='inv1',
1319+
author='user',
1320+
actions=EventActions(state_delta=numbers),
1321+
)
1322+
await session_service.append_event(session=session, event=event)
1323+
1324+
reloaded = await session_service.get_session(
1325+
app_name='my_app', user_id='u1', session_id='s1'
1326+
)
1327+
1328+
assert {key: reloaded.state[key] for key in numbers} == numbers
1329+
1330+
12741331
@pytest.mark.asyncio
12751332
async def test_temp_state_is_not_persisted_in_state_or_events(session_service):
12761333
app_name = 'my_app'

0 commit comments

Comments
 (0)