NEWS: cli: manual page for notmuch configuration options
[notmuch] / lib / query.cc
1 /* query.cc - Support for searching a notmuch database
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-private.h"
22 #include "database-private.h"
23
24 #include <glib.h> /* GHashTable, GPtrArray */
25
26 struct _notmuch_query {
27     notmuch_database_t *notmuch;
28     const char *query_string;
29     notmuch_sort_t sort;
30     notmuch_string_list_t *exclude_terms;
31 };
32
33 typedef struct _notmuch_mset_messages {
34     notmuch_messages_t base;
35     notmuch_database_t *notmuch;
36     Xapian::MSetIterator iterator;
37     Xapian::MSetIterator iterator_end;
38 } notmuch_mset_messages_t;
39
40 struct _notmuch_doc_id_set {
41     unsigned int *bitmap;
42     unsigned int bound;
43 };
44
45 #define DOCIDSET_WORD(bit) ((bit) / sizeof (unsigned int))
46 #define DOCIDSET_BIT(bit) ((bit) % sizeof (unsigned int))
47
48 struct visible _notmuch_threads {
49     notmuch_query_t *query;
50
51     /* The ordered list of doc ids matched by the query. */
52     GArray *doc_ids;
53     /* Our iterator's current position in doc_ids. */
54     unsigned int doc_id_pos;
55     /* The set of matched docid's that have not been assigned to a
56      * thread. Initially, this contains every docid in doc_ids. */
57     notmuch_doc_id_set_t match_set;
58 };
59
60 notmuch_query_t *
61 notmuch_query_create (notmuch_database_t *notmuch,
62                       const char *query_string)
63 {
64     notmuch_query_t *query;
65
66 #ifdef DEBUG_QUERY
67     fprintf (stderr, "Query string is:\n%s\n", query_string);
68 #endif
69
70     query = talloc (NULL, notmuch_query_t);
71     if (unlikely (query == NULL))
72         return NULL;
73
74     query->notmuch = notmuch;
75
76     query->query_string = talloc_strdup (query, query_string);
77
78     query->sort = NOTMUCH_SORT_NEWEST_FIRST;
79
80     query->exclude_terms = _notmuch_string_list_create (query);
81
82     return query;
83 }
84
85 const char *
86 notmuch_query_get_query_string (notmuch_query_t *query)
87 {
88     return query->query_string;
89 }
90
91 void
92 notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
93 {
94     query->sort = sort;
95 }
96
97 notmuch_sort_t
98 notmuch_query_get_sort (notmuch_query_t *query)
99 {
100     return query->sort;
101 }
102
103 void
104 notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag)
105 {
106     char *term = talloc_asprintf (query, "%s%s", _find_prefix ("tag"), tag);
107     _notmuch_string_list_append (query->exclude_terms, term);
108 }
109
110 /* We end up having to call the destructors explicitly because we had
111  * to use "placement new" in order to initialize C++ objects within a
112  * block that we allocated with talloc. So C++ is making talloc
113  * slightly less simple to use, (we wouldn't need
114  * talloc_set_destructor at all otherwise).
115  */
116 static int
117 _notmuch_messages_destructor (notmuch_mset_messages_t *messages)
118 {
119     messages->iterator.~MSetIterator ();
120     messages->iterator_end.~MSetIterator ();
121
122     return 0;
123 }
124
125 /* Return a query that does not match messages with the excluded tags
126  * registered with the query.  Any tags that explicitly appear in
127  * xquery will not be excluded. */
128 static Xapian::Query
129 _notmuch_exclude_tags (notmuch_query_t *query, Xapian::Query xquery)
130 {
131     for (notmuch_string_node_t *term = query->exclude_terms->head; term;
132          term = term->next) {
133         Xapian::TermIterator it = xquery.get_terms_begin ();
134         Xapian::TermIterator end = xquery.get_terms_end ();
135         for (; it != end; it++) {
136             if ((*it).compare (term->string) == 0)
137                 break;
138         }
139         if (it == end)
140             xquery = Xapian::Query (Xapian::Query::OP_AND_NOT,
141                                     xquery, Xapian::Query (term->string));
142     }
143     return xquery;
144 }
145
146 notmuch_messages_t *
147 notmuch_query_search_messages (notmuch_query_t *query)
148 {
149     notmuch_database_t *notmuch = query->notmuch;
150     const char *query_string = query->query_string;
151     notmuch_mset_messages_t *messages;
152
153     messages = talloc (query, notmuch_mset_messages_t);
154     if (unlikely (messages == NULL))
155         return NULL;
156
157     try {
158
159         messages->base.is_of_list_type = FALSE;
160         messages->base.iterator = NULL;
161         messages->notmuch = notmuch;
162         new (&messages->iterator) Xapian::MSetIterator ();
163         new (&messages->iterator_end) Xapian::MSetIterator ();
164
165         talloc_set_destructor (messages, _notmuch_messages_destructor);
166
167         Xapian::Enquire enquire (*notmuch->xapian_db);
168         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
169                                                    _find_prefix ("type"),
170                                                    "mail"));
171         Xapian::Query string_query, final_query;
172         Xapian::MSet mset;
173         unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
174                               Xapian::QueryParser::FLAG_PHRASE |
175                               Xapian::QueryParser::FLAG_LOVEHATE |
176                               Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
177                               Xapian::QueryParser::FLAG_WILDCARD |
178                               Xapian::QueryParser::FLAG_PURE_NOT);
179
180         if (strcmp (query_string, "") == 0 ||
181             strcmp (query_string, "*") == 0)
182         {
183             final_query = mail_query;
184         } else {
185             string_query = notmuch->query_parser->
186                 parse_query (query_string, flags);
187             final_query = Xapian::Query (Xapian::Query::OP_AND,
188                                          mail_query, string_query);
189         }
190
191         final_query = _notmuch_exclude_tags (query, final_query);
192
193         enquire.set_weighting_scheme (Xapian::BoolWeight());
194
195         switch (query->sort) {
196         case NOTMUCH_SORT_OLDEST_FIRST:
197             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE);
198             break;
199         case NOTMUCH_SORT_NEWEST_FIRST:
200             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, TRUE);
201             break;
202         case NOTMUCH_SORT_MESSAGE_ID:
203             enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, FALSE);
204             break;
205         case NOTMUCH_SORT_UNSORTED:
206             break;
207         }
208
209 #if DEBUG_QUERY
210         fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
211 #endif
212
213         enquire.set_query (final_query);
214
215         mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
216
217         messages->iterator = mset.begin ();
218         messages->iterator_end = mset.end ();
219
220         return &messages->base;
221
222     } catch (const Xapian::Error &error) {
223         fprintf (stderr, "A Xapian exception occurred performing query: %s\n",
224                  error.get_msg().c_str());
225         fprintf (stderr, "Query string was: %s\n", query->query_string);
226         notmuch->exception_reported = TRUE;
227         talloc_free (messages);
228         return NULL;
229     }
230 }
231
232 notmuch_bool_t
233 _notmuch_mset_messages_valid (notmuch_messages_t *messages)
234 {
235     notmuch_mset_messages_t *mset_messages;
236
237     mset_messages = (notmuch_mset_messages_t *) messages;
238
239     return (mset_messages->iterator != mset_messages->iterator_end);
240 }
241
242 static Xapian::docid
243 _notmuch_mset_messages_get_doc_id (notmuch_messages_t *messages)
244 {
245     notmuch_mset_messages_t *mset_messages;
246
247     mset_messages = (notmuch_mset_messages_t *) messages;
248
249     if (! _notmuch_mset_messages_valid (&mset_messages->base))
250         return 0;
251
252     return *mset_messages->iterator;
253 }
254
255 notmuch_message_t *
256 _notmuch_mset_messages_get (notmuch_messages_t *messages)
257 {
258     notmuch_message_t *message;
259     Xapian::docid doc_id;
260     notmuch_private_status_t status;
261     notmuch_mset_messages_t *mset_messages;
262
263     mset_messages = (notmuch_mset_messages_t *) messages;
264
265     if (! _notmuch_mset_messages_valid (&mset_messages->base))
266         return NULL;
267
268     doc_id = *mset_messages->iterator;
269
270     message = _notmuch_message_create (mset_messages,
271                                        mset_messages->notmuch, doc_id,
272                                        &status);
273
274     if (message == NULL &&
275        status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
276     {
277         INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
278     }
279
280     return message;
281 }
282
283 void
284 _notmuch_mset_messages_move_to_next (notmuch_messages_t *messages)
285 {
286     notmuch_mset_messages_t *mset_messages;
287
288     mset_messages = (notmuch_mset_messages_t *) messages;
289
290     mset_messages->iterator++;
291 }
292
293 static notmuch_bool_t
294 _notmuch_doc_id_set_init (void *ctx,
295                           notmuch_doc_id_set_t *doc_ids,
296                           GArray *arr)
297 {
298     unsigned int max = 0;
299     unsigned int *bitmap;
300
301     for (unsigned int i = 0; i < arr->len; i++)
302         max = MAX(max, g_array_index (arr, unsigned int, i));
303     bitmap = talloc_zero_array (ctx, unsigned int, 1 + max / sizeof (*bitmap));
304
305     if (bitmap == NULL)
306         return FALSE;
307
308     doc_ids->bitmap = bitmap;
309     doc_ids->bound = max + 1;
310
311     for (unsigned int i = 0; i < arr->len; i++) {
312         unsigned int doc_id = g_array_index (arr, unsigned int, i);
313         bitmap[DOCIDSET_WORD(doc_id)] |= 1 << DOCIDSET_BIT(doc_id);
314     }
315
316     return TRUE;
317 }
318
319 notmuch_bool_t
320 _notmuch_doc_id_set_contains (notmuch_doc_id_set_t *doc_ids,
321                               unsigned int doc_id)
322 {
323     if (doc_id >= doc_ids->bound)
324         return FALSE;
325     return doc_ids->bitmap[DOCIDSET_WORD(doc_id)] & (1 << DOCIDSET_BIT(doc_id));
326 }
327
328 void
329 _notmuch_doc_id_set_remove (notmuch_doc_id_set_t *doc_ids,
330                             unsigned int doc_id)
331 {
332     if (doc_id < doc_ids->bound)
333         doc_ids->bitmap[DOCIDSET_WORD(doc_id)] &= ~(1 << DOCIDSET_BIT(doc_id));
334 }
335
336 /* Glib objects force use to use a talloc destructor as well, (but not
337  * nearly as ugly as the for messages due to C++ objects). At
338  * this point, I'd really like to have some talloc-friendly
339  * equivalents for the few pieces of glib that I'm using. */
340 static int
341 _notmuch_threads_destructor (notmuch_threads_t *threads)
342 {
343     if (threads->doc_ids)
344         g_array_unref (threads->doc_ids);
345
346     return 0;
347 }
348
349 notmuch_threads_t *
350 notmuch_query_search_threads (notmuch_query_t *query)
351 {
352     notmuch_threads_t *threads;
353     notmuch_messages_t *messages;
354
355     threads = talloc (query, notmuch_threads_t);
356     if (threads == NULL)
357         return NULL;
358     threads->doc_ids = NULL;
359     talloc_set_destructor (threads, _notmuch_threads_destructor);
360
361     threads->query = query;
362
363     messages = notmuch_query_search_messages (query);
364     if (messages == NULL) {
365             talloc_free (threads);
366             return NULL;
367     }
368
369     threads->doc_ids = g_array_new (FALSE, FALSE, sizeof (unsigned int));
370     while (notmuch_messages_valid (messages)) {
371         unsigned int doc_id = _notmuch_mset_messages_get_doc_id (messages);
372         g_array_append_val (threads->doc_ids, doc_id);
373         notmuch_messages_move_to_next (messages);
374     }
375     threads->doc_id_pos = 0;
376
377     talloc_free (messages);
378
379     if (! _notmuch_doc_id_set_init (threads, &threads->match_set,
380                                     threads->doc_ids)) {
381         talloc_free (threads);
382         return NULL;
383     }
384
385     return threads;
386 }
387
388 void
389 notmuch_query_destroy (notmuch_query_t *query)
390 {
391     talloc_free (query);
392 }
393
394 notmuch_bool_t
395 notmuch_threads_valid (notmuch_threads_t *threads)
396 {
397     unsigned int doc_id;
398
399     while (threads->doc_id_pos < threads->doc_ids->len) {
400         doc_id = g_array_index (threads->doc_ids, unsigned int,
401                                 threads->doc_id_pos);
402         if (_notmuch_doc_id_set_contains (&threads->match_set, doc_id))
403             break;
404
405         threads->doc_id_pos++;
406     }
407
408     return threads->doc_id_pos < threads->doc_ids->len;
409 }
410
411 notmuch_thread_t *
412 notmuch_threads_get (notmuch_threads_t *threads)
413 {
414     unsigned int doc_id;
415
416     if (! notmuch_threads_valid (threads))
417         return NULL;
418
419     doc_id = g_array_index (threads->doc_ids, unsigned int,
420                             threads->doc_id_pos);
421     return _notmuch_thread_create (threads->query,
422                                    threads->query->notmuch,
423                                    doc_id,
424                                    &threads->match_set,
425                                    threads->query->sort);
426 }
427
428 void
429 notmuch_threads_move_to_next (notmuch_threads_t *threads)
430 {
431     threads->doc_id_pos++;
432 }
433
434 void
435 notmuch_threads_destroy (notmuch_threads_t *threads)
436 {
437     talloc_free (threads);
438 }
439
440 unsigned
441 notmuch_query_count_messages (notmuch_query_t *query)
442 {
443     notmuch_database_t *notmuch = query->notmuch;
444     const char *query_string = query->query_string;
445     Xapian::doccount count = 0;
446
447     try {
448         Xapian::Enquire enquire (*notmuch->xapian_db);
449         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
450                                                    _find_prefix ("type"),
451                                                    "mail"));
452         Xapian::Query string_query, final_query;
453         Xapian::MSet mset;
454         unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
455                               Xapian::QueryParser::FLAG_PHRASE |
456                               Xapian::QueryParser::FLAG_LOVEHATE |
457                               Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
458                               Xapian::QueryParser::FLAG_WILDCARD |
459                               Xapian::QueryParser::FLAG_PURE_NOT);
460
461         if (strcmp (query_string, "") == 0 ||
462             strcmp (query_string, "*") == 0)
463         {
464             final_query = mail_query;
465         } else {
466             string_query = notmuch->query_parser->
467                 parse_query (query_string, flags);
468             final_query = Xapian::Query (Xapian::Query::OP_AND,
469                                          mail_query, string_query);
470         }
471
472         final_query = _notmuch_exclude_tags (query, final_query);
473
474         enquire.set_weighting_scheme(Xapian::BoolWeight());
475         enquire.set_docid_order(Xapian::Enquire::ASCENDING);
476
477 #if DEBUG_QUERY
478         fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
479 #endif
480
481         enquire.set_query (final_query);
482
483         mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
484
485         count = mset.get_matches_estimated();
486
487     } catch (const Xapian::Error &error) {
488         fprintf (stderr, "A Xapian exception occurred: %s\n",
489                  error.get_msg().c_str());
490         fprintf (stderr, "Query string was: %s\n", query->query_string);
491     }
492
493     return count;
494 }
495
496 unsigned
497 notmuch_query_count_threads (notmuch_query_t *query)
498 {
499     notmuch_messages_t *messages;
500     GHashTable *hash;
501     unsigned int count;
502     notmuch_sort_t sort;
503
504     sort = query->sort;
505     query->sort = NOTMUCH_SORT_UNSORTED;
506     messages = notmuch_query_search_messages (query);
507     query->sort = sort;
508     if (messages == NULL)
509         return 0;
510
511     hash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);
512     if (hash == NULL) {
513         talloc_free (messages);
514         return 0;
515     }
516
517     while (notmuch_messages_valid (messages)) {
518         notmuch_message_t *message = notmuch_messages_get (messages);
519         const char *thread_id = notmuch_message_get_thread_id (message);
520         char *thread_id_copy = talloc_strdup (messages, thread_id);
521         if (unlikely (thread_id_copy == NULL)) {
522             notmuch_message_destroy (message);
523             count = 0;
524             goto DONE;
525         }
526         g_hash_table_insert (hash, thread_id_copy, NULL);
527         notmuch_message_destroy (message);
528         notmuch_messages_move_to_next (messages);
529     }
530
531     count = g_hash_table_size (hash);
532
533   DONE:
534     g_hash_table_unref (hash);
535     talloc_free (messages);
536
537     return count;
538 }