From 4851581975f4e2980898bf19bec8300d8a69e353 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 20 Jan 2022 20:57:59 +0100 Subject: [PATCH] Isolated DB migraiton stuff --- src/libs/services/database/CMakeLists.txt | 1 + src/libs/services/database/impl/Migration.cpp | 421 ++++++++++++++++++ src/libs/services/database/impl/Migration.hpp | 55 +++ src/libs/services/database/impl/Session.cpp | 397 +---------------- .../database/include/services/database/Db.hpp | 24 +- .../include/services/database/Session.hpp | 4 +- 6 files changed, 486 insertions(+), 416 deletions(-) create mode 100644 src/libs/services/database/impl/Migration.cpp create mode 100644 src/libs/services/database/impl/Migration.hpp diff --git a/src/libs/services/database/CMakeLists.txt b/src/libs/services/database/CMakeLists.txt index 8e8b9fe3..dc1a11f5 100644 --- a/src/libs/services/database/CMakeLists.txt +++ b/src/libs/services/database/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(lmsdatabase SHARED impl/Cluster.cpp impl/Db.cpp impl/Listen.cpp + impl/Migration.cpp impl/TrackArtistLink.cpp impl/TrackFeatures.cpp impl/TrackList.cpp diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp new file mode 100644 index 00000000..e179bfa0 --- /dev/null +++ b/src/libs/services/database/impl/Migration.cpp @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "Migration.hpp" + +#include "services/database/Db.hpp" +#include "services/database/ScanSettings.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +namespace Database +{ + VersionInfo::pointer + VersionInfo::getOrCreate(Session& session) + { + session.checkUniqueLocked(); + + pointer versionInfo {session.getDboSession().find()}; + if (!versionInfo) + return session.getDboSession().add(std::make_unique()); + + return versionInfo; + } + + VersionInfo::pointer + VersionInfo::get(Session& session) + { + session.checkSharedLocked(); + + return session.getDboSession().find(); + } + +} + +namespace Database::Migration +{ + class ScopedNoForeignKeys + { + public: + ScopedNoForeignKeys(Db& db) : _db {db} + { + _db.executeSql("PRAGMA foreign_keys=OFF"); + } + ~ScopedNoForeignKeys() + { + _db.executeSql("PRAGMA foreign_keys=ON"); + } + + ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete; + ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete; + ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete; + ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete; + + private: + Db& _db; + }; + + void + doDbMigration(Session& session) + { + static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; + + ScopedNoForeignKeys noPragmaKeys {session.getDb()}; + + 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}; + } + + LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; + + 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"( +CREATE TABLE IF NOT EXISTS "track_bookmark" ( + "id" integer primary key autoincrement, + "version" integer not null, + "offset" integer, + "comment" text not null, + "track_id" bigint, + "user_id" bigint, + 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"( +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"); + + // 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"( +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"( +CREATE TABLE "user_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "login_name" text not null, + "password_salt" text not null, + "password_hash" text not null, + "last_login" text, + "subsonic_transcode_enable" boolean not null, + "subsonic_transcode_format" integer not null, + "subsonic_transcode_bitrate" integer not null, + "subsonic_artist_list_mode" integer not null, + "ui_theme" integer not null, + "cur_playing_track_pos" integer not null, + "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"); + + // 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 ''"); + + // 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"( +CREATE TABLE "user_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "login_name" text not null, + "password_salt" text not null, + "password_hash" text not null, + "last_login" text, + "subsonic_transcode_enable" boolean not null, + "subsonic_transcode_format" integer not null, + "subsonic_transcode_bitrate" integer not null, + "subsonic_artist_list_mode" integer not null, + "ui_theme" integer not null, + "cur_playing_track_pos" integer not null, + "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("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(); + } + else if (version == 30) + { + // 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, + "scan_version" integer not null, + "track_number" integer not null, + "disc_number" integer not null, + "name" text not null, + "duration" integer, + "date" integer text, + "original_date" integer text, + "file_path" text not null, + "file_last_write" text, + "file_added" text, + "has_cover" boolean not null, + "mbid" text not null, + "copyright" text not null, + "copyright_url" text not null, + "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"); + + // 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"); + + session.getDboSession().execute(R"( +CREATE TABLE IF NOT EXISTS "starred_artist" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "artist_id" bigint, + "user_id" bigint, + constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred, + 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" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "release_id" bigint, + "user_id" bigint, + constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred, + 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" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "track_id" bigint, + "user_id" bigint, + constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + 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" ( + "id" integer primary key autoincrement, + "version" integer not null, + "date_time" text, + "scrobbler" integer not null, + "scrobbling_state" integer not null, + "track_id" bigint, + "user_id" bigint, + 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 + { + 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"}; + } + + VersionInfo::get(session).modify()->setVersion(++version); + } + } +} diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp new file mode 100644 index 00000000..ce5ae962 --- /dev/null +++ b/src/libs/services/database/impl/Migration.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace Database +{ + class Session; + + using Version = std::size_t; + static constexpr Version LMS_DATABASE_VERSION {32}; + class VersionInfo + { + public: + using pointer = Wt::Dbo::ptr; + + static VersionInfo::pointer getOrCreate(Session& session); + static VersionInfo::pointer get(Session& session); + + Version getVersion() const { return _version; } + void setVersion(Version version) { _version = static_cast(version); } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _version, "db_version"); + } + + private: + int _version {LMS_DATABASE_VERSION}; + }; + + namespace Migration + { + void doDbMigration(Session& session); + } +} diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp index 1fbdb2cb..660e7082 100644 --- a/src/libs/services/database/impl/Session.cpp +++ b/src/libs/services/database/impl/Session.cpp @@ -19,10 +19,7 @@ #include "services/database/Session.hpp" -#include -#include -#include -#include +#include #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -43,394 +40,11 @@ #include "services/database/TrackList.hpp" #include "services/database/TrackFeatures.hpp" #include "services/database/User.hpp" +#include "Migration.hpp" namespace Database { - using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION {32}; - - class VersionInfo - { - public: - using pointer = Wt::Dbo::ptr; - - static VersionInfo::pointer getOrCreate(Session& session) - { - session.checkUniqueLocked(); - - pointer versionInfo {session.getDboSession().find()}; - if (!versionInfo) - return session.getDboSession().add(std::make_unique()); - - return versionInfo; - } - - static VersionInfo::pointer get(Session& session) - { - session.checkSharedLocked(); - - return session.getDboSession().find(); - } - - Version getVersion() const { return _version; } - void setVersion(Version version) { _version = static_cast(version); } - - template - void persist(Action& a) - { - Wt::Dbo::field(a, _version, "db_version"); - } - - private: - int _version {LMS_DATABASE_VERSION}; - }; - -void -Session::doDatabaseMigrationIfNeeded() -{ - static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; - - Db::ScopedNoForeignKeys noPragmaKeys {_db}; - - while (1) - { - auto uniqueTransaction {createUniqueTransaction()}; - - Version version; - try - { - version = VersionInfo::getOrCreate(*this)->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}; - } - - LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; - - if (version == 5) - { - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 7) - { - _session.execute("DROP TABLE similarity_settings"); - _session.execute("DROP TABLE similarity_settings_feature"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 9) - { - _session.execute(R"( -CREATE TABLE IF NOT EXISTS "track_bookmark" ( - "id" integer primary key autoincrement, - "version" integer not null, - "offset" integer, - "comment" text not null, - "track_id" bigint, - "user_id" bigint, - 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(*this).modify()->addAudioFileExtension(".m4b"); - ScanSettings::get(*this).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(*this).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(*this).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(*this).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(*this).modify()->incScanVersion(); - } - else if (version == 15) - { - _session.execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); - } - else if (version == 16) - { - _session.execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 17) - { - // Drop colums total_disc/total_track from release - _session.execute(R"( -CREATE TABLE "release_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "name" text not null, - "mbid" text not null -))"); - _session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); - _session.execute("DROP TABLE release"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 18) - { - _session.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.execute(R"( -CREATE TABLE "user_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "type" integer not null, - "login_name" text not null, - "password_salt" text not null, - "password_hash" text not null, - "last_login" text, - "subsonic_transcode_enable" boolean not null, - "subsonic_transcode_format" integer not null, - "subsonic_transcode_bitrate" integer not null, - "subsonic_artist_list_mode" integer not null, - "ui_theme" integer not null, - "cur_playing_track_pos" integer not null, - "repeat_all" boolean not null, - "radio" boolean not null -))"); - _session.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.execute("DROP TABLE user"); - _session.execute("ALTER TABLE user_backup RENAME TO user"); - } - else if (version == 20) - { - _session.execute("DROP TABLE subsonic_settings"); - } - else if (version == 21) - { - _session.execute("ALTER TABLE track ADD track_replay_gain REAL"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 22) - { - _session.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(*this).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(*this).modify()->incScanVersion(); - } - else if (version == 24) - { - // User's AuthMode - _session.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(*this).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(*this).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(*this).modify()->incScanVersion(); - } - else if (version == 28) - { - // Drop Auth mode - _session.execute(R"( -CREATE TABLE "user_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "type" integer not null, - "login_name" text not null, - "password_salt" text not null, - "password_hash" text not null, - "last_login" text, - "subsonic_transcode_enable" boolean not null, - "subsonic_transcode_format" integer not null, - "subsonic_transcode_bitrate" integer not null, - "subsonic_artist_list_mode" integer not null, - "ui_theme" integer not null, - "cur_playing_track_pos" integer not null, - "repeat_all" boolean not null, - "radio" boolean not null -))"); - _session.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.execute("DROP TABLE user"); - _session.execute("ALTER TABLE user_backup RENAME TO user"); - } - else if (version == 29) - { - _session.execute("ALTER TABLE tracklist_entry ADD date_time TEXT"); - _session.execute("ALTER TABLE user ADD listenbrainz_token TEXT"); - _session.execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); - _session.execute("ALTER TABLE track ADD recording_mbid TEXT"); - - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 30) - { - // drop "year" and "original_year" (rescan needed to convert them into dates) - _session.execute(R"( -CREATE TABLE "track_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "scan_version" integer not null, - "track_number" integer not null, - "disc_number" integer not null, - "name" text not null, - "duration" integer, - "date" integer text, - "original_date" integer text, - "file_path" text not null, - "file_last_write" text, - "file_added" text, - "has_cover" boolean not null, - "mbid" text not null, - "copyright" text not null, - "copyright_url" text not null, - "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.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.execute("DROP TABLE track"); - _session.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(*this).modify()->incScanVersion(); - } - else if (version == 31) - { - // new star system, using dedicated ObjectSets per scrobbler - _session.execute("DROP TABLE user_artist_starred"); - _session.execute("DROP TABLE user_release_starred"); - _session.execute("DROP TABLE user_track_starred"); - - _session.execute(R"( -CREATE TABLE IF NOT EXISTS "starred_artist" ( - "id" integer primary key autoincrement, - "version" integer not null, - "scrobbler" integer not null, - "date_time" text, - "artist_id" bigint, - "user_id" bigint, - constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred, - constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred -))"); - - _session.execute(R"( -CREATE TABLE IF NOT EXISTS "starred_release" ( - "id" integer primary key autoincrement, - "version" integer not null, - "scrobbler" integer not null, - "date_time" text, - "release_id" bigint, - "user_id" bigint, - constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred, - constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred -))"); - - _session.execute(R"( -CREATE TABLE IF NOT EXISTS "starred_track" ( - "id" integer primary key autoincrement, - "version" integer not null, - "scrobbler" integer not null, - "date_time" text, - "track_id" bigint, - "user_id" bigint, - constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, - constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred -))"); - - _session.execute(R"( -CREATE TABLE IF NOT EXISTS "listen" ( - "id" integer primary key autoincrement, - "version" integer not null, - "date_time" text, - "scrobbler" integer not null, - "scrobbling_state" integer not null, - "track_id" bigint, - "user_id" bigint, - 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 - { - 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"}; - } - - VersionInfo::get(*this).modify()->setVersion(++version); - } -} - Session::Session(Db& db) : _db {db} { @@ -496,8 +110,9 @@ void Session::prepareTables() { // Creation case - try { - _session.createTables(); + try + { + _session.createTables(); LMS_LOG(DB, INFO) << "Tables created"; } @@ -506,7 +121,7 @@ Session::prepareTables() LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what(); } - doDatabaseMigrationIfNeeded(); + Migration::doDbMigration(*this); // Indexes { diff --git a/src/libs/services/database/include/services/database/Db.hpp b/src/libs/services/database/include/services/database/Db.hpp index 5effb856..0fea5b24 100644 --- a/src/libs/services/database/include/services/database/Db.hpp +++ b/src/libs/services/database/include/services/database/Db.hpp @@ -42,6 +42,8 @@ class Db Session& getTLSSession(); + void executeSql(const std::string& sql); + private: friend class Session; @@ -66,28 +68,6 @@ class Db std::unique_ptr _connection; }; - class ScopedNoForeignKeys - { - public: - ScopedNoForeignKeys(Db& db) : _db {db} - { - _db.executeSql("PRAGMA foreign_keys=OFF"); - } - ~ScopedNoForeignKeys() - { - _db.executeSql("PRAGMA foreign_keys=ON"); - } - - ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete; - ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete; - ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete; - ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete; - - private: - Db& _db; - }; - - void executeSql(const std::string& sql); RecursiveSharedMutex _sharedMutex; std::unique_ptr _connectionPool; diff --git a/src/libs/services/database/include/services/database/Session.hpp b/src/libs/services/database/include/services/database/Session.hpp index 7871a2e8..1aab2d66 100644 --- a/src/libs/services/database/include/services/database/Session.hpp +++ b/src/libs/services/database/include/services/database/Session.hpp @@ -19,7 +19,6 @@ #pragma once -#include #include #include @@ -72,10 +71,9 @@ namespace Database void prepareTables(); // need to run only once at startup Wt::Dbo::Session& getDboSession() { return _session; } + Db& getDb() { return _db; } private: - void doDatabaseMigrationIfNeeded(); - Db& _db; Wt::Dbo::Session _session; };