diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index e179bfa0..3588b379 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -73,60 +73,45 @@ namespace Database::Migration Db& _db; }; + static void - doDbMigration(Session& session) + migrateFromV5(Session& session) { - static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; + session.getDboSession().execute("DELETE FROM auth_token"); // format has changed + } - ScopedNoForeignKeys noPragmaKeys {session.getDb()}; + static + void + migrateFromV6(Session& session) + { + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - while (1) - { - auto uniqueTransaction {session.createUniqueTransaction()}; - Version version; - try - { - version = VersionInfo::getOrCreate(session)->getVersion(); - LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; - if (version == LMS_DATABASE_VERSION) - { - LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!"; - return; - } - } - catch (std::exception& e) - { - LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); - throw LmsException {outdatedMsg}; - } + static + void + migrateFromV7(Session& session) + { + session.getDboSession().execute("DROP TABLE similarity_settings"); + session.getDboSession().execute("DROP TABLE similarity_settings_feature"); + session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(ScanSettings::RecommendationEngineType::Clusters)) + ")"); + } - LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; + static + void + migrateFromV8(Session& session) + { + // Better cover handling, need to rescan the whole files + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - if (version == 5) - { - session.getDboSession().execute("DELETE FROM auth_token"); // format has changed - } - else if (version == 6) - { - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 7) - { - session.getDboSession().execute("DROP TABLE similarity_settings"); - session.getDboSession().execute("DROP TABLE similarity_settings_feature"); - session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(ScanSettings::RecommendationEngineType::Clusters)) + ")"); - } - else if (version == 8) - { - // Better cover handling, need to rescan the whole files - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 9) - { - session.getDboSession().execute(R"( + static + void + migrateFromV9(Session& session) + { + session.getDboSession().execute(R"( CREATE TABLE IF NOT EXISTS "track_bookmark" ( "id" integer primary key autoincrement, "version" integer not null, @@ -137,78 +122,109 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" ( constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred );)"); - } - else if (version == 10) - { - ScanSettings::get(session).modify()->addAudioFileExtension(".m4b"); - ScanSettings::get(session).modify()->addAudioFileExtension(".alac"); - } - else if (version == 11) - { - // Sanitize bad MBID, need to rescan the whole files - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 12) - { - // Artist and release that have a badly parsed name but a MBID had no chance to updat the name - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 13) - { - // Always store UUID in lower case + better WMA parsing - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 14) - { - // SortName now set from metadata - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 15) - { - session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); - } - else if (version == 16) - { - session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)"); - session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)"); + } - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 17) - { - // Drop colums total_disc/total_track from release - session.getDboSession().execute(R"( + static + void + migrateFromV10(Session& session) + { + ScanSettings::get(session).modify()->addAudioFileExtension(".m4b"); + ScanSettings::get(session).modify()->addAudioFileExtension(".alac"); + } + + static + void + migrateFromV11(Session& session) + { + // Sanitize bad MBID, need to rescan the whole files + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV12(Session& session) + { + // Artist and release that have a badly parsed name but a MBID had no chance to updat the name + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV13(Session& session) + { + // Always store UUID in lower case + better WMA parsing + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV14(Session& session) + { + // SortName now set from metadata + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV15(Session& session) + { + session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); + } + + static + void + migrateFromV16(Session& session) + { + session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)"); + session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV17(Session& session) + { + // Drop colums total_disc/total_track from release + session.getDboSession().execute(R"( CREATE TABLE "release_backup" ( "id" integer primary key autoincrement, "version" integer not null, "name" text not null, "mbid" text not null ))"); - session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); - session.getDboSession().execute("DROP TABLE release"); - session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release"); + session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); + session.getDboSession().execute("DROP TABLE release"); + session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 18) - { - session.getDboSession().execute(R"( + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + + static + void + migrateFromV18(Session& session) + { + session.getDboSession().execute(R"( CREATE TABLE IF NOT EXISTS "subsonic_settings" ( "id" integer primary key autoincrement, "version" integer not null, "api_enabled" boolean not null, "artist_list_mode" integer not null ))"); - } - else if (version == 19) - { - session.getDboSession().execute(R"( + } + + static + void + migrateFromV19(Session& session) + { + session.getDboSession().execute(R"( CREATE TABLE "user_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -226,67 +242,96 @@ CREATE TABLE "user_backup" ( "repeat_all" boolean not null, "radio" boolean not null ))"); - session.getDboSession().execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "} - + (User::defaultSubsonicTranscodeEnable ? "1" : "0") - + ", " + std::to_string(static_cast(User::defaultSubsonicTranscodeFormat)) - + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate) - + ", " + std::to_string(static_cast(User::defaultSubsonicArtistListMode)) - + ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); - session.getDboSession().execute("DROP TABLE user"); - session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); - } - else if (version == 20) - { - session.getDboSession().execute("DROP TABLE subsonic_settings"); - } - else if (version == 21) - { - session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL"); - session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL"); + session.getDboSession().execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "} + + (User::defaultSubsonicTranscodeEnable ? "1" : "0") + + ", " + std::to_string(static_cast(User::defaultSubsonicTranscodeFormat)) + + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate) + + ", " + std::to_string(static_cast(User::defaultSubsonicArtistListMode)) + + ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); + session.getDboSession().execute("DROP TABLE user"); + session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); + } - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 22) - { - session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''"); + static + void + migrateFromV20(Session& session) + { + session.getDboSession().execute("DROP TABLE subsonic_settings"); + } - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 23) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 24) - { - // User's AuthMode - session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); - } - else if (version == 25) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 26) - { - // Composer, mixer, etc. support - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 27) - { - // Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 28) - { - // Drop Auth mode - session.getDboSession().execute(R"( + static + void + migrateFromV21(Session& session) + { + session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL"); + session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + + static + void + migrateFromV22(Session& session) + { + session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + + static + void + migrateFromV23(Session& session) + { + // Better cover detection + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV24(Session& session) + { + // User's AuthMode + session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); + } + + static + void + migrateFromV25(Session& session) + { + // Better cover detection + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV26(Session& session) + { + // Composer, mixer, etc. support + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV27(Session& session) + { + // Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV28(Session& session) + { + // Drop Auth mode + session.getDboSession().execute(R"( CREATE TABLE "user_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -304,27 +349,33 @@ CREATE TABLE "user_backup" ( "repeat_all" boolean not null, "radio" boolean not null ))"); - session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); - session.getDboSession().execute("DROP TABLE user"); - session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); - } - else if (version == 29) - { - session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT"); - session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT"); - session.getDboSession().execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); - session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT"); + session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); + session.getDboSession().execute("DROP TABLE user"); + session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); + } - session.getDboSession().execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__"); + static + void + migrateFromV29(Session& session) + { + session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT"); + session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT"); + session.getDboSession().execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); + session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT"); - // MBID changes - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 30) - { - // drop "year" and "original_year" (rescan needed to convert them into dates) - session.getDboSession().execute(R"( + session.getDboSession().execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__"); + + // MBID changes + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + + static + void + migrateFromV30(Session& session) + { + // drop "year" and "original_year" (rescan needed to convert them into dates) + session.getDboSession().execute(R"( CREATE TABLE "track_backup" ( "id" integer primary key autoincrement, "version" integer not null, @@ -345,22 +396,21 @@ CREATE TABLE "track_backup" ( "release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT, constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track"); - session.getDboSession().execute("DROP TABLE track"); - session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); + session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track"); + session.getDboSession().execute("DROP TABLE track"); + session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - else if (version == 31) - { - // new star system, using dedicated ObjectSets per scrobbler - session.getDboSession().execute("DROP TABLE user_artist_starred"); - session.getDboSession().execute("DROP TABLE user_release_starred"); - session.getDboSession().execute("DROP TABLE user_track_starred"); + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } - session.getDboSession().execute(R"( -CREATE TABLE IF NOT EXISTS "starred_artist" ( + static + void + migrateFromV31(Session& session) + { + // new star system, using dedicated entries per scrobbler and date time + session.getDboSession().execute(R"( +CREATE TABLE "starred_artist" ( "id" integer primary key autoincrement, "version" integer not null, "scrobbler" integer not null, @@ -371,8 +421,8 @@ CREATE TABLE IF NOT EXISTS "starred_artist" ( constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute(R"( -CREATE TABLE IF NOT EXISTS "starred_release" ( + session.getDboSession().execute(R"( +CREATE TABLE "starred_release" ( "id" integer primary key autoincrement, "version" integer not null, "scrobbler" integer not null, @@ -383,8 +433,8 @@ CREATE TABLE IF NOT EXISTS "starred_release" ( constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - session.getDboSession().execute(R"( -CREATE TABLE IF NOT EXISTS "starred_track" ( + session.getDboSession().execute(R"( +CREATE TABLE "starred_track" ( "id" integer primary key autoincrement, "version" integer not null, "scrobbler" integer not null, @@ -395,9 +445,57 @@ CREATE TABLE IF NOT EXISTS "starred_track" ( constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - // new listen system, no longer using tracklists - session.getDboSession().execute(R"( -CREATE TABLE IF NOT EXISTS "listen" ( + // Can't migrate using class mapping as mapping may evolve in the future + + // use time_t to avoid rounding issues later + const Wt::WDateTime now {Wt::WDateTime::fromTime_t(Wt::WDateTime::currentDateTime().toTime_t())}; + + std::map userScrobblers; + auto getScrobbler {[&](IdType::ValueType userId) + { + auto itScrobbler {userScrobblers.find(userId)}; + if (itScrobbler != std::cend(userScrobblers)) + return itScrobbler->second; + + auto query {session.getDboSession().query("SELECT scrobbler FROM user WHERE id = ?").bind(userId)}; + auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())}; + assert(inserted); + return itInserted->second; + }}; + + auto migrateStarEntries = [&session, &getScrobbler, &now](const std::string& colName, const std::string& oldTableName, const std::string& newTableName) + { + using UserIdObjectId = std::tuple; + + std::vector starredEntries; + auto query {session.getDboSession().query("SELECT user_id, " + colName + " from " + oldTableName)}; + auto results {query.resultList()}; + starredEntries.reserve(results.size()); + for (const auto& entry : results) + starredEntries.push_back(entry); + + LMS_LOG(DB, INFO) << "Found " << starredEntries.size() << " " << colName << " to migrate"; + + for (const auto [userId, entryId] : starredEntries) + { + session.getDboSession().execute("INSERT INTO " + newTableName + " ('version', 'scrobbler', 'date_time', '" + colName + "', 'user_id') VALUES (?, ?, ?, ?, ?)") + .bind(0) + .bind(getScrobbler(userId)) + .bind(now.toString().toUTF8()) + .bind(entryId) + .bind(userId); + } + + session.getDboSession().execute("DROP TABLE " + oldTableName); + }; + + migrateStarEntries("artist_id","user_artist_starred", "starred_artist"); + migrateStarEntries("release_id","user_release_starred", "starred_release"); + migrateStarEntries("track_id","user_track_starred", "starred_track"); + + // new listen system, no longer using tracklists + session.getDboSession().execute(R"( +CREATE TABLE "listen" ( "id" integer primary key autoincrement, "version" integer not null, "date_time" text, @@ -408,12 +506,82 @@ CREATE TABLE IF NOT EXISTS "listen" ( constraint "fk_listen_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, constraint "fk_listen_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred ))"); - } - else + } + + void + doDbMigration(Session& session) + { + static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; + + ScopedNoForeignKeys noPragmaKeys {session.getDb()}; + + using MigrationFunction = std::function; + + const std::map migrationFunctions + { + {5, migrateFromV5}, + {6, migrateFromV6}, + {7, migrateFromV7}, + {8, migrateFromV8}, + {9, migrateFromV9}, + {10, migrateFromV10}, + {11, migrateFromV11}, + {12, migrateFromV12}, + {13, migrateFromV13}, + {14, migrateFromV14}, + {15, migrateFromV15}, + {16, migrateFromV16}, + {17, migrateFromV17}, + {18, migrateFromV18}, + {19, migrateFromV19}, + {20, migrateFromV20}, + {21, migrateFromV21}, + {22, migrateFromV22}, + {23, migrateFromV23}, + {24, migrateFromV24}, + {25, migrateFromV25}, + {26, migrateFromV26}, + {27, migrateFromV27}, + {28, migrateFromV28}, + {29, migrateFromV29}, + {30, migrateFromV30}, + {31, migrateFromV31}, + }; + + while (1) + { + auto uniqueTransaction {session.createUniqueTransaction()}; + + Version version; + try { - LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; - throw LmsException { LMS_DATABASE_VERSION > version ? outdatedMsg : "Server binary outdated, please upgrade it to handle this database"}; + version = VersionInfo::getOrCreate(session)->getVersion(); + LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; } + catch (std::exception& e) + { + LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); + throw LmsException {outdatedMsg}; + } + + if (version == LMS_DATABASE_VERSION) + { + LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!"; + return; + } + else if (version > LMS_DATABASE_VERSION) + { + throw LmsException {"Server binary outdated, please upgrade it to handle this database"}; + } + + if (version < migrationFunctions.begin()->first) + throw LmsException {outdatedMsg}; + + LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; + + auto itMigrationFunc {migrationFunctions.find(version)}; + assert(itMigrationFunc != std::cend(migrationFunctions)); + itMigrationFunc->second(session); VersionInfo::get(session).modify()->setVersion(++version); }