#include "storage/database.hpp" #include "util/file_util.hpp" #include "util/id_util.hpp" #include #include 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 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 out; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* val = reinterpret_cast(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 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(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 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 Database::getLayoutProfile(const std::string& id) const { std::lock_guard 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 out; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* text = reinterpret_cast(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(); const std::string now = IdUtil::nowIso8601(); const std::string body = profile.dump(); std::lock_guard 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 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