Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
removed_image_repository.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 darktable is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 darktable is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with darktable. If not, see <http://www.gnu.org/licenses/>.
17*/
18
20
21#include "common/logging.h"
22
23#include "database/database.h"
24#include "database/sql_debug.h"
25#include "system/macros.h"
26#include "system/mem_alloc.h"
27
28#include <sqlite3.h>
29
30/* Every table a removed image owns rows in, listed in the order a restore has to put them
31 * back: the film roll before the image whose film_id references it, the image before
32 * everything keyed on its id. `filter` is how the table is matched to one image while
33 * staging -- a film roll is reached through the image rather than by a column of its own,
34 * and `meta_data` spells the image column `id` where the rest spell it `imgid`.
35 *
36 * `child_key` names that column again for the tables the restore must CLEAR before it
37 * copies back, and is NULL for the two that are not the image's children -- its film roll,
38 * which other images share, and the image row itself, which a restore must never delete.
39 * Clearing matters because the schema does not cascade uniformly: `module_order`,
40 * `color_labels` and `meta_data` carry no foreign key on images(id), so their rows outlive
41 * the removal, and `color_labels` has no unique constraint either -- copying the staged row
42 * on top of a survivor would duplicate it, and duplicate it again on every further
43 * remove/undo cycle. Clearing first makes the restore idempotent and independent of which
44 * tables the schema happens to cascade. */
45typedef struct _removed_table_t
46{
47 const char *name;
48 const char *filter;
49 const char *child_key;
51
53 = { { "film_rolls", "id IN (SELECT film_id FROM main.images WHERE id = ?2)", NULL },
54 { "images", "id = ?2", NULL },
55 { "history", "imgid = ?2", "imgid" },
56 { "masks_history", "imgid = ?2", "imgid" },
57 { "module_order", "imgid = ?2", "imgid" },
58 { "tagged_images", "imgid = ?2", "imgid" },
59 { "color_labels", "imgid = ?2", "imgid" },
60 { "meta_data", "id = ?2", "id" },
61 { "history_hash", "imgid = ?2", "imgid" } };
62
63/* The columns of `memory.removed_<table>` as a comma-separated list, minus the two this
64 * module prepends. Asking the database rather than restating them here is the whole reason
65 * the twins are declared as `CREATE TABLE ... AS SELECT`: a schema migration then reaches
66 * the copies below without an edit in this file. */
67static gchar *_columns_of(const char *table)
68{
69 gchar *pragma = g_strdup_printf("PRAGMA memory.table_info('removed_%s')", table);
70 sqlite3_stmt *stmt = NULL;
72 dt_free(pragma);
73 if(IS_NULL_PTR(stmt)) return NULL;
74
75 GString *columns = g_string_new(NULL);
76 while(sqlite3_step(stmt) == SQLITE_ROW)
77 {
78 const char *name = (const char *)sqlite3_column_text(stmt, 1);
79 if(!g_strcmp0(name, "snap_id") || !g_strcmp0(name, "undo_imgid")) continue;
80 if(columns->len > 0) g_string_append_c(columns, ',');
81 g_string_append(columns, name);
82 }
83 sqlite3_finalize(stmt);
84
85 // an empty list means the twin table is missing, which no query below can survive
86 const gboolean empty = (columns->len == 0);
87 return g_string_free(columns, empty);
88}
89
90/* Run one `?1 = snap_id, ?2 = imgid` statement to completion. Every query in this file has
91 * that shape, which is what makes the file short. */
92static gboolean _run(const char *query, const int snap_id, const int32_t imgid)
93{
94 sqlite3_stmt *stmt = NULL;
96 if(IS_NULL_PTR(stmt)) return FALSE;
97
98 DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, snap_id);
99 DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, imgid);
100 const gboolean ok = (sqlite3_step(stmt) == SQLITE_DONE);
101 sqlite3_finalize(stmt);
102 return ok;
103}
104
105/* The id the next snapshot of this image gets: one past the highest it already has. Called
106 * only from inside dt_removed_image_repository_create()'s transaction, which is what makes
107 * reading it and writing rows under it one step -- see the comment there. */
108static int _next_id(const int32_t imgid)
109{
110 sqlite3_stmt *stmt = NULL;
112 "SELECT MAX(snap_id) FROM memory.removed_images WHERE undo_imgid = ?1",
113 -1, &stmt, NULL);
114 if(IS_NULL_PTR(stmt)) return 0;
115
116 DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, imgid);
117
118 int snap_id = 0;
119 if(sqlite3_step(stmt) == SQLITE_ROW) snap_id = sqlite3_column_int(stmt, 0) + 1;
120 sqlite3_finalize(stmt);
121 return snap_id;
122}
123
124/* What the staging actually costs, and whether discarding a record gives it back. A snapshot
125 * is a full copy of every row an image owns, held in an in-memory database until the undo
126 * record is dropped -- and neither VmRSS nor the process heap can answer whether it was given
127 * back, since SQLite keeps freed pages in its own cache and glibc rarely returns them to the
128 * kernel. sqlite3_memory_used() measures the one allocator that matters here. Under
129 * `-d memory` the pair of prints below draws the curve: it rises once per staged image and
130 * has to come back down as the records are discarded. */
131static void _debug_memory(const char *stage, const int snap_id, const int32_t imgid)
132{
133 // dt_print() gates on the channel itself, so this costs one atomic read when -d memory is off
134 dt_print(DT_DEBUG_MEMORY, "[removed_image] %s snapshot %d for image %d -- sqlite holds %lld bytes\n",
135 stage, snap_id, imgid, (long long)sqlite3_memory_used());
136}
137
139{
140 gboolean all_ok = TRUE;
141
143
144 /* The id is taken INSIDE the transaction, which is what makes "read the highest id, then
145 * write rows under the next one" a single step: dt_database_start_transaction() holds the
146 * database write lock for the whole span, so no other thread can read the same id in
147 * between. Taken outside, two removals of the same image racing each other would not fail
148 * -- the twins carry no unique constraint, a `CREATE TABLE ... AS SELECT` copying none --
149 * they would silently share one snapshot, and discarding either undo record would take the
150 * other's rows with it. */
151 const int snap_id = _next_id(imgid);
152
153 /* One exit, at the bottom: a table whose columns cannot be read and a statement that fails
154 * are the same outcome here, since the transaction is rolled back whole and there is
155 * nothing to gain by staging the rest. */
156 for(gsize i = 0; i < G_N_ELEMENTS(_removed_tables); i++)
157 {
158 gchar *columns = _columns_of(_removed_tables[i].name);
159 all_ok = !IS_NULL_PTR(columns);
160
161 if(all_ok)
162 {
163 gchar *query = g_strdup_printf("INSERT INTO memory.removed_%s (snap_id, undo_imgid, %s)"
164 " SELECT ?1, ?2, %s FROM main.%s WHERE %s",
165 _removed_tables[i].name, columns, columns,
167 all_ok = _run(query, snap_id, imgid);
168 dt_free(query);
169 dt_free(columns);
170 }
171
172 if(!all_ok) break;
173 }
174
175 /* Who else is in this image's group. dt_grouping_remove_from_group() hands the group to a
176 * new leader on the way out, rewriting the group_id of images that are not being removed
177 * at all -- so that rewrite lives in no table above, and undoing the removal has to undo
178 * it too. */
179 all_ok = all_ok && _run("INSERT INTO memory.removed_groups (snap_id, undo_imgid, id, group_id)"
180 " SELECT ?1, ?2, id, group_id FROM main.images"
181 " WHERE group_id = (SELECT group_id FROM main.images WHERE id = ?2)",
182 snap_id, imgid);
183
184 if(all_ok)
186 else
188
189 _debug_memory("staged", snap_id, imgid);
190 return all_ok ? snap_id : -1;
191}
192
193gboolean dt_removed_image_repository_restore(const int snap_id, const int32_t imgid)
194{
196
197 /* A whole film roll removed in one go comes back one image per undo record, in whatever
198 * order the undo list holds, so an image's group leader is regularly still missing when
199 * its own row goes back in. Deferring the foreign keys to the commit is what lets the
200 * restore run in table order; SQLite clears the pragma at the commit on its own. */
201 sqlite3_exec(dt_database_get_sqlite3_global(), "PRAGMA defer_foreign_keys = ON", NULL, NULL, NULL);
202
203 gboolean all_ok = TRUE;
204
205 // one exit at the bottom, for the same reason as the staging loop above
206 for(gsize i = 0; i < G_N_ELEMENTS(_removed_tables); i++)
207 {
208 gchar *columns = _columns_of(_removed_tables[i].name);
209 all_ok = !IS_NULL_PTR(columns);
210
211 if(all_ok)
212 {
213 // whatever survived the removal in this table goes, so the staged copy is the only one
214 if(!IS_NULL_PTR(_removed_tables[i].child_key))
215 {
216 gchar *clear = g_strdup_printf("DELETE FROM main.%s WHERE %s = ?2",
217 _removed_tables[i].name, _removed_tables[i].child_key);
218 all_ok = _run(clear, snap_id, imgid);
219 dt_free(clear);
220 }
221
222 /* OR IGNORE because the film roll is regularly still there: only the roll's LAST image
223 * takes it down, and every image of the roll staged a copy of it. */
224 gchar *query = g_strdup_printf("INSERT OR IGNORE INTO main.%s (%s)"
225 " SELECT %s FROM memory.removed_%s"
226 " WHERE snap_id = ?1 AND undo_imgid = ?2",
227 _removed_tables[i].name, columns, columns,
229 all_ok = all_ok && _run(query, snap_id, imgid);
230 dt_free(query);
231 dt_free(columns);
232 }
233
234 if(!all_ok) break;
235 }
236
237 // give the group its leader back -- those rows were never deleted, so this is an update
238 all_ok = all_ok && _run("UPDATE main.images SET group_id ="
239 " (SELECT g.group_id FROM memory.removed_groups AS g"
240 " WHERE g.snap_id = ?1 AND g.undo_imgid = ?2 AND g.id = main.images.id)"
241 " WHERE id IN (SELECT id FROM memory.removed_groups"
242 " WHERE snap_id = ?1 AND undo_imgid = ?2)",
243 snap_id, imgid);
244
245 /* Anything the previous statement pointed at an image that has not come back -- because
246 * its own undo record has not been popped, or never will be -- is repointed at itself:
247 * main.images.group_id has a foreign key on main.images.id and the commit would refuse it.
248 * A group left split by a partial undo is the honest outcome, not something to invent a
249 * leader for. */
250 all_ok = all_ok && _run("UPDATE main.images SET group_id = id"
251 " WHERE id IN (SELECT id FROM memory.removed_groups"
252 " WHERE snap_id = ?1 AND undo_imgid = ?2)"
253 " AND group_id NOT IN (SELECT id FROM main.images)",
254 snap_id, imgid);
255
256 if(all_ok)
258 else
260
261 return all_ok;
262}
263
264void dt_removed_image_repository_clear(const int snap_id, const int32_t imgid)
265{
266 for(gsize i = 0; i < G_N_ELEMENTS(_removed_tables); i++)
267 {
268 gchar *query = g_strdup_printf("DELETE FROM memory.removed_%s WHERE snap_id = ?1 AND undo_imgid = ?2",
270 _run(query, snap_id, imgid);
271 dt_free(query);
272 }
273
274 _run("DELETE FROM memory.removed_groups WHERE snap_id = ?1 AND undo_imgid = ?2", snap_id, imgid);
275
276 _debug_memory("dropped", snap_id, imgid);
277}
278
279// clang-format off
280// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
281// vim: shiftwidth=2 expandtab tabstop=2 cindent
282// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
283// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
sqlite3 * dt_database_get_sqlite3_global(void)
Definition database.c:3782
void dt_database_rollback_transaction(void)
Definition database.c:4899
#define dt_database_start_transaction()
Definition database.h:290
#define dt_database_release_transaction()
Definition database.h:291
@ DT_DEBUG_MEMORY
Definition logging.h:59
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
#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
#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 * name
Definition pdf.h:90
static const _removed_table_t _removed_tables[]
static gboolean _run(const char *query, const int snap_id, const int32_t imgid)
void dt_removed_image_repository_clear(const int snap_id, const int32_t imgid)
Drop snapshot snap_id of imgid, making the removal permanent.
static gchar * _columns_of(const char *table)
gboolean dt_removed_image_repository_restore(const int snap_id, const int32_t imgid)
Copy snapshot snap_id of imgid back into main, film roll included.
static void _debug_memory(const char *stage, const int snap_id, const int32_t imgid)
static int _next_id(const int32_t imgid)
int dt_removed_image_repository_create(const int32_t imgid)
Copy every row imgid owns out of main, so the removal about to happen can be undone.
memory.removed_*: the staging area behind "undo remove from library".
Checked wrappers around the sqlite3_* calls: trace the statement, assert the return code,...
#define DT_DEBUG_SQLITE3_PREPARE_V2(a, b, c, d, e)
Definition sql_debug.h:137
#define DT_DEBUG_SQLITE3_BIND_INT(a, b, c)
Definition sql_debug.h:145