]> git.notmuchmail.org Git - notmuch/blobdiff - lib/database.cc
lib: merge internal prefix tables
[notmuch] / lib / database.cc
index 4173b68162ac8be100ece4ac7497f56bee3ada18..b954b34cc3be57fd76fe182753f10849685e6371 100644 (file)
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ * along with this program.  If not, see https://www.gnu.org/licenses/ .
  *
  * Author: Carl Worth <cworth@cworth.org>
  */
 
 #include "database-private.h"
 #include "parse-time-vrp.h"
+#include "query-fp.h"
 #include "string-util.h"
 
 #include <iostream>
@@ -41,6 +42,7 @@ using namespace std;
 typedef struct {
     const char *name;
     const char *prefix;
+    notmuch_field_flag_t flags;
 } prefix_t;
 
 #define NOTMUCH_DATABASE_VERSION 3
@@ -48,6 +50,12 @@ typedef struct {
 #define STRINGIFY(s) _SUB_STRINGIFY(s)
 #define _SUB_STRINGIFY(s) #s
 
+#if HAVE_XAPIAN_DB_RETRY_LOCK
+#define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK)
+#else
+#define DB_ACTION Xapian::DB_CREATE_OR_OPEN
+#endif
+
 /* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
  *
  * We currently have three different types of documents (mail, ghost,
@@ -90,6 +98,9 @@ typedef struct {
  *                     STRING is the name of a file within that
  *                     directory for this mail message.
  *
+ *      property:       Has a property with key=value
+ *                 FIXME: if no = is present, should match on any value
+ *
  *    A mail document also has four values:
  *
  *     TIMESTAMP:      The time_t value corresponding to the message's
@@ -101,6 +112,9 @@ typedef struct {
  *
  *     SUBJECT:        The value of the "Subject" header
  *
+ *     LAST_MOD:       The revision number as of the last tag or
+ *                     filename change.
+ *
  * In addition, terms from the content of the message are added with
  * "from", "to", "attachment", and "subject" prefixes for use by the
  * user in searching. Similarly, terms from the path of the mail
@@ -181,6 +195,14 @@ typedef struct {
  *                     generated is 1 and the value will be
  *                     incremented for each thread ID.
  *
+ *     C*              metadata keys starting with C indicate
+ *                     configuration data. It can be managed with the
+ *                     n_database_*config* API.  There is a convention
+ *                     of hierarchical keys separated by '.' (e.g.
+ *                     query.notmuch stores the value for the named
+ *                     query 'notmuch'), but it is not enforced by the
+ *                     API.
+ *
  * Obsolete metadata
  * -----------------
  *
@@ -213,7 +235,7 @@ typedef struct {
 
 /* With these prefix values we follow the conventions published here:
  *
- * http://xapian.org/docs/omega/termprefixes.html
+ * https://xapian.org/docs/omega/termprefixes.html
  *
  * as much as makes sense. Note that I took some liberty in matching
  * the reserved prefix values to notmuch concepts, (for example, 'G'
@@ -226,36 +248,38 @@ typedef struct {
  * nearly universal to all mail messages).
  */
 
-static prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
-    { "type",                  "T" },
-    { "reference",             "XREFERENCE" },
-    { "replyto",               "XREPLYTO" },
-    { "directory",             "XDIRECTORY" },
-    { "file-direntry",         "XFDIRENTRY" },
-    { "directory-direntry",    "XDDIRENTRY" },
-};
-
-static prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
-    { "thread",                        "G" },
-    { "tag",                   "K" },
-    { "is",                    "K" },
-    { "id",                    "Q" },
-    { "path",                  "P" },
+static const
+prefix_t prefix_table[] = {
+    /* name                    term prefix     flags */
+    { "type",                  "T",            NOTMUCH_FIELD_NO_FLAGS },
+    { "reference",             "XREFERENCE",   NOTMUCH_FIELD_NO_FLAGS },
+    { "replyto",               "XREPLYTO",     NOTMUCH_FIELD_NO_FLAGS },
+    { "directory",             "XDIRECTORY",   NOTMUCH_FIELD_NO_FLAGS },
+    { "file-direntry",         "XFDIRENTRY",   NOTMUCH_FIELD_NO_FLAGS },
+    { "directory-direntry",    "XDDIRENTRY",   NOTMUCH_FIELD_NO_FLAGS },
+    { "thread",                        "G",            NOTMUCH_FIELD_EXTERNAL },
+    { "tag",                   "K",            NOTMUCH_FIELD_EXTERNAL },
+    { "is",                    "K",            NOTMUCH_FIELD_EXTERNAL },
+    { "id",                    "Q",            NOTMUCH_FIELD_EXTERNAL },
+    { "path",                  "P",            NOTMUCH_FIELD_EXTERNAL },
+    { "property",              "XPROPERTY",    NOTMUCH_FIELD_EXTERNAL },
     /*
-     * Without the ":", since this is a multi-letter prefix, Xapian
-     * will add a colon itself if the first letter of the path is
-     * upper-case ASCII. Including the ":" forces there to always be a
-     * colon, which keeps our own logic simpler.
+     * Unconditionally add ':' to reduce potential ambiguity with
+     * overlapping prefixes and/or terms that start with capital
+     * letters. See Xapian document termprefixes.html for related
+     * discussion.
      */
-    { "folder",                        "XFOLDER:" },
-};
-
-static prefix_t PROBABILISTIC_PREFIX[]= {
-    { "from",                  "XFROM" },
-    { "to",                    "XTO" },
-    { "attachment",            "XATTACHMENT" },
-    { "mimetype",              "XMIMETYPE"},
-    { "subject",               "XSUBJECT"},
+    { "folder",                        "XFOLDER:",     NOTMUCH_FIELD_EXTERNAL },
+    { "from",                  "XFROM",        NOTMUCH_FIELD_EXTERNAL |
+                                               NOTMUCH_FIELD_PROBABILISTIC },
+    { "to",                    "XTO",          NOTMUCH_FIELD_EXTERNAL |
+                                               NOTMUCH_FIELD_PROBABILISTIC },
+    { "attachment",            "XATTACHMENT",  NOTMUCH_FIELD_EXTERNAL |
+                                               NOTMUCH_FIELD_PROBABILISTIC },
+    { "mimetype",              "XMIMETYPE",    NOTMUCH_FIELD_EXTERNAL |
+                                               NOTMUCH_FIELD_PROBABILISTIC },
+    { "subject",               "XSUBJECT",     NOTMUCH_FIELD_EXTERNAL |
+                                               NOTMUCH_FIELD_PROBABILISTIC },
 };
 
 const char *
