Commit a7787462 authored by Charles Lindsay's avatar Charles Lindsay

Limit search results from DB; fix #7049

This caps the search results at 1000 emails, due to our unfortunate
requirement of constructing an object for each search result.  A better
way to proceed here would be to do the search only as items were loaded
in the SearchFolder, but that gets complicated when the search phrase
gets updated.
parent 0aff47c2
--
-- 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);
......@@ -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<Geary.EmailIdentifier>? 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<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error;
......
......@@ -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<Geary.Email>? 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<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error;
......
......@@ -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<Geary.Email>? _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
......
......@@ -588,7 +588,7 @@ private class Geary.ImapDB.Account : BaseObject {
}
public async Gee.Collection<Geary.Email>? 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<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error {
Gee.Collection<Geary.Email> search_results = new Gee.HashSet<Geary.Email>();
......@@ -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) {
......
......@@ -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);
......
......@@ -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);
}
......
......@@ -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);
......
......@@ -496,11 +496,14 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
}
public override async Gee.Collection<Geary.Email>? 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<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? 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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment