daemon: store/ query layer — tasks + settings (prep for the download.add vertical slice)

The read/write surface the dispatcher handlers need, so download.add
persists and download.list / download.get project real rows when the
engine lands.

- store/tasks — TaskRow (1:1 with the schema), insert / get / remove /
  set_state / count, and list(filter, sort, offset, limit) that does
  all the WHERE / ORDER BY / LIMIT in SQL (M1 DoD: a 1000-row list
  never materialised client-side). Filter covers states / category /
  queue / case-insensitive filename+url substring / date range; sort
  is a whitelisted column + direction with NULLs last, default
  newest-first; the enum spellings in a state IN (...) come from
  proto::to_string, never from user text. to_summary() projects a row
  onto proto::TaskSummary including the flattened error block when the
  task failed / retry_wait / auto-paused.
- store/settings — key -> JSON-text with a built-in default table
  mirroring Settings.schema.json / ADR 0012; get_raw / set_raw /
  overrides plus typed get_int / get_string / get_string_array for the
  governor config and saveTo.allowedRoots. Full settings.get/set wire
  projection lands with those handlers.
- veloxd_store now links velox::proto + nlohmann_json for the
  projection.

Test veloxd.store_tasks: insert/get round trip, PK duplicate rejected,
the error-block projection, list total+paging+sort+every filter,
set_state pause_reason clear-on-unpause, remove, and settings default
vs override. ASan+UBSan and TSan clean; 34 daemon/cli tests green.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
2026-09-10 19:43:13 +04:00
co-authored by Claude Sonnet 5
parent ab479e7885
commit 6787784936
7 changed files with 754 additions and 1 deletions
+89
View File
@@ -0,0 +1,89 @@
#pragma once
// Read/write access to the `tasks` table, plus the projection onto the wire TaskSummary.
// download.list does its filtering, sorting and paging here (M1 DoD: a 1000-row list under
// 50 ms, never materialised client-side).
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "store/sqlite.hpp"
#include "velox_proto.hpp"
namespace velox::daemon::store {
// One row of `tasks`, 1:1 with the schema. std::optional maps a NULL column.
struct TaskRow {
std::string task_id;
std::string url;
std::string save_dir;
std::string filename;
std::string state = "new";
std::string start_mode = "auto";
std::string created_at;
std::optional<std::string> effective_url;
std::optional<std::string> category_id;
std::optional<std::string> queue_id;
std::optional<std::string> description;
std::optional<std::string> pause_reason;
std::optional<std::string> etag;
std::optional<std::string> last_modified;
std::optional<std::string> content_type;
std::optional<std::string> last_try_at;
std::optional<std::string> completed_at;
std::optional<std::string> checksum_algo;
std::optional<std::string> checksum_value;
std::optional<std::int64_t> size_bytes;
std::int64_t downloaded_bytes = 0;
bool resumable = false;
std::optional<std::int64_t> req_segments;
std::int64_t eff_segments = 0;
std::optional<std::int64_t> req_buffer_bytes;
std::optional<std::int64_t> eff_buffer_bytes;
std::optional<std::int64_t> queue_position;
std::optional<std::string> error_code;
std::optional<std::string> error_message;
std::optional<std::int64_t> error_http_status;
std::optional<bool> error_retryable;
std::optional<std::int64_t> error_attempt;
std::optional<std::string> error_next_retry_at;
};
class Tasks {
public:
explicit Tasks(Db& db) : db_(db) {}
DbResult<void> insert(const TaskRow& row);
DbResult<std::optional<TaskRow>> get(std::string_view task_id);
struct Page {
std::int64_t total = 0; // rows matching the filter, ignoring paging
std::vector<TaskRow> rows;
};
DbResult<Page> list(const std::optional<velox::proto::TaskFilter>& filter,
const std::optional<velox::proto::TaskSort>& sort, std::int64_t offset,
std::int64_t limit);
// Move a task to `state`; `pause_reason` is written only when state == "paused"
// (cleared otherwise). Returns false if there is no such task.
DbResult<bool> set_state(std::string_view task_id, std::string_view state,
const std::optional<std::string>& pause_reason);
DbResult<bool> remove(std::string_view task_id);
DbResult<std::int64_t> count();
private:
Db& db_;
};
// Project a row onto the wire type. `state` and `error.code` strings are assumed valid
// (the CHECK constraints and the state machine keep them so).
velox::proto::TaskSummary to_summary(const TaskRow& row);
} // namespace velox::daemon::store