#include "store/sqlite.hpp" #include #include #include namespace velox::daemon::store { // --- Db ------------------------------------------------------------------------------ Db::~Db() { if (db_ != nullptr) sqlite3_close(db_); } Db::Db(Db&& o) noexcept : db_(std::exchange(o.db_, nullptr)) {} Db& Db::operator=(Db&& o) noexcept { if (this != &o) { if (db_ != nullptr) sqlite3_close(db_); db_ = std::exchange(o.db_, nullptr); } return *this; } DbError Db::last_error() const { return DbError{sqlite3_extended_errcode(db_), sqlite3_errmsg(db_)}; } DbResult Db::open(const std::string& path) { sqlite3* handle = nullptr; const int rc = sqlite3_open_v2( path.c_str(), &handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, nullptr); if (rc != SQLITE_OK) { DbError e{rc, handle != nullptr ? sqlite3_errmsg(handle) : "sqlite3_open_v2 failed"}; if (handle != nullptr) sqlite3_close(handle); return std::unexpected(std::move(e)); } Db db(handle); // Not a secret store (credentials go to the Secret Service), but task URLs and pairing // hashes still are not world-readable. SQLite honours the umask; pin 0600 explicitly. if (path != ":memory:" && !path.empty() && path.front() != ':') { ::chmod(path.c_str(), 0600); ::chmod((path + "-wal").c_str(), 0600); ::chmod((path + "-shm").c_str(), 0600); } // WAL for crash-safe concurrent readers (docs/01 ยง1). busy_timeout so a writer waits // rather than returning SQLITE_BUSY under the RPC loop. foreign_keys is per-connection. for (const char* pragma : {"PRAGMA journal_mode=WAL", "PRAGMA synchronous=NORMAL", "PRAGMA foreign_keys=ON", "PRAGMA busy_timeout=5000"}) { if (auto r = db.exec(pragma); !r) return std::unexpected(r.error()); } return db; } DbResult Db::exec(std::string_view sql) { char* err = nullptr; const int rc = sqlite3_exec(db_, std::string(sql).c_str(), nullptr, nullptr, &err); if (rc != SQLITE_OK) { DbError e{rc, err != nullptr ? err : sqlite3_errmsg(db_)}; sqlite3_free(err); return std::unexpected(std::move(e)); } return {}; } DbResult Db::prepare(std::string_view sql) { sqlite3_stmt* s = nullptr; const int rc = sqlite3_prepare_v2(db_, sql.data(), static_cast(sql.size()), &s, nullptr); if (rc != SQLITE_OK) return std::unexpected(last_error()); return Stmt(db_, s); } std::int64_t Db::user_version() { auto st = prepare("PRAGMA user_version"); if (!st) return -1; auto row = st->step(); if (!row || !*row) return -1; return st->column_int(0); } DbResult Db::set_user_version(std::int64_t v) { // PRAGMA does not accept a bound parameter; the value is our own integer. return exec("PRAGMA user_version=" + std::to_string(v)); } // --- Stmt ---------------------------------------------------------------------------- Stmt::~Stmt() { if (stmt_ != nullptr) sqlite3_finalize(stmt_); } Stmt::Stmt(Stmt&& o) noexcept : db_(std::exchange(o.db_, nullptr)), stmt_(std::exchange(o.stmt_, nullptr)) {} Stmt& Stmt::operator=(Stmt&& o) noexcept { if (this != &o) { if (stmt_ != nullptr) sqlite3_finalize(stmt_); db_ = std::exchange(o.db_, nullptr); stmt_ = std::exchange(o.stmt_, nullptr); } return *this; } DbError Stmt::last_error() const { return DbError{sqlite3_extended_errcode(db_), sqlite3_errmsg(db_)}; } DbResult Stmt::bind(int i, std::int64_t v) { if (sqlite3_bind_int64(stmt_, i, v) != SQLITE_OK) return std::unexpected(last_error()); return {}; } DbResult Stmt::bind(int i, std::string_view v) { if (sqlite3_bind_text(stmt_, i, v.data(), static_cast(v.size()), SQLITE_TRANSIENT) != SQLITE_OK) return std::unexpected(last_error()); return {}; } DbResult Stmt::bind_null(int i) { if (sqlite3_bind_null(stmt_, i) != SQLITE_OK) return std::unexpected(last_error()); return {}; } DbResult Stmt::step() { const int rc = sqlite3_step(stmt_); if (rc == SQLITE_ROW) return true; if (rc == SQLITE_DONE) return false; return std::unexpected(last_error()); } DbResult Stmt::reset() { if (sqlite3_reset(stmt_) != SQLITE_OK) return std::unexpected(last_error()); return {}; } std::int64_t Stmt::column_int(int i) const { return sqlite3_column_int64(stmt_, i); } std::string Stmt::column_text(int i) const { const auto* p = sqlite3_column_text(stmt_, i); if (p == nullptr) return {}; return std::string(reinterpret_cast(p), static_cast(sqlite3_column_bytes(stmt_, i))); } bool Stmt::column_is_null(int i) const { return sqlite3_column_type(stmt_, i) == SQLITE_NULL; } } // namespace velox::daemon::store