BareGit
#include "data_source_sqlite.h"

#include <utility>

DataSourceSqlite::DataSourceSqlite(std::unique_ptr<mw::SQLite> database)
    : database(std::move(database))
{}

DataSourceSqlite::~DataSourceSqlite() = default;

mw::E<std::unique_ptr<DataSourceSqlite>> DataSourceSqlite::open(
    const std::string& path)
{
    if(path.empty() || path.find('\0') != std::string::npos)
    {
        return std::unexpected(mw::runtimeError("Invalid database path"));
    }
    ASSIGN_OR_RETURN(auto database, mw::SQLite::connectFile(path));
    DO_OR_RETURN(database->execute(R"(
        CREATE TABLE IF NOT EXISTS status_history (
            id INTEGER PRIMARY KEY,
            service_id TEXT NOT NULL CHECK(length(service_id) > 0),
            timestamp INTEGER NOT NULL,
            duration_microsecond INTEGER NOT NULL
                CHECK(duration_microsecond >= 0),
            status INTEGER NOT NULL CHECK(status BETWEEN 0 AND 2)
        )
    )"));
    DO_OR_RETURN(database->execute(R"(
        CREATE INDEX IF NOT EXISTS status_history_service_time
        ON status_history(service_id, timestamp, id)
    )"));
    return std::unique_ptr<DataSourceSqlite>(
        new DataSourceSqlite(std::move(database)));
}

mw::E<void> DataSourceSqlite::save(const StatusRecord& record)
{
    const int status = static_cast<int>(record.status);
    if(record.service_id.empty() ||
       record.service_id.find('\0') != std::string::npos ||
       record.duration_microsecond < 0 || status < 0 || status > 2)
    {
        return std::unexpected(mw::runtimeError("Invalid status record"));
    }
    ASSIGN_OR_RETURN(auto statement, database->statementFromStr(R"(
        INSERT INTO status_history
            (service_id, timestamp, duration_microsecond, status)
        VALUES (?, ?, ?, ?)
    )"));
    DO_OR_RETURN(statement.bind(record.service_id, record.timestamp,
                                record.duration_microsecond, status));
    return database->execute(std::move(statement));
}

mw::E<std::optional<StatusRecord>> DataSourceSqlite::latest(
    const std::string& service_id)
{
    ASSIGN_OR_RETURN(auto statement, database->statementFromStr(R"(
        SELECT timestamp, duration_microsecond, status FROM status_history
        WHERE service_id = ? ORDER BY timestamp DESC, id DESC LIMIT 1
    )"));
    DO_OR_RETURN(statement.bind(service_id));
    ASSIGN_OR_RETURN(auto rows,
        (database->eval<std::int64_t, std::int64_t, int>(
            std::move(statement))));
    if(rows.empty())
    {
        return std::optional<StatusRecord>{};
    }
    const auto& [timestamp, duration, status] = rows.front();
    return StatusRecord{service_id, timestamp, duration,
                        static_cast<ProbeStatus>(status)};
}

mw::E<std::vector<StatusRecord>> DataSourceSqlite::history(
    const std::string& service_id, std::int64_t start, std::int64_t end)
{
    if(start > end)
    {
        return std::unexpected(mw::runtimeError("Reversed history bounds"));
    }
    ASSIGN_OR_RETURN(auto statement, database->statementFromStr(R"(
        SELECT timestamp, duration_microsecond, status FROM status_history
        WHERE service_id = ? AND timestamp >= ? AND timestamp < ?
        ORDER BY timestamp, id
    )"));
    DO_OR_RETURN(statement.bind(service_id, start, end));
    ASSIGN_OR_RETURN(auto rows,
        (database->eval<std::int64_t, std::int64_t, int>(
            std::move(statement))));
    std::vector<StatusRecord> records;
    records.reserve(rows.size());
    for(const auto& [timestamp, duration, status] : rows)
    {
        records.push_back({service_id, timestamp, duration,
                           static_cast<ProbeStatus>(status)});
    }
    return records;
}