// SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie // SPDX-License-Identifier: GPL-3.0-or-later //! A search of the notmuch database, created with `Database.queryCreate` or //! `Database.queryCreateWithSyntax`. //! //! Configure the search with `setSort`, `setOmitExcluded`, and //! `addTagExclude`, then run it with `searchMessages` or `searchThreads`, or //! count the results with `countMessages` and `countThreads`. Everything a //! query returns is owned by the query and freed by `deinit`. const Query = @This(); const std = @import("std"); const c = @import("c"); pub const Exclude = @import("enums.zig").Exclude; pub const Sort = @import("enums.zig").Sort; const Database = @import("Database.zig"); const Error = @import("error.zig").Error; const MessagesIterator = @import("MessagesIterator.zig"); const status = @import("enums.zig").status; const ThreadsIterator = @import("ThreadsIterator.zig"); const wrap = @import("error.zig").wrap; /// The underlying `notmuch_query_t` handle; only needed directly when /// calling libnotmuch C APIs these bindings do not wrap. query: *c.notmuch_query_t, pub fn init(query: *c.notmuch_query_t) Query { return .{ .query = query }; } /// Return the query string of this query. See `Database.queryCreate` /// /// The returned string belongs to the query: it must not be modified or /// freed by the caller, and it remains valid until the query is /// deinitialized. pub fn getQueryString(self: *const Query) [:0]const u8 { return std.mem.span(c.notmuch_query_get_query_string(self.query)); } test getQueryString { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const query = try test_db.database.queryCreate("tag:inbox"); defer query.deinit(); try std.testing.expectEqualStrings("tag:inbox", query.getQueryString()); } /// Return the notmuch database of this query. See `Database.queryCreate`. pub fn getDatabase(self: *const Query) ?Database { return .{ .database = c.notmuch_query_get_database(self.query) orelse return null, }; } test getDatabase { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const query = try test_db.database.queryCreate("tag:inbox"); defer query.deinit(); // The query's owning database is the one it was created from. const db = query.getDatabase() orelse return error.TestUnexpectedResult; try std.testing.expectEqual(test_db.database.database, db.database); } /// Specify whether to omit excluded results or simply flag them. By default, /// this is set to `true`. /// /// If set to `true` or `all`, `searchMessages` will omit excluded messages /// from the results, and `searchThreads` will omit threads that match only /// in excluded messages. If set to `true`, `searchThreads` will include /// all messages in threads that match in at least one non-excluded message. /// Otherwise, if set to `all`, `searchThreads` will omit excluded messages from /// all threads. /// /// If set to `false` or `flag` then both `searchMessages` and `searchThreads` /// will return all matching messages/threads regardless of exclude status. /// If set to `flag` then the exclude flag will be set for any excluded /// message that is returned by `searchMessages`, and the thread counts for /// threads returned by `searchThreads` will be the number of non-excluded /// messages/matches. Otherwise, if set to `false`, then the exclude status is /// completely ignored. /// /// The performance difference when calling `searchMessages` should be /// relatively small (and both should be very fast). However, in some cases, /// `searchThreads` is very much faster when omitting excluded messages as it /// does not need to construct the threads that only match in excluded messages. pub fn setOmitExcluded(self: *const Query, omit_excluded: Exclude) void { c.notmuch_query_set_omit_excluded(self.query, @intFromEnum(omit_excluded)); } test setOmitExcluded { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database; const spam = try db.findMessage("three@example.org") orelse return error.TestUnexpectedResult; defer spam.deinit(); try spam.addTag("spam"); const query = try db.queryCreate("*"); defer query.deinit(); try query.addTagExclude("spam"); // With .false, the exclusion is disabled entirely. query.setOmitExcluded(.false); { var messages = try query.searchMessages(); defer messages.deinit(); var count: usize = 0; while (try messages.next()) |message| : (count += 1) message.deinit(); try std.testing.expectEqual(fixture.corpus.len, count); } // With .flag, excluded messages are returned but marked with the // excluded message flag. query.setOmitExcluded(.flag); { var messages = try query.searchMessages(); defer messages.deinit(); var excluded_count: usize = 0; while (try messages.next()) |message| { defer message.deinit(); if (try message.getFlag(.excluded)) excluded_count += 1; } try std.testing.expectEqual(@as(usize, 1), excluded_count); } } /// Specify the sorting desired for this query. pub fn setSort(self: *const Query, sort: Sort) void { c.notmuch_query_set_sort(self.query, @intFromEnum(sort)); } test setSort { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const query = try test_db.database.queryCreate("*"); defer query.deinit(); // Every sort order round-trips through getSort. See `searchMessages` // for the effect on search results. for ([_]Sort{ .oldest_first, .newest_first, .message_id, .unsorted }) |sort| { query.setSort(sort); try std.testing.expectEqual(sort, query.getSort()); } } /// Return the sort specified for this query. pub fn getSort(self: *const Query) Sort { return @enumFromInt(c.notmuch_query_get_sort(self.query)); } test getSort { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); // The default sort order is newest first. const query = try test_db.database.queryCreate("*"); defer query.deinit(); try std.testing.expectEqual(Sort.newest_first, query.getSort()); } pub const AddTagExcludeError = error{ /// The tag is explicitly present in the query, so not excluded. Ignored, /// A Xapian exception occurred. Most likely a problem lazily parsing the /// query string. XapianException, /// libnotmuch returned an undocumented status code. Unexpected, }; /// Add a tag that will be excluded from the query results by default. This /// exclusion will be ignored if this tag appears explicitly in the query. pub fn addTagExclude(self: *const Query, tag: [:0]const u8) AddTagExcludeError!void { return switch (status(c.notmuch_query_add_tag_exclude(self.query, tag))) { .success => {}, .ignored => error.Ignored, .xapian_exception => error.XapianException, else => error.Unexpected, }; } test addTagExclude { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database; const spam = try db.findMessage("three@example.org") orelse return error.TestUnexpectedResult; defer spam.deinit(); try spam.addTag("spam"); const query = try db.queryCreate("*"); defer query.deinit(); try query.addTagExclude("spam"); query.setOmitExcluded(.true); var messages = try query.searchMessages(); defer messages.deinit(); var count: usize = 0; while (try messages.next()) |message| { defer message.deinit(); count += 1; } try std.testing.expectEqual(fixture.corpus.len - 1, count); } /// Execute a query for threads, returning a `ThreadsIterator` object which can /// be used to iterate over the results. The returned threads object is owned by /// the query and as such, will only be valid until `Query.deinit`. /// /// Typical usage might be: /// ``` /// const db: Database = …; /// defer db.deinit() catch {}; /// const query = try db.queryCreate(query_string); /// defer query.deinit(); /// var it = try query.searchThreads(); /// defer it.deinit(); /// while (try it.next()) |thread| { /// defer thread.deinit(); /// … /// } /// ``` /// /// Note: If you are finished with a thread before its containing query, you /// can call `Thread.deinit` to clean up some memory sooner (as in the /// above example). Otherwise, if your thread objects are long-lived, then you /// don't need to call `Thread.deinit` and all the memory will still be /// reclaimed when the query is destroyed. pub fn searchThreads(self: *const Query) Error!ThreadsIterator { var threads: ?*c.notmuch_threads_t = null; try wrap(c.notmuch_query_search_threads(self.query, &threads)); return .{ .threads = threads, }; } test searchThreads { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const query = try test_db.database.queryCreate("*"); defer query.deinit(); query.setSort(.oldest_first); // The corpus holds a two-message thread plus two standalone messages. var threads = try query.searchThreads(); defer threads.deinit(); const first = try threads.next() orelse return error.TestUnexpectedResult; defer first.deinit(); try std.testing.expectEqualStrings("Lunch plans", first.getSubject()); var remaining: usize = 0; while (try threads.next()) |thread| : (remaining += 1) thread.deinit(); try std.testing.expectEqual(@as(usize, 2), remaining); } /// Execute a query for messages, returning a MessagesIterator object which can /// be used to iterate over the results. The returned messages object is owned /// by the query and as such, will only be valid until `Query.deinit`. /// /// Typical usage might be: /// ``` /// const db: Database = …; /// defer db.deinit() catch {}; /// const query = try db.queryCreate(query_string); /// defer query.deinit(); /// var it = try query.searchMessages(); /// defer it.deinit(); /// while (try it.next()) |message| { /// defer message.deinit(); /// … /// } /// ``` /// /// Note: If you are finished with a message before its containing query, you /// can call `Message.deinit` to clean up some memory sooner (as in the /// above example). Otherwise, if your message objects are long-lived, then you /// don't need to call `Message.deinit` and all the memory will still be /// reclaimed when the query is deinitialized. pub fn searchMessages(self: *const Query) Error!MessagesIterator { var out: ?*c.notmuch_messages_t = undefined; try wrap(c.notmuch_query_search_messages(self.query, &out)); return .{ .messages = out, }; } test searchMessages { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const query = try test_db.database.queryCreate("*"); defer query.deinit(); query.setSort(.oldest_first); var oldest = try query.searchMessages(); defer oldest.deinit(); const first = try oldest.next() orelse return error.TestUnexpectedResult; defer first.deinit(); try std.testing.expectEqualStrings("Lunch plans", first.getHeader("Subject") orelse ""); query.setSort(.newest_first); var newest = try query.searchMessages(); defer newest.deinit(); const last = try newest.next() orelse return error.TestUnexpectedResult; defer last.deinit(); try std.testing.expectEqualStrings("Zig bindings", last.getHeader("Subject") orelse ""); } pub const CountMessagesError = error{ /// Syntax error in the query (returned for s-expression queries, whose /// parsing is deferred until the query runs). BadQuerySyntax, /// A Xapian exception occurred. XapianException, /// libnotmuch returned an undocumented status code. Unexpected, }; /// Return the number of messages matching a search. /// /// This function performs a search and returns the number of matching messages. pub fn countMessages(self: *const Query) CountMessagesError!u32 { std.debug.assert(@typeInfo(c_uint).int.bits == 32); var count: c_uint = undefined; return switch (status(c.notmuch_query_count_messages(self.query, &count))) { .success => @intCast(count), .bad_query_syntax => error.BadQuerySyntax, .xapian_exception => error.XapianException, else => error.Unexpected, }; } test countMessages { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const query = try test_db.database.queryCreate("from:carol@example.org"); defer query.deinit(); try std.testing.expectEqual(@as(u32, 1), try query.countMessages()); try std.testing.expectEqual(@as(u32, 1), try query.countThreads()); } pub const CountThreadsError = error{ /// Syntax error in the query (returned for s-expression queries, whose /// parsing is deferred until the query runs). BadQuerySyntax, /// Memory allocation failed. OutOfMemory, /// A Xapian exception occurred. XapianException, /// libnotmuch returned an undocumented status code. Unexpected, }; /// Return the number of threads matching a search. /// /// This function performs a search and returns the number of unique thread IDs /// in the matching messages. This is the same as number of threads matching /// a search. /// /// Note that this is a significantly heavier operation than /// `countMessages`. pub fn countThreads(self: *const Query) CountThreadsError!u32 { std.debug.assert(@typeInfo(c_uint).int.bits == 32); var count: c_uint = undefined; return switch (status(c.notmuch_query_count_threads(self.query, &count))) { .success => @intCast(count), .bad_query_syntax => error.BadQuerySyntax, .out_of_memory => error.OutOfMemory, .xapian_exception => error.XapianException, else => error.Unexpected, }; } test countThreads { const fixture = @import("testing.zig"); var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); // Four messages, but the lunch pair shares a thread. const query = try test_db.database.queryCreate("*"); defer query.deinit(); try std.testing.expectEqual(@as(u32, 4), try query.countMessages()); try std.testing.expectEqual(@as(u32, 3), try query.countThreads()); } /// Deinitialize the `Query` along with any associated resources. /// /// This will in turn deinitialize any `ThreadsIterator` and `MessagesIterator` /// objects generated by this query, (and in turn any `Thread` and `Message` /// objects generated from those results, etc.), if such objects haven't already /// been deinitialized. pub fn deinit(self: *const Query) void { c.notmuch_query_destroy(self.query); }