Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
collection_query.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2009-2011 johannes hanika.
4 Copyright (C) 2010-2011 Henrik Andersson.
5 Copyright (C) 2011-2016 Tobias Ellinghaus.
6 Copyright (C) 2012, 2019-2022 Pascal Obry.
7 Copyright (C) 2025-2026 Aurelien PIERRE.
8
9 darktable is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 darktable is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with darktable. If not, see <http://www.gnu.org/licenses/>.
21*/
22
23#include <string.h>
24
26#include "database/database.h"
27#include "database/sql_debug.h"
29#include "common/datetime.h"
31#include "common/image.h"
32#include "common/utility.h"
33#include "system/dtpthread.h"
34#include "system/macros.h"
35#include "system/mem_alloc.h"
36
37// The one collection. dt_collection_new() has a single call site (dt_collection_init_global(),
38// reached from the GUI bootstrap only), so there is no
39// handle to pass around -- an argument no caller chooses is not a parameter.
40//
41// The composed SQL never leaves this file. Callers describe what they want with
42// dt_collection_query_set_rules() and read results back as ids and counts.
44static gchar **_where_ext = NULL; // composed from the rules below, never handed in
45static uint32_t _tagid = 0;
46static gchar *_query = NULL;
47static uint32_t _count = 0;
50static const char *const *_order_names = NULL;
51static int _order_names_count = 0;
52
53
54#define LIMIT_QUERY "LIMIT ?1, ?2"
55
56// for term should be an int initialized to and_operator_initial()
57// before use.
58#define and_operator_initial() (0)
59static char * and_operator(int *term)
60{
62 if(*term == 0)
63 {
64 *term = 1;
65 return "";
66 }
67 else
68 {
69 return " AND ";
70 }
71
72 assert(0); // Not reached.
73}
74
75#define or_operator_initial() (0)
76static char * or_operator(int *term)
77{
79 if(*term == 0)
80 {
81 *term = 1;
82 return "";
83 }
84 else
85 {
86 return " OR ";
87 }
88
89 assert(0); // Not reached.
90}
91
96
97void dt_collection_query_set_order_names(const char *const *names, const int count)
98{
99 // Borrowed, not copied: these are gettext's, and gettext outlives us.
101 _order_names_count = count;
102}
103
104static int _store(gchar *query)
105{
106 /* The generation advances only when the composed text actually changes. Consumers hash it
107 * in place of the text (gui/dtgtk/thumbtable.c), and every
108 * DT_COLLECTION_CHANGE_RELOAD recomposes an IDENTICAL query -- the enum is defined by it.
109 * An unconditional bump would turn each of those reloads (a rating, a tag, an import
110 * batch) into a full "collection changed" reset downstream. */
111 const gboolean changed = (g_strcmp0(_query, query) != 0);
114 if(changed) _generation++;
115 return 1;
116}
117
124static gchar *get_query_string(const dt_collection_properties_t property, const gchar *text,
125 const gboolean recursive)
126{
127 char *escaped_text = sqlite3_mprintf("%q", text);
128 const unsigned int escaped_length = strlen(escaped_text);
129 gchar *query = NULL;
130
131 switch(property)
132 {
133 case DT_COLLECTION_PROP_QUERY: // raw user-provided SQL WHERE expression (advanced)
134 // Intentionally NOT escaped: this is a power-user escape hatch that injects a raw
135 // read-only WHERE clause against the local library. A malformed expression makes the
136 // prepared statement fail gracefully (empty collection), it does not crash.
137 if(text && *text)
138 query = g_strdup_printf("(%s)", text);
139 else
140 query = g_strdup("1=1");
141 break;
142
143 case DT_COLLECTION_PROP_FILMROLL: // film roll
144 if(!(escaped_text && *escaped_text))
145 // clang-format off
146 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s%%'))",
148 // clang-format on
149 else
150 // clang-format off
151 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s'))",
153 // clang-format on
154 break;
155
156 case DT_COLLECTION_PROP_FOLDERS: // folders
157 {
158 // Recursion is normally the explicit `recursive` flag; a still-present trailing '*' is
159 // only recognized as a fallback for collections/presets saved before that flag existed,
160 // and for the Queries tab's raw rule editor, which has no checkbox and still relies on
161 // typing '*' by hand -- so this is permanent, not a transitional shim.
162 const gboolean has_star = (escaped_length > 0) && (escaped_text[escaped_length-1] == '*');
163 if(recursive || has_star)
164 {
166 // clang-format off
167 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s' OR folder LIKE '%s"
168 G_DIR_SEPARATOR_S "%%'))",
170 // clang-format on
171 }
172 // replace |% at the end with /% to only show subfolders
173 else if ((escaped_length > 1) && (strcmp(escaped_text+escaped_length-2, "|%") == 0 ))
174 {
176 // clang-format off
177 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s"
178 G_DIR_SEPARATOR_S "%%'))",
180 // clang-format on
181 }
182 else
183 {
184 // clang-format off
185 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s'))",
187 // clang-format on
188 }
189 }
190 break;
191
192 case DT_COLLECTION_PROP_COLORLABEL: // colorlabel
193 {
194 if(!(escaped_text && *escaped_text) || strcmp(escaped_text, "%") == 0)
195 // clang-format off
196 query = g_strdup_printf("(id IN (SELECT imgid FROM main.color_labels WHERE color IS NOT NULL))");
197 // clang-format on
198 else
199 {
200 int color = 0;
201 if(strcmp(escaped_text, _("red")) == 0)
202 color = 0;
203 else if(strcmp(escaped_text, _("yellow")) == 0)
204 color = 1;
205 else if(strcmp(escaped_text, _("green")) == 0)
206 color = 2;
207 else if(strcmp(escaped_text, _("blue")) == 0)
208 color = 3;
209 else if(strcmp(escaped_text, _("purple")) == 0)
210 color = 4;
211 // clang-format off
212 query = g_strdup_printf("(id IN (SELECT imgid FROM main.color_labels WHERE color=%d))", color);
213 // clang-format on
214 }
215 }
216 break;
217
218 case DT_COLLECTION_PROP_HISTORY: // history
219 {
220 if(strcmp(escaped_text, _("altered")) == 0)
221 {
222 query = g_strdup("EXISTS (SELECT 1 FROM main.history h WHERE h.imgid = id)");
223 }
224 else if(strcmp(escaped_text, _("unaltered")) == 0)
225 {
226 query = g_strdup("NOT EXISTS (SELECT 1 FROM main.history h WHERE h.imgid = id)");
227 }
228 else
229 {
230 query = g_strdup("1");
231 }
232 }
233 break;
234
235 case DT_COLLECTION_PROP_GEOTAGGING: // geotagging
236 {
237 const gboolean not_tagged = strcmp(escaped_text, _("not tagged")) == 0;
238 const gboolean no_location = strcmp(escaped_text, _("tagged")) == 0;
239 const gboolean all_tagged = strcmp(escaped_text, _("tagged*")) == 0;
240 char *escaped_text2 = g_strstr_len(escaped_text, -1, "|");
241 char *name_clause = g_strdup_printf("t.name LIKE \'%s\' || \'%s\'",
243
245 {
247 name_clause = g_strdup_printf("(t.name LIKE \'%s\' || \'%s\' OR t.name LIKE \'%s\' || \'%s|%%\')",
249 }
250
252 // clang-format off
253 query = g_strdup_printf("(id %s IN (SELECT id AS imgid FROM main.images "
254 "WHERE (longitude IS NOT NULL AND latitude IS NOT NULL))) ",
255 all_tagged ? "" : "not");
256 // clang-format on
257 else
258 // clang-format off
259 query = g_strdup_printf("(id IN (SELECT id AS imgid FROM main.images "
260 "WHERE (longitude IS NOT NULL AND latitude IS NOT NULL))"
261 "AND id %s IN (SELECT imgid FROM main.tagged_images AS ti"
262 " JOIN data.tags AS t"
263 " ON t.id = ti.tagid"
264 " AND %s)) ",
265 no_location ? "not" : "",
267 // clang-format on
268 }
269 break;
270
271 case DT_COLLECTION_PROP_LOCAL_COPY: // local copy
272 // clang-format off
273 query = g_strdup_printf("(id %s IN (SELECT id AS imgid FROM main.images WHERE (flags & %d))) ",
274 (strcmp(escaped_text, _("not copied locally")) == 0) ? "not" : "",
276 // clang-format on
277 break;
278
279 case DT_COLLECTION_PROP_CAMERA: // camera
280 // Start query with a false statement to avoid special casing the first condition
281 query = g_strdup_printf("((1=0)");
282 GList *lists = NULL;
283 dt_collection_get_makermodels(text, NULL, &lists);
285 {
286 GList *tuple = element->data;
287 char *clause = sqlite3_mprintf(" OR (maker = '%q' AND model = '%q')", tuple->data, tuple->next->data);
290 dt_free(tuple->data);
291 dt_free(tuple->next->data);
293 tuple = NULL;
294 }
295 g_list_free(lists);
296 lists = NULL;
298 break;
299
300 case DT_COLLECTION_PROP_TAG: // tag
301 {
302 if(!strcmp(escaped_text, _("not tagged")))
303 {
304 // clang-format off
305 query = g_strdup_printf("(id NOT IN (SELECT DISTINCT imgid FROM main.tagged_images "
306 "WHERE tagid NOT IN memory.darktable_tags))");
307 // clang-format on
308 }
309 else
310 {
311 if ((escaped_length > 0) && (escaped_text[escaped_length-1] == '*'))
312 {
313 // shift-click adds an asterix * to include items in and under this hierarchy
314 // without using a wildcard % which also would include similar named items
316 // clang-format off
317 query = g_strdup_printf("(id IN (SELECT imgid FROM main.tagged_images WHERE tagid IN "
318 "(SELECT id FROM data.tags "
319 "WHERE LOWER(name) = LOWER('%s')"
320 " OR SUBSTR(LOWER(name), 1, LENGTH('%s') + 1) = LOWER('%s|'))))",
322 // clang-format on
323 }
324 else if ((escaped_length > 0) && (escaped_text[escaped_length-1] == '%'))
325 {
326 // ends with % or |%
328 // clang-format off
329 query = g_strdup_printf("(id IN (SELECT imgid FROM main.tagged_images WHERE tagid IN "
330 "(SELECT id FROM data.tags WHERE SUBSTR(LOWER(name), 1, LENGTH('%s')) = LOWER('%s'))))",
332 // clang-format on
333 }
334 else
335 {
336 // default
337 // clang-format off
338 query = g_strdup_printf("(id IN (SELECT imgid FROM main.tagged_images WHERE tagid IN "
339 "(SELECT id FROM data.tags WHERE LOWER(name) = LOWER('%s'))))",
341 // clang-format on
342 }
343 }
344 }
345 break;
346
347 case DT_COLLECTION_PROP_LENS: // lens
348 query = g_strdup_printf("(lens LIKE '%%%s%%')", escaped_text);
349 break;
350
351 case DT_COLLECTION_PROP_FOCAL_LENGTH: // focal length
352 {
353 gchar *operator, *number1, *number2;
355
356 if(operator && strcmp(operator, "[]") == 0)
357 {
358 if(number1 && number2)
359 query = g_strdup_printf("((focal_length >= %s) AND (focal_length <= %s))", number1, number2);
360 }
361 else if(operator && number1)
362 query = g_strdup_printf("(focal_length %s %s)", operator, number1);
363 else if(number1)
364 // clang-format off
365 query = g_strdup_printf("(CAST(focal_length AS INTEGER) = CAST(%s AS INTEGER))", number1);
366 // clang-format on
367 else
368 query = g_strdup_printf("(focal_length LIKE '%%%s%%')", escaped_text);
369
370 dt_free(operator);
373 }
374 break;
375
376 case DT_COLLECTION_PROP_ISO: // iso
377 {
378 gchar *operator, *number1, *number2;
380
381 if(operator && strcmp(operator, "[]") == 0)
382 {
383 if(number1 && number2)
384 query = g_strdup_printf("((iso >= %s) AND (iso <= %s))", number1, number2);
385 }
386 else if(operator && number1)
387 query = g_strdup_printf("(iso %s %s)", operator, number1);
388 else if(number1)
389 query = g_strdup_printf("(iso = %s)", number1);
390 else
391 query = g_strdup_printf("(iso LIKE '%%%s%%')", escaped_text);
392
393 dt_free(operator);
396 }
397 break;
398
399 case DT_COLLECTION_PROP_APERTURE: // aperture
400 {
401 gchar *operator, *number1, *number2;
403
404 if(operator && strcmp(operator, "[]") == 0)
405 {
406 if(number1 && number2)
407 // clang-format off
408 query = g_strdup_printf("((ROUND(aperture,1) >= %s) AND (ROUND(aperture,1) <= %s))", number1,
409 number2);
410 // clang-format on
411 }
412 else if(operator && number1)
413 query = g_strdup_printf("(ROUND(aperture,1) %s %s)", operator, number1);
414 else if(number1)
415 query = g_strdup_printf("(ROUND(aperture,1) = %s)", number1);
416 else
417 query = g_strdup_printf("(ROUND(aperture,1) LIKE '%%%s%%')", escaped_text);
418
419 dt_free(operator);
422 }
423 break;
424
425 case DT_COLLECTION_PROP_EXPOSURE: // exposure
426 {
427 gchar *operator, *number1, *number2;
429
430 if(operator && strcmp(operator, "[]") == 0)
431 {
432 if(number1 && number2)
433 // clang-format off
434 query = g_strdup_printf("((exposure >= %s - 1.0/100000) AND (exposure <= %s + 1.0/100000))", number1,
435 number2);
436 // clang-format on
437 }
438 else if(operator && number1)
439 query = g_strdup_printf("(exposure %s %s)", operator, number1);
440 else if(number1)
441 // clang-format off
442 query = g_strdup_printf("(CASE WHEN exposure < 0.4 THEN ((exposure >= %s - 1.0/100000) AND (exposure <= %s + 1.0/100000)) "
443 "ELSE (ROUND(exposure,2) >= %s - 1.0/100000) AND (ROUND(exposure,2) <= %s + 1.0/100000) END)",
445 // clang-format on
446 else
447 query = g_strdup_printf("(exposure LIKE '%%%s%%')", escaped_text);
448
449 dt_free(operator);
452 }
453 break;
454
455 case DT_COLLECTION_PROP_FILENAME: // filename
456 {
458
459 for (GList *l = list; l; l = g_list_next(l))
460 {
461 char *name = (char*)l->data; // remember the original content of this list node
462 l->data = g_strdup_printf("(filename LIKE '%%%s%%')", name);
463 dt_free(name); // free the original filename
464 }
465
466 char *subquery = dt_util_glist_to_str(" OR ", list);
467 query = g_strdup_printf("(%s)", subquery);
469 g_list_free_full(list, dt_free_gpointer); // free the SQL clauses as well as the list
470 list = NULL;
471
472 break;
473 }
480 {
481 const int local_property = property;
482 char *colname = NULL;
483
484 switch(local_property)
485 {
486 case DT_COLLECTION_PROP_DAY: colname = "datetime_taken" ; break ;
487 case DT_COLLECTION_PROP_TIME: colname = "datetime_taken" ; break ;
488 case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: colname = "import_timestamp" ; break ;
489 case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: colname = "change_timestamp" ; break ;
490 case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: colname = "export_timestamp" ; break ;
491 case DT_COLLECTION_PROP_PRINT_TIMESTAMP: colname = "print_timestamp" ; break ;
492 }
493 gchar *operator, *number1, *number2;
495 if(number1 && number1[strlen(number1) - 1] == '%')
496 number1[strlen(number1) - 1] = '\0';
499
500 if(strcmp(operator, "[]") == 0)
501 {
502 if(number1 && number2)
503 query = g_strdup_printf("((%s >= %" G_GINT64_FORMAT ") AND (%s <= %" G_GINT64_FORMAT "))", colname, nb1, colname, nb2);
504 }
505 else if((strcmp(operator, "=") == 0 || strcmp(operator, "") == 0) && number1 && number2)
506 query = g_strdup_printf("((%s >= %" G_GINT64_FORMAT ") AND (%s <= %" G_GINT64_FORMAT "))", colname, nb1, colname, nb2);
507 else if(strcmp(operator, "<>") == 0 && number1 && number2)
508 // a date/period spans the range [nb1;nb2]; "not equal" means anything OUTSIDE it
509 // (before its start OR after its end). AND here would be unsatisfiable (nb1 < nb2).
510 query = g_strdup_printf("((%s < %" G_GINT64_FORMAT ") OR (%s > %" G_GINT64_FORMAT "))", colname, nb1, colname, nb2);
511 else if(number1)
512 query = g_strdup_printf("(%s %s %" G_GINT64_FORMAT ")", colname, operator, nb1);
513 else
514 query = g_strdup("1 = 1");
515
516 dt_free(operator);
519 break;
520 }
521
522 case DT_COLLECTION_PROP_GROUPING: // grouping
523 query = g_strdup_printf("(id %s group_id)", (strcmp(escaped_text, _("group leaders")) == 0) ? "=" : "!=");
524 break;
525
526 case DT_COLLECTION_PROP_MODULE: // dev module
527 {
528 // clang-format off
529 query = g_strdup_printf("(id IN (SELECT imgid AS id FROM main.history AS h "
530 "JOIN memory.darktable_iop_names AS m ON m.operation = h.operation "
531 "WHERE h.enabled = 1 AND m.name LIKE '%s'))", escaped_text);
532 // clang-format on
533 }
534 break;
535
536 case DT_COLLECTION_PROP_ORDER: // module order
537 {
538 // The text here is a LOCALISED module-order name, and turning one back into an id is
539 // presentation: this module cannot see translations. The caller installs the resolver.
540 const int i = _order_resolver ? _order_resolver(escaped_text) : -1;
541 if(i >= 0)
542 // clang-format off
543 query = g_strdup_printf("(id IN (SELECT imgid FROM main.module_order WHERE version = %d))", i);
544 // clang-format on
545 else
546 // clang-format off
547 query = g_strdup_printf("(id NOT IN (SELECT imgid FROM main.module_order))");
548 // clang-format on
549 }
550 break;
551
552 case DT_COLLECTION_PROP_RATING: // image rating
553 {
554 gchar *operator, *number1, *number2;
556
557 if(operator && strcmp(operator, "[]") == 0)
558 {
559 if(number1 && number2)
560 {
561 if(atoi(number1) == -1)
562 { // rejected + star rating
563 // clang-format off
564 query = g_strdup_printf("(flags & 7 >= %s AND flags & 7 <= %s)", number1, number2);
565 // clang-format on
566 }
567 else
568 { // non-rejected + star rating
569 // clang-format off
570 query = g_strdup_printf("((flags & 8 == 0) AND (flags & 7 >= %s AND flags & 7 <= %s))", number1, number2);
571 // clang-format on
572 }
573 }
574 }
575 else if(operator && number1)
576 {
577 if(g_strcmp0(operator, "<=") == 0 || g_strcmp0(operator, "<") == 0)
578 { // all below rating + rejected
579 // clang-format off
580 query = g_strdup_printf("(flags & 8 == 8 OR flags & 7 %s %s)", operator, number1);
581 // clang-format on
582 }
583 else if(g_strcmp0(operator, ">=") == 0 || g_strcmp0(operator, ">") == 0)
584 {
585 if(atoi(number1) >= 0)
586 { // non rejected above rating
587 // clang-format off
588 query = g_strdup_printf("(flags & 8 == 0 AND flags & 7 %s %s)", operator, number1);
589 // clang-format on
590 }
591 // otherwise no filter (rejected + all ratings)
592 }
593 else
594 { // <> exclusion operator
595 if(atoi(number1) == -1)
596 { // all except rejected
597 query = g_strdup_printf("(flags & 8 == 0)");
598 }
599 else
600 { // all except star rating (including rejected)
601 query = g_strdup_printf("(flags & 8 == 8 OR flags & 7 %s %s)", operator, number1);
602 }
603 }
604 }
605 else if(number1)
606 {
607 if(atoi(number1) == -1)
608 { // rejected only
609 query = g_strdup_printf("(flags & 8 == 8)");
610 }
611 else
612 { // non-rejected + star rating
613 query = g_strdup_printf("(flags & 8 == 0 AND flags & 7 == %s)", number1);
614 }
615 }
616
617 dt_free(operator);
620 }
621 break;
622
623 default:
624 {
625 if(property >= DT_COLLECTION_PROP_METADATA
627 {
629 if(strcmp(escaped_text, _("not defined")) != 0)
630 // clang-format off
631 query = g_strdup_printf("(id IN (SELECT id FROM main.meta_data WHERE key = %d AND value "
632 "LIKE '%%%s%%'))", keyid, escaped_text);
633 // clang-format on
634 else
635 // clang-format off
636 query = g_strdup_printf("(id NOT IN (SELECT id FROM main.meta_data WHERE key = %d))",
637 keyid);
638 // clang-format off
639 }
640 }
641 break;
642 }
644
645 if(IS_NULL_PTR(query)) // We've screwed up and not done a query string, send a placeholder
646 query = g_strdup_printf("(1=1)");
647
648 return query;
649}
650
651static dt_collection_name_value_t *_name_value_new(char *name, int id, int count, int status)
652{
654 v->name = name;
655 v->id = id;
656 v->count = count;
657 v->status = status;
658 return v;
659}
660
667static gchar *_extended_where_excluding(const int exclude, const gboolean apply_exclude)
668{
669 gchar *complete_string = g_strdup("");
670 if(_where_ext && apply_exclude)
671 {
672 for(int i = 0; !IS_NULL_PTR(_where_ext[i]); i++)
673 {
674 if(i == exclude) continue;
676 }
677 }
678 gchar *where_ext = g_strdup_printf("(1=1%s)", complete_string);
680 return where_ext;
681}
682
683static gchar *_extended_where(void)
684{
686 gchar *where_ext = g_strdup_printf("(1=1%s)", complete_string);
688 return where_ext;
689}
690
691static void _set_selq_pre_sort(char **selq_pre){
692 const uint32_t tagid = _tagid;
693 char tag[16] = { 0 };
694 snprintf(tag, sizeof(tag), "%u", tagid);
695
696 // clang-format off
698 "SELECT DISTINCT mi.id FROM (SELECT"
699 " id, group_id, film_id, filename, datetime_taken, "
700 " flags, version, aspect_ratio,"
701 " maker, model, lens, aperture, exposure, focal_length,"
702 " iso, import_timestamp, change_timestamp,"
703 " export_timestamp, print_timestamp"
704 " FROM main.images AS mi %s%s WHERE ",
705 tagid ? " LEFT JOIN main.tagged_images AS ti"
706 " ON ti.imgid = mi.id AND ti.tagid = " : "",
707 tagid ? tag : "");
708 // clang-format on
709}
710
711static gchar *_sort_query(void){
712 gchar *sq = NULL;
713 const gchar *order = (_params.descending) ? "DESC" : "ASC";
714
715 switch(_params.sort)
716 {
722 {
723 const int local_order = _params.sort;
724 char *colname;
725
726 switch(local_order)
727 {
728 case DT_COLLECTION_SORT_DATETIME: colname = "datetime_taken" ; break ;
729 case DT_COLLECTION_SORT_IMPORT_TIMESTAMP: colname = "import_timestamp" ; break ;
730 case DT_COLLECTION_SORT_CHANGE_TIMESTAMP: colname = "change_timestamp" ; break ;
731 case DT_COLLECTION_SORT_EXPORT_TIMESTAMP: colname = "export_timestamp" ; break ;
732 case DT_COLLECTION_SORT_PRINT_TIMESTAMP: colname = "print_timestamp" ; break ;
733 default: colname = "";
734 }
735 // clang-format off
736 sq = g_strdup_printf("ORDER BY %s %s", colname, order);
737 // clang-format on
738 break;
739 }
740
742 // clang-format off
743 sq = g_strdup_printf("ORDER BY CASE WHEN flags & 8 = 8 THEN -1 ELSE flags & 7 END %s", order);
744 // clang-format on
745 break;
746
748 // clang-format off
749 sq = g_strdup_printf("ORDER BY filename %s", order);
750 // clang-format on
751 break;
752
754 // clang-format off
755 sq = g_strdup_printf("ORDER BY mi.id %s", order);
756 // clang-format on
757 break;
758
760 // clang-format off
761 sq = g_strdup_printf("ORDER BY color %s", order);
762 // clang-format on
763 break;
764
766 // clang-format off
767 sq = g_strdup_printf("ORDER BY group_id %s, mi.id-group_id != 0", order);
768 // clang-format on
769 break;
770
772 // clang-format off
773 sq = g_strdup_printf("ORDER BY folder %s", order);
774 // clang-format on
775 break;
776
778 // clang-format off
779 sq = g_strdup_printf("ORDER BY m.value %s", order);
780 // clang-format on
781 break;
782
784 default:/*fall through for default*/
785 // shouldn't happen
786 // clang-format off
787 sq = g_strdup_printf("ORDER BY mi.id %s", order);
788 // clang-format on
789 break;
790 }
791
792 // Finish with unique IDs in case we have aliasing
793 // try to keep grouped images next to each other, then similar files
794 sq = dt_util_dstrcat(sq, ", group_id ASC, mi.id-group_id != 0, filename ASC, version ASC, mi.id ASC");
795
796 return sq;
797}
798
799static int _recompose(void){
800 uint32_t result;
801 gchar *wq, *sq, *selq_pre, *selq_post, *query;
802 wq = sq = selq_pre = selq_post = query = NULL;
803
804 /* build where part */
805 gchar *where_ext = _extended_where();
807 {
809 }
811 {
812 char *rejected_check = g_strdup_printf("((flags & %d) = %d)", DT_IMAGE_REJECTED, DT_IMAGE_REJECTED);
813 int and_term = 1; // that effectively makes the use of and_operator() useless
814
815 // DON'T SELECT IMAGES MARKED TO BE DELETED.
816 wq = g_strdup_printf(" ((flags & %d) != %d) ", DT_IMAGE_REMOVE, DT_IMAGE_REMOVE);
817
818 /* From there, the other arguments are OR so we need parentheses if any rating filter is used */
819 gboolean got_rating_filter
824
827
829 /* Rejected was a mutually-exclusive rating in initial design, but got converted to
830 a toggle state circa 2019, aka images can now have a rating AND be rejected.
831 Which sucks because users will not expect rejected images to show when they target n stars ratings.
832 Aka we collect images that are rejected OR (have rating == n AND are not rejected).
833 Also, because rating flags are bitmasks but not octal, we can't build a single bitmask to
834 turn into a single SQL request
835 */
838
840 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
842
844 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
846
848 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
850
852 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
854
856 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
858
860 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
862
863 /* Closing the OR parentheses */
865 wq = dt_util_dstrcat(wq, ") ");
866
867 gboolean got_altered_filter
869
872
875 // clang-format off
876 wq = dt_util_dstrcat(wq, " %s id IN (SELECT imgid FROM main.history)",
878 // clang-format on
879
881 // clang-format off
882 wq = dt_util_dstrcat(wq, " %s id NOT IN (SELECT imgid FROM main.history) ",
884 // clang-format on
885
887 wq = dt_util_dstrcat(wq, ") ");
888
889 /* add text filter if any */
891 {
892 // clang-format off
893 wq = dt_util_dstrcat(wq, " %s id IN (SELECT id FROM main.meta_data WHERE value LIKE '%s'"
894 " UNION SELECT imgid AS id FROM main.tagged_images AS ti, data.tags AS t"
895 " WHERE t.id=ti.tagid AND (t.name LIKE '%s' OR t.synonyms LIKE '%s')"
896 " UNION SELECT id FROM main.images"
897 " WHERE filename LIKE '%s'"
898 " UNION SELECT i.id FROM main.images AS i, main.film_rolls AS fr"
899 " WHERE fr.id=i.film_id AND fr.folder LIKE '%s')",
905 // clang-format on
906 }
907
908 /* add colorlabel filter if any */
912
914 {
915 int color_mask = 0;
926
927 // color_mask = 31 when all flags are on
929
931
932 // clang-format off
933 if(color_mask > 0)
934 wq = dt_util_dstrcat(wq, " %s id IN (SELECT id FROM"
935 " (SELECT imgid AS id, SUM(1 << color) AS mask FROM main.color_labels GROUP BY imgid)"
936 " WHERE ((mask & %i) > 0))",
938
940 wq = dt_util_dstrcat(wq, " %s id NOT IN (SELECT id FROM"
941 " (SELECT imgid AS id, SUM(1 << color) AS mask FROM main.color_labels GROUP BY imgid)"
942 " WHERE ((mask & 31) > 0))",
944
945 // clang-format on
946 wq = dt_util_dstrcat(wq, ")");
947 }
948
949 /* add where ext if wanted */
952
954 }
955 else
956 {
957 // No filter set: no collection, because filters are toggle in.
958 // Just setup some bullshit condition impossible to match.
959 wq = g_strdup(" id=0");
960 }
961
963
964 /* build select part includes where */
965 /* only COLOR */
968 {
970 // clang-format off
971 selq_post = dt_util_dstrcat(selq_post, ") AS mi LEFT OUTER JOIN main.color_labels AS b ON mi.id = b.imgid");
972 // clang-format on
973 }
974 /* only PATH */
977 {
979 // clang-format off
981 (selq_post,
982 ") AS mi JOIN (SELECT id AS film_rolls_id, folder FROM main.film_rolls) ON film_id = film_rolls_id");
983 // clang-format on
984 }
985 /* only TITLE */
988 {
990 // clang-format off
991 selq_post = dt_util_dstrcat(selq_post, ") AS mi LEFT OUTER JOIN main.meta_data AS m ON mi.id = m.id AND m.key = %d ",
993 // clang-format on
994 }
996 {
997 const uint32_t tagid = _tagid;
998 char tag[16] = { 0 };
999 snprintf(tag, sizeof(tag), "%u", tagid);
1000 // clang-format off
1002 "SELECT DISTINCT mi.id FROM (SELECT"
1003 " id, group_id, film_id, filename, datetime_taken, "
1004 " flags, version, %s position, aspect_ratio,"
1005 " maker, model, lens, aperture, exposure, focal_length,"
1006 " iso, import_timestamp, change_timestamp,"
1007 " export_timestamp, print_timestamp"
1008 " FROM main.images AS mi %s%s ) AS mi ",
1009 tagid ? "CASE WHEN ti.position IS NULL THEN 0 ELSE ti.position END AS" : "",
1010 tagid ? " LEFT JOIN main.tagged_images AS ti"
1011 " ON ti.imgid = mi.id AND ti.tagid = " : "",
1012 tagid ? tag : "");
1013 // clang-format on
1014 }
1015 else
1016 {
1017 const uint32_t tagid = _tagid;
1018 char tag[16] = { 0 };
1019 snprintf(tag, sizeof(tag), "%u", tagid);
1020 // clang-format off
1022 "SELECT DISTINCT mi.id FROM (SELECT"
1023 " id, group_id, film_id, filename, datetime_taken, "
1024 " flags, version, %s position, aspect_ratio,"
1025 " maker, model, lens, aperture, exposure, focal_length,"
1026 " iso, import_timestamp, change_timestamp,"
1027 " export_timestamp, print_timestamp"
1028 " FROM main.images AS mi %s%s ) AS mi WHERE ",
1029 tagid ? "CASE WHEN ti.position IS NULL THEN 0 ELSE ti.position END AS" : "",
1030 tagid ? " LEFT JOIN main.tagged_images AS ti"
1031 " ON ti.imgid = mi.id AND ti.tagid = " : "",
1032 tagid ? tag : "");
1033 // clang-format on
1034 }
1035
1036
1037 /* build sort order part */
1040 {
1041 sq = _sort_query();
1042 }
1043
1044 /* store the new query */
1045 query
1046 = dt_util_dstrcat(query, "%s%s%s %s%s", selq_pre, wq, selq_post ? selq_post : "", sq ? sq : "",
1048
1049 result = _store(query);
1050
1051 /* free memory used */
1052 dt_free(sq);
1053 dt_free(wq);
1056 dt_free(query);
1057
1058 return result;
1059}
1060
1061static uint32_t _compute_count(void){
1062 uint32_t count = 1;
1065 "SELECT COUNT(DISTINCT imgid) from memory.collected_images",
1066 -1, &stmt, NULL);
1067 if(IS_NULL_PTR(stmt)) return count;
1070 _count = count;
1071 return count;
1072}
1073
1074
1075static const gchar *_ensure_query(void)
1076{
1078 return _query;
1079}
1080
1086static gchar **_compose_where_ext(const dt_collection_rule_t *rules, const int n_rules)
1087{
1088 static const char *const conj[] = { "AND", "OR", "AND NOT" };
1089
1090 gchar **parts = g_malloc0_n(n_rules + 1, sizeof(gchar *));
1091 for(int i = 0; i < n_rules; i++)
1092 {
1093 const dt_collection_rule_t *r = &rules[i];
1094 const int mode = CLAMP(r->mode, 0, 2);
1095
1096 if(IS_NULL_PTR(r->text) || r->text[0] == '\0')
1097 {
1098 parts[i] = g_strdup((mode == 1) ? " OR 1=1" : "");
1099 }
1100 else
1101 {
1102 gchar *where = get_query_string(r->property, r->text, r->recursive);
1103 parts[i] = g_strdup_printf(" %s %s", conj[mode], where);
1104 dt_free(where);
1105 }
1106 }
1107 return parts;
1108}
1109
1111 const dt_collection_rule_t *rules, const int n_rules,
1112 const uint32_t tagid)
1113{
1114 if(IS_NULL_PTR(params)) return 0;
1115
1116 // Copy the rules in: the caller owns its own and may change them under us.
1118 _params = *params;
1119 _params.text_filter = params->text_filter ? g_strdup(params->text_filter) : NULL;
1120
1122 _where_ext = (rules && n_rules > 0) ? _compose_where_ext(rules, n_rules) : NULL;
1123 _tagid = tagid;
1124
1125 return _recompose();
1126}
1127
1129{
1130 return _recompose();
1131}
1132
1134{
1135 return _count;
1136}
1137
1138void dt_collection_query_set_iop_names(const dt_iop_name_row_t *rows, const size_t count)
1139{
1140 if(IS_NULL_PTR(rows) || count == 0) return;
1141
1142 // Faster than building a huge VALUES string: reuse a prepared statement and bind per module.
1145 "INSERT INTO memory.darktable_iop_names (operation, name) VALUES (?1, ?2)",
1146 -1, &stmt, NULL);
1147 if(IS_NULL_PTR(stmt)) return;
1148
1150 for(size_t i = 0; i < count; i++)
1151 {
1154 DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, rows[i].operation, -1, SQLITE_TRANSIENT);
1157 }
1160}
1161
1163{
1164 // Bumped by every accepted recomposition. Callers that used to hash the query text to notice a
1165 // collection change compare this instead: one number that cannot go stale field by field.
1166 return _generation;
1167}
1168
1169GList *dt_collection_query_get_group_members(const int32_t group_id, const int32_t exclude_imgid)
1170{
1171 const gchar *collection_query = _ensure_query();
1172 if(IS_NULL_PTR(collection_query)) return NULL;
1173
1175 // clang-format off
1176 gchar *query = g_strdup_printf("SELECT id"
1177 " FROM main.images"
1178 " WHERE group_id = %d AND id IN (%s)",
1179 group_id, collection_query);
1180 // clang-format on
1182 dt_free(query);
1183
1184 GList *ids = NULL;
1185 while(sqlite3_step(stmt) == SQLITE_ROW)
1186 {
1187 const int32_t id = sqlite3_column_int(stmt, 0);
1188 if(id != exclude_imgid) ids = g_list_prepend(ids, GINT_TO_POINTER(id));
1189 }
1191 return g_list_reverse(ids);
1192}
1193
1195{
1196 if(IS_NULL_PTR(req)) return NULL;
1197 const dt_collection_properties_t property = req->property;
1198
1199 GList *out = NULL;
1200 gchar *where_ext = _extended_where_excluding(req->exclude_rule, req->apply_exclude);
1201
1202 // Camera is special: it groups on two text columns and combines them into a display name.
1203 if(property == DT_COLLECTION_PROP_CAMERA)
1204 {
1205 gchar *q = g_strdup_printf("SELECT maker, model, COUNT(*) AS count FROM main.images AS mi"
1206 " WHERE %s GROUP BY maker, model", where_ext);
1210 int index = 0;
1211 while(stmt && sqlite3_step(stmt) == SQLITE_ROW)
1212 {
1213 const char *maker = (const char *)sqlite3_column_text(stmt, 0);
1214 const char *model = (const char *)sqlite3_column_text(stmt, 1);
1217 }
1219 g_free(q);
1220 return g_list_reverse(out);
1221 }
1222
1223 const gboolean is_date = property == DT_COLLECTION_PROP_DAY || property == DT_COLLECTION_PROP_TIME
1228 const gboolean has_status
1229 = (property == DT_COLLECTION_PROP_FOLDERS || property == DT_COLLECTION_PROP_FILMROLL);
1230 gchar *query = NULL;
1231
1232 switch(property)
1233 {
1235 query = g_strdup_printf("SELECT folder, film_rolls_id, COUNT(*) AS count, status"
1236 " FROM main.images AS mi"
1237 " JOIN (SELECT fr.id AS film_rolls_id, folder, status"
1238 " FROM main.film_rolls AS fr"
1239 " JOIN memory.film_folder AS ff ON fr.id = ff.id)"
1240 " ON film_id = film_rolls_id"
1241 " WHERE %s GROUP BY folder, film_rolls_id", where_ext);
1242 break;
1243
1245 query = g_strdup_printf("SELECT name, 1 AS tagid, SUM(count) AS count"
1246 " FROM (SELECT tagid, COUNT(*) as count"
1247 " FROM main.images AS mi JOIN main.tagged_images ON id = imgid"
1248 " WHERE %s GROUP BY tagid)"
1249 " JOIN (SELECT name, id AS tag_id FROM data.tags)"
1250 " ON tagid = tag_id GROUP BY name", where_ext);
1251 query = dt_util_dstrcat(query, " UNION ALL "
1252 "SELECT '%s' AS name, 0 as id, COUNT(*) AS count "
1253 "FROM main.images AS mi WHERE mi.id NOT IN"
1254 " (SELECT DISTINCT imgid FROM main.tagged_images AS ti"
1255 " WHERE ti.tagid NOT IN memory.darktable_tags)",
1256 _("not tagged"));
1257 break;
1258
1260 query = g_strdup_printf("SELECT CASE WHEN mi.longitude IS NULL OR mi.latitude IS null THEN '%s'"
1261 " ELSE CASE WHEN ta.imgid IS NULL THEN '%s' ELSE '%s' || ta.tagname END"
1262 " END AS name, ta.tagid AS tag_id, COUNT(*) AS count"
1263 " FROM main.images AS mi"
1264 " LEFT JOIN (SELECT imgid, t.id AS tagid, SUBSTR(t.name, %d) AS tagname"
1265 " FROM main.tagged_images AS ti JOIN data.tags AS t ON ti.tagid = t.id"
1266 " JOIN data.locations AS l ON l.tagid = t.id) AS ta ON ta.imgid = mi.id"
1267 " WHERE %s GROUP BY name, tag_id",
1268 _("not tagged"), _("tagged"), _("tagged"),
1270 break;
1271
1273 query = g_strdup_printf("SELECT (datetime_taken / 86400000000) * 86400000000 AS date, 1, COUNT(*) AS count"
1274 " FROM main.images AS mi"
1275 " WHERE datetime_taken IS NOT NULL AND datetime_taken <> 0 AND %s"
1276 " GROUP BY date", where_ext);
1277 break;
1278
1284 {
1285 char *colname = NULL;
1286 switch(property)
1287 {
1288 case DT_COLLECTION_PROP_TIME: colname = "datetime_taken"; break;
1289 case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: colname = "import_timestamp"; break;
1290 case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: colname = "change_timestamp"; break;
1291 case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: colname = "export_timestamp"; break;
1292 case DT_COLLECTION_PROP_PRINT_TIMESTAMP: colname = "print_timestamp"; break;
1293 default: break; // unreachable: outer switch already restricts to the timestamp cases
1294 }
1295 query = g_strdup_printf("SELECT %s AS date, 1, COUNT(*) AS count FROM main.images AS mi"
1296 " WHERE %s IS NOT NULL AND %s <> 0 AND %s GROUP BY date",
1298 break;
1299 }
1300
1302 query = g_strdup_printf("SELECT CASE WHEN EXISTS (SELECT 1 FROM main.history h WHERE h.imgid = mi.id)"
1303 " THEN '%s' ELSE '%s' END as altered, 1, COUNT(*) AS count"
1304 " FROM main.images AS mi WHERE %s GROUP BY altered ORDER BY altered ASC",
1305 _("altered"), _("unaltered"), where_ext);
1306 break;
1307
1309 query = g_strdup_printf("SELECT CASE WHEN (flags & %d) THEN '%s' ELSE '%s' END as lcp, 1, COUNT(*) AS count"
1310 " FROM main.images AS mi WHERE %s GROUP BY lcp ORDER BY lcp ASC",
1311 DT_IMAGE_LOCAL_COPY, _("copied locally"), _("not copied locally"), where_ext);
1312 break;
1313
1315 query = g_strdup_printf("SELECT CASE color WHEN 0 THEN '%s' WHEN 1 THEN '%s' WHEN 2 THEN '%s'"
1316 " WHEN 3 THEN '%s' WHEN 4 THEN '%s' ELSE '' END, color, COUNT(*) AS count"
1317 " FROM main.images AS mi"
1318 " JOIN (SELECT imgid AS color_labels_id, color FROM main.color_labels)"
1319 " ON id = color_labels_id WHERE %s GROUP BY color ORDER BY color DESC",
1320 _("red"), _("yellow"), _("green"), _("blue"), _("purple"), where_ext);
1321 break;
1322
1324 query = g_strdup_printf("SELECT lens, 1, COUNT(*) AS count FROM main.images AS mi WHERE %s"
1325 " GROUP BY lens ORDER BY lens", where_ext);
1326 break;
1327
1329 query = g_strdup_printf("SELECT CAST(focal_length AS INTEGER) AS focal_length, 1, COUNT(*) AS count"
1330 " FROM main.images AS mi WHERE %s GROUP BY CAST(focal_length AS INTEGER)"
1331 " ORDER BY CAST(focal_length AS INTEGER)", where_ext);
1332 break;
1333
1335 query = g_strdup_printf("SELECT CAST(iso AS INTEGER) AS iso, 1, COUNT(*) AS count"
1336 " FROM main.images AS mi WHERE %s GROUP BY iso ORDER BY iso", where_ext);
1337 break;
1338
1340 query = g_strdup_printf("SELECT ROUND(aperture,1) AS aperture, 1, COUNT(*) AS count"
1341 " FROM main.images AS mi WHERE %s GROUP BY aperture ORDER BY aperture", where_ext);
1342 break;
1343
1345 query = g_strdup_printf("SELECT CASE WHEN (exposure < 0.4) THEN '1/' || CAST(1/exposure + 0.9 AS INTEGER)"
1346 " ELSE ROUND(exposure,2) || '\"' END as _exposure, 1, COUNT(*) AS count"
1347 " FROM main.images AS mi WHERE %s GROUP BY _exposure ORDER BY exposure", where_ext);
1348 break;
1349
1351 query = g_strdup_printf("SELECT filename, 1, COUNT(*) AS count FROM main.images AS mi WHERE %s"
1352 " GROUP BY filename ORDER BY filename", where_ext);
1353 break;
1354
1356 query = g_strdup_printf("SELECT CASE WHEN id = group_id THEN '%s' ELSE '%s' END as group_leader, 1,"
1357 " COUNT(*) AS count FROM main.images AS mi WHERE %s"
1358 " GROUP BY group_leader ORDER BY group_leader ASC",
1359 _("group leaders"), _("group followers"), where_ext);
1360 break;
1361
1363 query = g_strdup_printf("SELECT m.name AS module_name, 1, COUNT(*) AS count FROM main.images AS mi"
1364 " JOIN (SELECT DISTINCT imgid, operation FROM main.history WHERE enabled = 1) AS h"
1365 " ON h.imgid = mi.id JOIN memory.darktable_iop_names AS m"
1366 " ON m.operation = h.operation WHERE %s GROUP BY module_name ORDER BY module_name",
1367 where_ext);
1368 break;
1369
1371 {
1372 char *orders = NULL;
1373 for(int i = 0; i < _order_names_count; i++)
1374 orders = dt_util_dstrcat(orders, "WHEN mo.version = %d THEN '%s' ", i, _order_names[i]);
1375 orders = dt_util_dstrcat(orders, "ELSE '%s' ", _("none"));
1376 query = g_strdup_printf("SELECT CASE %s END as ver, 1, COUNT(*) AS count FROM main.images AS mi"
1377 " LEFT JOIN (SELECT imgid, version FROM main.module_order) mo ON mo.imgid = mi.id"
1378 " WHERE %s GROUP BY ver ORDER BY ver", orders, where_ext);
1379 g_free(orders);
1380 break;
1381 }
1382
1384 query = g_strdup_printf("SELECT CASE WHEN (flags & 8) == 8 THEN -1 ELSE (flags & 7) END AS rating, 1,"
1385 " COUNT(*) AS count FROM main.images AS mi WHERE %s GROUP BY rating ORDER BY rating",
1386 where_ext);
1387 break;
1388
1389 default:
1391 {
1393 // whether this metadata field is hidden is a display preference the caller resolved
1394 if(!req->metadata_hidden)
1395 query = g_strdup_printf("SELECT CASE WHEN value IS NULL THEN '%s' ELSE value END AS value, 1,"
1396 " COUNT(*) AS count, CASE WHEN value IS NULL THEN 0 ELSE 1 END AS force_order"
1397 " FROM main.images AS mi"
1398 " LEFT JOIN (SELECT id AS meta_data_id, value FROM main.meta_data WHERE key = %d)"
1399 " ON id = meta_data_id WHERE %s GROUP BY value ORDER BY force_order, value",
1400 _("not defined"), keyid, where_ext);
1401 }
1402 else // film roll
1403 {
1404 // likewise the film-roll ordering: a preference, resolved by the caller
1405 const char *order_by = req->filmroll_order_by;
1406 query = g_strdup_printf("SELECT folder, film_rolls_id, COUNT(*) AS count, status FROM main.images AS mi"
1407 " JOIN (SELECT fr.id AS film_rolls_id, folder, status FROM main.film_rolls AS fr"
1408 " JOIN memory.film_folder AS ff ON ff.id = fr.id) ON film_id = film_rolls_id"
1409 " WHERE %s GROUP BY folder ORDER BY %s", where_ext, order_by);
1410 }
1411 break;
1412 }
1414 if(!query) return NULL;
1415
1418 while(stmt && sqlite3_step(stmt) == SQLITE_ROW)
1419 {
1420 char *name;
1421 if(is_date)
1422 {
1423 char sdt[DT_DATETIME_EXIF_LENGTH] = { 0 };
1425 if(property == DT_COLLECTION_PROP_DAY) sdt[10] = '\0';
1426 name = g_strdup(sdt);
1427 }
1428 else
1429 {
1430 const char *txt = (const char *)sqlite3_column_text(stmt, 0);
1431 name = txt ? g_strdup(txt) : g_strdup("");
1432 }
1433 const int id = sqlite3_column_int(stmt, 1);
1434 const int count = sqlite3_column_int(stmt, 2);
1435 const int status = has_status ? sqlite3_column_int(stmt, 3) : -1;
1437 }
1439 g_free(query);
1440 return g_list_reverse(out);
1441}
1442
1443void dt_collection_query_get_makermodels(const gchar *filter, GList **sanitized, GList **exif)
1444{
1445 gchar *needle = NULL;
1446 gboolean wildcard = FALSE;
1447
1449 if (sanitized)
1451
1452 if (filter && filter[0] != '\0')
1453 {
1454 needle = g_utf8_strdown(filter, -1);
1455 wildcard = (needle && needle[strlen(needle) - 1] == '%') ? TRUE : FALSE;
1456 if(wildcard)
1457 needle[strlen(needle) - 1] = '\0';
1458 }
1459
1462 "SELECT maker, model FROM main.images GROUP BY maker, model",
1463 -1, &stmt, NULL);
1464 if(IS_NULL_PTR(stmt)) return;
1465 while(sqlite3_step(stmt) == SQLITE_ROW)
1466 {
1467 const char *exif_maker = (char *)sqlite3_column_text(stmt, 0);
1468 const char *exif_model = (char *)sqlite3_column_text(stmt, 1);
1469
1470 gchar *makermodel = dt_collection_get_makermodel(exif_maker, exif_model);
1471
1472 gchar *haystack = g_utf8_strdown(makermodel, -1);
1474 || (!wildcard && !g_strcmp0(haystack, needle)))
1475 {
1476 if (exif)
1477 {
1478 // Append a two element list with maker and model
1482 *exif = g_list_append(*exif, inner_list);
1483 }
1484
1485 if (sanitized)
1486 {
1487 gchar *key = g_strdup(makermodel);
1489 }
1490 }
1492 dt_free(makermodel);
1493 }
1495 dt_free(needle);
1496
1497 if(sanitized)
1498 {
1501 }
1502}
1503
1505 gboolean recursive)
1506{
1507 // Build the same WHERE clause the collection would use for this single rule, then
1508 // enumerate the matching image ids. Independent of the currently active collection so it
1509 // can feed batch/background operations (remove, attach tag, pre-render thumbnails, ...).
1510 GList *result = NULL;
1511 gchar *where = get_query_string(property, text, recursive);
1512 if(IS_NULL_PTR(where)) return NULL;
1513
1514 gchar *query = g_strdup_printf("SELECT id FROM main.images WHERE %s", where);
1515 dt_free(where);
1516
1519 if(stmt)
1520 {
1521 while(sqlite3_step(stmt) == SQLITE_ROW)
1524 }
1525 dt_free(query);
1526
1527 return g_list_reverse(result);
1528}
1529
1531{
1532 // The first image of the collection that is NOT in `imgids`, searched after the list first and
1533 // then before it. Used to pick what to show once the listed images are gone.
1534 if(IS_NULL_PTR(imgids)) return -1;
1535
1536 gchar *txt = NULL;
1537 int i = 0;
1538 for(GList *l = imgids; l; l = g_list_next(l))
1539 {
1540 const int id = GPOINTER_TO_INT(l->data);
1541 if(i == 0)
1542 txt = dt_util_dstrcat(txt, "%d", id);
1543 else
1544 txt = dt_util_dstrcat(txt, ",%d", id);
1545 i++;
1546 }
1547
1548 int32_t next = -1;
1549 // 2. search the first imgid not in the list but AFTER the list (or in a gap inside the list)
1550 // we need to be carefull that some images in the list may not be present on screen (collapsed groups)
1551 // clang-format off
1552 gchar *query = g_strdup_printf("SELECT imgid"
1553 " FROM memory.collected_images"
1554 " WHERE imgid NOT IN (%s)"
1555 " AND rowid > (SELECT rowid"
1556 " FROM memory.collected_images"
1557 " WHERE imgid IN (%s)"
1558 " ORDER BY rowid LIMIT 1)"
1559 " ORDER BY rowid LIMIT 1",
1560 txt, txt);
1561 // clang-format on
1565 {
1566 next = sqlite3_column_int(stmt2, 0);
1567 }
1569 dt_free(query);
1570 // 3. if next is still unvalid, let's try to find the first untouched image BEFORE the list
1571 if(next < 0)
1572 {
1573 // clang-format off
1574 query = g_strdup_printf("SELECT imgid"
1575 " FROM memory.collected_images"
1576 " WHERE imgid NOT IN (%s)"
1577 " AND rowid < (SELECT rowid"
1578 " FROM memory.collected_images"
1579 " WHERE imgid IN (%s)"
1580 " ORDER BY rowid LIMIT 1)"
1581 " ORDER BY rowid DESC LIMIT 1",
1582 txt, txt);
1583 // clang-format on
1586 {
1587 next = sqlite3_column_int(stmt2, 0);
1588 }
1590 dt_free(query);
1591 }
1592 dt_free(txt);
1593 return next;
1594}
1595
1597{
1598 /* No cached statements any more: everything in this file is prepared and finalised per
1599 * call, so there is nothing to finalise ahead of the connection closing. */
1600 dt_free(_query);
1601 _query = NULL;
1605 _where_ext = NULL;
1606}
1607
1611
1612 /* check if we can get a query from collection */
1613 gchar *query = g_strdup(_ensure_query());
1614 if(IS_NULL_PTR(query)) return;
1615
1616 // The caller re-restricts the collection to the selection first when the GUI is in culling
1617 // mode: that is a decision about the interface, and this module cannot see one.
1618
1619 // 1. drop previous data
1620
1621 // clang-format off
1623 "DELETE FROM memory.collected_images",
1624 NULL, NULL, NULL);
1625 // reset autoincrement. need in star_key_accel_callback
1627 "DELETE FROM memory.sqlite_sequence"
1628 " WHERE name='collected_images'",
1629 NULL, NULL, NULL);
1630 // clang-format on
1631
1632 // 2. insert collected images into the temporary table
1633 gchar *ins_query = g_strdup_printf("INSERT INTO memory.collected_images (imgid) %s", query);
1634
1640
1641 dt_free(query);
1643
1644 // Re-restricting to the culling selection, and telling the user what just happened, are both
1645 // the caller's: this module rebuilds the table and counts what landed in it.
1647}
1648
1650 GList *list = NULL;
1651 const gchar *query = _ensure_query();
1652 if(query)
1653 {
1654 const gboolean use_limit = (_params.query_flags & COLLECTION_QUERY_USE_LIMIT) != 0;
1657 use_limit ? "SELECT imgid FROM memory.collected_images LIMIT -1, ?1"
1658 : "SELECT imgid FROM memory.collected_images",
1659 -1, &stmt, NULL);
1660 if(IS_NULL_PTR(stmt)) return NULL;
1662
1663 while(sqlite3_step(stmt) == SQLITE_ROW)
1664 {
1665 const int32_t imgid = sqlite3_column_int(stmt, 0);
1666 list = g_list_prepend(list, GINT_TO_POINTER(imgid));
1667 }
1669 }
1670
1671 return g_list_reverse(list); // list built in reverse order, so un-reverse it
1672}
1673
1676 return -1;
1677 const gchar *query = _ensure_query();
1682
1683 int result = -1;
1685 {
1686 result = sqlite3_column_int(stmt, 0);
1687 }
1688
1690
1691 return result;
1692
1693}
1694
1695int dt_collection_query_image_offset(const int32_t imgid){
1696 if(imgid == UNKNOWN_IMAGE) return 0;
1697 int offset = 0;
1700 "SELECT imgid FROM memory.collected_images",
1701 -1, &stmt, NULL);
1702 if(IS_NULL_PTR(stmt)) return 0;
1703
1704 gboolean found = FALSE;
1705
1706 while(sqlite3_step(stmt) == SQLITE_ROW)
1707 {
1708 const int id = sqlite3_column_int(stmt, 0);
1709 if(imgid == id)
1710 {
1711 found = TRUE;
1712 break;
1713 }
1714 offset++;
1715 }
1716
1718
1719 if(!found) offset = 0;
1720
1721 return offset;
1722}
1723
1725 // Restore previous collection
1726 DT_DEBUG_SQLITE3_EXEC(dt_database_get_sqlite3_global(), "DELETE FROM memory.collected_images", NULL, NULL, NULL);
1728 "INSERT INTO memory.collected_images"
1729 " SELECT * FROM memory.collected_backup",
1730 NULL, NULL, NULL);
1731}
1732
1734 // Backup current collection
1735 DT_DEBUG_SQLITE3_EXEC(dt_database_get_sqlite3_global(), "DELETE FROM memory.collected_backup", NULL, NULL, NULL);
1737 "INSERT INTO memory.collected_backup"
1738 " SELECT * FROM memory.collected_images",
1739 NULL, NULL, NULL);
1740}
1741
1743{
1744 // Drop everything the user has not selected. Deciding that culling mode means this, backing
1745 // the collection up first and resetting the selection afterwards, are the caller's -- they
1746 // are statements about selection and about a view mode, neither of which this module knows.
1748 "DELETE FROM memory.collected_images"
1749 " WHERE imgid NOT IN "
1750 " (SELECT imgid FROM main.selected_images)",
1751 NULL, NULL, NULL);
1752
1753 /* The published count follows every mutation of memory.collected_images this module
1754 * makes. The refresh already counted, but the caller runs this restriction AFTER the
1755 * refresh -- the original computed its count after both, and dt_collection_get_count()
1756 * in culling mode must report the culled subset, not the full collection. */
1758}
1759
1760// clang-format off
1761// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1762// vim: shiftwidth=2 expandtab tabstop=2 cindent
1763// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1764// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void dt_collection_split_operator_exposure(const gchar *input, char **number1, char **number2, char **operator)
Definition collection.c:584
dt_collection_t * dt_collection_get_global(void)
Definition collection.c:138
void dt_collection_split_operator_number(const gchar *input, char **number1, char **number2, char **operator)
Definition collection.c:455
void dt_collection_get_makermodels(const gchar *filter, GList **sanitized, GList **exif)
Definition collection.c:646
gchar * dt_collection_get_makermodel(const char *exif_maker, const char *exif_model)
Definition collection.c:651
void dt_collection_split_operator_datetime(const gchar *input, char **number1, char **number2, char **operator)
Definition collection.c:523
dt_collection_properties_t
Definition collection.h:120
@ DT_COLLECTION_PROP_EXPOSURE
Definition collection.h:128
@ DT_COLLECTION_PROP_MODULE
Definition collection.h:147
@ DT_COLLECTION_PROP_RATING
Definition collection.h:149
@ DT_COLLECTION_PROP_QUERY
Definition collection.h:151
@ DT_COLLECTION_PROP_TIME
Definition collection.h:133
@ DT_COLLECTION_PROP_METADATA
Definition collection.h:142
@ DT_COLLECTION_PROP_GROUPING
Definition collection.h:143
@ DT_COLLECTION_PROP_TAG
Definition collection.h:140
@ DT_COLLECTION_PROP_FILMROLL
Definition collection.h:121
@ DT_COLLECTION_PROP_LENS
Definition collection.h:126
@ DT_COLLECTION_PROP_CAMERA
Definition collection.h:125
@ DT_COLLECTION_PROP_LOCAL_COPY
Definition collection.h:144
@ DT_COLLECTION_PROP_GEOTAGGING
Definition collection.h:139
@ DT_COLLECTION_PROP_FILENAME
Definition collection.h:123
@ DT_COLLECTION_PROP_ISO
Definition collection.h:130
@ DT_COLLECTION_PROP_COLORLABEL
Definition collection.h:141
@ DT_COLLECTION_PROP_DAY
Definition collection.h:132
@ DT_COLLECTION_PROP_ORDER
Definition collection.h:148
@ DT_COLLECTION_PROP_FOLDERS
Definition collection.h:122
@ DT_COLLECTION_PROP_APERTURE
Definition collection.h:127
@ DT_COLLECTION_PROP_IMPORT_TIMESTAMP
Definition collection.h:134
@ DT_COLLECTION_PROP_FOCAL_LENGTH
Definition collection.h:129
@ DT_COLLECTION_PROP_EXPORT_TIMESTAMP
Definition collection.h:136
@ DT_COLLECTION_PROP_CHANGE_TIMESTAMP
Definition collection.h:135
@ DT_COLLECTION_PROP_HISTORY
Definition collection.h:146
@ DT_COLLECTION_PROP_PRINT_TIMESTAMP
Definition collection.h:137
@ COLLECTION_QUERY_USE_ONLY_WHERE_EXT
Definition collection.h:74
@ COLLECTION_QUERY_USE_WHERE_EXT
Definition collection.h:73
@ COLLECTION_QUERY_USE_SORT
Definition collection.h:71
@ COLLECTION_QUERY_USE_LIMIT
Definition collection.h:72
@ COLLECTION_FILTER_ALTERED
Definition collection.h:81
@ COLLECTION_FILTER_UNALTERED
Definition collection.h:82
@ COLLECTION_FILTER_GREEN
Definition collection.h:92
@ COLLECTION_FILTER_NONE
Definition collection.h:80
@ COLLECTION_FILTER_2_STAR
Definition collection.h:86
@ COLLECTION_FILTER_MAGENTA
Definition collection.h:94
@ COLLECTION_FILTER_0_STAR
Definition collection.h:84
@ COLLECTION_FILTER_1_STAR
Definition collection.h:85
@ COLLECTION_FILTER_BLUE
Definition collection.h:93
@ COLLECTION_FILTER_5_STAR
Definition collection.h:89
@ COLLECTION_FILTER_YELLOW
Definition collection.h:91
@ COLLECTION_FILTER_REJECTED
Definition collection.h:83
@ COLLECTION_FILTER_4_STAR
Definition collection.h:88
@ COLLECTION_FILTER_3_STAR
Definition collection.h:87
@ COLLECTION_FILTER_RED
Definition collection.h:90
@ COLLECTION_FILTER_WHITE
Definition collection.h:95
@ DT_COLLECTION_SORT_EXPORT_TIMESTAMP
Definition collection.h:106
@ DT_COLLECTION_SORT_IMPORT_TIMESTAMP
Definition collection.h:104
@ DT_COLLECTION_SORT_DATETIME
Definition collection.h:103
@ DT_COLLECTION_SORT_GROUP
Definition collection.h:111
@ DT_COLLECTION_SORT_FILENAME
Definition collection.h:102
@ DT_COLLECTION_SORT_RATING
Definition collection.h:108
@ DT_COLLECTION_SORT_PATH
Definition collection.h:112
@ DT_COLLECTION_SORT_TITLE
Definition collection.h:113
@ DT_COLLECTION_SORT_CHANGE_TIMESTAMP
Definition collection.h:105
@ DT_COLLECTION_SORT_NONE
Definition collection.h:101
@ DT_COLLECTION_SORT_PRINT_TIMESTAMP
Definition collection.h:107
@ DT_COLLECTION_SORT_ID
Definition collection.h:109
@ DT_COLLECTION_SORT_COLOR
Definition collection.h:110
static const char *const * _order_names
static int _order_names_count
static uint32_t _tagid
static int _recompose(void)
static uint64_t _generation
uint64_t dt_collection_query_get_generation(void)
void dt_collection_query_set_order_names(const char *const *names, const int count)
static gchar ** _where_ext
void dt_collection_query_pop(void)
static gchar * _sort_query(void)
static const gchar * _ensure_query(void)
int dt_collection_query_set_rules(const dt_collection_params_t *params, const dt_collection_rule_t *rules, const int n_rules, const uint32_t tagid)
GList * dt_collection_query_get_group_members(const int32_t group_id, const int32_t exclude_imgid)
void dt_collection_query_set_order_resolver(dt_collection_query_order_resolver_t fn)
static gchar * _query
GList * dt_collection_query_get_property_values(const dt_collection_values_request_t *req)
void dt_collection_query_cleanup(void)
static dt_collection_name_value_t * _name_value_new(char *name, int id, int count, int status)
void dt_collection_query_set_iop_names(const dt_iop_name_row_t *rows, const size_t count)
Fill memory.darktable_iop_names, the table the module-name rules join against.
static gchar * _extended_where(void)
static char * or_operator(int *term)
void dt_collection_query_push(void)
static gchar * _extended_where_excluding(const int exclude, const gboolean apply_exclude)
void dt_collection_query_get_makermodels(const gchar *filter, GList **sanitized, GList **exif)
static uint32_t _compute_count(void)
static dt_collection_params_t _params
#define LIMIT_QUERY
static uint32_t _count
int dt_collection_query_recompose(void)
void dt_collection_query_refresh_memory_table(void)
GList * dt_collection_query_get_images_for_rule(const dt_collection_properties_t property, const char *text, gboolean recursive)
static void _set_selq_pre_sort(char **selq_pre)
#define or_operator_initial()
uint32_t dt_collection_query_count(void)
static char * and_operator(int *term)
int32_t dt_collection_query_find_neighbour(GList *imgids)
static int _store(gchar *query)
static dt_collection_query_order_resolver_t _order_resolver
static gchar * get_query_string(const dt_collection_properties_t property, const gchar *text, const gboolean recursive)
int32_t dt_collection_query_get_nth(const int nth)
GList * dt_collection_query_get_images(const uint32_t limit)
static gchar ** _compose_where_ext(const dt_collection_rule_t *rules, const int n_rules)
int dt_collection_query_image_offset(const int32_t imgid)
void dt_collection_query_restrict_to_selection(void)
int(* dt_collection_query_order_resolver_t)(const char *text)
@ DT_COLORLABELS_PURPLE
Definition colorlabels.h:46
@ DT_COLORLABELS_GREEN
Definition colorlabels.h:44
@ DT_COLORLABELS_YELLOW
Definition colorlabels.h:43
@ DT_COLORLABELS_BLUE
Definition colorlabels.h:45
@ DT_COLORLABELS_RED
Definition colorlabels.h:42
const float v
const dt_colormatrix_t dt_aligned_pixel_t out
sqlite3 * dt_database_get_sqlite3_global(void)
Definition database.c:3790
gboolean dt_database_is_open(void)
Definition database.c:3779
#define dt_database_start_transaction()
Definition database.h:290
#define dt_database_release_transaction()
Definition database.h:291
GTimeSpan dt_datetime_exif_to_gtimespan(const char *sdt)
Definition datetime.c:420
gboolean dt_datetime_gtimespan_to_exif(char *sdt, const size_t sdt_size, const GTimeSpan gts)
Definition datetime.c:405
#define DT_DATETIME_EXIF_LENGTH
Definition datetime.h:39
GHashTable * names
GtkWidget* -> the name the application knows it by.
GtkWidget * status
result of the last capture
GdkRGBA color[]
Definition geotagging.c:541
#define UNKNOWN_IMAGE
Definition image.h:78
@ DT_IMAGE_REMOVE
Definition image.h:128
@ DT_IMAGE_LOCAL_COPY
Definition image.h:134
@ DT_IMAGE_REJECTED
Definition image.h:113
@ DT_VIEW_STAR_4
Definition image.h:353
@ DT_VIEW_STAR_5
Definition image.h:354
@ DT_VIEW_STAR_3
Definition image.h:352
@ DT_VIEW_STAR_1
Definition image.h:350
@ DT_VIEW_STAR_2
Definition image.h:351
@ DT_VIEW_DESERT
Definition image.h:349
const char * maker
const char * model
static const dt_iop_order_entry_t * orders[5]
Definition iop_order.c:949
static const dt_iop_drawlayer_params_t * params
Definition layers.h:80
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
const char * dt_map_location_data_tag_root()
char * key
dt_metadata_t dt_metadata_get_keyid_by_display_order(const uint32_t order)
@ DT_METADATA_NUMBER
Definition metadata.h:57
@ DT_METADATA_XMP_DC_TITLE
Definition metadata.h:51
const char * name
Definition pdf.h:90
Checked wrappers around the sqlite3_* calls: trace the statement, assert the return code,...
#define DT_DEBUG_SQLITE3_EXEC(a, b, c, d, e)
Definition sql_debug.h:129
#define DT_DEBUG_SQLITE3_PREPARE_V2(a, b, c, d, e)
Definition sql_debug.h:137
#define DT_DEBUG_SQLITE3_BIND_TEXT(a, b, c, d, e)
Definition sql_debug.h:148
#define DT_DEBUG_SQLITE3_BIND_INT(a, b, c)
Definition sql_debug.h:145
const float r
unsigned __int64 uint64_t
Definition strptime.c:75
dt_collection_query_flags_t query_flags
Definition collection.h:192
dt_collection_sort_t sort
Definition collection.h:201
dt_collection_filter_flag_t filter_flags
Definition collection.h:195
One module's identity, for the rule that searches by module name.
GList * dt_util_str_to_glist(const gchar *separator, const gchar *text)
Definition utility.c:856
gchar * dt_util_glist_to_str(const gchar *separator, GList *items)
Definition utility.c:193
gchar * dt_util_dstrcat(gchar *str, const gchar *format,...)
Definition utility.c:122