chatgpt wrote the database layer for me

This commit is contained in:
2025-12-18 15:10:04 -06:00
parent d5ff3307ee
commit 2ebffaec48
2 changed files with 284 additions and 0 deletions

View File

@@ -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

View File

@@ -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