#include "migrations.hpp"
#include <cstdint>
#include <format>
#include <string>
#include <vector>
#include <mw/database.hpp>
#include <mw/error.hpp>
#include <mw/utils.hpp>
#include <spdlog/spdlog.h>
#include "backup.hpp"
namespace overseer::db
{
namespace
{
struct MigrationStep
{
int64_t from;
int64_t to;
mw::E<void> (*fn)(mw::SQLite&);
};
const std::vector<MigrationStep>& registry()
{
static const std::vector<MigrationStep> r = {
{0, 1, &migrateDB0To1},
};
return r;
}
mw::E<int64_t> getUserVersion(mw::SQLite& db)
{
auto rt = db.evalToValue<int64_t>("PRAGMA user_version;");
if(!rt.has_value())
{
return std::unexpected(rt.error());
}
return *rt;
}
mw::E<void> setUserVersion(mw::SQLite& db, int64_t v)
{
// PRAGMA does not accept bound parameters, so format directly.
// `v` is an internal integer; safe to format.
return db.execute(std::format("PRAGMA user_version = {};", v));
}
} // namespace
mw::E<void> migrateIfNeeded(mw::SQLite& db, const std::string& db_path)
{
auto current_r = getUserVersion(db);
if(!current_r.has_value())
{
return std::unexpected(current_r.error());
}
const int64_t current = *current_r;
const int64_t target = HIGHEST_KNOWN_VERSION;
if(current == target)
{
spdlog::info("Database schema is at version {} (current).", current);
return {};
}
if(current > target)
{
return std::unexpected(mw::runtimeError(std::format(
"Database schema version {} is newer than this binary "
"knows about (target {}). Refusing to start.",
current, target)));
}
spdlog::info("Migrating database schema from version {} to {}.",
current, target);
// Take ONE backup before any migration runs.
if(!db_path.empty())
{
if(auto bk = backupDatabaseFile(db, db_path); !bk.has_value())
{
return std::unexpected(bk.error());
}
}
for(const auto& step : registry())
{
if(step.from < current)
{
continue;
}
if(step.from >= target)
{
break;
}
spdlog::info("Applying migration {} -> {}.", step.from, step.to);
if(auto rt = db.execute("BEGIN IMMEDIATE;"); !rt.has_value())
{
return std::unexpected(rt.error());
}
if(auto rt = step.fn(db); !rt.has_value())
{
// best-effort rollback; ignore its error
(void)db.execute("ROLLBACK;");
return std::unexpected(rt.error());
}
if(auto rt = setUserVersion(db, step.to); !rt.has_value())
{
(void)db.execute("ROLLBACK;");
return std::unexpected(rt.error());
}
if(auto rt = db.execute("COMMIT;"); !rt.has_value())
{
(void)db.execute("ROLLBACK;");
return std::unexpected(rt.error());
}
}
spdlog::info("Database migration complete.");
return {};
}
} // namespace overseer::db