354 lines
12 KiB
C++
354 lines
12 KiB
C++
#include "config.hpp"
|
|
#include <boost/system/errc.hpp>
|
|
#include <boost/uuid/basic_random_generator.hpp>
|
|
#include <exception>
|
|
#include <ranczo-io/utils/config.hpp>
|
|
|
|
#include <sqlite3.h>
|
|
|
|
namespace ranczo {
|
|
/// UWAGA:
|
|
/// Funkcja zakłada, że pod adresem data()[size()] znajduje się legalna pamięć.
|
|
/// To jest bezpieczne tylko, jeśli string_view pochodzi z:
|
|
/// - C-stringa
|
|
/// - std::string
|
|
/// - innego bufora, który ma '\0' za końcem
|
|
///
|
|
/// Jeśli string_view zawiera dokładnie taki fragment:
|
|
/// "abc" → OK
|
|
/// "abc\0xyz" → będzie wykrywać '\0' po size()==3 → OK
|
|
/// Jeśli string_view powstał z danych binary/packed → UWAŻAJ.
|
|
|
|
inline bool is_null_terminated(std::string_view sv) {
|
|
return true;
|
|
// if(sv.empty())
|
|
// return true; // pusty string_view jest "C-stringiem"
|
|
|
|
// const char * ptr = sv.data();
|
|
// return ptr[sv.size()] == '\0';
|
|
}
|
|
|
|
std::string SettingsStore::Value::to_db_text() const {
|
|
using namespace boost::json;
|
|
object o;
|
|
|
|
std::visit(
|
|
[&](auto const & v) {
|
|
using U = std::decay_t< decltype(v) >;
|
|
|
|
if constexpr(std::is_same_v< U, int64_t >) {
|
|
o["type"] = "int";
|
|
o["value"] = static_cast< std::int64_t >(v);
|
|
} else if constexpr(std::is_same_v< U, double >) {
|
|
o["type"] = "double";
|
|
o["value"] = v;
|
|
} else if constexpr(std::is_same_v< U, std::string >) {
|
|
o["type"] = "string";
|
|
o["value"] = v;
|
|
} else if constexpr(std::is_same_v< U, bool >) {
|
|
o["type"] = "bool";
|
|
o["value"] = v;
|
|
} else if constexpr(std::is_same_v< U, Json >) {
|
|
o["type"] = "json";
|
|
o["value"] = v;
|
|
} else {
|
|
static_assert(sizeof(U) == 0, "Unhandled variant type");
|
|
}
|
|
},
|
|
data_);
|
|
|
|
return serialize(o);
|
|
}
|
|
|
|
SettingsStore::Value SettingsStore::Value::from_db_text(std::string_view s) {
|
|
using namespace boost::json;
|
|
value j;
|
|
|
|
try {
|
|
j = parse(s);
|
|
} catch(const std::exception & e) {
|
|
spdlog::error("Value::from_db_text parse error: {}", e.what());
|
|
// traktujemy to jako zwykły string
|
|
return Value{s};
|
|
}
|
|
|
|
if(!j.is_object()) {
|
|
// traktujemy jako ogólny JSON
|
|
return Value{j};
|
|
}
|
|
|
|
object & o = j.as_object();
|
|
auto * t_it = o.if_contains("type");
|
|
auto * v_it = o.if_contains("value");
|
|
|
|
if(!t_it || !v_it || !t_it->is_string()) {
|
|
// coś dziwnego -> traktuj jako ogólny JSON
|
|
return Value{j};
|
|
}
|
|
|
|
std::string type = std::string(t_it->as_string().c_str());
|
|
|
|
try {
|
|
if(type == "int") {
|
|
return Value{static_cast< int64_t >(v_it->as_int64())};
|
|
} else if(type == "double") {
|
|
return Value{v_it->as_double()};
|
|
} else if(type == "string") {
|
|
return Value{std::string(v_it->as_string().c_str())};
|
|
} else if(type == "bool") {
|
|
return Value{v_it->as_bool()};
|
|
} else if(type == "json") {
|
|
return Value{*v_it};
|
|
} else {
|
|
// nieznany typ -> zachowaj jako cały JSON
|
|
return Value{j};
|
|
}
|
|
} catch(const std::exception & e) {
|
|
spdlog::error("Value::from_db_text type conversion error: {}", e.what());
|
|
// fallback na ogólny json
|
|
return Value{j};
|
|
}
|
|
}
|
|
|
|
SettingsStore::SettingsStore(std::string_view app_db_path, executor_type main_exec, std::size_t db_threads)
|
|
: main_exec_(main_exec), db_pool_(db_threads), db_path_(app_db_path) {
|
|
open_or_create();
|
|
prepare_schema();
|
|
}
|
|
|
|
SettingsStore::~SettingsStore() {
|
|
if(db_) {
|
|
sqlite3_close(db_);
|
|
db_ = nullptr;
|
|
}
|
|
db_pool_.join();
|
|
}
|
|
|
|
bool SettingsStore::contains(std::string_view component, std::string_view key) const {
|
|
BOOST_ASSERT(component.size() < 256);
|
|
BOOST_ASSERT(key.size() < 256);
|
|
BOOST_ASSERT(is_null_terminated(component));
|
|
BOOST_ASSERT(is_null_terminated(key));
|
|
|
|
spdlog::debug("SettingsStore::contains_sync({}, {})", component, key);
|
|
|
|
const char * sql =
|
|
"SELECT 1 FROM settings "
|
|
"WHERE component = ?1 AND key = ?2 LIMIT 1";
|
|
|
|
sqlite3_stmt * stmt = nullptr;
|
|
check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare SELECT contains");
|
|
|
|
auto stmt_guard = make_stmt_guard(stmt);
|
|
|
|
check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), -1, SQLITE_TRANSIENT), "bind component");
|
|
check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key");
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
if(rc == SQLITE_ROW) {
|
|
return true;
|
|
}
|
|
if(rc == SQLITE_DONE) {
|
|
return false;
|
|
}
|
|
|
|
check_sqlite(rc, "step SELECT contains");
|
|
return false;
|
|
}
|
|
|
|
awaitable< bool > SettingsStore::async_contains(std::string_view component, std::string_view key) const noexcept {
|
|
BOOST_ASSERT(component.size() < 256);
|
|
BOOST_ASSERT(key.size() < 256);
|
|
BOOST_ASSERT(is_null_terminated(component));
|
|
BOOST_ASSERT(is_null_terminated(key));
|
|
|
|
auto caller_exec = co_await boost::asio::this_coro::executor;
|
|
bool exists = false;
|
|
|
|
// Przeskok do puli DB
|
|
co_await boost::asio::post(db_pool_, boost::asio::use_awaitable);
|
|
|
|
try {
|
|
exists = contains(component, key);
|
|
_log.debug("{}/{} is {}", component, key, exists ? "awailable" : "unawailable");
|
|
} catch(const std::exception & e) {
|
|
spdlog::error("async_contains exception: {}", e.what());
|
|
exists = false;
|
|
}
|
|
|
|
// Z powrotem na executor wywołujący
|
|
co_await boost::asio::post(caller_exec, boost::asio::use_awaitable);
|
|
_log.debug("{}/{} is {}", component, key, exists ? "awailable" : "unawailable");
|
|
co_return exists;
|
|
}
|
|
|
|
void SettingsStore::save(std::string_view component, std::string_view key, const Value & value) {
|
|
BOOST_ASSERT(component.size() < 256);
|
|
BOOST_ASSERT(key.size() < 256);
|
|
BOOST_ASSERT(is_null_terminated(component));
|
|
BOOST_ASSERT(is_null_terminated(key));
|
|
|
|
spdlog::debug("SettingsStore::save_sync({}, {})", component, key);
|
|
|
|
const char * sql =
|
|
"INSERT INTO settings(component, key, value) "
|
|
"VALUES(?1, ?2, ?3) "
|
|
"ON CONFLICT(component, key) "
|
|
"DO UPDATE SET value = excluded.value";
|
|
|
|
sqlite3_stmt * stmt = nullptr;
|
|
check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare INSERT/UPDATE");
|
|
auto stmt_guard = make_stmt_guard(stmt);
|
|
|
|
check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), static_cast<int>(component.size()), SQLITE_TRANSIENT), "bind component");
|
|
check_sqlite(sqlite3_bind_text(stmt, 2, key.data(),static_cast<int>(key.size()), SQLITE_TRANSIENT), "bind key");
|
|
|
|
std::string s = value.to_db_text();
|
|
check_sqlite(sqlite3_bind_text(stmt, 3, s.c_str(), static_cast<int>(s.size()), SQLITE_TRANSIENT), "bind value");
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
check_sqlite(rc == SQLITE_DONE ? SQLITE_OK : rc, "step INSERT/UPDATE");
|
|
}
|
|
|
|
awaitable_expected< void > SettingsStore::async_save(std::string_view component, std::string_view key, const Value & value) noexcept {
|
|
BOOST_ASSERT(component.size() < 256);
|
|
BOOST_ASSERT(key.size() < 256);
|
|
BOOST_ASSERT(is_null_terminated(component));
|
|
BOOST_ASSERT(is_null_terminated(key));
|
|
|
|
auto caller_exec = co_await boost::asio::this_coro::executor;
|
|
|
|
// do puli DB
|
|
co_await boost::asio::post(db_pool_, boost::asio::use_awaitable);
|
|
|
|
try {
|
|
save(component, key, value);
|
|
} catch(const std::exception & e) {
|
|
spdlog::error("async_save exception: {}", e.what());
|
|
}
|
|
|
|
// z powrotem
|
|
co_await boost::asio::post(caller_exec, boost::asio::use_awaitable);
|
|
co_return _void{};
|
|
}
|
|
|
|
void SettingsStore::open_or_create() {
|
|
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX; // bezpieczne między wątkami
|
|
|
|
int rc = sqlite3_open_v2(db_path_.c_str(), &db_, flags, nullptr);
|
|
if(rc != SQLITE_OK) {
|
|
std::string msg = sqlite3_errmsg(db_);
|
|
sqlite3_close(db_);
|
|
db_ = nullptr;
|
|
throw std::runtime_error("Failed to open DB: " + msg);
|
|
}
|
|
|
|
spdlog::info("Opened SQLite DB: {}", db_path_);
|
|
}
|
|
|
|
void SettingsStore::prepare_schema() {
|
|
// value jako TEXT (zapisujemy tam JSON)
|
|
const char * sql =
|
|
"CREATE TABLE IF NOT EXISTS settings ("
|
|
" component TEXT NOT NULL,"
|
|
" key TEXT NOT NULL,"
|
|
" value TEXT,"
|
|
" PRIMARY KEY(component, key)"
|
|
");";
|
|
|
|
char * errmsg = nullptr;
|
|
int rc = sqlite3_exec(db_, sql, nullptr, nullptr, &errmsg);
|
|
if(rc != SQLITE_OK) {
|
|
std::string msg = errmsg ? errmsg : "unknown error";
|
|
sqlite3_free(errmsg);
|
|
throw std::runtime_error("Failed to create schema: " + msg);
|
|
}
|
|
}
|
|
|
|
void SettingsStore::check_sqlite(int rc, const char * what) {
|
|
if(rc != SQLITE_OK && rc != SQLITE_DONE && rc != SQLITE_ROW) {
|
|
throw std::runtime_error(std::string("SQLite error in ") + what + ": " + sqlite3_errstr(rc));
|
|
}
|
|
}
|
|
|
|
awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async_get(std::string_view component,
|
|
std::string_view key) noexcept {
|
|
BOOST_ASSERT(component.size() < 256);
|
|
BOOST_ASSERT(key.size() < 256);
|
|
BOOST_ASSERT(is_null_terminated(component));
|
|
BOOST_ASSERT(is_null_terminated(key));
|
|
|
|
auto caller_exec = co_await boost::asio::this_coro::executor;
|
|
|
|
std::optional< Value > result;
|
|
|
|
// przeskok do puli DB
|
|
co_await boost::asio::post(db_pool_, boost::asio::use_awaitable);
|
|
|
|
try {
|
|
result = get(component, key);
|
|
|
|
} catch(const std::exception & e) {
|
|
spdlog::error("async_get exception: {}", e.what());
|
|
result = std::nullopt;
|
|
}
|
|
|
|
_log.debug("get({}, {}) done, make context switch", component, key);
|
|
// z powrotem na executor wywołujący
|
|
co_await boost::asio::post(caller_exec, boost::asio::use_awaitable);
|
|
|
|
_log.debug("get({}, {}) returning valu", component, key);
|
|
co_return result;
|
|
}
|
|
|
|
std::optional< SettingsStore::Value > SettingsStore::get(std::string_view component, std::string_view key) {
|
|
BOOST_ASSERT(component.size() < 256);
|
|
BOOST_ASSERT(key.size() < 256);
|
|
BOOST_ASSERT(is_null_terminated(component));
|
|
BOOST_ASSERT(is_null_terminated(key));
|
|
|
|
_log.debug("get({}, {})", component, key);
|
|
|
|
const char * sql =
|
|
"SELECT value FROM settings "
|
|
"WHERE component = ?1 AND key = ?2";
|
|
|
|
sqlite3_stmt * stmt = nullptr;
|
|
check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare SELECT");
|
|
|
|
auto stmt_guard = make_stmt_guard(stmt);
|
|
|
|
check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), static_cast<int>(component.size()), SQLITE_TRANSIENT), "bind component");
|
|
check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), static_cast<int>(key.size()), SQLITE_TRANSIENT), "bind key");
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
if(rc == SQLITE_ROW) {
|
|
if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
|
|
_log.debug("get({}, {}) return nullopt", component, key);
|
|
return std::nullopt;
|
|
}
|
|
const unsigned char * text = sqlite3_column_text(stmt, 0);
|
|
int len = sqlite3_column_bytes(stmt, 0);
|
|
if(!text || len <= 0) {
|
|
_log.debug("get({}, {}) return nullopt", component, key);
|
|
return std::nullopt;
|
|
}
|
|
_log.debug("get({}, {}) return value", component, key);
|
|
return Value::from_db_text({reinterpret_cast< const char * >(text), static_cast<size_t>(len)});
|
|
}
|
|
if(rc == SQLITE_DONE) {
|
|
_log.debug("get({}, {}) not found", component, key);
|
|
return std::nullopt;
|
|
}
|
|
|
|
check_sqlite(rc, "step SELECT");
|
|
return std::nullopt; // tu nie dojdziemy
|
|
}
|
|
|
|
SettingsStore::StmtGuard::~StmtGuard() {
|
|
if(stmt)
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
} // namespace ranczo
|