diff --git a/sql/version-011.sql b/sql/version-011.sql new file mode 100644 index 00000000..e4aa2ddb --- /dev/null +++ b/sql/version-011.sql @@ -0,0 +1,7 @@ +-- +-- Add the internaldate column as a time_t value so we can sort on it. +-- + +ALTER TABLE MessageTable ADD COLUMN internaldate_time_t INTEGER; + +CREATE INDEX MessageTableInternalDateTimeTIndex ON MessageTable(internaldate_time_t); diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index b0449b1b..65af7b97 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -84,7 +84,7 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account { Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; public abstract async Gee.Collection? local_search_async(string keywords, - Geary.Email.Field requested_fields, bool partial_ok, + Geary.Email.Field requested_fields, bool partial_ok, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error; diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index a783abcd..90c03dbe 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -187,10 +187,14 @@ public interface Geary.Account : BaseObject { * Performs a search with the given keyword string. Optionally, a list of folders not to search * can be passed as well as a list of email identifiers to restrict the search to only those messages. * Returns a list of email objects with the requested fields. If partial_ok is false, mail - * will only be returned if it includes all requested fields. + * will only be returned if it includes all requested fields. The list + * is ordered descending by Geary.EmailProperties.date_received, and is + * limited to a maximum number of results and starting offset, so you can + * walk the table. limit can be negative to mean "no limit" but offset + * must not be negative. */ public abstract async Gee.Collection? local_search_async(string keywords, - Geary.Email.Field requested_fields, bool partial_ok, + Geary.Email.Field requested_fields, bool partial_ok, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error; diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala index db46464f..b201423b 100644 --- a/src/engine/api/geary-search-folder.vala +++ b/src/engine/api/geary-search-folder.vala @@ -26,6 +26,9 @@ public class Geary.SearchFolderProperties : Geary.FolderProperties { * Special folder type used to query and display search results. */ public class Geary.SearchFolder : Geary.AbstractLocalFolder { + // Max number of emails that can ever be in the folder. + public static const int MAX_RESULT_EMAILS = 1000; + public override Account account { get { return _account; } } private static FolderRoot? path = null; @@ -74,8 +77,13 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { int result_mutex_token = yield result_mutex.claim_async(); Error? error = null; try { + // TODO: don't limit this to MAX_RESULT_EMAILS. Instead, we could + // be smarter about only fetching the search results in + // list_email_async() etc., but this leads to some more + // complications when redoing the search. Gee.Collection? _new_results = yield account.local_search_async( - keywords, Geary.Email.Field.PROPERTIES, false, exclude_folders, null, cancellable); + keywords, Geary.Email.Field.PROPERTIES, false, MAX_RESULT_EMAILS, 0, + exclude_folders, null, cancellable); if (_new_results == null) { // No results? Remove all existing results and return early. If there are no diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index f6c553d0..e44018cf 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -588,7 +588,7 @@ private class Geary.ImapDB.Account : BaseObject { } public async Gee.Collection? search_async(string prepared_query, - Geary.Email.Field requested_fields, bool partial_ok, + Geary.Email.Field requested_fields, bool partial_ok, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error { Gee.Collection search_results = new Gee.HashSet(); @@ -596,8 +596,21 @@ private class Geary.ImapDB.Account : BaseObject { // TODO: support blacklist, search_ids yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { - Db.Statement stmt = cx.prepare("SELECT id FROM MessageSearchTable WHERE MessageSearchTable MATCH ?"); + string sql = """ + SELECT id + FROM MessageSearchTable + JOIN MessageTable USING (id) + WHERE MessageSearchTable MATCH ? + ORDER BY internaldate_time_t DESC + """; + if (limit > 0) + sql += " LIMIT ? OFFSET ?"; + Db.Statement stmt = cx.prepare(sql); stmt.bind_string(0, prepared_query); + if (limit > 0) { + stmt.bind_int(1, limit); + stmt.bind_int(2, offset); + } Db.Result result = stmt.exec(cancellable); while (!result.finished) { diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala index 4cf0cf85..28787d79 100644 --- a/src/engine/imap-db/imap-db-database.vala +++ b/src/engine/imap-db/imap-db-database.vala @@ -38,6 +38,10 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase { case 10: post_upgrade_add_search_table(); break; + + case 11: + post_upgrade_populate_internal_date_time_t(); + break; } } @@ -145,6 +149,40 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase { return "english"; } + // Version 11. + private void post_upgrade_populate_internal_date_time_t() { + try { + exec_transaction(Db.TransactionType.RW, (cx) => { + Db.Result select = cx.query("SELECT id, internaldate FROM MessageTable"); + while (!select.finished) { + int64 id = select.rowid_at(0); + string? internaldate = select.string_at(1); + + try { + time_t as_time_t = (internaldate != null ? + new Geary.Imap.InternalDate(internaldate).as_time_t : -1); + + Db.Statement update = cx.prepare( + "UPDATE MessageTable SET internaldate_time_t=? WHERE id=?"); + update.bind_int64(0, (int64) as_time_t); + update.bind_rowid(1, id); + update.exec(); + } catch (Error e) { + debug("Error converting internaldate '%s' to time_t: %s", + internaldate, e.message); + } + + select.next(); + } + + return Db.TransactionOutcome.COMMIT; + }); + } catch (Error e) { + debug("Error populating internaldate_time_t column during upgrade to database schema 11: %s", + e.message); + } + } + private void on_prepare_database_connection(Db.Connection cx) throws Error { cx.set_busy_timeout_msec(Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC); cx.set_foreign_keys(true); diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala index 20fcf2b0..8fc7bdf2 100644 --- a/src/engine/imap-db/imap-db-folder.vala +++ b/src/engine/imap-db/imap-db-folder.vala @@ -878,8 +878,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { "INSERT INTO MessageTable " + "(fields, date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, " + "message_id, in_reply_to, reference_ids, subject, header, body, preview, flags, " - + "internaldate, rfc822_size) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + "internaldate, internaldate_time_t, rfc822_size) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); stmt.bind_int(0, row.fields); stmt.bind_string(1, row.date); stmt.bind_int64(2, row.date_time_t); @@ -898,7 +898,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { stmt.bind_string(15, row.preview); stmt.bind_string(16, row.email_flags); stmt.bind_string(17, row.internaldate); - stmt.bind_long(18, row.rfc822_size); + stmt.bind_int64(18, row.internaldate_time_t); + stmt.bind_long(19, row.rfc822_size); message_id = stmt.exec_insert(cancellable); do_associate_with_folder(cx, message_id, email, cancellable); @@ -1076,7 +1077,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { break; case Geary.Email.Field.PROPERTIES: - append = "internaldate, rfc822_size"; + append = "internaldate, internaldate_time_t, rfc822_size"; break; } } @@ -1262,10 +1263,11 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { if (new_fields.is_any_set(Geary.Email.Field.PROPERTIES)) { Db.Statement stmt = cx.prepare( - "UPDATE MessageTable SET internaldate=?, rfc822_size=? WHERE id=?"); + "UPDATE MessageTable SET internaldate=?, internaldate_time_t=?, rfc822_size=? WHERE id=?"); stmt.bind_string(0, row.internaldate); - stmt.bind_long(1, row.rfc822_size); - stmt.bind_rowid(2, row.id); + stmt.bind_int64(1, row.internaldate_time_t); + stmt.bind_long(2, row.rfc822_size); + stmt.bind_rowid(3, row.id); stmt.exec(cancellable); } diff --git a/src/engine/imap-db/imap-db-message-row.vala b/src/engine/imap-db/imap-db-message-row.vala index 16162e2a..235771e8 100644 --- a/src/engine/imap-db/imap-db-message-row.vala +++ b/src/engine/imap-db/imap-db-message-row.vala @@ -33,6 +33,7 @@ private class Geary.ImapDB.MessageRow { public string? email_flags { get; set; default = null; } public string? internaldate { get; set; default = null; } + public time_t internaldate_time_t { get; set; default = -1; } public long rfc822_size { get; set; default = -1; } public MessageRow() { @@ -91,6 +92,7 @@ private class Geary.ImapDB.MessageRow { if (fields.is_all_set(Geary.Email.Field.PROPERTIES)) { internaldate = results.string_for("internaldate"); + internaldate_time_t = (time_t) results.int64_for("internaldate_time_t"); rfc822_size = results.long_for("rfc822_size"); } } @@ -242,6 +244,7 @@ private class Geary.ImapDB.MessageRow { if (email.fields.is_all_set(Geary.Email.Field.PROPERTIES)) { Geary.Imap.EmailProperties? imap_properties = (Geary.Imap.EmailProperties) email.properties; internaldate = (imap_properties != null) ? imap_properties.internaldate.original : null; + internaldate_time_t = (imap_properties != null) ? imap_properties.internaldate.as_time_t : -1; rfc822_size = (imap_properties != null) ? imap_properties.rfc822_size.value : -1; fields = fields.set(Geary.Email.Field.PROPERTIES); diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index aeeda22c..1b2119c6 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -496,11 +496,14 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { } public override async Gee.Collection? local_search_async(string keywords, - Geary.Email.Field requested_fields, bool partial_ok, + Geary.Email.Field requested_fields, bool partial_ok, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error { + if (offset < 0) + throw new EngineError.BAD_PARAMETERS("Offset must not be negative"); + return yield local.search_async(local.prepare_search_query(keywords), - requested_fields, partial_ok, folder_blacklist, search_ids, cancellable); + requested_fields, partial_ok, limit, offset, folder_blacklist, search_ids, cancellable); } private void on_login_failed(Geary.Credentials? credentials) {