Checked for duplicates
I searched the open and closed issues for SQLite store batch rollback / partial commits and did not find an existing report.
Description
SqliteStore.batch() and AsyncSqliteStore.abatch() can partially persist changes when a later operation in the same batch raises.
Both SQLite store _cursor() context managers execute COMMIT in a finally block. As a result, they commit earlier mutations even when control exits the context because of an exception (or, for async code, cancellation).
One observable case is a TTL refresh performed by a GetOp before a later unserializable PutOp.
Reproduction
Sync:
from langgraph.store.base import GetOp, PutOp
from langgraph.store.sqlite import SqliteStore
original_expiration = "2000-01-01 00:00:00"
with SqliteStore.from_conn_string(
":memory:",
ttl={"default_ttl": 10, "refresh_on_read": True},
) as store:
store.setup()
store.put(("test",), "key", {"value": "original"})
store.conn.execute(
"UPDATE store SET expires_at = ? WHERE prefix = ? AND key = ?",
(original_expiration, "test", "key"),
)
try:
store.batch([
GetOp(("test",), "key", refresh_ttl=True),
PutOp(("test",), "invalid", {"value": object()}),
])
except TypeError:
pass
expires_at = store.conn.execute(
"SELECT expires_at FROM store WHERE prefix = ? AND key = ?",
("test", "key"),
).fetchone()[0]
assert expires_at == original_expiration # currently fails
The equivalent async reproduction using AsyncSqliteStore.abatch() has the same result.
Actual behavior
The batch raises TypeError, but the TTL refresh from the preceding GetOp is committed. The database is left in a partially updated state.
Expected behavior
A failed batch should be atomic: any exception should roll back all changes made in that cursor context, and the original exception should be re-raised. The async path should also roll back on cancellation.
Root cause and proposed fix
The sync and async SQLite store _cursor() context managers unconditionally commit in finally:
libs/checkpoint-sqlite/langgraph/store/sqlite/base.py
libs/checkpoint-sqlite/langgraph/store/sqlite/aio.py
I propose changing both context managers to:
COMMIT on normal exit;
ROLLBACK on any BaseException, then re-raise;
- add sync and async regression tests demonstrating that an earlier TTL refresh is not persisted when a later operation fails.
I have a tested implementation ready. If this approach is acceptable, could a maintainer please assign this issue to me? I will then open the PR in accordance with the contribution policy.
Checked for duplicates
I searched the open and closed issues for SQLite store batch rollback / partial commits and did not find an existing report.
Description
SqliteStore.batch()andAsyncSqliteStore.abatch()can partially persist changes when a later operation in the same batch raises.Both SQLite store
_cursor()context managers executeCOMMITin afinallyblock. As a result, they commit earlier mutations even when control exits the context because of an exception (or, for async code, cancellation).One observable case is a TTL refresh performed by a
GetOpbefore a later unserializablePutOp.Reproduction
Sync:
The equivalent async reproduction using
AsyncSqliteStore.abatch()has the same result.Actual behavior
The batch raises
TypeError, but the TTL refresh from the precedingGetOpis committed. The database is left in a partially updated state.Expected behavior
A failed batch should be atomic: any exception should roll back all changes made in that cursor context, and the original exception should be re-raised. The async path should also roll back on cancellation.
Root cause and proposed fix
The sync and async SQLite store
_cursor()context managers unconditionally commit infinally:libs/checkpoint-sqlite/langgraph/store/sqlite/base.pylibs/checkpoint-sqlite/langgraph/store/sqlite/aio.pyI propose changing both context managers to:
COMMITon normal exit;ROLLBACKon anyBaseException, then re-raise;I have a tested implementation ready. If this approach is acceptable, could a maintainer please assign this issue to me? I will then open the PR in accordance with the contribution policy.