LCOV - code coverage report
Current view: top level - src - db_save_prefixes.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 67.1 % 76 51
Test Date: 2026-07-12 01:01:34 Functions: 100.0 % 2 2
Branches: 59.1 % 66 39

             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                 :             : }
        

Generated by: LCOV version 2.0-1