fix: sqlite-es snapshot sequence#18
Conversation
WalkthroughThe PR enhances snapshot handling in the SQLite event sourcing layer by introducing transactional atomicity and monotonic sequence guards. Snapshot updates now use ChangesSnapshot atomicity and sequence consistency
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
42b178c to
5bc8232
Compare
5bc8232 to
87d8fcd
Compare
87d8fcd to
00d5969
Compare
Merge activity
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/sqlite-es/src/event_repository.rs (1)
407-420:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftHandle pre-fix snapshot rows during upgrade.
This fixes the read/write contract for new snapshots, but rows already written by the old code path still have
last_sequence = next_snapshotandsnapshot_version = 0per the PR summary. Those rows will still deserialize with the wrongcurrent_sequenceafter upgrade, so affected aggregates keep double-applying events until some later write happens to replace the snapshot. Please add an upgrade path here (for example, ignore or clear legacysnapshot_version = 0rows so they rebuild from events, or ship a migration/backfill), and add a regression test that seeds one of those legacy rows rather than only round-tripping snapshots written by the fixed implementation.Also applies to: 539-577
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6a93bc05-fe77-4734-94e2-f0de7bc9e99a
📒 Files selected for processing (2)
crates/sqlite-es/src/event_repository.rscrates/sqlite-es/src/sql_query.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
- GitHub Check: hooks
- GitHub Check: examples
- GitHub Check: clippy
- GitHub Check: fmt
- GitHub Check: test
- GitHub Check: clippy
- GitHub Check: check
- GitHub Check: hooks
- GitHub Check: check
- GitHub Check: test
- GitHub Check: examples
- GitHub Check: fmt
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
When handling clippy errors about function lengths or cognitive complexity, don't split up functions more than necessary. Instead ask the user if we can add a clippy allow.
Files:
crates/sqlite-es/src/sql_query.rscrates/sqlite-es/src/event_repository.rs
crates/**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
crates/**/*.rs: Package by Feature, Not by Layer. Organize by business domain, not technical layers. FORBIDDEN catch-all modules: types.rs, error.rs, models.rs, utils.rs, helpers.rs, http.rs, dto.rs, entities.rs, services.rs, domain.rs. CORRECT: lifecycle.rs, projection.rs, reactor.rs, view_backend.rs. Each feature module contains ALL related code.
CRITICAL: NEVER write directly to the events table — no direct INSERTs, no manual sequence numbers, no bypassing CqrsFramework. Always use CqrsFramework::execute() or execute_with_metadata() to emit events through aggregate commands.
CRITICAL: Single CQRS Framework Instance Per Aggregate. In any consuming application, each aggregate must have exactly ONE SqliteCqrs, constructed once at startup. Never call sqlite_cqrs() or CqrsFramework::new() per request. Direct construction is fine in test/CLI/migration code.
Use cqrs-es Services for side-effects in handle() to ensure atomicity with events. Naming: {Action}er trait -> {Domain}Service implements -> {Domain}Manager orchestrates.
Log in command handlers, not callers. All logging for command execution belongs in the aggregate's handle() method. The handler has full aggregate state making log messages rich without the caller needing extra context.
Make invalid states unrepresentable through the type system. Use ADTs and enums to encode business rules and state transitions directly in types rather than runtime validation.
Accept domain newtypes and convert to SDK primitives inside the callee. Exception: cross-crate boundaries where the callee can't depend on caller's domain types — destructure at the call site.
Favor FP/ADT patterns over OOP. Use pattern matching, combinators, type-driven design. Prefer iterators over imperative loops unless it increases complexity. Use itertools for richer iterator chains.
Follow comprehensive commenting guidelines. Comment complex business logic, algorithm rationale, external system behavior, non-obvious constraints, test data context, ...
Files:
crates/sqlite-es/src/sql_query.rscrates/sqlite-es/src/event_repository.rs

Motivation
sqlite-es's snapshot persistence conflates two different counters:
persistwrites the third element of cqrs-es's snapshot tuple into the
last_sequencecolumn, but that element is the snapshot version counter (
next_snapshotfrom
PersistedEventStore::commit), not an event sequence. Reads compound thebug:
row_to_serialized_snapshothardcodescurrent_snapshot: 0, so theversion counter also never advances past 1. Any consumer using
PersistedEventStore::new_snapshot_storewith sqlite-es reloads a snapshotwhose state is at sequence N but whose
current_sequenceclaims 1, thenre-applies events 2..N that are already folded into the snapshot — silent
double-application that permanently fails
Lifecycle-wrapped aggregates onthe first post-snapshot load. The crate's own snapshot tests passed because
they wrote and read through the same misinterpretation.
Solution
persistderiveslast_sequencefromevents.last().sequence(mirroringevent-sorcery's internal repository) and treats the tuple's third element as
the snapshot version; an empty-events snapshot update is rejected with the
new
EmptySnapshotUpdateerror.update_snapshot/select_snapshotnow write and read thesnapshot_versioncolumn (already in the canonical migration — itsDEFAULT 0is what masked the omission), androw_to_serialized_snapshotpopulatesSerializedSnapshot::current_snapshotfrom it.
snapshot_round_trip_applies_events_exactly_once: a fullCqrsFramework::executeround trip through a snapshot-backed store thatrehydrates from a fresh store and asserts each event applied exactly once
(it reproduced
[a, b, b, c, b, c]before the fix).event-sorcery's internal repository follows separately.
Closes RAI-921.
Summary by CodeRabbit
Bug Fixes
Tests