This commit is contained in:
@@ -122,6 +122,21 @@ CREATE TABLE IF NOT EXISTS io_modules (
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paths (
|
||||
id TEXT PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
map_id TEXT NOT NULL,
|
||||
from_position_id TEXT NOT NULL,
|
||||
to_position_id TEXT NOT NULL,
|
||||
auto_created INTEGER NOT NULL DEFAULT 1,
|
||||
points_json TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(map_id, from_position_id, to_position_id),
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboards (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -431,6 +446,29 @@ bool Database::applySchemaMigrations(std::string& err)
|
||||
setMeta("schema_version", "7");
|
||||
}
|
||||
|
||||
ver = getMeta("schema_version").value_or("1");
|
||||
if (ver == "7")
|
||||
{
|
||||
if (!execSql(db_,
|
||||
"CREATE TABLE IF NOT EXISTS paths ("
|
||||
"id TEXT PRIMARY KEY, "
|
||||
"site_id TEXT NOT NULL, "
|
||||
"map_id TEXT NOT NULL, "
|
||||
"from_position_id TEXT NOT NULL, "
|
||||
"to_position_id TEXT NOT NULL, "
|
||||
"auto_created INTEGER NOT NULL DEFAULT 1, "
|
||||
"points_json TEXT NOT NULL DEFAULT '[]', "
|
||||
"created_at TEXT NOT NULL, "
|
||||
"updated_at TEXT NOT NULL, "
|
||||
"UNIQUE(map_id, from_position_id, to_position_id), "
|
||||
"FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE, "
|
||||
"FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE"
|
||||
")",
|
||||
err))
|
||||
return false;
|
||||
setMeta("schema_version", "8");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
256
src/storage/path_store.cpp
Normal file
256
src/storage/path_store.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
#include "storage/path_store.hpp"
|
||||
|
||||
#include "storage/database.hpp"
|
||||
#include "util/id_util.hpp"
|
||||
#include "util/string_util.hpp"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
{
|
||||
auto text = [&](int col) -> std::string {
|
||||
if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
|
||||
return "";
|
||||
const char* v = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
|
||||
return v ? std::string(v) : "";
|
||||
};
|
||||
nlohmann::json points = nlohmann::json::array();
|
||||
try
|
||||
{
|
||||
const std::string raw = text(6);
|
||||
if (!raw.empty())
|
||||
points = nlohmann::json::parse(raw);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
points = nlohmann::json::array();
|
||||
}
|
||||
return {{"id", text(0)},
|
||||
{"site_id", text(1)},
|
||||
{"map_id", text(2)},
|
||||
{"from_position_id", text(3)},
|
||||
{"to_position_id", text(4)},
|
||||
{"auto_created", sqlite3_column_int(stmt, 5) != 0},
|
||||
{"points", points.is_array() ? points : nlohmann::json::array()},
|
||||
{"created_at", text(7)},
|
||||
{"updated_at", text(8)}};
|
||||
}
|
||||
|
||||
constexpr const char* kSelect =
|
||||
"SELECT id, site_id, map_id, from_position_id, to_position_id, auto_created, points_json, created_at, updated_at "
|
||||
"FROM paths";
|
||||
|
||||
} // namespace
|
||||
|
||||
PathStore::PathStore(Database& db) : db_(db) {}
|
||||
|
||||
nlohmann::json PathStore::list(const std::string& site_id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
std::string sql = kSelect;
|
||||
if (!site_id.empty())
|
||||
sql += " WHERE site_id = ?1";
|
||||
sql += " ORDER BY map_id, from_position_id, to_position_id";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return out;
|
||||
if (!site_id.empty())
|
||||
sqlite3_bind_text(stmt, 1, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
out.push_back(rowToJson(stmt));
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> PathStore::find(const std::string& id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
const std::string sql = std::string(kSelect) + " WHERE id = ?1";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -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)
|
||||
out = rowToJson(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> PathStore::findBetween(const std::string& map_id,
|
||||
const std::string& from_position_id,
|
||||
const std::string& to_position_id) const
|
||||
{
|
||||
if (map_id.empty() || from_position_id.empty() || to_position_id.empty())
|
||||
return std::nullopt;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
const std::string sql = std::string(kSelect) +
|
||||
" WHERE map_id = ?1 AND from_position_id = ?2 AND to_position_id = ?3 LIMIT 1";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return std::nullopt;
|
||||
sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, from_position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, to_position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
std::optional<nlohmann::json> out;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
out = rowToJson(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> PathStore::upsert(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
if (!payload.is_object())
|
||||
{
|
||||
err = "payload must be an object";
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string site_id = StringUtil::trimCopy(payload.value("site_id", ""));
|
||||
const std::string map_id = StringUtil::trimCopy(payload.value("map_id", ""));
|
||||
const std::string from_id = StringUtil::trimCopy(payload.value("from_position_id", ""));
|
||||
const std::string to_id = StringUtil::trimCopy(payload.value("to_position_id", ""));
|
||||
if (site_id.empty() || map_id.empty() || from_id.empty() || to_id.empty())
|
||||
{
|
||||
err = "missing required fields";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (from_id == to_id)
|
||||
{
|
||||
err = "from and to positions must differ";
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& points = payload.contains("points") && payload["points"].is_array() ? payload["points"]
|
||||
: nlohmann::json::array();
|
||||
const std::string points_json = points.dump();
|
||||
const bool auto_created = payload.value("auto_created", true);
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
|
||||
std::string id = payload.value("id", IdUtil::newId());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* lookup = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id FROM paths WHERE map_id = ?1 AND from_position_id = ?2 AND to_position_id = ?3",
|
||||
-1,
|
||||
&lookup,
|
||||
nullptr) == SQLITE_OK)
|
||||
{
|
||||
sqlite3_bind_text(lookup, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(lookup, 2, from_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(lookup, 3, to_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(lookup) == SQLITE_ROW)
|
||||
{
|
||||
const char* existing = reinterpret_cast<const char*>(sqlite3_column_text(lookup, 0));
|
||||
if (existing)
|
||||
id = existing;
|
||||
}
|
||||
sqlite3_finalize(lookup);
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"INSERT INTO paths(id, site_id, map_id, from_position_id, to_position_id, auto_created, "
|
||||
"points_json, created_at, updated_at) "
|
||||
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9) "
|
||||
"ON CONFLICT(map_id, from_position_id, to_position_id) DO UPDATE SET "
|
||||
"points_json=excluded.points_json, auto_created=excluded.auto_created, updated_at=excluded.updated_at",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return std::nullopt;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, from_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, to_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 6, auto_created ? 1 : 0);
|
||||
sqlite3_bind_text(stmt, 7, points_json.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 8, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 9, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
||||
sqlite3_finalize(stmt);
|
||||
if (!ok)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
sqlite3_stmt* get_stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), (std::string(kSelect) + " WHERE id = ?1").c_str(), -1, &get_stmt, nullptr) !=
|
||||
SQLITE_OK)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return std::nullopt;
|
||||
}
|
||||
sqlite3_bind_text(get_stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
std::optional<nlohmann::json> out;
|
||||
if (sqlite3_step(get_stmt) == SQLITE_ROW)
|
||||
out = rowToJson(get_stmt);
|
||||
sqlite3_finalize(get_stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool PathStore::remove(const std::string& id, std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM paths WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
const bool ok = sqlite3_changes(db_.handle()) > 0;
|
||||
sqlite3_finalize(stmt);
|
||||
if (!ok)
|
||||
err = "path not found";
|
||||
return ok;
|
||||
}
|
||||
|
||||
int PathStore::removeForMap(const std::string& map_id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM paths WHERE map_id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return 0;
|
||||
sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
const int n = sqlite3_changes(db_.handle());
|
||||
sqlite3_finalize(stmt);
|
||||
return n;
|
||||
}
|
||||
|
||||
int PathStore::removeForPosition(const std::string& map_id, const std::string& position_id)
|
||||
{
|
||||
if (map_id.empty() || position_id.empty())
|
||||
return 0;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"DELETE FROM paths WHERE map_id = ?1 AND (from_position_id = ?2 OR to_position_id = ?2)",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
return 0;
|
||||
sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
const int n = sqlite3_changes(db_.handle());
|
||||
sqlite3_finalize(stmt);
|
||||
return n;
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
33
src/storage/path_store.hpp
Normal file
33
src/storage/path_store.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class Database;
|
||||
|
||||
class PathStore
|
||||
{
|
||||
public:
|
||||
explicit PathStore(Database& db);
|
||||
|
||||
nlohmann::json list(const std::string& site_id = "") const;
|
||||
std::optional<nlohmann::json> find(const std::string& id) const;
|
||||
std::optional<nlohmann::json> findBetween(const std::string& map_id,
|
||||
const std::string& from_position_id,
|
||||
const std::string& to_position_id) const;
|
||||
std::optional<nlohmann::json> upsert(const nlohmann::json& payload, std::string& err);
|
||||
bool remove(const std::string& id, std::string& err);
|
||||
int removeForMap(const std::string& map_id);
|
||||
int removeForPosition(const std::string& map_id, const std::string& position_id);
|
||||
|
||||
private:
|
||||
Database& db_;
|
||||
mutable std::mutex mu_;
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
Reference in New Issue
Block a user