[geary/wip/composer-folks: 13/22] Add Geary.ContactStore.search method, impementation and tests
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/composer-folks: 13/22] Add Geary.ContactStore.search method, impementation and tests
- Date: Sat, 15 Jun 2019 14:31:59 +0000 (UTC)
commit 71cb7fcdfe039bd4108ec75ab00860aa1e361438
Author: Michael Gratton <mike vee net>
Date: Wed Jun 12 07:43:02 2019 +1000
Add Geary.ContactStore.search method, impementation and tests
src/engine/api/geary-contact-store.vala | 7 ++
src/engine/common/common-contact-store-impl.vala | 112 ++++++++++++++++-----
src/engine/imap-db/imap-db-database.vala | 22 ++++
test/engine/api/geary-account-mock.vala | 20 ----
test/engine/api/geary-contact-store-mock.vala | 21 +++-
.../common/common-contact-store-impl-test.vala | 63 +++++++++++-
test/engine/imap-db/imap-db-database-test.vala | 42 ++++++++
7 files changed, 239 insertions(+), 48 deletions(-)
---
diff --git a/src/engine/api/geary-contact-store.vala b/src/engine/api/geary-contact-store.vala
index 43266274..0424e7bd 100644
--- a/src/engine/api/geary-contact-store.vala
+++ b/src/engine/api/geary-contact-store.vala
@@ -20,6 +20,13 @@ public interface Geary.ContactStore : GLib.Object {
GLib.Cancellable? cancellable)
throws GLib.Error;
+ /** Searches for contacts based on a specific string */
+ public abstract async Gee.Collection<Contact> search(string query,
+ uint min_importance,
+ uint limit,
+ GLib.Cancellable? cancellable)
+ throws GLib.Error;
+
/** Updates (or adds) a set of contacts in the underlying store */
public abstract async void update_contacts(Gee.Collection<Contact> updated,
GLib.Cancellable? cancellable)
diff --git a/src/engine/common/common-contact-store-impl.vala
b/src/engine/common/common-contact-store-impl.vala
index 550a3110..e8ae147d 100644
--- a/src/engine/common/common-contact-store-impl.vala
+++ b/src/engine/common/common-contact-store-impl.vala
@@ -20,7 +20,6 @@ internal class Geary.ContactStoreImpl : BaseObject, Geary.ContactStore {
this.backing = backing;
}
- /** Returns the contact matching the given email address, if any */
public async Contact? get_by_rfc822(Geary.RFC822.MailboxAddress mailbox,
GLib.Cancellable? cancellable)
throws GLib.Error {
@@ -35,6 +34,24 @@ internal class Geary.ContactStoreImpl : BaseObject, Geary.ContactStore {
return contact;
}
+ public async Gee.Collection<Contact> search(string query,
+ uint min_importance,
+ uint limit,
+ GLib.Cancellable? cancellable)
+ throws GLib.Error {
+ Gee.Collection<Contact>? contacts = null;
+ yield this.backing.exec_transaction_async(
+ Db.TransactionType.RO,
+ (cx, cancellable) => {
+ contacts = do_search_contact(
+ cx, query, min_importance, limit, cancellable
+ );
+ return Db.TransactionOutcome.COMMIT;
+ },
+ cancellable);
+ return contacts;
+ }
+
public async void update_contacts(Gee.Collection<Contact> updated,
GLib.Cancellable? cancellable)
throws GLib.Error {
@@ -49,6 +66,75 @@ internal class Geary.ContactStoreImpl : BaseObject, Geary.ContactStore {
cancellable);
}
+ private Contact? do_fetch_contact(Db.Connection cx,
+ string email,
+ GLib.Cancellable? cancellable)
+ throws GLib.Error {
+ Db.Statement stmt = cx.prepare(
+ "SELECT real_name, highest_importance, normalized_email, flags FROM ContactTable "
+ + "WHERE email=?");
+ stmt.bind_string(0, email);
+
+ Db.Result result = stmt.exec(cancellable);
+
+ Contact? contact = null;
+ if (!result.finished) {
+ contact = new Contact(
+ email,
+ result.string_at(0),
+ result.int_at(1),
+ result.string_at(2)
+ );
+ contact.flags.deserialize(result.string_at(3));
+ }
+ return contact;
+ }
+
+ private Gee.Collection<Contact> do_search_contact(Db.Connection cx,
+ string query,
+ uint min_importance,
+ uint limit,
+ GLib.Cancellable? cancellable)
+ throws GLib.Error {
+ Gee.Collection<Contact> contacts = new Gee.LinkedList<Contact>();
+ string normalised_query = query.make_valid().normalize();
+ if (!String.is_empty(normalised_query)) {
+ normalised_query = "%%%s%%".printf(normalised_query);
+ Db.Statement stmt = cx.prepare("""
+ SELECT * FROM ContactTable
+ WHERE highest_importance >= ? AND (
+ real_name LIKE ? COLLATE UTF8ICASE OR
+ normalized_email LIKE ? COLLATE UTF8ICASE
+ )
+ ORDER BY highest_importance DESC,
+ real_name IS NULL,
+ real_name COLLATE UTF8ICASE,
+ email COLLATE UTF8ICASE
+ LIMIT ?
+ """);
+ stmt.bind_uint(0, min_importance);
+ stmt.bind_string(1, normalised_query);
+ stmt.bind_string(2, normalised_query);
+ stmt.bind_uint(3, limit);
+
+ Db.Result result = stmt.exec(cancellable);
+
+ while (!result.finished) {
+ Contact contact = new Contact(
+ result.string_for("email"),
+ result.string_for("real_name"),
+ result.int_for("highest_importance"),
+ result.string_for("normalized_email")
+ );
+ contact.flags.deserialize(result.string_for("flags"));
+ contacts.add(contact);
+
+ result.next(cancellable);
+ }
+ }
+ return contacts;
+ }
+
private void do_update_contact(Db.Connection cx,
Contact updated,
GLib.Cancellable? cancellable)
@@ -100,28 +186,4 @@ internal class Geary.ContactStoreImpl : BaseObject, Geary.ContactStore {
}
}
- private Contact? do_fetch_contact(Db.Connection cx,
- string email,
- GLib.Cancellable? cancellable)
- throws GLib.Error {
- Db.Statement stmt = cx.prepare(
- "SELECT real_name, highest_importance, normalized_email, flags FROM ContactTable "
- + "WHERE email=?");
- stmt.bind_string(0, email);
-
- Db.Result result = stmt.exec(cancellable);
-
- Contact? contact = null;
- if (!result.finished) {
- contact = new Contact(
- email,
- result.string_at(0),
- result.int_at(1),
- result.string_at(2)
- );
- contact.flags.deserialize(result.string_at(3));
- }
- return contact;
- }
-
}
diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala
index f8a4b588..cbf65d65 100644
--- a/src/engine/imap-db/imap-db-database.vala
+++ b/src/engine/imap-db/imap-db-database.vala
@@ -5,10 +5,22 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
+[CCode (cname = "g_utf8_casefold")]
+extern string utf8_casefold(string data, ssize_t len);
extern int sqlite3_unicodesn_register_tokenizer(Sqlite.Database db);
private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
+ /** Name of UTF-8 case-sensitive SQLite collation function name. */
+ public const string UTF8_CASE_INSENSITIVE_COLLATION = "UTF8ICASE";
+
+ private static int case_insensitive_collation(int a_len, void* a_bytes,
+ int b_len, void* b_bytes) {
+ string a_str = utf8_casefold((string) a_bytes, a_len).collate_key();
+ string b_str = utf8_casefold((string) b_bytes, b_len).collate_key();
+ return strcmp(a_str, b_str);
+ }
+
internal GLib.File attachments_path;
private const int OPEN_PUMP_EVENT_LOOP_MSEC = 100;
@@ -586,6 +598,16 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
cx.set_recursive_triggers(true);
cx.set_synchronous(Db.SynchronousMode.NORMAL);
sqlite3_unicodesn_register_tokenizer(cx.db);
+ if (cx.db.create_collation(
+ UTF8_CASE_INSENSITIVE_COLLATION,
+ Sqlite.UTF8,
+ Database.case_insensitive_collation
+ ) != Sqlite.OK) {
+ throw new DatabaseError.GENERAL(
+ "Failed to register collation function %s",
+ UTF8_CASE_INSENSITIVE_COLLATION
+ );
+ }
}
}
diff --git a/test/engine/api/geary-account-mock.vala b/test/engine/api/geary-account-mock.vala
index 7fb35b34..2159df5a 100644
--- a/test/engine/api/geary-account-mock.vala
+++ b/test/engine/api/geary-account-mock.vala
@@ -16,26 +16,6 @@ public class Geary.MockAccount : Account, MockObject {
}
- public class MockContactStore : GLib.Object, ContactStore {
-
- internal MockContactStore() {
-
- }
-
- public async Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address,
- GLib.Cancellable? cancellable)
- throws GLib.Error {
- throw new EngineError.UNSUPPORTED("Mock method");
- }
-
- public async void update_contacts(Gee.Collection<Contact> contacts,
- GLib.Cancellable? cancellable)
- throws GLib.Error {
- throw new EngineError.UNSUPPORTED("Mock method");
- }
-
- }
-
public class MockClientService : ClientService {
diff --git a/test/engine/api/geary-contact-store-mock.vala b/test/engine/api/geary-contact-store-mock.vala
index dfffcd7e..63143006 100644
--- a/test/engine/api/geary-contact-store-mock.vala
+++ b/test/engine/api/geary-contact-store-mock.vala
@@ -14,7 +14,26 @@ internal class Geary.ContactStoreMock : ContactStore, MockObject, GLib.Object {
public async Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address,
GLib.Cancellable? cancellable)
throws GLib.Error {
- return object_call<Contact?>("get_by_rfc822", { address }, null);
+ return object_call<Contact?>(
+ "get_by_rfc822", { address, cancellable }, null
+ );
+ }
+
+ public async Gee.Collection<Contact> search(string query,
+ uint min_importance,
+ uint limit,
+ GLib.Cancellable? cancellable)
+ throws GLib.Error {
+ return object_call<Gee.Collection<Contact>>(
+ "search",
+ {
+ box_arg(query),
+ uint_arg(min_importance),
+ uint_arg(limit),
+ cancellable
+ },
+ Gee.Collection.empty<Contact>()
+ );
}
public async void update_contacts(Gee.Collection<Contact> updated,
diff --git a/test/engine/common/common-contact-store-impl-test.vala
b/test/engine/common/common-contact-store-impl-test.vala
index e57d3e3f..1c864922 100644
--- a/test/engine/common/common-contact-store-impl-test.vala
+++ b/test/engine/common/common-contact-store-impl-test.vala
@@ -17,6 +17,9 @@ class Geary.ContactStoreImplTest : TestCase {
public ContactStoreImplTest() {
base("Geary.ContactStoreImplTest");
add_test("get_by_rfc822", get_by_rfc822);
+ add_test("search_no_match", search_no_match);
+ add_test("search_email_match", search_email_match);
+ add_test("search_name_match", search_name_match);
add_test("update_new_contact", update_new_contact);
add_test("update_existing_contact", update_existing_contact);
}
@@ -51,7 +54,7 @@ INSERT INTO ContactTable (
) VALUES (
1,
'test example com',
- 'Test',
+ 'Test Name',
'Test example com',
50
);
@@ -80,7 +83,7 @@ INSERT INTO ContactTable (
assert_non_null(existing, "Existing contact");
assert_string("Test example com", existing.email, "Existing email");
assert_string("test example com", existing.normalized_email, "Existing normalized_email");
- assert_string("Test", existing.real_name, "Existing real_name");
+ assert_string("Test Name", existing.real_name, "Existing real_name");
assert_int(50, existing.highest_importance, "Existing highest_importance");
assert_false(existing.flags.always_load_remote_images(), "Existing flags");
@@ -93,6 +96,62 @@ INSERT INTO ContactTable (
assert_null(missing, "Missing contact");
}
+ public void search_no_match() throws GLib.Error {
+ test_article.search.begin(
+ "blarg",
+ 0,
+ 10,
+ null,
+ (obj, ret) => { async_complete(ret); }
+ );
+ Gee.Collection<Contact> results = test_article.search.end(
+ async_result()
+ );
+ assert_int(0, results.size);
+ }
+
+ public void search_email_match() throws GLib.Error {
+ test_article.search.begin(
+ "example.com",
+ 0,
+ 10,
+ null,
+ (obj, ret) => { async_complete(ret); }
+ );
+ Gee.Collection<Contact> results = test_article.search.end(
+ async_result()
+ );
+ assert_int(1, results.size, "results.size");
+
+ Contact search_hit = Collection.get_first(results);
+ assert_string("Test example com", search_hit.email, "Existing email");
+ assert_string("test example com", search_hit.normalized_email, "Existing normalized_email");
+ assert_string("Test Name", search_hit.real_name, "Existing real_name");
+ assert_int(50, search_hit.highest_importance, "Existing highest_importance");
+ assert_false(search_hit.flags.always_load_remote_images(), "Existing flags");
+ }
+
+ public void search_name_match() throws GLib.Error {
+ test_article.search.begin(
+ "Test Name",
+ 0,
+ 10,
+ null,
+ (obj, ret) => { async_complete(ret); }
+ );
+ Gee.Collection<Contact> results = test_article.search.end(
+ async_result()
+ );
+ assert_int(1, results.size, "results.size");
+
+ Contact search_hit = Collection.get_first(results);
+ assert_string("Test example com", search_hit.email, "Existing email");
+ assert_string("test example com", search_hit.normalized_email, "Existing normalized_email");
+ assert_string("Test Name", search_hit.real_name, "Existing real_name");
+ assert_int(50, search_hit.highest_importance, "Existing highest_importance");
+ assert_false(search_hit.flags.always_load_remote_images(), "Existing flags");
+ }
+
public void update_new_contact() throws GLib.Error {
Contact not_persisted = new Contact(
"New example com",
diff --git a/test/engine/imap-db/imap-db-database-test.vala b/test/engine/imap-db/imap-db-database-test.vala
index 9e1b6e00..f937456e 100644
--- a/test/engine/imap-db/imap-db-database-test.vala
+++ b/test/engine/imap-db/imap-db-database-test.vala
@@ -16,6 +16,8 @@ class Geary.ImapDB.DatabaseTest : TestCase {
base("Geary.ImapDb.DatabaseTest");
add_test("open_new", open_new);
add_test("upgrade_0_6", upgrade_0_6);
+ add_test("utf8_case_insensitive_collation",
+ utf8_case_insensitive_collation);
}
public override void set_up() throws GLib.Error {
@@ -123,6 +125,46 @@ class Geary.ImapDB.DatabaseTest : TestCase {
db.close();
}
+ public void utf8_case_insensitive_collation() throws GLib.Error {
+ Database db = new Database(
+ this.tmp_dir.get_child("test.db"),
+ GLib.File.new_for_path(_SOURCE_ROOT_DIR).get_child("sql"),
+ this.tmp_dir.get_child("attachments"),
+ new Geary.SimpleProgressMonitor(Geary.ProgressType.DB_UPGRADE),
+ new Geary.SimpleProgressMonitor(Geary.ProgressType.DB_VACUUM)
+ );
+
+ db.open.begin(
+ Geary.Db.DatabaseFlags.CREATE_FILE, null,
+ (obj, ret) => { async_complete(ret); }
+ );
+ db.open.end(async_result());
+
+ db.exec("""
+ CREATE TABLE Test (id INTEGER PRIMARY KEY, test_str TEXT);
+ INSERT INTO Test (test_str) VALUES ('a');
+ INSERT INTO Test (test_str) VALUES ('B');
+ INSERT INTO Test (test_str) VALUES ('BB');
+ INSERT INTO Test (test_str) VALUES ('🤯');
+ """);
+ string[] expected = { "🤯", "BB", "B", "a" };
+
+ Db.Result result = db.query(
+ "SELECT test_str FROM Test ORDER BY test_str COLLATE UTF8ICASE DESC"
+ );
+
+ int i = 0;
+ while (!result.finished) {
+ assert_true(i < expected.length, "Too many rows");
+ assert_string(expected[i], result.string_at(0));
+ i++;
+ result.next();
+ }
+ assert_true(i == expected.length, "Not enough rows");
+
+ // Need to close it again to stop the GC process running
+ db.close();
+ }
private void unpack_archive(GLib.File archive, GLib.File dest)
throws Error {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]