lib: Fix notmuch_query_search_threads to return NULL on any Xapian exception.
[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 #include <xapian.h>
27
28 struct _notmuch_query {
29     notmuch_database_t *notmuch;
30     const char *query_string;
31     notmuch_sort_t sort;
32 };
33
34 typedef struct _notmuch_mset_messages {
35     notmuch_messages_t base;
36     notmuch_database_t *notmuch;
37     Xapian::MSetIterator iterator;
38     Xapian::MSetIterator iterator_end;
39 } notmuch_mset_messages_t;
40
41 struct _notmuch_threads {
42     notmuch_query_t *query;
43     GHashTable *threads;
44     notmuch_messages_t *messages;
45
46     /* This thread ID is our iterator state. */
47     const char *thread_id;
48 };
49
50 notmuch_query_t *
51 notmuch_query_create (notmuch_database_t *notmuch,
52                       const char *query_string)
53 {
54     notmuch_query_t *query;
55
56 #ifdef DEBUG_QUERY
57     fprintf (stderr, "Query string is:\n%s\n", query_string);
58 #endif
59
60     query = talloc (NULL, notmuch_query_t);
61     if (unlikely (query == NULL))
62         return NULL;
63
64     query->notmuch = notmuch;
65
66     query->query_string = talloc_strdup (query, query_string);
67
68     query->sort = NOTMUCH_SORT_NEWEST_FIRST;
69
70     return query;
71 }
72
73 void
74 notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
75 {
76     query->sort = sort;
77 }
78
79 /* We end up having to call the destructors explicitly because we had
80  * to use "placement new" in order to initialize C++ objects within a
81  * block that we allocated with talloc. So C++ is making talloc
82  * slightly less simple to use, (we wouldn't need
83  * talloc_set_destructor at all otherwise).
84  */
85 static int
86 _notmuch_messages_destructor (notmuch_mset_messages_t *messages)
87 {
88     messages->iterator.~MSetIterator ();
89     messages->iterator_end.~MSetIterator ();
90
91     return 0;
92 }
93
94 notmuch_messages_t *
95 notmuch_query_search_messages (notmuch_query_t *query)
96 {
97     notmuch_database_t *notmuch = query->notmuch;
98     const char *query_string = query->query_string;
99     notmuch_mset_messages_t *messages;
100
101     messages = talloc (query, notmuch_mset_messages_t);
102     if (unlikely (messages == NULL))
103         return NULL;
104
105     try {
106
107         messages->base.is_of_list_type = FALSE;
108         messages->base.iterator = NULL;
109         messages->notmuch = notmuch;
110         new (&messages->iterator) Xapian::MSetIterator ();
111         new (&messages->iterator_end) Xapian::MSetIterator ();
112
113         talloc_set_destructor (messages, _notmuch_messages_destructor);
114
115         Xapian::Enquire enquire (*notmuch->xapian_db);
116         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
117                                                    _find_prefix ("type"),
118                                                    "mail"));
119         Xapian::Query string_query, final_query;
120         Xapian::MSet mset;
121         unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
122                               Xapian::QueryParser::FLAG_PHRASE |
123                               Xapian::QueryParser::FLAG_LOVEHATE |
124                               Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
125                               Xapian::QueryParser::FLAG_WILDCARD |
126                               Xapian::QueryParser::FLAG_PURE_NOT);
127
128         if (strcmp (query_string, "") == 0 ||
129             strcmp (query_string, "*") == 0)
130         {
131             final_query = mail_query;
132         } else {
133             string_query = notmuch->query_parser->
134                 parse_query (query_string, flags);
135             final_query = Xapian::Query (Xapian::Query::OP_AND,
136                                          mail_query, string_query);
137         }
138
139         enquire.set_weighting_scheme (Xapian::BoolWeight());
140
141         switch (query->sort) {
142         case NOTMUCH_SORT_OLDEST_FIRST:
143             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE);
144             break;
145         case NOTMUCH_SORT_NEWEST_FIRST:
146             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, TRUE);
147             break;
148         case NOTMUCH_SORT_MESSAGE_ID:
149             enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, FALSE);
150             break;
151         case NOTMUCH_SORT_UNSORTED:
152             break;
153         }
154
155 #if DEBUG_QUERY
156         fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
157 #endif
158
159         enquire.set_query (final_query);
160
161         mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
162
163         messages->iterator = mset.begin ();
164         messages->iterator_end = mset.end ();
165
166         return &messages->base;
167
168     } catch (const Xapian::Error &error) {
169         fprintf (stderr, "A Xapian exception occurred performing query: %s\n",
170                  error.get_msg().c_str());
171         fprintf (stderr, "Query string was: %s\n", query->query_string);
172         notmuch->exception_reported = TRUE;
173         talloc_free (messages);
174         return NULL;
175     }
176 }
177
178 notmuch_bool_t
179 _notmuch_mset_messages_valid (notmuch_messages_t *messages)
180 {
181     notmuch_mset_messages_t *mset_messages;
182
183     mset_messages = (notmuch_mset_messages_t *) messages;
184
185     return (mset_messages->iterator != mset_messages->iterator_end);
186 }
187
188 notmuch_message_t *
189 _notmuch_mset_messages_get (notmuch_messages_t *messages)
190 {
191     notmuch_message_t *message;
192     Xapian::docid doc_id;
193     notmuch_private_status_t status;
194     notmuch_mset_messages_t *mset_messages;
195
196     mset_messages = (notmuch_mset_messages_t *) messages;
197
198     if (! _notmuch_mset_messages_valid (&mset_messages->base))
199         return NULL;
200
201     doc_id = *mset_messages->iterator;
202
203     message = _notmuch_message_create (mset_messages,
204                                        mset_messages->notmuch, doc_id,
205                                        &status);
206
207     if (message == NULL &&
208        status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
209     {
210         INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
211     }
212
213     return message;
214 }
215
216 void
217 _notmuch_mset_messages_move_to_next (notmuch_messages_t *messages)
218 {
219     notmuch_mset_messages_t *mset_messages;
220
221     mset_messages = (notmuch_mset_messages_t *) messages;
222
223     mset_messages->iterator++;
224 }
225
226 /* Glib objects force use to use a talloc destructor as well, (but not
227  * nearly as ugly as the for messages due to C++ objects). At
228  * this point, I'd really like to have some talloc-friendly
229  * equivalents for the few pieces of glib that I'm using. */
230 static int
231 _notmuch_threads_destructor (notmuch_threads_t *threads)
232 {
233     g_hash_table_unref (threads->threads);
234
235     return 0;
236 }
237
238 notmuch_threads_t *
239 notmuch_query_search_threads (notmuch_query_t *query)
240 {
241     notmuch_threads_t *threads;
242
243     threads = talloc (query, notmuch_threads_t);
244     if (threads == NULL)
245         return NULL;
246
247     threads->query = query;
248     threads->threads = g_hash_table_new_full (g_str_hash, g_str_equal,
249                                               free, NULL);
250
251     threads->messages = notmuch_query_search_messages (query);
252     if (threads->messages == NULL) {
253             talloc_free (threads);
254             return NULL;
255     }
256
257     threads->thread_id = NULL;
258
259     talloc_set_destructor (threads, _notmuch_threads_destructor);
260
261     return threads;
262 }
263
264 void
265 notmuch_query_destroy (notmuch_query_t *query)
266 {
267     talloc_free (query);
268 }
269
270 notmuch_bool_t
271 notmuch_threads_valid (notmuch_threads_t *threads)
272 {
273     notmuch_message_t *message;
274
275     if (threads->thread_id)
276         return TRUE;
277
278     while (notmuch_messages_valid (threads->messages))
279     {
280         message = notmuch_messages_get (threads->messages);
281
282         threads->thread_id = notmuch_message_get_thread_id (message);
283
284         if (! g_hash_table_lookup_extended (threads->threads,
285                                             threads->thread_id,
286                                             NULL, NULL))
287         {
288             g_hash_table_insert (threads->threads,
289                                  xstrdup (threads->thread_id), NULL);
290             notmuch_messages_move_to_next (threads->messages);
291             return TRUE;
292         }
293
294         notmuch_messages_move_to_next (threads->messages);
295     }
296
297     threads->thread_id = NULL;
298     return FALSE;
299 }
300
301 notmuch_thread_t *
302 notmuch_threads_get (notmuch_threads_t *threads)
303 {
304     if (! notmuch_threads_valid (threads))
305         return NULL;
306
307     return _notmuch_thread_create (threads->query,
308                                    threads->query->notmuch,
309                                    threads->thread_id,
310                                    threads->query->query_string,
311                                    threads->query->sort);
312 }
313
314 void
315 notmuch_threads_move_to_next (notmuch_threads_t *threads)
316 {
317     threads->thread_id = NULL;
318 }
319
320 void
321 notmuch_threads_destroy (notmuch_threads_t *threads)
322 {
323     talloc_free (threads);
324 }
325
326 unsigned
327 notmuch_query_count_messages (notmuch_query_t *query)
328 {
329     notmuch_database_t *notmuch = query->notmuch;
330     const char *query_string = query->query_string;
331     Xapian::doccount count = 0;
332
333     try {
334         Xapian::Enquire enquire (*notmuch->xapian_db);
335         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
336                                                    _find_prefix ("type"),
337                                                    "mail"));
338         Xapian::Query string_query, final_query;
339         Xapian::MSet mset;
340         unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
341                               Xapian::QueryParser::FLAG_PHRASE |
342                               Xapian::QueryParser::FLAG_LOVEHATE |
343                               Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
344                               Xapian::QueryParser::FLAG_WILDCARD |
345                               Xapian::QueryParser::FLAG_PURE_NOT);
346
347         if (strcmp (query_string, "") == 0 ||
348             strcmp (query_string, "*") == 0)
349         {
350             final_query = mail_query;
351         } else {
352             string_query = notmuch->query_parser->
353                 parse_query (query_string, flags);
354             final_query = Xapian::Query (Xapian::Query::OP_AND,
355                                          mail_query, string_query);
356         }
357
358         enquire.set_weighting_scheme(Xapian::BoolWeight());
359         enquire.set_docid_order(Xapian::Enquire::ASCENDING);
360
361 #if DEBUG_QUERY
362         fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
363 #endif
364
365         enquire.set_query (final_query);
366
367         mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
368
369         count = mset.get_matches_estimated();
370
371     } catch (const Xapian::Error &error) {
372         fprintf (stderr, "A Xapian exception occurred: %s\n",
373                  error.get_msg().c_str());
374         fprintf (stderr, "Query string was: %s\n", query->query_string);
375     }
376
377     return count;
378 }