chatgpt wrote the database layer for me
This commit is contained in:
188
src/types.cpp
188
src/types.cpp
@@ -1 +1,189 @@
|
|||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
|
#include "clickhouse/base/uuid.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
using namespace clickhouse;
|
||||||
|
|
||||||
|
namespace database_utils {
|
||||||
|
|
||||||
|
// ---------------- UUID helpers ----------------
|
||||||
|
clickhouse::UUID parse_uuid(const std::string& str) {
|
||||||
|
if (str.size() != 36) throw std::runtime_error("Invalid UUID string: " + str);
|
||||||
|
|
||||||
|
std::string hexstr;
|
||||||
|
for (char c : str) if (c != '-') hexstr += c;
|
||||||
|
|
||||||
|
if (hexstr.size() != 32) throw std::runtime_error("Invalid UUID format: " + str);
|
||||||
|
|
||||||
|
uint64_t high = 0, low = 0;
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::hex << hexstr.substr(0, 16);
|
||||||
|
ss >> high;
|
||||||
|
ss.clear(); ss.str("");
|
||||||
|
ss << std::hex << hexstr.substr(16, 16);
|
||||||
|
ss >> low;
|
||||||
|
|
||||||
|
return clickhouse::UUID{high, low};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string uuid_to_string(const clickhouse::UUID& u) {
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::hex << std::setw(16) << std::setfill('0') << u.first
|
||||||
|
<< std::setw(16) << std::setfill('0') << u.second;
|
||||||
|
std::string s = ss.str();
|
||||||
|
// Insert dashes for standard UUID format
|
||||||
|
return s.substr(0,8) + "-" + s.substr(8,4) + "-" + s.substr(12,4) + "-" + s.substr(16,4) + "-" + s.substr(20,12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Users ----------------
|
||||||
|
std::vector<UserRecord> get_all_users(const CHClient& client) {
|
||||||
|
std::vector<UserRecord> out;
|
||||||
|
client->Select(
|
||||||
|
"SELECT user_id, username, password FROM users",
|
||||||
|
[&](const Block& b){
|
||||||
|
for (size_t i = 0; i < b.GetRowCount(); ++i) {
|
||||||
|
UserRecord u;
|
||||||
|
u.user_id = uuid_to_string(b[0]->As<ColumnUUID>()->At(i));
|
||||||
|
u.login.username = b[1]->As<ColumnString>()->At(i);
|
||||||
|
u.login.password = b[2]->As<ColumnString>()->At(i);
|
||||||
|
out.push_back(std::move(u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool register_user(const CHClient& client, const std::string& username, const std::string& password) {
|
||||||
|
Block b;
|
||||||
|
auto u = std::make_shared<ColumnString>();
|
||||||
|
auto p = std::make_shared<ColumnString>();
|
||||||
|
u->Append(username);
|
||||||
|
p->Append(password);
|
||||||
|
b.AppendColumn("username", u);
|
||||||
|
b.AppendColumn("password", p);
|
||||||
|
|
||||||
|
client->Insert("users", b);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool authenticate_user(const CHClient& client, const std::string& username, const std::string& password) {
|
||||||
|
bool ok = false;
|
||||||
|
client->Select(
|
||||||
|
"SELECT count() FROM users WHERE username = '" + username + "' AND password = '" + password + "'",
|
||||||
|
[&](const Block& b){ ok = b[0]->As<ColumnUInt64>()->At(0) > 0; }
|
||||||
|
);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Load latest snapshot ----------------
|
||||||
|
std::optional<GradeSnapshot> load_latest_grades(const CHClient& client, const std::string& user_id) {
|
||||||
|
GradeSnapshot snap;
|
||||||
|
bool found = false;
|
||||||
|
client->Select(
|
||||||
|
"SELECT response_id, success, total_classes FROM grade_responses "
|
||||||
|
"WHERE user_id = '" + user_id + "' ORDER BY fetched_at DESC LIMIT 1",
|
||||||
|
[&](const Block& b){
|
||||||
|
if (b.GetRowCount() == 0) return;
|
||||||
|
found = true;
|
||||||
|
snap.response_id = uuid_to_string(b[0]->As<ColumnUUID>()->At(0));
|
||||||
|
snap.response.success = b[1]->As<ColumnUInt8>()->At(0);
|
||||||
|
snap.response.totalClasses = b[2]->As<ColumnInt32>()->At(0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!found) return std::nullopt;
|
||||||
|
return snap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Diff ----------------
|
||||||
|
std::vector<AssignmentDiff> diff_grade_responses(
|
||||||
|
const api_utils::GradesResponse& old_resp,
|
||||||
|
const api_utils::GradesResponse& new_resp
|
||||||
|
) {
|
||||||
|
std::unordered_map<std::string, api_utils::AssignmentGrade> old_map;
|
||||||
|
for (const auto& c : old_resp.grades)
|
||||||
|
for (const auto& g : c.grades)
|
||||||
|
old_map[c.className + "::" + g.name] = g;
|
||||||
|
|
||||||
|
std::vector<AssignmentDiff> diffs;
|
||||||
|
for (const auto& c : new_resp.grades) {
|
||||||
|
for (const auto& g : c.grades) {
|
||||||
|
std::string key = c.className + "::" + g.name;
|
||||||
|
auto it = old_map.find(key);
|
||||||
|
if (it == old_map.end()) {
|
||||||
|
diffs.push_back({"", "", std::nullopt, g});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (it->second.score != g.score || it->second.attempts != g.attempts) {
|
||||||
|
diffs.push_back({"", "", it->second, g});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Conditional insert ----------------
|
||||||
|
bool conditionally_insert_grades(
|
||||||
|
const CHClient& client,
|
||||||
|
const std::string& user_id,
|
||||||
|
const api_utils::GradesResponse& new_resp
|
||||||
|
) {
|
||||||
|
auto old = load_latest_grades(client, user_id);
|
||||||
|
if (!old) return true;
|
||||||
|
return !diff_grade_responses(old->response, new_resp).empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void insert_grade_updates(
|
||||||
|
std::shared_ptr<clickhouse::Client> client,
|
||||||
|
const std::string& user_id_str,
|
||||||
|
const std::string& old_response_id_str,
|
||||||
|
const std::string& new_response_id_str,
|
||||||
|
const std::vector<AssignmentDiff>& diffs
|
||||||
|
) {
|
||||||
|
if (diffs.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Query query(
|
||||||
|
"INSERT INTO grade_updates "
|
||||||
|
"(user_id, old_response_id, new_response_id, class_grade_id, assignment_id, assignment_name, "
|
||||||
|
"old_score, new_score, old_attempts, new_attempts) "
|
||||||
|
"VALUES "
|
||||||
|
"({user_id: String}, {old_response_id: String}, {new_response_id: String}, "
|
||||||
|
"{class_grade_id: String}, {assignment_id: String}, {assignment_name: String}, "
|
||||||
|
"{old_score: Nullable(String)}, {new_score: String}, "
|
||||||
|
"{old_attempts: Nullable(String)}, {new_attempts: String})"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::string user_uuid_str = uuid_to_string(parse_uuid(user_id_str));
|
||||||
|
std::string old_response_uuid_str = uuid_to_string(parse_uuid(old_response_id_str));
|
||||||
|
std::string new_response_uuid_str = uuid_to_string(parse_uuid(new_response_id_str));
|
||||||
|
|
||||||
|
for (const auto& d : diffs) {
|
||||||
|
query.SetParam("user_id", user_uuid_str);
|
||||||
|
query.SetParam("old_response_id", old_response_uuid_str);
|
||||||
|
query.SetParam("new_response_id", new_response_uuid_str);
|
||||||
|
|
||||||
|
query.SetParam("class_grade_id", uuid_to_string(parse_uuid(d.class_grade_id)));
|
||||||
|
query.SetParam("assignment_id", uuid_to_string(parse_uuid(d.assignment_id)));
|
||||||
|
query.SetParam("assignment_name", d.new_grade.name);
|
||||||
|
|
||||||
|
if (d.old_grade) {
|
||||||
|
query.SetParam("old_score", std::to_string(d.old_grade->score));
|
||||||
|
query.SetParam("old_attempts", d.old_grade->attempts);
|
||||||
|
} else {
|
||||||
|
query.SetParam("old_score", QueryParamValue());
|
||||||
|
query.SetParam("old_attempts", QueryParamValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
query.SetParam("new_score", std::to_string(d.new_grade.score));
|
||||||
|
query.SetParam("new_attempts", d.new_grade.attempts);
|
||||||
|
|
||||||
|
client->Execute(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace database_utils
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,97 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <clickhouse/client.h>
|
||||||
|
#include "skwyward-api-utils.hpp"
|
||||||
|
|
||||||
|
namespace database_utils {
|
||||||
|
|
||||||
|
// ---------- DB Handle ----------
|
||||||
|
|
||||||
|
using CHClient = std::shared_ptr<clickhouse::Client>;
|
||||||
|
|
||||||
|
// ---------- User ----------
|
||||||
|
|
||||||
|
struct UserRecord {
|
||||||
|
std::string user_id; // UUID
|
||||||
|
api_utils::Login login;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- Snapshot ----------
|
||||||
|
|
||||||
|
struct GradeSnapshot {
|
||||||
|
std::string response_id;
|
||||||
|
api_utils::GradesResponse response;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- Assignment Diff ----------
|
||||||
|
|
||||||
|
struct AssignmentDiff {
|
||||||
|
std::string class_grade_id;
|
||||||
|
std::string assignment_id;
|
||||||
|
|
||||||
|
std::optional<api_utils::AssignmentGrade> old_grade;
|
||||||
|
api_utils::AssignmentGrade new_grade;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- User ops ----------
|
||||||
|
|
||||||
|
std::vector<UserRecord>
|
||||||
|
get_all_users(const CHClient& client);
|
||||||
|
|
||||||
|
bool
|
||||||
|
register_user(
|
||||||
|
const CHClient& client,
|
||||||
|
const std::string& username,
|
||||||
|
const std::string& password
|
||||||
|
);
|
||||||
|
|
||||||
|
bool
|
||||||
|
authenticate_user(
|
||||||
|
const CHClient& client,
|
||||||
|
const std::string& username,
|
||||||
|
const std::string& password
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------- Grades ----------
|
||||||
|
|
||||||
|
std::optional<GradeSnapshot>
|
||||||
|
load_latest_grades(
|
||||||
|
const CHClient& client,
|
||||||
|
const std::string& user_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------- Diff ----------
|
||||||
|
|
||||||
|
std::vector<AssignmentDiff>
|
||||||
|
diff_grade_responses(
|
||||||
|
const api_utils::GradesResponse& old_resp,
|
||||||
|
const api_utils::GradesResponse& new_resp
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------- Conditional insert ----------
|
||||||
|
|
||||||
|
bool
|
||||||
|
conditionally_insert_grades(
|
||||||
|
const CHClient& client,
|
||||||
|
const std::string& user_id,
|
||||||
|
const api_utils::GradesResponse& new_resp
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------- Persist diffs ----------
|
||||||
|
|
||||||
|
void
|
||||||
|
insert_grade_updates(
|
||||||
|
const CHClient& client,
|
||||||
|
const std::string& user_id,
|
||||||
|
const std::string& old_response_id,
|
||||||
|
const std::string& new_response_id,
|
||||||
|
const std::vector<AssignmentDiff>& diffs
|
||||||
|
);
|
||||||
|
|
||||||
|
} // namespace database_utils
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user