mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-09-26 10:00:05 +00:00
The server now checks the libsqlite both at compile time and on server startup. The version the executable was built with and the one it's running with may be different, so long as they're both at or above the minimum supported version. One or both version numbers are printed on startup, depending on if they're identical or not. The compile-time ("Built with") version depends on the sqlite3.h header used during compilation, while the runtime ("Using") version depends on either: * The sqlite3.c version used during compilation, if statically linked. (Which may be different from the header version and still compile and run fine.) * The version of the libsqlite3.so or sqlite3.dll that the server loaded, if dynamically linked. Version mismatches here are normal, especially on Unix systems with their own system libraries. The current minimum version is 3.33.0, from 2020-08-14, as that's the one that introduced the UPDATE-FROM syntax used during login by Database::updateSelectedByPlayerId(). Also rearranged the prints and initialization calls in main() slightly.
289 lines
9.3 KiB
C++
289 lines
9.3 KiB
C++
#include "db/internal.hpp"
|
|
#include "settings.hpp"
|
|
|
|
#include <string>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
std::mutex dbCrit;
|
|
sqlite3 *db;
|
|
|
|
/*
|
|
* When migrating from DB version 3 to 4, we change the username column
|
|
* to be case-insensitive. This function ensures there aren't any
|
|
* duplicates, e.g. username and USERNAME, before doing the migration.
|
|
* I handled this in the code itself rather than the migration file just so
|
|
* we can have a more detailed error message than what SQLite provides.
|
|
*/
|
|
static void checkCaseSensitiveDupes() {
|
|
const char* sql = "SELECT Login, COUNT(*) FROM Accounts GROUP BY LOWER(Login) HAVING COUNT(*) > 1;";
|
|
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
int stat = sqlite3_step(stmt);
|
|
|
|
if (stat == SQLITE_DONE) {
|
|
// no rows returned, so we're good
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
} else if (stat != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check for duplicate accounts: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
std::cout << "[FATAL] Case-sensitive duplicates detected in the Login column." << std::endl;
|
|
std::cout << "Either manually delete/rename the offending accounts, or run the pruning script:" << std::endl;
|
|
std::cout << "https://github.com/OpenFusionProject/scripts/tree/main/db_migration/caseinsens.py" << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
static void createMetaTable() {
|
|
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
|
|
|
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
|
|
|
const char* sql = R"(
|
|
CREATE TABLE Meta(
|
|
Key TEXT NOT NULL UNIQUE,
|
|
Value INTEGER NOT NULL
|
|
);
|
|
)";
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
|
exit(1);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
sql = R"(
|
|
INSERT INTO Meta (Key, Value)
|
|
VALUES (?, ?);
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
sqlite3_bind_text(stmt, 1, "ProtocolVersion", -1, NULL);
|
|
sqlite3_bind_int(stmt, 2, PROTOCOL_VERSION);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
|
exit(1);
|
|
}
|
|
|
|
sqlite3_reset(stmt);
|
|
sqlite3_bind_text(stmt, 1, "DatabaseVersion", -1, NULL);
|
|
sqlite3_bind_int(stmt, 2, DATABASE_VERSION);
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
|
exit(1);
|
|
}
|
|
|
|
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
|
std::cout << "[INFO] Created new meta table" << std::endl;
|
|
}
|
|
|
|
static void checkMetaTable() {
|
|
// first check if meta table exists
|
|
const char* sql = R"(
|
|
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Meta';
|
|
)";
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
int count = sqlite3_column_int(stmt, 0);
|
|
if (count == 0) {
|
|
sqlite3_finalize(stmt);
|
|
// check if there's other non-internal tables first
|
|
sql = R"(
|
|
SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (sqlite3_step(stmt) != SQLITE_ROW || sqlite3_column_int(stmt, 0) != 0) {
|
|
sqlite3_finalize(stmt);
|
|
std::cout << "[FATAL] Existing DB is outdated" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// create meta table
|
|
sqlite3_finalize(stmt);
|
|
return createMetaTable();
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
// check protocol version
|
|
sql = R"(
|
|
SELECT Value FROM Meta WHERE Key = 'ProtocolVersion';
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check DB Protocol Version: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
if (sqlite3_column_int(stmt, 0) != PROTOCOL_VERSION) {
|
|
sqlite3_finalize(stmt);
|
|
std::cout << "[FATAL] DB Protocol Version doesn't match Server Build" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
sql = R"(
|
|
SELECT Value FROM Meta WHERE Key = 'DatabaseVersion';
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check DB Version: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
int dbVersion = sqlite3_column_int(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (dbVersion > DATABASE_VERSION) {
|
|
std::cout << "[FATAL] Server Build is incompatible with DB Version" << std::endl;
|
|
exit(1);
|
|
} else if (dbVersion < DATABASE_VERSION) {
|
|
// we're gonna migrate; back up the DB
|
|
std::cout << "[INFO] Backing up database" << std::endl;
|
|
// copy db file over using binary streams
|
|
std::ifstream src(settings::DBPATH, std::ios::binary);
|
|
std::ofstream dst(settings::DBPATH + ".old." + std::to_string(dbVersion), std::ios::binary);
|
|
dst << src.rdbuf();
|
|
src.close();
|
|
dst.close();
|
|
}
|
|
|
|
while (dbVersion != DATABASE_VERSION) {
|
|
// need to run this before we do any migration logic
|
|
if (dbVersion == 3)
|
|
checkCaseSensitiveDupes();
|
|
|
|
// db migrations
|
|
std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl;
|
|
|
|
std::string path = "sql/migration" + std::to_string(dbVersion) + ".sql";
|
|
std::ifstream file(path);
|
|
if (!file.is_open()) {
|
|
std::cout << "[FATAL] Failed to migrate database: Couldn't open migration file" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
std::ostringstream stream;
|
|
stream << file.rdbuf();
|
|
std::string sql = stream.str();
|
|
int rc = sqlite3_exec(db, sql.c_str(), NULL, NULL, NULL);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
std::cout << "[FATAL] Failed to migrate database: " << sqlite3_errmsg(db) << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
dbVersion++;
|
|
std::cout << "[INFO] Successful Database Migration to Version " << dbVersion << std::endl;
|
|
}
|
|
}
|
|
|
|
static void createTables() {
|
|
std::ifstream file("sql/tables.sql");
|
|
if (!file.is_open()) {
|
|
std::cout << "[FATAL] Failed to open database scheme" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
std::ostringstream stream;
|
|
stream << file.rdbuf();
|
|
std::string read = stream.str();
|
|
const char* sql = read.c_str();
|
|
|
|
char* errMsg = 0;
|
|
int rc = sqlite3_exec(db, sql, NULL, NULL, &errMsg);
|
|
if (rc != SQLITE_OK) {
|
|
std::cout << "[FATAL] Database failed to create tables: " << errMsg << std::endl;
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static int getTableSize(std::string tableName) {
|
|
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
|
|
|
const char* sql = "SELECT COUNT(*) FROM ?";
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
|
|
sqlite3_step(stmt);
|
|
int result = sqlite3_column_int(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
void Database::init() {
|
|
std::cout << "[INFO] Built with libsqlite " SQLITE_VERSION << std::endl;
|
|
|
|
if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER)
|
|
std::cout << "[INFO] Using libsqlite " << std::string(sqlite3_libversion()) << std::endl;
|
|
|
|
if (sqlite3_libversion_number() < MIN_SUPPORTED_SQLITE_NUMBER) {
|
|
std::cerr << "[FATAL] Runtime sqlite version too old. Minimum compatible version: " MIN_SUPPORTED_SQLITE << std::endl;
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
void Database::open() {
|
|
|
|
// XXX: move locks here
|
|
int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
|
|
if (rc != SQLITE_OK) {
|
|
std::cout << "[FATAL] Cannot open database: " << sqlite3_errmsg(db) << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// foreign keys in sqlite are off by default; enable them
|
|
sqlite3_exec(db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL);
|
|
|
|
// just in case a DB operation collides with an external manual modification
|
|
sqlite3_busy_timeout(db, 2000);
|
|
|
|
checkMetaTable();
|
|
createTables();
|
|
|
|
std::cout << "[INFO] Database in operation ";
|
|
int accounts = getTableSize("Accounts");
|
|
int players = getTableSize("Players");
|
|
std::string message = "";
|
|
if (accounts > 0) {
|
|
message += ": Found " + std::to_string(accounts) + " Account";
|
|
if (accounts > 1)
|
|
message += "s";
|
|
}
|
|
if (players > 0) {
|
|
message += " and " + std::to_string(players) + " Player Character";
|
|
if (players > 1)
|
|
message += "s";
|
|
}
|
|
std::cout << message << std::endl;
|
|
}
|
|
|
|
void Database::close() {
|
|
sqlite3_close(db);
|
|
}
|