Branch data Line data Source code
1 : : /**
2 : : * @file db_save_prefixes.c
3 : : * @brief Database operations for directory prefix paths
4 : : */
5 : :
6 : : #include "precizer.h"
7 : :
8 : : /**
9 : : * @brief Return the number of rows changed by the current SQLite connection
10 : : *
11 : : * SQLite 3.37.0 added `sqlite3_total_changes64()`. Older system SQLite
12 : : * packages only provide `sqlite3_total_changes()`, so dynamic builds use the
13 : : * older 32-bit counter and widen its result to keep the local return type stable.
14 : : * This legacy can be removed in 2036 (10-year Long-Term Support)
15 : : * Replacement: `db_retrieve_total_changes()` -> `sqlite3_total_changes64()`
16 : : *
17 : : * @param[in] db SQLite database connection
18 : : * @return Total number of changed rows reported by SQLite for this connection
19 : : */
20 : 696 : static sqlite3_int64 db_retrieve_total_changes(sqlite3 *db)
21 : : {
22 : : #if SQLITE_VERSION_NUMBER >= 3037000
23 : 696 : return(sqlite3_total_changes64(db));
24 : : #else
25 : : return((sqlite3_int64)sqlite3_total_changes(db));
26 : : #endif
27 : : }
28 : :
29 : : /**
30 : : * @brief Save the current traversal roots into the `paths` table
31 : : *
32 : : * The positional directories accepted by normal scanning mode are stored in
33 : : * `config->roots`. This function writes each root exactly as it was accepted
34 : : * from the command line, so the database keeps the user's chosen root spelling
35 : : * while file records stay relative to that root
36 : : *
37 : : * In `--compare` mode the function returns immediately because compare
38 : : * arguments are database files
39 : : *
40 : : * Prefix rows are written in one transaction whenever the selected mode allows
41 : : * database changes. With `--force`, obsolete path rows are removed before the
42 : : * current roots are inserted. If an SQLite operation fails, the transaction is
43 : : * rolled back. With `--dry-run` against an already existing physical database,
44 : : * inserts are skipped so the on-disk database is not modified
45 : : *
46 : : * The primary database is marked as modified only when at least one prefix row
47 : : * changes and any required transaction commits successfully
48 : : *
49 : : * For example, after parsing `precizer --database tree.db /home/me/tree`,
50 : : * `config->roots` contains `/home/me/tree`, and this function ensures that the
51 : : * prefix exists in the database
52 : : *
53 : : * @return `SUCCESS` when all required prefixes are present or intentionally
54 : : * skipped by mode. `FAILURE` when an SQLite operation fails
55 : : */
56 : 474 : Return db_save_prefixes(void)
57 : : {
58 : : /* Status returned by this function through provide()
59 : : Default value assumes successful completion */
60 : 474 : Return status = SUCCESS;
61 : :
62 : : /* Result code returned by the most recent SQLite operation */
63 : 474 : int rc = SQLITE_OK;
64 : :
65 : : /* Interrupt the function smoothly */
66 : : /* Interrupt when Ctrl+C */
67 [ - + ]: 474 : if(global_interrupt_flag == true)
68 : : {
69 : 0 : provide(status);
70 : : }
71 : :
72 : : /* Skip in comparison mode */
73 [ + + ]: 474 : if(config->compare == true)
74 : : {
75 : 112 : provide(status);
76 : : }
77 : :
78 : : /*
79 : : * Dry-run mode has two database scenarios.
80 : : * If a physical primary database already exists, it is opened read-only and
81 : : * must not be changed, so prefix saving stops here.
82 : : * If no physical database exists, dry-run uses an in-memory SQLite database.
83 : : * That temporary database still needs path prefixes so the simulated scan
84 : : * behaves like a normal run
85 : : */
86 [ + + + + ]: 362 : if(config->dry_run == true && config->db_primary_file_exists == true)
87 : : {
88 : 11 : provide(status);
89 : : }
90 : :
91 : : /*
92 : : * Remember how many rows this database connection has changed so far. After
93 : : * the transaction, a larger value means that prefix rows were actually changed
94 : : */
95 : 351 : const sqlite3_int64 total_changes_before = db_retrieve_total_changes(config->db);
96 : :
97 : : /*
98 : : * Start one transaction for the complete prefix update. This keeps removals
99 : : * and additions together so a later error can roll back the whole change
100 : : */
101 : 351 : rc = sqlite3_exec(config->db,"BEGIN TRANSACTION",NULL,NULL,NULL);
102 : :
103 [ - + ]: 351 : if(SQLITE_OK != rc)
104 : : {
105 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to begin prefix update transaction");
106 : 0 : provide(FAILURE);
107 : : }
108 : :
109 : : /*
110 : : * Update the paths table inside the transaction. Force mode first removes
111 : : * obsolete prefixes, then the remaining modes add traversal roots that are
112 : : * not already present
113 : : */
114 [ + + + - ]: 351 : if(config->force == true && config->dry_run == false)
115 : : {
116 : : /* Prepared statement used to delete obsolete prefix rows */
117 : 2 : sqlite3_stmt *delete_stmt = NULL;
118 : :
119 : : /* Query that removes prefix rows which are no longer needed */
120 : 2 : const char *delete_sql = "DELETE FROM paths WHERE ID IN (SELECT path_id FROM the_path_id_does_not_exists);";
121 : :
122 [ + - ]: 2 : if(SUCCESS == status)
123 : : {
124 : 2 : rc = sqlite3_prepare_v2(config->db,delete_sql,-1,&delete_stmt,NULL);
125 : :
126 [ - + ]: 2 : if(SQLITE_OK != rc)
127 : : {
128 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare delete statement");
129 : 0 : status = FAILURE;
130 : : }
131 : : }
132 : :
133 [ + - ]: 2 : if(SUCCESS == status)
134 : : {
135 : : /* Execute SQL statement */
136 : 2 : rc = sqlite3_step(delete_stmt);
137 : :
138 [ - + ]: 2 : if(SQLITE_DONE != rc)
139 : : {
140 : 0 : log_sqlite_error(config->db,rc,NULL,"Delete statement didn't return DONE");
141 : 0 : status = FAILURE;
142 : : }
143 : : }
144 : :
145 : 2 : rc = sqlite3_finalize(delete_stmt);
146 : :
147 [ + - - + ]: 2 : if(SUCCESS == status && SQLITE_OK != rc)
148 : : {
149 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize delete statement");
150 : 0 : status = FAILURE;
151 : : }
152 : : }
153 : :
154 : : /*
155 : : * Insert every configured traversal root that is not already present.
156 : : * Read-only dry-run scans return before the transaction starts. Each loop
157 : : * iteration exposes the current root descriptor as `root`
158 : : */
159 [ + + + + ]: 1055 : m_string_array_foreach(conf(roots),root)
160 : : {
161 : : /* Read-only text view of the current root descriptor */
162 : 353 : const char *root_path = m_text(root);
163 : :
164 : : /* Number of bytes in the current root path, excluding its terminator */
165 : : size_t root_path_length;
166 : :
167 [ + - ]: 353 : if(SUCCESS == status)
168 : : {
169 : 353 : status = m_string_length(root,&root_path_length);
170 : : }
171 : :
172 : : /* Query that inserts the current prefix unless its unique value already exists */
173 : 353 : const char *insert_sql = "INSERT OR IGNORE INTO paths(prefix) VALUES(?1);";
174 : :
175 : : /* Prepared statement for the current insert attempt */
176 : 353 : sqlite3_stmt *insert_stmt = NULL;
177 : :
178 : : /* Existing prefixes are ignored by the database constraint */
179 [ + - ]: 353 : if(SUCCESS == status)
180 : : {
181 : : /* Create SQL statement. Prepare to write */
182 : 353 : rc = sqlite3_prepare_v2(config->db,insert_sql,-1,&insert_stmt,NULL);
183 : :
184 [ - + ]: 353 : if(SQLITE_OK != rc)
185 : : {
186 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare insert statement %s",insert_sql);
187 : 0 : status = FAILURE;
188 : : }
189 : : }
190 : :
191 [ + - ]: 353 : if(SUCCESS == status)
192 : : {
193 : 353 : rc = sqlite3_bind_text(insert_stmt,1,root_path,(int)root_path_length,NULL);
194 : :
195 [ - + ]: 353 : if(SQLITE_OK != rc)
196 : : {
197 : 0 : log_sqlite_error(config->db,rc,NULL,"Error binding value in insert");
198 : 0 : status = FAILURE;
199 : : }
200 : : }
201 : :
202 : : /* Execute SQL statement */
203 [ + - ]: 353 : if(SUCCESS == status)
204 : : {
205 : 353 : rc = sqlite3_step(insert_stmt);
206 : :
207 [ - + ]: 353 : if(SQLITE_DONE != rc)
208 : : {
209 : 0 : log_sqlite_error(config->db,rc,NULL,"Insert statement didn't return DONE");
210 : 0 : status = FAILURE;
211 : : }
212 : : }
213 : :
214 : 353 : rc = sqlite3_finalize(insert_stmt);
215 : :
216 [ + - - + ]: 353 : if(SUCCESS == status && SQLITE_OK != rc)
217 : : {
218 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize insert statement");
219 : 0 : status = FAILURE;
220 : : }
221 : :
222 [ - + ]: 353 : if(SUCCESS != status)
223 : : {
224 : 0 : break;
225 : : }
226 : : }
227 : :
228 : : /*
229 : : * Finish the transaction after all prefix operations. Commit the complete
230 : : * update after success, or roll it back if any operation failed
231 : : */
232 [ + - ]: 351 : if(SUCCESS == status)
233 : : {
234 : : /* Commit transaction */
235 : 351 : rc = sqlite3_exec(config->db,"COMMIT",NULL,NULL,NULL);
236 : :
237 [ - + ]: 351 : if(SQLITE_OK != rc)
238 : : {
239 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to commit prefix update transaction");
240 : 0 : status = FAILURE;
241 : : }
242 : : }
243 : :
244 : : /*
245 : : * Roll back after an error only while the transaction is still open. SQLite
246 : : * may already have rolled it back automatically for some failures
247 : : */
248 [ - + - - ]: 351 : if(SUCCESS != status && sqlite3_get_autocommit(config->db) == 0)
249 : : {
250 : : /* Attempt rollback */
251 : 0 : rc = sqlite3_exec(config->db,"ROLLBACK",NULL,NULL,NULL);
252 : :
253 [ # # ]: 0 : if(SQLITE_OK == rc)
254 : : {
255 : 0 : slog(TRACE,"The prefix update transaction has been rolled back\n");
256 : : } else {
257 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to rollback prefix update transaction");
258 : 0 : status = FAILURE;
259 : : }
260 : : }
261 : :
262 [ + - ]: 351 : if(SUCCESS == status
263 [ + + ]: 351 : && config->dry_run == false
264 [ + + ]: 345 : && total_changes_before < db_retrieve_total_changes(config->db))
265 : : {
266 : : /*
267 : : * In-memory dry-run prefix writes are only simulation data.
268 : : * They must not mark the real primary database as modified
269 : : */
270 : : /* Reflect changes in global */
271 : 203 : config->db_primary_file_modified = true;
272 : : }
273 : :
274 : 351 : provide(status);
275 : : }
|