feat(backend): add sqlite migrations + app state + tests
这个提交包含在:
135
backend/src/db/sqlite_db.cc
普通文件
135
backend/src/db/sqlite_db.cc
普通文件
@@ -0,0 +1,135 @@
|
||||
#include "csp/db/sqlite_db.h"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace csp::db {
|
||||
|
||||
namespace {
|
||||
|
||||
void ThrowSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK) return;
|
||||
const char* msg = db ? sqlite3_errmsg(db) : "";
|
||||
throw std::runtime_error(std::string(what) + ": " + msg);
|
||||
}
|
||||
|
||||
} // 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: apply the bundled init SQL.
|
||||
// In later iterations we'll add a migrations table + incremental runner.
|
||||
db.Exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
// 001_init.sql (embedded)
|
||||
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 "",
|
||||
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,
|
||||
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,
|
||||
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 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 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);
|
||||
)SQL");
|
||||
}
|
||||
|
||||
} // namespace csp::db
|
||||
在新工单中引用
屏蔽一个用户