ranczo-io/libs/config.cpp
2025-11-27 09:42:53 +01:00

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