You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3.1 KiB

+++ title = "SQLite cache for list performance" priority = 3 status = "todo" ticket_type = "feature" dependencies = [] +++ Add an optional SQLite cache in .nbd/cache.db to accelerate nbd list and nbd ready for large ticket stores.

Motivation

list_tickets currently does O(n) file reads on every call. For stores with hundreds of tickets this is measurably slow. A SQLite cache avoids re-reading unchanged files by comparing file modification times (mtimes).

Approach

Crate dependency

Add turso to Cargo.toml (file-based SQLite, no sync feature needed):

turso = "0.4.3"

Note: Turso requires tokio. The existing async-std runtime in nbd must be replaced with tokio (or a compatibility shim used). Recommendation: switch #[async_std::main] to #[tokio::main] and update Cargo.toml accordingly.

store.rs additions

New async function open_cache(root: &Path) -> Result<turso::Connection>:

  • Opens (or creates) .nbd/cache.db via turso::Builder::new_local(path).build().await?.
  • Runs a migration: CREATE TABLE IF NOT EXISTS tickets (id TEXT PRIMARY KEY, json TEXT NOT NULL, mtime INTEGER NOT NULL).

New function list_tickets_cached(root: &Path) -> Result<Vec<Ticket>>:

  1. Open cache via open_cache.
  2. Read directory listing to get file names and mtimes.
  3. For each file: query the DB (connection.query(...).await?) for a row with matching mtime; if found, use cached JSON; otherwise read file, parse, and upsert with connection.execute(...).await?.
  4. Delete DB rows for IDs no longer on disk.
  5. Return deserialized tickets sorted by priority desc.

Keep existing list_tickets as the non-cached fallback. cmd_list and cmd_ready use list_tickets_cached, falling back to list_tickets on error.

Turso API reference

  • Open local file DB: turso::Builder::new_local("path/to/file.db").build().await?
  • Execute (INSERT/UPDATE/DELETE): conn.execute("SQL", params).await?
  • Query rows: let mut rows = conn.query("SQL", params).await?
  • Iterate rows: while let Some(row) = rows.next().await? { row.get_value(0)? }

Migration strategy

  • The cache is always optional. If cache.db can't be opened or any cache operation fails, fall back to list_tickets (log a warning to stderr).
  • The cache is never the source of truth — the JSON files are. The cache is always reconstructable by deleting .nbd/cache.db.

Decision point

Decide whether to enable the cache unconditionally or gate it behind a flag (--cache / NBD_CACHE=1). Recommendation: enable by default once the feature is stable.

Tests

  • Unit test: cache hit returns same data as direct file read.
  • Unit test: cache miss (mtime changed) re-reads the file.
  • Unit test: deleted ticket is evicted from cache.
  • Performance test (optional): benchmark 1000-ticket list with and without cache.

Files touched

  • Cargo.toml — add turso, replace async-std with tokio
  • src/main.rs — switch to #[tokio::main]
  • src/store.rsopen_cache, list_tickets_cached
  • src/tests.rs — cache unit tests
  • docs/ARCHITECTURE.md — document the cache layer