Branch data Line data Source code
1 : : #include "precizer.h"
2 : :
3 : : /**
4 : : * @brief Attaches a secondary database to the primary database connection.
5 : : *
6 : : * The path comes from the two `--compare` positional arguments. SQLite attaches
7 : : * the selected database under an internal schema alias such as `db1` or `db2`
8 : : *
9 : : * @param[in] db_A Index of the database path in `config->db_file_paths`.
10 : : * @param[in] db_B Database number (1 or 2) to be used in the ATTACH DATABASE command.
11 : : * @return Return structure indicating the operation status.
12 : : */
13 : 224 : static Return db_attach(
14 : : int db_A,
15 : : int db_B)
16 : : {
17 : : /* This function was reviewed line by line by a human and is not AI-generated
18 : : Any change to this function requires separate explicit approval */
19 : :
20 : : /* Status returned by this function through provide()
21 : : Default value assumes successful completion */
22 : 224 : Return status = SUCCESS;
23 : :
24 : 224 : m_create(char,sql,MEMORY_STRING);
25 : 224 : sqlite3_stmt *stmt = NULL;
26 : :
27 : 224 : const memory *db_file_path = m_item_ro(memory,conf(db_file_paths),db_A);
28 : 224 : size_t db_file_path_length = 0;
29 : :
30 [ - + ]: 224 : if(db_file_path == NULL)
31 : : {
32 : 0 : report("Database path item is unavailable for index: %d",db_A);
33 : 0 : status = FAILURE;
34 : : }
35 : :
36 [ + - ]: 224 : if(SUCCESS == status)
37 : : {
38 : 224 : status = m_string_length(db_file_path,&db_file_path_length);
39 : : }
40 : :
41 [ + - ]: 224 : if(SUCCESS == status)
42 : : {
43 : 224 : run(m_formatted_string(sql,"ATTACH DATABASE ?1 AS db%d;",db_B));
44 : : }
45 : :
46 [ + - ]: 224 : if(SUCCESS == status)
47 : : {
48 : 224 : int rc = sqlite3_prepare_v2(config->db,m_text(sql),-1,&stmt,NULL);
49 : :
50 [ - + ]: 224 : if(SQLITE_OK != rc)
51 : : {
52 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to prepare attach statement");
53 : 0 : status = FAILURE;
54 : : }
55 : : }
56 : :
57 [ + - ]: 224 : if(SUCCESS == status)
58 : : {
59 : 224 : int rc = sqlite3_bind_text(stmt,1,m_text(db_file_path),(int)db_file_path_length,NULL);
60 : :
61 [ - + ]: 224 : if(SQLITE_OK != rc)
62 : : {
63 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to bind database path in attach");
64 : 0 : status = FAILURE;
65 : : }
66 : : }
67 : :
68 [ + - ]: 224 : if(SUCCESS == status)
69 : : {
70 : 224 : int rc = sqlite3_step(stmt);
71 : :
72 [ - + ]: 224 : if(SQLITE_DONE != rc)
73 : : {
74 : 0 : log_sqlite_error(config->db,rc,NULL,"Attach statement didn't return DONE");
75 : 0 : status = FAILURE;
76 : : }
77 : : }
78 : :
79 : 224 : int rc = sqlite3_finalize(stmt);
80 : :
81 [ + - - + ]: 224 : if(SUCCESS == status && SQLITE_OK != rc)
82 : : {
83 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize attach statement");
84 : 0 : status = FAILURE;
85 : : }
86 : :
87 : 224 : call(m_del(sql));
88 : :
89 : 224 : provide(status);
90 : : }
91 : :
92 : : /**
93 : : * @brief Detach database by alias
94 : : *
95 : : * @param[in] db_alias Attached database alias name
96 : : * @return Return status code
97 : : */
98 : 224 : static Return db_detach(const char *db_alias)
99 : : {
100 : : /* This function was reviewed line by line by a human and is not AI-generated
101 : : Any change to this function requires separate explicit approval */
102 : :
103 : : /* Status returned by this function through provide()
104 : : Default value assumes successful completion */
105 : 224 : Return status = SUCCESS;
106 : :
107 : 224 : sqlite3_stmt *stmt = NULL;
108 : :
109 : 224 : const char *sql = "DETACH DATABASE ?1;";
110 : :
111 : 224 : int rc = sqlite3_prepare_v2(config->db,sql,-1,&stmt,NULL);
112 : :
113 [ - + ]: 224 : if(SQLITE_OK != rc)
114 : : {
115 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to prepare detach statement");
116 : 0 : status = FAILURE;
117 : : }
118 : :
119 [ + - ]: 224 : if(SUCCESS == status)
120 : : {
121 : 224 : rc = sqlite3_bind_text(stmt,1,db_alias,-1,SQLITE_STATIC);
122 : :
123 [ - + ]: 224 : if(SQLITE_OK != rc)
124 : : {
125 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to bind database alias in detach");
126 : 0 : status = FAILURE;
127 : : }
128 : : }
129 : :
130 [ + - ]: 224 : if(SUCCESS == status)
131 : : {
132 : 224 : rc = sqlite3_step(stmt);
133 : :
134 [ - + ]: 224 : if(SQLITE_DONE != rc)
135 : : {
136 : 0 : log_sqlite_error(config->db,rc,NULL,"Detach statement didn't return DONE");
137 : 0 : status = FAILURE;
138 : : }
139 : : }
140 : :
141 : 224 : rc = sqlite3_finalize(stmt);
142 : :
143 [ + - - + ]: 224 : if(SUCCESS == status && SQLITE_OK != rc)
144 : : {
145 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize detach statement");
146 : 0 : status = FAILURE;
147 : : }
148 : :
149 : 224 : provide(status);
150 : : }
151 : :
152 : : /**
153 : : * @brief Run one compare query and print only the paths that pass output filters
154 : : *
155 : : * Executes the supplied comparison query, applies the shared --include/--ignore
156 : : * decision before reporting each row, and reports only visible relative paths.
157 : : * Only visible paths contribute to category state, so compare summaries and
158 : : * equality messages are evaluated against the filtered scope. The category
159 : : * heading is emitted only before the first visible path and stays visible in
160 : : * `--silent` only when `show_headings_in_silent` enables it
161 : : *
162 : : * @param[in] compare_sql SQL query that returns relative paths for one compare category
163 : : * @param[out] differences_found Set to `true` after the first visible path is printed
164 : : * so hidden rows stay outside the reported comparison scope
165 : : * @param[in] show_headings_in_silent True to keep the category heading visible in `--silent`
166 : : * @param[in] heading_format Heading format string with two `%s` database-name slots
167 : : * @param[in] db_A_name First database name for the heading
168 : : * @param[in] db_B_name Second database name for the heading
169 : : * @return Return structure indicating the operation status
170 : : */
171 : 266 : static Return db_report_category(
172 : : const char *compare_sql,
173 : : bool *differences_found,
174 : : const bool show_headings_in_silent,
175 : : const char *heading_format,
176 : : const char *db_A_name,
177 : : const char *db_B_name)
178 : : {
179 : : /* This function was reviewed line by line by a human and is not AI-generated
180 : : Any change to this function requires separate explicit approval */
181 : :
182 : : /* Status returned by this function through provide()
183 : : Default value assumes successful completion */
184 : 266 : Return status = SUCCESS;
185 : :
186 : 266 : sqlite3_stmt *select_stmt = NULL;
187 : :
188 : 266 : int rc = sqlite3_prepare_v2(config->db,compare_sql,-1,&select_stmt,NULL);
189 : :
190 [ - + ]: 266 : if(SQLITE_OK != rc)
191 : : {
192 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
193 : 0 : status = FAILURE;
194 : : }
195 : :
196 [ + - ]: 266 : if(SUCCESS == status)
197 : : {
198 : 266 : bool first_visible_path = true;
199 : 266 : m_create(char,relative_path,MEMORY_STRING);
200 : :
201 [ + + ]: 426 : while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
202 : : {
203 : : // Interrupt the loop smoothly
204 : : // Interrupt when Ctrl+C
205 [ - + ]: 160 : if(global_interrupt_flag == true)
206 : : {
207 : 0 : break;
208 : : }
209 : :
210 : 160 : const unsigned char *db_relative_path = sqlite3_column_text(select_stmt,0);
211 : :
212 [ - + ]: 160 : if(db_relative_path == NULL)
213 : : {
214 : 0 : rc = sqlite3_errcode(config->db);
215 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to read relative path from select result");
216 : 0 : status = FAILURE;
217 : 0 : break;
218 : : }
219 : :
220 : 160 : size_t relative_path_size = (size_t)sqlite3_column_bytes(select_stmt,0) + 1U;
221 : :
222 : 160 : run(m_copy_fixed_string(relative_path,relative_path_size,db_relative_path));
223 : :
224 [ - + ]: 160 : if(SUCCESS != status)
225 : : {
226 : 0 : break;
227 : : }
228 : :
229 : 160 : bool ignore = false;
230 : :
231 : 160 : status = match_include_ignore(relative_path,
232 : : NULL,
233 : : &ignore);
234 : :
235 [ - + ]: 160 : if(SUCCESS != status)
236 : : {
237 : 0 : break;
238 : : }
239 : :
240 [ + + ]: 160 : if(ignore == true)
241 : : {
242 : 32 : continue;
243 : : }
244 : :
245 [ + + ]: 128 : if(first_visible_path == true)
246 : : {
247 : 112 : first_visible_path = false;
248 : :
249 : : // Outside --silent the heading is always shown
250 : : // In --silent it stays only when multiple compare categories can mix together in one output
251 [ + + + + ]: 112 : if((rational_logger_mode & SILENT) == 0 || show_headings_in_silent == true)
252 : : {
253 : 106 : slog(EVERY|VISIBLE_IN_SILENT,heading_format,db_A_name,db_B_name);
254 : : }
255 : : }
256 : :
257 : 128 : *differences_found = true;
258 : 128 : slog(EVERY|UNDECOR|VISIBLE_IN_SILENT,"%s\n",m_text(relative_path));
259 : : }
260 : :
261 : 266 : m_del(relative_path);
262 : :
263 [ + - + - : 266 : if(SUCCESS == status && global_interrupt_flag == false && SQLITE_DONE != rc)
- + ]
264 : : {
265 : 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
266 : 0 : status = FAILURE;
267 : : }
268 : : }
269 : :
270 : 266 : rc = sqlite3_finalize(select_stmt);
271 : :
272 [ - + ]: 266 : if(SQLITE_OK != rc)
273 : : {
274 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize SQLite statement");
275 : 0 : status = FAILURE;
276 : : }
277 : :
278 : 266 : provide(status);
279 : : }
280 : :
281 : : /**
282 : : * @brief Compare the two databases selected by `--compare`
283 : : *
284 : : * @details The two database file paths come from `config->db_file_paths`, not
285 : : * from traversal roots. The function verifies both files, attaches them to the
286 : : * primary SQLite connection, reports requested difference categories, and
287 : : * prints summary lines for missing paths and checksum mismatches
288 : : *
289 : : * The comparison scope can be limited with `--compare-filter` and further
290 : : * narrowed by `--ignore` and `--include`; without filters the function checks
291 : : * first-source paths, second-source paths, and SHA512 mismatches. For example,
292 : : * `precizer --compare first.db second.db --compare-filter=sha512` validates both DB
293 : : * files and reports checksum differences for paths present in both databases
294 : : *
295 : : * @return Return status code
296 : : */
297 : 484 : Return db_compare(void)
298 : : {
299 : : /* This function was reviewed line by line by a human and is not AI-generated
300 : : Any change to this function requires separate explicit approval */
301 : :
302 : : /* Status returned by this function through provide()
303 : : Default value assumes successful completion */
304 : 484 : Return status = SUCCESS;
305 : :
306 : 484 : bool attached_db1 = false;
307 : 484 : bool attached_db2 = false;
308 : :
309 : : /* Interrupt the function smoothly */
310 : : /* Interrupt when Ctrl+C */
311 [ - + ]: 484 : if(global_interrupt_flag == true)
312 : : {
313 : 0 : provide(status);
314 : : }
315 : :
316 : : /* Skip if comparison mode is not enabled */
317 [ + + ]: 484 : if(config->compare != true)
318 : : {
319 : 366 : slog(TRACE,"Database comparison mode is not enabled. Skipping comparison\n");
320 : 366 : provide(status);
321 : : }
322 : :
323 : 118 : slog(EVERY,"The comparison of %s and %s databases is starting…\n",config->db_file_names[0],config->db_file_names[1]);
324 : :
325 : : /* Validate database paths */
326 [ + + + + ]: 464 : m_string_array_foreach(conf(db_file_paths),db_file_path)
327 : : {
328 : 234 : const char *db_file_path_text = m_text(db_file_path);
329 : :
330 [ - + ]: 234 : if(NOT_FOUND == file_availability(db_file_path_text,NULL,SHOULD_BE_A_FILE))
331 : : {
332 : 0 : slog(ERROR,"The database file %s is either inaccessible or not a valid file\n",
333 : : db_file_path_text);
334 : 0 : status = FAILURE;
335 : 0 : break;
336 : : }
337 : :
338 [ + - ]: 234 : if(SUCCESS == status)
339 : : {
340 : : /*
341 : : * Validate the integrity of the database file
342 : : */
343 : 234 : status = db_integrity_check(db_file_path_text);
344 : :
345 [ + + ]: 234 : if(SUCCESS != status)
346 : : {
347 : 6 : break;
348 : : }
349 : : }
350 : : }
351 : :
352 : : /* Attach databases */
353 [ + + ]: 118 : if(SUCCESS == status)
354 : : {
355 : : // Attach the database 1
356 : 112 : status = db_attach(0,1);
357 : :
358 [ + - ]: 112 : if(SUCCESS == status)
359 : : {
360 : 112 : attached_db1 = true;
361 : : }
362 : : }
363 : :
364 [ + + ]: 118 : if(SUCCESS == status)
365 : : {
366 : : // Attach the database 2
367 : 112 : status = db_attach(1,2);
368 : :
369 [ + - ]: 112 : if(SUCCESS == status)
370 : : {
371 : 112 : attached_db2 = true;
372 : : }
373 : : }
374 : :
375 : : /* SQL queries for comparison */
376 : 118 : const char *compare_A_sql = "SELECT a.relative_path "
377 : : "FROM db2.files AS a "
378 : : "LEFT JOIN db1.files AS b on b.relative_path = a.relative_path "
379 : : "WHERE b.relative_path IS NULL "
380 : : "ORDER BY a.relative_path ASC;";
381 : :
382 : 118 : const char *compare_B_sql = "SELECT a.relative_path "
383 : : "FROM db1.files AS a "
384 : : "LEFT join db2.files AS b on b.relative_path = a.relative_path "
385 : : "WHERE b.relative_path IS NULL "
386 : : "ORDER BY a.relative_path ASC;";
387 : :
388 : : // True when user provided at least one --compare-filter option.
389 : : // False means default compare mode: all three categories are enabled.
390 : 118 : const bool filter_specified = config->compare_filter != CF_NONE_SPECIFIED;
391 : :
392 : : // Enables "first-source" category:
393 : : // show paths that exist in db1 but are missing in db2.
394 : : // This category is active either explicitly by filter or by default mode.
395 : 236 : const bool check_first_source = (config->compare_filter & CF_FIRST_SOURCE)
396 [ + + + + ]: 118 : || filter_specified == false;
397 : :
398 : : // Enables "second-source" category:
399 : : // show paths that exist in db2 but are missing in db1.
400 : : // This category is active either explicitly by filter or by default mode.
401 : 236 : const bool check_second_source = (config->compare_filter & CF_SECOND_SOURCE)
402 [ + + + + ]: 118 : || filter_specified == false;
403 : :
404 : : // Enables checksum verification category for common relative paths.
405 : : // Active either explicitly by checksum filter or by default mode.
406 : 236 : const bool verify_checksum_consistency = (config->compare_filter & CF_CHECKSUM_MISMATCH)
407 [ + + + + ]: 118 : || filter_specified == false;
408 : :
409 : : /*
410 : : * Count the enabled comparison categories before producing output.
411 : : * In silent mode, a heading is unnecessary when only one category can
412 : : * produce paths because every printed path belongs to that category.
413 : : * When several categories are enabled, keep headings visible so paths from
414 : : * different categories remain distinguishable
415 : : */
416 : 118 : unsigned int active_compare_categories = 0u;
417 : :
418 [ + + ]: 118 : if(check_first_source == true)
419 : : {
420 : 96 : active_compare_categories++;
421 : : }
422 : :
423 [ + + ]: 118 : if(check_second_source == true)
424 : : {
425 : 96 : active_compare_categories++;
426 : : }
427 : :
428 [ + + ]: 118 : if(verify_checksum_consistency == true)
429 : : {
430 : 92 : active_compare_categories++;
431 : : }
432 : :
433 : : // Keeps category headings visible only when silent output can mix multiple categories
434 : 118 : const bool show_headings_in_silent = active_compare_categories > 1u;
435 : :
436 : : // Comparison result flags grouped in one place for summary evaluation
437 : 118 : bool first_source_differences_found = false;
438 : 118 : bool second_source_differences_found = false;
439 : 118 : bool checksum_mismatches_found = false;
440 : :
441 : : /* Compare files existence between databases */
442 [ + + ]: 118 : if(check_first_source == true)
443 : : {
444 : 96 : run(db_report_category(compare_B_sql,
445 : : &first_source_differences_found,
446 : : show_headings_in_silent,
447 : : BOLD "These files are no longer in the %s but still exist in the %s" RESET "\n",
448 : : config->db_file_names[1],
449 : : config->db_file_names[0]));
450 : : }
451 : :
452 [ + + ]: 118 : if(check_second_source == true)
453 : : {
454 : 96 : run(db_report_category(compare_A_sql,
455 : : &second_source_differences_found,
456 : : show_headings_in_silent,
457 : : BOLD "These files are no longer in the %s but still exist in the %s" RESET "\n",
458 : : config->db_file_names[0],
459 : : config->db_file_names[1]));
460 : : }
461 : :
462 : : #if 0
463 : : // Disabled multi-root path index implementation
464 : : const char *compare_checksums_sql = "select a.relative_path from db2.files a inner join db1.files b"
465 : : " on b.relative_path = a.relative_path "
466 : : " and b.sha512 is not a.sha512"
467 : : " order by a.relative_path asc;";
468 : :
469 : : const char *compare_checksums_sql = "SELECT p.path,f1.relative_path "
470 : : "FROM db1.files AS f1 "
471 : : "JOIN db1.paths AS p ON f1.path_prefix_index = p.ID "
472 : : "JOIN db2.files AS f2 ON f1.relative_path = f2.relative_path "
473 : : "JOIN db2.paths AS p2 ON f2.path_prefix_index = p2.ID "
474 : : "WHERE f1.sha512 IS NOT f2.sha512 AND p.path = p2.path "
475 : : "ORDER BY p.path,f1.relative_path ASC;";
476 : : #else
477 : : // One PATH solution
478 : 118 : const char *compare_checksums_sql = "SELECT a.relative_path "
479 : : "FROM db2.files AS a "
480 : : "INNER JOIN db1.files AS b ON b.relative_path = a.relative_path "
481 : : "WHERE b.sha512 IS NOT a.sha512 "
482 : : "ORDER BY a.relative_path ASC;";
483 : : #endif
484 : :
485 [ + + ]: 118 : if(verify_checksum_consistency == true)
486 : : {
487 : 92 : run(db_report_category(compare_checksums_sql,
488 : : &checksum_mismatches_found,
489 : : show_headings_in_silent,
490 : : BOLD "The SHA512 checksums of these files do not match between %s and %s" RESET "\n",
491 : : config->db_file_names[0],
492 : : config->db_file_names[1]));
493 : : }
494 : :
495 : : /* Cleanup */
496 [ + + ]: 118 : if(attached_db1 == true)
497 : : {
498 : 112 : call(db_detach("db1"));
499 : : }
500 : :
501 [ + + ]: 118 : if(attached_db2 == true)
502 : : {
503 : 112 : call(db_detach("db2"));
504 : : }
505 : :
506 : : /* Output results */
507 [ + + ]: 118 : if(SUCCESS == status)
508 : : {
509 : 112 : const bool full_compare_scope = check_first_source == true
510 [ + + ]: 90 : && check_second_source == true
511 [ + + + + ]: 202 : && verify_checksum_consistency == true;
512 : :
513 [ + + ]: 112 : if(full_compare_scope == true
514 [ + + ]: 70 : && first_source_differences_found == false
515 [ + + ]: 50 : && second_source_differences_found == false
516 [ + + ]: 36 : && checksum_mismatches_found == false)
517 : : {
518 : 28 : slog(EVERY,BOLD "All files are identical against %s and %s" RESET "\n",
519 : : config->db_file_names[0],
520 : : config->db_file_names[1]);
521 : :
522 [ + + ]: 84 : } else if(full_compare_scope == false){
523 : :
524 [ + + ]: 42 : if(check_first_source == true
525 [ + + ]: 20 : && first_source_differences_found == false)
526 : : {
527 : 8 : slog(EVERY,BOLD "No first-source differences found between %s and %s" RESET "\n",
528 : : config->db_file_names[0],
529 : : config->db_file_names[1]);
530 : : }
531 : :
532 [ + + ]: 42 : if(check_second_source == true
533 [ + + ]: 20 : && second_source_differences_found == false)
534 : : {
535 : 8 : slog(EVERY,BOLD "No second-source differences found between %s and %s" RESET "\n",
536 : : config->db_file_names[0],
537 : : config->db_file_names[1]);
538 : : }
539 : : }
540 : :
541 [ + + + + ]: 112 : if(verify_checksum_consistency == true && checksum_mismatches_found == false)
542 : : {
543 : 46 : slog(EVERY,BOLD "All SHA512 checksums of files are identical against %s and %s" RESET "\n",
544 : : config->db_file_names[0],
545 : : config->db_file_names[1]);
546 : : }
547 : :
548 [ + + ]: 112 : if(full_compare_scope == true
549 [ + + ]: 70 : && first_source_differences_found == false
550 [ + + ]: 50 : && second_source_differences_found == false
551 [ + + ]: 36 : && checksum_mismatches_found == false)
552 : : {
553 : 28 : slog(EVERY,BOLD "The databases %s and %s are absolutely equal" RESET "\n",
554 : : config->db_file_names[0],
555 : : config->db_file_names[1]);
556 : : }
557 : : }
558 : :
559 : 118 : slog(EVERY,"Comparison of %s and %s databases is complete\n",
560 : : config->db_file_names[0],
561 : : config->db_file_names[1]);
562 : :
563 : 118 : provide(status);
564 : : }
|