464 lines
14 KiB
C++
464 lines
14 KiB
C++
/* This file is part of Clementine.
|
|
|
|
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 = 17;
|
|
|
|
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.sql";
|
|
else
|
|
filename = QString(":/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) {
|
|
QSqlQuery query(db.exec(command));
|
|
if (CheckErrors(query.lastError()))
|
|
qFatal("Unable to update music library database");
|
|
}
|
|
}
|
|
|
|
bool Database::CheckErrors(const QSqlError& error) {
|
|
if (error.isValid()) {
|
|
qDebug() << error;
|
|
emit Error("LibraryBackend: " + error.text());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|