diff --git a/Makefile b/Makefile index 6876067..ac4c089 100644 --- a/Makefile +++ b/Makefile @@ -66,12 +66,16 @@ $(TEST_PROJECT): \ tests/test_investigation_project.c \ src/core/investigation_project.c \ src/database/database.c \ - src/database/schema.c + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(TEST_DATABASE): \ tests/test_database.c \ src/database/database.c \ + src/database/transaction.c \ + src/database/statement.c \ src/database/schema.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 @@ -79,6 +83,7 @@ $(TEST_STATEMENT): \ tests/test_statement.c \ src/database/database.c \ src/database/schema.c \ + src/database/transaction.c \ src/database/statement.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 @@ -87,7 +92,8 @@ $(TEST_TRANSACTION): \ src/database/database.c \ src/database/schema.c \ src/database/statement.c \ - src/database/transaction.c + src/database/transaction.c \ + src/database/error.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(TEST_ERROR): \ @@ -95,6 +101,7 @@ $(TEST_ERROR): \ src/database/database.c \ src/database/schema.c \ src/database/statement.c \ + src/database/transaction.c \ src/database/error.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 diff --git a/docs/tickets/open/TICKET-026.md b/docs/tickets/open/TICKET-026.md new file mode 100644 index 0000000..4d56a63 --- /dev/null +++ b/docs/tickets/open/TICKET-026.md @@ -0,0 +1,366 @@ +# Ticket #026 — Migrer l’initialisation vers la couche Database + +## Contexte + +Le ticket #025 a introduit l’infrastructure principale de la couche Database : + +- contexte opaque `Database` ; +- ouverture et fermeture des connexions ; +- requêtes préparées avec `DatabaseStatement` ; +- bindings et lecture typée ; +- gestion des transactions ; +- infrastructure d’erreurs. + +Cependant, `database_initialize()` utilise encore directement plusieurs fonctions de l’API SQLite : + +```c +sqlite3_open_v2() +sqlite3_prepare_v2() +sqlite3_bind_text() +sqlite3_step() +sqlite3_reset() +sqlite3_clear_bindings() +sqlite3_finalize() +sqlite3_close() + +La fonction exécute également directement les commandes SQL suivantes : + +```sql + +BEGIN IMMEDIATE; +COMMIT; +ROLLBACK; +``` + +De son côté, `schema_install_v1()` reçoit toujours directement un `sqlite3 *`, ce qui contourne l’abstraction `Database`. + +## Objectif + +Réécrire l’initialisation d’une nouvelle enquête afin qu’elle utilise l’infrastructure créée au ticket #025. + +L’initialisation doit rester atomique : + +- soit la base est entièrement initialisée ; +- soit aucune donnée partielle n’est conservée. + +## Travail à réaliser + +### Adapter l’installation du schéma + +Modifier la signature actuelle : + +```C +bool schema_install_v1( + sqlite3 *database +); +``` + +afin qu’elle reçoive un contexte Database : + +```C +bool schema_install_v1( + Database *database +); +``` + +L’installation du fichier SQL complet pourra continuer à utiliser `sqlite3_exec()` en interne, car le schéma contient plusieurs instructions SQL. + +L’accès au handle SQLite devra passer par l’API interne : + +```C +database_get_handle() +``` + +La fonction ne devra réaliser ni `COMMIT` ni `ROLLBACK`. + +### Migrer l’insertion des métadonnées + +Réécrire les fonctions responsables de l’insertion dans la table `metadata` avec `DatabaseStatement`. + +Les opérations devront utiliser l’API suivante : + +```C +database_statement_prepare() +database_statement_bind_text() +database_statement_step() +database_statement_reset() +database_statement_clear_bindings() +database_statement_finalize() +``` + +Les métadonnées obligatoires restent : + +``` +schema_version +application +created_at +investigation_uuid +``` + +Une seule requête préparée devra pouvoir être réutilisée pour insérer les quatre métadonnées. + +### Migrer l’insertion de l’enquête + +Réécrire l’insertion dans la table `investigation` avec `DatabaseStatement`. + +Les champs insérés restent : + +```C +database_open() +database_transaction_begin() +schema_install_v1() +database_transaction_commit() +database_transaction_rollback() +database_close() +``` + +Le déroulement attendu est : + +``` +validation des paramètres + ↓ +création du timestamp UTC + ↓ +création de l’UUID + ↓ +ouverture de Database + ↓ +début de transaction + ↓ +installation du schéma V1 + ↓ +insertion des métadonnées + ↓ +insertion de l’enquête + ↓ +commit + ↓ +fermeture de Database +``` + +Tout échec après le début de la transaction doit provoquer un rollback. + +### Utiliser l’infrastructure d’erreurs + +Les erreurs rencontrées pendant : + +- l’installation du schéma ; +- la préparation d’une requête ; +- le binding d’un paramètre ; +- l’exécution d’une requête ; +- le début d’une transaction ; +- le commit ; +- le rollback ; + +doivent être enregistrées dans le contexte Database lorsque celui-ci est disponible. + +L’infrastructure interne existante pourra être utilisée : + +```C +database_set_error() +database_clear_error_internal() +``` + +```C +DATABASE_ERROR_SQLITE +``` + +```C +DATABASE_ERROR_INVALID_ARGUMENT +``` + +```C +DATABASE_ERROR_INVALID_STATE +``` + +### Nettoyer l’ancien code SQLite + +Supprimer de la logique d’initialisation les appels directs à : + +```C +sqlite3_open_v2() +sqlite3_prepare_v2() +sqlite3_bind_text() +sqlite3_step() +sqlite3_reset() +sqlite3_clear_bindings() +sqlite3_finalize() +sqlite3_close() +``` + +Supprimer également les commandes manuelles : + +```SQL +BEGIN IMMEDIATE; +COMMIT; +ROLLBACK; +``` + +La fonction utilitaire `database_execute_sql()` pourra être conservée uniquement si elle reste nécessaire à `database_open()` pour l’activation des clés étrangères. + +### Mettre à jour la documentation publique + +Dans `include/database/database.h`, supprimer la mention temporaire : + +``` +Cette fonction conserve temporairement son rôle actuel pendant le +refactoring du ticket #025. +``` + +La documentation de `database_initialize()` devra préciser que : + +- l’initialisation est transactionnelle ; +- un échec provoque un rollback ; +- la base est fermée avant le retour de la fonction. + +## Tests à ajouter ou adapter + +### Initialisation réussie + +Vérifier qu’une initialisation valide crée : + +- le schéma V1 ; +- les tables attendues ; +- les métadonnées obligatoires ; +- une seule ligne dans investigation ; +- le bon nom d’enquête ; +- le bon chemin racine ; +- un UUID non vide ; +- une date de création non vide ; +- une date de modification non vide. + +### Paramètres invalides + +Vérifier le refus des cas suivants : + +``` +database_path == NULL +database_path vide +investigation_name == NULL +investigation_name vide +investigation_root_path == NULL +investigation_root_path vide +``` + +### Rollback + +Provoquer un échec après le démarrage de la transaction. + +Une seconde tentative d’initialisation sur une base déjà initialisée pourra être utilisée pour provoquer une erreur de contrainte ou de création de schéma. + +Après l’échec, vérifier que : + +- aucune donnée partielle supplémentaire n’est conservée ; +- aucune seconde enquête n’est ajoutée ; +- les métadonnées existantes ne sont pas dupliquées ; +- la transaction est annulée ; +- la base reste lisible ; +- la connexion peut être fermée proprement. + +### Régression + +Tous les tests existants doivent rester valides : + +``` +InvestigationNode +InvestigationTreeModel +InvestigationTreeBuilder +InvestigationProject +Database +DatabaseStatement +DatabaseTransaction +DatabaseError +``` + +## Critères d’acceptation + +- [ ] `database_initialize()` n’appelle plus directement `sqlite3_open_v2()`. +- [ ] `database_initialize()` utilise `database_open()`. +- [ ] `database_initialize()` utilise `database_close()`. +- [ ] Les transactions utilisent le module `transaction`. +- [ ] Les insertions utilisent `DatabaseStatement`. +- [ ] `InvestigationNode` Investigation `schema_install_v1()` reçoit un `Database *`. +InvestigationNode +Investigation `schema_install_v1()` reçoit un `Database *`. +- [ ] `schema_install_v1()` récupère le handle avec l’API interne. +- [ ] Aucun `sqlite3_stmt *` n’est manipulé dans le code d’initialisation. +- [ ] Aucun `BEGIN`, `COMMIT` ou `ROLLBACK` manuel ne reste dans `database.c`. +- [ ] Tout échec après le début de la transaction provoque un rollback. +- [ ] Les erreurs sont enregistrées dans la couche Database. +- [ ] Les tests de réussite sont valides. +- [ ] Les tests de paramètres invalides sont valides. +- [ ] Les tests de rollback sont valides. +- [ ] Les tests de régression sont valides. +- [ ] `make` réussit sans erreur. +- [ ] `make test` réussit. +- [ ] `git diff --check` ne retourne aucune erreur. + +## Hors périmètre + +Ce ticket ne doit pas ajouter : + +- de CRUD métier ; +- de dépôt ou repository métier ; +- de migration entre plusieurs versions de schéma ; +- de système GResource pour embarquer `schema_v1.sql` ; +- de création automatique des dossiers parents ; +- de modification du schéma SQL V1 ; +- de nouvelles tables ; +- de logique d’interface graphique ; +- de nouvelles fonctionnalités utilisateur. + +## Fichiers principalement concernés + +```text +include/database/database.h +include/database/schema.h + +src/database/database.c +src/database/database_internal.h +src/database/schema.c + +tests/test_database.c + +Makefile +``` + +## Résultat attendu + +À la fin du ticket, `database_initialize()` doit être un utilisateur normal de la couche Database. + +La fonction ne doit plus contourner cette couche avec des appels SQLite directs. + +L’initialisation d’une enquête doit être entièrement transactionnelle, testée et cohérente avec l’architecture mise en place au ticket #025. + +## Commit attendu + +Une fois tous les critères d’acceptation validés, le ticket pourra être enregistré avec le commit suivant : + +```text +refactor(database): migrate initialization to database layer +``` + +Le commit doit principalement contenir : + +``` +include/database/database.h +include/database/schema.h + +src/database/database.c +src/database/database_internal.h +src/database/schema.c + +tests/test_database.c + +Makefile +``` +Avant le commit, exécuter : + +```bash +make clean +make +make test +git diff --check +git status --short +``` + diff --git a/include/database/database.h b/include/database/database.h index 585b079..d80e87c 100644 --- a/include/database/database.h +++ b/include/database/database.h @@ -45,8 +45,21 @@ void database_close( /** * @brief Initialise la base SQLite d'une nouvelle enquête. * - * Cette fonction conserve temporairement son rôle actuel pendant le - * refactoring du ticket #025. + * Cette fonction : + * + * - valide les paramètres ; + * - ouvre la connexion Database ; + * - démarre une transaction ; + * - installe le schéma SQLite V1 ; + * - insère les métadonnées obligatoires ; + * - insère l'enquête courante ; + * - valide la transaction ; + * - ferme la connexion avant de retourner. + * + * Tout échec survenant après le début de la transaction provoque + * l'annulation des modifications. + * + * La fonction ne crée pas les dossiers parents du fichier SQLite. * * @param database_path Chemin complet du fichier Enquete.sqlite. * @param investigation_name Nom de l'enquête. diff --git a/include/database/schema.h b/include/database/schema.h index e3f5295..276765d 100644 --- a/include/database/schema.h +++ b/include/database/schema.h @@ -6,9 +6,9 @@ #ifndef LABFY_INVESTIGATION_SCHEMA_H #define LABFY_INVESTIGATION_SCHEMA_H -#include +#include "database/database.h" -#include +#include /** * @brief Installe le schéma SQLite V1 dans une base ouverte. @@ -23,12 +23,12 @@ * * Elle ne réalise ni COMMIT ni ROLLBACK. * - * @param database Connexion SQLite ouverte. + * @param database Connexion Database ouverte. * * @return true si le schéma a été correctement installé, sinon false. */ bool schema_install_v1( - sqlite3 *database + Database *database ); #endif diff --git a/labfy-investigation b/labfy-investigation index 56a51c4..21dd012 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/database/database.c b/src/database/database.c index f03cd6a..c0179e7 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -5,6 +5,8 @@ #include "database/database.h" #include "database/schema.h" +#include "database/transaction.h" +#include "database/statement.h" #include "database_internal.h" @@ -135,97 +137,55 @@ static bool database_execute_sql( } /** - * @brief Insère une paire clé-valeur dans la table metadata. + * @brief Insère une paire clé-valeur avec une requête préparée. + * + * La requête est réinitialisée après chaque insertion afin de pouvoir + * être réutilisée. */ static bool database_insert_metadata( - sqlite3 *database, - sqlite3_stmt *statement, + DatabaseStatement *statement, const char *key, const char *value ) { - int result = SQLITE_ERROR; - - if (database == NULL || - statement == NULL || + if (statement == NULL || key == NULL || value == NULL) { return false; } - result = sqlite3_bind_text( - statement, - 1, - key, - -1, - SQLITE_TRANSIENT - ); - - if (result != SQLITE_OK) + if (!database_statement_bind_text( + statement, + 1, + key + )) { - g_warning( - "Impossible de lier la clé '%s' : %s", - key, - sqlite3_errmsg(database) - ); - return false; } - result = sqlite3_bind_text( - statement, - 2, - value, - -1, - SQLITE_TRANSIENT - ); - - if (result != SQLITE_OK) + if (!database_statement_bind_text( + statement, + 2, + value + )) { - g_warning( - "Impossible de lier la valeur de '%s' : %s", - key, - sqlite3_errmsg(database) - ); - return false; } - result = sqlite3_step(statement); - - if (result != SQLITE_DONE) + if (database_statement_step(statement) != + DATABASE_STATEMENT_STEP_DONE) { - g_warning( - "Impossible d'insérer la métadonnée '%s' : %s", - key, - sqlite3_errmsg(database) - ); - return false; } - result = sqlite3_reset(statement); - - if (result != SQLITE_OK) + if (!database_statement_reset(statement)) { - g_warning( - "Impossible de réinitialiser la requête metadata : %s", - sqlite3_errmsg(database) - ); - return false; } - result = sqlite3_clear_bindings(statement); - - if (result != SQLITE_OK) + if (!database_statement_clear_bindings(statement)) { - g_warning( - "Impossible d'effacer les paramètres metadata : %s", - sqlite3_errmsg(database) - ); - return false; } @@ -236,13 +196,12 @@ static bool database_insert_metadata( * @brief Insère toutes les métadonnées obligatoires. */ static bool database_insert_all_metadata( - sqlite3 *database, + Database *database, const char *created_at, const char *investigation_uuid ) { - sqlite3_stmt *statement = NULL; - int result = SQLITE_ERROR; + DatabaseStatement *statement = NULL; bool success = false; if (database == NULL || @@ -252,59 +211,39 @@ static bool database_insert_all_metadata( return false; } - result = sqlite3_prepare_v2( + statement = database_statement_prepare( database, - database_insert_metadata_sql, - -1, - &statement, - NULL + database_insert_metadata_sql ); - if (result != SQLITE_OK) + if (statement == NULL) { - g_warning( - "Impossible de préparer l'insertion des métadonnées : %s", - sqlite3_errmsg(database) - ); - return false; } success = database_insert_metadata( - database, statement, "schema_version", DATABASE_SCHEMA_VERSION ) && database_insert_metadata( - database, statement, "application", DATABASE_APPLICATION_NAME ) && database_insert_metadata( - database, statement, "created_at", created_at ) && database_insert_metadata( - database, statement, "investigation_uuid", investigation_uuid ); - if (sqlite3_finalize(statement) != SQLITE_OK) - { - g_warning( - "Impossible de finaliser la requête metadata : %s", - sqlite3_errmsg(database) - ); - - return false; - } + database_statement_finalize(statement); return success; } @@ -313,15 +252,14 @@ static bool database_insert_all_metadata( * @brief Insère la ligne représentant l'enquête courante. */ static bool database_insert_investigation( - sqlite3 *database, + Database *database, const char *investigation_uuid, const char *investigation_name, const char *investigation_root_path, const char *created_at ) { - sqlite3_stmt *statement = NULL; - int result = SQLITE_ERROR; + DatabaseStatement *statement = NULL; bool success = false; if (database == NULL || @@ -333,117 +271,46 @@ static bool database_insert_investigation( return false; } - result = sqlite3_prepare_v2( + statement = database_statement_prepare( database, - database_insert_investigation_sql, - -1, - &statement, - NULL + database_insert_investigation_sql ); - if (result != SQLITE_OK) + if (statement == NULL) { - g_warning( - "Impossible de préparer l'insertion de l'enquête : %s", - sqlite3_errmsg(database) - ); - return false; } - result = sqlite3_bind_text( - statement, - 1, - investigation_uuid, - -1, - SQLITE_TRANSIENT - ); + success = + database_statement_bind_text( + statement, + 1, + investigation_uuid + ) && + database_statement_bind_text( + statement, + 2, + investigation_name + ) && + database_statement_bind_text( + statement, + 3, + investigation_root_path + ) && + database_statement_bind_text( + statement, + 4, + created_at + ) && + database_statement_bind_text( + statement, + 5, + created_at + ) && + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE; - if (result != SQLITE_OK) - { - goto cleanup; - } - - result = sqlite3_bind_text( - statement, - 2, - investigation_name, - -1, - SQLITE_TRANSIENT - ); - - if (result != SQLITE_OK) - { - goto cleanup; - } - - result = sqlite3_bind_text( - statement, - 3, - investigation_root_path, - -1, - SQLITE_TRANSIENT - ); - - if (result != SQLITE_OK) - { - goto cleanup; - } - - result = sqlite3_bind_text( - statement, - 4, - created_at, - -1, - SQLITE_TRANSIENT - ); - - if (result != SQLITE_OK) - { - goto cleanup; - } - - result = sqlite3_bind_text( - statement, - 5, - created_at, - -1, - SQLITE_TRANSIENT - ); - - if (result != SQLITE_OK) - { - goto cleanup; - } - - result = sqlite3_step(statement); - - if (result != SQLITE_DONE) - { - goto cleanup; - } - - success = true; - -cleanup: - - if (!success) - { - g_warning( - "Impossible d'insérer l'enquête : %s", - sqlite3_errmsg(database) - ); - } - - if (sqlite3_finalize(statement) != SQLITE_OK) - { - g_warning( - "Impossible de finaliser la requête investigation : %s", - sqlite3_errmsg(database) - ); - - success = false; - } + database_statement_finalize(statement); return success; } @@ -584,11 +451,11 @@ bool database_initialize( const char *investigation_root_path ) { - sqlite3 *database = NULL; + Database *database = NULL; + char *created_at = NULL; char *investigation_uuid = NULL; - int result = SQLITE_ERROR; bool transaction_started = false; bool success = false; @@ -612,43 +479,27 @@ bool database_initialize( if (investigation_uuid == NULL) { - g_free(created_at); - return false; + goto cleanup; } - result = sqlite3_open_v2( - database_path, - &database, - SQLITE_OPEN_READWRITE | - SQLITE_OPEN_CREATE | - SQLITE_OPEN_PRIVATECACHE, - NULL + database = database_open( + database_path ); - if (result != SQLITE_OK) - { - g_warning( - "Impossible d'ouvrir la base '%s' : %s", - database_path, - database != NULL - ? sqlite3_errmsg(database) - : sqlite3_errstr(result) - ); - - goto cleanup; - } - - if (!database_execute_sql( - database, - "PRAGMA foreign_keys = ON;" - )) + if (database == NULL) { goto cleanup; } - if (!database_execute_sql( - database, - "BEGIN IMMEDIATE;" + /* + * Accès temporaire au handle SQLite. + * + * Il reste nécessaire tant que schema_install_v1() et les anciennes + * fonctions d'insertion n'ont pas encore été migrées vers Database. + */ + + if (!database_transaction_begin( + database )) { goto cleanup; @@ -656,11 +507,13 @@ bool database_initialize( transaction_started = true; - if (!schema_install_v1(database)) + if (!schema_install_v1( + database + )) { goto rollback; } - + if (!database_insert_all_metadata( database, created_at, @@ -681,9 +534,8 @@ bool database_initialize( goto rollback; } - if (!database_execute_sql( - database, - "COMMIT;" + if (!database_transaction_commit( + database )) { goto rollback; @@ -698,9 +550,8 @@ rollback: if (transaction_started) { - if (!database_execute_sql( - database, - "ROLLBACK;" + if (!database_transaction_rollback( + database )) { g_warning( @@ -714,21 +565,7 @@ rollback: cleanup: - if (database != NULL) - { - result = sqlite3_close(database); - - if (result != SQLITE_OK) - { - g_warning( - "Impossible de fermer proprement la base '%s' : %s", - database_path, - sqlite3_errstr(result) - ); - - success = false; - } - } + database_close(database); g_free(investigation_uuid); g_free(created_at); diff --git a/src/database/schema.c b/src/database/schema.c index b237b9c..79c5e8f 100644 --- a/src/database/schema.c +++ b/src/database/schema.c @@ -5,15 +5,22 @@ #include "database/schema.h" +#include "database_internal.h" + #include +#include /** - * @brief Charge le schéma SQL V1 depuis les ressources intégrées. + * @brief Charge le schéma SQL V1 depuis le fichier du projet. + * + * @param database Connexion utilisée pour enregistrer une éventuelle erreur. * * @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(), * ou NULL en cas d'échec. */ -static char *schema_load_v1_sql(void) +static char *schema_load_v1_sql( + Database *database +) { char *schema_sql = NULL; GError *error = NULL; @@ -25,6 +32,14 @@ static char *schema_load_v1_sql(void) &error )) { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + error != NULL + ? error->message + : "Impossible de charger le schéma SQLite V1." + ); + g_warning( "Impossible de charger database/schema_v1.sql : %s", error != NULL @@ -41,9 +56,10 @@ static char *schema_load_v1_sql(void) } bool schema_install_v1( - sqlite3 *database + Database *database ) { + sqlite3 *database_handle = NULL; char *schema_sql = NULL; char *error_message = NULL; @@ -54,7 +70,24 @@ bool schema_install_v1( return false; } - schema_sql = schema_load_v1_sql(); + database_handle = database_get_handle( + database + ); + + if (database_handle == NULL) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La connexion SQLite est absente." + ); + + return false; + } + + schema_sql = schema_load_v1_sql( + database + ); if (schema_sql == NULL) { @@ -62,7 +95,7 @@ bool schema_install_v1( } result = sqlite3_exec( - database, + database_handle, schema_sql, NULL, NULL, @@ -73,11 +106,19 @@ bool schema_install_v1( if (result != SQLITE_OK) { + database_set_error( + database, + DATABASE_ERROR_SQLITE, + error_message != NULL + ? error_message + : sqlite3_errmsg(database_handle) + ); + g_warning( "Impossible d'installer le schéma SQLite V1 : %s", error_message != NULL ? error_message - : sqlite3_errmsg(database) + : sqlite3_errmsg(database_handle) ); sqlite3_free(error_message); @@ -87,5 +128,9 @@ bool schema_install_v1( sqlite3_free(error_message); + database_clear_error_internal( + database + ); + return true; } diff --git a/src/database/statement.c b/src/database/statement.c index f0bb005..67b4c03 100644 --- a/src/database/statement.c +++ b/src/database/statement.c @@ -110,13 +110,24 @@ bool database_statement_bind_text( ) { sqlite3 *database_handle = NULL; + const char *error_message = NULL; int result = SQLITE_ERROR; - if (statement == NULL || - statement->handle == NULL || + if (statement == NULL) + { + return false; + } + + if (statement->handle == NULL || index <= 0 || value == NULL) { + database_set_error( + statement->database, + DATABASE_ERROR_INVALID_ARGUMENT, + "Paramètres invalides pour la liaison d'un texte." + ); + return false; } @@ -134,17 +145,30 @@ bool database_statement_bind_text( statement->database ); + error_message = + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result); + + database_set_error( + statement->database, + DATABASE_ERROR_SQLITE, + error_message + ); + g_warning( "Impossible de lier le paramètre texte %d : %s", index, - database_handle != NULL - ? sqlite3_errmsg(database_handle) - : sqlite3_errstr(result) + error_message ); return false; } + database_clear_error_internal( + statement->database + ); + return true; } @@ -155,12 +179,23 @@ bool database_statement_bind_int64( ) { sqlite3 *database_handle = NULL; + const char *error_message = NULL; int result = SQLITE_ERROR; - if (statement == NULL || - statement->handle == NULL || + if (statement == NULL) + { + return false; + } + + if (statement->handle == NULL || index <= 0) { + database_set_error( + statement->database, + DATABASE_ERROR_INVALID_ARGUMENT, + "Paramètres invalides pour la liaison d'un entier." + ); + return false; } @@ -176,17 +211,30 @@ bool database_statement_bind_int64( statement->database ); + error_message = + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result); + + database_set_error( + statement->database, + DATABASE_ERROR_SQLITE, + error_message + ); + g_warning( "Impossible de lier le paramètre entier %d : %s", index, - database_handle != NULL - ? sqlite3_errmsg(database_handle) - : sqlite3_errstr(result) + error_message ); return false; } + database_clear_error_internal( + statement->database + ); + return true; } @@ -196,12 +244,23 @@ bool database_statement_bind_null( ) { sqlite3 *database_handle = NULL; + const char *error_message = NULL; int result = SQLITE_ERROR; - if (statement == NULL || - statement->handle == NULL || + if (statement == NULL) + { + return false; + } + + if (statement->handle == NULL || index <= 0) { + database_set_error( + statement->database, + DATABASE_ERROR_INVALID_ARGUMENT, + "Paramètres invalides pour la liaison de NULL." + ); + return false; } @@ -216,17 +275,30 @@ bool database_statement_bind_null( statement->database ); + error_message = + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result); + + database_set_error( + statement->database, + DATABASE_ERROR_SQLITE, + error_message + ); + g_warning( "Impossible de lier le paramètre NULL %d : %s", index, - database_handle != NULL - ? sqlite3_errmsg(database_handle) - : sqlite3_errstr(result) + error_message ); return false; } + database_clear_error_internal( + statement->database + ); + return true; } @@ -235,25 +307,44 @@ DatabaseStatementStepResult database_statement_step( ) { sqlite3 *database_handle = NULL; + const char *error_message = NULL; int result = SQLITE_ERROR; - if (statement == NULL || - statement->handle == NULL) + if (statement == NULL) { return DATABASE_STATEMENT_STEP_ERROR; } + if (statement->handle == NULL) + { + database_set_error( + statement->database, + DATABASE_ERROR_INVALID_STATE, + "La requête préparée SQLite est absente." + ); + + return DATABASE_STATEMENT_STEP_ERROR; + } + result = sqlite3_step( statement->handle ); if (result == SQLITE_ROW) { + database_clear_error_internal( + statement->database + ); + return DATABASE_STATEMENT_STEP_ROW; } if (result == SQLITE_DONE) { + database_clear_error_internal( + statement->database + ); + return DATABASE_STATEMENT_STEP_DONE; } @@ -261,11 +352,20 @@ DatabaseStatementStepResult database_statement_step( statement->database ); - g_warning( - "Impossible d'exécuter la requête SQL : %s", + error_message = database_handle != NULL ? sqlite3_errmsg(database_handle) - : sqlite3_errstr(result) + : sqlite3_errstr(result); + + database_set_error( + statement->database, + DATABASE_ERROR_SQLITE, + error_message + ); + + g_warning( + "Impossible d'exécuter la requête SQL : %s", + error_message ); return DATABASE_STATEMENT_STEP_ERROR; diff --git a/src/database/transaction.c b/src/database/transaction.c index fcc9845..33d2c6f 100644 --- a/src/database/transaction.c +++ b/src/database/transaction.c @@ -22,17 +22,34 @@ static bool database_transaction_execute( char *error_message = NULL; int result = SQLITE_ERROR; - if (database == NULL || - sql == NULL || - sql[0] == '\0') + if (database == NULL) { return false; } - database_handle = database_get_handle(database); + if (sql == NULL || sql[0] == '\0') + { + database_set_error( + database, + DATABASE_ERROR_INVALID_ARGUMENT, + "La commande SQL de transaction est absente." + ); + + return false; + } + + database_handle = database_get_handle( + database + ); if (database_handle == NULL) { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La connexion SQLite est absente." + ); + return false; } @@ -46,6 +63,14 @@ static bool database_transaction_execute( if (result != SQLITE_OK) { + database_set_error( + database, + DATABASE_ERROR_SQLITE, + error_message != NULL + ? error_message + : sqlite3_errmsg(database_handle) + ); + g_warning( "Impossible d'exécuter la transaction SQL : %s", error_message != NULL @@ -67,12 +92,22 @@ bool database_transaction_begin( Database *database ) { - if (database == NULL || - database_get_transaction_active(database)) + if (database == NULL) { return false; } + if (database_get_transaction_active(database)) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "Une transaction est déjà active." + ); + + return false; + } + if (!database_transaction_execute( database, "BEGIN IMMEDIATE;" @@ -86,6 +121,10 @@ bool database_transaction_begin( true ); + database_clear_error_internal( + database + ); + return true; } @@ -93,12 +132,22 @@ bool database_transaction_commit( Database *database ) { - if (database == NULL || - !database_get_transaction_active(database)) + if (database == NULL) { return false; } + if (!database_get_transaction_active(database)) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "Aucune transaction active à valider." + ); + + return false; + } + if (!database_transaction_execute( database, "COMMIT;" @@ -112,6 +161,10 @@ bool database_transaction_commit( false ); + database_clear_error_internal( + database + ); + return true; } @@ -119,12 +172,22 @@ bool database_transaction_rollback( Database *database ) { - if (database == NULL || - !database_get_transaction_active(database)) + if (database == NULL) { return false; } + if (!database_get_transaction_active(database)) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "Aucune transaction active à annuler." + ); + + return false; + } + if (!database_transaction_execute( database, "ROLLBACK;" @@ -138,5 +201,12 @@ bool database_transaction_rollback( false ); + /* + * On n'efface pas automatiquement l'erreur précédente. + * + * Un rollback est souvent exécuté à la suite d'une autre erreur. + * Cette erreur initiale doit rester disponible pour le diagnostic. + */ + return true; } diff --git a/tests/test_database b/tests/test_database index adf363d..c402d3f 100755 Binary files a/tests/test_database and b/tests/test_database differ diff --git a/tests/test_database.c b/tests/test_database.c index 7912035..9404a18 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -346,9 +346,169 @@ static void test_database_initialize_missing_parent(void) ); } +/** + * @brief Vérifie qu'une seconde initialisation échouée est annulée. + * + * La première initialisation crée une base valide. + * La seconde tentative doit échouer sans modifier les données existantes. + */ +static void test_database_initialize_rollback(void) +{ + char *temporary_directory = NULL; + char *database_path = NULL; + + char *investigation_count = NULL; + char *metadata_count = NULL; + char *investigation_name = NULL; + char *investigation_root_path = NULL; + char *integrity_result = NULL; + + sqlite3 *database = NULL; + GError *error = NULL; + int result = SQLITE_ERROR; + + temporary_directory = g_dir_make_tmp( + "labfy-database-rollback-test-XXXXXX", + &error + ); + + assert(temporary_directory != NULL); + assert(error == NULL); + + database_path = g_build_filename( + temporary_directory, + "Enquete.sqlite", + NULL + ); + + assert(database_path != NULL); + + /* + * Première initialisation valide. + */ + assert( + database_initialize( + database_path, + "Enquete_Initiale", + temporary_directory + ) + ); + + /* + * La seconde initialisation doit échouer. + * + * Elle provoquera une erreur pendant l'installation du schéma + * ou lors de l'insertion des métadonnées déjà existantes. + */ + assert( + !database_initialize( + database_path, + "Enquete_Seconde", + "/tmp/racine-seconde" + ) + ); + + result = sqlite3_open_v2( + database_path, + &database, + SQLITE_OPEN_READONLY, + NULL + ); + + assert(result == SQLITE_OK); + assert(database != NULL); + + investigation_count = test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM investigation;" + ); + + metadata_count = test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM metadata " + "WHERE key IN " + "(" + " 'schema_version'," + " 'application'," + " 'created_at'," + " 'investigation_uuid'" + ");" + ); + + investigation_name = test_database_read_single_text( + database, + "SELECT name " + "FROM investigation " + "LIMIT 1;" + ); + + investigation_root_path = test_database_read_single_text( + database, + "SELECT root_path " + "FROM investigation " + "LIMIT 1;" + ); + + integrity_result = test_database_read_single_text( + database, + "PRAGMA integrity_check;" + ); + + /* + * Aucune seconde enquête ne doit avoir été ajoutée. + */ + assert(strcmp(investigation_count, "1") == 0); + + /* + * Les quatre métadonnées initiales doivent toujours être présentes + * sans duplication. + */ + assert(strcmp(metadata_count, "4") == 0); + + /* + * Les données de la première initialisation doivent être intactes. + */ + assert( + strcmp( + investigation_name, + "Enquete_Initiale" + ) == 0 + ); + + assert( + strcmp( + investigation_root_path, + temporary_directory + ) == 0 + ); + + /* + * La base doit rester cohérente après le rollback. + */ + assert(strcmp(integrity_result, "ok") == 0); + + result = sqlite3_close(database); + + assert(result == SQLITE_OK); + + assert(g_remove(database_path) == 0); + assert(g_rmdir(temporary_directory) == 0); + + g_free(integrity_result); + g_free(investigation_root_path); + g_free(investigation_name); + g_free(metadata_count); + g_free(investigation_count); + g_free(database_path); + g_free(temporary_directory); +} + int main(void) { test_database_initialize_valid_database(); + test_database_initialize_rollback(); test_database_initialize_invalid_parameters(); test_database_initialize_missing_parent(); diff --git a/tests/test_error b/tests/test_error index 48f2c16..60f0127 100755 Binary files a/tests/test_error and b/tests/test_error differ diff --git a/tests/test_error.c b/tests/test_error.c index 2d064b9..4952194 100644 --- a/tests/test_error.c +++ b/tests/test_error.c @@ -9,6 +9,7 @@ #include #include +#include /** * @brief Vérifie l'état initial d'une connexion. @@ -121,11 +122,333 @@ static void test_success_clears_previous_error(void) database_close(database); } +/** + * @brief Vérifie les erreurs produites par un binding texte. + */ +static void test_statement_bind_error(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + const char *error_message = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT ?;" + ); + + assert(statement != NULL); + + /* + * La requête ne possède qu'un seul paramètre. + */ + assert( + !database_statement_bind_text( + statement, + 2, + "hors limite" + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_SQLITE + ); + + error_message = database_error_get_message( + database + ); + + assert(error_message != NULL); + assert(error_message[0] != '\0'); + + /* + * Un binding valide efface l'erreur précédente. + */ + assert( + database_statement_bind_text( + statement, + 1, + "valeur valide" + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + /* + * Un indice invalide appartient aux erreurs d'argument. + */ + assert( + !database_statement_bind_text( + statement, + 0, + "valeur" + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_INVALID_ARGUMENT + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie les erreurs produites par les bindings entier et NULL. + */ +static void test_statement_other_bind_errors(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT ?;" + ); + + assert(statement != NULL); + + /* + * Indice hors limite : erreur SQLite. + */ + assert( + !database_statement_bind_int64( + statement, + 2, + INT64_C(42) + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_SQLITE + ); + + /* + * Un binding valide efface l'erreur précédente. + */ + assert( + database_statement_bind_int64( + statement, + 1, + INT64_C(42) + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + /* + * Indice invalide : erreur d'argument. + */ + assert( + !database_statement_bind_int64( + statement, + 0, + INT64_C(42) + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_INVALID_ARGUMENT + ); + + /* + * Le binding NULL valide efface l'erreur précédente. + */ + assert( + database_statement_bind_null( + statement, + 1 + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + /* + * Indice hors limite : erreur SQLite. + */ + assert( + !database_statement_bind_null( + statement, + 2 + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_SQLITE + ); + + /* + * Indice invalide : erreur d'argument. + */ + assert( + !database_statement_bind_null( + statement, + 0 + ) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_INVALID_ARGUMENT + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie l'enregistrement d'une erreur pendant sqlite3_step(). + */ +static void test_statement_step_error(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + const char *error_message = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "CREATE TABLE step_error_test" + "(" + " value TEXT NOT NULL UNIQUE" + ");" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); + + /* + * Première insertion valide. + */ + statement = database_statement_prepare( + database, + "INSERT INTO step_error_test (value) " + "VALUES (?);" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "valeur unique" + ) + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); + + /* + * La seconde insertion viole la contrainte UNIQUE. + * La préparation et le binding réussissent, mais step() échoue. + */ + statement = database_statement_prepare( + database, + "INSERT INTO step_error_test (value) " + "VALUES (?);" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "valeur unique" + ) + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ERROR + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_SQLITE + ); + + error_message = database_error_get_message( + database + ); + + assert(error_message != NULL); + assert(error_message[0] != '\0'); + + database_statement_finalize(statement); + + /* + * Une opération valide ultérieure efface l'erreur. + */ + statement = database_statement_prepare( + database, + "SELECT 1;" + ); + + assert(statement != NULL); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + database_statement_finalize(statement); + database_close(database); +} + int main(void) { test_initial_error_state(); test_statement_error(); test_success_clears_previous_error(); + test_statement_bind_error(); + test_statement_other_bind_errors(); + test_statement_step_error(); printf( "DatabaseError : tous les tests sont valides.\n" diff --git a/tests/test_investigation_project b/tests/test_investigation_project index ee88091..ebee53a 100755 Binary files a/tests/test_investigation_project and b/tests/test_investigation_project differ diff --git a/tests/test_statement b/tests/test_statement index e72da1b..d1be0bc 100755 Binary files a/tests/test_statement and b/tests/test_statement differ diff --git a/tests/test_transaction b/tests/test_transaction index 004b7cd..7291ecd 100755 Binary files a/tests/test_transaction and b/tests/test_transaction differ diff --git a/tests/test_transaction.c b/tests/test_transaction.c index 27c694d..f3149f2 100644 --- a/tests/test_transaction.c +++ b/tests/test_transaction.c @@ -6,6 +6,7 @@ #include "database/database.h" #include "database/statement.h" #include "database/transaction.h" +#include "database/error.h" #include #include @@ -224,11 +225,93 @@ static void test_invalid_transaction_states(void) database_close(database); } +/** + * @brief Vérifie l'enregistrement des erreurs de transaction. + */ +static void test_transaction_error_state(void) +{ + Database *database = NULL; + const char *error_message = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + /* + * Aucun commit n'est possible sans transaction active. + */ + assert( + !database_transaction_commit(database) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_INVALID_STATE + ); + + error_message = database_error_get_message( + database + ); + + assert(error_message != NULL); + assert(error_message[0] != '\0'); + + /* + * Un début de transaction valide efface l'erreur précédente. + */ + assert( + database_transaction_begin(database) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + /* + * Une transaction imbriquée est refusée. + */ + assert( + !database_transaction_begin(database) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_INVALID_STATE + ); + + database_error_clear(database); + + assert( + database_transaction_rollback(database) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + /* + * Un second rollback est impossible. + */ + assert( + !database_transaction_rollback(database) + ); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_INVALID_STATE + ); + + database_close(database); +} + int main(void) { test_transaction_commit(); test_transaction_rollback(); test_invalid_transaction_states(); + test_transaction_error_state(); printf( "DatabaseTransaction : tous les tests sont valides.\n"