From 09d568d00c86ed70edaed82a3ada13eebc9b7b4a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 29 Aug 2026 21:06:09 -0500 Subject: [PATCH] test query search error paths; map BadQuerySyntax in count functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S-expression queries defer parsing until the query runs, so count and search return BAD_QUERY_SYNTAX then — undocumented in the header's retval lists and previously misreported as Unexpected by the narrowed count error sets. Tests cover the sexp syntax-error path across all four operations, Xapian syntax leniency, and XapianException from searching a closed database. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017i7R7ReKJAHGQpitxGXqrb --- src/Query.zig | 8 ++++++++ src/tests.zig | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/Query.zig b/src/Query.zig index 9fe17dc..04baa87 100644 --- a/src/Query.zig +++ b/src/Query.zig @@ -166,6 +166,9 @@ pub fn searchMessages(self: *const Query) Error!MessagesIterator { } 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. @@ -180,12 +183,16 @@ pub fn countMessages(self: *const Query) CountMessagesError!u32 { 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, }; } 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. @@ -207,6 +214,7 @@ pub fn countThreads(self: *const Query) CountThreadsError!u32 { 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, diff --git a/src/tests.zig b/src/tests.zig index 93d3efa..d3390f9 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -432,6 +432,40 @@ test "Query tag exclusion error paths" { try query.addTagExclude("other"); } +test "Query search error paths" { + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + const db = test_db.database; + + // Xapian-syntax queries are lenient: a malformed string is treated as + // literal terms and still searches successfully. + const lenient = try db.queryCreate("tag:(unbalanced"); + defer lenient.deinit(); + _ = try lenient.countMessages(); + + // S-expression queries defer parsing until the query runs, so a bad + // sexp errors at search/count time. + const bad = try db.queryCreateWithSyntax("(unbalanced", .sexp); + defer bad.deinit(); + try std.testing.expectError(error.BadQuerySyntax, bad.countMessages()); + try std.testing.expectError(error.BadQuerySyntax, bad.countThreads()); + try std.testing.expectError(error.BadQuerySyntax, bad.searchMessages()); + try std.testing.expectError(error.BadQuerySyntax, bad.searchThreads()); +} + +test "Query searches on a closed database" { + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + + const query = try test_db.database.queryCreate("*"); + defer query.deinit(); + try test_db.database.close(); + + try std.testing.expectError(error.XapianException, query.countMessages()); + try std.testing.expectError(error.XapianException, query.searchMessages()); + try std.testing.expectError(error.XapianException, query.searchThreads()); +} + test "Message identity, headers, and dates" { var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); -- 2.51.2