diff --git a/Makefile b/Makefile index a6a646d..6876067 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,9 @@ TEST_TREE_MODEL = tests/test_investigation_tree_model TEST_TREE_BUILDER = tests/test_investigation_tree_builder TEST_PROJECT = tests/test_investigation_project TEST_DATABASE = tests/test_database +TEST_STATEMENT = tests/test_statement +TEST_TRANSACTION = tests/test_transaction +TEST_ERROR = tests/test_error all: $(TARGET) @@ -72,18 +75,47 @@ $(TEST_DATABASE): \ src/database/schema.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_STATEMENT): \ + tests/test_statement.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + +$(TEST_TRANSACTION): \ + tests/test_transaction.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + +$(TEST_ERROR): \ + tests/test_error.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/error.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ $(TEST_TREE_BUILDER) \ $(TEST_PROJECT) \ - $(TEST_DATABASE) + $(TEST_DATABASE) \ + $(TEST_STATEMENT) \ + $(TEST_TRANSACTION) \ + $(TEST_ERROR) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @./$(TEST_TREE_BUILDER) @./$(TEST_PROJECT) @./$(TEST_DATABASE) + @$(TEST_STATEMENT) + @$(TEST_TRANSACTION) + @$(TEST_ERROR) @echo "Tous les tests sont valides." %.o: %.c @@ -98,6 +130,9 @@ clean: $(TEST_TREE_MODEL) \ $(TEST_TREE_BUILDER) \ $(TEST_PROJECT) \ - $(TEST_DATABASE) + $(TEST_DATABASE) \ + $(TEST_STATEMENT) \ + $(TEST_TRANSACTION) \ + $(TEST_ERROR) .PHONY: clean run test diff --git a/database/schema_v1.sql b/database/schema_v1.sql index 010e433..5fba212 100644 --- a/database/schema_v1.sql +++ b/database/schema_v1.sql @@ -4,8 +4,6 @@ * Schéma SQLite officiel V1 ******************************************************************************/ -PRAGMA foreign_keys = ON; - CREATE TABLE metadata ( key TEXT PRIMARY KEY, diff --git a/docs/tickets/open/TICKET-024.md b/docs/tickets/closed/TICKET-024.md similarity index 100% rename from docs/tickets/open/TICKET-024.md rename to docs/tickets/closed/TICKET-024.md diff --git a/docs/tickets/closed/TICKET-025.md b/docs/tickets/closed/TICKET-025.md new file mode 100644 index 0000000..e4594b0 --- /dev/null +++ b/docs/tickets/closed/TICKET-025.md @@ -0,0 +1,215 @@ +# Ticket #025 + +## Titre + +Refactoriser la couche Database et établir l'infrastructure d'accès aux données. + +--- + +## Objectif + +Mettre en place l'architecture définitive de la couche Database afin de fournir +une API interne homogène pour tous les futurs modules métier. + +Ce ticket ne comprend **aucun CRUD métier**. + +Son objectif est uniquement de construire les fondations techniques qui seront +réutilisées par l'ensemble de l'application. + +--- + +## Motivations + +À terme, Labfy Investigation manipulera de nombreux objets métier : + +- investigations ; +- sources ; +- recherches ; +- preuves ; +- entités ; +- relations ; +- chronologie ; +- journal ; +- hypothèses ; +- catégories ; +- tags. + +Sans une couche d'abstraction, chaque module devrait manipuler directement +SQLite, ce qui entraînerait : + +- une forte duplication de code ; +- une gestion incohérente des erreurs ; +- des transactions difficiles à maintenir ; +- une architecture difficile à faire évoluer. + +L'objectif de ce ticket est d'éviter cette dette technique. + +--- + +## Architecture cible + +La couche Database devra évoluer vers l'organisation suivante : + +```text +include/database/ +├── database.h +├── connection.h +├── transaction.h +├── statement.h +├── error.h +├── schema.h +``` + +```text +src/database/ +├── database.c +├── connection.c +├── transaction.c +├── statement.c +├── error.c +├── schema.c +``` + +Les futurs modules métier seront ajoutés ultérieurement. + +--- + +## Structure Database + +Créer un type opaque : + +```c +typedef struct Database Database; +``` + +L'implémentation reste privée. + +La structure interne contiendra au minimum : + +- le pointeur `sqlite3 *`; +- le chemin de la base ; +- l'état de la transaction ; +- la version du schéma. + +Cette abstraction permettra de faire évoluer facilement l'implémentation sans +modifier l'API publique. + +--- + +## Connexion + +Créer les fonctions : + +```c +Database *database_open( + const char *database_path +); + +void database_close( + Database *database +); +``` + +Ces fonctions sont responsables de : + +- l'ouverture de SQLite ; +- l'activation des PRAGMA nécessaires ; +- la fermeture propre de la base. + +--- + +## Transactions + +Créer les fonctions : + +```c +bool database_begin( + Database *database +); + +bool database_commit( + Database *database +); + +bool database_rollback( + Database *database +); +``` + +Toute gestion des transactions devra passer exclusivement par ces fonctions. + +--- + +## Gestion des requêtes préparées + +Créer une couche d'abstraction pour les instructions préparées. + +L'objectif est de centraliser : + +- `sqlite3_prepare_v2()` +- `sqlite3_bind_*()` +- `sqlite3_step()` +- `sqlite3_reset()` +- `sqlite3_finalize()` + +Aucun futur module métier ne devra appeler directement ces fonctions SQLite. + +--- + +## Gestion des erreurs + +Créer un point d'entrée unique pour les erreurs SQLite. + +Toutes les erreurs devront passer par une fonction dédiée. + +Cette centralisation facilitera : + +- le débogage ; +- la journalisation ; +- l'intégration future avec le journal d'audit. + +--- + +## Responsabilités + +À la fin du ticket : + +- la couche Database est la seule autorisée à manipuler SQLite ; +- les autres modules ne connaissent pas `sqlite3`; +- les transactions sont centralisées ; +- les erreurs sont uniformisées ; +- les requêtes préparées sont encapsulées. + +--- + +## Hors périmètre + +Ce ticket ne doit pas : + +- ajouter de nouvelles tables ; +- modifier le schéma SQL ; +- créer un CRUD métier ; +- modifier GTK ; +- implémenter des migrations ; +- ajouter des fonctionnalités d'investigation. + +--- + +## Critères d'acceptation + +- [ ] Création du type opaque `Database`. +- [ ] API publique de connexion disponible. +- [ ] API publique des transactions disponible. +- [ ] Encapsulation des requêtes préparées. +- [ ] Gestion centralisée des erreurs. +- [ ] Aucune utilisation directe de `sqlite3` en dehors du dossier `database`. +- [ ] Documentation des nouvelles API. +- [ ] Tous les tests existants restent valides. + +--- + +## Commit attendu + +```text +refactor(database): introduce database infrastructure layer +``` diff --git a/include/database/database.h b/include/database/database.h index 74301e8..585b079 100644 --- a/include/database/database.h +++ b/include/database/database.h @@ -1,6 +1,6 @@ /****************************************************************************** * @file database.h - * @brief Interface publique d'initialisation de la base SQLite d'une enquête. + * @brief API principale de la couche Database. ******************************************************************************/ #ifndef LABFY_INVESTIGATION_DATABASE_H @@ -9,24 +9,50 @@ #include /** - * @brief Initialise la base SQLite d'une enquête. + * @brief Contexte opaque représentant une connexion SQLite. + */ +typedef struct Database Database; + +/** + * @brief Ouvre une base SQLite existante ou à créer. * - * La fonction crée ou ouvre le fichier SQLite indiqué, installe le schéma - * courant, puis enregistre les métadonnées de l'enquête. + * La fonction : * - * Aucun handle SQLite n'est exposé au code appelant. + * - valide le chemin ; + * - ouvre la connexion SQLite ; + * - active les clés étrangères ; + * - conserve une copie du chemin. * - * @param database_path - * Chemin complet du fichier Enquete.sqlite. + * @param database_path Chemin du fichier SQLite. * - * @param investigation_name - * Nom de l'enquête. + * @return Une nouvelle instance de Database, ou NULL en cas d'échec. + */ +Database *database_open( + const char *database_path +); + +/** + * @brief Ferme une base SQLite et libère ses ressources. * - * @param investigation_root_path - * Chemin complet du dossier racine de l'enquête. + * Cette fonction accepte NULL. * - * @return true si la base a été correctement initialisée, - * sinon false. + * @param database Instance à fermer. + */ +void database_close( + Database *database +); + +/** + * @brief Initialise la base SQLite d'une nouvelle enquête. + * + * Cette fonction conserve temporairement son rôle actuel pendant le + * refactoring du ticket #025. + * + * @param database_path Chemin complet du fichier Enquete.sqlite. + * @param investigation_name Nom de l'enquête. + * @param investigation_root_path Chemin racine de l'enquête. + * + * @return true si l'initialisation réussit, sinon false. */ bool database_initialize( const char *database_path, diff --git a/include/database/error.h b/include/database/error.h new file mode 100644 index 0000000..3eff2ff --- /dev/null +++ b/include/database/error.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * @file error.h + * @brief Gestion des erreurs de la couche Database. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_DATABASE_ERROR_H +#define LABFY_INVESTIGATION_DATABASE_ERROR_H + +#include "database/database.h" + +/** + * @brief Catégories d'erreurs de la couche Database. + */ +typedef enum +{ + DATABASE_ERROR_NONE = 0, + DATABASE_ERROR_INVALID_ARGUMENT, + DATABASE_ERROR_MEMORY, + DATABASE_ERROR_SQLITE, + DATABASE_ERROR_INVALID_STATE +} DatabaseErrorCode; + +/** + * @brief Retourne le code de la dernière erreur. + * + * @param database Connexion Database. + * + * @return Code d'erreur, ou DATABASE_ERROR_NONE. + */ +DatabaseErrorCode database_error_get_code( + const Database *database +); + +/** + * @brief Retourne le message de la dernière erreur. + * + * Le pointeur retourné appartient à Database et ne doit pas être libéré. + * + * @param database Connexion Database. + * + * @return Message d'erreur, ou NULL. + */ +const char *database_error_get_message( + const Database *database +); + +/** + * @brief Efface la dernière erreur enregistrée. + * + * @param database Connexion Database. + */ +void database_error_clear( + Database *database +); + +#endif diff --git a/include/database/statement.h b/include/database/statement.h new file mode 100644 index 0000000..6ed5ae0 --- /dev/null +++ b/include/database/statement.h @@ -0,0 +1,217 @@ +/****************************************************************************** + * @file statement.h + * @brief API des requêtes préparées de la couche Database. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_STATEMENT_H +#define LABFY_INVESTIGATION_STATEMENT_H + +#include "database/database.h" + +#include +#include + +/** + * @brief Résultat de l'exécution d'une requête préparée. + */ +typedef enum +{ + DATABASE_STATEMENT_STEP_ERROR = 0, + DATABASE_STATEMENT_STEP_ROW, + DATABASE_STATEMENT_STEP_DONE +} DatabaseStatementStepResult; + +/** + * @brief Requête SQLite préparée encapsulée. + */ +typedef struct DatabaseStatement DatabaseStatement; + +/** + * @brief Prépare une requête SQL. + * + * La requête doit contenir une seule instruction SQL. + * + * @param database Connexion ouverte. + * @param sql Requête SQL à préparer. + * + * @return Une nouvelle requête préparée, ou NULL en cas d'échec. + */ +DatabaseStatement *database_statement_prepare( + Database *database, + const char *sql +); + +/** + * @brief Exécute une étape d'une requête préparée. + * + * Pour une requête SELECT, DATABASE_STATEMENT_STEP_ROW indique + * qu'une ligne est disponible. + * + * DATABASE_STATEMENT_STEP_DONE indique que l'exécution est terminée. + * + * @param statement Requête préparée. + * + * @return Résultat de l'étape. + */ +DatabaseStatementStepResult database_statement_step( + DatabaseStatement *statement +); + +/** + * @brief Réinitialise une requête préparée afin de pouvoir la réexécuter. + * + * Les paramètres déjà liés sont conservés. + * + * @param statement Requête préparée. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_reset( + DatabaseStatement *statement +); + +/** + * @brief Efface tous les paramètres liés à une requête préparée. + * + * Après cet appel, les paramètres SQL redeviennent non liés et seront + * interprétés comme NULL si la requête est exécutée sans nouveau bind. + * + * @param statement Requête préparée. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_clear_bindings( + DatabaseStatement *statement +); + +/** + * @brief Indique si une colonne contient la valeur SQL NULL. + * + * Les indices de colonnes SQLite commencent à 0. + * + * @param statement Requête positionnée sur une ligne. + * @param column_index Indice de la colonne. + * @param is_null Destination du résultat. + * + * @return true si la colonne a pu être inspectée, sinon false. + */ +bool database_statement_column_is_null( + DatabaseStatement *statement, + int column_index, + bool *is_null +); + +/** + * @brief Lit un entier signé sur 64 bits depuis une colonne. + * + * Les indices de colonnes SQLite commencent à 0. + * La colonne doit être de type SQLITE_INTEGER. + * + * @param statement Requête positionnée sur une ligne. + * @param column_index Indice de la colonne. + * @param value Destination de la valeur. + * + * @return true si l'entier a pu être lu, sinon false. + */ +bool database_statement_column_int64( + DatabaseStatement *statement, + int column_index, + int64_t *value +); + +/** + * @brief Copie le contenu texte d'une colonne. + * + * Les indices de colonnes SQLite commencent à 0. + * + * Si la colonne contient SQL NULL, la fonction réussit et place NULL + * dans value. + * + * La chaîne retournée doit être libérée avec g_free(). + * + * @param statement Requête positionnée sur une ligne. + * @param column_index Indice de la colonne. + * @param value Destination de la chaîne allouée. + * + * @return true si la colonne a pu être lue, sinon false. + */ +bool database_statement_column_text( + DatabaseStatement *statement, + int column_index, + char **value +); + +/** + * @brief Lie une chaîne de caractères à un paramètre SQL. + * + * Les indices SQLite commencent à 1. + * + * @param statement Requête préparée. + * @param index Indice du paramètre. + * @param value Chaîne à lier. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_bind_text( + DatabaseStatement *statement, + int index, + const char *value +); + +/** + * @brief Lie un entier signé sur 64 bits à un paramètre SQL. + * + * @param statement Requête préparée. + * @param index Indice du paramètre. + * @param value Valeur à lier. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_bind_int64( + DatabaseStatement *statement, + int index, + int64_t value +); + +/** + * @brief Lie la valeur SQL NULL à un paramètre. + * + * @param statement Requête préparée. + * @param index Indice du paramètre. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_bind_null( + DatabaseStatement *statement, + int index +); + +/** + * @brief Lie une chaîne de caractères à un paramètre SQL. + * + * Les indices des paramètres SQLite commencent à 1. + * + * @param statement Requête préparée. + * @param index Indice du paramètre SQL. + * @param value Chaîne de caractères à lier. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_bind_text( + DatabaseStatement *statement, + int index, + const char *value +); + +/** + * @brief Finalise une requête préparée et libère ses ressources. + * + * Cette fonction accepte NULL. + * + * @param statement Requête à finaliser. + */ +void database_statement_finalize( + DatabaseStatement *statement +); + +#endif diff --git a/include/database/transaction.h b/include/database/transaction.h new file mode 100644 index 0000000..8b46b02 --- /dev/null +++ b/include/database/transaction.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * @file transaction.h + * @brief Gestion des transactions SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_TRANSACTION_H +#define LABFY_INVESTIGATION_TRANSACTION_H + +#include "database/database.h" + +#include + +/** + * @brief Démarre une transaction d'écriture immédiate. + * + * Une seule transaction peut être active sur une instance Database. + * + * @param database Connexion à la base. + * + * @return true en cas de succès, sinon false. + */ +bool database_transaction_begin( + Database *database +); + +/** + * @brief Valide la transaction active. + * + * @param database Connexion à la base. + * + * @return true en cas de succès, sinon false. + */ +bool database_transaction_commit( + Database *database +); + +/** + * @brief Annule la transaction active. + * + * @param database Connexion à la base. + * + * @return true en cas de succès, sinon false. + */ +bool database_transaction_rollback( + Database *database +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index e7b6317..56a51c4 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/database/database.c b/src/database/database.c index 04af0e0..f03cd6a 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -6,6 +6,8 @@ #include "database/database.h" #include "database/schema.h" +#include "database_internal.h" + #include #include @@ -19,6 +21,18 @@ */ #define DATABASE_APPLICATION_NAME "Labfy Investigation" +struct Database +{ + sqlite3 *handle; + char *database_path; + + bool transaction_active; + int schema_version; + + DatabaseErrorCode error_code; + char *error_message; +}; + /** * @brief Requête d'insertion d'une métadonnée. */ @@ -461,6 +475,109 @@ static char *database_create_utc_timestamp(void) return timestamp; } +sqlite3 *database_get_handle( + Database *database +) +{ + if (database == NULL) + { + return NULL; + } + + return database->handle; +} + +bool database_get_transaction_active( + const Database *database +) +{ + if (database == NULL) + { + return false; + } + + return database->transaction_active; +} + +void database_set_transaction_active( + Database *database, + bool transaction_active +) +{ + if (database == NULL) + { + return; + } + + database->transaction_active = transaction_active; +} + +Database *database_open( + const char *database_path +) +{ + Database *database = NULL; + int result = SQLITE_ERROR; + + if (database_path == NULL || database_path[0] == '\0') + { + return NULL; + } + + database = g_new0(Database, 1); + + if (database == NULL) + { + return NULL; + } + + database->database_path = g_strdup(database_path); + + if (database->database_path == NULL) + { + g_free(database); + return NULL; + } + + result = sqlite3_open_v2( + database_path, + &database->handle, + SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE | + SQLITE_OPEN_PRIVATECACHE, + NULL + ); + + if (result != SQLITE_OK) + { + g_warning( + "Impossible d'ouvrir la base '%s' : %s", + database_path, + database->handle != NULL + ? sqlite3_errmsg(database->handle) + : sqlite3_errstr(result) + ); + + database_close(database); + + return NULL; + } + + if (!database_execute_sql( + database->handle, + "PRAGMA foreign_keys = ON;" + )) + { + database_close(database); + return NULL; + } + + database->transaction_active = false; + database->schema_version = 0; + + return database; +} + bool database_initialize( const char *database_path, const char *investigation_name, @@ -618,3 +735,98 @@ cleanup: return success; } + +void database_set_error( + Database *database, + DatabaseErrorCode error_code, + const char *error_message +) +{ + char *error_message_copy = NULL; + + if (database == NULL) + { + return; + } + + if (error_message != NULL) + { + error_message_copy = g_strdup(error_message); + } + + g_free(database->error_message); + + database->error_code = error_code; + database->error_message = error_message_copy; +} + +DatabaseErrorCode database_get_error_code_internal( + const Database *database +) +{ + if (database == NULL) + { + return DATABASE_ERROR_NONE; + } + + return database->error_code; +} + +const char *database_get_error_message_internal( + const Database *database +) +{ + if (database == NULL) + { + return NULL; + } + + return database->error_message; +} + +void database_clear_error_internal( + Database *database +) +{ + if (database == NULL) + { + return; + } + + g_free(database->error_message); + + database->error_message = NULL; + database->error_code = DATABASE_ERROR_NONE; +} + +void database_close( + Database *database +) +{ + int result = SQLITE_OK; + + if (database == NULL) + { + return; + } + + if (database->handle != NULL) + { + result = sqlite3_close(database->handle); + + if (result != SQLITE_OK) + { + g_warning( + "Impossible de fermer proprement la base '%s' : %s", + database->database_path != NULL + ? database->database_path + : "(chemin inconnu)", + sqlite3_errstr(result) + ); + } + } + + g_free(database->error_message); + g_free(database->database_path); + g_free(database); +} diff --git a/src/database/database_internal.h b/src/database/database_internal.h new file mode 100644 index 0000000..9070400 --- /dev/null +++ b/src/database/database_internal.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * @file database_internal.h + * @brief API interne de la couche Database. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_DATABASE_INTERNAL_H +#define LABFY_INVESTIGATION_DATABASE_INTERNAL_H + +#include "database/database.h" +#include "database/error.h" + +#include +#include + +/** + * @brief Retourne le handle SQLite interne. + * + * Cette fonction est réservée aux modules de src/database/. + * + * @param database Connexion Database. + * + * @return Handle SQLite, ou NULL si l'instance est invalide. + */ +sqlite3 *database_get_handle( + Database *database +); + +/** + * @brief Indique si une transaction est active. + */ +bool database_get_transaction_active( + const Database *database +); + +/** + * @brief Modifie l'état interne de la transaction. + */ +void database_set_transaction_active( + Database *database, + bool transaction_active +); + +/** + * @brief Enregistre une erreur dans une connexion Database. + */ +void database_set_error( + Database *database, + DatabaseErrorCode error_code, + const char *error_message +); + +/** + * @brief Retourne le code d'erreur interne. + */ +DatabaseErrorCode database_get_error_code_internal( + const Database *database +); + +/** + * @brief Retourne le message d'erreur interne. + */ +const char *database_get_error_message_internal( + const Database *database +); + +/** + * @brief Efface l'erreur interne. + */ +void database_clear_error_internal( + Database *database +); + +#endif diff --git a/src/database/error.c b/src/database/error.c new file mode 100644 index 0000000..1a77f9a --- /dev/null +++ b/src/database/error.c @@ -0,0 +1,31 @@ +/****************************************************************************** + * @file error.c + * @brief Gestion des erreurs de la couche Database. + ******************************************************************************/ + +#include "database/error.h" + +#include "database_internal.h" +#include + +DatabaseErrorCode database_error_get_code( + const Database *database +) +{ + return database_get_error_code_internal(database); +} + +const char *database_error_get_message( + const Database *database +) +{ + return database_get_error_message_internal(database); +} + +void database_error_clear( + Database *database +) +{ + database_clear_error_internal(database); +} + diff --git a/src/database/statement.c b/src/database/statement.c new file mode 100644 index 0000000..f0bb005 --- /dev/null +++ b/src/database/statement.c @@ -0,0 +1,523 @@ +/****************************************************************************** + * @file statement.c + * @brief Encapsulation des requêtes préparées SQLite. + ******************************************************************************/ + +#include "database/statement.h" + +#include "database_internal.h" + +#include +#include + +/** + * @brief Représentation privée d'une requête préparée. + */ +struct DatabaseStatement +{ + Database *database; + sqlite3_stmt *handle; +}; + +DatabaseStatement *database_statement_prepare( + Database *database, + const char *sql +) +{ + DatabaseStatement *statement = NULL; + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (database == NULL) + { + return NULL; + } + + if (sql == NULL || sql[0] == '\0') + { + database_set_error( + database, + DATABASE_ERROR_INVALID_ARGUMENT, + "La requête SQL est absente." + ); + + return NULL; + } + + database_handle = database_get_handle(database); + + if (database_handle == NULL) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La connexion SQLite est absente." + ); + + return NULL; + } + + statement = g_new0(DatabaseStatement, 1); + + if (statement == NULL) + { + database_set_error( + database, + DATABASE_ERROR_MEMORY, + "Impossible d'allouer la requête préparée." + ); + + return NULL; + } + + result = sqlite3_prepare_v2( + database_handle, + sql, + -1, + &statement->handle, + NULL + ); + + if (result != SQLITE_OK) + { + database_set_error( + database, + DATABASE_ERROR_SQLITE, + sqlite3_errmsg(database_handle) + ); + + g_warning( + "Impossible de préparer la requête SQL : %s", + sqlite3_errmsg(database_handle) + ); + + g_free(statement); + + return NULL; + } + + statement->database = database; + + database_clear_error_internal(database); + + return statement; +} + +bool database_statement_bind_text( + DatabaseStatement *statement, + int index, + const char *value +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (statement == NULL || + statement->handle == NULL || + index <= 0 || + value == NULL) + { + return false; + } + + result = sqlite3_bind_text( + statement->handle, + index, + value, + -1, + SQLITE_TRANSIENT + ); + + if (result != SQLITE_OK) + { + database_handle = database_get_handle( + statement->database + ); + + g_warning( + "Impossible de lier le paramètre texte %d : %s", + index, + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + + return false; + } + + return true; +} + +bool database_statement_bind_int64( + DatabaseStatement *statement, + int index, + int64_t value +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (statement == NULL || + statement->handle == NULL || + index <= 0) + { + return false; + } + + result = sqlite3_bind_int64( + statement->handle, + index, + value + ); + + if (result != SQLITE_OK) + { + database_handle = database_get_handle( + statement->database + ); + + g_warning( + "Impossible de lier le paramètre entier %d : %s", + index, + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + + return false; + } + + return true; +} + +bool database_statement_bind_null( + DatabaseStatement *statement, + int index +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (statement == NULL || + statement->handle == NULL || + index <= 0) + { + return false; + } + + result = sqlite3_bind_null( + statement->handle, + index + ); + + if (result != SQLITE_OK) + { + database_handle = database_get_handle( + statement->database + ); + + g_warning( + "Impossible de lier le paramètre NULL %d : %s", + index, + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + + return false; + } + + return true; +} + +DatabaseStatementStepResult database_statement_step( + DatabaseStatement *statement +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (statement == NULL || + statement->handle == NULL) + { + return DATABASE_STATEMENT_STEP_ERROR; + } + + result = sqlite3_step( + statement->handle + ); + + if (result == SQLITE_ROW) + { + return DATABASE_STATEMENT_STEP_ROW; + } + + if (result == SQLITE_DONE) + { + return DATABASE_STATEMENT_STEP_DONE; + } + + database_handle = database_get_handle( + statement->database + ); + + g_warning( + "Impossible d'exécuter la requête SQL : %s", + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + + return DATABASE_STATEMENT_STEP_ERROR; +} + +bool database_statement_reset( + DatabaseStatement *statement +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (statement == NULL || + statement->handle == NULL) + { + return false; + } + + result = sqlite3_reset( + statement->handle + ); + + if (result != SQLITE_OK) + { + database_handle = database_get_handle( + statement->database + ); + + g_warning( + "Impossible de réinitialiser la requête SQL : %s", + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + + return false; + } + + return true; +} + +bool database_statement_clear_bindings( + DatabaseStatement *statement +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_ERROR; + + if (statement == NULL || + statement->handle == NULL) + { + return false; + } + + result = sqlite3_clear_bindings( + statement->handle + ); + + if (result != SQLITE_OK) + { + database_handle = database_get_handle( + statement->database + ); + + g_warning( + "Impossible d'effacer les paramètres de la requête SQL : %s", + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + + return false; + } + + return true; +} + +bool database_statement_column_is_null( + DatabaseStatement *statement, + int column_index, + bool *is_null +) +{ + int column_count = 0; + + if (statement == NULL || + statement->handle == NULL || + is_null == NULL) + { + return false; + } + + column_count = sqlite3_column_count( + statement->handle + ); + + if (column_index < 0 || + column_index >= column_count) + { + return false; + } + + *is_null = + sqlite3_column_type( + statement->handle, + column_index + ) == SQLITE_NULL; + + return true; +} + +bool database_statement_column_text( + DatabaseStatement *statement, + int column_index, + char **value +) +{ + const unsigned char *column_text = NULL; + int column_count = 0; + int text_length = 0; + + if (statement == NULL || + statement->handle == NULL || + value == NULL) + { + return false; + } + + *value = NULL; + + column_count = sqlite3_column_count( + statement->handle + ); + + if (column_index < 0 || + column_index >= column_count) + { + return false; + } + + if (sqlite3_column_type( + statement->handle, + column_index + ) == SQLITE_NULL) + { + return true; + } + + column_text = sqlite3_column_text( + statement->handle, + column_index + ); + + if (column_text == NULL) + { + return false; + } + + text_length = sqlite3_column_bytes( + statement->handle, + column_index + ); + + *value = g_strndup( + (const char *) column_text, + (gsize) text_length + ); + + if (*value == NULL) + { + return false; + } + + return true; +} + +bool database_statement_column_int64( + DatabaseStatement *statement, + int column_index, + int64_t *value +) +{ + int column_count = 0; + int column_type = SQLITE_NULL; + + if (statement == NULL || + statement->handle == NULL || + value == NULL) + { + return false; + } + + column_count = sqlite3_column_count( + statement->handle + ); + + if (column_index < 0 || + column_index >= column_count) + { + return false; + } + + column_type = sqlite3_column_type( + statement->handle, + column_index + ); + + if (column_type != SQLITE_INTEGER) + { + return false; + } + + *value = sqlite3_column_int64( + statement->handle, + column_index + ); + + return true; +} + +void database_statement_finalize( + DatabaseStatement *statement +) +{ + sqlite3 *database_handle = NULL; + int result = SQLITE_OK; + + if (statement == NULL) + { + return; + } + + database_handle = database_get_handle( + statement->database + ); + + if (statement->handle != NULL) + { + result = sqlite3_finalize( + statement->handle + ); + + if (result != SQLITE_OK) + { + g_warning( + "Impossible de finaliser la requête SQL : %s", + database_handle != NULL + ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + } + } + + g_free(statement); +} diff --git a/src/database/transaction.c b/src/database/transaction.c new file mode 100644 index 0000000..fcc9845 --- /dev/null +++ b/src/database/transaction.c @@ -0,0 +1,142 @@ +/****************************************************************************** + * @file transaction.c + * @brief Gestion des transactions SQLite. + ******************************************************************************/ + +#include "database/transaction.h" + +#include "database_internal.h" + +#include +#include + +/** + * @brief Exécute une commande SQL de transaction. + */ +static bool database_transaction_execute( + Database *database, + const char *sql +) +{ + sqlite3 *database_handle = NULL; + char *error_message = NULL; + int result = SQLITE_ERROR; + + if (database == NULL || + sql == NULL || + sql[0] == '\0') + { + return false; + } + + database_handle = database_get_handle(database); + + if (database_handle == NULL) + { + return false; + } + + result = sqlite3_exec( + database_handle, + sql, + NULL, + NULL, + &error_message + ); + + if (result != SQLITE_OK) + { + g_warning( + "Impossible d'exécuter la transaction SQL : %s", + error_message != NULL + ? error_message + : sqlite3_errmsg(database_handle) + ); + + sqlite3_free(error_message); + + return false; + } + + sqlite3_free(error_message); + + return true; +} + +bool database_transaction_begin( + Database *database +) +{ + if (database == NULL || + database_get_transaction_active(database)) + { + return false; + } + + if (!database_transaction_execute( + database, + "BEGIN IMMEDIATE;" + )) + { + return false; + } + + database_set_transaction_active( + database, + true + ); + + return true; +} + +bool database_transaction_commit( + Database *database +) +{ + if (database == NULL || + !database_get_transaction_active(database)) + { + return false; + } + + if (!database_transaction_execute( + database, + "COMMIT;" + )) + { + return false; + } + + database_set_transaction_active( + database, + false + ); + + return true; +} + +bool database_transaction_rollback( + Database *database +) +{ + if (database == NULL || + !database_get_transaction_active(database)) + { + return false; + } + + if (!database_transaction_execute( + database, + "ROLLBACK;" + )) + { + return false; + } + + database_set_transaction_active( + database, + false + ); + + return true; +} diff --git a/tests/test_database b/tests/test_database index a6f4d90..adf363d 100755 Binary files a/tests/test_database and b/tests/test_database differ diff --git a/tests/test_error b/tests/test_error new file mode 100755 index 0000000..48f2c16 Binary files /dev/null and b/tests/test_error differ diff --git a/tests/test_error.c b/tests/test_error.c new file mode 100644 index 0000000..2d064b9 --- /dev/null +++ b/tests/test_error.c @@ -0,0 +1,135 @@ +/****************************************************************************** + * @file test_error.c + * @brief Tests de l'infrastructure d'erreurs Database. + ******************************************************************************/ + +#include "database/database.h" +#include "database/error.h" +#include "database/statement.h" + +#include +#include + +/** + * @brief Vérifie l'état initial d'une connexion. + */ +static void test_initial_error_state(void) +{ + Database *database = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + assert( + database_error_get_message(database) == NULL + ); + + database_close(database); +} + +/** + * @brief Vérifie l'enregistrement d'une erreur SQLite. + */ +static void test_statement_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 FROM;" + ); + + assert(statement == NULL); + + 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_error_clear(database); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + assert( + database_error_get_message(database) == NULL + ); + + database_close(database); +} + +/** + * @brief Vérifie qu'une opération valide efface l'erreur précédente. + */ +static void test_success_clears_previous_error(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT FROM;" + ); + + assert(statement == NULL); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_SQLITE + ); + + statement = database_statement_prepare( + database, + "SELECT 1;" + ); + + assert(statement != NULL); + + assert( + database_error_get_code(database) == + DATABASE_ERROR_NONE + ); + + assert( + database_error_get_message(database) == NULL + ); + + database_statement_finalize(statement); + database_close(database); +} + +int main(void) +{ + test_initial_error_state(); + test_statement_error(); + test_success_clears_previous_error(); + + printf( + "DatabaseError : tous les tests sont valides.\n" + ); + + return 0; +} diff --git a/tests/test_investigation_project b/tests/test_investigation_project index 18cee8e..ee88091 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 new file mode 100755 index 0000000..e72da1b Binary files /dev/null and b/tests/test_statement differ diff --git a/tests/test_statement.c b/tests/test_statement.c new file mode 100644 index 0000000..0436b4f --- /dev/null +++ b/tests/test_statement.c @@ -0,0 +1,689 @@ +/****************************************************************************** + * @file test_statement.c + * @brief Tests du module DatabaseStatement. + ******************************************************************************/ + +#include "database/database.h" +#include "database/statement.h" + +#include +#include +#include + +/** + * @brief Vérifie la préparation et la finalisation d'une requête valide. + */ +static void test_prepare_valid_statement(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT 1;" + ); + + assert(statement != NULL); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie qu'une requête SQL invalide est refusée. + */ +static void test_prepare_invalid_statement(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT FROM;" + ); + + assert(statement == NULL); + + database_close(database); +} + +/** + * @brief Vérifie le traitement des paramètres invalides. + */ +static void test_invalid_parameters(void) +{ + Database *database = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + assert( + database_statement_prepare( + NULL, + "SELECT 1;" + ) == NULL + ); + + assert( + database_statement_prepare( + database, + NULL + ) == NULL + ); + + assert( + database_statement_prepare( + database, + "" + ) == NULL + ); + + /* + * La fonction doit accepter NULL sans provoquer d'erreur. + */ + database_statement_finalize(NULL); + + database_close(database); +} + +/** + * @brief Vérifie la liaison valide d'un paramètre texte. + */ +static void test_bind_valid_text(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "Labfy Investigation" + ) + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie le refus des paramètres invalides. + */ +static void test_bind_invalid_text(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT ?;" + ); + + assert(statement != NULL); + + assert( + !database_statement_bind_text( + NULL, + 1, + "test" + ) + ); + + assert( + !database_statement_bind_text( + statement, + 0, + "test" + ) + ); + + assert( + !database_statement_bind_text( + statement, + 1, + NULL + ) + ); + + /* + * La requête ne possède qu'un seul paramètre. + */ + assert( + !database_statement_bind_text( + statement, + 2, + "hors limite" + ) + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie l'exécution d'une requête retournant une ligne. + */ +static void test_step_select_statement(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT 1;" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie l'exécution d'une requête sans ligne de résultat. + */ +static void test_step_non_query_statement(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "CREATE TABLE test_step" + "(" + " id INTEGER PRIMARY KEY" + ");" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie le traitement d'une requête absente. + */ +static void test_step_invalid_statement(void) +{ + assert( + database_statement_step(NULL) == + DATABASE_STATEMENT_STEP_ERROR + ); +} + +/** + * @brief Vérifie qu'une requête peut être réexécutée après reset. + */ +static void test_reset_statement(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "Labfy Investigation" + ) + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + assert( + database_statement_reset(statement) + ); + + /* + * Le paramètre texte est toujours lié après le reset. + */ + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie le refus d'une requête absente. + */ +static void test_reset_invalid_statement(void) +{ + assert( + !database_statement_reset(NULL) + ); +} + +/** + * @brief Vérifie l'effacement des paramètres d'une requête préparée. + */ +static void test_clear_bindings(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "Première valeur" + ) + ); + + assert( + database_statement_clear_bindings( + statement + ) + ); + + /* + * Le paramètre peut recevoir une nouvelle valeur après effacement. + */ + assert( + database_statement_bind_text( + statement, + 1, + "Nouvelle valeur" + ) + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie le refus d'une requête absente. + */ +static void test_clear_bindings_invalid_statement(void) +{ + assert( + !database_statement_clear_bindings(NULL) + ); +} + +/** + * @brief Vérifie la détection des colonnes SQL NULL. + */ +static void test_column_is_null(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + bool is_null = false; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT NULL, 'Labfy Investigation';" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_is_null( + statement, + 0, + &is_null + ) + ); + + assert(is_null); + + assert( + database_statement_column_is_null( + statement, + 1, + &is_null + ) + ); + + assert(!is_null); + + assert( + !database_statement_column_is_null( + NULL, + 0, + &is_null + ) + ); + + assert( + !database_statement_column_is_null( + statement, + -1, + &is_null + ) + ); + + assert( + !database_statement_column_is_null( + statement, + 2, + &is_null + ) + ); + + assert( + !database_statement_column_is_null( + statement, + 0, + NULL + ) + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie la lecture sécurisée des colonnes texte. + */ +static void test_column_text(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + char *value = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT 'Labfy Investigation', NULL;" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_text( + statement, + 0, + &value + ) + ); + + assert(value != NULL); + assert( + g_strcmp0( + value, + "Labfy Investigation" + ) == 0 + ); + + g_free(value); + value = NULL; + + /* + * Une colonne SQL NULL produit un pointeur NULL, + * mais la lecture reste valide. + */ + assert( + database_statement_column_text( + statement, + 1, + &value + ) + ); + + assert(value == NULL); + + assert( + !database_statement_column_text( + NULL, + 0, + &value + ) + ); + + assert( + !database_statement_column_text( + statement, + -1, + &value + ) + ); + + assert( + !database_statement_column_text( + statement, + 2, + &value + ) + ); + + assert( + !database_statement_column_text( + statement, + 0, + NULL + ) + ); + + database_statement_finalize(statement); + database_close(database); +} + +/** + * @brief Vérifie la lecture stricte des colonnes entières. + */ +static void test_column_int64(void) +{ + Database *database = NULL; + DatabaseStatement *statement = NULL; + int64_t value = INT64_C(0); + + database = database_open(":memory:"); + + assert(database != NULL); + + statement = database_statement_prepare( + database, + "SELECT 42, NULL, '42';" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &value + ) + ); + + assert(value == INT64_C(42)); + + /* + * SQL NULL n'est pas un entier valide. + */ + assert( + !database_statement_column_int64( + statement, + 1, + &value + ) + ); + + /* + * Le texte '42' n'est pas accepté comme entier. + */ + assert( + !database_statement_column_int64( + statement, + 2, + &value + ) + ); + + assert( + !database_statement_column_int64( + NULL, + 0, + &value + ) + ); + + assert( + !database_statement_column_int64( + statement, + -1, + &value + ) + ); + + assert( + !database_statement_column_int64( + statement, + 3, + &value + ) + ); + + assert( + !database_statement_column_int64( + statement, + 0, + NULL + ) + ); + + database_statement_finalize(statement); + database_close(database); +} + +int main(void) +{ + test_prepare_valid_statement(); + test_prepare_invalid_statement(); + test_invalid_parameters(); + test_bind_valid_text(); + test_bind_invalid_text(); + test_step_select_statement(); + test_step_non_query_statement(); + test_step_invalid_statement(); + test_reset_statement(); + test_reset_invalid_statement(); + test_clear_bindings(); + test_clear_bindings_invalid_statement(); + test_column_is_null(); + test_column_text(); + test_column_int64(); + + printf( + "DatabaseStatement : tous les tests sont valides.\n" + ); + + return 0; +} diff --git a/tests/test_transaction b/tests/test_transaction new file mode 100755 index 0000000..004b7cd Binary files /dev/null and b/tests/test_transaction differ diff --git a/tests/test_transaction.c b/tests/test_transaction.c new file mode 100644 index 0000000..27c694d --- /dev/null +++ b/tests/test_transaction.c @@ -0,0 +1,238 @@ +/****************************************************************************** + * @file test_transaction.c + * @brief Tests du module DatabaseTransaction. + ******************************************************************************/ + +#include "database/database.h" +#include "database/statement.h" +#include "database/transaction.h" + +#include +#include + +/** + * @brief Crée la table utilisée par les tests. + */ +static void test_create_table( + Database *database +) +{ + DatabaseStatement *statement = NULL; + + statement = database_statement_prepare( + database, + "CREATE TABLE transaction_test" + "(" + " value TEXT NOT NULL" + ");" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); +} + +/** + * @brief Insère une valeur dans la table de test. + */ +static void test_insert_value( + Database *database, + const char *value +) +{ + DatabaseStatement *statement = NULL; + + statement = database_statement_prepare( + database, + "INSERT INTO transaction_test (value)" + " VALUES (?);" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + value + ) + ); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize(statement); +} + +/** + * @brief Compte les lignes de la table de test. + */ +static int64_t test_count_values( + Database *database +) +{ + DatabaseStatement *statement = NULL; + int64_t count = INT64_C(0); + + statement = database_statement_prepare( + database, + "SELECT COUNT(*) FROM transaction_test;" + ); + + assert(statement != NULL); + + assert( + database_statement_step(statement) == + DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &count + ) + ); + + database_statement_finalize(statement); + + return count; +} + +/** + * @brief Vérifie qu'un commit conserve les modifications. + */ +static void test_transaction_commit(void) +{ + Database *database = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + test_create_table(database); + + assert( + database_transaction_begin(database) + ); + + test_insert_value( + database, + "valeur validée" + ); + + assert( + database_transaction_commit(database) + ); + + assert( + test_count_values(database) == + INT64_C(1) + ); + + database_close(database); +} + +/** + * @brief Vérifie qu'un rollback annule les modifications. + */ +static void test_transaction_rollback(void) +{ + Database *database = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + test_create_table(database); + + assert( + database_transaction_begin(database) + ); + + test_insert_value( + database, + "valeur annulée" + ); + + assert( + database_transaction_rollback(database) + ); + + assert( + test_count_values(database) == + INT64_C(0) + ); + + database_close(database); +} + +/** + * @brief Vérifie le refus des états de transaction invalides. + */ +static void test_invalid_transaction_states(void) +{ + Database *database = NULL; + + database = database_open(":memory:"); + + assert(database != NULL); + + assert( + !database_transaction_commit(database) + ); + + assert( + !database_transaction_rollback(database) + ); + + assert( + database_transaction_begin(database) + ); + + assert( + !database_transaction_begin(database) + ); + + assert( + database_transaction_rollback(database) + ); + + assert( + !database_transaction_rollback(database) + ); + + assert( + !database_transaction_begin(NULL) + ); + + assert( + !database_transaction_commit(NULL) + ); + + assert( + !database_transaction_rollback(NULL) + ); + + database_close(database); +} + +int main(void) +{ + test_transaction_commit(); + test_transaction_rollback(); + test_invalid_transaction_states(); + + printf( + "DatabaseTransaction : tous les tests sont valides.\n" + ); + + return 0; +}