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 "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
|
||||
|
||||
#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