2018-02-27 18:06:05 +01:00
|
|
|
/*
|
|
|
|
* Strawberry Music Player
|
|
|
|
* This file was part of Clementine.
|
|
|
|
* Copyright 2012, David Sansome <me@davidsansome.com>
|
2021-03-20 21:14:47 +01:00
|
|
|
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
2018-02-27 18:06:05 +01:00
|
|
|
*
|
|
|
|
* Strawberry 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.
|
|
|
|
*
|
|
|
|
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
2018-08-09 18:39:44 +02:00
|
|
|
*
|
2018-02-27 18:06:05 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
2020-06-14 23:54:18 +02:00
|
|
|
#include <cstddef>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <sqlite3.h>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <boost/scope_exit.hpp>
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QObject>
|
|
|
|
#include <QThread>
|
|
|
|
#include <QMutex>
|
|
|
|
#include <QIODevice>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <QDir>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QFile>
|
|
|
|
#include <QList>
|
|
|
|
#include <QByteArray>
|
|
|
|
#include <QVariant>
|
|
|
|
#include <QString>
|
|
|
|
#include <QStringBuilder>
|
|
|
|
#include <QStringList>
|
2020-07-18 04:05:07 +02:00
|
|
|
#include <QRegularExpression>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QUrl>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <QSqlDriver>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QSqlDatabase>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <QSqlQuery>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QSqlError>
|
|
|
|
#include <QStandardPaths>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <QtDebug>
|
2018-05-01 00:41:33 +02:00
|
|
|
|
|
|
|
#include "core/logging.h"
|
|
|
|
#include "taskmanager.h"
|
|
|
|
#include "database.h"
|
|
|
|
#include "application.h"
|
2021-09-09 21:45:46 +02:00
|
|
|
#include "sqlquery.h"
|
2018-05-01 00:41:33 +02:00
|
|
|
#include "scopedtransaction.h"
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2020-04-13 19:05:55 +02:00
|
|
|
const char *Database::kDatabaseFilename = "strawberry.db";
|
2021-07-11 01:02:53 +02:00
|
|
|
const int Database::kSchemaVersion = 15;
|
2022-07-10 18:57:00 +02:00
|
|
|
const int Database::kMinSupportedSchemaVersion = 10;
|
2018-02-27 18:06:05 +01:00
|
|
|
const char *Database::kMagicAllSongsTables = "%allsongstables";
|
|
|
|
|
|
|
|
int Database::sNextConnectionId = 1;
|
|
|
|
QMutex Database::sNextConnectionIdMutex;
|
|
|
|
|
|
|
|
Database::Database(Application *app, QObject *parent, const QString &database_name) :
|
|
|
|
QObject(parent),
|
|
|
|
app_(app),
|
2020-09-22 18:58:44 +02:00
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
2018-02-27 18:06:05 +01:00
|
|
|
mutex_(QMutex::Recursive),
|
2020-09-22 18:58:44 +02:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
injected_database_name_(database_name),
|
|
|
|
query_hash_(0),
|
2019-07-24 23:29:09 +02:00
|
|
|
startup_schema_version_(-1),
|
|
|
|
original_thread_(nullptr) {
|
|
|
|
|
|
|
|
original_thread_ = thread();
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
{
|
|
|
|
QMutexLocker l(&sNextConnectionIdMutex);
|
|
|
|
connection_id_ = sNextConnectionId++;
|
|
|
|
}
|
|
|
|
|
2018-04-06 22:13:11 +02:00
|
|
|
directory_ = QDir::toNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
Connect();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-07-24 19:16:51 +02:00
|
|
|
Database::~Database() {
|
|
|
|
|
|
|
|
QMutexLocker l(&connect_mutex_);
|
|
|
|
|
2019-07-25 17:56:28 +02:00
|
|
|
for (QString &connection_id : QSqlDatabase::connectionNames()) {
|
|
|
|
qLog(Error) << "Connection" << connection_id << "is still open!";
|
2019-07-24 19:16:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2019-07-22 20:53:05 +02:00
|
|
|
|
2019-07-24 23:29:09 +02:00
|
|
|
void Database::ExitAsync() {
|
2021-09-09 21:53:14 +02:00
|
|
|
QMetaObject::invokeMethod(this, "Exit", Qt::QueuedConnection);
|
2019-07-24 23:29:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void Database::Exit() {
|
|
|
|
|
2021-09-27 19:09:18 +02:00
|
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
2019-07-24 23:29:09 +02:00
|
|
|
Close();
|
|
|
|
moveToThread(original_thread_);
|
|
|
|
emit ExitFinished();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
QSqlDatabase Database::Connect() {
|
|
|
|
|
|
|
|
QMutexLocker l(&connect_mutex_);
|
|
|
|
|
|
|
|
// Create the directory if it doesn't exist
|
|
|
|
if (!QFile::exists(directory_)) {
|
|
|
|
QDir dir;
|
|
|
|
if (!dir.mkpath(directory_)) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString connection_id = QString("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
|
|
|
|
|
|
|
|
// Try to find an existing connection for this thread
|
2019-07-25 17:56:28 +02:00
|
|
|
QSqlDatabase db;
|
|
|
|
if (QSqlDatabase::connectionNames().contains(connection_id)) {
|
|
|
|
db = QSqlDatabase::database(connection_id);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
db = QSqlDatabase::addDatabase("QSQLITE", connection_id);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
if (db.isOpen()) {
|
|
|
|
return db;
|
|
|
|
}
|
2020-11-09 22:49:33 +01:00
|
|
|
db.setConnectOptions("QSQLITE_BUSY_TIMEOUT=30000");
|
2019-07-25 17:56:28 +02:00
|
|
|
//qLog(Debug) << "Opened database with connection id" << connection_id;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2021-08-23 21:21:08 +02:00
|
|
|
if (injected_database_name_.isNull()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
db.setDatabaseName(directory_ + "/" + kDatabaseFilename);
|
2021-08-23 21:21:08 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
db.setDatabaseName(injected_database_name_);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
if (!db.open()) {
|
|
|
|
app_->AddError("Database: " + db.lastError().text());
|
|
|
|
return db;
|
|
|
|
}
|
|
|
|
|
2019-07-30 22:45:22 +02:00
|
|
|
if (db.tables().count() == 0) {
|
|
|
|
// Set up initial schema
|
|
|
|
qLog(Info) << "Creating initial database schema";
|
|
|
|
UpdateDatabaseSchema(0, db);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2022-07-10 18:57:00 +02:00
|
|
|
if (SchemaVersion(&db) < kMinSupportedSchemaVersion) {
|
|
|
|
qFatal("Database schema too old.");
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Attach external databases
|
2021-03-21 04:47:11 +01:00
|
|
|
QStringList keys = attached_databases_.keys();
|
|
|
|
for (const QString &key : keys) {
|
2018-02-27 18:06:05 +01:00
|
|
|
QString filename = attached_databases_[key].filename_;
|
|
|
|
|
|
|
|
if (!injected_database_name_.isNull()) filename = injected_database_name_;
|
|
|
|
|
|
|
|
// Attach the db
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
q.prepare("ATTACH DATABASE :filename AS :alias");
|
2021-09-09 21:45:46 +02:00
|
|
|
q.BindValue(":filename", filename);
|
|
|
|
q.BindValue(":alias", key);
|
|
|
|
if (!q.Exec()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
qFatal("Couldn't attach external database '%s'", key.toLatin1().constData());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (startup_schema_version_ == -1) {
|
|
|
|
UpdateMainSchema(&db);
|
|
|
|
}
|
|
|
|
|
2020-10-17 17:29:09 +02:00
|
|
|
// We might have to initialize the schema in some attached databases now, if they were deleted and don't match up with the main schema version.
|
2021-03-21 04:47:11 +01:00
|
|
|
keys = attached_databases_.keys();
|
|
|
|
for (const QString &key : keys) {
|
2021-08-23 21:21:08 +02:00
|
|
|
if (attached_databases_[key].is_temporary_ && attached_databases_[key].schema_.isEmpty()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
continue;
|
2021-08-23 21:21:08 +02:00
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
// Find out if there are any tables in this database
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
q.prepare(QString("SELECT ROWID FROM %1.sqlite_master WHERE type='table'").arg(key));
|
2021-09-09 21:45:46 +02:00
|
|
|
if (!q.Exec() || !q.next()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
q.finish();
|
|
|
|
ExecSchemaCommandsFromFile(db, attached_databases_[key].schema_, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return db;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-07-24 19:16:51 +02:00
|
|
|
void Database::Close() {
|
|
|
|
|
|
|
|
QMutexLocker l(&connect_mutex_);
|
|
|
|
|
|
|
|
const QString connection_id = QString("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
|
|
|
|
|
|
|
|
// Try to find an existing connection for this thread
|
2019-07-25 17:56:28 +02:00
|
|
|
if (QSqlDatabase::connectionNames().contains(connection_id)) {
|
|
|
|
{
|
|
|
|
QSqlDatabase db = QSqlDatabase::database(connection_id);
|
|
|
|
if (db.isOpen()) {
|
|
|
|
db.close();
|
|
|
|
//qLog(Debug) << "Closed database with connection id" << connection_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
QSqlDatabase::removeDatabase(connection_id);
|
2019-07-24 19:16:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-07-30 22:45:22 +02:00
|
|
|
int Database::SchemaVersion(QSqlDatabase *db) {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
// Get the database's schema version
|
|
|
|
int schema_version = 0;
|
|
|
|
{
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(*db);
|
|
|
|
q.prepare("SELECT version FROM schema_version");
|
|
|
|
if (q.Exec() && q.next()) {
|
|
|
|
schema_version = q.value(0).toInt();
|
|
|
|
}
|
|
|
|
// Implicit invocation of ~SqlQuery() when leaving the scope to release any remaining database locks!
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
2019-07-30 22:45:22 +02:00
|
|
|
return schema_version;
|
|
|
|
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2019-07-30 22:45:22 +02:00
|
|
|
void Database::UpdateMainSchema(QSqlDatabase *db) {
|
|
|
|
|
|
|
|
int schema_version = SchemaVersion(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
startup_schema_version_ = schema_version;
|
|
|
|
|
|
|
|
if (schema_version > kSchemaVersion) {
|
|
|
|
qLog(Warning) << "The database schema (version" << schema_version << ") is newer than I was expecting";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (schema_version < kSchemaVersion) {
|
|
|
|
// Update the schema
|
|
|
|
for (int v = schema_version + 1; v <= kSchemaVersion; ++v) {
|
|
|
|
UpdateDatabaseSchema(v, *db);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::RecreateAttachedDb(const QString &database_name) {
|
|
|
|
|
|
|
|
if (!attached_databases_.contains(database_name)) {
|
|
|
|
qLog(Warning) << "Attached database does not exist:" << database_name;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString filename = attached_databases_[database_name].filename_;
|
|
|
|
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
{
|
|
|
|
QSqlDatabase db(Connect());
|
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
q.prepare("DETACH DATABASE :alias");
|
2021-09-09 21:45:46 +02:00
|
|
|
q.BindValue(":alias", database_name);
|
|
|
|
if (!q.Exec()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
qLog(Warning) << "Failed to detach database" << database_name;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!QFile::remove(filename)) {
|
|
|
|
qLog(Warning) << "Failed to remove file" << filename;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
// We can't just re-attach the database now because it needs to be done for each thread.
|
|
|
|
// Close all the database connections, so each thread will re-attach it when they next connect.
|
2018-02-27 18:06:05 +01:00
|
|
|
for (const QString &name : QSqlDatabase::connectionNames()) {
|
|
|
|
QSqlDatabase::removeDatabase(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::AttachDatabase(const QString &database_name, const AttachedDatabase &database) {
|
|
|
|
attached_databases_[database_name] = database;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::AttachDatabaseOnDbConnection(const QString &database_name, const AttachedDatabase &database, QSqlDatabase &db) {
|
|
|
|
|
|
|
|
AttachDatabase(database_name, database);
|
|
|
|
|
|
|
|
// Attach the db
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
q.prepare("ATTACH DATABASE :filename AS :alias");
|
2021-09-09 21:45:46 +02:00
|
|
|
q.BindValue(":filename", database.filename_);
|
|
|
|
q.BindValue(":alias", database_name);
|
|
|
|
if (!q.Exec()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
qFatal("Couldn't attach external database '%s'", database_name.toLatin1().constData());
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::DetachDatabase(const QString &database_name) {
|
|
|
|
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
{
|
|
|
|
QSqlDatabase db(Connect());
|
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
q.prepare("DETACH DATABASE :alias");
|
2021-09-09 21:45:46 +02:00
|
|
|
q.BindValue(":alias", database_name);
|
|
|
|
if (!q.Exec()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
qLog(Warning) << "Failed to detach database" << database_name;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attached_databases_.remove(database_name);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
|
2018-08-15 01:28:37 +02:00
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
QString filename;
|
2021-08-23 21:21:08 +02:00
|
|
|
if (version == 0) {
|
|
|
|
filename = ":/schema/schema.sql";
|
|
|
|
}
|
2018-08-15 01:28:37 +02:00
|
|
|
else {
|
|
|
|
filename = QString(":/schema/schema-%1.sql").arg(version);
|
|
|
|
qLog(Debug) << "Applying database schema update" << version << "from" << filename;
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
ExecSchemaCommandsFromFile(db, filename, version - 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::UrlEncodeFilenameColumn(const QString &table, QSqlDatabase &db) {
|
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery select(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
select.prepare(QString("SELECT ROWID, filename FROM %1").arg(table));
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery update(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
update.prepare(QString("UPDATE %1 SET filename=:filename WHERE ROWID=:id").arg(table));
|
2021-09-09 21:45:46 +02:00
|
|
|
if (!select.Exec()) {
|
|
|
|
ReportErrors(select);
|
|
|
|
}
|
2018-10-02 00:38:52 +02:00
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
while (select.next()) {
|
|
|
|
const int rowid = select.value(0).toInt();
|
|
|
|
const QString filename = select.value(1).toString();
|
|
|
|
|
|
|
|
if (filename.isEmpty() || filename.contains("://")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QUrl url = QUrl::fromLocalFile(filename);
|
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
update.BindValue(":filename", url.toEncoded());
|
|
|
|
update.BindValue(":id", rowid);
|
|
|
|
if (!update.Exec()) {
|
|
|
|
ReportErrors(update);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::ExecSchemaCommandsFromFile(QSqlDatabase &db, const QString &filename, int schema_version, bool in_transaction) {
|
2018-08-15 01:28:37 +02:00
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
// Open and read the database schema
|
|
|
|
QFile schema_file(filename);
|
2021-07-14 20:52:57 +02:00
|
|
|
if (!schema_file.open(QIODevice::ReadOnly)) {
|
2021-08-09 23:32:26 +02:00
|
|
|
qFatal("Couldn't open schema file %s for reading: %s", filename.toUtf8().constData(), schema_file.errorString().toUtf8().constData());
|
2021-07-14 20:52:57 +02:00
|
|
|
return;
|
|
|
|
}
|
2022-03-17 00:04:12 +01:00
|
|
|
QByteArray data = schema_file.readAll();
|
|
|
|
QString schema = QString::fromUtf8(data);
|
|
|
|
if (schema.contains("\r\n")) {
|
|
|
|
schema = schema.replace("\r\n", "\n");
|
|
|
|
}
|
2021-07-14 20:52:57 +02:00
|
|
|
schema_file.close();
|
2022-03-17 00:04:12 +01:00
|
|
|
ExecSchemaCommands(db, schema, schema_version, in_transaction);
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::ExecSchemaCommands(QSqlDatabase &db, const QString &schema, int schema_version, bool in_transaction) {
|
|
|
|
|
|
|
|
// Run each command
|
2022-03-17 00:04:12 +01:00
|
|
|
QStringList commands;
|
|
|
|
commands = schema.split(QRegularExpression("; *\n\n"));
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
// We don't want this list to reflect possible DB schema changes so we initialize it before executing any statements.
|
|
|
|
// If no outer transaction is provided the song tables need to be queried before beginning an inner transaction! Otherwise
|
|
|
|
// DROP TABLE commands on song tables may fail due to database locks.
|
2018-02-27 18:06:05 +01:00
|
|
|
const QStringList song_tables(SongsTables(db, schema_version));
|
|
|
|
|
|
|
|
if (!in_transaction) {
|
|
|
|
ScopedTransaction inner_transaction(&db);
|
|
|
|
ExecSongTablesCommands(db, song_tables, commands);
|
|
|
|
inner_transaction.Commit();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ExecSongTablesCommands(db, song_tables, commands);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_tables, const QStringList &commands) {
|
2018-08-29 21:42:24 +02:00
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
for (const QString &command : commands) {
|
2018-05-10 15:29:28 +02:00
|
|
|
// There are now lots of "songs" tables that need to have the same schema: songs and device_*_songs.
|
|
|
|
// We allow a magic value in the schema files to update all songs tables at once.
|
2018-02-27 18:06:05 +01:00
|
|
|
if (command.contains(kMagicAllSongsTables)) {
|
|
|
|
for (const QString &table : song_tables) {
|
2018-05-01 00:41:33 +02:00
|
|
|
// Another horrible hack: device songs tables don't have matching _fts tables, so if this command tries to touch one, ignore it.
|
2018-08-29 21:42:24 +02:00
|
|
|
if (table.startsWith("device_") && command.contains(QString(kMagicAllSongsTables) + "_fts")) {
|
2018-02-27 18:06:05 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
|
|
|
|
QString new_command(command);
|
|
|
|
new_command.replace(kMagicAllSongsTables, table);
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery query(db);
|
|
|
|
query.prepare(new_command);
|
|
|
|
if (!query.Exec()) {
|
|
|
|
ReportErrors(query);
|
2018-02-27 18:06:05 +01:00
|
|
|
qFatal("Unable to update music collection database");
|
2021-09-09 21:45:46 +02:00
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
2018-08-29 21:42:24 +02:00
|
|
|
}
|
|
|
|
else {
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery query(db);
|
|
|
|
query.prepare(command);
|
|
|
|
if (!query.Exec()) {
|
|
|
|
ReportErrors(query);
|
|
|
|
qFatal("Unable to update music collection database");
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
}
|
2019-07-30 22:45:22 +02:00
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
QStringList Database::SongsTables(QSqlDatabase &db, const int schema_version) {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2019-09-15 20:27:32 +02:00
|
|
|
Q_UNUSED(schema_version);
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
QStringList ret;
|
|
|
|
|
|
|
|
// look for the tables in the main db
|
|
|
|
for (const QString &table : db.tables()) {
|
|
|
|
if (table == "songs" || table.endsWith("_songs")) ret << table;
|
|
|
|
}
|
|
|
|
|
|
|
|
// look for the tables in attached dbs
|
2021-03-21 04:47:11 +01:00
|
|
|
QStringList keys = attached_databases_.keys();
|
|
|
|
for (const QString &key : keys) {
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
2018-02-27 18:06:05 +01:00
|
|
|
q.prepare(QString("SELECT NAME FROM %1.sqlite_master WHERE type='table' AND name='songs' OR name LIKE '%songs'").arg(key));
|
2021-09-09 21:45:46 +02:00
|
|
|
if (q.Exec()) {
|
2018-02-27 18:06:05 +01:00
|
|
|
while (q.next()) {
|
|
|
|
QString tab_name = key + "." + q.value(0).toString();
|
|
|
|
ret << tab_name;
|
|
|
|
}
|
|
|
|
}
|
2021-09-09 21:45:46 +02:00
|
|
|
else {
|
|
|
|
ReportErrors(q);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ret << "playlist_items";
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
void Database::ReportErrors(const SqlQuery &query) {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2021-09-09 21:45:46 +02:00
|
|
|
const QSqlError sql_error = query.lastError();
|
|
|
|
if (sql_error.isValid()) {
|
|
|
|
qLog(Error) << "Unable to execute SQL query: " << sql_error;
|
|
|
|
qLog(Error) << "Failed query: " << query.LastQuery();
|
|
|
|
QString error;
|
|
|
|
error += "Unable to execute SQL query: " + sql_error.text() + "<br />";
|
|
|
|
error += "Failed query: " + query.LastQuery();
|
|
|
|
emit Error(error);
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-20 19:04:08 +02:00
|
|
|
bool Database::IntegrityCheck(const QSqlDatabase &db) {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
qLog(Debug) << "Starting database integrity check";
|
2021-09-09 21:45:46 +02:00
|
|
|
const int task_id = app_->task_manager()->StartTask(tr("Integrity check"));
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
bool ok = false;
|
|
|
|
bool error_reported = false;
|
|
|
|
// Ask for 10 error messages at most.
|
2021-09-09 21:45:46 +02:00
|
|
|
SqlQuery q(db);
|
|
|
|
q.prepare("PRAGMA integrity_check(10)");
|
|
|
|
if (q.Exec()) {
|
|
|
|
while (q.next()) {
|
|
|
|
QString message = q.value(0).toString();
|
|
|
|
|
|
|
|
// If no errors are found, a single row with the value "ok" is returned
|
|
|
|
if (message == "ok") {
|
|
|
|
ok = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (!error_reported) { app_->AddError(tr("Database corruption detected.")); }
|
|
|
|
app_->AddError("Database: " + message);
|
|
|
|
error_reported = true;
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
}
|
2021-09-09 21:45:46 +02:00
|
|
|
else {
|
|
|
|
ReportErrors(q);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
app_->task_manager()->SetTaskFinished(task_id);
|
|
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::DoBackup() {
|
|
|
|
|
2021-09-13 20:49:33 +02:00
|
|
|
QSqlDatabase db(Connect());
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2020-11-09 23:10:43 +01:00
|
|
|
if (!db.isOpen()) return;
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
// Before we overwrite anything, make sure the database is not corrupt
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
|
2020-11-09 23:10:43 +01:00
|
|
|
const bool ok = IntegrityCheck(db);
|
|
|
|
if (ok && SchemaVersion(&db) == kSchemaVersion) {
|
2018-02-27 18:06:05 +01:00
|
|
|
BackupFile(db.databaseName());
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-22 13:41:38 +02:00
|
|
|
bool Database::OpenDatabase(const QString &filename, sqlite3 **connection) {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
int ret = sqlite3_open(filename.toUtf8(), connection);
|
|
|
|
if (ret != 0) {
|
|
|
|
if (*connection) {
|
|
|
|
const char *error_message = sqlite3_errmsg(*connection);
|
|
|
|
qLog(Error) << "Failed to open database for backup:" << filename << error_message;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
qLog(Error) << "Failed to open database for backup:" << filename;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::BackupFile(const QString &filename) {
|
|
|
|
|
|
|
|
qLog(Debug) << "Starting database backup";
|
|
|
|
QString dest_filename = QString("%1.bak").arg(filename);
|
|
|
|
const int task_id = app_->task_manager()->StartTask(tr("Backing up database"));
|
|
|
|
|
|
|
|
sqlite3 *source_connection = nullptr;
|
|
|
|
sqlite3 *dest_connection = nullptr;
|
|
|
|
|
2021-06-20 19:04:08 +02:00
|
|
|
BOOST_SCOPE_EXIT((&source_connection)(&dest_connection)(task_id)(app_)) { // clazy:exclude=rule-of-three NOLINT(google-explicit-constructor)
|
2018-02-27 18:06:05 +01:00
|
|
|
// Harmless to call sqlite3_close() with a nullptr pointer.
|
|
|
|
sqlite3_close(source_connection);
|
|
|
|
sqlite3_close(dest_connection);
|
|
|
|
app_->task_manager()->SetTaskFinished(task_id);
|
|
|
|
}
|
|
|
|
BOOST_SCOPE_EXIT_END
|
|
|
|
|
|
|
|
bool success = OpenDatabase(filename, &source_connection);
|
|
|
|
if (!success) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
success = OpenDatabase(dest_filename, &dest_connection);
|
|
|
|
if (!success) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sqlite3_backup *backup = sqlite3_backup_init(dest_connection, "main", source_connection, "main");
|
|
|
|
if (!backup) {
|
|
|
|
const char *error_message = sqlite3_errmsg(dest_connection);
|
|
|
|
qLog(Error) << "Failed to start database backup:" << error_message;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ret = SQLITE_OK;
|
|
|
|
do {
|
|
|
|
ret = sqlite3_backup_step(backup, 16);
|
|
|
|
const int page_count = sqlite3_backup_pagecount(backup);
|
2019-04-12 19:55:19 +02:00
|
|
|
app_->task_manager()->SetTaskProgress(task_id, page_count - sqlite3_backup_remaining(backup), page_count);
|
|
|
|
}
|
|
|
|
while (ret == SQLITE_OK);
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
if (ret != SQLITE_DONE) {
|
|
|
|
qLog(Error) << "Database backup failed";
|
|
|
|
}
|
|
|
|
|
|
|
|
sqlite3_backup_finish(backup);
|
|
|
|
|
|
|
|
}
|