[PATCH v4 02/11] lib: Database version 3: Introduce fine-grained "features"
authorAustin Clements <amdragon@mit.edu>
Mon, 25 Aug 2014 17:26:00 +0000 (13:26 +2000)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 18:04:18 +0000 (10:04 -0800)
e4/567c28326981e9150d97a9a7224bd4db3233ef [new file with mode: 0644]

diff --git a/e4/567c28326981e9150d97a9a7224bd4db3233ef b/e4/567c28326981e9150d97a9a7224bd4db3233ef
new file mode 100644 (file)
index 0000000..8e42357
--- /dev/null
@@ -0,0 +1,553 @@
+Return-Path: <amdragon@mit.edu>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id B4619431FBD\r
+       for <notmuch@notmuchmail.org>; Mon, 25 Aug 2014 10:28:56 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -2.3\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-2.3 tagged_above=-999 required=5\r
+       tests=[RCVD_IN_DNSWL_MED=-2.3] autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id 2rkgTrINkSl2 for <notmuch@notmuchmail.org>;\r
+       Mon, 25 Aug 2014 10:28:49 -0700 (PDT)\r
+Received: from dmz-mailsec-scanner-4.mit.edu (dmz-mailsec-scanner-4.mit.edu\r
+       [18.9.25.15])\r
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))\r
+       (No client certificate requested)\r
+       by olra.theworths.org (Postfix) with ESMTPS id 7901A431FC2\r
+       for <notmuch@notmuchmail.org>; Mon, 25 Aug 2014 10:28:21 -0700 (PDT)\r
+X-AuditID: 1209190f-f79aa6d000005b45-b5-53fb7234b52e\r
+Received: from mailhub-auth-4.mit.edu ( [18.7.62.39])\r
+       (using TLS with cipher AES256-SHA (256/256 bits))\r
+       (Client did not present a certificate)\r
+       by dmz-mailsec-scanner-4.mit.edu (Symantec Messaging Gateway) with SMTP\r
+       id 16.D9.23365.4327BF35; Mon, 25 Aug 2014 13:28:20 -0400 (EDT)\r
+Received: from outgoing.mit.edu (outgoing-auth-1.mit.edu [18.9.28.11])\r
+       by mailhub-auth-4.mit.edu (8.13.8/8.9.2) with ESMTP id s7PHQEx3001075; \r
+       Mon, 25 Aug 2014 13:26:15 -0400\r
+Received: from drake.dyndns.org (31-35-14.wireless.csail.mit.edu\r
+       [128.31.35.14]) (authenticated bits=0)\r
+       (User authenticated as amdragon@ATHENA.MIT.EDU)\r
+       by outgoing.mit.edu (8.13.8/8.12.4) with ESMTP id s7PHQBNZ029607\r
+       (version=TLSv1/SSLv3 cipher=AES256-SHA bits=256 verify=NOT);\r
+       Mon, 25 Aug 2014 13:26:13 -0400\r
+Received: from amthrax by drake.dyndns.org with local (Exim 4.77)\r
+       (envelope-from <amdragon@mit.edu>)\r
+       id 1XLy1z-0003jM-TA; Mon, 25 Aug 2014 13:26:11 -0400\r
+From: Austin Clements <amdragon@mit.edu>\r
+To: notmuch@notmuchmail.org\r
+Subject: [PATCH v4 02/11] lib: Database version 3: Introduce fine-grained\r
+       "features"\r
+Date: Mon, 25 Aug 2014 13:26:00 -0400\r
+Message-Id: <1408987569-14146-3-git-send-email-amdragon@mit.edu>\r
+X-Mailer: git-send-email 2.0.0\r
+In-Reply-To: <1408987569-14146-1-git-send-email-amdragon@mit.edu>\r
+References: <1408987569-14146-1-git-send-email-amdragon@mit.edu>\r
+X-Brightmail-Tracker:\r
+ H4sIAAAAAAAAA+NgFjrBIsWRmVeSWpSXmKPExsUixG6nrmtS9DvY4NY5fYsbrd2MFk3TnS2u\r
+       35zJ7MDscev+a3aPZ6tuMXtsOfSeOYA5issmJTUnsyy1SN8ugSuj6chutoLH3YwVC/9dZW5g\r
+       7MjtYuTkkBAwkWjq/skMYYtJXLi3nq2LkYtDSGA2k8Tk4zdZIZyNjBLLNtxkgXCOMUnMevGU\r
+       HcKZyyixvHcRI0g/m4CGxO9bi5lAbBEBaYmdd2ezgtjMAl4Syy70g9nCAiESt9pmsIHYLAKq\r
+       EnvuXgPr5RVwkLj5o5kF4g45iYYbn8BqOAUcJaYvnA02Uwio5vvl88wTGPkXMDKsYpRNya3S\r
+       zU3MzClOTdYtTk7My0st0jXRy80s0UtNKd3ECAowTkn+HYzfDiodYhTgYFTi4b0R/ztYiDWx\r
+       rLgy9xCjJAeTkijvtHygEF9SfkplRmJxRnxRaU5q8SFGCQ5mJRHeZpAcb0piZVVqUT5MSpqD\r
+       RUmc9621VbCQQHpiSWp2ampBahFMVoaDQ0mC934BUKNgUWp6akVaZk4JQpqJgxNkOA/QcP9C\r
+       kOHFBYm5xZnpEPlTjIpS4ryGIAkBkERGaR5cLywBvGIUB3pFmNcGpIoHmDzgul8BDWYCGmza\r
+       8xNkcEkiQkqqgbGqMGdlybb8BTcSb8xR/rtqn7ORgfDUKR3nNn9b83T71iOxs9h3/Ar99cl6\r
+       8q+FOtafXp95qphRa/0m0F+5n2GPCEPkv6j6nfOPl+6yvSXRFz89eWsY1+VfduJt/r8mikjN\r
+       tV7rvSPuk8qH5JUM3bt7pbbvUhOp+s8rUfypruVOS8/W9r4NLNuUWIozEg21mIuKEwGTgOK5\r
+       2wIAAA==\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Mon, 25 Aug 2014 17:28:57 -0000\r
+\r
+Previously, our database schema was versioned by a single number.\r
+Each database schema change had to occur "atomically" in Notmuch's\r
+development history: before some commit, Notmuch used version N, after\r
+that commit, it used version N+1.  Hence, each new schema version\r
+could introduce only one change, the task of developing a schema\r
+change fell on a single person, and it all had to happen and be\r
+perfect in a single commit series.  This made introducing a new schema\r
+version hard.  We've seen only two schema changes in the history of\r
+Notmuch.\r
+\r
+This commit introduces database schema version 3; hopefully the last\r
+schema version we'll need for a while.  With this version, we switch\r
+from a single version number to "features": a set of named,\r
+independent aspects of the database schema.\r
+\r
+Features should make backwards compatibility easier.  For many things,\r
+it should be easy to support databases both with and without a\r
+feature, which will allow us to make upgrades optional and will enable\r
+"unstable" features that can be developed and tested over time.\r
+\r
+Features also make forwards compatibility easier.  The features\r
+recorded in a database include "compatibility flags," which can\r
+indicate to an older version of Notmuch when it must support a given\r
+feature to open the database for read or for write.  This lets us\r
+replace the old vague "I don't recognize this version, so something\r
+might go wrong, but I promise to try my best" warnings upon opening a\r
+database with an unknown version with precise errors.  If a database\r
+is safe to open for read/write despite unknown features, an older\r
+version will know that and issue no message at all.  If the database\r
+is not safe to open for read/write because of unknown features, an\r
+older version will know that, too, and can tell the user exactly which\r
+required features it lacks support for.\r
+---\r
+ lib/database-private.h | 108 ++++++++++++++++++++++++++-\r
+ lib/database.cc        | 197 ++++++++++++++++++++++++++++++++++++++++---------\r
+ 2 files changed, 271 insertions(+), 34 deletions(-)\r
+\r
+diff --git a/lib/database-private.h b/lib/database-private.h\r
+index d3e65fd..ca0751c 100644\r
+--- a/lib/database-private.h\r
++++ b/lib/database-private.h\r
+@@ -36,16 +36,106 @@\r
\r
+ #pragma GCC visibility push(hidden)\r
\r
++/* Bit masks for _notmuch_database::features.  Features are named,\r
++ * independent aspects of the database schema.\r
++ *\r
++ * A database stores the set of features that it "uses" (implicitly\r
++ * before database version 3 and explicitly as of version 3).\r
++ *\r
++ * A given library version will "recognize" a particular set of\r
++ * features; if a database uses a feature that the library does not\r
++ * recognize, the library will refuse to open it.  It is assumed the\r
++ * set of recognized features grows monotonically over time.  A\r
++ * library version will "implement" some subset of the recognized\r
++ * features: some operations may require that the database use (or not\r
++ * use) some feature, while other operations may support both\r
++ * databases that use and that don't use some feature.\r
++ *\r
++ * On disk, the database stores string names for these features (see\r
++ * the feature_names array).  These enum bit values are never\r
++ * persisted to disk and may change freely.\r
++ */\r
++enum _notmuch_features {\r
++    /* If set, file names are stored in "file-direntry" terms.  If\r
++     * unset, file names are stored in document data.\r
++     *\r
++     * Introduced: version 1. */\r
++    NOTMUCH_FEATURE_FILE_TERMS = 1 << 0,\r
++\r
++    /* If set, directory timestamps are stored in documents with\r
++     * XDIRECTORY terms and relative paths.  If unset, directory\r
++     * timestamps are stored in documents with XTIMESTAMP terms and\r
++     * absolute paths.\r
++     *\r
++     * Introduced: version 1. */\r
++    NOTMUCH_FEATURE_DIRECTORY_DOCS = 1 << 1,\r
++\r
++    /* If set, the from, subject, and message-id headers are stored in\r
++     * message document values.  If unset, message documents *may*\r
++     * have these values, but if the value is empty, it must be\r
++     * retrieved from the message file.\r
++     *\r
++     * Introduced: optional in version 1, required as of version 3.\r
++     */\r
++    NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES = 1 << 2,\r
++\r
++    /* If set, folder terms are boolean and path terms exist.  If\r
++     * unset, folder terms are probabilistic and stemmed and path\r
++     * terms do not exist.\r
++     *\r
++     * Introduced: version 2. */\r
++    NOTMUCH_FEATURE_BOOL_FOLDER = 1 << 3,\r
++};\r
++\r
++/* In C++, a named enum is its own type, so define bitwise operators\r
++ * on _notmuch_features. */\r
++inline _notmuch_features\r
++operator|(_notmuch_features a, _notmuch_features b)\r
++{\r
++    return static_cast<_notmuch_features>(\r
++      static_cast<unsigned>(a) | static_cast<unsigned>(b));\r
++}\r
++\r
++inline _notmuch_features\r
++operator&(_notmuch_features a, _notmuch_features b)\r
++{\r
++    return static_cast<_notmuch_features>(\r
++      static_cast<unsigned>(a) & static_cast<unsigned>(b));\r
++}\r
++\r
++inline _notmuch_features\r
++operator~(_notmuch_features a)\r
++{\r
++    return static_cast<_notmuch_features>(~static_cast<unsigned>(a));\r
++}\r
++\r
++inline _notmuch_features&\r
++operator|=(_notmuch_features &a, _notmuch_features b)\r
++{\r
++    a = a | b;\r
++    return a;\r
++}\r
++\r
++inline _notmuch_features&\r
++operator&=(_notmuch_features &a, _notmuch_features b)\r
++{\r
++    a = a & b;\r
++    return a;\r
++}\r
++\r
+ struct _notmuch_database {\r
+     notmuch_bool_t exception_reported;\r
\r
+     char *path;\r
\r
+-    notmuch_bool_t needs_upgrade;\r
+     notmuch_database_mode_t mode;\r
+     int atomic_nesting;\r
+     Xapian::Database *xapian_db;\r
\r
++    /* Bit mask of features used by this database.  This is a\r
++     * bitwise-OR of NOTMUCH_FEATURE_* values (above). */\r
++    enum _notmuch_features features;\r
++\r
+     unsigned int last_doc_id;\r
+     uint64_t last_thread_id;\r
\r
+@@ -55,6 +145,22 @@ struct _notmuch_database {\r
+     Xapian::ValueRangeProcessor *date_range_processor;\r
+ };\r
\r
++/* Prior to database version 3, features were implied by the database\r
++ * version number, so hard-code them for earlier versions. */\r
++#define NOTMUCH_FEATURES_V0 ((enum _notmuch_features)0)\r
++#define NOTMUCH_FEATURES_V1 (NOTMUCH_FEATURES_V0 | NOTMUCH_FEATURE_FILE_TERMS | \\r
++                           NOTMUCH_FEATURE_DIRECTORY_DOCS)\r
++#define NOTMUCH_FEATURES_V2 (NOTMUCH_FEATURES_V1 | NOTMUCH_FEATURE_BOOL_FOLDER)\r
++\r
++/* Current database features.  If any of these are missing from a\r
++ * database, request an upgrade.\r
++ * NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES is not included because\r
++ * upgrade doesn't currently introduce the feature (though brand new\r
++ * databases will have it). */\r
++#define NOTMUCH_FEATURES_CURRENT \\r
++    (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_DIRECTORY_DOCS | \\r
++     NOTMUCH_FEATURE_BOOL_FOLDER)\r
++\r
+ /* Return the list of terms from the given iterator matching a prefix.\r
+  * The prefix will be stripped from the strings in the returned list.\r
+  * The list will be allocated using ctx as the talloc context.\r
+diff --git a/lib/database.cc b/lib/database.cc\r
+index 9c0952a..2b566f7 100644\r
+--- a/lib/database.cc\r
++++ b/lib/database.cc\r
+@@ -20,6 +20,7 @@\r
\r
+ #include "database-private.h"\r
+ #include "parse-time-vrp.h"\r
++#include "string-util.h"\r
\r
+ #include <iostream>\r
\r
+@@ -42,7 +43,7 @@ typedef struct {\r
+     const char *prefix;\r
+ } prefix_t;\r
\r
+-#define NOTMUCH_DATABASE_VERSION 2\r
++#define NOTMUCH_DATABASE_VERSION 3\r
\r
+ #define STRINGIFY(s) _SUB_STRINGIFY(s)\r
+ #define _SUB_STRINGIFY(s) #s\r
+@@ -154,6 +155,17 @@ typedef struct {\r
+  *                    changes are made to the database (such as by\r
+  *                    indexing new fields).\r
+  *\r
++ *    features        The set of features supported by this\r
++ *                    database. This consists of a set of\r
++ *                    '\n'-separated lines, where each is a feature\r
++ *                    name, a '\t', and compatibility flags.  If the\r
++ *                    compatibility flags contain 'w', then the\r
++ *                    opener must support this feature to safely\r
++ *                    write this database.  If the compatibility\r
++ *                    flags contain 'r', then the opener must\r
++ *                    support this feature to read this database.\r
++ *                    Introduced in database version 3.\r
++ *\r
+  *    last_thread_id  The last thread ID generated. This is stored\r
+  *                    as a 16-byte hexadecimal ASCII representation\r
+  *                    of a 64-bit unsigned integer. The first ID\r
+@@ -254,6 +266,28 @@ _find_prefix (const char *name)\r
+     return "";\r
+ }\r
\r
++static const struct {\r
++    /* NOTMUCH_FEATURE_* value. */\r
++    _notmuch_features value;\r
++    /* Feature name as it appears in the database.  This name should\r
++     * be appropriate for displaying to the user if an older version\r
++     * of notmuch doesn't support this feature. */\r
++    const char *name;\r
++    /* Compatibility flags when this feature is declared. */\r
++    const char *flags;\r
++} feature_names[] = {\r
++    { NOTMUCH_FEATURE_FILE_TERMS,\r
++      "multiple paths per message", "rw" },\r
++    { NOTMUCH_FEATURE_DIRECTORY_DOCS,\r
++      "relative directory paths", "rw" },\r
++    /* Header values are not required for reading a database because a\r
++     * reader can just refer to the message file. */\r
++    { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES,\r
++      "from/subject/message-ID in database", "w" },\r
++    { NOTMUCH_FEATURE_BOOL_FOLDER,\r
++      "exact folder:/path: search", "rw" },\r
++};\r
++\r
+ const char *\r
+ notmuch_status_to_string (notmuch_status_t status)\r
+ {\r
+@@ -591,6 +625,11 @@ notmuch_database_create (const char *path, notmuch_database_t **database)\r
+                                   &notmuch);\r
+     if (status)\r
+       goto DONE;\r
++\r
++    /* Upgrade doesn't add this feature to existing databases, but new\r
++     * databases have it. */\r
++    notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES;\r
++\r
+     status = notmuch_database_upgrade (notmuch, NULL, NULL);\r
+     if (status) {\r
+       notmuch_database_close(notmuch);\r
+@@ -619,6 +658,83 @@ _notmuch_database_ensure_writable (notmuch_database_t *notmuch)\r
+     return NOTMUCH_STATUS_SUCCESS;\r
+ }\r
\r
++/* Parse a database features string from the given database version.\r
++ * Returns the feature bit set.\r
++ *\r
++ * For version < 3, this ignores the features string and returns a\r
++ * hard-coded set of features.\r
++ *\r
++ * If there are unrecognized features that are required to open the\r
++ * database in mode (which should be 'r' or 'w'), return a\r
++ * comma-separated list of unrecognized but required features in\r
++ * *incompat_out suitable for presenting to the user.  *incompat_out\r
++ * will be allocated from ctx.\r
++ */\r
++static _notmuch_features\r
++_parse_features (const void *ctx, const char *features, unsigned int version,\r
++               char mode, char **incompat_out)\r
++{\r
++    _notmuch_features res = static_cast<_notmuch_features>(0);\r
++    unsigned int namelen, i;\r
++    size_t llen = 0;\r
++    const char *flags;\r
++\r
++    /* Prior to database version 3, features were implied by the\r
++     * version number. */\r
++    if (version == 0)\r
++      return NOTMUCH_FEATURES_V0;\r
++    else if (version == 1)\r
++      return NOTMUCH_FEATURES_V1;\r
++    else if (version == 2)\r
++      return NOTMUCH_FEATURES_V2;\r
++\r
++    /* Parse the features string */\r
++    while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) {\r
++      flags = strchr (features, '\t');\r
++      if (! flags || flags > features + llen)\r
++          continue;\r
++      namelen = flags - features;\r
++\r
++      for (i = 0; i < ARRAY_SIZE (feature_names); ++i) {\r
++          if (strlen (feature_names[i].name) == namelen &&\r
++              strncmp (feature_names[i].name, features, namelen) == 0) {\r
++              res |= feature_names[i].value;\r
++              break;\r
++          }\r
++      }\r
++\r
++      if (i == ARRAY_SIZE (feature_names) && incompat_out) {\r
++          /* Unrecognized feature */\r
++          const char *have = strchr (flags, mode);\r
++          if (have && have < features + llen) {\r
++              /* This feature is required to access this database in\r
++               * 'mode', but we don't understand it. */\r
++              if (! *incompat_out)\r
++                  *incompat_out = talloc_strdup (ctx, "");\r
++              *incompat_out = talloc_asprintf_append_buffer (\r
++                  *incompat_out, "%s%.*s", **incompat_out ? ", " : "",\r
++                  namelen, features);\r
++          }\r
++      }\r
++    }\r
++\r
++    return res;\r
++}\r
++\r
++static char *\r
++_print_features (const void *ctx, unsigned int features)\r
++{\r
++    unsigned int i;\r
++    char *res = talloc_strdup (ctx, "");\r
++\r
++    for (i = 0; i < ARRAY_SIZE (feature_names); ++i)\r
++      if (features & feature_names[i].value)\r
++          res = talloc_asprintf_append_buffer (\r
++              res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags);\r
++\r
++    return res;\r
++}\r
++\r
+ notmuch_status_t\r
+ notmuch_database_open (const char *path,\r
+                      notmuch_database_mode_t mode,\r
+@@ -627,7 +743,7 @@ notmuch_database_open (const char *path,\r
+     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;\r
+     void *local = talloc_new (NULL);\r
+     notmuch_database_t *notmuch = NULL;\r
+-    char *notmuch_path, *xapian_path;\r
++    char *notmuch_path, *xapian_path, *incompat_features;\r
+     struct stat st;\r
+     int err;\r
+     unsigned int i, version;\r
+@@ -677,7 +793,6 @@ notmuch_database_open (const char *path,\r
+     if (notmuch->path[strlen (notmuch->path) - 1] == '/')\r
+       notmuch->path[strlen (notmuch->path) - 1] = '\0';\r
\r
+-    notmuch->needs_upgrade = FALSE;\r
+     notmuch->mode = mode;\r
+     notmuch->atomic_nesting = 0;\r
+     try {\r
+@@ -686,37 +801,44 @@ notmuch_database_open (const char *path,\r
+       if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {\r
+           notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,\r
+                                                              Xapian::DB_CREATE_OR_OPEN);\r
+-          version = notmuch_database_get_version (notmuch);\r
+-\r
+-          if (version > NOTMUCH_DATABASE_VERSION) {\r
+-              fprintf (stderr,\r
+-                       "Error: Notmuch database at %s\n"\r
+-                       "       has a newer database format version (%u) than supported by this\n"\r
+-                       "       version of notmuch (%u). Refusing to open this database in\n"\r
+-                       "       read-write mode.\n",\r
+-                       notmuch_path, version, NOTMUCH_DATABASE_VERSION);\r
+-              notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;\r
+-              notmuch_database_destroy (notmuch);\r
+-              notmuch = NULL;\r
+-              status = NOTMUCH_STATUS_FILE_ERROR;\r
+-              goto DONE;\r
+-          }\r
+-\r
+-          if (version < NOTMUCH_DATABASE_VERSION)\r
+-              notmuch->needs_upgrade = TRUE;\r
+       } else {\r
+           notmuch->xapian_db = new Xapian::Database (xapian_path);\r
+-          version = notmuch_database_get_version (notmuch);\r
+-          if (version > NOTMUCH_DATABASE_VERSION)\r
+-          {\r
+-              fprintf (stderr,\r
+-                       "Warning: Notmuch database at %s\n"\r
+-                       "         has a newer database format version (%u) than supported by this\n"\r
+-                       "         version of notmuch (%u). Some operations may behave incorrectly,\n"\r
+-                       "         (but the database will not be harmed since it is being opened\n"\r
+-                       "         in read-only mode).\n",\r
+-                       notmuch_path, version, NOTMUCH_DATABASE_VERSION);\r
+-          }\r
++      }\r
++\r
++      /* Check version.  As of database version 3, we represent\r
++       * changes in terms of features, so assume a version bump\r
++       * means a dramatically incompatible change. */\r
++      version = notmuch_database_get_version (notmuch);\r
++      if (version > NOTMUCH_DATABASE_VERSION) {\r
++          fprintf (stderr,\r
++                   "Error: Notmuch database at %s\n"\r
++                   "       has a newer database format version (%u) than supported by this\n"\r
++                   "       version of notmuch (%u).\n",\r
++                   notmuch_path, version, NOTMUCH_DATABASE_VERSION);\r
++          notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;\r
++          notmuch_database_destroy (notmuch);\r
++          notmuch = NULL;\r
++          status = NOTMUCH_STATUS_FILE_ERROR;\r
++          goto DONE;\r
++      }\r
++\r
++      /* Check features. */\r
++      incompat_features = NULL;\r
++      notmuch->features = _parse_features (\r
++          local, notmuch->xapian_db->get_metadata ("features").c_str (),\r
++          version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',\r
++          &incompat_features);\r
++      if (incompat_features) {\r
++          fprintf (stderr,\r
++                   "Error: Notmuch database at %s\n"\r
++                   "       requires features (%s)\n"\r
++                   "       not supported by this version of notmuch.\n",\r
++                   notmuch_path, incompat_features);\r
++          notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;\r
++          notmuch_database_destroy (notmuch);\r
++          notmuch = NULL;\r
++          status = NOTMUCH_STATUS_FILE_ERROR;\r
++          goto DONE;\r
+       }\r
\r
+       notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();\r
+@@ -1048,7 +1170,9 @@ notmuch_database_get_version (notmuch_database_t *notmuch)\r
+ notmuch_bool_t\r
+ notmuch_database_needs_upgrade (notmuch_database_t *notmuch)\r
+ {\r
+-    return notmuch->needs_upgrade;\r
++    return notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE &&\r
++      ((NOTMUCH_FEATURES_CURRENT & ~notmuch->features) ||\r
++       (notmuch_database_get_version (notmuch) < NOTMUCH_DATABASE_VERSION));\r
+ }\r
\r
+ static volatile sig_atomic_t do_progress_notify = 0;\r
+@@ -1077,6 +1201,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,\r
+                                                  double progress),\r
+                         void *closure)\r
+ {\r
++    void *local = talloc_new (NULL);\r
+     Xapian::WritableDatabase *db;\r
+     struct sigaction action;\r
+     struct itimerval timerval;\r
+@@ -1114,6 +1239,10 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,\r
+       timer_is_active = TRUE;\r
+     }\r
\r
++    /* Set the target features so we write out changes in the desired\r
++     * format. */\r
++    notmuch->features |= NOTMUCH_FEATURES_CURRENT;\r
++\r
+     /* Before version 1, each message document had its filename in the\r
+      * data field. Copy that into the new format by calling\r
+      * notmuch_message_add_filename.\r
+@@ -1226,6 +1355,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,\r
+       notmuch_query_destroy (query);\r
+     }\r
\r
++    db->set_metadata ("features", _print_features (local, notmuch->features));\r
+     db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));\r
+     db->flush ();\r
\r
+@@ -1302,6 +1432,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,\r
+       sigaction (SIGALRM, &action, NULL);\r
+     }\r
\r
++    talloc_free (local);\r
+     return NOTMUCH_STATUS_SUCCESS;\r
+ }\r
\r
+-- \r
+2.0.0\r
+\r