diff --git a/sql/migration3.sql b/sql/migration3.sql new file mode 100644 index 0000000..83db748 --- /dev/null +++ b/sql/migration3.sql @@ -0,0 +1,28 @@ +/* + It is recommended in the SQLite manual to turn off + foreign keys when making schema changes that involve them +*/ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +-- Change username column (Login) to be case-insensitive +CREATE TABLE Temp ( + AccountID INTEGER NOT NULL, + Login TEXT NOT NULL UNIQUE COLLATE NOCASE, + Password TEXT NOT NULL, + Selected INTEGER DEFAULT 1 NOT NULL, + AccountLevel INTEGER NOT NULL, + Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, + LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, + BannedUntil INTEGER DEFAULT 0 NOT NULL, + BannedSince INTEGER DEFAULT 0 NOT NULL, + BanReason TEXT DEFAULT '' NOT NULL, + PRIMARY KEY(AccountID AUTOINCREMENT) +); +INSERT INTO Temp SELECT * FROM Accounts; +DROP TABLE Accounts; +ALTER TABLE Temp RENAME TO Accounts; +-- Update DB Version +UPDATE Meta SET Value = 4 WHERE Key = 'DatabaseVersion'; +UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration'; +COMMIT; +PRAGMA foreign_keys=ON; diff --git a/sql/tables.sql b/sql/tables.sql index cbf97f8..564e2ac 100644 --- a/sql/tables.sql +++ b/sql/tables.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS Accounts ( AccountID INTEGER NOT NULL, - Login TEXT NOT NULL UNIQUE, + Login TEXT NOT NULL UNIQUE COLLATE NOCASE, Password TEXT NOT NULL, Selected INTEGER DEFAULT 1 NOT NULL, AccountLevel INTEGER NOT NULL, diff --git a/src/db/Database.hpp b/src/db/Database.hpp index 89bc315..240cd76 100644 --- a/src/db/Database.hpp +++ b/src/db/Database.hpp @@ -5,7 +5,7 @@ #include #include -#define DATABASE_VERSION 3 +#define DATABASE_VERSION 4 namespace Database { diff --git a/src/db/init.cpp b/src/db/init.cpp index 37a6b04..a8d214a 100644 --- a/src/db/init.cpp +++ b/src/db/init.cpp @@ -9,6 +9,37 @@ 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 @@ -143,6 +174,10 @@ static void checkMetaTable() { } 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;