From 9a62d1d9314c22c66e564084757ffb5d278cd7eb Mon Sep 17 00:00:00 2001 From: Mahir Puri Date: Fri, 16 Oct 2026 23:10:11 -0400 Subject: [PATCH] fix: keep integers beyond 64 bits exact in SqliteSessionService state SQLite reads an integer outside the signed 64-bit range as a REAL, so the state merge turned values like 0xFEDCBA9876543210 into floats. Rebuild those keys from the original JSON text (via ->) instead of the parsed value, and bind the session delta by number since it is now used more than once in the statement. Fixes #7576 --- .../adk/sessions/sqlite_session_service.py | 19 ++++++++++++++----- .../sessions/test_session_service.py | 4 +++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/google/adk/sessions/sqlite_session_service.py b/src/google/adk/sessions/sqlite_session_service.py index b0a25e0a3be..cbc1b935b12 100644 --- a/src/google/adk/sessions/sqlite_session_service.py +++ b/src/google/adk/sessions/sqlite_session_service.py @@ -56,12 +56,19 @@ # json_group_object writes a REAL with 15 significant digits, which rounds # floats such as 0.1 + 0.2 for every key in the row. A REAL is written with 17 # digits when 15 do not read back as the same value, so floats round-trip. +# +# An integer outside the signed 64-bit range also comes out of json_each as a +# REAL (its type column still says 'integer'), so we can't rebuild it from +# value. The -> operator gives back the original JSON text of the element +# instead, which keeps those integers exact. _MERGE_STATE_SQL = """ SELECT json_group_object( key, CASE WHEN type IN ('object','array') THEN json(value) WHEN type IN ('true','false') THEN json(type) + WHEN type = 'integer' AND typeof(value) = 'real' + THEN json(raw) WHEN type = 'real' THEN json( CASE WHEN CAST(printf('%!.15g', value) AS REAL) = value @@ -71,9 +78,11 @@ ELSE value END) FROM ( - SELECT key, value, type FROM json_each({delta}) + SELECT key, value, type, {delta} -> fullkey AS raw + FROM json_each({delta}) UNION ALL - SELECT key, value, type FROM json_each({state}) + SELECT key, value, type, {state} -> fullkey AS raw + FROM json_each({state}) WHERE key NOT IN (SELECT key FROM json_each({delta})) ) """ @@ -675,10 +684,10 @@ async def _update_session_state_in_db( delta_json = json.dumps(delta) await db.execute( "UPDATE sessions SET" - f" state=({_MERGE_STATE_SQL.format(delta='?', state='state')})," - " update_time=? WHERE app_name=? AND user_id=? AND id=?", + # The delta is used several times in the merge, so bind it by number. + f" state=({_MERGE_STATE_SQL.format(delta='?1', state='state')})," + " update_time=?2 WHERE app_name=?3 AND user_id=?4 AND id=?5", ( - delta_json, delta_json, now, app_name, diff --git a/tests/unittests/sessions/test_session_service.py b/tests/unittests/sessions/test_session_service.py index 9b4e5776276..53e4584fbff 100644 --- a/tests/unittests/sessions/test_session_service.py +++ b/tests/unittests/sessions/test_session_service.py @@ -1272,10 +1272,12 @@ async def test_user_state_none_valued_delta_is_stored_not_dropped( assert session1.state.get('user:pref') is None -# Floats that need more than 15 significant digits to read back unchanged. +# Floats that need more than 15 significant digits to read back unchanged, plus +# an unsigned 64-bit hash that does not fit in a signed 64-bit integer. _EXACT_NUMBERS = { 'ratio': 0.1 + 0.2, 'epoch_seconds': 1727500000.1234567, + 'fingerprint': 0xFEDCBA9876543210, }