Files
App/src/storage/database.cpp
HiepLM 098e1b2b69
Some checks failed
Test / test (push) Has been cancelled
Chuyển lưu trữ dữ liệu sang data base
2026-06-17 11:16:30 +07:00

413 lines
11 KiB
C++

#include "storage/database.hpp"
#include "util/file_util.hpp"
#include "util/id_util.hpp"
#include <sqlite3.h>
#include <cstdio>
namespace lm {
namespace {
bool execSql(sqlite3* db, const char* sql, std::string& err)
{
char* msg = nullptr;
const int rc = sqlite3_exec(db, sql, nullptr, nullptr, &msg);
if (rc != SQLITE_OK)
{
err = msg ? msg : sqlite3_errstr(rc);
sqlite3_free(msg);
return false;
}
return true;
}
const char* kSchemaSql = R"SQL(
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS documents (
name TEXT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS layout_profiles (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS maps (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
width REAL,
height REAL,
resolution REAL,
origin_x REAL DEFAULT 0,
origin_y REAL DEFAULT 0,
origin_yaw REAL DEFAULT 0,
image_file TEXT,
yaml_file TEXT,
zones_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sounds (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
file_name TEXT,
duration_ms INTEGER,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS recordings (
id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
map_id TEXT,
file_path TEXT,
started_at TEXT,
ended_at TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS dashboards (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_by TEXT NOT NULL DEFAULT '',
created_by_user TEXT,
is_default INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS dashboard_edit_groups (
dashboard_id TEXT NOT NULL,
group_id TEXT NOT NULL,
PRIMARY KEY (dashboard_id, group_id),
FOREIGN KEY (dashboard_id) REFERENCES dashboards(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS dashboard_widgets (
id TEXT PRIMARY KEY,
dashboard_id TEXT NOT NULL,
type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
mission_id TEXT,
mission_group TEXT,
config_json TEXT NOT NULL DEFAULT '{}',
sort_order INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (dashboard_id) REFERENCES dashboards(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS dashboard_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
active_dashboard_id TEXT
);
)SQL";
} // namespace
Database::Database(std::filesystem::path data_dir)
: data_dir_(std::move(data_dir)), db_path_(data_dir_ / "RBS.db")
{
}
void Database::close()
{
if (db_)
{
sqlite3_close(db_);
db_ = nullptr;
}
}
bool Database::openDb(std::string& err)
{
std::error_code ec;
std::filesystem::create_directories(data_dir_, ec);
const auto legacy_path = data_dir_ / "test3.db";
if (!std::filesystem::exists(db_path_) && std::filesystem::exists(legacy_path))
{
std::filesystem::rename(legacy_path, db_path_, ec);
for (const char* suffix : {"-wal", "-shm"})
{
const auto from = legacy_path.string() + suffix;
const auto to = db_path_.string() + suffix;
if (std::filesystem::exists(from))
std::filesystem::rename(from, to, ec);
}
}
const int rc = sqlite3_open(db_path_.string().c_str(), &db_);
if (rc != SQLITE_OK)
{
err = sqlite3_errmsg(db_);
db_ = nullptr;
return false;
}
sqlite3_busy_timeout(db_, 5000);
if (!execSql(db_, "PRAGMA journal_mode=WAL;", err))
return false;
if (!execSql(db_, "PRAGMA synchronous=NORMAL;", err))
return false;
if (!execSql(db_, "PRAGMA foreign_keys=ON;", err))
return false;
return true;
}
bool Database::ensureDataDirs(std::string& err)
{
std::error_code ec;
for (const auto& dir : {mapsDir(), soundsDir(), recordingsDir()})
{
if (!std::filesystem::create_directories(dir, ec) && ec)
{
err = "failed to create directory: " + dir.string();
return false;
}
}
return true;
}
bool Database::applySchema(std::string& err)
{
return execSql(db_, kSchemaSql, err);
}
std::optional<std::string> Database::getMeta(const std::string& key) const
{
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, "SELECT value FROM meta WHERE key = ?1", -1, &stmt, nullptr) != SQLITE_OK)
return std::nullopt;
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
std::optional<std::string> out;
if (sqlite3_step(stmt) == SQLITE_ROW)
{
const char* val = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (val)
out = val;
}
sqlite3_finalize(stmt);
return out;
}
bool Database::setMeta(const std::string& key, const std::string& value)
{
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_,
"INSERT INTO meta(key, value) VALUES(?1, ?2) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
-1,
&stmt,
nullptr) != SQLITE_OK)
return false;
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, value.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
bool Database::getDocument(const std::string& name, nlohmann::json& out) const
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, "SELECT content FROM documents WHERE name = ?1", -1, &stmt, nullptr) != SQLITE_OK)
return false;
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
bool found = false;
if (sqlite3_step(stmt) == SQLITE_ROW)
{
const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (text)
{
try
{
out = nlohmann::json::parse(text);
found = true;
}
catch (...)
{
found = false;
}
}
}
sqlite3_finalize(stmt);
return found;
}
bool Database::setDocument(const std::string& name, const nlohmann::json& doc)
{
std::lock_guard<std::mutex> lock(mu_);
const std::string now = IdUtil::nowIso8601();
const std::string body = doc.dump();
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_,
"INSERT INTO documents(name, content, updated_at) VALUES(?1, ?2, ?3) "
"ON CONFLICT(name) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at",
-1,
&stmt,
nullptr) != SQLITE_OK)
return false;
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, body.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
std::optional<nlohmann::json> Database::getLayoutProfile(const std::string& id) const
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, "SELECT content FROM layout_profiles WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
return std::nullopt;
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
std::optional<nlohmann::json> out;
if (sqlite3_step(stmt) == SQLITE_ROW)
{
const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (text)
{
try
{
out = nlohmann::json::parse(text);
}
catch (...)
{
out = std::nullopt;
}
}
}
sqlite3_finalize(stmt);
return out;
}
bool Database::setLayoutProfile(const nlohmann::json& profile)
{
if (!profile.is_object() || !profile.contains("id") || !profile["id"].is_string())
return false;
const std::string id = profile["id"].get<std::string>();
const std::string now = IdUtil::nowIso8601();
const std::string body = profile.dump();
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_,
"INSERT INTO layout_profiles(id, content, updated_at) VALUES(?1, ?2, ?3) "
"ON CONFLICT(id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at",
-1,
&stmt,
nullptr) != SQLITE_OK)
return false;
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, body.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
bool Database::deleteLayoutProfile(const std::string& id)
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, "DELETE FROM layout_profiles WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
return false;
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
bool Database::migrateFromJsonIfNeeded(std::string& err)
{
if (getMeta("json_imported"))
return true;
const auto importDoc = [&](const std::string& name, const std::filesystem::path& path) {
if (!std::filesystem::exists(path))
return;
try
{
const auto doc = nlohmann::json::parse(FileUtil::readBinary(path));
setDocument(name, doc);
}
catch (...)
{
}
};
importDoc("auth", data_dir_ / "auth.json");
importDoc("missions", data_dir_ / "missions.json");
importDoc("mission_queue", data_dir_ / "mission_queue.json");
importDoc("robot_runtime", data_dir_ / "robot_runtime.json");
const auto state_path = data_dir_ / "state.json";
if (std::filesystem::exists(state_path))
{
try
{
setDocument("state", nlohmann::json::parse(FileUtil::readBinary(state_path)));
}
catch (...)
{
}
}
const auto models_dir = data_dir_ / "models";
if (std::filesystem::is_directory(models_dir))
{
for (const auto& entry : std::filesystem::directory_iterator(models_dir))
{
if (!entry.is_regular_file() || entry.path().extension() != ".json")
continue;
try
{
const auto profile = nlohmann::json::parse(FileUtil::readBinary(entry.path()));
setLayoutProfile(profile);
}
catch (...)
{
}
}
}
setMeta("schema_version", "1");
setMeta("json_imported", IdUtil::nowIso8601());
std::fprintf(stderr, "SQLite: imported JSON data into %s\n", db_path_.string().c_str());
return true;
}
bool Database::init(std::string& err)
{
if (!openDb(err))
return false;
if (!applySchema(err))
return false;
if (!ensureDataDirs(err))
return false;
if (!migrateFromJsonIfNeeded(err))
return false;
if (!getMeta("schema_version"))
setMeta("schema_version", "1");
return true;
}
} // namespace lm