@@ -263,19 +287,9 @@ _find_prefix (const char *name)
 {
     unsigned int i;
 
-    for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++) {
-       if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
-           return BOOLEAN_PREFIX_INTERNAL[i].prefix;
-    }
-
-    for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
-       if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
-           return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
-    }
-
-    for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
-       if (strcmp (name, PROBABILISTIC_PREFIX[i].name) == 0)
-           return PROBABILISTIC_PREFIX[i].prefix;
+    for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+       if (strcmp (name, prefix_table[i].name) == 0)
+           return prefix_table[i].prefix;
     }
 
     INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
@@ -310,6 +324,8 @@ static const struct {
      * them. */
     { NOTMUCH_FEATURE_INDEXED_MIMETYPES,
       "indexed MIME types", "w"},
+    { NOTMUCH_FEATURE_LAST_MOD,
+      "modification tracking", "w"},
 };
 
 const char *
@@ -342,12 +358,47 @@ notmuch_status_to_string (notmuch_status_t status)
        return "Unsupported operation";
     case NOTMUCH_STATUS_UPGRADE_REQUIRED:
        return "Operation requires a database upgrade";
+    case NOTMUCH_STATUS_PATH_ERROR:
+       return "Path supplied is illegal for this function";
     default:
     case NOTMUCH_STATUS_LAST_STATUS:
        return "Unknown error status value";
     }
 }
 
