2010-05-09 02:10:26 +02:00
|
|
|
/* This file is part of Clementine.
|
2010-11-20 14:27:10 +01:00
|
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
2010-05-09 02:10:26 +02:00
|
|
|
|
|
|
|
Clementine 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.
|
|
|
|
|
|
|
|
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2010-06-30 21:55:46 +02:00
|
|
|
#include "config.h"
|
2010-05-09 02:10:26 +02:00
|
|
|
#include "database.h"
|
2010-07-04 01:00:07 +02:00
|
|
|
#include "scopedtransaction.h"
|
2010-12-09 13:34:08 +01:00
|
|
|
#include "utilities.h"
|
2012-02-12 14:41:50 +01:00
|
|
|
#include "core/application.h"
|
2011-04-22 18:50:29 +02:00
|
|
|
#include "core/logging.h"
|
2012-03-13 15:32:44 +01:00
|
|
|
#include "core/taskmanager.h"
|
2010-05-09 02:10:26 +02:00
|
|
|
|
2012-03-16 14:50:20 +01:00
|
|
|
#include <boost/scope_exit.hpp>
|
|
|
|
|
2013-11-22 17:05:55 +01:00
|
|
|
#include <sqlite3.h>
|
|
|
|
|
2010-06-20 22:59:54 +02:00
|
|
|
#include <QCoreApplication>
|
|
|
|
#include <QDir>
|
2010-05-09 02:10:26 +02:00
|
|
|
#include <QLibrary>
|
|
|
|
#include <QLibraryInfo>
|
|
|
|
#include <QSqlDriver>
|
|
|
|
#include <QSqlQuery>
|
2010-06-20 22:59:54 +02:00
|
|
|
#include <QtDebug>
|
|
|
|
#include <QThread>
|
2011-06-14 16:46:02 +02:00
|
|
|
#include <QUrl>
|
2010-05-09 02:10:26 +02:00
|
|
|
#include <QVariant>
|
|
|
|
|
|
|
|
const char* Database::kDatabaseFilename = "clementine.db";
|
2013-05-11 23:17:54 +02:00
|
|
|
const int Database::kSchemaVersion = 46;
|
2010-10-17 19:50:20 +02:00
|
|
|
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
2010-05-09 02:10:26 +02:00
|
|
|
|
2010-06-26 14:41:18 +02:00
|
|
|
int Database::sNextConnectionId = 1;
|
|
|
|
QMutex Database::sNextConnectionIdMutex;
|
|
|
|
|
2010-06-20 22:51:57 +02:00
|
|
|
Database::Token::Token(const QString& token, int start, int end)
|
2014-02-07 16:34:20 +01:00
|
|
|
: token(token), start_offset(start), end_offset(end) {}
|
2010-06-20 22:51:57 +02:00
|
|
|
|
2010-06-20 22:07:32 +02:00
|
|
|
struct sqlite3_tokenizer_module {
|
|
|
|
int iVersion;
|
2014-02-07 16:34:20 +01:00
|
|
|
int (*xCreate)(int argc, /* Size of argv array */
|
|
|
|
const char* const* argv, /* Tokenizer argument strings */
|
|
|
|
sqlite3_tokenizer** ppTokenizer); /* OUT: Created tokenizer */
|
2010-06-20 22:07:32 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
int (*xDestroy)(sqlite3_tokenizer* pTokenizer);
|
2010-06-20 22:07:32 +02:00
|
|
|
|
|
|
|
int (*xOpen)(
|
2014-02-07 16:34:20 +01:00
|
|
|
sqlite3_tokenizer* pTokenizer, /* Tokenizer object */
|
|
|
|
const char* pInput, int nBytes, /* Input buffer */
|
|
|
|
sqlite3_tokenizer_cursor** ppCursor); /* OUT: Created tokenizer cursor */
|
2010-06-20 22:07:32 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
int (*xClose)(sqlite3_tokenizer_cursor* pCursor);
|
2010-06-20 22:07:32 +02:00
|
|
|
|
|
|
|
int (*xNext)(
|
2014-02-07 16:34:20 +01:00
|
|
|
sqlite3_tokenizer_cursor* pCursor, /* Tokenizer cursor */
|
|
|
|
const char** ppToken, int* pnBytes, /* OUT: Normalized text for token */
|
|
|
|
int* piStartOffset, /* OUT: Byte offset of token in input buffer */
|
|
|
|
int* piEndOffset, /* OUT: Byte offset of end of token in input buffer */
|
|
|
|
int* piPosition); /* OUT: Number of tokens returned before this one */
|
2010-06-20 22:07:32 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
struct sqlite3_tokenizer {
|
2014-02-07 16:34:20 +01:00
|
|
|
const sqlite3_tokenizer_module* pModule; /* The module for this tokenizer */
|
2010-06-20 22:07:32 +02:00
|
|
|
/* Tokenizer implementations will typically add additional fields */
|
|
|
|
};
|
|
|
|
|
|
|
|
struct sqlite3_tokenizer_cursor {
|
2014-02-07 16:34:20 +01:00
|
|
|
sqlite3_tokenizer* pTokenizer; /* Tokenizer for this cursor. */
|
2010-06-20 22:07:32 +02:00
|
|
|
/* Tokenizer implementations will typically add additional fields */
|
|
|
|
};
|
|
|
|
|
2014-02-06 16:49:49 +01:00
|
|
|
sqlite3_tokenizer_module* Database::sFTSTokenizer = nullptr;
|
2010-06-20 21:46:51 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
int Database::FTSCreate(int argc, const char* const* argv,
|
|
|
|
sqlite3_tokenizer** tokenizer) {
|
2010-06-20 21:46:51 +02:00
|
|
|
*tokenizer = reinterpret_cast<sqlite3_tokenizer*>(new UnicodeTokenizer);
|
|
|
|
|
|
|
|
return SQLITE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
int Database::FTSDestroy(sqlite3_tokenizer* tokenizer) {
|
2014-02-07 16:34:20 +01:00
|
|
|
UnicodeTokenizer* real_tokenizer =
|
|
|
|
reinterpret_cast<UnicodeTokenizer*>(tokenizer);
|
2010-06-20 21:46:51 +02:00
|
|
|
delete real_tokenizer;
|
|
|
|
return SQLITE_OK;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
int Database::FTSOpen(sqlite3_tokenizer* pTokenizer, const char* input,
|
|
|
|
int bytes, sqlite3_tokenizer_cursor** cursor) {
|
2010-06-20 21:46:51 +02:00
|
|
|
UnicodeTokenizerCursor* new_cursor = new UnicodeTokenizerCursor;
|
|
|
|
new_cursor->pTokenizer = pTokenizer;
|
|
|
|
new_cursor->position = 0;
|
|
|
|
|
|
|
|
QString str = QString::fromUtf8(input, bytes).toLower();
|
|
|
|
QChar* data = str.data();
|
|
|
|
// Decompose and strip punctuation.
|
|
|
|
QList<Token> tokens;
|
|
|
|
QString token;
|
|
|
|
int start_offset = 0;
|
|
|
|
int offset = 0;
|
|
|
|
for (int i = 0; i < str.length(); ++i) {
|
|
|
|
QChar c = data[i];
|
|
|
|
ushort unicode = c.unicode();
|
2010-06-21 14:07:56 +02:00
|
|
|
if (unicode <= 0x007f) {
|
2010-06-20 21:46:51 +02:00
|
|
|
offset += 1;
|
|
|
|
} else if (unicode >= 0x0080 && unicode <= 0x07ff) {
|
|
|
|
offset += 2;
|
2010-06-21 14:07:56 +02:00
|
|
|
} else if (unicode >= 0x0800) {
|
2010-06-20 21:46:51 +02:00
|
|
|
offset += 3;
|
|
|
|
}
|
2010-06-21 14:07:56 +02:00
|
|
|
// Unicode astral planes unsupported in Qt?
|
|
|
|
/*else if (unicode >= 0x010000 && unicode <= 0x10ffff) {
|
|
|
|
offset += 4;
|
|
|
|
}*/
|
2010-06-20 21:46:51 +02:00
|
|
|
|
|
|
|
if (!data[i].isLetterOrNumber()) {
|
|
|
|
// Token finished.
|
|
|
|
if (token.length() != 0) {
|
2010-06-20 22:51:57 +02:00
|
|
|
tokens << Token(token, start_offset, offset - 1);
|
2010-06-20 21:46:51 +02:00
|
|
|
start_offset = offset;
|
|
|
|
token.clear();
|
|
|
|
} else {
|
|
|
|
++start_offset;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (data[i].decompositionTag() != QChar::NoDecomposition) {
|
|
|
|
token.push_back(data[i].decomposition()[0]);
|
|
|
|
} else {
|
|
|
|
token.push_back(data[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i == str.length() - 1) {
|
|
|
|
if (token.length() != 0) {
|
2010-06-20 22:51:57 +02:00
|
|
|
tokens << Token(token, start_offset, offset);
|
2010-06-20 21:46:51 +02:00
|
|
|
token.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
new_cursor->tokens = tokens;
|
|
|
|
*cursor = reinterpret_cast<sqlite3_tokenizer_cursor*>(new_cursor);
|
|
|
|
|
|
|
|
return SQLITE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
int Database::FTSClose(sqlite3_tokenizer_cursor* cursor) {
|
2014-02-07 16:34:20 +01:00
|
|
|
UnicodeTokenizerCursor* real_cursor =
|
|
|
|
reinterpret_cast<UnicodeTokenizerCursor*>(cursor);
|
2010-06-20 21:46:51 +02:00
|
|
|
delete real_cursor;
|
|
|
|
|
|
|
|
return SQLITE_OK;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
int Database::FTSNext(sqlite3_tokenizer_cursor* cursor, const char** token,
|
|
|
|
int* bytes, int* start_offset, int* end_offset,
|
|
|
|
int* position) {
|
|
|
|
UnicodeTokenizerCursor* real_cursor =
|
|
|
|
reinterpret_cast<UnicodeTokenizerCursor*>(cursor);
|
2010-06-20 21:46:51 +02:00
|
|
|
|
|
|
|
QList<Token> tokens = real_cursor->tokens;
|
|
|
|
if (real_cursor->position >= tokens.size()) {
|
|
|
|
return SQLITE_DONE;
|
|
|
|
}
|
|
|
|
|
|
|
|
Token t = tokens[real_cursor->position];
|
|
|
|
QByteArray utf8 = t.token.toUtf8();
|
|
|
|
*token = utf8.constData();
|
|
|
|
*bytes = utf8.size();
|
|
|
|
*start_offset = t.start_offset;
|
|
|
|
*end_offset = t.end_offset;
|
|
|
|
*position = real_cursor->position++;
|
|
|
|
|
|
|
|
real_cursor->current_utf8 = utf8;
|
|
|
|
|
|
|
|
return SQLITE_OK;
|
|
|
|
}
|
|
|
|
|
2010-05-09 02:10:26 +02:00
|
|
|
void Database::StaticInit() {
|
2010-06-21 11:32:57 +02:00
|
|
|
sFTSTokenizer = new sqlite3_tokenizer_module;
|
|
|
|
sFTSTokenizer->iVersion = 0;
|
|
|
|
sFTSTokenizer->xCreate = &Database::FTSCreate;
|
|
|
|
sFTSTokenizer->xDestroy = &Database::FTSDestroy;
|
|
|
|
sFTSTokenizer->xOpen = &Database::FTSOpen;
|
|
|
|
sFTSTokenizer->xNext = &Database::FTSNext;
|
|
|
|
sFTSTokenizer->xClose = &Database::FTSClose;
|
2010-05-09 02:10:26 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
Database::Database(Application* app, QObject* parent,
|
|
|
|
const QString& database_name)
|
|
|
|
: QObject(parent),
|
|
|
|
app_(app),
|
|
|
|
mutex_(QMutex::Recursive),
|
|
|
|
injected_database_name_(database_name),
|
|
|
|
query_hash_(0),
|
|
|
|
startup_schema_version_(-1) {
|
2010-06-26 14:41:18 +02:00
|
|
|
{
|
|
|
|
QMutexLocker l(&sNextConnectionIdMutex);
|
2014-01-29 17:30:58 +01:00
|
|
|
connection_id_ = sNextConnectionId++;
|
2010-06-26 14:41:18 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
directory_ =
|
|
|
|
QDir::toNativeSeparators(Utilities::GetConfigPath(Utilities::Path_Root));
|
2010-05-09 02:10:26 +02:00
|
|
|
|
2010-11-27 17:14:09 +01:00
|
|
|
attached_databases_["jamendo"] = AttachedDatabase(
|
2014-02-07 16:34:20 +01:00
|
|
|
directory_ + "/jamendo.db", ":/schema/jamendo.sql", false);
|
2010-11-27 17:14:09 +01:00
|
|
|
|
2010-06-02 18:22:20 +02:00
|
|
|
QMutexLocker l(&mutex_);
|
2010-05-09 02:10:26 +02:00
|
|
|
Connect();
|
|
|
|
}
|
|
|
|
|
|
|
|
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_)) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
const QString connection_id = QString("%1_thread_%2").arg(connection_id_).arg(
|
|
|
|
reinterpret_cast<quint64>(QThread::currentThread()));
|
2010-05-09 02:10:26 +02:00
|
|
|
|
|
|
|
// Try to find an existing connection for this thread
|
|
|
|
QSqlDatabase db = QSqlDatabase::database(connection_id);
|
|
|
|
if (db.isOpen()) {
|
|
|
|
return db;
|
|
|
|
}
|
|
|
|
|
|
|
|
db = QSqlDatabase::addDatabase("QSQLITE", connection_id);
|
|
|
|
|
|
|
|
if (!injected_database_name_.isNull())
|
|
|
|
db.setDatabaseName(injected_database_name_);
|
|
|
|
else
|
|
|
|
db.setDatabaseName(directory_ + "/" + kDatabaseFilename);
|
|
|
|
|
|
|
|
if (!db.open()) {
|
2012-02-12 14:41:50 +01:00
|
|
|
app_->AddError("Database: " + db.lastError().text());
|
2010-05-09 02:10:26 +02:00
|
|
|
return db;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find Sqlite3 functions in the Qt plugin.
|
|
|
|
StaticInit();
|
|
|
|
|
2013-03-03 13:00:24 +01:00
|
|
|
{
|
|
|
|
QSqlQuery set_fts_tokenizer("SELECT fts3_tokenizer(:name, :pointer)", db);
|
|
|
|
set_fts_tokenizer.bindValue(":name", "unicode");
|
2014-02-07 16:34:20 +01:00
|
|
|
set_fts_tokenizer.bindValue(
|
|
|
|
":pointer", QByteArray(reinterpret_cast<const char*>(&sFTSTokenizer),
|
|
|
|
sizeof(&sFTSTokenizer)));
|
2013-03-03 13:00:24 +01:00
|
|
|
if (!set_fts_tokenizer.exec()) {
|
|
|
|
qLog(Warning) << "Couldn't register FTS3 tokenizer";
|
|
|
|
}
|
|
|
|
// Implicit invocation of ~QSqlQuery() when leaving the scope
|
|
|
|
// to release any remaining database locks!
|
2010-06-21 14:38:24 +02:00
|
|
|
}
|
|
|
|
|
2010-05-09 02:10:26 +02:00
|
|
|
if (db.tables().count() == 0) {
|
|
|
|
// Set up initial schema
|
2012-12-25 07:37:27 +01:00
|
|
|
qLog(Info) << "Creating initial database schema";
|
2010-05-09 02:10:26 +02:00
|
|
|
UpdateDatabaseSchema(0, db);
|
|
|
|
}
|
|
|
|
|
2010-11-27 17:14:09 +01:00
|
|
|
// Attach external databases
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& key : attached_databases_.keys()) {
|
2010-12-05 13:53:12 +01:00
|
|
|
QString filename = attached_databases_[key].filename_;
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!injected_database_name_.isNull()) filename = injected_database_name_;
|
2010-11-27 17:14:09 +01:00
|
|
|
|
|
|
|
// Attach the db
|
|
|
|
QSqlQuery q("ATTACH DATABASE :filename AS :alias", db);
|
|
|
|
q.bindValue(":filename", filename);
|
|
|
|
q.bindValue(":alias", key);
|
|
|
|
if (!q.exec()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
qFatal("Couldn't attach external database '%s'",
|
|
|
|
key.toAscii().constData());
|
2010-11-27 17:14:09 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-01-29 17:30:58 +01:00
|
|
|
if (startup_schema_version_ == -1) {
|
2011-02-25 21:10:41 +01:00
|
|
|
UpdateMainSchema(&db);
|
2010-05-09 02:10:26 +02:00
|
|
|
}
|
|
|
|
|
2010-12-07 20:07:12 +01:00
|
|
|
// We might have to initialise the schema in some attached databases now, if
|
|
|
|
// they were deleted and don't match up with the main schema version.
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& key : attached_databases_.keys()) {
|
2013-08-01 18:13:14 +02:00
|
|
|
if (attached_databases_[key].is_temporary_ &&
|
|
|
|
attached_databases_[key].schema_.isEmpty())
|
|
|
|
continue;
|
2010-12-07 20:07:12 +01:00
|
|
|
// Find out if there are any tables in this database
|
2014-02-07 16:34:20 +01:00
|
|
|
QSqlQuery q(QString(
|
|
|
|
"SELECT ROWID FROM %1.sqlite_master"
|
|
|
|
" WHERE type='table'").arg(key),
|
|
|
|
db);
|
2010-12-07 20:07:12 +01:00
|
|
|
if (!q.exec() || !q.next()) {
|
2013-03-03 13:00:24 +01:00
|
|
|
q.finish();
|
|
|
|
ExecSchemaCommandsFromFile(db, attached_databases_[key].schema_, 0);
|
2010-12-07 20:07:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-05-09 02:10:26 +02:00
|
|
|
return db;
|
|
|
|
}
|
|
|
|
|
2011-02-25 21:10:41 +01:00
|
|
|
void Database::UpdateMainSchema(QSqlDatabase* db) {
|
|
|
|
// Get the database's schema version
|
|
|
|
int schema_version = 0;
|
2013-03-03 13:00:24 +01:00
|
|
|
{
|
|
|
|
QSqlQuery q("SELECT version FROM schema_version", *db);
|
2014-02-07 16:34:20 +01:00
|
|
|
if (q.next()) schema_version = q.value(0).toInt();
|
2013-03-03 13:00:24 +01:00
|
|
|
// Implicit invocation of ~QSqlQuery() when leaving the scope
|
|
|
|
// to release any remaining database locks!
|
|
|
|
}
|
2011-02-25 21:10:41 +01:00
|
|
|
|
|
|
|
startup_schema_version_ = schema_version;
|
|
|
|
|
|
|
|
if (schema_version > kSchemaVersion) {
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Warning) << "The database schema (version" << schema_version
|
|
|
|
<< ") is newer than I was expecting";
|
2011-02-25 21:10:41 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (schema_version < kSchemaVersion) {
|
|
|
|
// Update the schema
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int v = schema_version + 1; v <= kSchemaVersion; ++v) {
|
2011-02-25 21:10:41 +01:00
|
|
|
UpdateDatabaseSchema(v, *db);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-27 17:14:09 +01:00
|
|
|
void Database::RecreateAttachedDb(const QString& database_name) {
|
|
|
|
if (!attached_databases_.contains(database_name)) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Warning) << "Attached database does not exist:" << database_name;
|
2010-11-27 17:14:09 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString filename = attached_databases_[database_name].filename_;
|
|
|
|
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
{
|
|
|
|
QSqlDatabase db(Connect());
|
|
|
|
|
|
|
|
QSqlQuery q("DETACH DATABASE :alias", db);
|
|
|
|
q.bindValue(":alias", database_name);
|
|
|
|
if (!q.exec()) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Warning) << "Failed to detach database" << database_name;
|
2010-11-27 17:14:09 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!QFile::remove(filename)) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Warning) << "Failed to remove file" << filename;
|
2010-11-27 17:14:09 +01: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.
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& name : QSqlDatabase::connectionNames()) {
|
2010-11-27 17:14:09 +01:00
|
|
|
QSqlDatabase::removeDatabase(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-08-01 18:13:14 +02:00
|
|
|
void Database::AttachDatabase(const QString& database_name,
|
|
|
|
const AttachedDatabase& database) {
|
|
|
|
attached_databases_[database_name] = database;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void Database::AttachDatabaseOnDbConnection(const QString& database_name,
|
|
|
|
const AttachedDatabase& database,
|
2013-10-25 20:31:56 +02:00
|
|
|
QSqlDatabase& db) {
|
|
|
|
AttachDatabase(database_name, database);
|
|
|
|
|
|
|
|
// Attach the db
|
|
|
|
QSqlQuery q("ATTACH DATABASE :filename AS :alias", db);
|
|
|
|
q.bindValue(":filename", database.filename_);
|
|
|
|
q.bindValue(":alias", database_name);
|
|
|
|
if (!q.exec()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
qFatal("Couldn't attach external database '%s'",
|
|
|
|
database_name.toAscii().constData());
|
2013-10-25 20:31:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-08-01 18:13:14 +02:00
|
|
|
void Database::DetachDatabase(const QString& database_name) {
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
{
|
|
|
|
QSqlDatabase db(Connect());
|
|
|
|
|
|
|
|
QSqlQuery q("DETACH DATABASE :alias", db);
|
|
|
|
q.bindValue(":alias", database_name);
|
|
|
|
if (!q.exec()) {
|
|
|
|
qLog(Warning) << "Failed to detach database" << database_name;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attached_databases_.remove(database_name);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void Database::UpdateDatabaseSchema(int version, QSqlDatabase& db) {
|
2010-05-09 02:10:26 +02:00
|
|
|
QString filename;
|
|
|
|
if (version == 0)
|
2010-10-17 13:22:34 +02:00
|
|
|
filename = ":/schema/schema.sql";
|
2010-05-09 02:10:26 +02:00
|
|
|
else
|
2010-10-17 13:22:34 +02:00
|
|
|
filename = QString(":/schema/schema-%1.sql").arg(version);
|
2013-03-03 13:00:24 +01:00
|
|
|
|
2011-06-14 16:46:02 +02:00
|
|
|
if (version == 31) {
|
|
|
|
// This version used to do a bad job of converting filenames in the songs
|
|
|
|
// table to file:// URLs. Now we do it properly here instead.
|
2013-03-03 13:00:24 +01:00
|
|
|
ScopedTransaction t(&db);
|
|
|
|
|
2011-06-14 16:46:02 +02:00
|
|
|
UrlEncodeFilenameColumn("songs", db);
|
|
|
|
UrlEncodeFilenameColumn("playlist_items", db);
|
2011-11-12 18:15:10 +01:00
|
|
|
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& table : db.tables()) {
|
2011-11-12 18:15:10 +01:00
|
|
|
if (table.startsWith("device_") && table.endsWith("_songs")) {
|
|
|
|
UrlEncodeFilenameColumn(table, db);
|
|
|
|
}
|
|
|
|
}
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Debug) << "Applying database schema update" << version << "from"
|
|
|
|
<< filename;
|
2013-03-10 07:58:09 +01:00
|
|
|
ExecSchemaCommandsFromFile(db, filename, version - 1, true);
|
2013-03-03 13:00:24 +01:00
|
|
|
t.Commit();
|
|
|
|
} else {
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Debug) << "Applying database schema update" << version << "from"
|
|
|
|
<< filename;
|
2013-03-03 13:00:24 +01:00
|
|
|
ExecSchemaCommandsFromFile(db, filename, version - 1);
|
2011-06-14 16:46:02 +02:00
|
|
|
}
|
2010-07-04 01:00:07 +02:00
|
|
|
}
|
|
|
|
|
2011-06-14 16:46:02 +02:00
|
|
|
void Database::UrlEncodeFilenameColumn(const QString& table, QSqlDatabase& db) {
|
|
|
|
QSqlQuery select(QString("SELECT ROWID, filename FROM %1").arg(table), db);
|
2014-02-07 16:34:20 +01:00
|
|
|
QSqlQuery update(
|
|
|
|
QString("UPDATE %1 SET filename=:filename WHERE ROWID=:id").arg(table),
|
|
|
|
db);
|
2011-06-14 16:46:02 +02:00
|
|
|
select.exec();
|
|
|
|
if (CheckErrors(select)) return;
|
|
|
|
while (select.next()) {
|
|
|
|
const int rowid = select.value(0).toInt();
|
|
|
|
const QString filename = select.value(1).toString();
|
2013-03-03 13:00:24 +01:00
|
|
|
|
2011-06-14 16:46:02 +02:00
|
|
|
if (filename.isEmpty() || filename.contains("://")) {
|
|
|
|
continue;
|
|
|
|
}
|
2013-03-03 13:00:24 +01:00
|
|
|
|
2011-06-14 16:46:02 +02:00
|
|
|
const QUrl url = QUrl::fromLocalFile(filename);
|
2013-03-03 13:00:24 +01:00
|
|
|
|
2011-06-14 16:46:02 +02:00
|
|
|
update.bindValue(":filename", url.toEncoded());
|
|
|
|
update.bindValue(":id", rowid);
|
|
|
|
update.exec();
|
|
|
|
CheckErrors(update);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-03-03 13:00:24 +01:00
|
|
|
void Database::ExecSchemaCommandsFromFile(QSqlDatabase& db,
|
2013-03-10 07:58:09 +01:00
|
|
|
const QString& filename,
|
2013-03-03 13:00:24 +01:00
|
|
|
int schema_version,
|
2013-03-10 07:58:09 +01:00
|
|
|
bool in_transaction) {
|
2010-05-09 02:10:26 +02:00
|
|
|
// Open and read the database schema
|
|
|
|
QFile schema_file(filename);
|
|
|
|
if (!schema_file.open(QIODevice::ReadOnly))
|
|
|
|
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
|
2014-02-07 16:34:20 +01:00
|
|
|
ExecSchemaCommands(db, QString::fromUtf8(schema_file.readAll()),
|
|
|
|
schema_version, in_transaction);
|
2010-07-04 01:00:07 +02:00
|
|
|
}
|
2010-05-09 02:10:26 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void Database::ExecSchemaCommands(QSqlDatabase& db, const QString& schema,
|
|
|
|
int schema_version, bool in_transaction) {
|
2010-05-09 02:10:26 +02:00
|
|
|
// Run each command
|
2013-05-13 23:04:24 +02:00
|
|
|
const QStringList commands(schema.split(QRegExp("; *\n\n")));
|
2011-01-04 18:12:29 +01:00
|
|
|
|
|
|
|
// We don't want this list to reflect possible DB schema changes
|
|
|
|
// so we initialize it before executing any statements.
|
2013-03-03 13:00:24 +01:00
|
|
|
// 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.
|
2013-03-10 07:58:09 +01:00
|
|
|
const QStringList song_tables(SongsTables(db, schema_version));
|
2013-03-03 13:00:24 +01:00
|
|
|
|
2013-03-10 07:58:09 +01:00
|
|
|
if (!in_transaction) {
|
|
|
|
ScopedTransaction inner_transaction(&db);
|
|
|
|
ExecSongTablesCommands(db, song_tables, commands);
|
|
|
|
inner_transaction.Commit();
|
2013-03-03 13:00:24 +01:00
|
|
|
} else {
|
2013-03-10 07:58:09 +01:00
|
|
|
ExecSongTablesCommands(db, song_tables, commands);
|
2013-03-03 13:00:24 +01:00
|
|
|
}
|
|
|
|
}
|
2011-01-04 18:12:29 +01:00
|
|
|
|
2013-03-03 13:00:24 +01:00
|
|
|
void Database::ExecSongTablesCommands(QSqlDatabase& db,
|
2013-03-10 07:58:09 +01:00
|
|
|
const QStringList& song_tables,
|
|
|
|
const QStringList& commands) {
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& command : commands) {
|
2010-10-17 19:50:20 +02:00
|
|
|
// There are now lots of "songs" tables that need to have the same schema:
|
|
|
|
// songs, magnatune_songs, and device_*_songs. We allow a magic value
|
|
|
|
// in the schema files to update all songs tables at once.
|
|
|
|
if (command.contains(kMagicAllSongsTables)) {
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& table : song_tables) {
|
2013-03-15 10:25:38 +01:00
|
|
|
// Another horrible hack: device songs tables don't have matching _fts
|
|
|
|
// tables, so if this command tries to touch one, ignore it.
|
|
|
|
if (table.startsWith("device_") &&
|
|
|
|
command.contains(QString(kMagicAllSongsTables) + "_fts")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
|
2010-10-17 19:50:20 +02:00
|
|
|
QString new_command(command);
|
|
|
|
new_command.replace(kMagicAllSongsTables, table);
|
|
|
|
QSqlQuery query(db.exec(new_command));
|
2011-02-05 14:43:04 +01:00
|
|
|
if (CheckErrors(query))
|
2010-10-17 19:50:20 +02:00
|
|
|
qFatal("Unable to update music library database");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
QSqlQuery query(db.exec(command));
|
2014-02-07 16:34:20 +01:00
|
|
|
if (CheckErrors(query)) qFatal("Unable to update music library database");
|
2010-10-17 19:50:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-04-27 00:06:58 +02:00
|
|
|
QStringList Database::SongsTables(QSqlDatabase& db, int schema_version) const {
|
2010-10-17 19:50:20 +02:00
|
|
|
QStringList ret;
|
2010-12-28 16:36:01 +01:00
|
|
|
|
|
|
|
// look for the tables in the main db
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& table : db.tables()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (table == "songs" || table.endsWith("_songs")) ret << table;
|
2010-05-09 02:10:26 +02:00
|
|
|
}
|
2010-12-28 16:36:01 +01:00
|
|
|
|
|
|
|
// look for the tables in attached dbs
|
2014-01-29 17:30:58 +01:00
|
|
|
for (const QString& key : attached_databases_.keys()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
QSqlQuery q(
|
|
|
|
QString(
|
|
|
|
"SELECT NAME FROM %1.sqlite_master"
|
|
|
|
" WHERE type='table' AND name='songs' OR name LIKE '%songs'")
|
|
|
|
.arg(key),
|
|
|
|
db);
|
2010-12-28 16:36:01 +01:00
|
|
|
if (q.exec()) {
|
2014-01-29 17:30:58 +01:00
|
|
|
while (q.next()) {
|
2010-12-28 16:36:01 +01:00
|
|
|
QString tab_name = key + "." + q.value(0).toString();
|
|
|
|
ret << tab_name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-04-27 00:06:58 +02:00
|
|
|
if (schema_version > 29) {
|
|
|
|
// The playlist_items table became a songs table in version 29.
|
|
|
|
ret << "playlist_items";
|
|
|
|
}
|
|
|
|
|
2010-10-17 19:50:20 +02:00
|
|
|
return ret;
|
2010-05-09 02:10:26 +02:00
|
|
|
}
|
|
|
|
|
2011-02-05 14:43:04 +01:00
|
|
|
bool Database::CheckErrors(const QSqlQuery& query) {
|
|
|
|
QSqlError last_error = query.lastError();
|
|
|
|
if (last_error.isValid()) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Error) << "db error: " << last_error;
|
|
|
|
qLog(Error) << "faulty query: " << query.lastQuery();
|
|
|
|
qLog(Error) << "bound values: " << query.boundValues();
|
2011-02-05 14:43:04 +01:00
|
|
|
|
2010-05-09 02:10:26 +02:00
|
|
|
return true;
|
|
|
|
}
|
2011-02-05 14:43:04 +01:00
|
|
|
|
2010-05-09 02:10:26 +02:00
|
|
|
return false;
|
|
|
|
}
|
2012-03-13 15:32:44 +01:00
|
|
|
|
|
|
|
bool Database::IntegrityCheck(QSqlDatabase db) {
|
2012-04-13 15:49:29 +02:00
|
|
|
qLog(Debug) << "Starting database integrity check";
|
2012-03-13 15:32:44 +01:00
|
|
|
int task_id = app_->task_manager()->StartTask(tr("Integrity check"));
|
|
|
|
|
|
|
|
bool ok = false;
|
|
|
|
bool error_reported = false;
|
|
|
|
// Ask for 10 error messages at most.
|
|
|
|
QSqlQuery q(QString("PRAGMA integrity_check(10)"), db);
|
|
|
|
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) {
|
2014-02-07 16:34:20 +01:00
|
|
|
app_->AddError(
|
|
|
|
tr("Database corruption detected. Please read "
|
|
|
|
"https://code.google.com/p/clementine-player/wiki/"
|
|
|
|
"DatabaseCorruption "
|
|
|
|
"for instructions on how to recover your database"));
|
2012-03-13 15:32:44 +01:00
|
|
|
}
|
|
|
|
app_->AddError("Database: " + message);
|
|
|
|
error_reported = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
app_->task_manager()->SetTaskFinished(task_id);
|
|
|
|
|
|
|
|
return ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::DoBackup() {
|
|
|
|
QSqlDatabase db(this->Connect());
|
|
|
|
|
|
|
|
// Before we overwrite anything, make sure the database is not corrupt
|
2012-05-20 21:54:17 +02:00
|
|
|
QMutexLocker l(&mutex_);
|
2012-03-13 15:32:44 +01:00
|
|
|
const bool ok = IntegrityCheck(db);
|
|
|
|
|
|
|
|
if (ok) {
|
2012-03-16 14:50:20 +01:00
|
|
|
BackupFile(db.databaseName());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
bool Database::OpenDatabase(const QString& filename,
|
|
|
|
sqlite3** connection) const {
|
2013-11-22 17:05:55 +01:00
|
|
|
int ret = sqlite3_open(filename.toUtf8(), connection);
|
2012-03-16 14:50:20 +01:00
|
|
|
if (ret != 0) {
|
|
|
|
if (*connection) {
|
2013-11-22 17:05:55 +01:00
|
|
|
const char* error_message = sqlite3_errmsg(*connection);
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Error) << "Failed to open database for backup:" << filename
|
2012-03-16 14:50:20 +01:00
|
|
|
<< error_message;
|
|
|
|
} else {
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Error) << "Failed to open database for backup:" << filename;
|
2012-03-16 14:50:20 +01:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Database::BackupFile(const QString& filename) {
|
2012-04-13 15:49:29 +02:00
|
|
|
qLog(Debug) << "Starting database backup";
|
2012-03-16 14:50:20 +01:00
|
|
|
QString dest_filename = QString("%1.bak").arg(filename);
|
2014-02-07 16:34:20 +01:00
|
|
|
const int task_id =
|
|
|
|
app_->task_manager()->StartTask(tr("Backing up database"));
|
2012-03-16 14:50:20 +01:00
|
|
|
|
2014-02-06 16:49:49 +01:00
|
|
|
sqlite3* source_connection = nullptr;
|
|
|
|
sqlite3* dest_connection = nullptr;
|
2012-03-16 14:50:20 +01:00
|
|
|
|
|
|
|
BOOST_SCOPE_EXIT((source_connection)(dest_connection)(task_id)(app_)) {
|
2014-02-06 16:49:49 +01:00
|
|
|
// Harmless to call sqlite3_close() with a nullptr pointer.
|
2013-11-22 17:05:55 +01:00
|
|
|
sqlite3_close(source_connection);
|
|
|
|
sqlite3_close(dest_connection);
|
2012-03-16 14:50:20 +01:00
|
|
|
app_->task_manager()->SetTaskFinished(task_id);
|
2014-02-07 16:34:20 +01:00
|
|
|
}
|
|
|
|
BOOST_SCOPE_EXIT_END
|
2012-03-16 14:50:20 +01:00
|
|
|
|
|
|
|
bool success = OpenDatabase(filename, &source_connection);
|
|
|
|
if (!success) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
success = OpenDatabase(dest_filename, &dest_connection);
|
|
|
|
if (!success) {
|
|
|
|
return;
|
2012-03-13 15:32:44 +01:00
|
|
|
}
|
2012-03-16 14:50:20 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
sqlite3_backup* backup =
|
|
|
|
sqlite3_backup_init(dest_connection, "main", source_connection, "main");
|
2012-03-16 14:50:20 +01:00
|
|
|
if (!backup) {
|
2013-11-22 17:05:55 +01:00
|
|
|
const char* error_message = sqlite3_errmsg(dest_connection);
|
2012-03-16 14:50:20 +01:00
|
|
|
qLog(Error) << "Failed to start database backup:" << error_message;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ret = SQLITE_OK;
|
|
|
|
do {
|
2013-11-22 17:05:55 +01:00
|
|
|
ret = sqlite3_backup_step(backup, 16);
|
|
|
|
const int page_count = sqlite3_backup_pagecount(backup);
|
2012-03-16 14:50:20 +01:00
|
|
|
app_->task_manager()->SetTaskProgress(
|
2013-11-22 17:05:55 +01:00
|
|
|
task_id, page_count - sqlite3_backup_remaining(backup), page_count);
|
2012-03-16 14:50:20 +01:00
|
|
|
} while (ret == SQLITE_OK);
|
|
|
|
|
|
|
|
if (ret != SQLITE_DONE) {
|
|
|
|
qLog(Error) << "Database backup failed";
|
|
|
|
}
|
|
|
|
|
2013-11-22 17:05:55 +01:00
|
|
|
sqlite3_backup_finish(backup);
|
2012-03-13 15:32:44 +01:00
|
|
|
}
|