mirror of
https://git.notmuchmail.org/git/notmuch
synced 2024-11-25 04:18:08 +01:00
lib: Implement versioning in the database and provide upgrade function.
The recent support for renames in the database is our first time (since notmuch has had more than a single user) that we have a database format change. To support smooth upgrades we now encode a database format version number in the Xapian metadata. Going forward notmuch will emit a warning if used to read from a database with a newer version than it natively supports, and will refuse to write to a database with a newer version. The library also provides functions to query the database format version: notmuch_database_get_version to ask if notmuch wants a newer version than that: notmuch_database_needs_upgrade and a function to actually perform that upgrade: notmuch_database_upgrade
This commit is contained in:
parent
21f8fd6967
commit
909f52bd8c
5 changed files with 274 additions and 8 deletions
|
@ -33,6 +33,8 @@ struct _notmuch_database {
|
||||||
Xapian::QueryParser *query_parser;
|
Xapian::QueryParser *query_parser;
|
||||||
Xapian::TermGenerator *term_gen;
|
Xapian::TermGenerator *term_gen;
|
||||||
Xapian::ValueRangeProcessor *value_range_processor;
|
Xapian::ValueRangeProcessor *value_range_processor;
|
||||||
|
|
||||||
|
notmuch_bool_t needs_upgrade;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Convert tags from Xapian internal format to notmuch format.
|
/* Convert tags from Xapian internal format to notmuch format.
|
||||||
|
|
216
lib/database.cc
216
lib/database.cc
|
@ -22,6 +22,8 @@
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <xapian.h>
|
#include <xapian.h>
|
||||||
|
|
||||||
#include <glib.h> /* g_free, GPtrArray, GHashTable */
|
#include <glib.h> /* g_free, GPtrArray, GHashTable */
|
||||||
|
@ -35,7 +37,12 @@ typedef struct {
|
||||||
const char *prefix;
|
const char *prefix;
|
||||||
} prefix_t;
|
} prefix_t;
|
||||||
|
|
||||||
/* Here's the current schema for our database:
|
#define NOTMUCH_DATABASE_VERSION 1
|
||||||
|
|
||||||
|
#define STRINGIFY(s) _SUB_STRINGIFY(s)
|
||||||
|
#define _SUB_STRINGIFY(s) #s
|
||||||
|
|
||||||
|
/* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
|
||||||
*
|
*
|
||||||
* We currently have two different types of documents: mail and directory.
|
* We currently have two different types of documents: mail and directory.
|
||||||
*
|
*
|
||||||
|
@ -467,6 +474,7 @@ notmuch_database_create (const char *path)
|
||||||
|
|
||||||
notmuch = notmuch_database_open (path,
|
notmuch = notmuch_database_open (path,
|
||||||
NOTMUCH_DATABASE_MODE_READ_WRITE);
|
NOTMUCH_DATABASE_MODE_READ_WRITE);
|
||||||
|
notmuch_database_upgrade (notmuch, NULL, NULL);
|
||||||
|
|
||||||
DONE:
|
DONE:
|
||||||
if (notmuch_path)
|
if (notmuch_path)
|
||||||
|
@ -494,7 +502,7 @@ notmuch_database_open (const char *path,
|
||||||
char *notmuch_path = NULL, *xapian_path = NULL;
|
char *notmuch_path = NULL, *xapian_path = NULL;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
int err;
|
int err;
|
||||||
unsigned int i;
|
unsigned int i, version;
|
||||||
|
|
||||||
if (asprintf (¬much_path, "%s/%s", path, ".notmuch") == -1) {
|
if (asprintf (¬much_path, "%s/%s", path, ".notmuch") == -1) {
|
||||||
notmuch_path = NULL;
|
notmuch_path = NULL;
|
||||||
|
@ -522,13 +530,42 @@ notmuch_database_open (const char *path,
|
||||||
if (notmuch->path[strlen (notmuch->path) - 1] == '/')
|
if (notmuch->path[strlen (notmuch->path) - 1] == '/')
|
||||||
notmuch->path[strlen (notmuch->path) - 1] = '\0';
|
notmuch->path[strlen (notmuch->path) - 1] = '\0';
|
||||||
|
|
||||||
|
notmuch->needs_upgrade = FALSE;
|
||||||
notmuch->mode = mode;
|
notmuch->mode = mode;
|
||||||
try {
|
try {
|
||||||
if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
|
if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
|
||||||
notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
|
notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
|
||||||
Xapian::DB_CREATE_OR_OPEN);
|
Xapian::DB_CREATE_OR_OPEN);
|
||||||
|
version = notmuch_database_get_version (notmuch);
|
||||||
|
|
||||||
|
if (version > NOTMUCH_DATABASE_VERSION) {
|
||||||
|
fprintf (stderr,
|
||||||
|
"Error: Notmuch database at %s\n"
|
||||||
|
" has a newer database format version (%u) than supported by this\n"
|
||||||
|
" version of notmuch (%u). Refusing to open this database in\n"
|
||||||
|
" read-write mode.\n",
|
||||||
|
notmuch_path, version, NOTMUCH_DATABASE_VERSION);
|
||||||
|
notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
|
||||||
|
notmuch_database_close (notmuch);
|
||||||
|
notmuch = NULL;
|
||||||
|
goto DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < NOTMUCH_DATABASE_VERSION)
|
||||||
|
notmuch->needs_upgrade = TRUE;
|
||||||
} else {
|
} else {
|
||||||
notmuch->xapian_db = new Xapian::Database (xapian_path);
|
notmuch->xapian_db = new Xapian::Database (xapian_path);
|
||||||
|
version = notmuch_database_get_version (notmuch);
|
||||||
|
if (version > NOTMUCH_DATABASE_VERSION)
|
||||||
|
{
|
||||||
|
fprintf (stderr,
|
||||||
|
"Warning: Notmuch database at %s\n"
|
||||||
|
" has a newer database format version (%u) than supported by this\n"
|
||||||
|
" version of notmuch (%u). Some operations may behave incorrectly,\n"
|
||||||
|
" (but the database will not be harmed since it is being opened\n"
|
||||||
|
" in read-only mode).\n",
|
||||||
|
notmuch_path, version, NOTMUCH_DATABASE_VERSION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notmuch->query_parser = new Xapian::QueryParser;
|
notmuch->query_parser = new Xapian::QueryParser;
|
||||||
notmuch->term_gen = new Xapian::TermGenerator;
|
notmuch->term_gen = new Xapian::TermGenerator;
|
||||||
|
@ -592,6 +629,181 @@ notmuch_database_get_path (notmuch_database_t *notmuch)
|
||||||
return notmuch->path;
|
return notmuch->path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned int
|
||||||
|
notmuch_database_get_version (notmuch_database_t *notmuch)
|
||||||
|
{
|
||||||
|
unsigned int version;
|
||||||
|
string version_string;
|
||||||
|
const char *str;
|
||||||
|
char *end;
|
||||||
|
|
||||||
|
version_string = notmuch->xapian_db->get_metadata ("version");
|
||||||
|
if (version_string.empty ())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
str = version_string.c_str ();
|
||||||
|
if (str == NULL || *str == '\0')
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
version = strtoul (str, &end, 10);
|
||||||
|
if (*end != '\0')
|
||||||
|
INTERNAL_ERROR ("Malformed database version: %s", str);
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
notmuch_bool_t
|
||||||
|
notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
|
||||||
|
{
|
||||||
|
return notmuch->needs_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
static volatile sig_atomic_t do_progress_notify = 0;
|
||||||
|
|
||||||
|
static void
|
||||||
|
handle_sigalrm (unused (int signal))
|
||||||
|
{
|
||||||
|
do_progress_notify = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upgrade the current database.
|
||||||
|
*
|
||||||
|
* After opening a database in read-write mode, the client should
|
||||||
|
* check if an upgrade is needed (notmuch_database_needs_upgrade) and
|
||||||
|
* if so, upgrade with this function before making any modifications.
|
||||||
|
*
|
||||||
|
* The optional progress_notify callback can be used by the caller to
|
||||||
|
* provide progress indication to the user. If non-NULL it will be
|
||||||
|
* called periodically with 'count' as the number of messages upgraded
|
||||||
|
* so far and 'total' the overall number of messages that will be
|
||||||
|
* converted.
|
||||||
|
*/
|
||||||
|
notmuch_status_t
|
||||||
|
notmuch_database_upgrade (notmuch_database_t *notmuch,
|
||||||
|
void (*progress_notify) (void *closure,
|
||||||
|
unsigned int count,
|
||||||
|
unsigned int total),
|
||||||
|
void *closure)
|
||||||
|
{
|
||||||
|
Xapian::WritableDatabase *db;
|
||||||
|
struct sigaction action;
|
||||||
|
struct itimerval timerval;
|
||||||
|
notmuch_bool_t timer_is_active = FALSE;
|
||||||
|
unsigned int version;
|
||||||
|
notmuch_status_t status;
|
||||||
|
|
||||||
|
status = _notmuch_database_ensure_writable (notmuch);
|
||||||
|
if (status)
|
||||||
|
return status;
|
||||||
|
|
||||||
|
db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
|
||||||
|
|
||||||
|
version = notmuch_database_get_version (notmuch);
|
||||||
|
|
||||||
|
if (version >= NOTMUCH_DATABASE_VERSION)
|
||||||
|
return NOTMUCH_STATUS_SUCCESS;
|
||||||
|
|
||||||
|
if (progress_notify) {
|
||||||
|
/* Setup our handler for SIGALRM */
|
||||||
|
memset (&action, 0, sizeof (struct sigaction));
|
||||||
|
action.sa_handler = handle_sigalrm;
|
||||||
|
sigemptyset (&action.sa_mask);
|
||||||
|
action.sa_flags = SA_RESTART;
|
||||||
|
sigaction (SIGALRM, &action, NULL);
|
||||||
|
|
||||||
|
/* Then start a timer to send SIGALRM once per second. */
|
||||||
|
timerval.it_interval.tv_sec = 1;
|
||||||
|
timerval.it_interval.tv_usec = 0;
|
||||||
|
timerval.it_value.tv_sec = 1;
|
||||||
|
timerval.it_value.tv_usec = 0;
|
||||||
|
setitimer (ITIMER_REAL, &timerval, NULL);
|
||||||
|
|
||||||
|
timer_is_active = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Before version 1, each message document had its filename in the
|
||||||
|
* data field. Move that into the new format by calling
|
||||||
|
* notmuch_message_add_filename.
|
||||||
|
*/
|
||||||
|
if (version < 1) {
|
||||||
|
unsigned int count = 0, total;
|
||||||
|
notmuch_query_t *query = notmuch_query_create (notmuch, "");
|
||||||
|
notmuch_messages_t *messages;
|
||||||
|
notmuch_message_t *message;
|
||||||
|
|
||||||
|
total = notmuch_query_count_messages (query);
|
||||||
|
|
||||||
|
for (messages = notmuch_query_search_messages (query);
|
||||||
|
notmuch_messages_has_more (messages);
|
||||||
|
notmuch_messages_advance (messages))
|
||||||
|
{
|
||||||
|
if (do_progress_notify)
|
||||||
|
progress_notify (closure, count, total);
|
||||||
|
|
||||||
|
message = notmuch_messages_get (messages);
|
||||||
|
|
||||||
|
_notmuch_message_upgrade_filename_storage (message);
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Also, before version 1 we stored directory timestamps in
|
||||||
|
* XTIMESTAMP documents instead of the current XDIRECTORY
|
||||||
|
* documents. So convert those as well. */
|
||||||
|
if (version < 1) {
|
||||||
|
Xapian::TermIterator t, t_end;
|
||||||
|
|
||||||
|
t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
|
||||||
|
|
||||||
|
for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
|
||||||
|
t != t_end;
|
||||||
|
t++)
|
||||||
|
{
|
||||||
|
Xapian::PostingIterator p, p_end;
|
||||||
|
std::string term = *t;
|
||||||
|
|
||||||
|
p_end = notmuch->xapian_db->postlist_end (term);
|
||||||
|
|
||||||
|
for (p = notmuch->xapian_db->postlist_begin (term);
|
||||||
|
p != p_end;
|
||||||
|
p++)
|
||||||
|
{
|
||||||
|
Xapian::Document document;
|
||||||
|
time_t mtime;
|
||||||
|
notmuch_directory_t *directory;
|
||||||
|
|
||||||
|
document = find_document_for_doc_id (notmuch, *p);
|
||||||
|
mtime = Xapian::sortable_unserialise (
|
||||||
|
document.get_value (NOTMUCH_VALUE_TIMESTAMP));
|
||||||
|
|
||||||
|
directory = notmuch_database_get_directory (notmuch,
|
||||||
|
term.c_str() + 10);
|
||||||
|
notmuch_directory_set_mtime (directory, mtime);
|
||||||
|
notmuch_directory_destroy (directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
|
||||||
|
db->flush ();
|
||||||
|
|
||||||
|
if (timer_is_active) {
|
||||||
|
/* Now stop the timer. */
|
||||||
|
timerval.it_interval.tv_sec = 0;
|
||||||
|
timerval.it_interval.tv_usec = 0;
|
||||||
|
timerval.it_value.tv_sec = 0;
|
||||||
|
timerval.it_value.tv_usec = 0;
|
||||||
|
setitimer (ITIMER_REAL, &timerval, NULL);
|
||||||
|
|
||||||
|
/* And disable the signal handler. */
|
||||||
|
action.sa_handler = SIG_IGN;
|
||||||
|
sigaction (SIGALRM, &action, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NOTMUCH_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
/* We allow the user to use arbitrarily long paths for directories. But
|
/* 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
|
* we have a term-length limit. So if we exceed that, we'll use the
|
||||||
* SHA-1 of the path for the database term.
|
* SHA-1 of the path for the database term.
|
||||||
|
|
|
@ -416,6 +416,24 @@ _notmuch_message_add_filename (notmuch_message_t *message,
|
||||||
return NOTMUCH_STATUS_SUCCESS;
|
return NOTMUCH_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Move the filename from the data field (as it was in database format
|
||||||
|
* version 0) to a file-direntry term instead (as in database format
|
||||||
|
* version 1).
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
_notmuch_message_upgrade_filename_storage (notmuch_message_t *message)
|
||||||
|
{
|
||||||
|
char *filename;
|
||||||
|
|
||||||
|
filename = talloc_strdup (message, message->doc.get_data ().c_str ());
|
||||||
|
if (filename && *filename != '\0') {
|
||||||
|
_notmuch_message_add_filename (message, filename);
|
||||||
|
message->doc.set_data ("");
|
||||||
|
_notmuch_message_sync (message);
|
||||||
|
}
|
||||||
|
talloc_free (filename);
|
||||||
|
}
|
||||||
|
|
||||||
const char *
|
const char *
|
||||||
notmuch_message_get_filename (notmuch_message_t *message)
|
notmuch_message_get_filename (notmuch_message_t *message)
|
||||||
{
|
{
|
||||||
|
@ -441,7 +459,10 @@ notmuch_message_get_filename (notmuch_message_t *message)
|
||||||
{
|
{
|
||||||
/* A message document created by an old version of notmuch
|
/* A message document created by an old version of notmuch
|
||||||
* (prior to rename support) will have the filename in the
|
* (prior to rename support) will have the filename in the
|
||||||
* data of the document rather than as a file-direntry term. */
|
* data of the document rather than as a file-direntry term.
|
||||||
|
*
|
||||||
|
* It would be nice to do the upgrade of the document directly
|
||||||
|
* here, but the database is likely open in read-only mode. */
|
||||||
const char *data;
|
const char *data;
|
||||||
|
|
||||||
data = message->doc.get_data ().c_str ();
|
data = message->doc.get_data ().c_str ();
|
||||||
|
|
|
@ -238,6 +238,9 @@ _notmuch_message_gen_terms (notmuch_message_t *message,
|
||||||
const char *prefix_name,
|
const char *prefix_name,
|
||||||
const char *text);
|
const char *text);
|
||||||
|
|
||||||
|
void
|
||||||
|
_notmuch_message_upgrade_filename_storage (notmuch_message_t *message);
|
||||||
|
|
||||||
notmuch_status_t
|
notmuch_status_t
|
||||||
_notmuch_message_add_filename (notmuch_message_t *message,
|
_notmuch_message_add_filename (notmuch_message_t *message,
|
||||||
const char *filename);
|
const char *filename);
|
||||||
|
|
|
@ -183,15 +183,43 @@ notmuch_database_close (notmuch_database_t *database);
|
||||||
const char *
|
const char *
|
||||||
notmuch_database_get_path (notmuch_database_t *database);
|
notmuch_database_get_path (notmuch_database_t *database);
|
||||||
|
|
||||||
|
/* Return the database format version of the given database. */
|
||||||
|
unsigned int
|
||||||
|
notmuch_database_get_version (notmuch_database_t *database);
|
||||||
|
|
||||||
|
/* Does this database need to be upgraded before writing to it?
|
||||||
|
*
|
||||||
|
* If this function returns TRUE then no functions that modify the
|
||||||
|
* database (notmuch_database_add_message, notmuch_message_add_tag,
|
||||||
|
* notmuch_directory_set_mtime, etc.) will work unless the function
|
||||||
|
* notmuch_database_upgrade is called successfully first. */
|
||||||
|
notmuch_bool_t
|
||||||
|
notmuch_database_needs_upgrade (notmuch_database_t *database);
|
||||||
|
|
||||||
|
/* Upgrade the current database.
|
||||||
|
*
|
||||||
|
* After opening a database in read-write mode, the client should
|
||||||
|
* check if an upgrade is needed (notmuch_database_needs_upgrade) and
|
||||||
|
* if so, upgrade with this function before making any modifications.
|
||||||
|
*
|
||||||
|
* The optional progress_notify callback can be used by the caller to
|
||||||
|
* provide progress indication to the user. If non-NULL it will be
|
||||||
|
* called periodically with 'count' as the number of messages upgraded
|
||||||
|
* so far and 'total' the overall number of messages that will be
|
||||||
|
* converted.
|
||||||
|
*/
|
||||||
|
notmuch_status_t
|
||||||
|
notmuch_database_upgrade (notmuch_database_t *database,
|
||||||
|
void (*progress_notify) (void *closure,
|
||||||
|
unsigned int count,
|
||||||
|
unsigned int total),
|
||||||
|
void *closure);
|
||||||
|
|
||||||
/* Retrieve a directory object from the database for 'path'.
|
/* Retrieve a directory object from the database for 'path'.
|
||||||
*
|
*
|
||||||
* Here, 'path' should be a path relative to the path of 'database'
|
* Here, 'path' should be a path relative to the path of 'database'
|
||||||
* (see notmuch_database_get_path), or else should be an absolute path
|
* (see notmuch_database_get_path), or else should be an absolute path
|
||||||
* with initial components that match the path of 'database'.
|
* with initial components that match the path of 'database'.
|
||||||
*
|
|
||||||
* Note: The resulting notmuch_directory_t object will represent the
|
|
||||||
* state as it currently exists in the database, (and will not reflect
|
|
||||||
* subsequent changes).
|
|
||||||
*/
|
*/
|
||||||
notmuch_directory_t *
|
notmuch_directory_t *
|
||||||
notmuch_database_get_directory (notmuch_database_t *database,
|
notmuch_database_get_directory (notmuch_database_t *database,
|
||||||
|
@ -990,7 +1018,7 @@ notmuch_directory_set_mtime (notmuch_directory_t *directory,
|
||||||
/* Get the mtime of a directory, (as previously stored with
|
/* Get the mtime of a directory, (as previously stored with
|
||||||
* notmuch_directory_set_mtime).
|
* notmuch_directory_set_mtime).
|
||||||
*
|
*
|
||||||
* Returns 0 if not mtime has previously been stored for this
|
* Returns 0 if no mtime has previously been stored for this
|
||||||
* directory.*/
|
* directory.*/
|
||||||
time_t
|
time_t
|
||||||
notmuch_directory_get_mtime (notmuch_directory_t *directory);
|
notmuch_directory_get_mtime (notmuch_directory_t *directory);
|
||||||
|
|
Loading…
Reference in a new issue