refactor(database): introduce database infrastructure layer

This commit is contained in:
grayTerminal-sh 2026-07-16 09:14:34 +02:00
parent c2df647b46
commit 0c6630e6cf
22 changed files with 2655 additions and 17 deletions

View file

@ -34,6 +34,9 @@ TEST_TREE_MODEL = tests/test_investigation_tree_model
TEST_TREE_BUILDER = tests/test_investigation_tree_builder TEST_TREE_BUILDER = tests/test_investigation_tree_builder
TEST_PROJECT = tests/test_investigation_project TEST_PROJECT = tests/test_investigation_project
TEST_DATABASE = tests/test_database TEST_DATABASE = tests/test_database
TEST_STATEMENT = tests/test_statement
TEST_TRANSACTION = tests/test_transaction
TEST_ERROR = tests/test_error
all: $(TARGET) all: $(TARGET)
@ -72,18 +75,47 @@ $(TEST_DATABASE): \
src/database/schema.c src/database/schema.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(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: \
$(TEST_NODE) \ $(TEST_NODE) \
$(TEST_TREE_MODEL) \ $(TEST_TREE_MODEL) \
$(TEST_TREE_BUILDER) \ $(TEST_TREE_BUILDER) \
$(TEST_PROJECT) \ $(TEST_PROJECT) \
$(TEST_DATABASE) $(TEST_DATABASE) \
$(TEST_STATEMENT) \
$(TEST_TRANSACTION) \
$(TEST_ERROR)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@./$(TEST_TREE_BUILDER) @./$(TEST_TREE_BUILDER)
@./$(TEST_PROJECT) @./$(TEST_PROJECT)
@./$(TEST_DATABASE) @./$(TEST_DATABASE)
@$(TEST_STATEMENT)
@$(TEST_TRANSACTION)
@$(TEST_ERROR)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -98,6 +130,9 @@ clean:
$(TEST_TREE_MODEL) \ $(TEST_TREE_MODEL) \
$(TEST_TREE_BUILDER) \ $(TEST_TREE_BUILDER) \
$(TEST_PROJECT) \ $(TEST_PROJECT) \
$(TEST_DATABASE) $(TEST_DATABASE) \
$(TEST_STATEMENT) \
$(TEST_TRANSACTION) \
$(TEST_ERROR)
.PHONY: clean run test .PHONY: clean run test

View file

@ -4,8 +4,6 @@
* Schéma SQLite officiel V1 * Schéma SQLite officiel V1
******************************************************************************/ ******************************************************************************/
PRAGMA foreign_keys = ON;
CREATE TABLE metadata CREATE TABLE metadata
( (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,

View file

@ -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
```

View file

@ -1,6 +1,6 @@
/****************************************************************************** /******************************************************************************
* @file database.h * @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 #ifndef LABFY_INVESTIGATION_DATABASE_H
@ -9,24 +9,50 @@
#include <stdbool.h> #include <stdbool.h>
/** /**
* @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 * La fonction :
* courant, puis enregistre les métadonnées de l'enquête.
* *
* 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 * @param database_path Chemin du fichier SQLite.
* Chemin complet du fichier Enquete.sqlite.
* *
* @param investigation_name * @return Une nouvelle instance de Database, ou NULL en cas d'échec.
* Nom de l'enquête. */
Database *database_open(
const char *database_path
);
/**
* @brief Ferme une base SQLite et libère ses ressources.
* *
* @param investigation_root_path * Cette fonction accepte NULL.
* Chemin complet du dossier racine de l'enquête.
* *
* @return true si la base a é correctement initialisée, * @param database Instance à fermer.
* sinon false. */
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( bool database_initialize(
const char *database_path, const char *database_path,

56
include/database/error.h Normal file
View file

@ -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

View file

@ -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 <stdbool.h>
#include <stdint.h>
/**
* @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

View file

@ -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 <stdbool.h>
/**
* @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

Binary file not shown.

View file

@ -6,6 +6,8 @@
#include "database/database.h" #include "database/database.h"
#include "database/schema.h" #include "database/schema.h"
#include "database_internal.h"
#include <glib.h> #include <glib.h>
#include <sqlite3.h> #include <sqlite3.h>
@ -19,6 +21,18 @@
*/ */
#define DATABASE_APPLICATION_NAME "Labfy Investigation" #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. * @brief Requête d'insertion d'une métadonnée.
*/ */
@ -461,6 +475,109 @@ static char *database_create_utc_timestamp(void)
return timestamp; 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( bool database_initialize(
const char *database_path, const char *database_path,
const char *investigation_name, const char *investigation_name,
@ -618,3 +735,98 @@ cleanup:
return success; 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);
}

View file

@ -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 <sqlite3.h>
#include <stdbool.h>
/**
* @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

31
src/database/error.c Normal file
View file

@ -0,0 +1,31 @@
/******************************************************************************
* @file error.c
* @brief Gestion des erreurs de la couche Database.
******************************************************************************/
#include "database/error.h"
#include "database_internal.h"
#include <stddef.h>
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);
}

523
src/database/statement.c Normal file
View file

@ -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 <glib.h>
#include <sqlite3.h>
/**
* @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);
}

142
src/database/transaction.c Normal file
View file

@ -0,0 +1,142 @@
/******************************************************************************
* @file transaction.c
* @brief Gestion des transactions SQLite.
******************************************************************************/
#include "database/transaction.h"
#include "database_internal.h"
#include <glib.h>
#include <sqlite3.h>
/**
* @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;
}

Binary file not shown.

BIN
tests/test_error Executable file

Binary file not shown.

135
tests/test_error.c Normal file
View file

@ -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 <assert.h>
#include <stdio.h>
/**
* @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;
}

Binary file not shown.

BIN
tests/test_statement Executable file

Binary file not shown.

689
tests/test_statement.c Normal file
View file

@ -0,0 +1,689 @@
/******************************************************************************
* @file test_statement.c
* @brief Tests du module DatabaseStatement.
******************************************************************************/
#include "database/database.h"
#include "database/statement.h"
#include <assert.h>
#include <stdio.h>
#include <glib.h>
/**
* @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;
}

BIN
tests/test_transaction Executable file

Binary file not shown.

238
tests/test_transaction.c Normal file
View file

@ -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 <assert.h>
#include <stdio.h>
/**
* @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;
}