search: Add stable queries to thread search results
authorAustin Clements <amdragon@MIT.EDU>
Thu, 24 Oct 2013 15:19:08 +0000 (11:19 -0400)
committerDavid Bremner <david@tethera.net>
Sat, 9 Nov 2013 00:43:29 +0000 (20:43 -0400)
These queries will match exactly the set of messages currently in the
thread, even if more messages later arrive.  Two queries are provided:
one for matched messages and one for unmatched messages.

This can be used to fix race conditions with tagging threads from
search results.  While tagging based on a thread: query can affect
messages that arrived after the search, tagging based on stable
queries affects only the messages the user was shown in the search UI.

Since we want clients to be able to depend on the presence of these
queries, this ushers in schema version 2.

devel/schemata
notmuch-client.h
notmuch-search.c
test/json
test/missing-headers
test/sexp

index cdd0e433f6f442665fc1066f78bfafebe07ce3a2..41dc4a60fff36608e25425c7f113c6f2a1b667b0 100644 (file)
@@ -14,7 +14,17 @@ are interleaved. Keys are printed as keywords (symbols preceded by a
 colon), e.g. (:id "123" :time 54321 :from "foobar"). Null is printed as
 nil, true as t and false as nil.
 
-This is version 1 of the structured output format.
+This is version 2 of the structured output format.
+
+Version history
+---------------
+
+v1
+- First versioned schema release.
+- Added part.content-length and part.content-transfer-encoding fields.
+
+v2
+- Added the thread_summary.query field.
 
 Common non-terminals
 --------------------
@@ -145,7 +155,15 @@ thread_summary = {
     authors:        string,   # comma-separated names with | between
                               # matched and unmatched
     subject:        string,
-    tags:           [string*]
+    tags:           [string*],
+
+    # Two stable query strings identifying exactly the matched and
+    # unmatched messages currently in this thread.  The messages
+    # matched by these queries will not change even if more messages
+    # arrive in the thread.  If there are no matched or unmatched
+    # messages, the corresponding query will be null (there is no
+    # query that matches nothing).  (Added in schema version 2.)
+    query:          [string|null, string|null],
 }
 
 notmuch reply schema
index 4ecb3ae9d44c511818684b5c56ba85c2b1d0c34f..278b498a246adac024778e8d403f30e4db8155ad 100644 (file)
@@ -138,7 +138,7 @@ chomp_newline (char *str)
  * this.  New (required) map fields can be added without increasing
  * this.
  */
-#define NOTMUCH_FORMAT_CUR 1
+#define NOTMUCH_FORMAT_CUR 2
 /* The minimum supported structured output format version.  Requests
  * for format versions below this will return an error. */
 #define NOTMUCH_FORMAT_MIN 1
index d9d39ec35b1607ada4855145cc23f7b0322e1af2..7c973b3d6666ac46eca508b1364b4a9b4eb04304 100644 (file)
@@ -20,6 +20,7 @@
 
 #include "notmuch-client.h"
 #include "sprinter.h"
+#include "string-util.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -46,6 +47,45 @@ sanitize_string (const void *ctx, const char *str)
     return out;
 }
 
