Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
15 kB · 429 lines
Zig
at zig-0.16
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430// 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);}