+void
+_notmuch_database_log (notmuch_database_t *notmuch,
+                     const char *format,
+                     ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    if (notmuch->status_string)
+       talloc_free (notmuch->status_string);
+
+    notmuch->status_string = talloc_vasprintf (notmuch, format, va_args);
+    va_end (va_args);
+}
+
+void
+_notmuch_database_log_append (notmuch_database_t *notmuch,
+                     const char *format,
+                     ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    if (notmuch->status_string)
+       notmuch->status_string = talloc_vasprintf_append (notmuch->status_string, format, va_args);
+    else
+       notmuch->status_string = talloc_vasprintf (notmuch, format, va_args);
+
+    va_end (va_args);
+}
+
 static void
 find_doc_ids_for_term (notmuch_database_t *notmuch,
                       const char *term,
@@ -456,7 +507,7 @@ notmuch_database_find_message (notmuch_database_t *notmuch,
 
        return NOTMUCH_STATUS_SUCCESS;
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "A Xapian exception occurred finding message: %s.\n",
+       _notmuch_database_log (notmuch, "A Xapian exception occurred finding message: %s.\n",
                 error.get_msg().c_str());
        notmuch->exception_reported = TRUE;
        *message_ret = NULL;
@@ -640,6 +691,12 @@ notmuch_database_create_verbose (const char *path,
        goto DONE;
     }
 
+    if (path[0] != '/') {
+       message = strdup ("Error: Database path must be absolute.\n");
+       status = NOTMUCH_STATUS_PATH_ERROR;
+       goto DONE;
+    }
+
     err = stat (path, &st);
     if (err) {
        IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: %s.\n",
@@ -705,13 +762,30 @@ notmuch_status_t
 _notmuch_database_ensure_writable (notmuch_database_t *notmuch)
 {
     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
-       fprintf (stderr, "Cannot write to a read-only database.\n");
+       _notmuch_database_log (notmuch, "Cannot write to a read-only database.\n");
        return NOTMUCH_STATUS_READ_ONLY_DATABASE;
     }
 
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+/* Allocate a revision number for the next change. */
+unsigned long
+_notmuch_database_new_revision (notmuch_database_t *notmuch)
+{
+    unsigned long new_revision = notmuch->revision + 1;
+
+    /* If we're in an atomic section, hold off on updating the
+     * committed revision number until we commit the atomic section.
+     */
+    if (notmuch->atomic_nesting)
+       notmuch->atomic_dirty = TRUE;
+    else
+       notmuch->revision = new_revision;
+
+    return new_revision;
+}
+
 /* Parse a database features string from the given database version.
  * Returns the feature bit set.
  *
@@ -830,6 +904,12 @@ notmuch_database_open_verbose (const char *path,
        goto DONE;
     }
 
+    if (path[0] != '/') {
+       message = strdup ("Error: Database path must be absolute.\n");
+       status = NOTMUCH_STATUS_PATH_ERROR;
+       goto DONE;
+    }
+
     if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
        message = strdup ("Out of memory\n");
        status = NOTMUCH_STATUS_OUT_OF_MEMORY;
@@ -863,6 +943,7 @@ notmuch_database_open_verbose (const char *path,
 
     notmuch = talloc_zero (NULL, notmuch_database_t);
     notmuch->exception_reported = FALSE;
+    notmuch->status_string = NULL;
     notmuch->path = talloc_strdup (notmuch, path);
 
     if (notmuch->path[strlen (notmuch->path) - 1] == '/')
@@ -872,10 +953,11 @@ notmuch_database_open_verbose (const char *path,
     notmuch->atomic_nesting = 0;
     try {
        string last_thread_id;
+       string last_mod;
 
        if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
            notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
-                                                              Xapian::DB_CREATE_OR_OPEN);
+                                                              DB_ACTION);
        } else {
            notmuch->xapian_db = new Xapian::Database (xapian_path);
        }
@@ -930,11 +1012,30 @@ notmuch_database_open_verbose (const char *path,
                INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
        }
 
+       /* Get current highest revision number. */
+       last_mod = notmuch->xapian_db->get_value_upper_bound (
+           NOTMUCH_VALUE_LAST_MOD);
+       if (last_mod.empty ())
+           notmuch->revision = 0;
+       else
+           notmuch->revision = Xapian::sortable_unserialise (last_mod);
+       notmuch->uuid = talloc_strdup (
+           notmuch, notmuch->xapian_db->get_uuid ().c_str ());
+
        notmuch->query_parser = new Xapian::QueryParser;
        notmuch->term_gen = new Xapian::TermGenerator;
        notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
        notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
        notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+#if HAVE_XAPIAN_FIELD_PROCESSOR
+       /* This currently relies on the query parser to pass anything
+        * with a .. to the range processor */
+       notmuch->date_field_processor = new DateFieldProcessor();
+       notmuch->query_parser->add_boolean_prefix("date", notmuch->date_field_processor);
+       notmuch->query_field_processor = new QueryFieldProcessor (*notmuch->query_parser, notmuch);
+       notmuch->query_parser->add_boolean_prefix("query", notmuch->query_field_processor);
+#endif
+       notmuch->last_mod_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:");
 
        notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
        notmuch->query_parser->set_database (*notmuch->xapian_db);
@@ -942,16 +1043,18 @@ notmuch_database_open_verbose (const char *path,
        notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
        notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
        notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor);
-
-       for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
-           prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
-           notmuch->query_parser->add_boolean_prefix (prefix->name,
-                                                      prefix->prefix);
-       }
-
-       for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
-           prefix_t *prefix = &PROBABILISTIC_PREFIX[i];
-           notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
+       notmuch->query_parser->add_valuerangeprocessor (notmuch->last_mod_range_processor);
+
+       for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+           const prefix_t *prefix = &prefix_table[i];
+           if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) {
+               if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC) {
+                   notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
+               } else {
+                   notmuch->query_parser->add_boolean_prefix (prefix->name,
+                                                              prefix->prefix);
+               }
+           }
        }
     } catch (const Xapian::Error &error) {
        IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
@@ -1004,7 +1107,7 @@ notmuch_database_close (notmuch_database_t *notmuch)
        } catch (const Xapian::Error &error) {
            status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
            if (! notmuch->exception_reported) {
-               fprintf (stderr, "Error: A Xapian exception occurred closing database: %s\n",
+               _notmuch_database_log (notmuch, "Error: A Xapian exception occurred closing database: %s\n",
                         error.get_msg().c_str());
            }
        }
@@ -1020,11 +1123,19 @@ notmuch_database_close (notmuch_database_t *notmuch)
     notmuch->value_range_processor = NULL;
     delete notmuch->date_range_processor;
     notmuch->date_range_processor = NULL;
+    delete notmuch->last_mod_range_processor;
+    notmuch->last_mod_range_processor = NULL;
+
+#if HAVE_XAPIAN_FIELD_PROCESSOR
+    delete notmuch->date_field_processor;
+    notmuch->date_field_processor = NULL;
+    delete notmuch->query_field_processor;
+    notmuch->query_field_processor = NULL;
+#endif
 
     return status;
 }
 
