[geary/wip/composer-folks: 13/22] Add Geary.ContactStore.search method, impementation and tests



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]