733 行
26 KiB
C++
733 行
26 KiB
C++
#include "csp/db/sqlite_db.h"
|
||
|
||
#include <chrono>
|
||
#include <optional>
|
||
#include <stdexcept>
|
||
#include <string>
|
||
#include <utility>
|
||
|
||
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<seconds>(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<const char*>(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<int64_t> 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
|