-#if HAVE_XAPIAN_COMPACT
 static int
 unlink_cb (const char *path,
           unused (const struct stat *sb),
@@ -1136,12 +1247,12 @@ notmuch_database_compact (const char *path,
     }
 
     if (stat (backup_path, &statbuf) != -1) {
-       fprintf (stderr, "Path already exists: %s\n", backup_path);
+       _notmuch_database_log (notmuch, "Path already exists: %s\n", backup_path);
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
     }
     if (errno != ENOENT) {
-       fprintf (stderr, "Unknown error while stat()ing path: %s\n",
+       _notmuch_database_log (notmuch, "Unknown error while stat()ing path: %s\n",
                 strerror (errno));
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
@@ -1161,20 +1272,20 @@ notmuch_database_compact (const char *path,
        compactor.set_destdir (compact_xapian_path);
        compactor.compact ();
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "Error while compacting: %s\n", error.get_msg().c_str());
+       _notmuch_database_log (notmuch, "Error while compacting: %s\n", error.get_msg().c_str());
        ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
        goto DONE;
     }
 
     if (rename (xapian_path, backup_path)) {
-       fprintf (stderr, "Error moving %s to %s: %s\n",
+       _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
                 xapian_path, backup_path, strerror (errno));
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
     }
 
     if (rename (compact_xapian_path, xapian_path)) {
-       fprintf (stderr, "Error moving %s to %s: %s\n",
+       _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
                 compact_xapian_path, xapian_path, strerror (errno));
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
@@ -1182,7 +1293,7 @@ notmuch_database_compact (const char *path,
 
     if (! keep_backup) {
        if (rmtree (backup_path)) {
-           fprintf (stderr, "Error removing old database %s: %s\n",
+           _notmuch_database_log (notmuch, "Error removing old database %s: %s\n",
                     backup_path, strerror (errno));
            ret = NOTMUCH_STATUS_FILE_ERROR;
            goto DONE;
@@ -1193,6 +1304,10 @@ notmuch_database_compact (const char *path,
     if (notmuch) {
        notmuch_status_t ret2;
 
+       const char *str = notmuch_database_status_string (notmuch);
+       if (status_cb && str)
+           status_cb (str, closure);
+
        ret2 = notmuch_database_destroy (notmuch);
 
        /* don't clobber previous error status */
@@ -1204,17 +1319,6 @@ notmuch_database_compact (const char *path,
 
     return ret;
 }
-#else
-notmuch_status_t
-notmuch_database_compact (unused (const char *path),
-                         unused (const char *backup_path),
-                         unused (notmuch_compact_status_cb_t status_cb),
-                         unused (void *closure))
-{
-    fprintf (stderr, "notmuch was compiled against a xapian version lacking compaction support.\n");
-    return NOTMUCH_STATUS_UNSUPPORTED_OPERATION;
-}
-#endif
 
 notmuch_status_t
 notmuch_database_destroy (notmuch_database_t *notmuch)
@@ -1299,6 +1403,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
     enum _notmuch_features target_features, new_features;
     notmuch_status_t status;
     notmuch_private_status_t private_status;
+    notmuch_query_t *query = NULL;
     unsigned int count = 0, total = 0;
 
     status = _notmuch_database_ensure_writable (notmuch);
@@ -1314,7 +1419,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        return NOTMUCH_STATUS_SUCCESS;
 
     if (progress_notify) {
-       /* Setup our handler for SIGALRM */
+       /* Set up our handler for SIGALRM */
        memset (&action, 0, sizeof (struct sigaction));
        action.sa_handler = handle_sigalrm;
        sigemptyset (&action.sa_mask);
@@ -1333,10 +1438,18 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 
     /* Figure out how much total work we need to do. */
     if (new_features &
-       (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) {
-       notmuch_query_t *query = notmuch_query_create (notmuch, "");
-       total += notmuch_query_count_messages (query);
+       (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER |
+        NOTMUCH_FEATURE_LAST_MOD)) {
+       query = notmuch_query_create (notmuch, "");
+       unsigned msg_count;
+
+       status = notmuch_query_count_messages_st (query, &msg_count);
+       if (status)
+           goto DONE;
+
+       total += msg_count;
        notmuch_query_destroy (query);
+       query = NULL;
     }
     if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
        t_end = db->allterms_end ("XTIMESTAMP");
@@ -1360,13 +1473,18 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 
     /* Perform per-message upgrades. */
     if (new_features &
-       (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) {
-       notmuch_query_t *query = notmuch_query_create (notmuch, "");
+       (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER |
+        NOTMUCH_FEATURE_LAST_MOD)) {
        notmuch_messages_t *messages;
        notmuch_message_t *message;
        char *filename;
 
-       for (messages = notmuch_query_search_messages (query);
+       query = notmuch_query_create (notmuch, "");
+
+       status = notmuch_query_search_messages_st (query, &messages);
+       if (status)
+           goto DONE;
+       for (;
             notmuch_messages_valid (messages);
             notmuch_messages_move_to_next (messages))
        {
@@ -1397,6 +1515,14 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
            if (new_features & NOTMUCH_FEATURE_BOOL_FOLDER)
                _notmuch_message_upgrade_folder (message);
 
+           /* Prior to NOTMUCH_FEATURE_LAST_MOD, messages did not
+            * track modification revisions.  Give all messages the
+            * next available revision; since we just started tracking
+            * revisions for this database, that will be 1.
+            */
+           if (new_features & NOTMUCH_FEATURE_LAST_MOD)
+               _notmuch_message_upgrade_last_mod (message);
+
            _notmuch_message_sync (message);
 
            notmuch_message_destroy (message);
@@ -1405,6 +1531,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        }
 
        notmuch_query_destroy (query);
+       query = NULL;
     }
 
     /* Perform per-directory upgrades. */
@@ -1489,7 +1616,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
            }
 
            if (private_status) {
-               fprintf (stderr,
+               _notmuch_database_log (notmuch,
                         "Upgrade failed while creating ghost messages.\n");
                status = COERCE_STATUS (private_status, "Unexpected status from _notmuch_message_initialize_ghost");
                goto DONE;
@@ -1525,6 +1652,9 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        sigaction (SIGALRM, &action, NULL);
     }
 
+    if (query)
+       notmuch_query_destroy (query);
+
     talloc_free (local);
     return status;
 }
@@ -1536,10 +1666,13 @@ notmuch_database_begin_atomic (notmuch_database_t *notmuch)
        notmuch->atomic_nesting > 0)
        goto DONE;
 
+    if (notmuch_database_needs_upgrade (notmuch))
+       return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
     try {
        (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "A Xapian exception occurred beginning transaction: %s.\n",
+       _notmuch_database_log (notmuch, "A Xapian exception occurred beginning transaction: %s.\n",
                 error.get_msg().c_str());
        notmuch->exception_reported = TRUE;
        return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
@@ -1571,19 +1704,33 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch)
         * However, we rely on flushing to test atomicity. */
        const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
        if (thresh && atoi (thresh) == 1)
-           db->flush ();
+           db->commit ();
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "A Xapian exception occurred committing transaction: %s.\n",
+       _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n",
                 error.get_msg().c_str());
        notmuch->exception_reported = TRUE;
        return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
 
+    if (notmuch->atomic_dirty) {
+       ++notmuch->revision;
+       notmuch->atomic_dirty = FALSE;
+    }
+
 DONE:
     notmuch->atomic_nesting--;
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+unsigned long
+notmuch_database_get_revision (notmuch_database_t *notmuch,
+                               const char **uuid)
+{
+    if (uuid)
+       *uuid = notmuch->uuid;
+    return notmuch->revision;
+}
+
 /* We allow the user to use arbitrarily long paths for directories. But
  * we have a term-length limit. So if we exceed that, we'll use the
  * SHA-1 of the path for the database term.
@@ -1645,18 +1792,11 @@ _notmuch_database_split_path (void *ctx,
     slash = path + strlen (path) - 1;
 
     /* First, skip trailing slashes. */
-    while (slash != path) {
-       if (*slash != '/')
-           break;
-
+    while (slash != path && *slash == '/')
        --slash;
-    }
 
     /* Then, find a slash. */
-    while (slash != path) {
-       if (*slash == '/')
-           break;
-
+    while (slash != path && *slash != '/') {
        if (basename)
            *basename = slash;
 
@@ -1664,12 +1804,8 @@ _notmuch_database_split_path (void *ctx,
     }
 
     /* Finally, skip multiple slashes. */
-    while (slash != path) {
-       if (*slash != '/')
-           break;
-
+    while (slash != path && *(slash - 1) == '/')
        --slash;
-    }
 
     if (slash == path) {
        if (directory)
@@ -1678,7 +1814,7 @@ _notmuch_database_split_path (void *ctx,
            *basename = path;
     } else {
        if (directory)
-           *directory = talloc_strndup (ctx, path, slash - path + 1);
+           *directory = talloc_strndup (ctx, path, slash - path);
     }
 
     return NOTMUCH_STATUS_SUCCESS;
@@ -1819,7 +1955,7 @@ notmuch_database_get_directory (notmuch_database_t *notmuch,
        *directory = _notmuch_directory_create (notmuch, path,
                                                NOTMUCH_FIND_LOOKUP, &status);
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "A Xapian exception occurred getting directory: %s.\n",
+       _notmuch_database_log (notmuch, "A Xapian exception occurred getting directory: %s.\n",
                 error.get_msg().c_str());
        notmuch->exception_reported = TRUE;
        status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
@@ -2063,8 +2199,8 @@ _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
      * References header, if available.  If not, fall back to the
      * first message ID in the In-Reply-To header. */
     if (last_ref_message_id) {
-        _notmuch_message_add_term (message, "replyto",
-                                   last_ref_message_id);
+       _notmuch_message_add_term (message, "replyto",
+                                  last_ref_message_id);
     } else if (in_reply_to_message_id) {
        _notmuch_message_add_term (message, "replyto",
                             in_reply_to_message_id);
@@ -2173,15 +2309,15 @@ _consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch,
     if (stored_id.empty ()) {
        return NULL;
     } else {
-        Xapian::WritableDatabase *db;
+       Xapian::WritableDatabase *db;
 
        db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
 
        /* Clear the metadata for this message ID. We don't need it
         * anymore. */
-        db->set_metadata (metadata_key, "");
+       db->set_metadata (metadata_key, "");
 
-        return talloc_strdup (ctx, stored_id.c_str ());
+       return talloc_strdup (ctx, stored_id.c_str ());
     }
 }
 
@@ -2292,7 +2428,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
     if (ret)
        return ret;
 
-    message_file = _notmuch_message_file_open (filename);
+    message_file = _notmuch_message_file_open (notmuch, filename);
     if (message_file == NULL)
        return NOTMUCH_STATUS_FILE_ERROR;
 
@@ -2401,7 +2537,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
 
        _notmuch_message_sync (message);
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
+       _notmuch_database_log (notmuch, "A Xapian exception occurred adding message: %s.\n",
                 error.get_msg().c_str());
        notmuch->exception_reported = TRUE;
        ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
@@ -2493,7 +2629,7 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
                status = NOTMUCH_STATUS_OUT_OF_MEMORY;
        }
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "Error: A Xapian exception occurred finding message by filename: %s\n",
+       _notmuch_database_log (notmuch, "Error: A Xapian exception occurred finding message by filename: %s\n",
                 error.get_msg().c_str());
        notmuch->exception_reported = TRUE;
        status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
@@ -2546,9 +2682,15 @@ notmuch_database_get_all_tags (notmuch_database_t *db)
        _notmuch_string_list_sort (tags);
        return _notmuch_tags_create (db, tags);
     } catch (const Xapian::Error &error) {
-       fprintf (stderr, "A Xapian exception occurred getting tags: %s.\n",
+       _notmuch_database_log (db, "A Xapian exception occurred getting tags: %s.\n",
                 error.get_msg().c_str());
        db->exception_reported = TRUE;
        return NULL;
     }
 }
+
+const char *
+notmuch_database_status_string (const notmuch_database_t *notmuch)
+{
+    return notmuch->status_string;
+}