LCOV - code coverage report
Current view: top level - src - db_compare.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 78.5 % 149 117
Test Date: 2026-01-12 05:34:38 Functions: 100.0 % 5 5

            Line data    Source code
       1              : #include "precizer.h"
       2              : 
       3              : /**
       4              :  * @brief Composes an SQL ATTACH DATABASE query string.
       5              :  *
       6              :  * This function generates an SQL query string to attach a database with a specified path and number.
       7              :  *
       8              :  * @param[out] sql Pointer to a string that will hold the generated SQL query.
       9              :  * @param[in] db_path Path to the database file to be attached.
      10              :  * @param[in] db_num Database number (1 or 2) used in the query.
      11              :  * @return Return structure indicating the operation status.
      12              :  */
      13           64 : static Return compose_sql(
      14              :         char       **sql,
      15              :         const char *db_path,
      16              :         int        db_num)
      17              : {
      18              :         /// The status that will be passed to return() before exiting.
      19              :         /// By default, the function worked without errors.
      20           64 :         Return status = SUCCESS;
      21              : 
      22           64 :         if(asprintf(sql,"ATTACH DATABASE '%s' as db%d;",db_path,db_num) == -1)
      23              :         {
      24            0 :                 status = FAILURE;
      25            0 :                 report("Memory allocation failed for SQL query string");
      26              :         }
      27              : 
      28           64 :         provide(status);
      29              : }
      30              : 
      31              : /**
      32              :  * @brief Attaches a secondary database to the primary database connection.
      33              :  *
      34              :  * This function attaches a secondary database (specified by its index in the configuration)
      35              :  * to the primary SQLite database connection using the ATTACH DATABASE command.
      36              :  *
      37              :  * @param[in] db_A Index of the database path in the configuration array.
      38              :  * @param[in] db_B Database number (1 or 2) to be used in the ATTACH DATABASE command.
      39              :  * @return Return structure indicating the operation status.
      40              :  */
      41           64 : static Return db_attach(
      42              :         int db_A,
      43              :         int db_B)
      44              : {
      45              :         /// The status that will be passed to return() before exiting.
      46              :         /// By default, the function worked without errors.
      47           64 :         Return status = SUCCESS;
      48              : 
      49           64 :         char *select_sql = NULL;
      50              : 
      51           64 :         run(compose_sql(&select_sql,config->db_file_paths[db_A],db_B));
      52              : 
      53           64 :         if(SUCCESS == status)
      54              :         {
      55           64 :                 int rc = sqlite3_exec(config->db,select_sql,NULL,NULL,NULL);
      56              : 
      57           64 :                 if(rc!= SQLITE_OK)
      58              :                 {
      59            0 :                         log_sqlite_error(config->db,rc,NULL,"Can't execute");
      60            0 :                         status = FAILURE;
      61              :                 }
      62              :         }
      63              : 
      64           64 :         free(select_sql);
      65              : 
      66           64 :         provide(status);
      67              : }
      68              : 
      69              : /**
      70              :  * @brief Detach database by alias
      71              :  *
      72              :  * @param[in] db_alias Attached database alias name
      73              :  * @return Return status code
      74              :  */
      75           64 : static Return db_detach(const char *db_alias)
      76              : {
      77              :         /// The status that will be passed to return() before exiting.
      78              :         /// By default, the function worked without errors.
      79           64 :         Return status = SUCCESS;
      80              : 
      81           64 :         sqlite3_stmt *stmt = NULL;
      82              : 
      83           64 :         const char *sql = "DETACH DATABASE ?1;";
      84              : 
      85           64 :         int rc = sqlite3_prepare_v2(config->db,sql,-1,&stmt,NULL);
      86              : 
      87           64 :         if(SQLITE_OK != rc)
      88              :         {
      89            0 :                 log_sqlite_error(config->db,rc,NULL,"Failed to prepare detach statement");
      90            0 :                 status = FAILURE;
      91              :         }
      92              : 
      93           64 :         if(SUCCESS == status)
      94              :         {
      95           64 :                 rc = sqlite3_bind_text(stmt,1,db_alias,-1,SQLITE_STATIC);
      96              : 
      97           64 :                 if(SQLITE_OK != rc)
      98              :                 {
      99            0 :                         log_sqlite_error(config->db,rc,NULL,"Failed to bind database alias in detach");
     100            0 :                         status = FAILURE;
     101              :                 }
     102              :         }
     103              : 
     104           64 :         if(SUCCESS == status)
     105              :         {
     106           64 :                 rc = sqlite3_step(stmt);
     107              : 
     108           64 :                 if(SQLITE_DONE != rc)
     109              :                 {
     110            0 :                         log_sqlite_error(config->db,rc,NULL,"Detach statement didn't return DONE");
     111            0 :                         status = FAILURE;
     112              :                 }
     113              :         }
     114              : 
     115           64 :         if(stmt != NULL)
     116              :         {
     117           64 :                 sqlite3_finalize(stmt);
     118              :         }
     119              : 
     120           64 :         provide(status);
     121              : }
     122              : 
     123              : /**
     124              :  * @brief Compares changes between two databases.
     125              :  *
     126              :  * This function executes a provided SQL query to compare differences between two databases.
     127              :  * It identifies files that exist in one database but not the other, updating flags to reflect the comparison results.
     128              :  *
     129              :  * @param[in] compare_sql SQL query string for comparison.
     130              :  * @param[out] files_the_same Flag indicating whether files are identical between the databases.
     131              :  * @param[out] the_databases_are_equal Flag indicating whether the databases are equal.
     132              :  * @param[in] db_A Index of the first database in the configuration array.
     133              :  * @param[in] db_B Index of the second database in the configuration array.
     134              :  * @return Return structure indicating the operation status.
     135              :  */
     136           64 : static Return db_changes(
     137              :         const char *compare_sql,
     138              :         bool       *files_the_same,
     139              :         bool       *the_databases_are_equal,
     140              :         int        db_A,
     141              :         int        db_B)
     142              : {
     143              :         /// The status that will be passed to return() before exiting.
     144              :         /// By default, the function worked without errors.
     145           64 :         Return status = SUCCESS;
     146              : 
     147           64 :         bool first_iteration = true;
     148              : 
     149           64 :         sqlite3_stmt *select_stmt = NULL;
     150              : 
     151           64 :         int rc = sqlite3_prepare_v2(config->db,compare_sql,-1,&select_stmt,NULL);
     152              : 
     153           64 :         if(SQLITE_OK != rc)
     154              :         {
     155            0 :                 log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
     156            0 :                 status = FAILURE;
     157              :         }
     158              : 
     159           88 :         while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
     160              :         {
     161           24 :                 *the_databases_are_equal = false;
     162           24 :                 *files_the_same = false;
     163              : 
     164              :                 // Interrupt the loop smoothly
     165              :                 // Interrupt when Ctrl+C
     166           24 :                 if(global_interrupt_flag == true)
     167              :                 {
     168            0 :                         break;
     169              :                 }
     170              : 
     171           24 :                 if(first_iteration == true)
     172              :                 {
     173           24 :                         first_iteration = false;
     174           24 :                         slog(EVERY,BOLD "These files are no longer in the %s but still exist in the %s" RESET "\n",config->db_file_names[db_A],config->db_file_names[db_B]);
     175              :                 }
     176              : 
     177           24 :                 const unsigned char *relative_path = NULL;
     178           24 :                 relative_path = sqlite3_column_text(select_stmt,0);
     179              : 
     180           24 :                 if(relative_path != NULL)
     181              :                 {
     182           24 :                         slog(EVERY|UNDECOR,"%s\n",relative_path);
     183              :                 } else {
     184            0 :                         slog(ERROR,"General database error!\n");
     185            0 :                         status = FAILURE;
     186            0 :                         break;
     187              :                 }
     188              :         }
     189              : 
     190           64 :         if(SQLITE_DONE != rc)
     191              :         {
     192            0 :                 log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
     193            0 :                 status = FAILURE;
     194              :         }
     195              : 
     196           64 :         if(select_stmt != NULL)
     197              :         {
     198           64 :                 rc = sqlite3_finalize(select_stmt);
     199              : 
     200           64 :                 if(SQLITE_OK != rc)
     201              :                 {
     202            0 :                         log_sqlite_error(config->db,rc,NULL,"Failed to finalize SQLite statement");
     203            0 :                         status = FAILURE;
     204              :                 } else {
     205           64 :                         select_stmt = NULL;
     206              :                 }
     207              :         }
     208              : 
     209           64 :         provide(status);
     210              : }
     211              : 
     212              : /**
     213              :  * @brief Compare two databases
     214              :  * @details Compares content of two databases specified in Config structure
     215              :  *          Checks for file existence, missing files and SHA512 checksums
     216              :  * @return Return enum indicating operation status
     217              :  */
     218          192 : Return db_compare(void)
     219              : {
     220              :         /// The status that will be passed to return() before exiting.
     221              :         /// By default, the function worked without errors.
     222          192 :         Return status = SUCCESS;
     223              : 
     224          192 :         bool attached_db1 = false;
     225          192 :         bool attached_db2 = false;
     226              : 
     227              :         /* Interrupt the function smoothly */
     228              :         /* Interrupt when Ctrl+C */
     229          192 :         if(global_interrupt_flag == true)
     230              :         {
     231            0 :                 provide(status);
     232              :         }
     233              : 
     234              :         /* Skip if comparison mode is not enabled */
     235          192 :         if(config->compare != true)
     236              :         {
     237          156 :                 slog(TRACE,"Database comparison mode is not enabled. Skipping comparison\n");
     238          156 :                 provide(status);
     239              :         }
     240              : 
     241           36 :         slog(EVERY,"The comparison of %s and %s databases is starting…\n",
     242              :                 config->db_file_names[0],
     243              :                 config->db_file_names[1]);
     244              : 
     245              :         /* Validate database paths */
     246          102 :         for(int i = 0; config->db_file_paths[i]; i++)
     247              :         {
     248           70 :                 if(NOT_FOUND == file_availability(config->db_file_paths[i],SHOULD_BE_A_FILE))
     249              :                 {
     250            0 :                         slog(ERROR,"The database file %s is either inaccessible or not a valid file\n",
     251              :                                 config->db_file_paths[i]);
     252            0 :                         status = FAILURE;
     253            0 :                         break;
     254              :                 }
     255              : 
     256           70 :                 if(SUCCESS == status)
     257              :                 {
     258              :                         /*
     259              :                          * Validate the integrity of the database file
     260              :                          */
     261           70 :                         status = db_test(config->db_file_paths[i]);
     262              : 
     263           70 :                         if(SUCCESS != status)
     264              :                         {
     265            4 :                                 break;
     266              :                         }
     267              :                 }
     268              :         }
     269              : 
     270              :         /* Attach databases */
     271           36 :         if(SUCCESS == status)
     272              :         {
     273              :                 // Attach the database 1
     274           32 :                 status = db_attach(0,1);
     275              : 
     276           32 :                 if(SUCCESS == status)
     277              :                 {
     278           32 :                         attached_db1 = true;
     279              :                 }
     280              :         }
     281              : 
     282           36 :         if(SUCCESS == status)
     283              :         {
     284              :                 // Attach the database 2
     285           32 :                 status = db_attach(1,2);
     286              : 
     287           32 :                 if(SUCCESS == status)
     288              :                 {
     289           32 :                         attached_db2 = true;
     290              :                 }
     291              :         }
     292              : 
     293              :         /* SQL queries for comparison */
     294           36 :         const char *compare_A_sql = "SELECT a.relative_path "
     295              :                 "FROM db2.files AS a "
     296              :                 "LEFT JOIN db1.files AS b on b.relative_path = a.relative_path "
     297              :                 "WHERE b.relative_path IS NULL "
     298              :                 "ORDER BY a.relative_path ASC;";
     299              : 
     300           36 :         const char *compare_B_sql = "SELECT a.relative_path "
     301              :                 "FROM db1.files AS a "
     302              :                 "LEFT join db2.files AS b on b.relative_path = a.relative_path "
     303              :                 "WHERE b.relative_path IS NULL "
     304              :                 "ORDER BY a.relative_path ASC;";
     305              : 
     306           36 :         bool files_the_same = true;
     307           36 :         bool the_databases_are_equal = true;
     308              : 
     309              :         /* Compare files existence between databases */
     310           36 :         run(db_changes(compare_A_sql,
     311              :                 &files_the_same,
     312              :                 &the_databases_are_equal,
     313              :                 0,
     314              :                 1));
     315              : 
     316           36 :         run(db_changes(compare_B_sql,
     317              :                 &files_the_same,
     318              :                 &the_databases_are_equal,
     319              :                 1,
     320              :                 0));
     321              : 
     322              : #if 0
     323              :         // Old multiPATH solutions
     324              :         const char *compare_checksums = "select a.relative_path from db2.files a inner join db1.files b"
     325              :                 " on b.relative_path = a.relative_path "
     326              :                 " and b.sha512 != a.sha512"
     327              :                 " order by a.relative_path asc;";
     328              : 
     329              :         const char *compare_checksums = "SELECT p.path,f1.relative_path "
     330              :                 "FROM db1.files AS f1 "
     331              :                 "JOIN db1.paths AS p ON f1.path_prefix_index = p.ID "
     332              :                 "JOIN db2.files AS f2 ON f1.relative_path = f2.relative_path "
     333              :                 "JOIN db2.paths AS p2 ON f2.path_prefix_index = p2.ID "
     334              :                 "WHERE f1.sha512 <> f2.sha512 AND p.path = p2.path "
     335              :                 "ORDER BY p.path,f1.relative_path ASC;";
     336              : #else
     337              :         // One PATH solution
     338           36 :         const char *compare_checksums = "SELECT a.relative_path "
     339              :                 "FROM db2.files AS a "
     340              :                 "INNER JOIN db1.files b on b.relative_path = a.relative_path and b.sha512 != a.sha512 "
     341              :                 "ORDER BY a.relative_path ASC;";
     342              : #endif
     343              : 
     344              :         /* Compare SHA512 checksums */
     345           36 :         sqlite3_stmt *select_stmt = NULL;
     346           36 :         bool first_iteration = true;
     347           36 :         bool checksums = true;
     348              : 
     349           36 :         if(SUCCESS == status)
     350              :         {
     351           32 :                 int rc = sqlite3_prepare_v2(config->db,
     352              :                         compare_checksums,
     353              :                         -1,
     354              :                         &select_stmt,
     355              :                         NULL);
     356              : 
     357           32 :                 if(SQLITE_OK != rc)
     358              :                 {
     359            0 :                         log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
     360            0 :                         status = FAILURE;
     361              :                 }
     362              :         }
     363              : 
     364           36 :         if(SUCCESS == status)
     365              :         {
     366              :                 int rc;
     367              : 
     368           64 :                 while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
     369              :                 {
     370           32 :                         the_databases_are_equal = false;
     371           32 :                         checksums = false;
     372              : 
     373              :                         // Interrupt the loop smoothly
     374              :                         // Interrupt when Ctrl+C
     375           32 :                         if(global_interrupt_flag == true)
     376              :                         {
     377            0 :                                 break;
     378              :                         }
     379              : 
     380           32 :                         if(first_iteration == true)
     381              :                         {
     382           16 :                                 first_iteration = false;
     383           16 :                                 slog(EVERY,BOLD "The SHA512 checksums of these files do not match between %s and %s" RESET "\n",
     384              :                                         config->db_file_names[0],
     385              :                                         config->db_file_names[1]);
     386              :                         }
     387              : 
     388              : #if 0
     389              :                         const unsigned char *relative_path = NULL;
     390              :                         const unsigned char *path_prefix = NULL;
     391              :                         path_prefix = sqlite3_column_text(select_stmt,0);
     392              :                         relative_path = sqlite3_column_text(select_stmt,1);
     393              : #endif
     394              : 
     395           32 :                         const unsigned char *relative_path = sqlite3_column_text(select_stmt,0);
     396              : 
     397           32 :                         if(relative_path != NULL)
     398              :                         {
     399           32 :                                 slog(EVERY|UNDECOR,"%s\n",relative_path);
     400              :                         } else {
     401            0 :                                 slog(ERROR,"General database error!\n");
     402            0 :                                 status = FAILURE;
     403            0 :                                 break;
     404              :                         }
     405              :                 }
     406              : 
     407           32 :                 if(SQLITE_DONE != rc)
     408              :                 {
     409            0 :                         log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
     410            0 :                         status = FAILURE;
     411              :                 }
     412              :         }
     413              : 
     414              :         /* Cleanup */
     415           36 :         if(attached_db2 == true)
     416              :         {
     417           32 :                 call(db_finalize(config->db,"db2",&select_stmt));
     418              :         }
     419              : 
     420           36 :         if(attached_db1 == true)
     421              :         {
     422           32 :                 sqlite3_stmt *no_stmt = NULL;
     423           32 :                 call(db_finalize(config->db,"db1",&no_stmt));
     424              :         }
     425              : 
     426              :         /* Detach databases in attach order */
     427           36 :         if(attached_db1 == true)
     428              :         {
     429           32 :                 call(db_detach("db1"));
     430              :         }
     431              : 
     432           36 :         if(attached_db2 == true)
     433              :         {
     434           32 :                 call(db_detach("db2"));
     435              :         }
     436              : 
     437              :         /* Output results */
     438           36 :         if(SUCCESS == status)
     439              :         {
     440           32 :                 if(files_the_same == true && checksums == true)
     441              :                 {
     442           12 :                         slog(EVERY,BOLD "All files are identical against %s and %s" RESET "\n",
     443              :                                 config->db_file_names[0],
     444              :                                 config->db_file_names[1]);
     445              :                 }
     446              : 
     447           32 :                 if(checksums == true)
     448              :                 {
     449           16 :                         slog(EVERY,BOLD "All SHA512 checksums of files are identical against %s and %s" RESET "\n",
     450              :                                 config->db_file_names[0],
     451              :                                 config->db_file_names[1]);
     452              :                 }
     453              : 
     454           32 :                 if(the_databases_are_equal == true)
     455              :                 {
     456           12 :                         slog(EVERY,BOLD "The databases %s and %s are absolutely equal" RESET "\n",
     457              :                                 config->db_file_names[0],
     458              :                                 config->db_file_names[1]);
     459              :                 }
     460              :         }
     461              : 
     462           36 :         slog(EVERY,"Comparison of %s and %s databases is complete\n",
     463              :                 config->db_file_names[0],
     464              :                 config->db_file_names[1]);
     465              : 
     466           36 :         provide(status);
     467              : }
        

Generated by: LCOV version 2.0-1