Clementine-audio-player-Mac.../src/core/database.cpp

489 lines
15 KiB
C++

/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
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/>.
*/
#include "config.h"
#include "database.h"
#include "scopedtransaction.h"
#include <QCoreApplication>
#include <QDir>
#include <QLibrary>
#include <QLibraryInfo>
#include <QSqlDriver>
#include <QSqlQuery>
#include <QtDebug>
#include <QThread>
#include <QVariant>
const char* Database::kDatabaseFilename = "clementine.db";
const int Database::kSchemaVersion = 19;
const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
QMutex Database::sNextConnectionIdMutex;
Database::Token::Token(const QString& token, int start, int end)
: token(token),
start_offset(start),
end_offset(end) {
}
struct sqlite3_tokenizer_module {
int iVersion;
int (*xCreate)(
int argc, /* Size of argv array */
const char *const*argv, /* Tokenizer argument strings */
sqlite3_tokenizer **ppTokenizer /* OUT: Created tokenizer */
);
int (*xDestroy)(sqlite3_tokenizer *pTokenizer);
int (*xOpen)(
sqlite3_tokenizer *pTokenizer, /* Tokenizer object */
const char *pInput, int nBytes, /* Input buffer */
sqlite3_tokenizer_cursor **ppCursor /* OUT: Created tokenizer cursor */
);
int (*xClose)(sqlite3_tokenizer_cursor *pCursor);
int (*xNext)(
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 */
);
};
struct sqlite3_tokenizer {
const sqlite3_tokenizer_module *pModule; /* The module for this tokenizer */
/* Tokenizer implementations will typically add additional fields */
};
struct sqlite3_tokenizer_cursor {
sqlite3_tokenizer *pTokenizer; /* Tokenizer for this cursor. */
/* Tokenizer implementations will typically add additional fields */
};
int (*Database::_sqlite3_create_function) (
sqlite3*, const char*, int, int, void*,
void (*) (sqlite3_context*, int, sqlite3_value**),
void (*) (sqlite3_context*, int, sqlite3_value**),
void (*) (sqlite3_context*)) = NULL;
int (*Database::_sqlite3_value_type) (sqlite3_value*) = NULL;
sqlite_int64 (*Database::_sqlite3_value_int64) (sqlite3_value*) = NULL;
const uchar* (*Database::_sqlite3_value_text) (sqlite3_value*) = NULL;
void (*Database::_sqlite3_result_int64) (sqlite3_context*, sqlite_int64) = NULL;
void* (*Database::_sqlite3_user_data) (sqlite3_context*) = NULL;
bool Database::sStaticInitDone = false;
bool Database::sLoadedSqliteSymbols = false;
sqlite3_tokenizer_module* Database::sFTSTokenizer = NULL;
int Database::FTSCreate(int argc, const char* const* argv, sqlite3_tokenizer** tokenizer) {
*tokenizer = reinterpret_cast<sqlite3_tokenizer*>(new UnicodeTokenizer);
return SQLITE_OK;
}
int Database::FTSDestroy(sqlite3_tokenizer* tokenizer) {
UnicodeTokenizer* real_tokenizer = reinterpret_cast<UnicodeTokenizer*>(tokenizer);
delete real_tokenizer;
return SQLITE_OK;
}
int Database::FTSOpen(
sqlite3_tokenizer* pTokenizer,
const char* input,
int bytes,
sqlite3_tokenizer_cursor** cursor) {
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();
if (unicode <= 0x007f) {
offset += 1;
} else if (unicode >= 0x0080 && unicode <= 0x07ff) {
offset += 2;
} else if (unicode >= 0x0800) {
offset += 3;
}
// Unicode astral planes unsupported in Qt?
/*else if (unicode >= 0x010000 && unicode <= 0x10ffff) {
offset += 4;
}*/
if (!data[i].isLetterOrNumber()) {
// Token finished.
if (token.length() != 0) {
tokens << Token(token, start_offset, offset - 1);
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) {
tokens << Token(token, start_offset, offset);
token.clear();
}
}
}
new_cursor->tokens = tokens;
*cursor = reinterpret_cast<sqlite3_tokenizer_cursor*>(new_cursor);
return SQLITE_OK;
}
int Database::FTSClose(sqlite3_tokenizer_cursor* cursor) {
UnicodeTokenizerCursor* real_cursor = reinterpret_cast<UnicodeTokenizerCursor*>(cursor);
delete real_cursor;
return SQLITE_OK;
}
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);
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;
}
void Database::StaticInit() {
if (sStaticInitDone) {
return;
}
sStaticInitDone = true;
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;
#ifdef HAVE_STATIC_SQLITE
// We statically link libqsqlite.dll on windows and mac so these symbols are already
// available
_sqlite3_create_function = sqlite3_create_function;
_sqlite3_value_type = sqlite3_value_type;
_sqlite3_value_int64 = sqlite3_value_int64;
_sqlite3_value_text = sqlite3_value_text;
_sqlite3_result_int64 = sqlite3_result_int64;
_sqlite3_user_data = sqlite3_user_data;
sLoadedSqliteSymbols = true;
return;
#else // HAVE_STATIC_SQLITE
QString plugin_path = QLibraryInfo::location(QLibraryInfo::PluginsPath) +
"/sqldrivers/libqsqlite";
QLibrary library(plugin_path);
if (!library.load()) {
qDebug() << "QLibrary::load() failed for " << plugin_path;
return;
}
_sqlite3_create_function = reinterpret_cast<Sqlite3CreateFunc>(
library.resolve("sqlite3_create_function"));
_sqlite3_value_type = reinterpret_cast<int (*) (sqlite3_value*)>(
library.resolve("sqlite3_value_type"));
_sqlite3_value_int64 = reinterpret_cast<sqlite_int64 (*) (sqlite3_value*)>(
library.resolve("sqlite3_value_int64"));
_sqlite3_value_text = reinterpret_cast<const uchar* (*) (sqlite3_value*)>(
library.resolve("sqlite3_value_text"));
_sqlite3_result_int64 = reinterpret_cast<void (*) (sqlite3_context*, sqlite_int64)>(
library.resolve("sqlite3_result_int64"));
_sqlite3_user_data = reinterpret_cast<void* (*) (sqlite3_context*)>(
library.resolve("sqlite3_user_data"));
if (!_sqlite3_create_function ||
!_sqlite3_value_type ||
!_sqlite3_value_int64 ||
!_sqlite3_value_text ||
!_sqlite3_result_int64 ||
!_sqlite3_user_data) {
qDebug() << "Couldn't resolve sqlite symbols";
sLoadedSqliteSymbols = false;
} else {
sLoadedSqliteSymbols = true;
}
#endif
}
bool Database::Like(const char* needle, const char* haystack) {
uint hash = qHash(needle);
if (!query_hash_ || hash != query_hash_) {
// New query, parse and cache.
query_cache_ = QString::fromUtf8(needle).section('%', 1, 1).split(' ');
query_hash_ = hash;
}
// In place decompose string as it's faster :-)
QString b = QString::fromUtf8(haystack);
QChar* data = b.data();
for (int i = 0; i < b.length(); ++i) {
if (data[i].decompositionTag() != QChar::NoDecomposition) {
data[i] = data[i].decomposition()[0];
}
}
foreach (const QString& query, query_cache_) {
if (!b.contains(query, Qt::CaseInsensitive)) {
return false;
}
}
return true;
}
// Custom LIKE(X, Y) function for sqlite3 that supports case insensitive unicode matching.
void Database::SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv) {
Q_ASSERT(argc == 2 || argc == 3);
if (_sqlite3_value_type(argv[0]) == SQLITE_NULL ||
_sqlite3_value_type(argv[1]) == SQLITE_NULL) {
_sqlite3_result_int64(context, 0);
return;
}
Q_ASSERT(_sqlite3_value_type(argv[0]) == _sqlite3_value_type(argv[1]));
Database* library = reinterpret_cast<Database*>(_sqlite3_user_data(context));
Q_ASSERT(library);
switch (_sqlite3_value_type(argv[0])) {
case SQLITE_INTEGER: {
qint64 result = _sqlite3_value_int64(argv[0]) - _sqlite3_value_int64(argv[1]);
_sqlite3_result_int64(context, result ? 0 : 1);
break;
}
case SQLITE_TEXT: {
const char* data_a = reinterpret_cast<const char*>(_sqlite3_value_text(argv[0]));
const char* data_b = reinterpret_cast<const char*>(_sqlite3_value_text(argv[1]));
_sqlite3_result_int64(context, library->Like(data_a, data_b) ? 1 : 0);
break;
}
}
}
Database::Database(QObject* parent, const QString& database_name)
: QObject(parent),
mutex_(QMutex::Recursive),
injected_database_name_(database_name),
query_hash_(0)
{
{
QMutexLocker l(&sNextConnectionIdMutex);
connection_id_ = sNextConnectionId ++;
}
directory_ = QDir::toNativeSeparators(
QDir::homePath() + "/.config/" + QCoreApplication::organizationName());
QMutexLocker l(&mutex_);
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_)) {
}
}
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
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()) {
emit Error("Database: " + db.lastError().text());
return db;
}
// Find Sqlite3 functions in the Qt plugin.
StaticInit();
QSqlQuery set_fts_tokenizer("SELECT fts3_tokenizer(:name, :pointer)", db);
set_fts_tokenizer.bindValue(":name", "unicode");
set_fts_tokenizer.bindValue(":pointer", QByteArray(
reinterpret_cast<const char*>(&sFTSTokenizer), sizeof(&sFTSTokenizer)));
if (!set_fts_tokenizer.exec()) {
qWarning() << "Couldn't register FTS3 tokenizer";
}
// We want Unicode aware LIKE clauses and FTS if possible.
if (sLoadedSqliteSymbols) {
QVariant v = db.driver()->handle();
if (v.isValid() && qstrcmp(v.typeName(), "sqlite3*") == 0) {
sqlite3* handle = *static_cast<sqlite3**>(v.data());
if (handle) {
_sqlite3_create_function(
handle, // Sqlite3 handle.
"LIKE", // Function name (either override or new).
2, // Number of args.
SQLITE_ANY, // What types this function accepts.
this, // Custom data available via sqlite3_user_data().
&Database::SqliteLike, // Our function :-)
NULL, NULL);
}
}
}
if (db.tables().count() == 0) {
// Set up initial schema
UpdateDatabaseSchema(0, db);
}
// Get the database's schema version
QSqlQuery q("SELECT version FROM schema_version", db);
int schema_version = 0;
if (q.next())
schema_version = q.value(0).toInt();
if (schema_version > kSchemaVersion) {
qWarning() << "The database schema (version" << schema_version << ") is newer than I was expecting";
return db;
}
if (schema_version < kSchemaVersion) {
// Update the schema
for (int v=schema_version+1 ; v<= kSchemaVersion ; ++v) {
UpdateDatabaseSchema(v, db);
}
}
return db;
}
void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
QString filename;
if (version == 0)
filename = ":/schema/schema.sql";
else
filename = QString(":/schema/schema-%1.sql").arg(version);
ScopedTransaction t(&db);
ExecFromFile(filename, db);
t.Commit();
}
void Database::ExecFromFile(const QString &filename, QSqlDatabase &db) {
// 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());
ExecCommands(QString::fromUtf8(schema_file.readAll()), db);
}
void Database::ExecCommands(const QString &schema, QSqlDatabase &db) {
// Run each command
QStringList commands(schema.split(";\n\n"));
foreach (const QString& command, commands) {
// 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)) {
foreach (const QString& table, SongsTables(db)) {
qDebug() << "Updating" << table << "for" << kMagicAllSongsTables;
QString new_command(command);
new_command.replace(kMagicAllSongsTables, table);
QSqlQuery query(db.exec(new_command));
if (CheckErrors(query.lastError()))
qFatal("Unable to update music library database");
}
} else {
QSqlQuery query(db.exec(command));
if (CheckErrors(query.lastError()))
qFatal("Unable to update music library database");
}
}
}
QStringList Database::SongsTables(QSqlDatabase& db) const {
QStringList ret;
foreach (const QString& table, db.tables()) {
if (table == "songs" || table.endsWith("_songs"))
ret << table;
}
return ret;
}
bool Database::CheckErrors(const QSqlError& error) {
if (error.isValid()) {
qDebug() << error;
emit Error("LibraryBackend: " + error.text());
return true;
}
return false;
}