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