Skip to content

fix: select latest SQLite checkpoint when timestamps tie - #920

Merged
elijahbenizzy merged 2 commits into
apache:mainfrom
Kuang-xianxin:codex/sqlite-latest-checkpoint-ties
Oct 17, 2026
Merged

elijahbenizzy merged 2 commits into
apache:mainfrom
Kuang-xianxin:codex/sqlite-latest-checkpoint-ties

Conversation

@Kuang-xianxin

@Kuang-xianxin Kuang-xianxin commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

SQLitePersister.load(partition_key, None) and its async counterpart can return an older checkpoint when several saves share a timestamp. SQLite's default CURRENT_TIMESTAMP has second precision, and the current query orders only by that timestamp. In a deterministic reproduction, saving sequence 11 followed by sequence 12 and loading after reopening the database returns sequence 11. Across applications it can also return the wrong app.

Changes

  • Use descending rowid as the secondary ordering key in the latest-across-apps query, after created_at.
  • Apply the same fix to synchronous SQLite and aiosqlite persistence.
  • Add deterministic regressions for same-app and cross-app saves, default and explicit partitions, partition isolation, database reopening, and explicit historical lookup. A separate control verifies that timestamps remain the primary ordering key.

This uses the rowid already present in tables created by these persisters; no schema migration or change to app-specific sequence selection is needed. The tie-breaker follows insertion order in the persister-managed table, not a portable identifier for externally rewritten database rows.

Related: #807 explicitly notes the timestamp-tie limitation in its test-only PR. This PR fixes the behavior in both implementations; it does not duplicate those construction/lifecycle tests or depend on that PR.

How I tested this

Python 3.13.14 on Windows, using actual SQLite/aiosqlite databases:

python -m pytest tests/core/test_persistence.py tests/integrations/persisters/test_b_aiosqlite.py -q -k load_latest
Before the fix: 8 failed, 2 passed

python -m pytest tests/core/test_persistence.py tests/integrations/persisters/test_b_aiosqlite.py tests/core/test_application.py -q --timeout=20
After the fix: 199 passed

Pre-commit passes on all four changed files: Black 23.11.0, isort 5.12.0, flake8 6.1.0, whitespace/EOF, AST, and ASF headers. The frontend hook was explicitly skipped because this changes only Python persistence; the full external-database test matrix was not run.

Notes

Checklist

  • PR has an informative and human-readable title
  • Changes are limited to a single goal
  • Applicable pre-commit checks pass (frontend hook excluded as described above)
  • Any change in functionality is tested
  • Existing database format and explicit historical lookup are preserved

Use insertion order to disambiguate same-second checkpoints in sync and async SQLite persisters. Add durable reopening and partition regressions.

Assisted-by: Codex
@github-actions github-actions Bot added area/core Application, State, Graph, Actions area/storage Persisters, state storage area/integrations External integrations (LLMs, frameworks) labels Sep 7, 2026
@Kuang-xianxin

Copy link
Copy Markdown
Contributor Author

There are two distinct CI blockers on current head 4b78349: Build Burr and Release Validation await approval, while documentation ended with startup_failure before creating any jobs.

Could a maintainer approve the pending runs and inspect the documentation workflow's startup failure? The successful labeling/mergeability checks do not run the SQLite checkpoint regressions. No source change or workflow rerun was made in this follow-up.

@sangkyoonnam sangkyoonnam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clean repro. This looks correct to me.

I ran the new tests locally. On main (8161159), 8 of the 10 load_latest tests fail. On the head, both test files pass, 60 tests. Without touching created_at, three saves within the same second on main make load("pk", None) return the first save, a-app seq 11. The head returns the last one, z-app.

rowid is the right tie-break for rows the persister inserts, since sequence_id doesn't compare across apps.

One thing outside this PR: list_app_ids orders SELECT DISTINCT app_id by created_at, which isn't tied to each app's newest row. Saving a, z, a at distinct seconds returns ['z-app', 'a-app'] on both main and the head. Is that order meant to mean anything, or should I file an issue?

@Kuang-xianxin

Copy link
Copy Markdown
Contributor Author

Thanks @sangkyoonnam for the independent same-second and reopen checks. Your 8/10 failures on main and 60 passing tests on this head are useful additional evidence; they are your results, separate from my earlier local runs.

I checked the list_app_ids query in both SQLite implementations. SELECT DISTINCT app_id ... ORDER BY created_at does not select each app's newest save, so #920 does not repair the ordering you observed. I also read your follow-up #942: its per-app aggregation and four-persister coverage address that separate problem. The notebook consumer described there makes the order practically significant. Keeping it in #942 avoids mixing that API behavior and query-cost tradeoff into this checkpoint-tie fix. I have not independently run #942 or its PostgreSQL tests.

This PR's head remains 4b78349. The latest API checks show that Build Burr and Release Validation changed from pending approval to failure without creating any jobs; those statuses do not provide test results. The earlier documentation startup failure is separate. This still needs upstream CI/review before merging.

@elijahbenizzy elijahbenizzy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct tie-break: sequence_id doesn't compare across apps, rowid does, and we own the table so it's never WITHOUT ROWID. Nice deterministic tests. Elijah

@elijahbenizzy
elijahbenizzy enabled auto-merge (squash) October 17, 2026 15:29
auto-merge was automatically disabled October 17, 2026 16:11

Pull request was closed

@elijahbenizzy elijahbenizzy reopened this Oct 17, 2026
@elijahbenizzy
elijahbenizzy enabled auto-merge (squash) October 17, 2026 16:11
# Conflicts:
#	tests/integrations/persisters/test_b_aiosqlite.py
@elijahbenizzy

Copy link
Copy Markdown
Contributor

Merged main into your branch to resolve a conflict with #942 in the test file (both sets of tests kept, your commits untouched). Auto-merge is armed; it lands when CI is green.

@elijahbenizzy
elijahbenizzy merged commit 42a2e71 into apache:main Oct 17, 2026
28 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Application, State, Graph, Actions area/integrations External integrations (LLMs, frameworks) area/storage Persisters, state storage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants