diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 8201588c..97ea7126 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -121,6 +121,11 @@ namespace lms::db return ReadTransaction{ _session }; } + void Session::execute(std::string_view statement) + { + _session.execute(std::string{ statement }); + } + void Session::prepareTablesIfNeeded() { LMS_LOG(DB, INFO, "Preparing tables..."); diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index 7983e5d5..cecc7225 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -86,6 +86,8 @@ namespace lms::db #endif } + void execute(std::string_view statement); + // All these methods will acquire transactions void fullAnalyze(); // helper for retrieveEntriesToAnalyze + analyzeEntry void retrieveEntriesToAnalyze(std::vector& entryList); diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index 4ce77ebe..451581c8 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(test-database Common.cpp DatabaseTest.cpp Listen.cpp + Migration.cpp Release.cpp StarredArtist.cpp StarredRelease.cpp diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp new file mode 100644 index 00000000..69a9970c --- /dev/null +++ b/src/libs/database/test/Migration.cpp @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2021 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 "Common.hpp" +#include "core/String.hpp" +#include "database/Db.hpp" + +namespace lms::db::tests +{ + namespace + { + void executeStatements(db::Session& session, std::string_view statements) + { + auto transaction{ session.createWriteTransaction() }; + + for (std::string_view statement : core::stringUtils::splitString(statements, ';')) + { + statement = core::stringUtils::stringTrim(statement, " \t\r\n"); + if (!statement.empty()) + session.execute(statement); + } + } + } + + TEST(Database, migration) + { + TmpDatabase tmpDatabase; + + auto& db{ tmpDatabase.getDb() }; + + // Create tables and indexes as they were in version 33: + const std::string_view schemaV33{ R"( +CREATE TABLE IF NOT EXISTS "cluster_type" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "scan_settings_id" bigint, + constraint "fk_cluster_type_scan_settings" foreign key ("scan_settings_id") references "scan_settings" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "version_info" ( + "id" integer primary key autoincrement, + "version" integer not null, + "db_version" integer not null +); +CREATE TABLE IF NOT EXISTS "scan_settings" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scan_version" integer not null, + "media_directory" text not null, + "start_time" integer, + "update_period" integer not null, + "audio_file_extensions" text not null, + "similarity_engine_type" integer not null +); +CREATE TABLE IF NOT EXISTS "track_features" ( + "id" integer primary key autoincrement, + "version" integer not null, + "data" text not null, + "track_id" bigint, + constraint "fk_track_features_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "tracklist_entry" ( + "id" integer primary key autoincrement, + "version" integer not null, + "track_id" bigint, + "tracklist_id" bigint, date_time TEXT, + constraint "fk_tracklist_entry_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_tracklist_entry_tracklist" foreign key ("tracklist_id") references "tracklist" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "track_artist_link" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "name" integer not null, + "track_id" bigint, + "artist_id" bigint, + constraint "fk_track_artist_link_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "artist" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "sort_name" text not null, + "mbid" text not null +); +CREATE TABLE IF NOT EXISTS "cluster" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "cluster_type_id" bigint, + constraint "fk_cluster_cluster_type" foreign key ("cluster_type_id") references "cluster_type" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "auth_token" ( + "id" integer primary key autoincrement, + "version" integer not null, + "value" text not null, + "expiry" text, + "user_id" bigint, + constraint "fk_auth_token_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "tracklist" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "type" integer not null, + "public" boolean not null, + "user_id" bigint, + constraint "fk_tracklist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +); +CREATE TABLE IF NOT EXISTS "track_cluster" ( + "track_id" bigint, + "cluster_id" bigint, + primary key ("track_id", "cluster_id"), + constraint "fk_track_cluster_key1" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_cluster_key2" foreign key ("cluster_id") references "cluster" ("id") on delete cascade deferrable initially deferred +); +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 +); +CREATE TABLE IF NOT EXISTS "release" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "mbid" text not null +); +CREATE INDEX "track_cluster_track" on "track_cluster" ("track_id"); +CREATE INDEX "track_cluster_cluster" on "track_cluster" ("cluster_id"); +CREATE INDEX artist_name_idx ON artist(name); +CREATE INDEX artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE); +CREATE INDEX artist_mbid_idx ON artist(mbid); +CREATE INDEX auth_token_user_idx ON auth_token(user_id); +CREATE INDEX auth_token_expiry_idx ON auth_token(expiry); +CREATE INDEX auth_token_value_idx ON auth_token(value); +CREATE INDEX cluster_name_idx ON cluster(name); +CREATE INDEX cluster_cluster_type_idx ON cluster(cluster_type_id); +CREATE INDEX cluster_type_name_idx ON cluster_type(name); +CREATE INDEX tracklist_name_idx ON tracklist(name); +CREATE INDEX tracklist_user_idx ON tracklist(user_id); +CREATE INDEX track_features_track_idx ON track_features(track_id); +CREATE INDEX track_artist_link_artist_idx ON track_artist_link(artist_id); +CREATE INDEX track_artist_link_name_idx ON track_artist_link(name); +CREATE INDEX track_artist_link_track_idx ON track_artist_link(track_id); +CREATE INDEX track_artist_link_type_idx ON track_artist_link(type); +CREATE INDEX track_bookmark_track_idx ON track_bookmark(track_id); +CREATE INDEX track_bookmark_user_idx ON track_bookmark(user_id); +CREATE INDEX track_bookmark_user_track_idx ON track_bookmark(user_id,track_id); +CREATE INDEX release_name_idx ON release(name); +CREATE INDEX release_name_nocase_idx ON release(name COLLATE NOCASE); +CREATE INDEX release_mbid_idx ON release(mbid); +CREATE INDEX tracklist_entry_track_idx ON tracklist_entry(track_id); +CREATE TABLE IF NOT EXISTS "user" ( + "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 +, listenbrainz_token TEXT, scrobbler INTEGER NOT NULL DEFAULT(0)); +CREATE TABLE IF NOT EXISTS "track" ( + "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 +); +CREATE INDEX track_file_last_write_idx ON track(file_last_write); +CREATE INDEX track_path_idx ON track(file_path); +CREATE INDEX track_name_idx ON track(name); +CREATE INDEX track_name_nocase_idx ON track(name COLLATE NOCASE); +CREATE INDEX track_mbid_idx ON track(mbid); +CREATE INDEX track_recording_mbid_idx ON track(recording_mbid); +CREATE INDEX track_release_idx ON track(release_id); +CREATE INDEX track_date_idx ON track(date); +CREATE INDEX track_original_date_idx ON track(original_date); +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 +); +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 +); +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 +); +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 +); +CREATE INDEX listen_scrobbler_idx ON listen(scrobbler); +CREATE INDEX listen_user_scrobbler_idx ON listen(user_id,scrobbler); +CREATE INDEX starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler); +CREATE INDEX starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler); +CREATE INDEX starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler);)" }; + + const std::string_view createDummyData{R"( +-- Inserting artists +INSERT INTO artist (version, name, sort_name, mbid) VALUES +(1, 'Artist A', 'Artist A', 'mbid_artist_a'), +(2, 'Artist B', 'Artist B', 'mbid_artist_b'); + +-- Inserting releases +INSERT INTO release (version, name, mbid) VALUES +(1, 'Release X', 'mbid_release_x'), +(2, 'Release Y', 'mbid_release_y'); + +-- Inserting tracks without any associated artists or releases (Orphan Tracks) +INSERT INTO track (version, scan_version, track_number, disc_number, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url) +VALUES +(1, 1, 1, 1, 'Orphan Track 1', 180, '2024-05-08', '2024-05-08', '/path/to/orphan_track_1.mp3', '2024-05-08', '2024-05-08', 1, 'mbid_orphan_track_1', 'mbid_recording_orphan_track_1', 'Copyright 2024', 'http://example.com/copyright'), +(2, 1, 2, 1, 'Orphan Track 2', 210, '2024-05-08', '2024-05-08', '/path/to/orphan_track_2.mp3', '2024-05-08', '2024-05-08', 1, 'mbid_orphan_track_2', 'mbid_recording_orphan_track_2', 'Copyright 2024', 'http://example.com/copyright'); + +-- Inserting tracks with artists but no releases +INSERT INTO track (version, scan_version, track_number, disc_number, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url) +VALUES +(1, 1, 3, 1, 'Track with Artist A', 200, '2024-05-08', '2024-05-08', '/path/to/track_with_artist_a.mp3', '2024-05-08', '2024-05-08', 1, 'mbid_track_with_artist_a', 'mbid_recording_track_with_artist_a', 'Copyright 2024', 'http://example.com/copyright'), +(2, 1, 4, 1, 'Track with Artist B', 220, '2024-05-08', '2024-05-08', '/path/to/track_with_artist_b.mp3', '2024-05-08', '2024-05-08', 1, 'mbid_track_with_artist_b', 'mbid_recording_track_with_artist_b', 'Copyright 2024', 'http://example.com/copyright'); + +-- Inserting tracks with artists linked +INSERT INTO track_artist_link (version, type, name, track_id, artist_id) +VALUES +(1, 1, 'Artist A', 3, 1), +(2, 1, 'Artist B', 4, 2); + +-- Inserting tracks with artists and releases +INSERT INTO track (version, scan_version, track_number, disc_number, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain) +VALUES +(1, 1, 5, 1, 'Track with Artist and Release X', 180, '2024-05-08', '2024-05-08', '/path/to/track_with_artist_and_release_x.mp3', '2024-05-08', '2024-05-08', 1, 'mbid_track_with_artist_and_release_x', 'mbid_recording_track_with_artist_and_release_x', 'Copyright 2024', 'http://example.com/copyright', 1, 1, 10, 0, 0), +(1, 1, 6, 1, 'Track with Artist and Release Y', 210, '2024-05-08', '2024-05-08', '/path/to/track_with_artist_and_release_y.mp3', '2024-05-08', '2024-05-08', 1, 'mbid_track_with_artist_and_release_y', 'mbid_recording_track_with_artist_and_release_y', 'Copyright 2024', 'http://example.com/copyright', 1, 1, 10, 0, 0); + +-- Inserting tracks with artists and releases linked +INSERT INTO track_artist_link (version, type, name, track_id, artist_id) +VALUES +(1, 1, 'Artist A', 5, 1), +(2, 1, 'Artist B', 6, 2);)"}; + + Session session{ db }; + + { + auto transaction{ session.createWriteTransaction() }; + + executeStatements(session, schemaV33); + session.execute("INSERT INTO version_info (version, db_version) VALUES (1, 33)"); + + // Insert some base data + executeStatements(session, createDummyData); + } + + // Now perform full migration + db.getTLSSession().migrateSchemaIfNeeded(); + } +} \ No newline at end of file