mirror of
https://github.com/clementine-player/Clementine
synced 2025-01-18 20:40:43 +01:00
Support query splitting in library search.
Improve performance of LIKE(). Fixes issue #104
This commit is contained in:
parent
9adf70d77e
commit
95252ccc71
@ -43,6 +43,7 @@ int (*LibraryBackend::_sqlite3_value_type) (sqlite3_value*) = NULL;
|
||||
sqlite_int64 (*LibraryBackend::_sqlite3_value_int64) (sqlite3_value*) = NULL;
|
||||
const uchar* (*LibraryBackend::_sqlite3_value_text) (sqlite3_value*) = NULL;
|
||||
void (*LibraryBackend::_sqlite3_result_int64) (sqlite3_context*, sqlite_int64) = NULL;
|
||||
void* (*LibraryBackend::_sqlite3_user_data) (sqlite3_context*) = NULL;
|
||||
|
||||
bool LibraryBackend::sStaticInitDone = false;
|
||||
bool LibraryBackend::sLoadedSqliteSymbols = false;
|
||||
@ -82,12 +83,15 @@ void LibraryBackend::StaticInit() {
|
||||
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_result_int64 ||
|
||||
!_sqlite3_user_data) {
|
||||
qDebug() << "Couldn't resolve sqlite symbols";
|
||||
sLoadedSqliteSymbols = false;
|
||||
} else {
|
||||
@ -97,15 +101,29 @@ void LibraryBackend::StaticInit() {
|
||||
}
|
||||
|
||||
bool LibraryBackend::Like(const char* needle, const char* haystack) {
|
||||
QString a = QString::fromUtf8(needle).section('%', 1, 1);
|
||||
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;
|
||||
}
|
||||
QString b = QString::fromUtf8(haystack);
|
||||
return b.contains(a, Qt::CaseInsensitive);
|
||||
foreach (const QString& query, query_cache_) {
|
||||
if (b.contains(query, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom LIKE(X, Y) function for sqlite3 that supports case insensitive unicode matching.
|
||||
void LibraryBackend::SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv) {
|
||||
Q_ASSERT(argc == 2 || argc == 3);
|
||||
Q_ASSERT(_sqlite3_value_type(argv[0]) == _sqlite3_value_type(argv[1]));
|
||||
|
||||
LibraryBackend* library = reinterpret_cast<LibraryBackend*>(_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]);
|
||||
@ -115,7 +133,7 @@ void LibraryBackend::SqliteLike(sqlite3_context* context, int argc, sqlite3_valu
|
||||
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, Like(data_a, data_b) ? 1 : 0);
|
||||
_sqlite3_result_int64(context, library->Like(data_a, data_b) ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -128,7 +146,8 @@ LibraryBackendInterface::LibraryBackendInterface(QObject *parent)
|
||||
|
||||
LibraryBackend::LibraryBackend(QObject* parent, const QString& database_name)
|
||||
: LibraryBackendInterface(parent),
|
||||
injected_database_name_(database_name)
|
||||
injected_database_name_(database_name),
|
||||
query_hash_(0)
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup("Library");
|
||||
@ -187,7 +206,7 @@ QSqlDatabase LibraryBackend::Connect() {
|
||||
"LIKE", // Function name (either override or new).
|
||||
2, // Number of args.
|
||||
SQLITE_ANY, // What types this function accepts.
|
||||
NULL, // Custom data available via sqlite3_user_data().
|
||||
this, // Custom data available via sqlite3_user_data().
|
||||
&LibraryBackend::SqliteLike, // Our function :-)
|
||||
NULL, NULL);
|
||||
}
|
||||
|
@ -183,15 +183,21 @@ class LibraryBackend : public LibraryBackendInterface {
|
||||
QString injected_database_name_;
|
||||
|
||||
|
||||
uint query_hash_;
|
||||
QStringList query_cache_;
|
||||
|
||||
FRIEND_TEST(LibraryBackendTest, LikeWorksWithAllAscii);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeWorksWithUnicode);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeAsciiCaseInsensitive);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeUnicodeCaseInsensitive);
|
||||
FRIEND_TEST(LibraryBackendTest, LikePerformance);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeCacheInvalidated);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeQuerySplit);
|
||||
|
||||
// Do static initialisation like loading sqlite functions.
|
||||
static void StaticInit();
|
||||
// Custom LIKE() function for sqlite.
|
||||
static bool Like(const char* needle, const char* haystack);
|
||||
bool Like(const char* needle, const char* haystack);
|
||||
static void SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv);
|
||||
typedef int (*Sqlite3CreateFunc) (
|
||||
sqlite3*, const char*, int, int, void*,
|
||||
@ -205,6 +211,7 @@ class LibraryBackend : public LibraryBackendInterface {
|
||||
static sqlite_int64 (*_sqlite3_value_int64) (sqlite3_value*);
|
||||
static const uchar* (*_sqlite3_value_text) (sqlite3_value*);
|
||||
static void (*_sqlite3_result_int64) (sqlite3_context*, sqlite_int64);
|
||||
static void* (*_sqlite3_user_data) (sqlite3_context*);
|
||||
|
||||
static bool sStaticInitDone;
|
||||
static bool sLoadedSqliteSymbols;
|
||||
|
@ -68,6 +68,48 @@ class LibraryBackendTest : public ::testing::Test {
|
||||
QSqlDatabase database_;
|
||||
};
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
struct PerfTimer {
|
||||
PerfTimer(int iterations) : iterations_(iterations) {
|
||||
gettimeofday(&start_time_, NULL);
|
||||
}
|
||||
|
||||
~PerfTimer() {
|
||||
gettimeofday(&end_time_, NULL);
|
||||
|
||||
timeval elapsed_time;
|
||||
timersub(&end_time_, &start_time_, &elapsed_time);
|
||||
int elapsed_us = elapsed_time.tv_usec + elapsed_time.tv_sec * 1000000;
|
||||
|
||||
qDebug() << "Elapsed:" << elapsed_us << "us";
|
||||
qDebug() << "Time per iteration:" << float(elapsed_us) / iterations_ << "us";
|
||||
}
|
||||
|
||||
timeval start_time_;
|
||||
timeval end_time_;
|
||||
int iterations_;
|
||||
};
|
||||
|
||||
TEST_F(LibraryBackendTest, LikePerformance) {
|
||||
const int iterations = 1000000;
|
||||
|
||||
const char* needle = "foo";
|
||||
const char* haystack = "foobarbaz foobarbaz";
|
||||
qDebug() << "Simple query";
|
||||
{
|
||||
PerfTimer perf(iterations);
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
backend_->Like(needle, haystack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_F(LibraryBackendTest, DatabaseInitialises) {
|
||||
// Check that these tables exist
|
||||
QStringList tables = database_.tables();
|
||||
@ -157,22 +199,34 @@ TEST_F(LibraryBackendTest, GetAlbumArtNonExistent) {
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeWorksWithAllAscii) {
|
||||
EXPECT_TRUE(LibraryBackend::Like("%ar%", "bar"));
|
||||
EXPECT_FALSE(LibraryBackend::Like("%ar%", "foo"));
|
||||
EXPECT_TRUE(backend_->Like("%ar%", "bar"));
|
||||
EXPECT_FALSE(backend_->Like("%ar%", "foo"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeWorksWithUnicode) {
|
||||
EXPECT_TRUE(LibraryBackend::Like("%Снег%", "Снег"));
|
||||
EXPECT_FALSE(LibraryBackend::Like("%Снег%", "foo"));
|
||||
EXPECT_TRUE(backend_->Like("%Снег%", "Снег"));
|
||||
EXPECT_FALSE(backend_->Like("%Снег%", "foo"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeAsciiCaseInsensitive) {
|
||||
EXPECT_TRUE(LibraryBackend::Like("%ar%", "BAR"));
|
||||
EXPECT_FALSE(LibraryBackend::Like("%ar%", "FOO"));
|
||||
EXPECT_TRUE(backend_->Like("%ar%", "BAR"));
|
||||
EXPECT_FALSE(backend_->Like("%ar%", "FOO"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeUnicodeCaseInsensitive) {
|
||||
EXPECT_TRUE(LibraryBackend::Like("%снег%", "Снег"));
|
||||
EXPECT_TRUE(backend_->Like("%снег%", "Снег"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeCacheInvalidated) {
|
||||
EXPECT_TRUE(backend_->Like("%foo%", "foobar"));
|
||||
EXPECT_FALSE(backend_->Like("%baz%", "foobar"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeQuerySplit) {
|
||||
EXPECT_TRUE(backend_->Like("%foo bar%", "foobar"));
|
||||
EXPECT_TRUE(backend_->Like("%foo bar%", "barbaz"));
|
||||
EXPECT_TRUE(backend_->Like("%foo bar%", "foobaz"));
|
||||
EXPECT_FALSE(backend_->Like("%foo bar%", "baz"));
|
||||
}
|
||||
|
||||
// Test adding a single song to the database, then getting various information
|
||||
|
@ -25,7 +25,7 @@ int main(int argc, char** argv) {
|
||||
testing::InitGoogleMock(&argc, argv);
|
||||
|
||||
testing::AddGlobalTestEnvironment(new MetatypesEnvironment);
|
||||
QApplication a(argc, argv);
|
||||
QCoreApplication a(argc, argv);
|
||||
testing::AddGlobalTestEnvironment(new ResourcesEnvironment);
|
||||
|
||||
return RUN_ALL_TESTS();
|
||||
|
Loading…
Reference in New Issue
Block a user