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 (darktable.c), so there is no
38// handle to pass around -- an argument no caller chooses is not a parameter.
39//
40// The composed SQL never leaves this file. Callers describe what they want with
41// dt_collection_query_set_rules() and read results back as ids and counts.
43static gchar **_where_ext = NULL; // composed from the rules below, never handed in
44static uint32_t _tagid = 0;
45static gchar *_query = NULL;
46static uint32_t _count = 0;
49static const char *const *_order_names = NULL;
50static int _order_names_count = 0;
51
52
53#define LIMIT_QUERY "LIMIT ?1, ?2"
54
55// for term should be an int initialized to and_operator_initial()
56// before use.
57#define and_operator_initial() (0)
58static char * and_operator(int *term)
59{
61 if(*term == 0)
62 {
63 *term = 1;
64 return "";
65 }
66 else
67 {
68 return " AND ";
69 }
70
71 assert(0); // Not reached.
72}
73
74#define or_operator_initial() (0)
75static char * or_operator(int *term)
76{
78 if(*term == 0)
79 {
80 *term = 1;
81 return "";
82 }
83 else
84 {
85 return " OR ";
86 }
87
88 assert(0); // Not reached.
89}
90
95
96void dt_collection_query_set_order_names(const char *const *names, const int count)
97{
98 // Borrowed, not copied: these are gettext's, and gettext outlives us.
100 _order_names_count = count;
101}
102
103static int _store(gchar *query)
104{
105 /* The generation advances only when the composed text actually changes. Consumers hash it
106 * in place of the text (gui/dtgtk/thumbtable.c), and every
107 * DT_COLLECTION_CHANGE_RELOAD recomposes an IDENTICAL query -- the enum is defined by it.
108 * An unconditional bump would turn each of those reloads (a rating, a tag, an import
109 * batch) into a full "collection changed" reset downstream. */
110 const gboolean changed = (g_strcmp0(_query, query) != 0);
113 if(changed) _generation++;
114 return 1;
115}
116
123static gchar *get_query_string(const dt_collection_properties_t property, const gchar *text,
124 const gboolean recursive)
125{
126 char *escaped_text = sqlite3_mprintf("%q", text);
127 const unsigned int escaped_length = strlen(escaped_text);
128 gchar *query = NULL;
129
130 switch(property)
131 {
132 case DT_COLLECTION_PROP_QUERY: // raw user-provided SQL WHERE expression (advanced)
133 // Intentionally NOT escaped: this is a power-user escape hatch that injects a raw
134 // read-only WHERE clause against the local library. A malformed expression makes the
135 // prepared statement fail gracefully (empty collection), it does not crash.
136 if(text && *text)
137 query = g_strdup_printf("(%s)", text);
138 else
139 query = g_strdup("1=1");
140 break;
141
142 case DT_COLLECTION_PROP_FILMROLL: // film roll
143 if(!(escaped_text && *escaped_text))
144 // clang-format off
145 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s%%'))",
147 // clang-format on
148 else
149 // clang-format off
150 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s'))",
152 // clang-format on
153 break;
154
155 case DT_COLLECTION_PROP_FOLDERS: // folders
156 {
157 // Recursion is normally the explicit `recursive` flag; a still-present trailing '*' is
158 // only recognized as a fallback for collections/presets saved before that flag existed,
159 // and for the Queries tab's raw rule editor, which has no checkbox and still relies on
160 // typing '*' by hand -- so this is permanent, not a transitional shim.
161 const gboolean has_star = (escaped_length > 0) && (escaped_text[escaped_length-1] == '*');
162 if(recursive || has_star)
163 {
165 // clang-format off
166 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s' OR folder LIKE '%s"
167 G_DIR_SEPARATOR_S "%%'))",
169 // clang-format on
170 }
171 // replace |% at the end with /% to only show subfolders
172 else if ((escaped_length > 1) && (strcmp(escaped_text+escaped_length-2, "|%") == 0 ))
173 {
175 // clang-format off
176 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s"
177 G_DIR_SEPARATOR_S "%%'))",
179 // clang-format on
180 }
181 else
182 {
183 // clang-format off
184 query = g_strdup_printf("(film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s'))",
186 // clang-format on
187 }
188 }
189 break;
190
191 case DT_COLLECTION_PROP_COLORLABEL: // colorlabel
192 {
193 if(!(escaped_text && *escaped_text) || strcmp(escaped_text, "%") == 0)
194 // clang-format off
195 query = g_strdup_printf("(id IN (SELECT imgid FROM main.color_labels WHERE color IS NOT NULL))");
196 // clang-format on
197 else
198 {
199 int color = 0;
200 if(strcmp(escaped_text, _("red")) == 0)
201 color = 0;
202 else if(strcmp(escaped_text, _("yellow")) == 0)
203 color = 1;
204 else if(strcmp(escaped_text, _("green")) == 0)
205 color = 2;
206 else if(strcmp(escaped_text, _("blue")) == 0)
207 color = 3;
208 else if(strcmp(escaped_text, _("purple")) == 0)
209 color = 4;
210 // clang-format off
211 query = g_strdup_printf("(id IN (SELECT imgid FROM main.color_labels WHERE color=%d))", color);
212 // clang-format on
213 }
214 }
215 break;
216
217 case DT_COLLECTION_PROP_HISTORY: // history
218 {
219 if(strcmp(escaped_text, _("altered")) == 0)
220 {
221 query = g_strdup("EXISTS (SELECT 1 FROM main.history h WHERE h.imgid = id)");
222 }
223 else if(strcmp(escaped_text, _("unaltered")) == 0)
224 {
225 query = g_strdup("NOT EXISTS (SELECT 1 FROM main.history h WHERE h.imgid = id)");
226 }
227 else
228 {
229 query = g_strdup("1");
230 }
231 }
232 break;
233
234 case DT_COLLECTION_PROP_GEOTAGGING: // geotagging
235 {
236 const gboolean not_tagged = strcmp(escaped_text, _("not tagged")) == 0;
237 const gboolean no_location = strcmp(escaped_text, _("tagged")) == 0;
238 const gboolean all_tagged = strcmp(escaped_text, _("tagged*")) == 0;
239 char *escaped_text2 = g_strstr_len(escaped_text, -1, "|");
240 char *name_clause = g_strdup_printf("t.name LIKE \'%s\' || \'%s\'",
242
244 {
246 name_clause = g_strdup_printf("(t.name LIKE \'%s\' || \'%s\' OR t.name LIKE \'%s\' || \'%s|%%\')",
248 }
249
251 // clang-format off
252 query = g_strdup_printf("(id %s IN (SELECT id AS imgid FROM main.images "
253 "WHERE (longitude IS NOT NULL AND latitude IS NOT NULL))) ",
254 all_tagged ? "" : "not");
255 // clang-format on
256 else
257 // clang-format off
258 query = g_strdup_printf("(id IN (SELECT id AS imgid FROM main.images "
259 "WHERE (longitude IS NOT NULL AND latitude IS NOT NULL))"
260 "AND id %s IN (SELECT imgid FROM main.tagged_images AS ti"
261 " JOIN data.tags AS t"
262 " ON t.id = ti.tagid"
263 " AND %s)) ",
264 no_location ? "not" : "",
266 // clang-format on
267 }
268 break;
269
270 case DT_COLLECTION_PROP_LOCAL_COPY: // local copy
271 // clang-format off
272 query = g_strdup_printf("(id %s IN (SELECT id AS imgid FROM main.images WHERE (flags & %d))) ",
273 (strcmp(escaped_text, _("not copied locally")) == 0) ? "not" : "",
275 // clang-format on
276 break;
277
278 case DT_COLLECTION_PROP_CAMERA: // camera
279 // Start query with a false statement to avoid special casing the first condition
280 query = g_strdup_printf("((1=0)");
281 GList *lists = NULL;
284 {
285 GList *tuple = element->data;
286 char *clause = sqlite3_mprintf(" OR (maker = '%q' AND model = '%q')", tuple->data, tuple->next->data);
289 dt_free(tuple->data);
290 dt_free(tuple->next->data);
292 tuple = NULL;
293 }
295 lists = NULL;
297 break;
298
299 case DT_COLLECTION_PROP_TAG: // tag
300 {
301 if(!strcmp(escaped_text, _("not tagged")))
302 {
303 // clang-format off
304 query = g_strdup_printf("(id NOT IN (SELECT DISTINCT imgid FROM main.tagged_images "
305 "WHERE tagid NOT IN memory.darktable_tags))");
306 // clang-format on
307 }
308 else
309 {
310 if ((escaped_length > 0) && (escaped_text[escaped_length-1] == '*'))
311 {
312 // shift-click adds an asterix * to include items in and under this hierarchy
313 // without using a wildcard % which also would include similar named items
315 // clang-format off
316 query = g_strdup_printf("(id IN (SELECT imgid FROM main.tagged_images WHERE tagid IN "
317 "(SELECT id FROM data.tags "
318 "WHERE LOWER(name) = LOWER('%s')"
319 " OR SUBSTR(LOWER(name), 1, LENGTH('%s') + 1) = LOWER('%s|'))))",
321 // clang-format on
322 }
323 else if ((escaped_length > 0) && (escaped_text[escaped_length-1] == '%'))
324 {
325 // ends with % or |%
327 // clang-format off
328 query = g_strdup_printf("(id IN (SELECT imgid FROM main.tagged_images WHERE tagid IN "
329 "(SELECT id FROM data.tags WHERE SUBSTR(LOWER(name), 1, LENGTH('%s')) = LOWER('%s'))))",
331 // clang-format on
332 }
333 else
334 {
335 // default
336 // clang-format off
337 query = g_strdup_printf("(id IN (SELECT imgid FROM main.tagged_images WHERE tagid IN "
338 "(SELECT id FROM data.tags WHERE LOWER(name) = LOWER('%s'))))",
340 // clang-format on
341 }
342 }
343 }
344 break;
345
346 case DT_COLLECTION_PROP_LENS: // lens
347 query = g_strdup_printf("(lens LIKE '%%%s%%')", escaped_text);
348 break;
349
350 case DT_COLLECTION_PROP_FOCAL_LENGTH: // focal length
351 {
352 gchar *operator, *number1, *number2;
354
355 if(operator && strcmp(operator, "[]") == 0)
356 {
357 if(number1 && number2)
358 query = g_strdup_printf("((focal_length >= %s) AND (focal_length <= %s))", number1, number2);
359 }
360 else if(operator && number1)
361 query = g_strdup_printf("(focal_length %s %s)", operator, number1);
362 else if(number1)
363 // clang-format off
364 query = g_strdup_printf("(CAST(focal_length AS INTEGER) = CAST(%s AS INTEGER))", number1);
365 // clang-format on
366 else
367 query = g_strdup_printf("(focal_length LIKE '%%%s%%')", escaped_text);
368
369 dt_free(operator);
372 }
373 break;
374
375 case DT_COLLECTION_PROP_ISO: // iso
376 {
377 gchar *operator, *number1, *number2;
379
380 if(operator && strcmp(operator, "[]") == 0)
381 {
382 if(number1 && number2)
383 query = g_strdup_printf("((iso >= %s) AND (iso <= %s))", number1, number2);
384 }
385 else if(operator && number1)
386 query = g_strdup_printf("(iso %s %s)", operator, number1);
387 else if(number1)
388 query = g_strdup_printf("(iso = %s)", number1);
389 else
390 query = g_strdup_printf("(iso LIKE '%%%s%%')", escaped_text);
391
392 dt_free(operator);
395 }
396 break;
397
398 case DT_COLLECTION_PROP_APERTURE: // aperture
399 {
400 gchar *operator, *number1, *number2;
402
403 if(operator && strcmp(operator, "[]") == 0)
404 {
405 if(number1 && number2)
406 // clang-format off
407 query = g_strdup_printf("((ROUND(aperture,1) >= %s) AND (ROUND(aperture,1) <= %s))", number1,
408 number2);
409 // clang-format on
410 }
411 else if(operator && number1)
412 query = g_strdup_printf("(ROUND(aperture,1) %s %s)", operator, number1);
413 else if(number1)
414 query = g_strdup_printf("(ROUND(aperture,1) = %s)", number1);
415 else
416 query = g_strdup_printf("(ROUND(aperture,1) LIKE '%%%s%%')", escaped_text);
417
418 dt_free(operator);
421 }
422 break;
423
424 case DT_COLLECTION_PROP_EXPOSURE: // exposure
425 {
426 gchar *operator, *number1, *number2;
428
429 if(operator && strcmp(operator, "[]") == 0)
430 {
431 if(number1 && number2)
432 // clang-format off
433 query = g_strdup_printf("((exposure >= %s - 1.0/100000) AND (exposure <= %s + 1.0/100000))", number1,
434 number2);
435 // clang-format on
436 }
437 else if(operator && number1)
438 query = g_strdup_printf("(exposure %s %s)", operator, number1);
439 else if(number1)
440 // clang-format off
441 query = g_strdup_printf("(CASE WHEN exposure < 0.4 THEN ((exposure >= %s - 1.0/100000) AND (exposure <= %s + 1.0/100000)) "
442 "ELSE (ROUND(exposure,2) >= %s - 1.0/100000) AND (ROUND(exposure,2) <= %s + 1.0/100000) END)",
444 // clang-format on
445 else
446 query = g_strdup_printf("(exposure LIKE '%%%s%%')", escaped_text);
447
448 dt_free(operator);
451 }
452 break;
453
454 case DT_COLLECTION_PROP_FILENAME: // filename
455 {
457
458 for (GList *l = list; l; l = g_list_next(l))
459 {
460 char *name = (char*)l->data; // remember the original content of this list node
461 l->data = g_strdup_printf("(filename LIKE '%%%s%%')", name);
462 dt_free(name); // free the original filename
463 }
464
465 char *subquery = dt_util_glist_to_str(" OR ", list);
466 query = g_strdup_printf("(%s)", subquery);
468 g_list_free_full(list, dt_free_gpointer); // free the SQL clauses as well as the list
469 list = NULL;
470
471 break;
472 }
479 {
480 const int local_property = property;
481 char *colname = NULL;
482
483 switch(local_property)
484 {
485 case DT_COLLECTION_PROP_DAY: colname = "datetime_taken" ; break ;
486 case DT_COLLECTION_PROP_TIME: colname = "datetime_taken" ; break ;
487 case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: colname = "import_timestamp" ; break ;
488 case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: colname = "change_timestamp" ; break ;
489 case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: colname = "export_timestamp" ; break ;
490 case DT_COLLECTION_PROP_PRINT_TIMESTAMP: colname = "print_timestamp" ; break ;
491 }
492 gchar *operator, *number1, *number2;
494 if(number1 && number1[strlen(number1) - 1] == '%')
495 number1[strlen(number1) - 1] = '\0';
498
499 if(strcmp(operator, "[]") == 0)
500 {
501 if(number1 && number2)
502 query = g_strdup_printf("((%s >= %" G_GINT64_FORMAT ") AND (%s <= %" G_GINT64_FORMAT "))", colname, nb1, colname, nb2);
503 }
504 else if((strcmp(operator, "=") == 0 || strcmp(operator, "") == 0) && number1 && number2)
505 query = g_strdup_printf("((%s >= %" G_GINT64_FORMAT ") AND (%s <= %" G_GINT64_FORMAT "))", colname, nb1, colname, nb2);
506 else if(strcmp(operator, "<>") == 0 && number1 && number2)
507 // a date/period spans the range [nb1;nb2]; "not equal" means anything OUTSIDE it
508 // (before its start OR after its end). AND here would be unsatisfiable (nb1 < nb2).
509 query = g_strdup_printf("((%s < %" G_GINT64_FORMAT ") OR (%s > %" G_GINT64_FORMAT "))", colname, nb1, colname, nb2);
510 else if(number1)
511 query = g_strdup_printf("(%s %s %" G_GINT64_FORMAT ")", colname, operator, nb1);
512 else
513 query = g_strdup("1 = 1");
514
515 dt_free(operator);
518 break;
519 }
520
521 case DT_COLLECTION_PROP_GROUPING: // grouping
522 query = g_strdup_printf("(id %s group_id)", (strcmp(escaped_text, _("group leaders")) == 0) ? "=" : "!=");
523 break;
524
525 case DT_COLLECTION_PROP_MODULE: // dev module
526 {
527 // clang-format off
528 query = g_strdup_printf("(id IN (SELECT imgid AS id FROM main.history AS h "
529 "JOIN memory.darktable_iop_names AS m ON m.operation = h.operation "
530 "WHERE h.enabled = 1 AND m.name LIKE '%s'))", escaped_text);
531 // clang-format on
532 }
533 break;
534
535 case DT_COLLECTION_PROP_ORDER: // module order
536 {
537 // The text here is a LOCALISED module-order name, and turning one back into an id is
538 // presentation: this module cannot see translations. The caller installs the resolver.
539 const int i = _order_resolver ? _order_resolver(escaped_text) : -1;
540 if(i >= 0)
541 // clang-format off
542 query = g_strdup_printf("(id IN (SELECT imgid FROM main.module_order WHERE version = %d))", i);
543 // clang-format on
544 else
545 // clang-format off
546 query = g_strdup_printf("(id NOT IN (SELECT imgid FROM main.module_order))");
547 // clang-format on
548 }
549 break;
550
551 case DT_COLLECTION_PROP_RATING: // image rating
552 {
553 gchar *operator, *number1, *number2;
555
556 if(operator && strcmp(operator, "[]") == 0)
557 {
558 if(number1 && number2)
559 {
560 if(atoi(number1) == -1)
561 { // rejected + star rating
562 // clang-format off
563 query = g_strdup_printf("(flags & 7 >= %s AND flags & 7 <= %s)", number1, number2);
564 // clang-format on
565 }
566 else
567 { // non-rejected + star rating
568 // clang-format off
569 query = g_strdup_printf("((flags & 8 == 0) AND (flags & 7 >= %s AND flags & 7 <= %s))", number1, number2);
570 // clang-format on
571 }
572 }
573 }
574 else if(operator && number1)
575 {
576 if(g_strcmp0(operator, "<=") == 0 || g_strcmp0(operator, "<") == 0)
577 { // all below rating + rejected
578 // clang-format off
579 query = g_strdup_printf("(flags & 8 == 8 OR flags & 7 %s %s)", operator, number1);
580 // clang-format on
581 }
582 else if(g_strcmp0(operator, ">=") == 0 || g_strcmp0(operator, ">") == 0)
583 {
584 if(atoi(number1) >= 0)
585 { // non rejected above rating
586 // clang-format off
587 query = g_strdup_printf("(flags & 8 == 0 AND flags & 7 %s %s)", operator, number1);
588 // clang-format on
589 }
590 // otherwise no filter (rejected + all ratings)
591 }
592 else
593 { // <> exclusion operator
594 if(atoi(number1) == -1)
595 { // all except rejected
596 query = g_strdup_printf("(flags & 8 == 0)");
597 }
598 else
599 { // all except star rating (including rejected)
600 query = g_strdup_printf("(flags & 8 == 8 OR flags & 7 %s %s)", operator, number1);
601 }
602 }
603 }
604 else if(number1)
605 {
606 if(atoi(number1) == -1)
607 { // rejected only
608 query = g_strdup_printf("(flags & 8 == 8)");
609 }
610 else
611 { // non-rejected + star rating
612 query = g_strdup_printf("(flags & 8 == 0 AND flags & 7 == %s)", number1);
613 }
614 }
615
616 dt_free(operator);
619 }
620 break;
621
622 default:
623 {
624 if(property >= DT_COLLECTION_PROP_METADATA
626 {
628 if(strcmp(escaped_text, _("not defined")) != 0)
629 // clang-format off
630 query = g_strdup_printf("(id IN (SELECT id FROM main.meta_data WHERE key = %d AND value "
631 "LIKE '%%%s%%'))", keyid, escaped_text);
632 // clang-format on
633 else
634 // clang-format off
635 query = g_strdup_printf("(id NOT IN (SELECT id FROM main.meta_data WHERE key = %d))",
636 keyid);
637 // clang-format off
638 }
639 }
640 break;
641 }
643
644 if(IS_NULL_PTR(query)) // We've screwed up and not done a query string, send a placeholder
645 query = g_strdup_printf("(1=1)");
646
647 return query;
648}
649
650static dt_collection_name_value_t *_name_value_new(char *name, int id, int count, int status)
651{
653 v->name = name;
654 v->id = id;
655 v->count = count;
656 v->status = status;
657 return v;
658}
659
666static gchar *_extended_where_excluding(const int exclude, const gboolean apply_exclude)
667{
668 gchar *complete_string = g_strdup("");
669 if(_where_ext && apply_exclude)
670 {
671 for(int i = 0; !IS_NULL_PTR(_where_ext[i]); i++)
672 {
673 if(i == exclude) continue;
675 }
676 }
677 gchar *where_ext = g_strdup_printf("(1=1%s)", complete_string);
679 return where_ext;
680}
681
682static gchar *_extended_where(void)
683{
685 gchar *where_ext = g_strdup_printf("(1=1%s)", complete_string);
687 return where_ext;
688}
689
690static void _set_selq_pre_sort(char **selq_pre){
691 const uint32_t tagid = _tagid;
692 char tag[16] = { 0 };
693 snprintf(tag, sizeof(tag), "%u", tagid);
694
695 // clang-format off
697 "SELECT DISTINCT mi.id FROM (SELECT"
698 " id, group_id, film_id, filename, datetime_taken, "
699 " flags, version, aspect_ratio,"
700 " maker, model, lens, aperture, exposure, focal_length,"
701 " iso, import_timestamp, change_timestamp,"
702 " export_timestamp, print_timestamp"
703 " FROM main.images AS mi %s%s WHERE ",
704 tagid ? " LEFT JOIN main.tagged_images AS ti"
705 " ON ti.imgid = mi.id AND ti.tagid = " : "",
706 tagid ? tag : "");
707 // clang-format on
708}
709
710static gchar *_sort_query(void){
711 gchar *sq = NULL;
712 const gchar *order = (_params.descending) ? "DESC" : "ASC";
713
714 switch(_params.sort)
715 {
721 {
722 const int local_order = _params.sort;
723 char *colname;
724
725 switch(local_order)
726 {
727 case DT_COLLECTION_SORT_DATETIME: colname = "datetime_taken" ; break ;
728 case DT_COLLECTION_SORT_IMPORT_TIMESTAMP: colname = "import_timestamp" ; break ;
729 case DT_COLLECTION_SORT_CHANGE_TIMESTAMP: colname = "change_timestamp" ; break ;
730 case DT_COLLECTION_SORT_EXPORT_TIMESTAMP: colname = "export_timestamp" ; break ;
731 case DT_COLLECTION_SORT_PRINT_TIMESTAMP: colname = "print_timestamp" ; break ;
732 default: colname = "";
733 }
734 // clang-format off
735 sq = g_strdup_printf("ORDER BY %s %s", colname, order);
736 // clang-format on
737 break;
738 }
739
741 // clang-format off
742 sq = g_strdup_printf("ORDER BY CASE WHEN flags & 8 = 8 THEN -1 ELSE flags & 7 END %s", order);
743 // clang-format on
744 break;
745
747 // clang-format off
748 sq = g_strdup_printf("ORDER BY filename %s", order);
749 // clang-format on
750 break;
751
753 // clang-format off
754 sq = g_strdup_printf("ORDER BY mi.id %s", order);
755 // clang-format on
756 break;
757
759 // clang-format off
760 sq = g_strdup_printf("ORDER BY color %s", order);
761 // clang-format on
762 break;
763
765 // clang-format off
766 sq = g_strdup_printf("ORDER BY group_id %s, mi.id-group_id != 0", order);
767 // clang-format on
768 break;
769
771 // clang-format off
772 sq = g_strdup_printf("ORDER BY folder %s", order);
773 // clang-format on
774 break;
775
777 // clang-format off
778 sq = g_strdup_printf("ORDER BY m.value %s", order);
779 // clang-format on
780 break;
781
783 default:/*fall through for default*/
784 // shouldn't happen
785 // clang-format off
786 sq = g_strdup_printf("ORDER BY mi.id %s", order);
787 // clang-format on
788 break;
789 }
790
791 // Finish with unique IDs in case we have aliasing
792 // try to keep grouped images next to each other, then similar files
793 sq = dt_util_dstrcat(sq, ", group_id ASC, mi.id-group_id != 0, filename ASC, version ASC, mi.id ASC");
794
795 return sq;
796}
797
798static int _recompose(void){
799 uint32_t result;
800 gchar *wq, *sq, *selq_pre, *selq_post, *query;
801 wq = sq = selq_pre = selq_post = query = NULL;
802
803 /* build where part */
804 gchar *where_ext = _extended_where();
806 {
808 }
810 {
811 char *rejected_check = g_strdup_printf("((flags & %d) = %d)", DT_IMAGE_REJECTED, DT_IMAGE_REJECTED);
812 int and_term = 1; // that effectively makes the use of and_operator() useless
813
814 // DON'T SELECT IMAGES MARKED TO BE DELETED.
815 wq = g_strdup_printf(" ((flags & %d) != %d) ", DT_IMAGE_REMOVE, DT_IMAGE_REMOVE);
816
817 /* From there, the other arguments are OR so we need parentheses if any rating filter is used */
818 gboolean got_rating_filter
823
826
828 /* Rejected was a mutually-exclusive rating in initial design, but got converted to
829 a toggle state circa 2019, aka images can now have a rating AND be rejected.
830 Which sucks because users will not expect rejected images to show when they target n stars ratings.
831 Aka we collect images that are rejected OR (have rating == n AND are not rejected).
832 Also, because rating flags are bitmasks but not octal, we can't build a single bitmask to
833 turn into a single SQL request
834 */
837
839 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
841
843 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
845
847 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
849
851 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
853
855 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
857
859 wq = dt_util_dstrcat(wq, " %s ((flags & 7) = %i AND NOT %s) ", or_operator(&or_term),
861
862 /* Closing the OR parentheses */
864 wq = dt_util_dstrcat(wq, ") ");
865
866 gboolean got_altered_filter
868
871
874 // clang-format off
875 wq = dt_util_dstrcat(wq, " %s id IN (SELECT imgid FROM main.history)",
877 // clang-format on
878
880 // clang-format off
881 wq = dt_util_dstrcat(wq, " %s id NOT IN (SELECT imgid FROM main.history) ",
883 // clang-format on
884
886 wq = dt_util_dstrcat(wq, ") ");
887
888 /* add text filter if any */
890 {
891 // clang-format off
892 wq = dt_util_dstrcat(wq, " %s id IN (SELECT id FROM main.meta_data WHERE value LIKE '%s'"
893 " UNION SELECT imgid AS id FROM main.tagged_images AS ti, data.tags AS t"
894 " WHERE t.id=ti.tagid AND (t.name LIKE '%s' OR t.synonyms LIKE '%s')"
895 " UNION SELECT id FROM main.images"
896 " WHERE filename LIKE '%s'"
897 " UNION SELECT i.id FROM main.images AS i, main.film_rolls AS fr"
898 " WHERE fr.id=i.film_id AND fr.folder LIKE '%s')",
904 // clang-format on
905 }
906
907 /* add colorlabel filter if any */
911
913 {
914 int color_mask = 0;
925
926 // color_mask = 31 when all flags are on
928
930
931 // clang-format off
932 if(color_mask > 0)
933 wq = dt_util_dstrcat(wq, " %s id IN (SELECT id FROM"
934 " (SELECT imgid AS id, SUM(1 << color) AS mask FROM main.color_labels GROUP BY imgid)"
935 " WHERE ((mask & %i) > 0))",
937
939 wq = dt_util_dstrcat(wq, " %s id NOT IN (SELECT id FROM"
940 " (SELECT imgid AS id, SUM(1 << color) AS mask FROM main.color_labels GROUP BY imgid)"
941 " WHERE ((mask & 31) > 0))",
943
944 // clang-format on
945 wq = dt_util_dstrcat(wq, ")");
946 }
947
948 /* add where ext if wanted */
951
953 }
954 else
955 {
956 // No filter set: no collection, because filters are toggle in.
957 // Just setup some bullshit condition impossible to match.
958 wq = g_strdup(" id=0");
959 }
960
962
963 /* build select part includes where */
964 /* only COLOR */
967 {
969 // clang-format off
970 selq_post = dt_util_dstrcat(selq_post, ") AS mi LEFT OUTER JOIN main.color_labels AS b ON mi.id = b.imgid");
971 // clang-format on
972 }
973 /* only PATH */
976 {
978 // clang-format off
980 (selq_post,
981 ") AS mi JOIN (SELECT id AS film_rolls_id, folder FROM main.film_rolls) ON film_id = film_rolls_id");
982 // clang-format on
983 }
984 /* only TITLE */
987 {
989 // clang-format off
990 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 ",
992 // clang-format on
993 }
995 {
996 const uint32_t tagid = _tagid;
997 char tag[16] = { 0 };
998 snprintf(tag, sizeof(tag), "%u", tagid);
999 // clang-format off
1001 "SELECT DISTINCT mi.id FROM (SELECT"
1002 " id, group_id, film_id, filename, datetime_taken, "
1003 " flags, version, %s position, aspect_ratio,"
1004 " maker, model, lens, aperture, exposure, focal_length,"
1005 " iso, import_timestamp, change_timestamp,"
1006 " export_timestamp, print_timestamp"
1007 " FROM main.images AS mi %s%s ) AS mi ",
1008 tagid ? "CASE WHEN ti.position IS NULL THEN 0 ELSE ti.position END AS" : "",
1009 tagid ? " LEFT JOIN main.tagged_images AS ti"
1010 " ON ti.imgid = mi.id AND ti.tagid = " : "",
1011 tagid ? tag : "");
1012 // clang-format on
1013 }
1014 else
1015 {
1016 const uint32_t tagid = _tagid;
1017 char tag[16] = { 0 };
1018 snprintf(tag, sizeof(tag), "%u", tagid);
1019 // clang-format off
1021 "SELECT DISTINCT mi.id FROM (SELECT"
1022 " id, group_id, film_id, filename, datetime_taken, "
1023 " flags, version, %s position, aspect_ratio,"
1024 " maker, model, lens, aperture, exposure, focal_length,"
1025 " iso, import_timestamp, change_timestamp,"
1026 " export_timestamp, print_timestamp"
1027 " FROM main.images AS mi %s%s ) AS mi WHERE ",
1028 tagid ? "CASE WHEN ti.position IS NULL THEN 0 ELSE ti.position END AS" : "",
1029 tagid ? " LEFT JOIN main.tagged_images AS ti"
1030 " ON ti.imgid = mi.id AND ti.tagid = " : "",
1031 tagid ? tag : "");
1032 // clang-format on
1033 }
1034
1035
1036 /* build sort order part */
1039 {
1040 sq = _sort_query();
1041 }
1042
1043 /* store the new query */
1044 query
1045 = dt_util_dstrcat(query, "%s%s%s %s%s", selq_pre, wq, selq_post ? selq_post : "", sq ? sq : "",
1047
1048 result = _store(query);
1049
1050 /* free memory used */
1051 dt_free(sq);
1052 dt_free(wq);
1055 dt_free(query);
1056
1057 return result;
1058}
1059
1060static uint32_t _compute_count(void){
1061 uint32_t count = 1;
1064 "SELECT COUNT(DISTINCT imgid) from memory.collected_images",
1065 -1, &stmt, NULL);
1066 if(IS_NULL_PTR(stmt)) return count;
1069 _count = count;
1070 return count;
1071}
1072
1073
1074static const gchar *_ensure_query(void)
1075{
1077 return _query;
1078}
1079
1085static gchar **_compose_where_ext(const dt_collection_rule_t *rules, const int n_rules)
1086{
1087 static const char *const conj[] = { "AND", "OR", "AND NOT" };
1088
1089 gchar **parts = g_malloc0_n(n_rules + 1, sizeof(gchar *));
1090 for(int i = 0; i < n_rules; i++)
1091 {
1092 const dt_collection_rule_t *r = &rules[i];
1093 const int mode = CLAMP(r->mode, 0, 2);
1094
1095 if(IS_NULL_PTR(r->text) || r->text[0] == '\0')
1096 {
1097 parts[i] = g_strdup((mode == 1) ? " OR 1=1" : "");
1098 }
1099 else
1100 {
1101 gchar *where = get_query_string(r->property, r->text, r->recursive);
1102 parts[i] = g_strdup_printf(" %s %s", conj[mode], where);
1103 dt_free(where);
1104 }
1105 }
1106 return parts;
1107}
1108
1110 const dt_collection_rule_t *rules, const int n_rules,
1111 const uint32_t tagid)
1112{
1113 if(IS_NULL_PTR(params)) return 0;
1114
1115 // Copy the rules in: the caller owns its own and may change them under us.
1117 _params = *params;
1118 _params.text_filter = params->text_filter ? g_strdup(params->text_filter) : NULL;
1119
1121 _where_ext = (rules && n_rules > 0) ? _compose_where_ext(rules, n_rules) : NULL;
1122 _tagid = tagid;
1123
1124 return _recompose();
1125}
1126
1128{
1129 return _recompose();
1130}
1131
1133{
1134 return _count;
1135}
1136
1137void dt_collection_query_set_iop_names(const dt_iop_name_row_t *rows, const size_t count)
1138{
1139 if(IS_NULL_PTR(rows) || count == 0) return;
1140
1141 // Faster than building a huge VALUES string: reuse a prepared statement and bind per module.
1144 "INSERT INTO memory.darktable_iop_names (operation, name) VALUES (?1, ?2)",
1145 -1, &stmt, NULL);
1146 if(IS_NULL_PTR(stmt)) return;
1147
1149 for(size_t i = 0; i < count; i++)
1150 {
1153 DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, rows[i].operation, -1, SQLITE_TRANSIENT);
1156 }
1159}
1160
1162{
1163 // Bumped by every accepted recomposition. Callers that used to hash the query text to notice a
1164 // collection change compare this instead: one number that cannot go stale field by field.
1165 return _generation;
1166}
1167
1168GList *dt_collection_query_get_group_members(const int32_t group_id, const int32_t exclude_imgid)
1169{
1170 const gchar *collection_query = _ensure_query();
1171 if(IS_NULL_PTR(collection_query)) return NULL;
1172
1174 // clang-format off
1175 gchar *query = g_strdup_printf("SELECT id"
1176 " FROM main.images"
1177 " WHERE group_id = %d AND id IN (%s)",
1178 group_id, collection_query);
1179 // clang-format on
1181 dt_free(query);
1182
1183 GList *ids = NULL;
1184 while(sqlite3_step(stmt) == SQLITE_ROW)
1185 {
1186 const int32_t id = sqlite3_column_int(stmt, 0);
1187 if(id != exclude_imgid) ids = g_list_prepend(ids, GINT_TO_POINTER(id));
1188 }
1190 return g_list_reverse(ids);
1191}
1192
1194{
1195 if(IS_NULL_PTR(req)) return NULL;
1196 const dt_collection_properties_t property = req->property;
1197
1198 GList *out = NULL;
1199 gchar *where_ext = _extended_where_excluding(req->exclude_rule, req->apply_exclude);
1200
1201 // Camera is special: it groups on two text columns and combines them into a display name.
1202 if(property == DT_COLLECTION_PROP_CAMERA)
1203 {
1204 gchar *q = g_strdup_printf("SELECT maker, model, COUNT(*) AS count FROM main.images AS mi"
1205 " WHERE %s GROUP BY maker, model", where_ext);
1209 int index = 0;
1210 while(stmt && sqlite3_step(stmt) == SQLITE_ROW)
1211 {
1212 const char *maker = (const char *)sqlite3_column_text(stmt, 0);
1213 const char *model = (const char *)sqlite3_column_text(stmt, 1);
1216 }
1218 g_free(q);
1219 return g_list_reverse(out);
1220 }
1221
1222 const gboolean is_date = property == DT_COLLECTION_PROP_DAY || property == DT_COLLECTION_PROP_TIME
1227 const gboolean has_status
1228 = (property == DT_COLLECTION_PROP_FOLDERS || property == DT_COLLECTION_PROP_FILMROLL);
1229 gchar *query = NULL;
1230
1231 switch(property)
1232 {
1234 query = g_strdup_printf("SELECT folder, film_rolls_id, COUNT(*) AS count, status"
1235 " FROM main.images AS mi"
1236 " JOIN (SELECT fr.id AS film_rolls_id, folder, status"
1237 " FROM main.film_rolls AS fr"
1238 " JOIN memory.film_folder AS ff ON fr.id = ff.id)"
1239 " ON film_id = film_rolls_id"
1240 " WHERE %s GROUP BY folder, film_rolls_id", where_ext);
1241 break;
1242
1244 query = g_strdup_printf("SELECT name, 1 AS tagid, SUM(count) AS count"
1245 " FROM (SELECT tagid, COUNT(*) as count"
1246 " FROM main.images AS mi JOIN main.tagged_images ON id = imgid"
1247 " WHERE %s GROUP BY tagid)"
1248 " JOIN (SELECT name, id AS tag_id FROM data.tags)"
1249 " ON tagid = tag_id GROUP BY name", where_ext);
1250 query = dt_util_dstrcat(query, " UNION ALL "
1251 "SELECT '%s' AS name, 0 as id, COUNT(*) AS count "
1252 "FROM main.images AS mi WHERE mi.id NOT IN"
1253 " (SELECT DISTINCT imgid FROM main.tagged_images AS ti"
1254 " WHERE ti.tagid NOT IN memory.darktable_tags)",
1255 _("not tagged"));
1256 break;
1257
1259 query = g_strdup_printf("SELECT CASE WHEN mi.longitude IS NULL OR mi.latitude IS null THEN '%s'"
1260 " ELSE CASE WHEN ta.imgid IS NULL THEN '%s' ELSE '%s' || ta.tagname END"
1261 " END AS name, ta.tagid AS tag_id, COUNT(*) AS count"
1262 " FROM main.images AS mi"
1263 " LEFT JOIN (SELECT imgid, t.id AS tagid, SUBSTR(t.name, %d) AS tagname"
1264 " FROM main.tagged_images AS ti JOIN data.tags AS t ON ti.tagid = t.id"
1265 " JOIN data.locations AS l ON l.tagid = t.id) AS ta ON ta.imgid = mi.id"
1266 " WHERE %s GROUP BY name, tag_id",
1267 _("not tagged"), _("tagged"), _("tagged"),
1269 break;
1270
1272 query = g_strdup_printf("SELECT (datetime_taken / 86400000000) * 86400000000 AS date, 1, COUNT(*) AS count"
1273 " FROM main.images AS mi"
1274 " WHERE datetime_taken IS NOT NULL AND datetime_taken <> 0 AND %s"
1275 " GROUP BY date", where_ext);
1276 break;
1277
1283 {
1284 char *colname = NULL;
1285 switch(property)
1286 {
1287 case DT_COLLECTION_PROP_TIME: colname = "datetime_taken"; break;
1288 case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: colname = "import_timestamp"; break;
1289 case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: colname = "change_timestamp"; break;
1290 case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: colname = "export_timestamp"; break;
1291 case DT_COLLECTION_PROP_PRINT_TIMESTAMP: colname = "print_timestamp"; break;
1292 default: break; // unreachable: outer switch already restricts to the timestamp cases
1293 }
1294 query = g_strdup_printf("SELECT %s AS date, 1, COUNT(*) AS count FROM main.images AS mi"
1295 " WHERE %s IS NOT NULL AND %s <> 0 AND %s GROUP BY date",
1297 break;
1298 }
1299
1301 query = g_strdup_printf("SELECT CASE WHEN EXISTS (SELECT 1 FROM main.history h WHERE h.imgid = mi.id)"
1302 " THEN '%s' ELSE '%s' END as altered, 1, COUNT(*) AS count"
1303 " FROM main.images AS mi WHERE %s GROUP BY altered ORDER BY altered ASC",
1304 _("altered"), _("unaltered"), where_ext);
1305 break;
1306
1308 query = g_strdup_printf("SELECT CASE WHEN (flags & %d) THEN '%s' ELSE '%s' END as lcp, 1, COUNT(*) AS count"
1309 " FROM main.images AS mi WHERE %s GROUP BY lcp ORDER BY lcp ASC",
1310 DT_IMAGE_LOCAL_COPY, _("copied locally"), _("not copied locally"), where_ext);
1311 break;
1312
1314 query = g_strdup_printf("SELECT CASE color WHEN 0 THEN '%s' WHEN 1 THEN '%s' WHEN 2 THEN '%s'"
1315 " WHEN 3 THEN '%s' WHEN 4 THEN '%s' ELSE '' END, color, COUNT(*) AS count"
1316 " FROM main.images AS mi"
1317 " JOIN (SELECT imgid AS color_labels_id, color FROM main.color_labels)"
1318 " ON id = color_labels_id WHERE %s GROUP BY color ORDER BY color DESC",
1319 _("red"), _("yellow"), _("green"), _("blue"), _("purple"), where_ext);
1320 break;
1321
1323 query = g_strdup_printf("SELECT lens, 1, COUNT(*) AS count FROM main.images AS mi WHERE %s"
1324 " GROUP BY lens ORDER BY lens", where_ext);
1325 break;
1326
1328 query = g_strdup_printf("SELECT CAST(focal_length AS INTEGER) AS focal_length, 1, COUNT(*) AS count"
1329 " FROM main.images AS mi WHERE %s GROUP BY CAST(focal_length AS INTEGER)"
1330 " ORDER BY CAST(focal_length AS INTEGER)", where_ext);
1331 break;
1332
1334 query = g_strdup_printf("SELECT CAST(iso AS INTEGER) AS iso, 1, COUNT(*) AS count"
1335 " FROM main.images AS mi WHERE %s GROUP BY iso ORDER BY iso", where_ext);
1336 break;
1337
1339 query = g_strdup_printf("SELECT ROUND(aperture,1) AS aperture, 1, COUNT(*) AS count"
1340 " FROM main.images AS mi WHERE %s GROUP BY aperture ORDER BY aperture", where_ext);
1341 break;
1342
1344 query = g_strdup_printf("SELECT CASE WHEN (exposure < 0.4) THEN '1/' || CAST(1/exposure + 0.9 AS INTEGER)"
1345 " ELSE ROUND(exposure,2) || '\"' END as _exposure, 1, COUNT(*) AS count"
1346 " FROM main.images AS mi WHERE %s GROUP BY _exposure ORDER BY exposure", where_ext);
1347 break;
1348
1350 query = g_strdup_printf("SELECT filename, 1, COUNT(*) AS count FROM main.images AS mi WHERE %s"
1351 " GROUP BY filename ORDER BY filename", where_ext);
1352 break;
1353
1355 query = g_strdup_printf("SELECT CASE WHEN id = group_id THEN '%s' ELSE '%s' END as group_leader, 1,"
1356 " COUNT(*) AS count FROM main.images AS mi WHERE %s"
1357 " GROUP BY group_leader ORDER BY group_leader ASC",
1358 _("group leaders"), _("group followers"), where_ext);
1359 break;
1360
1362 query = g_strdup_printf("SELECT m.name AS module_name, 1, COUNT(*) AS count FROM main.images AS mi"
1363 " JOIN (SELECT DISTINCT imgid, operation FROM main.history WHERE enabled = 1) AS h"
1364 " ON h.imgid = mi.id JOIN memory.darktable_iop_names AS m"
1365 " ON m.operation = h.operation WHERE %s GROUP BY module_name ORDER BY module_name",
1366 where_ext);
1367 break;
1368
1370 {
1371 char *orders = NULL;
1372 for(int i = 0; i < _order_names_count; i++)
1373 orders = dt_util_dstrcat(orders, "WHEN mo.version = %d THEN '%s' ", i, _order_names[i]);
1374 orders = dt_util_dstrcat(orders, "ELSE '%s' ", _("none"));
1375 query = g_strdup_printf("SELECT CASE %s END as ver, 1, COUNT(*) AS count FROM main.images AS mi"
1376 " LEFT JOIN (SELECT imgid, version FROM main.module_order) mo ON mo.imgid = mi.id"
1377 " WHERE %s GROUP BY ver ORDER BY ver", orders, where_ext);
1378 g_free(orders);
1379 break;
1380 }
1381
1383 query = g_strdup_printf("SELECT CASE WHEN (flags & 8) == 8 THEN -1 ELSE (flags & 7) END AS rating, 1,"
1384 " COUNT(*) AS count FROM main.images AS mi WHERE %s GROUP BY rating ORDER BY rating",
1385 where_ext);
1386 break;
1387
1388 default:
1390 {
1392 // whether this metadata field is hidden is a display preference the caller resolved
1393 if(!req->metadata_hidden)
1394 query = g_strdup_printf("SELECT CASE WHEN value IS NULL THEN '%s' ELSE value END AS value, 1,"
1395 " COUNT(*) AS count, CASE WHEN value IS NULL THEN 0 ELSE 1 END AS force_order"
1396 " FROM main.images AS mi"
1397 " LEFT JOIN (SELECT id AS meta_data_id, value FROM main.meta_data WHERE key = %d)"
1398 " ON id = meta_data_id WHERE %s GROUP BY value ORDER BY force_order, value",
1399 _("not defined"), keyid, where_ext);
1400 }
1401 else // film roll
1402 {
1403 // likewise the film-roll ordering: a preference, resolved by the caller
1404 const char *order_by = req->filmroll_order_by;
1405 query = g_strdup_printf("SELECT folder, film_rolls_id, COUNT(*) AS count, status FROM main.images AS mi"
1406 " JOIN (SELECT fr.id AS film_rolls_id, folder, status FROM main.film_rolls AS fr"
1407 " JOIN memory.film_folder AS ff ON ff.id = fr.id) ON film_id = film_rolls_id"
1408 " WHERE %s GROUP BY folder ORDER BY %s", where_ext, order_by);
1409 }
1410 break;
1411 }
1413 if(!query) return NULL;
1414
1417 while(stmt && sqlite3_step(stmt) == SQLITE_ROW)
1418 {
1419 char *name;
1420 if(is_date)
1421 {
1422 char sdt[DT_DATETIME_EXIF_LENGTH] = { 0 };
1424 if(property == DT_COLLECTION_PROP_DAY) sdt[10] = '\0';
1425 name = g_strdup(sdt);
1426 }
1427 else
1428 {
1429 const char *txt = (const char *)sqlite3_column_text(stmt, 0);
1430 name = txt ? g_strdup(txt) : g_strdup("");
1431 }
1432 const int id = sqlite3_column_int(stmt, 1);
1433 const int count = sqlite3_column_int(stmt, 2);
1434 const int status = has_status ? sqlite3_column_int(stmt, 3) : -1;
1435 out = g_list_prepend(out, _name_value_new(name, id, count, status));
1436 }
1438 g_free(query);
1439 return g_list_reverse(out);
1440}
1441
1442void dt_collection_query_get_makermodels(const gchar *filter, GList **sanitized, GList **exif)
1443{
1444 gchar *needle = NULL;
1445 gboolean wildcard = FALSE;
1446
1448 if (sanitized)
1450
1451 if (filter && filter[0] != '\0')
1452 {
1453 needle = g_utf8_strdown(filter, -1);
1454 wildcard = (needle && needle[strlen(needle) - 1] == '%') ? TRUE : FALSE;
1455 if(wildcard)
1456 needle[strlen(needle) - 1] = '\0';
1457 }
1458
1461 "SELECT maker, model FROM main.images GROUP BY maker, model",
1462 -1, &stmt, NULL);
1463 if(IS_NULL_PTR(stmt)) return;
1464 while(sqlite3_step(stmt) == SQLITE_ROW)
1465 {
1466 const char *exif_maker = (char *)sqlite3_column_text(stmt, 0);
1467 const char *exif_model = (char *)sqlite3_column_text(stmt, 1);
1468
1469 gchar *makermodel = dt_collection_get_makermodel(exif_maker, exif_model);
1470
1471 gchar *haystack = g_utf8_strdown(makermodel, -1);
1473 || (!wildcard && !g_strcmp0(haystack, needle)))
1474 {
1475 if (exif)
1476 {
1477 // Append a two element list with maker and model
1481 *exif = g_list_append(*exif, inner_list);
1482 }
1483
1484 if (sanitized)
1485 {
1486 gchar *key = g_strdup(makermodel);
1488 }
1489 }
1491 dt_free(makermodel);
1492 }
1494 dt_free(needle);
1495
1496 if(sanitized)
1497 {
1500 }
1501}
1502
1504 gboolean recursive)
1505{
1506 // Build the same WHERE clause the collection would use for this single rule, then
1507 // enumerate the matching image ids. Independent of the currently active collection so it
1508 // can feed batch/background operations (remove, attach tag, pre-render thumbnails, ...).
1509 GList *result = NULL;
1510 gchar *where = get_query_string(property, text, recursive);
1511 if(IS_NULL_PTR(where)) return NULL;
1512
1513 gchar *query = g_strdup_printf("SELECT id FROM main.images WHERE %s", where);
1514 dt_free(where);
1515
1518 if(stmt)
1519 {
1520 while(sqlite3_step(stmt) == SQLITE_ROW)
1523 }
1524 dt_free(query);
1525
1526 return g_list_reverse(result);
1527}
1528
1530{
1531 // The first image of the collection that is NOT in `imgids`, searched after the list first and
1532 // then before it. Used to pick what to show once the listed images are gone.
1533 if(IS_NULL_PTR(imgids)) return -1;
1534
1535 gchar *txt = NULL;
1536 int i = 0;
1537 for(GList *l = imgids; l; l = g_list_next(l))
1538 {
1539 const int id = GPOINTER_TO_INT(l->data);
1540 if(i == 0)
1541 txt = dt_util_dstrcat(txt, "%d", id);
1542 else
1543 txt = dt_util_dstrcat(txt, ",%d", id);
1544 i++;
1545 }
1546
1547 int32_t next = -1;
1548 // 2. search the first imgid not in the list but AFTER the list (or in a gap inside the list)
1549 // we need to be carefull that some images in the list may not be present on screen (collapsed groups)
1550 // clang-format off
1551 gchar *query = g_strdup_printf("SELECT imgid"
1552 " FROM memory.collected_images"
1553 " WHERE imgid NOT IN (%s)"
1554 " AND rowid > (SELECT rowid"
1555 " FROM memory.collected_images"
1556 " WHERE imgid IN (%s)"
1557 " ORDER BY rowid LIMIT 1)"
1558 " ORDER BY rowid LIMIT 1",
1559 txt, txt);
1560 // clang-format on
1564 {
1565 next = sqlite3_column_int(stmt2, 0);
1566 }
1568 dt_free(query);
1569 // 3. if next is still unvalid, let's try to find the first untouched image BEFORE the list
1570 if(next < 0)
1571 {
1572 // clang-format off
1573 query = g_strdup_printf("SELECT imgid"
1574 " FROM memory.collected_images"
1575 " WHERE imgid NOT IN (%s)"
1576 " AND rowid < (SELECT rowid"
1577 " FROM memory.collected_images"
1578 " WHERE imgid IN (%s)"
1579 " ORDER BY rowid LIMIT 1)"
1580 " ORDER BY rowid DESC LIMIT 1",
1581 txt, txt);
1582 // clang-format on
1585 {
1586 next = sqlite3_column_int(stmt2, 0);
1587 }
1589 dt_free(query);
1590 }
1591 dt_free(txt);
1592 return next;
1593}
1594
1596{
1597 /* No cached statements any more: everything in this file is prepared and finalised per
1598 * call, so there is nothing to finalise ahead of the connection closing. */
1599 dt_free(_query);
1600 _query = NULL;
1604 _where_ext = NULL;
1605}
1606
1610
1611 /* check if we can get a query from collection */
1612 gchar *query = g_strdup(_ensure_query());
1613 if(IS_NULL_PTR(query)) return;
1614
1615 // The caller re-restricts the collection to the selection first when the GUI is in culling
1616 // mode: that is a decision about the interface, and this module cannot see one.
1617
1618 // 1. drop previous data
1619
1620 // clang-format off
1622 "DELETE FROM memory.collected_images",
1623 NULL, NULL, NULL);
1624 // reset autoincrement. need in star_key_accel_callback
1626 "DELETE FROM memory.sqlite_sequence"
1627 " WHERE name='collected_images'",
1628 NULL, NULL, NULL);
1629 // clang-format on
1630
1631 // 2. insert collected images into the temporary table
1632 gchar *ins_query = g_strdup_printf("INSERT INTO memory.collected_images (imgid) %s", query);
1633
1639
1640 dt_free(query);
1642
1643 // Re-restricting to the culling selection, and telling the user what just happened, are both
1644 // the caller's: this module rebuilds the table and counts what landed in it.
1646}
1647
1649 GList *list = NULL;
1650 const gchar *query = _ensure_query();
1651 if(query)
1652 {
1653 const gboolean use_limit = (_params.query_flags & COLLECTION_QUERY_USE_LIMIT) != 0;
1656 use_limit ? "SELECT imgid FROM memory.collected_images LIMIT -1, ?1"
1657 : "SELECT imgid FROM memory.collected_images",
1658 -1, &stmt, NULL);
1659 if(IS_NULL_PTR(stmt)) return NULL;
1661
1662 while(sqlite3_step(stmt) == SQLITE_ROW)
1663 {
1664 const int32_t imgid = sqlite3_column_int(stmt, 0);
1665 list = g_list_prepend(list, GINT_TO_POINTER(imgid));
1666 }
1668 }
1669
1670 return g_list_reverse(list); // list built in reverse order, so un-reverse it
1671}
1672
1675 return -1;
1676 const gchar *query = _ensure_query();
1681
1682 int result = -1;
1684 {
1685 result = sqlite3_column_int(stmt, 0);
1686 }
1687
1689
1690 return result;
1691
1692}
1693
1694int dt_collection_query_image_offset(const int32_t imgid){
1695 if(imgid == UNKNOWN_IMAGE) return 0;
1696 int offset = 0;
1699 "SELECT imgid FROM memory.collected_images",
1700 -1, &stmt, NULL);
1701 if(IS_NULL_PTR(stmt)) return 0;
1702
1703 gboolean found = FALSE;
1704
1705 while(sqlite3_step(stmt) == SQLITE_ROW)
1706 {
1707 const int id = sqlite3_column_int(stmt, 0);
1708 if(imgid == id)
1709 {
1710 found = TRUE;
1711 break;
1712 }
1713 offset++;
1714 }
1715
1717
1718 if(!found) offset = 0;
1719
1720 return offset;
1721}
1722
1724 // Restore previous collection
1725 DT_DEBUG_SQLITE3_EXEC(dt_database_get_sqlite3_global(), "DELETE FROM memory.collected_images", NULL, NULL, NULL);
1727 "INSERT INTO memory.collected_images"
1728 " SELECT * FROM memory.collected_backup",
1729 NULL, NULL, NULL);
1730}
1731
1733 // Backup current collection
1734 DT_DEBUG_SQLITE3_EXEC(dt_database_get_sqlite3_global(), "DELETE FROM memory.collected_backup", NULL, NULL, NULL);
1736 "INSERT INTO memory.collected_backup"
1737 " SELECT * FROM memory.collected_images",
1738 NULL, NULL, NULL);
1739}
1740
1742{
1743 // Drop everything the user has not selected. Deciding that culling mode means this, backing
1744 // the collection up first and resetting the selection afterwards, are the caller's -- they
1745 // are statements about selection and about a view mode, neither of which this module knows.
1747 "DELETE FROM memory.collected_images"
1748 " WHERE imgid NOT IN "
1749 " (SELECT imgid FROM main.selected_images)",
1750 NULL, NULL, NULL);
1751
1752 /* The published count follows every mutation of memory.collected_images this module
1753 * makes. The refresh already counted, but the caller runs this restriction AFTER the
1754 * refresh -- the original computed its count after both, and dt_collection_get_count()
1755 * in culling mode must report the culled subset, not the full collection. */
1757}
1758
1759// clang-format off
1760// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1761// vim: shiftwidth=2 expandtab tabstop=2 cindent
1762// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1763// 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:538
void dt_collection_split_operator_number(const gchar *input, char **number1, char **number2, char **operator)
Definition collection.c:409
void dt_collection_get_makermodels(const gchar *filter, GList **sanitized, GList **exif)
Definition collection.c:600
gchar * dt_collection_get_makermodel(const char *exif_maker, const char *exif_model)
Definition collection.c:605
void dt_collection_split_operator_datetime(const gchar *input, char **number1, char **number2, char **operator)
Definition collection.c:477
dt_collection_t * dt_collection_get_global(void)
Definition darktable.c:643
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:3753
gboolean dt_database_is_open(void)
Definition database.c:3742
#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
GdkRGBA color[]
Definition geotagging.c:539
#define UNKNOWN_IMAGE
Definition image.h:77
@ DT_IMAGE_REMOVE
Definition image.h:127
@ DT_IMAGE_LOCAL_COPY
Definition image.h:133
@ DT_IMAGE_REJECTED
Definition image.h:112
@ DT_VIEW_STAR_4
Definition image.h:329
@ DT_VIEW_STAR_5
Definition image.h:330
@ DT_VIEW_STAR_3
Definition image.h:328
@ DT_VIEW_STAR_1
Definition image.h:326
@ DT_VIEW_STAR_2
Definition image.h:327
@ DT_VIEW_DESERT
Definition image.h:325
const char * maker
const char * model
static const dt_iop_order_entry_t * orders[5]
Definition iop_order.c:949
#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:65
static void dt_free_gpointer(gpointer ptr)
Definition mem_alloc.h:104
#define dt_free(ptr)
Definition mem_alloc.h:97
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:182
dt_collection_sort_t sort
Definition collection.h:191
dt_collection_filter_flag_t filter_flags
Definition collection.h:185
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:832
gchar * dt_util_glist_to_str(const gchar *separator, GList *items)
Definition utility.c:169
gchar * dt_util_dstrcat(gchar *str, const gchar *format,...)
Definition utility.c:98