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