#include "csp/db/sqlite_db.h" #include #include #include #include #include namespace csp::db { namespace { void ThrowSqlite(int rc, sqlite3* db, const char* what) { if (rc == SQLITE_OK || rc == SQLITE_DONE || rc == SQLITE_ROW) return; const char* msg = db ? sqlite3_errmsg(db) : ""; throw std::runtime_error(std::string(what) + ": " + msg); } int64_t NowSec() { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } bool ColumnExists(sqlite3* db, const char* table, const char* col) { sqlite3_stmt* stmt = nullptr; const std::string sql = std::string("PRAGMA table_info(") + table + ")"; const int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return false; } bool found = false; while (sqlite3_step(stmt) == SQLITE_ROW) { const unsigned char* name = sqlite3_column_text(stmt, 1); if (name && std::string(reinterpret_cast(name)) == col) { found = true; break; } } sqlite3_finalize(stmt); return found; } void EnsureColumn(SqliteDb& db, const char* table, const char* col_name, const char* col_def) { if (ColumnExists(db.raw(), table, col_name)) return; db.Exec(std::string("ALTER TABLE ") + table + " ADD COLUMN " + col_def + ";"); } int CountRows(sqlite3* db, const char* table) { sqlite3_stmt* stmt = nullptr; const std::string sql = std::string("SELECT COUNT(1) FROM ") + table; int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return 0; } rc = sqlite3_step(stmt); int count = 0; if (rc == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return count; } std::optional QueryOneId(sqlite3* db, const std::string& sql) { sqlite3_stmt* stmt = nullptr; const int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return std::nullopt; } if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return std::nullopt; } const auto id = sqlite3_column_int64(stmt, 0); sqlite3_finalize(stmt); return id; } void InsertProblem(sqlite3* db, const std::string& slug, const std::string& title, const std::string& statement, int difficulty, const std::string& source, const std::string& sample_in, const std::string& sample_out, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO problems(slug,title,statement_md,difficulty,source,sample_input,sample_output,created_at) " "VALUES(?,?,?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert problem"); ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.slug"); ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.title"); ThrowSqlite(sqlite3_bind_text(stmt, 3, statement.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.statement"); ThrowSqlite(sqlite3_bind_int(stmt, 4, difficulty), db, "bind problem.difficulty"); ThrowSqlite(sqlite3_bind_text(stmt, 5, source.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.source"); ThrowSqlite(sqlite3_bind_text(stmt, 6, sample_in.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.sample_input"); ThrowSqlite(sqlite3_bind_text(stmt, 7, sample_out.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.sample_output"); ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_at), db, "bind problem.created_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert problem"); sqlite3_finalize(stmt); } void InsertProblemTag(sqlite3* db, int64_t problem_id, const std::string& tag) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO problem_tags(problem_id,tag) VALUES(?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert problem_tag"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_tag.problem_id"); ThrowSqlite(sqlite3_bind_text(stmt, 2, tag.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem_tag.tag"); ThrowSqlite(sqlite3_step(stmt), db, "insert problem_tag"); sqlite3_finalize(stmt); } void InsertKbArticle(sqlite3* db, const std::string& slug, const std::string& title, const std::string& content_md, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO kb_articles(slug,title,content_md,created_at) VALUES(?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert kb_article"); ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind kb_article.slug"); ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind kb_article.title"); ThrowSqlite(sqlite3_bind_text(stmt, 3, content_md.c_str(), -1, SQLITE_TRANSIENT), db, "bind kb_article.content"); ThrowSqlite(sqlite3_bind_int64(stmt, 4, created_at), db, "bind kb_article.created_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert kb_article"); sqlite3_finalize(stmt); } void InsertKbLink(sqlite3* db, int64_t article_id, int64_t problem_id) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO kb_article_links(article_id,problem_id) VALUES(?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert kb_article_link"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, article_id), db, "bind kb_article_link.article_id"); ThrowSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind kb_article_link.problem_id"); ThrowSqlite(sqlite3_step(stmt), db, "insert kb_article_link"); sqlite3_finalize(stmt); } void InsertContest(sqlite3* db, const std::string& title, int64_t starts_at, int64_t ends_at, const std::string& rule_json) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO contests(title,starts_at,ends_at,rule_json) VALUES(?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert contest"); ThrowSqlite(sqlite3_bind_text(stmt, 1, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest.title"); ThrowSqlite(sqlite3_bind_int64(stmt, 2, starts_at), db, "bind contest.starts_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 3, ends_at), db, "bind contest.ends_at"); ThrowSqlite(sqlite3_bind_text(stmt, 4, rule_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest.rule_json"); ThrowSqlite(sqlite3_step(stmt), db, "insert contest"); sqlite3_finalize(stmt); } void InsertContestProblem(sqlite3* db, int64_t contest_id, int64_t problem_id, int idx) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO contest_problems(contest_id,problem_id,idx) VALUES(?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert contest_problem"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db, "bind contest_problem.contest_id"); ThrowSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind contest_problem.problem_id"); ThrowSqlite(sqlite3_bind_int(stmt, 3, idx), db, "bind contest_problem.idx"); ThrowSqlite(sqlite3_step(stmt), db, "insert contest_problem"); sqlite3_finalize(stmt); } void InsertRedeemItem(sqlite3* db, const std::string& name, const std::string& description, const std::string& unit_label, int holiday_cost, int studyday_cost, int is_active, int is_global, int64_t created_by, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO redeem_items(" "name,description,unit_label,holiday_cost,studyday_cost,is_active,is_global,created_by,created_at,updated_at" ") VALUES(?,?,?,?,?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert redeem_item"); ThrowSqlite(sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT), db, "bind redeem_item.name"); ThrowSqlite(sqlite3_bind_text(stmt, 2, description.c_str(), -1, SQLITE_TRANSIENT), db, "bind redeem_item.description"); ThrowSqlite(sqlite3_bind_text(stmt, 3, unit_label.c_str(), -1, SQLITE_TRANSIENT), db, "bind redeem_item.unit_label"); ThrowSqlite(sqlite3_bind_int(stmt, 4, holiday_cost), db, "bind redeem_item.holiday_cost"); ThrowSqlite(sqlite3_bind_int(stmt, 5, studyday_cost), db, "bind redeem_item.studyday_cost"); ThrowSqlite(sqlite3_bind_int(stmt, 6, is_active), db, "bind redeem_item.is_active"); ThrowSqlite(sqlite3_bind_int(stmt, 7, is_global), db, "bind redeem_item.is_global"); ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_by), db, "bind redeem_item.created_by"); ThrowSqlite(sqlite3_bind_int64(stmt, 9, created_at), db, "bind redeem_item.created_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 10, created_at), db, "bind redeem_item.updated_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert redeem_item"); sqlite3_finalize(stmt); } } // namespace SqliteDb SqliteDb::OpenFile(const std::string& path) { sqlite3* db = nullptr; const int rc = sqlite3_open(path.c_str(), &db); if (rc != SQLITE_OK) { const char* msg = db ? sqlite3_errmsg(db) : ""; if (db) sqlite3_close(db); throw std::runtime_error(std::string("sqlite3_open failed: ") + msg); } return SqliteDb(db); } SqliteDb SqliteDb::OpenMemory() { sqlite3* db = nullptr; const int rc = sqlite3_open(":memory:", &db); ThrowSqlite(rc, db, "sqlite3_open(:memory:) failed"); return SqliteDb(db); } SqliteDb::~SqliteDb() { if (db_) sqlite3_close(db_); } SqliteDb::SqliteDb(SqliteDb&& other) noexcept : db_(other.db_) { other.db_ = nullptr; } SqliteDb& SqliteDb::operator=(SqliteDb&& other) noexcept { if (this == &other) return *this; if (db_) sqlite3_close(db_); db_ = other.db_; other.db_ = nullptr; return *this; } void SqliteDb::Exec(const std::string& sql) { char* err = nullptr; const int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &err); if (rc != SQLITE_OK) { std::string msg = err ? err : ""; sqlite3_free(err); ThrowSqlite(rc, db_, msg.c_str()); } } void ApplyMigrations(SqliteDb& db) { // Keep it simple for MVP: create missing tables, then patch missing columns. db.Exec("PRAGMA foreign_keys = ON;"); db.Exec(R"SQL( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_salt TEXT NOT NULL, password_hash TEXT NOT NULL, rating INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id INTEGER NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problems ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, statement_md TEXT NOT NULL, difficulty INTEGER NOT NULL DEFAULT 1, source TEXT NOT NULL DEFAULT "", statement_url TEXT NOT NULL DEFAULT "", llm_profile_json TEXT NOT NULL DEFAULT "{}", sample_input TEXT NOT NULL DEFAULT "", sample_output TEXT NOT NULL DEFAULT "", created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS problem_tags ( problem_id INTEGER NOT NULL, tag TEXT NOT NULL, PRIMARY KEY(problem_id, tag), FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS submissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, contest_id INTEGER, language TEXT NOT NULL, code TEXT NOT NULL, status TEXT NOT NULL, score INTEGER NOT NULL DEFAULT 0, time_ms INTEGER NOT NULL DEFAULT 0, memory_kb INTEGER NOT NULL DEFAULT 0, compile_log TEXT NOT NULL DEFAULT "", runtime_log TEXT NOT NULL DEFAULT "", created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE, FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS wrong_book ( user_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, last_submission_id INTEGER, note TEXT NOT NULL DEFAULT "", updated_at INTEGER NOT NULL, PRIMARY KEY(user_id, problem_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE, FOREIGN KEY(last_submission_id) REFERENCES submissions(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS contests ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, starts_at INTEGER NOT NULL, ends_at INTEGER NOT NULL, rule_json TEXT NOT NULL DEFAULT "{}" ); CREATE TABLE IF NOT EXISTS contest_problems ( contest_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, idx INTEGER NOT NULL, PRIMARY KEY(contest_id, problem_id), FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS contest_registrations ( contest_id INTEGER NOT NULL, user_id INTEGER NOT NULL, registered_at INTEGER NOT NULL, PRIMARY KEY(contest_id, user_id), FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS kb_articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, content_md TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS kb_article_links ( article_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, PRIMARY KEY(article_id, problem_id), FOREIGN KEY(article_id) REFERENCES kb_articles(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS import_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT NOT NULL, trigger TEXT NOT NULL DEFAULT "manual", total_count INTEGER NOT NULL DEFAULT 0, processed_count INTEGER NOT NULL DEFAULT 0, success_count INTEGER NOT NULL DEFAULT 0, failed_count INTEGER NOT NULL DEFAULT 0, options_json TEXT NOT NULL DEFAULT "{}", last_error TEXT NOT NULL DEFAULT "", started_at INTEGER NOT NULL, finished_at INTEGER, updated_at INTEGER NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS import_job_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id INTEGER NOT NULL, source_path TEXT NOT NULL, status TEXT NOT NULL DEFAULT "queued", title TEXT NOT NULL DEFAULT "", difficulty INTEGER NOT NULL DEFAULT 0, problem_id INTEGER, error_text TEXT NOT NULL DEFAULT "", started_at INTEGER, finished_at INTEGER, updated_at INTEGER NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(job_id) REFERENCES import_jobs(id) ON DELETE CASCADE, UNIQUE(job_id, source_path) ); CREATE TABLE IF NOT EXISTS problem_drafts ( user_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, language TEXT NOT NULL DEFAULT "cpp", code TEXT NOT NULL DEFAULT "", stdin TEXT NOT NULL DEFAULT "", updated_at INTEGER NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(user_id, problem_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problem_solution_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, problem_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT "queued", progress INTEGER NOT NULL DEFAULT 0, message TEXT NOT NULL DEFAULT "", created_by INTEGER NOT NULL DEFAULT 0, max_solutions INTEGER NOT NULL DEFAULT 3, created_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER, updated_at INTEGER NOT NULL, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problem_solutions ( id INTEGER PRIMARY KEY AUTOINCREMENT, problem_id INTEGER NOT NULL, variant INTEGER NOT NULL DEFAULT 1, title TEXT NOT NULL DEFAULT "", idea_md TEXT NOT NULL DEFAULT "", explanation_md TEXT NOT NULL DEFAULT "", code_cpp TEXT NOT NULL DEFAULT "", complexity TEXT NOT NULL DEFAULT "", tags_json TEXT NOT NULL DEFAULT "[]", source TEXT NOT NULL DEFAULT "llm", created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problem_solution_view_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, day_key TEXT NOT NULL, viewed_at INTEGER NOT NULL, charged INTEGER NOT NULL DEFAULT 0, cost INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS submission_feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, submission_id INTEGER NOT NULL UNIQUE, feedback_md TEXT NOT NULL DEFAULT "", links_json TEXT NOT NULL DEFAULT "[]", model_name TEXT NOT NULL DEFAULT "", status TEXT NOT NULL DEFAULT "ready", created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(submission_id) REFERENCES submissions(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS redeem_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT "", unit_label TEXT NOT NULL DEFAULT "小时", holiday_cost INTEGER NOT NULL DEFAULT 5, studyday_cost INTEGER NOT NULL DEFAULT 25, is_active INTEGER NOT NULL DEFAULT 1, is_global INTEGER NOT NULL DEFAULT 1, created_by INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS redeem_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, item_id INTEGER NOT NULL, item_name TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, day_type TEXT NOT NULL DEFAULT "studyday", unit_cost INTEGER NOT NULL DEFAULT 0, total_cost INTEGER NOT NULL DEFAULT 0, note TEXT NOT NULL DEFAULT "", created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(item_id) REFERENCES redeem_items(id) ON DELETE RESTRICT ); CREATE TABLE IF NOT EXISTS daily_task_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, task_code TEXT NOT NULL, day_key TEXT NOT NULL, reward INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(user_id, task_code, day_key) ); )SQL"); // Backward-compatible schema upgrades for existing deployments. EnsureColumn(db, "problems", "sample_input", "sample_input TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problems", "sample_output", "sample_output TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problems", "statement_url", "statement_url TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problems", "llm_profile_json", "llm_profile_json TEXT NOT NULL DEFAULT '{}'"); EnsureColumn(db, "import_jobs", "trigger", "trigger TEXT NOT NULL DEFAULT 'manual'"); EnsureColumn(db, "import_jobs", "options_json", "options_json TEXT NOT NULL DEFAULT '{}'"); EnsureColumn(db, "import_jobs", "last_error", "last_error TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "submissions", "contest_id", "contest_id INTEGER"); EnsureColumn(db, "submissions", "compile_log", "compile_log TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "submissions", "runtime_log", "runtime_log TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_drafts", "stdin", "stdin TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solution_jobs", "max_solutions", "max_solutions INTEGER NOT NULL DEFAULT 3"); EnsureColumn(db, "problem_solutions", "variant", "variant INTEGER NOT NULL DEFAULT 1"); EnsureColumn(db, "problem_solutions", "idea_md", "idea_md TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "explanation_md", "explanation_md TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "code_cpp", "code_cpp TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "complexity", "complexity TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "tags_json", "tags_json TEXT NOT NULL DEFAULT '[]'"); // Build indexes after compatibility ALTERs so old schemas won't fail on // missing columns (e.g. legacy submissions table without contest_id). db.Exec(R"SQL( CREATE INDEX IF NOT EXISTS idx_submissions_user_created_at ON submissions(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_submissions_problem_created_at ON submissions(problem_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_submissions_contest_user_created_at ON submissions(contest_id, user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_tags_tag ON problem_tags(tag); CREATE INDEX IF NOT EXISTS idx_kb_article_links_problem_id ON kb_article_links(problem_id); CREATE INDEX IF NOT EXISTS idx_import_jobs_created_at ON import_jobs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_import_job_items_job_status ON import_job_items(job_id, status, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_drafts_updated ON problem_drafts(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_solution_jobs_problem ON problem_solution_jobs(problem_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_solutions_problem ON problem_solutions(problem_id, variant, id); CREATE INDEX IF NOT EXISTS idx_solution_view_logs_user_problem ON problem_solution_view_logs(user_id, problem_id, viewed_at DESC); CREATE INDEX IF NOT EXISTS idx_solution_view_logs_user_day ON problem_solution_view_logs(user_id, day_key, viewed_at DESC); CREATE INDEX IF NOT EXISTS idx_redeem_items_active ON redeem_items(is_active, id); CREATE INDEX IF NOT EXISTS idx_redeem_records_user_created ON redeem_records(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_daily_task_logs_user_day ON daily_task_logs(user_id, day_key, created_at DESC); )SQL"); } void SeedDemoData(SqliteDb& db) { sqlite3* raw = db.raw(); const int64_t now = NowSec(); if (CountRows(raw, "problems") == 0) { InsertProblem( raw, "a-plus-b", "A + B", "给定两个整数 A 与 B,输出它们的和。", 1, "CSP-入门", "1 2\n", "3\n", now); InsertProblem( raw, "fibonacci-n", "Fibonacci 第 n 项", "输入 n (0<=n<=40),输出第 n 项 Fibonacci 数。", 2, "CSP-基础", "10\n", "55\n", now); InsertProblem( raw, "sort-numbers", "整数排序", "输入 n 和 n 个整数,按升序输出。", 2, "CSP-基础", "5\n5 1 4 2 3\n", "1 2 3 4 5\n", now); } if (CountRows(raw, "problem_tags") == 0) { const auto p1 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'"); const auto p3 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='sort-numbers'"); if (p1) { InsertProblemTag(raw, *p1, "math"); InsertProblemTag(raw, *p1, "implementation"); } if (p2) { InsertProblemTag(raw, *p2, "dp"); InsertProblemTag(raw, *p2, "recursion"); } if (p3) { InsertProblemTag(raw, *p3, "sort"); InsertProblemTag(raw, *p3, "array"); } } if (CountRows(raw, "kb_articles") == 0) { InsertKbArticle( raw, "cpp-fast-io", "C++ 快速输入输出", "# C++ 快速输入输出\n\n在 OI/CSP 中,建议关闭同步并解绑 cin/cout:\n\n```cpp\nstd::ios::sync_with_stdio(false);\nstd::cin.tie(nullptr);\n```\n", now); InsertKbArticle( raw, "intro-dp", "动态规划入门", "# 动态规划入门\n\n动态规划的核心是:**状态定义**、**状态转移**、**边界条件**。\n", now); } if (CountRows(raw, "kb_article_links") == 0) { const auto p1 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'"); const auto a1 = QueryOneId(raw, "SELECT id FROM kb_articles WHERE slug='cpp-fast-io'"); const auto a2 = QueryOneId(raw, "SELECT id FROM kb_articles WHERE slug='intro-dp'"); if (a1 && p1) InsertKbLink(raw, *a1, *p1); if (a2 && p2) InsertKbLink(raw, *a2, *p2); } if (CountRows(raw, "contests") == 0) { InsertContest( raw, "CSP 模拟赛(示例)", now - 3600, now + 7 * 24 * 3600, R"({"type":"acm","desc":"按通过题数与罚时排名"})"); } if (CountRows(raw, "contest_problems") == 0) { const auto contest_id = QueryOneId(raw, "SELECT id FROM contests ORDER BY id LIMIT 1"); const auto p1 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1 OFFSET 1"); if (contest_id && p1) InsertContestProblem(raw, *contest_id, *p1, 1); if (contest_id && p2) InsertContestProblem(raw, *contest_id, *p2, 2); } if (CountRows(raw, "redeem_items") == 0) { InsertRedeemItem( raw, "私人玩游戏时间", "全局用户可兑换:假期 1 小时 5 Rating;学习日/非节假日 1 小时 25 Rating。", "小时", 5, 25, 1, 1, 0, now); } } } // namespace csp::db