daemon: store/ — SQLite WAL schema + forward-only migrator (build step 3)

The daemon's persistent state. SQLite in WAL mode, foreign keys on,
5 s busy timeout so a writer waits rather than SQLITE_BUSY under the
RPC loop.

- store/sqlite — RAII Db/Stmt over the C API; errors returned as
  DbResult<T> (std::expected), never thrown — the RPC loop must not
  unwind. transaction() helper: BEGIN / fn / COMMIT, ROLLBACK on error.
- store/migrations/0001_initial.sql — the eight tables from the brief:
  settings, categories, queues, tasks, segments, rules, history,
  pairings. Notable choices:
    * tasks columns project onto proto TaskSummary with no computation;
      requested vs effective segments/buffer split per ADR 0010/0012;
      pause_reason column per ADR 0013.
    * segments end_byte is NOT constrained >= 0 so a whole-file
      zero-length download is one row with end_byte = -1 (ADR 0010 B3a).
    * pairings stores only token_sha256 — the plaintext token is
      returned once from session.pair and never persisted (CLAUDE.md §4).
    * indices on tasks(state), (category_id), (queue_id, queue_position),
      (created_at), (completed_at) for the "1000 tasks, download.list
      under 50 ms" DoD.
    * six built-in categories + a Main queue seeded.
- store/migrations — runs every embedded migration past PRAGMA
  user_version, each in its own transaction, forward-only. SQL files
  are embedded at build time by cmake/embed_migrations.cmake.

Test veloxd.store_migrations (ASan+UBSan and TSan clean): fresh DB ->
head, all tables present, seed rows, FK cascade (segment orphan
rejected, task delete cascades), the end_byte=-1 zero-length case,
idempotent re-run, and forward-only from every released user_version.

Also: daemon/docs/proto-requests-m1.md — P1 marked landed on lane/proto
as 1.4.0 (HandlerError/HandlerResult), to be adopted in rpc/ once that
merges to main; P2 resolved.

Not linked into the running daemon yet — the store is wired to the
dispatcher when download.add/list/get get real bodies, next.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
2026-09-10 15:27:20 +04:00
co-authored by Claude Sonnet 5
parent 0c7ce1437c
commit 4b279e8271
11 changed files with 731 additions and 1 deletions
+38
View File
@@ -0,0 +1,38 @@
#pragma once
// The schema migrator. Numbered SQL files in store/migrations/ are embedded at build time
// (see embed_migrations.cmake). On startup the daemon calls migrate_to_head(db): every
// migration whose version exceeds PRAGMA user_version is applied in order, each in its own
// transaction, and user_version is advanced to match.
//
// Forward-only: a released migration is immutable. The forward-only test in
// daemon/tests replays from every prior released user_version to head.
#include <cstdint>
#include <span>
#include <string_view>
#include "store/sqlite.hpp"
namespace velox::daemon::store {
struct Migration {
std::int64_t version; // 1, 2, 3, ... ; matches the NNNN_ prefix
std::string_view name; // e.g. "0001_initial"
std::string_view sql; // the file body
};
// The embedded set, sorted by version ascending. Defined in the generated header.
std::span<const Migration> embedded_migrations();
struct MigrationOutcome {
std::int64_t from_version = 0;
std::int64_t to_version = 0;
int applied = 0;
};
// Apply every migration newer than db.user_version(). A no-op (applied == 0) when the DB
// is already at or beyond the highest embedded version.
DbResult<MigrationOutcome> migrate_to_head(Db& db);
} // namespace velox::daemon::store