74 add persistence backend sqlite for long term record history#75
Merged
lxsaah merged 15 commits intoFeb 21, 2026
Merged
Conversation
…or SQLite integration
- Introduced `Extensions` struct for storing typed state during builder configuration and record setup. - Updated `AimDbBuilder` and `AimDb` to use the new `Extensions` for external crate integration. feat: implement record.query handler for persistence - Added `handle_record_query` function to process `record.query` requests. - Integrated a type-erased query handler mechanism to allow persistence backends to register query handlers. feat: create SQLite persistence backend - Added `aimdb-persistence-sqlite` crate with a `SqliteBackend` implementation. - Implemented asynchronous storage and querying of records using SQLite. feat: define persistence backend trait and extensions - Created `PersistenceBackend` trait for pluggable persistence backends. - Added builder and record registration extensions for persistence configuration. feat: implement query extensions for AimDb - Added `AimDbQueryExt` trait to provide query capabilities for latest and range queries. - Integrated error handling and deserialization for queried results. test: add unit tests for persistence functionality - Implemented tests for storing, querying, and cleaning up records in the SQLite backend. - Verified correct behavior for pattern matching and time range queries.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an optional, pluggable persistence layer to AimDB that stores long-term record history (via a buffer-subscriber) and enables historical queries, with a concrete SQLite backend and core support for type-erased extensions and remote query delegation.
Changes:
- Added new crates
aimdb-persistence(traits + builder/record/query extension traits) andaimdb-persistence-sqlite(SQLite backend using a dedicated writer thread). - Extended
aimdb-corewith anExtensionsTypeMap, anon_start()lifecycle hook, and an AimXrecord.queryhandler delegating to a type-erased query function stored in extensions. - Updated workspace/build tooling (workspace members + Makefile) and added design/README/changelog documentation.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/design/022-M10-persistence-backend.md | Adds a full design proposal/spec for persistence + query APIs. |
| aimdb-persistence/src/backend.rs | Introduces PersistenceBackend, StoredValue, and QueryParams. |
| aimdb-persistence/src/builder_ext.rs | Adds .with_persistence() wiring, retention task, and AimX query handler registration. |
| aimdb-persistence/src/error.rs | Defines PersistenceError for persistence operations. |
| aimdb-persistence/src/ext.rs | Adds .persist() record-registration extension (subscriber that stores values). |
| aimdb-persistence/src/lib.rs | Crate entrypoint + re-exports for persistence API surface. |
| aimdb-persistence/src/query_ext.rs | Adds AimDbQueryExt (query_latest, query_range, query_raw). |
| aimdb-persistence/README.md | Documents installation, usage, API, and backend implementation guidance. |
| aimdb-persistence/Cargo.toml | Declares new crate, features, and dependencies. |
| aimdb-persistence/CHANGELOG.md | Adds changelog for the new crate. |
| aimdb-persistence-sqlite/src/lib.rs | Implements SQLite backend actor thread, SQL schema, queries, and tests. |
| aimdb-persistence-sqlite/README.md | Documents runtime requirements, architecture, and API for SQLite backend. |
| aimdb-persistence-sqlite/Cargo.toml | Declares SQLite backend crate + deps (rusqlite bundled, tokio sync). |
| aimdb-persistence-sqlite/CHANGELOG.md | Adds changelog for the new SQLite backend crate. |
| aimdb-core/src/extensions.rs | Adds Extensions TypeMap support to core. |
| aimdb-core/src/builder.rs | Wires Extensions + on_start() into builder + build lifecycle. |
| aimdb-core/src/typed_api.rs | Exposes RecordRegistrar::extensions() for external extensions like persistence. |
| aimdb-core/src/remote/mod.rs | Re-exports the new query-handler types for remote delegation. |
| aimdb-core/src/remote/handler.rs | Adds AimX record.query method delegating to extension-registered handler. |
| aimdb-core/src/lib.rs | Exports Extensions module/type from aimdb-core. |
| Cargo.toml | Adds new persistence crates to workspace members. |
| Makefile | Adds build/test/clippy/doc targets for new crates; includes them in fmt loops. |
| _external/embassy | Updates submodule pointer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.
Summary
Adds an optional persistence layer to AimDB that stores long-term record
history in SQLite. Persistence is implemented as a buffer subscriber
(
tap_raw) — just like.tap()— keeping it fully within AimDB's existingproducer–consumer architecture. No new write paths or special-casing in the
core.
Motivation
Validation messages arrive roughly once per hour per city. Combined with
simulatable: false, this creates a UX problem: the AccuracyPanel is empty onpage refresh, on first visit, and permanently in demo mode. The in-memory ring
buffer cannot solve this because it clears on restart.
Solution: persist validation records to SQLite and query "latest per city"
on page load.
New Crates
aimdb-persistenceTrait definitions and extension traits that wire persistence into the AimDB
lifecycle without touching
aimdb-core:PersistenceBackendtraitstore,query,cleanup)AimDbBuilderPersistExt.with_persistence(backend, retention)onAimDbBuilderRecordRegistrarPersistExt.persist("record::name")onRecordRegistrarAimDbQueryExt.query_latest()/.query_range()/.query_raw()onAimDb<R>PersistenceStateExtensionsTypeMap, shared between subscriber and query timePersistenceErrorNotConfigured,Backend(String),BackendShutdown,Serializationaimdb-persistence-sqliteConcrete SQLite backend:
"aimdb-sqlite"OS thread holds therusqlite::Connection— the async executor is never blockedPRAGMA journal_mode = WAL) allows concurrent readersmpsc::sync_channel(64)for backpressure;tokio::sync::oneshotfor async replyprepare_cached()on all hot paths (INSERT, DELETE, SELECT)::new()— no Tokio runtime required at construction timeROW_NUMBER() OVER (PARTITION BY record_name ORDER BY stored_at DESC)window query for efficient top-N-per-group without full table scansSqliteBackendclones are droppedChanges to
aimdb-coreextensions.rsExtensionsTypeMap (HashMap<TypeId, Box<dyn Any + Send + Sync>>), exposed onAimDbBuilderandAimDbbuilder.rson_start()hook for spawning tasks afterbuild();extensions()/extensions_mut()accessors;extensionsfield moved intoAimDbInnertyped_api.rsextensions()accessor onRecordRegistrarso.persist()can retrievePersistenceStateremote/handler.rsrecord.queryAimX protocol handler delegating to a type-erasedQueryHandlerFnstored inExtensionsAPI
Key Design Decisions
aimdb-corehas no dependency on persistence types; state flows through theExtensionsTypeMap.persist()requires onlyT: Serialize;.with_remote_access()is not neededquery_rangehas an explicitlimit_per_record: Option<usize>— opt-in unbounded (None) vs capped (Some(n)), preventing accidental OOM on high-frequency recordson_start()— a cleanup task runs once at startup then every 24 hours; cleanup errors are always surfaced (viatracing::warn!oreprintln!)record.queryAimX protocol —aimdb-corestores a type-erasedQueryHandlerFnregistered bywith_persistence(); the handler defaults tolimit_per_record = Some(1)when no explicit limit is provided by the clientReview Fixes Applied
Two issues identified during review were fixed before merge:
Lossy
i64 → u64cast on reads (aimdb-persistence-sqlite/src/lib.rs)row.get::<_, i64>(2)? as u64torow.get::<_, i64>(2).map(|v| v.max(0) as u64)?0instead of wrapping tou64::MAXUnbounded
query_range(aimdb-persistence/src/query_ext.rs)limit_per_record: Option<usize>as a 4th parameter to both the trait and implNoneTest Plan
aimdb-persistence-sqlite: store/query, time-range, retention cleanup, SQL LIKE pattern escapingcargo build— all affected cratescargo test— 4/4 pass, 0 failurescargo clippy -- -D warnings— zero warnings