From 2ebffaec48cf03191ddb402ad9ef59a23663c839 Mon Sep 17 00:00:00 2001 From: Mars Ultor Date: Thu, 18 Dec 2025 15:10:04 -0600 Subject: [PATCH] chatgpt wrote the database layer for me --- src/types.cpp | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/types.hpp | 96 ++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) diff --git a/src/types.cpp b/src/types.cpp index 0be9e32..780e095 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -1 +1,189 @@ #include "types.hpp" +#include "clickhouse/base/uuid.h" +#include +#include +#include +#include + +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 get_all_users(const CHClient& client) { + std::vector 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()->At(i)); + u.login.username = b[1]->As()->At(i); + u.login.password = b[2]->As()->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(); + auto p = std::make_shared(); + 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()->At(0) > 0; } + ); + return ok; +} + +// ---------------- Load latest snapshot ---------------- +std::optional 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()->At(0)); + snap.response.success = b[1]->As()->At(0); + snap.response.totalClasses = b[2]->As()->At(0); + } + ); + if (!found) return std::nullopt; + return snap; +} + +// ---------------- Diff ---------------- +std::vector diff_grade_responses( + const api_utils::GradesResponse& old_resp, + const api_utils::GradesResponse& new_resp +) { + std::unordered_map old_map; + for (const auto& c : old_resp.grades) + for (const auto& g : c.grades) + old_map[c.className + "::" + g.name] = g; + + std::vector 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 client, + const std::string& user_id_str, + const std::string& old_response_id_str, + const std::string& new_response_id_str, + const std::vector& 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 + diff --git a/src/types.hpp b/src/types.hpp index 6f70f09..3a1d093 100644 --- a/src/types.hpp +++ b/src/types.hpp @@ -1 +1,97 @@ #pragma once + +#include +#include +#include +#include + +#include +#include "skwyward-api-utils.hpp" + +namespace database_utils { + +// ---------- DB Handle ---------- + +using CHClient = std::shared_ptr; + +// ---------- 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 old_grade; + api_utils::AssignmentGrade new_grade; +}; + +// ---------- User ops ---------- + +std::vector +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 +load_latest_grades( + const CHClient& client, + const std::string& user_id +); + +// ---------- Diff ---------- + +std::vector +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& diffs +); + +} // namespace database_utils +