/* This file is part of Clementine. Copyright 2010, David Sansome 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 . */ #include "config.h" #include "database.h" #include "scopedtransaction.h" #include #include #include #include #include #include #include #include #include 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(new UnicodeTokenizer); return SQLITE_OK; } int Database::FTSDestroy(sqlite3_tokenizer* tokenizer) { UnicodeTokenizer* real_tokenizer = reinterpret_cast(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 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(new_cursor); return SQLITE_OK; } int Database::FTSClose(sqlite3_tokenizer_cursor* cursor) { UnicodeTokenizerCursor* real_cursor = reinterpret_cast(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(cursor); QList 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( library.resolve("sqlite3_create_function")); _sqlite3_value_type = reinterpret_cast( library.resolve("sqlite3_value_type")); _sqlite3_value_int64 = reinterpret_cast( library.resolve("sqlite3_value_int64")); _sqlite3_value_text = reinterpret_cast( library.resolve("sqlite3_value_text")); _sqlite3_result_int64 = reinterpret_cast( library.resolve("sqlite3_result_int64")); _sqlite3_user_data = reinterpret_cast( 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(_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(_sqlite3_value_text(argv[0])); const char* data_b = reinterpret_cast(_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(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(&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(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; }