+/* Return two stable query strings that identify exactly the matched
+ * and unmatched messages currently in thread.  If there are no
+ * matched or unmatched messages, the returned buffers will be
+ * NULL. */
+static int
+get_thread_query (notmuch_thread_t *thread,
+                 char **matched_out, char **unmatched_out)
+{
+    notmuch_messages_t *messages;
+    char *escaped = NULL;
+    size_t escaped_len = 0;
+
+    *matched_out = *unmatched_out = NULL;
+
+    for (messages = notmuch_thread_get_messages (thread);
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
+    {
+       notmuch_message_t *message = notmuch_messages_get (messages);
+       const char *mid = notmuch_message_get_message_id (message);
+       /* Determine which query buffer to extend */
+       char **buf = notmuch_message_get_flag (
+           message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
+       /* Add this message's id: query.  Since "id" is an exclusive
+        * prefix, it is implicitly 'or'd together, so we only need to
+        * join queries with a space. */
+       if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
+           return -1;
+       if (*buf)
+           *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
+       else
+           *buf = talloc_strdup (thread, escaped);
+       if (!*buf)
+           return -1;
+    }
+    talloc_free (escaped);
+    return 0;
+}
+
 static int
 do_search_threads (sprinter_t *format,
                   notmuch_query_t *query,
@@ -131,6 +171,25 @@ do_search_threads (sprinter_t *format,
                format->string (format, authors);
                format->map_key (format, "subject");
                format->string (format, subject);
+               if (notmuch_format_version >= 2) {
+                   char *matched_query, *unmatched_query;
+                   if (get_thread_query (thread, &matched_query,
+                                         &unmatched_query) < 0) {
+                       fprintf (stderr, "Out of memory\n");
+                       return 1;
+                   }
+                   format->map_key (format, "query");
+                   format->begin_list (format);
+                   if (matched_query)
+                       format->string (format, matched_query);
+                   else
+                       format->null (format);
+                   if (unmatched_query)
+                       format->string (format, unmatched_query);
+                   else
+                       format->null (format);
+                   format->end (format);
+               }
            }
 
            talloc_free (ctx_quote);
index b87b7f6d7165d5854792f4971938bd01e00648f8..e07a29041a96ce821f3c51cb780ec5f2d780d7b9 100755 (executable)
--- a/test/json
+++ b/test/json
@@ -26,6 +26,7 @@ test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
  \"total\": 1,
  \"authors\": \"Notmuch Test Suite\",
  \"subject\": \"json-search-subject\",
+ \"query\": [\"id:$gen_msg_id\", null],
  \"tags\": [\"inbox\",
  \"unread\"]}]"
 
@@ -59,6 +60,7 @@ test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
  \"total\": 1,
  \"authors\": \"Notmuch Test Suite\",
  \"subject\": \"json-search-utf8-body-sübjéct\",
+ \"query\": [\"id:$gen_msg_id\", null],
  \"tags\": [\"inbox\",
  \"unread\"]}]"
 
index f14b8784b5c70cdbf63168226d4483b1ab1d08da..43e861bc69dffb8ec86036d5207738eb3ec12b54 100755 (executable)
@@ -43,7 +43,8 @@ test_expect_equal_json "$output" '
         ],
         "thread": "XXX",
         "timestamp": 978709437,
-        "total": 1
+        "total": 1,
+        "query": ["id:notmuch-sha1-7a6e4eac383ef958fcd3ebf2143db71b8ff01161", null]
     },
     {
         "authors": "Notmuch Test Suite",
@@ -56,7 +57,8 @@ test_expect_equal_json "$output" '
         ],
         "thread": "XXX",
         "timestamp": 0,
-        "total": 1
+        "total": 1,
+        "query": ["id:notmuch-sha1-ca55943aff7a72baf2ab21fa74fab3d632401334", null]
     }
 ]'
 
index 492a82f780d1f15f5cd26609115c9921c578889a..be815e122ccecdf72d942e99e2654b283af82a47 100755 (executable)
--- a/test/sexp
+++ b/test/sexp
@@ -19,7 +19,7 @@ test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :f
 test_begin_subtest "Search message: sexp"
 add_message "[subject]=\"sexp-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"sexp-search-message\""
 output=$(notmuch search --format=sexp "sexp-search-message" | notmuch_search_sanitize)
-test_expect_equal "$output" "((:thread \"0000000000000002\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :tags (\"inbox\" \"unread\")))"
+test_expect_equal "$output" "((:thread \"0000000000000002\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
 
 test_begin_subtest "Show message: sexp, utf-8"
 add_message "[subject]=\"sexp-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
@@ -44,7 +44,7 @@ test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename \"
 test_begin_subtest "Search message: sexp, utf-8"
 add_message "[subject]=\"sexp-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
 output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_search_sanitize)
-test_expect_equal "$output" "((:thread \"0000000000000005\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :tags (\"inbox\" \"unread\")))"
+test_expect_equal "$output" "((:thread \"0000000000000005\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
 
 
 test_done