]> git.cworth.org Git - notmuch/blob - lib/query.cc
lib/parse-sexp: parse single terms and the empty list.
[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 https://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     notmuch_exclude_t omit_excluded;
32     bool parsed;
33     notmuch_query_syntax_t syntax;
34     Xapian::Query xapian_query;
35     std::set<std::string> terms;
36 };
37
38 typedef struct _notmuch_mset_messages {
39     notmuch_messages_t base;
40     notmuch_database_t *notmuch;
41     Xapian::MSetIterator iterator;
42     Xapian::MSetIterator iterator_end;
43 } notmuch_mset_messages_t;
44
45 struct _notmuch_doc_id_set {
46     unsigned char *bitmap;
47     unsigned int bound;
48 };
49
50 #define DOCIDSET_WORD(bit) ((bit) / CHAR_BIT)
51 #define DOCIDSET_BIT(bit) ((bit) % CHAR_BIT)
52
53 struct _notmuch_threads {
54     notmuch_query_t *query;
55
56     /* The ordered list of doc ids matched by the query. */
57     GArray *doc_ids;
58     /* Our iterator's current position in doc_ids. */
59     unsigned int doc_id_pos;
60     /* The set of matched docid's that have not been assigned to a
61      * thread. Initially, this contains every docid in doc_ids. */
62     notmuch_doc_id_set_t match_set;
63 };
64
65 /* We need this in the message functions so forward declare. */
66 static bool
67 _notmuch_doc_id_set_init (void *ctx,
68                           notmuch_doc_id_set_t *doc_ids,
69                           GArray *arr);
70
71 static bool
72 _debug_query (void)
73 {
74     char *env = getenv ("NOTMUCH_DEBUG_QUERY");
75
76     return (env && strcmp (env, "") != 0);
77 }
78
79 /* Explicit destructor call for placement new */
80 static int
81 _notmuch_query_destructor (notmuch_query_t *query)
82 {
83     query->xapian_query.~Query();
84     query->terms.~set<std::string>();
85     return 0;
86 }
87
88 static notmuch_query_t *
89 _notmuch_query_constructor (notmuch_database_t *notmuch,
90                             const char *query_string)
91 {
92     notmuch_query_t *query;
93
94     if (_debug_query ())
95         fprintf (stderr, "Query string is:\n%s\n", query_string);
96
97     query = talloc (notmuch, notmuch_query_t);
98     if (unlikely (query == NULL))
99         return NULL;
100
101     new (&query->xapian_query) Xapian::Query ();
102     new (&query->terms) std::set<std::string> ();
103     query->parsed = false;
104
105     talloc_set_destructor (query, _notmuch_query_destructor);
106
107     query->notmuch = notmuch;
108
109     if (query_string)
110         query->query_string = talloc_strdup (query, query_string);
111     else
112         query->query_string = NULL;
113
114     query->sort = NOTMUCH_SORT_NEWEST_FIRST;
115
116     query->exclude_terms = _notmuch_string_list_create (query);
117
118     query->omit_excluded = NOTMUCH_EXCLUDE_TRUE;
119
120     return query;
121 }
122
123 notmuch_query_t *
124 notmuch_query_create (notmuch_database_t *notmuch,
125                       const char *query_string)
126 {
127
128     notmuch_query_t *query;
129     notmuch_status_t status;
130
131     status = notmuch_query_create_with_syntax (notmuch, query_string,
132                                                NOTMUCH_QUERY_SYNTAX_XAPIAN,
133                                                &query);
134     if (status)
135         return NULL;
136
137     return query;
138 }
139
140 notmuch_status_t
141 notmuch_query_create_with_syntax (notmuch_database_t *notmuch,
142                                   const char *query_string,
143                                   notmuch_query_syntax_t syntax,
144                                   notmuch_query_t **output)
145 {
146
147     notmuch_query_t *query;
148
149     if (! output)
150         return NOTMUCH_STATUS_NULL_POINTER;
151
152     query = _notmuch_query_constructor (notmuch, query_string);
153     if (! query)
154         return NOTMUCH_STATUS_OUT_OF_MEMORY;
155
156     if (syntax == NOTMUCH_QUERY_SYNTAX_SEXP && ! HAVE_SFSEXP) {
157         _notmuch_database_log (notmuch, "sexp query parser not available");
158         return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
159     }
160
161     query->syntax = syntax;
162
163     *output = query;
164
165     return NOTMUCH_STATUS_SUCCESS;
166 }
167
168 static notmuch_status_t
169 _notmuch_query_ensure_parsed_xapian (notmuch_query_t *query)
170 {
171     try {
172         query->xapian_query =
173             query->notmuch->query_parser->
174             parse_query (query->query_string, NOTMUCH_QUERY_PARSER_FLAGS);
175
176         /* Xapian doesn't support skip_to on terms from a query since
177          *  they are unordered, so cache a copy of all terms in
178          *  something searchable.
179          */
180
181         for (Xapian::TermIterator t = query->xapian_query.get_terms_begin ();
182              t != query->xapian_query.get_terms_end (); ++t)
183             query->terms.insert (*t);
184
185         query->parsed = true;
186
187     } catch (const Xapian::Error &error) {
188         if (! query->notmuch->exception_reported) {
189             _notmuch_database_log (query->notmuch,
190                                    "A Xapian exception occurred parsing query: %s\n",
191                                    error.get_msg ().c_str ());
192             _notmuch_database_log_append (query->notmuch,
193                                           "Query string was: %s\n",
194                                           query->query_string);
195             query->notmuch->exception_reported = true;
196         }
197
198         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
199     }
200     return NOTMUCH_STATUS_SUCCESS;
201 }
202
203 static notmuch_status_t
204 _notmuch_query_ensure_parsed_sexpr (notmuch_query_t *query)
205 {
206     if (query->parsed)
207         return NOTMUCH_STATUS_SUCCESS;
208
209     return _notmuch_sexp_string_to_xapian_query (query->notmuch, query->query_string,
210                                                  query->xapian_query);
211 }
212
213 static notmuch_status_t
214 _notmuch_query_ensure_parsed (notmuch_query_t *query)
215 {
216     if (query->parsed)
217         return NOTMUCH_STATUS_SUCCESS;
218
219 #if HAVE_SFSEXP
220     if (query->syntax == NOTMUCH_QUERY_SYNTAX_SEXP)
221         return _notmuch_query_ensure_parsed_sexpr (query);
222 #endif
223
224     return _notmuch_query_ensure_parsed_xapian (query);
225 }
226
227 const char *
228 notmuch_query_get_query_string (const notmuch_query_t *query)
229 {
230     return query->query_string;
231 }
232
233 void
234 notmuch_query_set_omit_excluded (notmuch_query_t *query,
235                                  notmuch_exclude_t omit_excluded)
236 {
237     query->omit_excluded = omit_excluded;
238 }
239
240 void
241 notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
242 {
243     query->sort = sort;
244 }
245
246 notmuch_sort_t
247 notmuch_query_get_sort (const notmuch_query_t *query)
248 {
249     return query->sort;
250 }
251
252 notmuch_status_t
253 notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag)
254 {
255     notmuch_status_t status;
256     char *term;
257
258     status = _notmuch_query_ensure_parsed (query);
259     if (status)
260         return status;
261
262     term = talloc_asprintf (query, "%s%s", _find_prefix ("tag"), tag);
263     if (query->terms.count (term) != 0)
264         return NOTMUCH_STATUS_IGNORED;
265
266     _notmuch_string_list_append (query->exclude_terms, term);
267     return NOTMUCH_STATUS_SUCCESS;
268 }
269
270 /* We end up having to call the destructors explicitly because we had
271  * to use "placement new" in order to initialize C++ objects within a
272  * block that we allocated with talloc. So C++ is making talloc
273  * slightly less simple to use, (we wouldn't need
274  * talloc_set_destructor at all otherwise).
275  */
276 static int
277 _notmuch_messages_destructor (notmuch_mset_messages_t *messages)
278 {
279     messages->iterator.~MSetIterator ();
280     messages->iterator_end.~MSetIterator ();
281
282     return 0;
283 }
284
285 /* Return a query that matches messages with the excluded tags
286  * registered with query. The caller of this function has to combine the returned
287  * query appropriately.*/
288 static Xapian::Query
289 _notmuch_exclude_tags (notmuch_query_t *query)
290 {
291     Xapian::Query exclude_query = Xapian::Query::MatchNothing;
292
293     for (notmuch_string_node_t *term = query->exclude_terms->head; term;
294          term = term->next) {
295         exclude_query = Xapian::Query (Xapian::Query::OP_OR,
296                                        exclude_query, Xapian::Query (term->string));
297     }
298     return exclude_query;
299 }
300
301
302 notmuch_status_t
303 notmuch_query_search_messages_st (notmuch_query_t *query,
304                                   notmuch_messages_t **out)
305 {
306     return notmuch_query_search_messages (query, out);
307 }
308
309 notmuch_status_t
310 notmuch_query_search_messages (notmuch_query_t *query,
311                                notmuch_messages_t **out)
312 {
313     return _notmuch_query_search_documents (query, "mail", out);
314 }
315
316 notmuch_status_t
317 _notmuch_query_search_documents (notmuch_query_t *query,
318                                  const char *type,
319                                  notmuch_messages_t **out)
320 {
321     notmuch_database_t *notmuch = query->notmuch;
322     const char *query_string = query->query_string;
323     notmuch_mset_messages_t *messages;
324     notmuch_status_t status;
325
326     status = _notmuch_query_ensure_parsed (query);
327     if (status)
328         return status;
329
330     messages = talloc (query, notmuch_mset_messages_t);
331     if (unlikely (messages == NULL))
332         return NOTMUCH_STATUS_OUT_OF_MEMORY;
333
334     try {
335
336         messages->base.is_of_list_type = false;
337         messages->base.iterator = NULL;
338         messages->notmuch = notmuch;
339         new (&messages->iterator) Xapian::MSetIterator ();
340         new (&messages->iterator_end) Xapian::MSetIterator ();
341
342         talloc_set_destructor (messages, _notmuch_messages_destructor);
343
344         Xapian::Enquire enquire (*notmuch->xapian_db);
345         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
346                                                    _find_prefix ("type"),
347                                                    type));
348         Xapian::Query final_query, exclude_query;
349         Xapian::MSet mset;
350         Xapian::MSetIterator iterator;
351
352         if (strcmp (query_string, "") == 0 ||
353             strcmp (query_string, "*") == 0) {
354             final_query = mail_query;
355         } else {
356             final_query = Xapian::Query (Xapian::Query::OP_AND,
357                                          mail_query, query->xapian_query);
358         }
359         messages->base.excluded_doc_ids = NULL;
360
361         if ((query->omit_excluded != NOTMUCH_EXCLUDE_FALSE) && (query->exclude_terms)) {
362             exclude_query = _notmuch_exclude_tags (query);
363
364             if (query->omit_excluded == NOTMUCH_EXCLUDE_TRUE ||
365                 query->omit_excluded == NOTMUCH_EXCLUDE_ALL) {
366                 final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
367                                              final_query, exclude_query);
368             } else { /* NOTMUCH_EXCLUDE_FLAG */
369                 exclude_query = Xapian::Query (Xapian::Query::OP_AND,
370                                                exclude_query, final_query);
371
372                 enquire.set_weighting_scheme (Xapian::BoolWeight ());
373                 enquire.set_query (exclude_query);
374
375                 mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
376
377                 GArray *excluded_doc_ids = g_array_new (false, false, sizeof (unsigned int));
378
379                 for (iterator = mset.begin (); iterator != mset.end (); iterator++) {
380                     unsigned int doc_id = *iterator;
381                     g_array_append_val (excluded_doc_ids, doc_id);
382                 }
383                 messages->base.excluded_doc_ids = talloc (messages, _notmuch_doc_id_set);
384                 _notmuch_doc_id_set_init (query, messages->base.excluded_doc_ids,
385                                           excluded_doc_ids);
386                 g_array_unref (excluded_doc_ids);
387             }
388         }
389
390
391         enquire.set_weighting_scheme (Xapian::BoolWeight ());
392
393         switch (query->sort) {
394         case NOTMUCH_SORT_OLDEST_FIRST:
395             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, false);
396             break;
397         case NOTMUCH_SORT_NEWEST_FIRST:
398             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, true);
399             break;
400         case NOTMUCH_SORT_MESSAGE_ID:
401             enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, false);
402             break;
403         case NOTMUCH_SORT_UNSORTED:
404             break;
405         }
406
407         if (_debug_query ()) {
408             fprintf (stderr, "Exclude query is:\n%s\n",
409                      exclude_query.get_description ().c_str ());
410             fprintf (stderr, "Final query is:\n%s\n",
411                      final_query.get_description ().c_str ());
412         }
413
414         enquire.set_query (final_query);
415
416         mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
417
418         messages->iterator = mset.begin ();
419         messages->iterator_end = mset.end ();
420
421         *out = &messages->base;
422         return NOTMUCH_STATUS_SUCCESS;
423
424     } catch (const Xapian::Error &error) {
425         _notmuch_database_log (notmuch,
426                                "A Xapian exception occurred performing query: %s\n",
427                                error.get_msg ().c_str ());
428         _notmuch_database_log_append (notmuch,
429                                       "Query string was: %s\n",
430                                       query->query_string);
431
432         notmuch->exception_reported = true;
433         talloc_free (messages);
434         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
435     }
436 }
437
438 bool
439 _notmuch_mset_messages_valid (notmuch_messages_t *messages)
440 {
441     notmuch_mset_messages_t *mset_messages;
442
443     mset_messages = (notmuch_mset_messages_t *) messages;
444
445     return (mset_messages->iterator != mset_messages->iterator_end);
446 }
447
448 static Xapian::docid
449 _notmuch_mset_messages_get_doc_id (notmuch_messages_t *messages)
450 {
451     notmuch_mset_messages_t *mset_messages;
452
453     mset_messages = (notmuch_mset_messages_t *) messages;
454
455     if (! _notmuch_mset_messages_valid (&mset_messages->base))
456         return 0;
457
458     return *mset_messages->iterator;
459 }
460
461 notmuch_message_t *
462 _notmuch_mset_messages_get (notmuch_messages_t *messages)
463 {
464     notmuch_message_t *message;
465     Xapian::docid doc_id;
466     notmuch_private_status_t status;
467     notmuch_mset_messages_t *mset_messages;
468
469     mset_messages = (notmuch_mset_messages_t *) messages;
470
471     if (! _notmuch_mset_messages_valid (&mset_messages->base))
472         return NULL;
473
474     doc_id = *mset_messages->iterator;
475
476     message = _notmuch_message_create (mset_messages,
477                                        mset_messages->notmuch, doc_id,
478                                        &status);
479
480     if (message == NULL &&
481         status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
482         INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
483     }
484
485     if (messages->excluded_doc_ids &&
486         _notmuch_doc_id_set_contains (messages->excluded_doc_ids, doc_id))
487         notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, true);
488
489     return message;
490 }
491
492 void
493 _notmuch_mset_messages_move_to_next (notmuch_messages_t *messages)
494 {
495     notmuch_mset_messages_t *mset_messages;
496
497     mset_messages = (notmuch_mset_messages_t *) messages;
498
499     mset_messages->iterator++;
500 }
501
502 static bool
503 _notmuch_doc_id_set_init (void *ctx,
504                           notmuch_doc_id_set_t *doc_ids,
505                           GArray *arr)
506 {
507     unsigned int max = 0;
508     unsigned char *bitmap;
509
510     for (unsigned int i = 0; i < arr->len; i++)
511         max = MAX (max, g_array_index (arr, unsigned int, i));
512     bitmap = talloc_zero_array (ctx, unsigned char, DOCIDSET_WORD (max) + 1);
513
514     if (bitmap == NULL)
515         return false;
516
517     doc_ids->bitmap = bitmap;
518     doc_ids->bound = max + 1;
519
520     for (unsigned int i = 0; i < arr->len; i++) {
521         unsigned int doc_id = g_array_index (arr, unsigned int, i);
522         bitmap[DOCIDSET_WORD (doc_id)] |= 1 << DOCIDSET_BIT (doc_id);
523     }
524
525     return true;
526 }
527
528 bool
529 _notmuch_doc_id_set_contains (notmuch_doc_id_set_t *doc_ids,
530                               unsigned int doc_id)
531 {
532     if (doc_id >= doc_ids->bound)
533         return false;
534     return doc_ids->bitmap[DOCIDSET_WORD (doc_id)] & (1 << DOCIDSET_BIT (doc_id));
535 }
536
537 void
538 _notmuch_doc_id_set_remove (notmuch_doc_id_set_t *doc_ids,
539                             unsigned int doc_id)
540 {
541     if (doc_id < doc_ids->bound)
542         doc_ids->bitmap[DOCIDSET_WORD (doc_id)] &= ~(1 << DOCIDSET_BIT (doc_id));
543 }
544
545 /* Glib objects force use to use a talloc destructor as well, (but not
546  * nearly as ugly as the for messages due to C++ objects). At
547  * this point, I'd really like to have some talloc-friendly
548  * equivalents for the few pieces of glib that I'm using. */
549 static int
550 _notmuch_threads_destructor (notmuch_threads_t *threads)
551 {
552     if (threads->doc_ids)
553         g_array_unref (threads->doc_ids);
554
555     return 0;
556 }
557
558 notmuch_status_t
559 notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out)
560 {
561     return notmuch_query_search_threads (query, out);
562 }
563
564 notmuch_status_t
565 notmuch_query_search_threads (notmuch_query_t *query,
566                               notmuch_threads_t **out)
567 {
568     notmuch_threads_t *threads;
569     notmuch_messages_t *messages;
570     notmuch_status_t status;
571
572     threads = talloc (query, notmuch_threads_t);
573     if (threads == NULL)
574         return NOTMUCH_STATUS_OUT_OF_MEMORY;
575     threads->doc_ids = NULL;
576     talloc_set_destructor (threads, _notmuch_threads_destructor);
577
578     threads->query = query;
579
580     status = notmuch_query_search_messages (query, &messages);
581     if (status) {
582         talloc_free (threads);
583         return status;
584     }
585
586     threads->doc_ids = g_array_new (false, false, sizeof (unsigned int));
587     while (notmuch_messages_valid (messages)) {
588         unsigned int doc_id = _notmuch_mset_messages_get_doc_id (messages);
589         g_array_append_val (threads->doc_ids, doc_id);
590         notmuch_messages_move_to_next (messages);
591     }
592     threads->doc_id_pos = 0;
593
594     talloc_free (messages);
595
596     if (! _notmuch_doc_id_set_init (threads, &threads->match_set,
597                                     threads->doc_ids)) {
598         talloc_free (threads);
599         return NOTMUCH_STATUS_OUT_OF_MEMORY;
600     }
601
602     *out = threads;
603     return NOTMUCH_STATUS_SUCCESS;
604 }
605
606 void
607 notmuch_query_destroy (notmuch_query_t *query)
608 {
609     talloc_free (query);
610 }
611
612 notmuch_bool_t
613 notmuch_threads_valid (notmuch_threads_t *threads)
614 {
615     unsigned int doc_id;
616
617     if (! threads)
618         return false;
619
620     while (threads->doc_id_pos < threads->doc_ids->len) {
621         doc_id = g_array_index (threads->doc_ids, unsigned int,
622                                 threads->doc_id_pos);
623         if (_notmuch_doc_id_set_contains (&threads->match_set, doc_id))
624             break;
625
626         threads->doc_id_pos++;
627     }
628
629     return threads->doc_id_pos < threads->doc_ids->len;
630 }
631
632 notmuch_thread_t *
633 notmuch_threads_get (notmuch_threads_t *threads)
634 {
635     unsigned int doc_id;
636
637     if (! notmuch_threads_valid (threads))
638         return NULL;
639
640     doc_id = g_array_index (threads->doc_ids, unsigned int,
641                             threads->doc_id_pos);
642     return _notmuch_thread_create (threads->query,
643                                    threads->query->notmuch,
644                                    doc_id,
645                                    &threads->match_set,
646                                    threads->query->exclude_terms,
647                                    threads->query->omit_excluded,
648                                    threads->query->sort);
649 }
650
651 void
652 notmuch_threads_move_to_next (notmuch_threads_t *threads)
653 {
654     threads->doc_id_pos++;
655 }
656
657 void
658 notmuch_threads_destroy (notmuch_threads_t *threads)
659 {
660     talloc_free (threads);
661 }
662
663 notmuch_status_t
664 notmuch_query_count_messages_st (notmuch_query_t *query, unsigned *count_out)
665 {
666     return notmuch_query_count_messages (query, count_out);
667 }
668
669 notmuch_status_t
670 notmuch_query_count_messages (notmuch_query_t *query, unsigned *count_out)
671 {
672     return _notmuch_query_count_documents (query, "mail", count_out);
673 }
674
675 notmuch_status_t
676 _notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsigned *count_out)
677 {
678     notmuch_database_t *notmuch = query->notmuch;
679     const char *query_string = query->query_string;
680     Xapian::doccount count = 0;
681     notmuch_status_t status;
682
683     status = _notmuch_query_ensure_parsed (query);
684     if (status)
685         return status;
686
687     try {
688         Xapian::Enquire enquire (*notmuch->xapian_db);
689         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
690                                                    _find_prefix ("type"),
691                                                    type));
692         Xapian::Query final_query, exclude_query;
693         Xapian::MSet mset;
694
695         if (strcmp (query_string, "") == 0 ||
696             strcmp (query_string, "*") == 0) {
697             final_query = mail_query;
698         } else {
699             final_query = Xapian::Query (Xapian::Query::OP_AND,
700                                          mail_query, query->xapian_query);
701         }
702
703         exclude_query = _notmuch_exclude_tags (query);
704
705         final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
706                                      final_query, exclude_query);
707
708         enquire.set_weighting_scheme (Xapian::BoolWeight ());
709         enquire.set_docid_order (Xapian::Enquire::ASCENDING);
710
711         if (_debug_query ()) {
712             fprintf (stderr, "Exclude query is:\n%s\n",
713                      exclude_query.get_description ().c_str ());
714             fprintf (stderr, "Final query is:\n%s\n",
715                      final_query.get_description ().c_str ());
716         }
717
718         enquire.set_query (final_query);
719
720         /*
721          * Set the checkatleast parameter to the number of documents
722          * in the database to make get_matches_estimated() exact.
723          * Set the max parameter to 1 to avoid fetching documents we will discard.
724          */
725         mset = enquire.get_mset (0, 1,
726                                  notmuch->xapian_db->get_doccount ());
727
728         count = mset.get_matches_estimated ();
729
730     } catch (const Xapian::Error &error) {
731         _notmuch_database_log (notmuch,
732                                "A Xapian exception occurred performing query: %s\n",
733                                error.get_msg ().c_str ());
734         _notmuch_database_log_append (notmuch,
735                                       "Query string was: %s\n",
736                                       query->query_string);
737         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
738     }
739
740     *count_out = count;
741     return NOTMUCH_STATUS_SUCCESS;
742 }
743
744 notmuch_status_t
745 notmuch_query_count_threads_st (notmuch_query_t *query, unsigned *count)
746 {
747     return notmuch_query_count_threads (query, count);
748 }
749
750 notmuch_status_t
751 notmuch_query_count_threads (notmuch_query_t *query, unsigned *count)
752 {
753     notmuch_messages_t *messages;
754     GHashTable *hash;
755     notmuch_sort_t sort;
756     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
757
758     sort = query->sort;
759     query->sort = NOTMUCH_SORT_UNSORTED;
760     ret = notmuch_query_search_messages (query, &messages);
761     if (ret)
762         return ret;
763     query->sort = sort;
764     if (messages == NULL)
765         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
766
767     hash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);
768     if (hash == NULL) {
769         talloc_free (messages);
770         return NOTMUCH_STATUS_OUT_OF_MEMORY;
771     }
772
773     while (notmuch_messages_valid (messages)) {
774         notmuch_message_t *message = notmuch_messages_get (messages);
775         const char *thread_id = notmuch_message_get_thread_id (message);
776         char *thread_id_copy = talloc_strdup (messages, thread_id);
777         if (unlikely (thread_id_copy == NULL)) {
778             notmuch_message_destroy (message);
779             ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
780             goto DONE;
781         }
782         g_hash_table_insert (hash, thread_id_copy, NULL);
783         notmuch_message_destroy (message);
784         notmuch_messages_move_to_next (messages);
785     }
786
787     *count = g_hash_table_size (hash);
788
789   DONE:
790     g_hash_table_unref (hash);
791     talloc_free (messages);
792
793     return ret;
794 }
795
796 notmuch_database_t *
797 notmuch_query_get_database (const notmuch_query_t *query)
798 {
799     return query->notmuch;
800 }