feat(poc/storage): add sqlite-storage spike
Proof-of-concept proving the SQLite-storage exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
"Schema, migration, and repository pattern are demonstrated."
Also satisfies the SRS-089 acceptance criteria explicitly:
"Storage implementation uses an embedded local data store and
migration mechanism."
Implements:
- A forward-only Migrator over a fixed Migration list, tracking
the applied version via PRAGMA user_version. Each migration is
applied inside an IMMEDIATE transaction; rolled back on failure.
- Three canonical migrations (initial schema, add nickname,
add last_connected_at) demonstrating ALTER TABLE flows.
- A LocalDatabaseRepository implementing both BookmarkRepository
and SettingsRepository traits.
- Bookmark.identity_ref is a reference to a secret name, never
a secret value (cross-checked by the secure-storage spike's
SS-AUD-001/002 scans). This is the SAD-067 separation.
Test suite (11/11 PASS on 2026-05-13):
- migrator brings fresh DB to latest version
- migrator is idempotent (no-op when already current)
- migrator applies only pending versions (catch-up upgrade)
- migrator rejects out-of-order versions
- migrator rejects DB newer than known migrations (downgrade guard)
- failed migration rolls back atomically
- bookmark CRUD round-trip
- bookmark list ordered by recency
- bookmark UNIQUE(host, identity_ref) enforcement
- settings upsert + delete
- open creates file and persists across reopen
Surfaced finding for the decision register: DEC-013 does not pin a
SQLite crate. The PoC uses rusqlite with the bundled feature
(no system libsqlite3 dependency); production code needs an
owner ruling on rusqlite vs. sqlx vs. sea-orm.
Authority: PoC plan §2, SRS-089, SDD-077, SAD-067,
SysDes-033/036/049/091.
Not product code; not promoted into chanora_storage.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# SQLite Storage Spike
|
||||
|
||||
Chanora proof-of-concept. **Not product code.**
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| PoC name | `sqlite-storage-spike` |
|
||||
| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 |
|
||||
| Purpose | Prove embedded local non-secret persistence |
|
||||
| Exit criterion | "Schema, migration, and repository pattern are demonstrated" |
|
||||
| Authority | SRS-089 (acceptance criteria: embedded data store + migration mechanism), SDD-077, SAD-067 |
|
||||
|
||||
## What it proves
|
||||
|
||||
- A **schema** managed entirely inside SQLite (`bookmarks`, `settings`)
|
||||
with a UNIQUE index, secondary indices, and `ON CONFLICT` upsert
|
||||
semantics for the settings table.
|
||||
- A **forward-only migration mechanism** keyed on
|
||||
`PRAGMA user_version`. Migrations are applied atomically inside an
|
||||
IMMEDIATE transaction and rolled back on failure.
|
||||
- A **repository pattern**: `BookmarkRepository` and
|
||||
`SettingsRepository` traits, implemented once by
|
||||
`LocalDatabaseRepository`. The rest of the Rust core can depend on
|
||||
the traits and avoid leaking SQLite-specific types upward
|
||||
(SAD-067, SDD-077).
|
||||
- **Cross-spike contract**: `Bookmark.identity_ref` is a *name*, not a
|
||||
secret. The secret behind that name lives in
|
||||
`SecretStorageRepository` (see `secure-storage-spike`).
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
sqlite-storage-spike/
|
||||
src/
|
||||
lib.rs # crate root, re-exports
|
||||
migrate.rs # Migrator + canonical migrations (v1..v3)
|
||||
repo.rs # traits, DTOs, LocalDatabaseRepository
|
||||
main.rs # sqlite-storage-cli driver
|
||||
tests/
|
||||
storage.rs # 11 tests — migration + repo + persistence
|
||||
Cargo.toml
|
||||
```
|
||||
|
||||
## Canonical migrations
|
||||
|
||||
| Version | Description |
|
||||
|---|---|
|
||||
| 1 | Initial `bookmarks` table + UNIQUE index on (server_host, identity_ref); `settings` key/value table. |
|
||||
| 2 | `ALTER TABLE bookmarks ADD COLUMN nickname TEXT`. |
|
||||
| 3 | `ALTER TABLE bookmarks ADD COLUMN last_connected_at INTEGER` + descending index. |
|
||||
|
||||
Future migrations are appended only. The migrator refuses to start if
|
||||
the DB's `user_version` exceeds the highest known migration (downgrade
|
||||
guard).
|
||||
|
||||
## Reproduce
|
||||
|
||||
Requires Rust stable (developed against 1.95). `rusqlite` is built with
|
||||
the `bundled` feature, so no system `libsqlite3` is needed.
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
cargo run --bin sqlite-storage-cli
|
||||
```
|
||||
|
||||
## Scope boundaries
|
||||
|
||||
- **Non-secret data only.** Any secret material (identity private
|
||||
keys, server passwords) belongs to `secure-storage-spike` /
|
||||
`SecretStorageRepository`. This is the SAD-067 separation.
|
||||
- **Single-process access.** Connection pooling, multi-process
|
||||
contention, and concurrent transactions are not exercised here.
|
||||
Production code will likely run on a single Rust core worker.
|
||||
- **No encryption at rest.** SQLCipher and similar are not in scope.
|
||||
Secrets must not land in this DB in the first place.
|
||||
- **No backup / restore.**
|
||||
- **No reverse migrations.** Forward-only is the documented policy;
|
||||
rollbacks require a fresh DB.
|
||||
- The migration-engine choice (`refinery`, `sqlx::migrate!`, etc.)
|
||||
remains open for the product crate. The PoC's ~50-line custom
|
||||
mechanism is intentional, to keep the audit surface explainable.
|
||||
|
||||
## Verification log
|
||||
|
||||
See `VERIFICATION.md` in this directory.
|
||||
Reference in New Issue
Block a user