X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=lib%2Fdatabase.cc;h=f8b95bc46f5fcacf61839c30b4b1cf17a124c9f4;hp=6842fafadad86967b4363abd5869a2aeaef345c2;hb=7a8046ced8c0e61ddd0ff463cfc17ed63e6edad3;hpb=14073b8851067db4dbf5727bf1f5547a66750934 diff --git a/lib/database.cc b/lib/database.cc index 6842fafa..f8b95bc4 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -24,7 +24,6 @@ #include #include -#include #include /* g_free, GPtrArray, GHashTable */ @@ -57,8 +56,12 @@ typedef struct { * * type: mail * - * id: Unique ID of mail, (from Message-ID header or generated - * as "notmuch-sha1-. + * id: Unique ID of mail. This is from the Message-ID header + * if present and not too long (see NOTMUCH_MESSAGE_ID_MAX). + * If it's present and too long, then we use + * "notmuch-sha1-". + * If this header is not present, we use + * "notmuch-sha1-". * * thread: The ID of the thread to which the mail belongs * @@ -66,7 +69,7 @@ typedef struct { * * Multiple terms of given prefix: * - * reference: All message IDs from In-Reply-To and Re ferences + * reference: All message IDs from In-Reply-To and References * headers in the message. * * tag: Any tags associated with this message by the user. @@ -86,8 +89,9 @@ typedef struct { * * 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. But the database doesn't really care itself - * about any of these. + * user in searching. Similarly, terms from the path of the mail + * message are added with a "folder" prefix. But the database doesn't + * really care itself about any of these. * * The data portion of a mail document is empty. * @@ -133,7 +137,7 @@ typedef struct { * ASCII integer. The initial database version * was 1, (though a schema existed before that * were no "version" database value existed at - * all). Succesive versions are allocated as + * all). Successive versions are allocated as * changes are made to the database (such as by * indexing new fields). * @@ -144,10 +148,12 @@ typedef struct { * incremented for each thread ID. * * thread_id_* A pre-allocated thread ID for a particular - * message. This is actually an arbitarily large - * family of metadata name. Any particular name - * is formed by concatenating "thread_id_" with a - * message ID. The value stored is a thread ID. + * message. This is actually an arbitrarily large + * family of metadata name. Any particular name is + * formed by concatenating "thread_id_" with a message + * ID (or the SHA1 sum of a message ID if it is very + * long---see description of 'id' in the mail + * document). The value stored is a thread ID. * * These thread ID metadata values are stored * whenever a message references a parent message @@ -179,7 +185,7 @@ typedef struct { * nearly universal to all mail messages). */ -prefix_t BOOLEAN_PREFIX_INTERNAL[] = { +static prefix_t BOOLEAN_PREFIX_INTERNAL[] = { { "type", "T" }, { "reference", "XREFERENCE" }, { "replyto", "XREPLYTO" }, @@ -188,18 +194,19 @@ prefix_t BOOLEAN_PREFIX_INTERNAL[] = { { "directory-direntry", "XDDIRENTRY" }, }; -prefix_t BOOLEAN_PREFIX_EXTERNAL[] = { +static prefix_t BOOLEAN_PREFIX_EXTERNAL[] = { { "thread", "G" }, { "tag", "K" }, { "is", "K" }, { "id", "Q" } }; -prefix_t PROBABILISTIC_PREFIX[]= { +static prefix_t PROBABILISTIC_PREFIX[]= { { "from", "XFROM" }, { "to", "XTO" }, { "attachment", "XATTACHMENT" }, - { "subject", "XSUBJECT"} + { "subject", "XSUBJECT"}, + { "folder", "XFOLDER"} }; int @@ -266,6 +273,8 @@ notmuch_status_to_string (notmuch_status_t status) return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)"; case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: return "Unbalanced number of calls to notmuch_message_freeze/thaw"; + case NOTMUCH_STATUS_UNBALANCED_ATOMIC: + return "Unbalanced number of calls to notmuch_database_begin_atomic/end_atomic"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; @@ -334,6 +343,23 @@ find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id) return notmuch->xapian_db->get_document (doc_id); } +/* Generate a compressed version of 'message_id' of the form: + * + * notmuch-sha1- + */ +static char * +_message_id_compressed (void *ctx, const char *message_id) +{ + char *sha1, *compressed; + + sha1 = notmuch_sha1_of_string (message_id); + + compressed = talloc_asprintf (ctx, "notmuch-sha1-%s", sha1); + free (sha1); + + return compressed; +} + notmuch_message_t * notmuch_database_find_message (notmuch_database_t *notmuch, const char *message_id) @@ -341,13 +367,23 @@ notmuch_database_find_message (notmuch_database_t *notmuch, notmuch_private_status_t status; unsigned int doc_id; - status = _notmuch_database_find_unique_doc_id (notmuch, "id", - message_id, &doc_id); + if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) + message_id = _message_id_compressed (notmuch, message_id); - if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) - return NULL; + try { + status = _notmuch_database_find_unique_doc_id (notmuch, "id", + message_id, &doc_id); - return _notmuch_message_create (notmuch, notmuch, doc_id, NULL); + if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) + return NULL; + + return _notmuch_message_create (notmuch, notmuch, doc_id, NULL); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred finding message: %s.\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + return NULL; + } } /* Advance 'str' past any whitespace or RFC 822 comments. A comment is @@ -388,7 +424,7 @@ skip_space_and_comments (const char **str) } /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822 - * comments, and the '<' and '>' delimeters. + * comments, and the '<' and '>' delimiters. * * If not NULL, then *next will be made to point to the first character * not parsed, (possibly pointing to the final '\0' terminator. @@ -577,6 +613,7 @@ notmuch_database_open (const char *path, notmuch->needs_upgrade = FALSE; notmuch->mode = mode; + notmuch->atomic_nesting = 0; try { string last_thread_id; @@ -615,6 +652,7 @@ notmuch_database_open (const char *path, } } + notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid (); last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id"); if (last_thread_id.empty ()) { notmuch->last_thread_id = 0; @@ -939,6 +977,61 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, return NOTMUCH_STATUS_SUCCESS; } +notmuch_status_t +notmuch_database_begin_atomic (notmuch_database_t *notmuch) +{ + if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY || + notmuch->atomic_nesting > 0) + goto DONE; + + try { + (static_cast (notmuch->xapian_db))->begin_transaction (false); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred beginning transaction: %s.\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + +DONE: + notmuch->atomic_nesting++; + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_database_end_atomic (notmuch_database_t *notmuch) +{ + Xapian::WritableDatabase *db; + + if (notmuch->atomic_nesting == 0) + return NOTMUCH_STATUS_UNBALANCED_ATOMIC; + + if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY || + notmuch->atomic_nesting > 1) + goto DONE; + + db = static_cast (notmuch->xapian_db); + try { + db->commit_transaction (); + + /* This is a hack for testing. Xapian never flushes on a + * non-flushed commit, even if the flush threshold is 1. + * However, we rely on flushing to test atomicity. */ + const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD"); + if (thresh && atoi (thresh) == 1) + db->commit (); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred committing transaction: %s.\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + +DONE: + notmuch->atomic_nesting--; + return NOTMUCH_STATUS_SUCCESS; +} + /* 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. @@ -1114,7 +1207,7 @@ _notmuch_database_filename_to_direntry (void *ctx, /* Given a legal 'path' for the database, return the relative path. * - * The return value will be a pointer to the originl path contents, + * The return value will be a pointer to the original path contents, * and will be either the original string (if 'path' was relative) or * a portion of the string (if path was absolute and begins with the * database path). @@ -1152,7 +1245,39 @@ notmuch_database_get_directory (notmuch_database_t *notmuch, { notmuch_status_t status; - return _notmuch_directory_create (notmuch, path, &status); + try { + return _notmuch_directory_create (notmuch, path, &status); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred getting directory: %s.\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + return NULL; + } +} + +/* Allocate a document ID that satisfies the following criteria: + * + * 1. The ID does not exist for any document in the Xapian database + * + * 2. The ID was not previously returned from this function + * + * 3. The ID is the smallest integer satisfying (1) and (2) + * + * This function will trigger an internal error if these constraints + * cannot all be satisfied, (that is, the pool of available document + * IDs has been exhausted). + */ +unsigned int +_notmuch_database_generate_doc_id (notmuch_database_t *notmuch) +{ + assert (notmuch->last_doc_id >= notmuch->xapian_db->get_lastdocid ()); + + notmuch->last_doc_id++; + + if (notmuch->last_doc_id == 0) + INTERNAL_ERROR ("Xapian document IDs are exhausted.\n"); + + return notmuch->last_doc_id; } static const char * @@ -1177,7 +1302,11 @@ _notmuch_database_generate_thread_id (notmuch_database_t *notmuch) static char * _get_metadata_thread_id_key (void *ctx, const char *message_id) { - return talloc_asprintf (ctx, "thread_id_%s", message_id); + if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) + message_id = _message_id_compressed (ctx, message_id); + + return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s", + message_id); } /* Find the thread ID to which the message with 'message_id' belongs. @@ -1230,7 +1359,7 @@ _resolve_message_id_to_thread_id (notmuch_database_t *notmuch, talloc_free (metadata_key); - return thread_id; + return talloc_strdup (ctx, thread_id); } static notmuch_status_t @@ -1405,7 +1534,7 @@ _notmuch_database_link_message_to_children (notmuch_database_t *notmuch, * In all cases, we assign to the current message the first thread_id * found (through either parent or child). We will also merge any * existing, distinct threads where this message belongs to both, - * (which is not uncommon when mesages are processed out of order). + * (which is not uncommon when messages are processed out of order). * * Finally, if no thread ID has been found through parent or child, we * call _notmuch_message_generate_thread_id to generate a new thread @@ -1530,10 +1659,12 @@ notmuch_database_add_message (notmuch_database_t *notmuch, if (message_id == NULL) message_id = talloc_strdup (message_file, header); - /* Reject a Message ID that's too long. */ - if (message_id && strlen (message_id) + 1 > NOTMUCH_TERM_MAX) { + /* If a message ID is too long, substitute its sha1 instead. */ + if (message_id && strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) { + char *compressed = _message_id_compressed (message_file, + message_id); talloc_free (message_id); - message_id = NULL; + message_id = compressed; } } @@ -1591,7 +1722,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", - error.get_description().c_str()); + error.get_msg().c_str()); notmuch->exception_reported = TRUE; ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION; goto DONE; @@ -1599,7 +1730,8 @@ notmuch_database_add_message (notmuch_database_t *notmuch, DONE: if (message) { - if (ret == NOTMUCH_STATUS_SUCCESS && message_ret) + if ((ret == NOTMUCH_STATUS_SUCCESS || + ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret) *message_ret = message; else notmuch_message_destroy (message); @@ -1615,95 +1747,102 @@ notmuch_status_t notmuch_database_remove_message (notmuch_database_t *notmuch, const char *filename) { - Xapian::WritableDatabase *db; - void *local = talloc_new (notmuch); + notmuch_message_t *message = + notmuch_database_find_message_by_filename (notmuch, filename); + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + + if (message) { + status = _notmuch_message_remove_filename (message, filename); + if (status == NOTMUCH_STATUS_SUCCESS) + _notmuch_message_delete (message); + else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) + _notmuch_message_sync (message); + } + + return status; +} + +notmuch_message_t * +notmuch_database_find_message_by_filename (notmuch_database_t *notmuch, + const char *filename) +{ + void *local; const char *prefix = _find_prefix ("file-direntry"); char *direntry, *term; Xapian::PostingIterator i, end; - Xapian::Document document; + notmuch_message_t *message = NULL; notmuch_status_t status; - status = _notmuch_database_ensure_writable (notmuch); - if (status) - return status; + local = talloc_new (notmuch); - db = static_cast (notmuch->xapian_db); - - status = _notmuch_database_filename_to_direntry (local, notmuch, - filename, &direntry); - if (status) - return status; - - term = talloc_asprintf (notmuch, "%s%s", prefix, direntry); - - find_doc_ids_for_term (notmuch, term, &i, &end); - - for ( ; i != end; i++) { - Xapian::TermIterator j; + try { + status = _notmuch_database_filename_to_direntry (local, notmuch, + filename, &direntry); + if (status) + return NULL; - document = find_document_for_doc_id (notmuch, *i); + term = talloc_asprintf (local, "%s%s", prefix, direntry); - document.remove_term (term); + find_doc_ids_for_term (notmuch, term, &i, &end); - j = document.termlist_begin (); - j.skip_to (prefix); + if (i != end) { + notmuch_private_status_t private_status; - /* Was this the last file-direntry in the message? */ - if (j == document.termlist_end () || - strncmp ((*j).c_str (), prefix, strlen (prefix))) - { - db->delete_document (document.get_docid ()); - status = NOTMUCH_STATUS_SUCCESS; - } else { - db->replace_document (document.get_docid (), document); - status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; + message = _notmuch_message_create (notmuch, notmuch, + *i, &private_status); } + } catch (const Xapian::Error &error) { + fprintf (stderr, "Error: A Xapian exception occurred finding message by filename: %s\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + message = NULL; } talloc_free (local); - return status; + return message; } -notmuch_tags_t * -_notmuch_convert_tags (void *ctx, Xapian::TermIterator &i, - Xapian::TermIterator &end) +notmuch_string_list_t * +_notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i, + Xapian::TermIterator &end, + const char *prefix) { - const char *prefix = _find_prefix ("tag"); - notmuch_tags_t *tags; - std::string tag; + int prefix_len = strlen (prefix); + notmuch_string_list_t *list; - /* Currently this iteration is written with the assumption that - * "tag" has a single-character prefix. */ - assert (strlen (prefix) == 1); - - tags = _notmuch_tags_create (ctx); - if (unlikely (tags == NULL)) + list = _notmuch_string_list_create (ctx); + if (unlikely (list == NULL)) return NULL; - i.skip_to (prefix); - - while (i != end) { - tag = *i; - - if (tag.empty () || tag[0] != *prefix) + for (i.skip_to (prefix); i != end; i++) { + /* Terminate loop at first term without desired prefix. */ + if (strncmp ((*i).c_str (), prefix, prefix_len)) break; - _notmuch_tags_add_tag (tags, tag.c_str () + 1); - - i++; + _notmuch_string_list_append (list, (*i).c_str () + prefix_len); } - _notmuch_tags_prepare_iterator (tags); - - return tags; + return list; } notmuch_tags_t * notmuch_database_get_all_tags (notmuch_database_t *db) { Xapian::TermIterator i, end; - i = db->xapian_db->allterms_begin(); - end = db->xapian_db->allterms_end(); - return _notmuch_convert_tags(db, i, end); + notmuch_string_list_t *tags; + + try { + i = db->xapian_db->allterms_begin(); + end = db->xapian_db->allterms_end(); + tags = _notmuch_database_get_terms_with_prefix (db, i, end, + _find_prefix ("tag")); + _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", + error.get_msg().c_str()); + db->exception_reported = TRUE; + return NULL; + } }