#include "db/internal.hpp" #include "settings.hpp" #include #include #include #include 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 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 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::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); }