fix(sessions): make SqliteSessionService state merges use dict.update() semantics - #6729
Open
chelsealong wants to merge 2 commits into
Open
fix(sessions): make SqliteSessionService state merges use dict.update() semantics#6729chelsealong wants to merge 2 commits into
chelsealong wants to merge 2 commits into
Conversation
…() semantics SqliteSessionService persisted state deltas with SQLite's json_patch() (RFC 7396 JSON Merge Patch), which deep-merges dict values and drops keys whose delta value is None. Every other BaseSessionService implementation uses dict.update() semantics: a dict-valued delta replaces the stored value outright, and a None-valued delta is stored as null. The mismatch meant the in-memory Session object and the row reloaded from SQLite could silently disagree after a restart. Replace the three json_patch() call sites with a query that merges via json_group_object, preferring delta keys and falling back to existing keys not present in the delta - shallow replace-per-key, matching dict.update(). Fixes google#6728
The json_group_object merge introduced for dict.update() semantics special-cased 'object'/'array' JSON types to re-wrap with json(value), but json_each() reports JSON true/false as type='true'/'false' with value already coerced to the bare integer 1/0. Those fell through to the ELSE branch and were emitted as raw SQL integers, silently turning every stored boolean into an int on the next unrelated state_delta. Route 'true'/'false' through json(type) as well, since type is literally the string 'true' or 'false'.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6728
Problem
SqliteSessionServicepersists state deltas with SQLite'sjson_patch(),which implements RFC 7396 JSON Merge Patch. Every other
BaseSessionServiceimplementation (
InMemorySessionService,DatabaseSessionService) appliesdict.update()semantics instead. Two consequences, both silent:replacing it, so keys written on earlier turns survive a full overwrite.
None-valued delta deletes the key instead of storingnull.The in-memory
Sessionobject is updated viadict.update()byBaseSessionService._update_session_state, so a single service contradictsitself: the live object and the row reloaded from SQLite disagree, and the
disagreement only surfaces after a reload or restart. Since
create_session_service_from_optionsreturns the SQLite-backed localservice by default,
adk run/adk webhit this out of the box.Fix
Replace the three
json_patch()call sites insqlite_session_service.py(
_upsert_app_state,_upsert_user_state,_update_session_state_in_db)with a query built on
json_each/json_group_objectthat mergesdelta keys over existing keys — a delta key always wins with its own
value (including SQL/JSON
null), and existing keys not present in thedelta are kept as-is. This is a shallow, per-key replace, matching
dict.update(), while remaining a single atomic SQL statement (noread-modify-write round trip) and using no JSON1 function newer than
json_patchitself.Follow-up fix (boolean corruption): the first version of this merge
SQL special-cased
type IN ('object','array')to re-wrap withjson(value), falling through to the rawvaluecolumn otherwise.json_each()reports JSONtrue/falseastype='true'/'false'withvaluealready coerced to the bare SQL integer1/0. Those fell intothe
ELSEbranch and were emitted as raw integers byjson_group_object, silently turning every stored boolean into an int(
{"flag": false}->{"flag": 0}) on the very next unrelatedstate_deltaapplied to that row — including keys that were never partof the delta, since existing keys go through the same reconstruction.
Fixed by also routing
type IN ('true','false')throughjson(type)(the
typecolumn is literally the string'true'/'false', sojson(type)yields the JSON literal, not a quoted string).Testing plan
Added three conformance tests to the shared, four-way parametrized
session_servicefixture intests/unittests/sessions/test_session_service.py(
IN_MEMORY,IN_MEMORY_WITH_LIGHT_COPY_ENABLED,DATABASE,SQLITE), soSqliteSessionServiceis checked against the same contract as every otherbackend:
test_dict_valued_state_delta_replaces_stored_value— a dict-valueddelta replaces the stored dict rather than deep-merging into it.
test_none_valued_state_delta_is_stored_not_dropped— aNone-valueddelta is stored as
null, not dropped.test_boolean_state_survives_unrelated_state_delta— a stored booleankeeps its type (and value) across an unrelated
state_delta.Confirmed all three fail without their respective fix. For the first two,
reverted the source change with
git checkout HEAD~1 -- src/google/adk/sessions/sqlite_session_service.py(pre-PR
json_patch()code), reran, restored:For the boolean-corruption test, reverted only the follow-up fix (checked
out this PR's first commit's version of the merge SQL, i.e. without the
type IN ('true','false')branch), reran, restored:With the fix applied:
Also ran formatting/lint tools used by this repo's pre-commit hooks
(all clean, no reformatting needed):
Scope
Only
sqlite_session_service.py(production fix) andtest_session_service.py(new tests) are touched. No dependencymanifests, CI/workflow files, or generated files were modified.
AI-assistance disclosure
This PR was prepared by an autonomous coding agent (Claude, via an
internal OSS-contribution pipeline), reviewed and pushed under this
account. The reproduction and root-cause analysis in the linked issue
were written by a different AI-assisted contributor and independently
re-verified here (ran the repro cases against the code before writing
the fix); the SQL fix, tests, and this PR are original to this run.