/* * Copyright (C) 2013 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 "database/Session.hpp" #include #include #include #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Db.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Track.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackArtistLink.hpp" #include "database/TrackList.hpp" #include "database/TrackFeatures.hpp" #include "database/User.hpp" namespace Database { #define LMS_DATABASE_VERSION 16 using Version = std::size_t; 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() { auto uniqueTransaction {createUniqueTransaction()}; static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; 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) return; } catch (std::exception& e) { LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); throw LmsException {outdatedMsg}; } while (version < LMS_DATABASE_VERSION) { 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 { 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::get(*this).modify()->setVersion(LMS_DATABASE_VERSION); } } Session::Session(Db& db) : _db {db} { _session.setConnectionPool(_db.getConnectionPool()); _session.mapClass("version_info"); _session.mapClass("artist"); _session.mapClass("auth_token"); _session.mapClass("cluster"); _session.mapClass("cluster_type"); _session.mapClass("release"); _session.mapClass("scan_settings"); _session.mapClass("track"); _session.mapClass("track_bookmark"); _session.mapClass("track_artist_link"); _session.mapClass("track_features"); _session.mapClass("tracklist"); _session.mapClass("tracklist_entry"); _session.mapClass("user"); } enum class OwnedLock { None, Shared, Unique, }; static thread_local std::map lockDebug; UniqueTransaction::UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session) : _lock {mutex}, _transaction {session} { assert(lockDebug[_lock.mutex()] == OwnedLock::None); lockDebug[_lock.mutex()] = OwnedLock::Unique; } UniqueTransaction::~UniqueTransaction() { assert(lockDebug[_lock.mutex()] == OwnedLock::Unique); lockDebug[_lock.mutex()] = OwnedLock::None; } SharedTransaction::SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session) : _lock {mutex}, _transaction {session} { assert(lockDebug[_lock.mutex()] == OwnedLock::None); lockDebug[_lock.mutex()] = OwnedLock::Shared; } SharedTransaction::~SharedTransaction() { assert(lockDebug[_lock.mutex()] == OwnedLock::Shared); lockDebug[_lock.mutex()] = OwnedLock::None; } void Session::checkUniqueLocked() { assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique); } void Session::checkSharedLocked() { assert(lockDebug[&_db.getMutex()] != OwnedLock::None); } UniqueTransaction Session::createUniqueTransaction() { return UniqueTransaction{_db.getMutex(), _session}; } SharedTransaction Session::createSharedTransaction() { return SharedTransaction{_db.getMutex(), _session}; } void Session::prepareTables() { // Creation case try { _session.createTables(); LMS_LOG(DB, INFO) << "Tables created"; } catch (Wt::Dbo::Exception& e) { LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what(); } doDatabaseMigrationIfNeeded(); // Indexes { auto uniqueTransaction {createUniqueTransaction()}; _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); } // Initial settings tables { auto uniqueTransaction {createUniqueTransaction()}; ScanSettings::init(*this); } } void Session::optimize() { LMS_LOG(DB, DEBUG) << "Optimizing db..."; { auto uniqueTransaction {createUniqueTransaction()}; _session.execute("ANALYZE"); } LMS_LOG(DB, DEBUG) << "Optimized db!"; } } // namespace Database