refactor(database): migrate initialization to database layer

This commit is contained in:
grayTerminal-sh 2026-07-16 11:17:06 +02:00
parent 0c6630e6cf
commit 564d67a65b
17 changed files with 1297 additions and 293 deletions

View file

@ -66,12 +66,16 @@ $(TEST_PROJECT): \
tests/test_investigation_project.c \ tests/test_investigation_project.c \
src/core/investigation_project.c \ src/core/investigation_project.c \
src/database/database.c \ src/database/database.c \
src/database/schema.c src/database/schema.c \
src/database/statement.c \
src/database/transaction.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_DATABASE): \ $(TEST_DATABASE): \
tests/test_database.c \ tests/test_database.c \
src/database/database.c \ src/database/database.c \
src/database/transaction.c \
src/database/statement.c \
src/database/schema.c src/database/schema.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
@ -79,6 +83,7 @@ $(TEST_STATEMENT): \
tests/test_statement.c \ tests/test_statement.c \
src/database/database.c \ src/database/database.c \
src/database/schema.c \ src/database/schema.c \
src/database/transaction.c \
src/database/statement.c src/database/statement.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
@ -87,7 +92,8 @@ $(TEST_TRANSACTION): \
src/database/database.c \ src/database/database.c \
src/database/schema.c \ src/database/schema.c \
src/database/statement.c \ src/database/statement.c \
src/database/transaction.c src/database/transaction.c \
src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_ERROR): \ $(TEST_ERROR): \
@ -95,6 +101,7 @@ $(TEST_ERROR): \
src/database/database.c \ src/database/database.c \
src/database/schema.c \ src/database/schema.c \
src/database/statement.c \ src/database/statement.c \
src/database/transaction.c \
src/database/error.c src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3

View file

@ -0,0 +1,366 @@
# Ticket #026 — Migrer linitialisation vers la couche Database
## Contexte
Le ticket #025 a introduit linfrastructure principale de la couche Database :
- contexte opaque `Database` ;
- ouverture et fermeture des connexions ;
- requêtes préparées avec `DatabaseStatement` ;
- bindings et lecture typée ;
- gestion des transactions ;
- infrastructure derreurs.
Cependant, `database_initialize()` utilise encore directement plusieurs fonctions de lAPI SQLite :
```c
sqlite3_open_v2()
sqlite3_prepare_v2()
sqlite3_bind_text()
sqlite3_step()
sqlite3_reset()
sqlite3_clear_bindings()
sqlite3_finalize()
sqlite3_close()
La fonction exécute également directement les commandes SQL suivantes :
```sql
BEGIN IMMEDIATE;
COMMIT;
ROLLBACK;
```
De son côté, `schema_install_v1()` reçoit toujours directement un `sqlite3 *`, ce qui contourne labstraction `Database`.
## Objectif
Réécrire linitialisation dune nouvelle enquête afin quelle utilise linfrastructure créée au ticket #025.
Linitialisation doit rester atomique :
- soit la base est entièrement initialisée ;
- soit aucune donnée partielle nest conservée.
## Travail à réaliser
### Adapter linstallation du schéma
Modifier la signature actuelle :
```C
bool schema_install_v1(
sqlite3 *database
);
```
afin quelle reçoive un contexte Database :
```C
bool schema_install_v1(
Database *database
);
```
Linstallation du fichier SQL complet pourra continuer à utiliser `sqlite3_exec()` en interne, car le schéma contient plusieurs instructions SQL.
Laccès au handle SQLite devra passer par lAPI interne :
```C
database_get_handle()
```
La fonction ne devra réaliser ni `COMMIT` ni `ROLLBACK`.
### Migrer linsertion des métadonnées
Réécrire les fonctions responsables de linsertion dans la table `metadata` avec `DatabaseStatement`.
Les opérations devront utiliser lAPI suivante :
```C
database_statement_prepare()
database_statement_bind_text()
database_statement_step()
database_statement_reset()
database_statement_clear_bindings()
database_statement_finalize()
```
Les métadonnées obligatoires restent :
```
schema_version
application
created_at
investigation_uuid
```
Une seule requête préparée devra pouvoir être réutilisée pour insérer les quatre métadonnées.
### Migrer linsertion de lenquête
Réécrire linsertion dans la table `investigation` avec `DatabaseStatement`.
Les champs insérés restent :
```C
database_open()
database_transaction_begin()
schema_install_v1()
database_transaction_commit()
database_transaction_rollback()
database_close()
```
Le déroulement attendu est :
```
validation des paramètres
création du timestamp UTC
création de lUUID
ouverture de Database
début de transaction
installation du schéma V1
insertion des métadonnées
insertion de lenquête
commit
fermeture de Database
```
Tout échec après le début de la transaction doit provoquer un rollback.
### Utiliser linfrastructure derreurs
Les erreurs rencontrées pendant :
- linstallation du schéma ;
- la préparation dune requête ;
- le binding dun paramètre ;
- lexécution dune requête ;
- le début dune transaction ;
- le commit ;
- le rollback ;
doivent être enregistrées dans le contexte Database lorsque celui-ci est disponible.
Linfrastructure interne existante pourra être utilisée :
```C
database_set_error()
database_clear_error_internal()
```
```C
DATABASE_ERROR_SQLITE
```
```C
DATABASE_ERROR_INVALID_ARGUMENT
```
```C
DATABASE_ERROR_INVALID_STATE
```
### Nettoyer lancien code SQLite
Supprimer de la logique dinitialisation les appels directs à :
```C
sqlite3_open_v2()
sqlite3_prepare_v2()
sqlite3_bind_text()
sqlite3_step()
sqlite3_reset()
sqlite3_clear_bindings()
sqlite3_finalize()
sqlite3_close()
```
Supprimer également les commandes manuelles :
```SQL
BEGIN IMMEDIATE;
COMMIT;
ROLLBACK;
```
La fonction utilitaire `database_execute_sql()` pourra être conservée uniquement si elle reste nécessaire à `database_open()` pour lactivation des clés étrangères.
### Mettre à jour la documentation publique
Dans `include/database/database.h`, supprimer la mention temporaire :
```
Cette fonction conserve temporairement son rôle actuel pendant le
refactoring du ticket #025.
```
La documentation de `database_initialize()` devra préciser que :
- linitialisation est transactionnelle ;
- un échec provoque un rollback ;
- la base est fermée avant le retour de la fonction.
## Tests à ajouter ou adapter
### Initialisation réussie
Vérifier quune initialisation valide crée :
- le schéma V1 ;
- les tables attendues ;
- les métadonnées obligatoires ;
- une seule ligne dans investigation ;
- le bon nom denquête ;
- le bon chemin racine ;
- un UUID non vide ;
- une date de création non vide ;
- une date de modification non vide.
### Paramètres invalides
Vérifier le refus des cas suivants :
```
database_path == NULL
database_path vide
investigation_name == NULL
investigation_name vide
investigation_root_path == NULL
investigation_root_path vide
```
### Rollback
Provoquer un échec après le démarrage de la transaction.
Une seconde tentative dinitialisation sur une base déjà initialisée pourra être utilisée pour provoquer une erreur de contrainte ou de création de schéma.
Après léchec, vérifier que :
- aucune donnée partielle supplémentaire nest conservée ;
- aucune seconde enquête nest ajoutée ;
- les métadonnées existantes ne sont pas dupliquées ;
- la transaction est annulée ;
- la base reste lisible ;
- la connexion peut être fermée proprement.
### Régression
Tous les tests existants doivent rester valides :
```
InvestigationNode
InvestigationTreeModel
InvestigationTreeBuilder
InvestigationProject
Database
DatabaseStatement
DatabaseTransaction
DatabaseError
```
## Critères dacceptation
- [ ] `database_initialize()` nappelle plus directement `sqlite3_open_v2()`.
- [ ] `database_initialize()` utilise `database_open()`.
- [ ] `database_initialize()` utilise `database_close()`.
- [ ] Les transactions utilisent le module `transaction`.
- [ ] Les insertions utilisent `DatabaseStatement`.
- [ ] `InvestigationNode` Investigation `schema_install_v1()` reçoit un `Database *`.
InvestigationNode
Investigation `schema_install_v1()` reçoit un `Database *`.
- [ ] `schema_install_v1()` récupère le handle avec lAPI interne.
- [ ] Aucun `sqlite3_stmt *` nest manipulé dans le code dinitialisation.
- [ ] Aucun `BEGIN`, `COMMIT` ou `ROLLBACK` manuel ne reste dans `database.c`.
- [ ] Tout échec après le début de la transaction provoque un rollback.
- [ ] Les erreurs sont enregistrées dans la couche Database.
- [ ] Les tests de réussite sont valides.
- [ ] Les tests de paramètres invalides sont valides.
- [ ] Les tests de rollback sont valides.
- [ ] Les tests de régression sont valides.
- [ ] `make` réussit sans erreur.
- [ ] `make test` réussit.
- [ ] `git diff --check` ne retourne aucune erreur.
## Hors périmètre
Ce ticket ne doit pas ajouter :
- de CRUD métier ;
- de dépôt ou repository métier ;
- de migration entre plusieurs versions de schéma ;
- de système GResource pour embarquer `schema_v1.sql` ;
- de création automatique des dossiers parents ;
- de modification du schéma SQL V1 ;
- de nouvelles tables ;
- de logique dinterface graphique ;
- de nouvelles fonctionnalités utilisateur.
## Fichiers principalement concernés
```text
include/database/database.h
include/database/schema.h
src/database/database.c
src/database/database_internal.h
src/database/schema.c
tests/test_database.c
Makefile
```
## Résultat attendu
À la fin du ticket, `database_initialize()` doit être un utilisateur normal de la couche Database.
La fonction ne doit plus contourner cette couche avec des appels SQLite directs.
Linitialisation dune enquête doit être entièrement transactionnelle, testée et cohérente avec larchitecture mise en place au ticket #025.
## Commit attendu
Une fois tous les critères dacceptation validés, le ticket pourra être enregistré avec le commit suivant :
```text
refactor(database): migrate initialization to database layer
```
Le commit doit principalement contenir :
```
include/database/database.h
include/database/schema.h
src/database/database.c
src/database/database_internal.h
src/database/schema.c
tests/test_database.c
Makefile
```
Avant le commit, exécuter :
```bash
make clean
make
make test
git diff --check
git status --short
```

View file

@ -45,8 +45,21 @@ void database_close(
/** /**
* @brief Initialise la base SQLite d'une nouvelle enquête. * @brief Initialise la base SQLite d'une nouvelle enquête.
* *
* Cette fonction conserve temporairement son rôle actuel pendant le * Cette fonction :
* refactoring du ticket #025. *
* - valide les paramètres ;
* - ouvre la connexion Database ;
* - démarre une transaction ;
* - installe le schéma SQLite V1 ;
* - insère les métadonnées obligatoires ;
* - insère l'enquête courante ;
* - valide la transaction ;
* - ferme la connexion avant de retourner.
*
* Tout échec survenant après le début de la transaction provoque
* l'annulation des modifications.
*
* La fonction ne crée pas les dossiers parents du fichier SQLite.
* *
* @param database_path Chemin complet du fichier Enquete.sqlite. * @param database_path Chemin complet du fichier Enquete.sqlite.
* @param investigation_name Nom de l'enquête. * @param investigation_name Nom de l'enquête.

View file

@ -6,9 +6,9 @@
#ifndef LABFY_INVESTIGATION_SCHEMA_H #ifndef LABFY_INVESTIGATION_SCHEMA_H
#define LABFY_INVESTIGATION_SCHEMA_H #define LABFY_INVESTIGATION_SCHEMA_H
#include <stdbool.h> #include "database/database.h"
#include <sqlite3.h> #include <stdbool.h>
/** /**
* @brief Installe le schéma SQLite V1 dans une base ouverte. * @brief Installe le schéma SQLite V1 dans une base ouverte.
@ -23,12 +23,12 @@
* *
* Elle ne réalise ni COMMIT ni ROLLBACK. * Elle ne réalise ni COMMIT ni ROLLBACK.
* *
* @param database Connexion SQLite ouverte. * @param database Connexion Database ouverte.
* *
* @return true si le schéma a é correctement installé, sinon false. * @return true si le schéma a é correctement installé, sinon false.
*/ */
bool schema_install_v1( bool schema_install_v1(
sqlite3 *database Database *database
); );
#endif #endif

Binary file not shown.

View file

@ -5,6 +5,8 @@
#include "database/database.h" #include "database/database.h"
#include "database/schema.h" #include "database/schema.h"
#include "database/transaction.h"
#include "database/statement.h"
#include "database_internal.h" #include "database_internal.h"
@ -135,97 +137,55 @@ static bool database_execute_sql(
} }
/** /**
* @brief Insère une paire clé-valeur dans la table metadata. * @brief Insère une paire clé-valeur avec une requête préparée.
*
* La requête est réinitialisée après chaque insertion afin de pouvoir
* être réutilisée.
*/ */
static bool database_insert_metadata( static bool database_insert_metadata(
sqlite3 *database, DatabaseStatement *statement,
sqlite3_stmt *statement,
const char *key, const char *key,
const char *value const char *value
) )
{ {
int result = SQLITE_ERROR; if (statement == NULL ||
if (database == NULL ||
statement == NULL ||
key == NULL || key == NULL ||
value == NULL) value == NULL)
{ {
return false; return false;
} }
result = sqlite3_bind_text( if (!database_statement_bind_text(
statement, statement,
1, 1,
key, key
-1, ))
SQLITE_TRANSIENT
);
if (result != SQLITE_OK)
{ {
g_warning(
"Impossible de lier la clé '%s' : %s",
key,
sqlite3_errmsg(database)
);
return false; return false;
} }
result = sqlite3_bind_text( if (!database_statement_bind_text(
statement, statement,
2, 2,
value, value
-1, ))
SQLITE_TRANSIENT
);
if (result != SQLITE_OK)
{ {
g_warning(
"Impossible de lier la valeur de '%s' : %s",
key,
sqlite3_errmsg(database)
);
return false; return false;
} }
result = sqlite3_step(statement); if (database_statement_step(statement) !=
DATABASE_STATEMENT_STEP_DONE)
if (result != SQLITE_DONE)
{ {
g_warning(
"Impossible d'insérer la métadonnée '%s' : %s",
key,
sqlite3_errmsg(database)
);
return false; return false;
} }
result = sqlite3_reset(statement); if (!database_statement_reset(statement))
if (result != SQLITE_OK)
{ {
g_warning(
"Impossible de réinitialiser la requête metadata : %s",
sqlite3_errmsg(database)
);
return false; return false;
} }
result = sqlite3_clear_bindings(statement); if (!database_statement_clear_bindings(statement))
if (result != SQLITE_OK)
{ {
g_warning(
"Impossible d'effacer les paramètres metadata : %s",
sqlite3_errmsg(database)
);
return false; return false;
} }
@ -236,13 +196,12 @@ static bool database_insert_metadata(
* @brief Insère toutes les métadonnées obligatoires. * @brief Insère toutes les métadonnées obligatoires.
*/ */
static bool database_insert_all_metadata( static bool database_insert_all_metadata(
sqlite3 *database, Database *database,
const char *created_at, const char *created_at,
const char *investigation_uuid const char *investigation_uuid
) )
{ {
sqlite3_stmt *statement = NULL; DatabaseStatement *statement = NULL;
int result = SQLITE_ERROR;
bool success = false; bool success = false;
if (database == NULL || if (database == NULL ||
@ -252,59 +211,39 @@ static bool database_insert_all_metadata(
return false; return false;
} }
result = sqlite3_prepare_v2( statement = database_statement_prepare(
database, database,
database_insert_metadata_sql, database_insert_metadata_sql
-1,
&statement,
NULL
); );
if (result != SQLITE_OK) if (statement == NULL)
{ {
g_warning(
"Impossible de préparer l'insertion des métadonnées : %s",
sqlite3_errmsg(database)
);
return false; return false;
} }
success = success =
database_insert_metadata( database_insert_metadata(
database,
statement, statement,
"schema_version", "schema_version",
DATABASE_SCHEMA_VERSION DATABASE_SCHEMA_VERSION
) && ) &&
database_insert_metadata( database_insert_metadata(
database,
statement, statement,
"application", "application",
DATABASE_APPLICATION_NAME DATABASE_APPLICATION_NAME
) && ) &&
database_insert_metadata( database_insert_metadata(
database,
statement, statement,
"created_at", "created_at",
created_at created_at
) && ) &&
database_insert_metadata( database_insert_metadata(
database,
statement, statement,
"investigation_uuid", "investigation_uuid",
investigation_uuid investigation_uuid
); );
if (sqlite3_finalize(statement) != SQLITE_OK) database_statement_finalize(statement);
{
g_warning(
"Impossible de finaliser la requête metadata : %s",
sqlite3_errmsg(database)
);
return false;
}
return success; return success;
} }
@ -313,15 +252,14 @@ static bool database_insert_all_metadata(
* @brief Insère la ligne représentant l'enquête courante. * @brief Insère la ligne représentant l'enquête courante.
*/ */
static bool database_insert_investigation( static bool database_insert_investigation(
sqlite3 *database, Database *database,
const char *investigation_uuid, const char *investigation_uuid,
const char *investigation_name, const char *investigation_name,
const char *investigation_root_path, const char *investigation_root_path,
const char *created_at const char *created_at
) )
{ {
sqlite3_stmt *statement = NULL; DatabaseStatement *statement = NULL;
int result = SQLITE_ERROR;
bool success = false; bool success = false;
if (database == NULL || if (database == NULL ||
@ -333,117 +271,46 @@ static bool database_insert_investigation(
return false; return false;
} }
result = sqlite3_prepare_v2( statement = database_statement_prepare(
database, database,
database_insert_investigation_sql, database_insert_investigation_sql
-1,
&statement,
NULL
); );
if (result != SQLITE_OK) if (statement == NULL)
{ {
g_warning(
"Impossible de préparer l'insertion de l'enquête : %s",
sqlite3_errmsg(database)
);
return false; return false;
} }
result = sqlite3_bind_text( success =
database_statement_bind_text(
statement, statement,
1, 1,
investigation_uuid, investigation_uuid
-1, ) &&
SQLITE_TRANSIENT database_statement_bind_text(
);
if (result != SQLITE_OK)
{
goto cleanup;
}
result = sqlite3_bind_text(
statement, statement,
2, 2,
investigation_name, investigation_name
-1, ) &&
SQLITE_TRANSIENT database_statement_bind_text(
);
if (result != SQLITE_OK)
{
goto cleanup;
}
result = sqlite3_bind_text(
statement, statement,
3, 3,
investigation_root_path, investigation_root_path
-1, ) &&
SQLITE_TRANSIENT database_statement_bind_text(
);
if (result != SQLITE_OK)
{
goto cleanup;
}
result = sqlite3_bind_text(
statement, statement,
4, 4,
created_at, created_at
-1, ) &&
SQLITE_TRANSIENT database_statement_bind_text(
);
if (result != SQLITE_OK)
{
goto cleanup;
}
result = sqlite3_bind_text(
statement, statement,
5, 5,
created_at, created_at
-1, ) &&
SQLITE_TRANSIENT database_statement_step(statement) ==
); DATABASE_STATEMENT_STEP_DONE;
if (result != SQLITE_OK) database_statement_finalize(statement);
{
goto cleanup;
}
result = sqlite3_step(statement);
if (result != SQLITE_DONE)
{
goto cleanup;
}
success = true;
cleanup:
if (!success)
{
g_warning(
"Impossible d'insérer l'enquête : %s",
sqlite3_errmsg(database)
);
}
if (sqlite3_finalize(statement) != SQLITE_OK)
{
g_warning(
"Impossible de finaliser la requête investigation : %s",
sqlite3_errmsg(database)
);
success = false;
}
return success; return success;
} }
@ -584,11 +451,11 @@ bool database_initialize(
const char *investigation_root_path const char *investigation_root_path
) )
{ {
sqlite3 *database = NULL; Database *database = NULL;
char *created_at = NULL; char *created_at = NULL;
char *investigation_uuid = NULL; char *investigation_uuid = NULL;
int result = SQLITE_ERROR;
bool transaction_started = false; bool transaction_started = false;
bool success = false; bool success = false;
@ -612,43 +479,27 @@ bool database_initialize(
if (investigation_uuid == NULL) if (investigation_uuid == NULL)
{ {
g_free(created_at);
return false;
}
result = sqlite3_open_v2(
database_path,
&database,
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 != NULL
? sqlite3_errmsg(database)
: sqlite3_errstr(result)
);
goto cleanup; goto cleanup;
} }
if (!database_execute_sql( database = database_open(
database, database_path
"PRAGMA foreign_keys = ON;" );
))
if (database == NULL)
{ {
goto cleanup; goto cleanup;
} }
if (!database_execute_sql( /*
database, * Accès temporaire au handle SQLite.
"BEGIN IMMEDIATE;" *
* Il reste nécessaire tant que schema_install_v1() et les anciennes
* fonctions d'insertion n'ont pas encore é migrées vers Database.
*/
if (!database_transaction_begin(
database
)) ))
{ {
goto cleanup; goto cleanup;
@ -656,7 +507,9 @@ bool database_initialize(
transaction_started = true; transaction_started = true;
if (!schema_install_v1(database)) if (!schema_install_v1(
database
))
{ {
goto rollback; goto rollback;
} }
@ -681,9 +534,8 @@ bool database_initialize(
goto rollback; goto rollback;
} }
if (!database_execute_sql( if (!database_transaction_commit(
database, database
"COMMIT;"
)) ))
{ {
goto rollback; goto rollback;
@ -698,9 +550,8 @@ rollback:
if (transaction_started) if (transaction_started)
{ {
if (!database_execute_sql( if (!database_transaction_rollback(
database, database
"ROLLBACK;"
)) ))
{ {
g_warning( g_warning(
@ -714,21 +565,7 @@ rollback:
cleanup: cleanup:
if (database != NULL) database_close(database);
{
result = sqlite3_close(database);
if (result != SQLITE_OK)
{
g_warning(
"Impossible de fermer proprement la base '%s' : %s",
database_path,
sqlite3_errstr(result)
);
success = false;
}
}
g_free(investigation_uuid); g_free(investigation_uuid);
g_free(created_at); g_free(created_at);

View file

@ -5,15 +5,22 @@
#include "database/schema.h" #include "database/schema.h"
#include "database_internal.h"
#include <glib.h> #include <glib.h>
#include <sqlite3.h>
/** /**
* @brief Charge le schéma SQL V1 depuis les ressources intégrées. * @brief Charge le schéma SQL V1 depuis le fichier du projet.
*
* @param database Connexion utilisée pour enregistrer une éventuelle erreur.
* *
* @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(), * @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(),
* ou NULL en cas d'échec. * ou NULL en cas d'échec.
*/ */
static char *schema_load_v1_sql(void) static char *schema_load_v1_sql(
Database *database
)
{ {
char *schema_sql = NULL; char *schema_sql = NULL;
GError *error = NULL; GError *error = NULL;
@ -25,6 +32,14 @@ static char *schema_load_v1_sql(void)
&error &error
)) ))
{ {
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
error != NULL
? error->message
: "Impossible de charger le schéma SQLite V1."
);
g_warning( g_warning(
"Impossible de charger database/schema_v1.sql : %s", "Impossible de charger database/schema_v1.sql : %s",
error != NULL error != NULL
@ -41,9 +56,10 @@ static char *schema_load_v1_sql(void)
} }
bool schema_install_v1( bool schema_install_v1(
sqlite3 *database Database *database
) )
{ {
sqlite3 *database_handle = NULL;
char *schema_sql = NULL; char *schema_sql = NULL;
char *error_message = NULL; char *error_message = NULL;
@ -54,7 +70,24 @@ bool schema_install_v1(
return false; return false;
} }
schema_sql = schema_load_v1_sql(); database_handle = database_get_handle(
database
);
if (database_handle == NULL)
{
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
"La connexion SQLite est absente."
);
return false;
}
schema_sql = schema_load_v1_sql(
database
);
if (schema_sql == NULL) if (schema_sql == NULL)
{ {
@ -62,7 +95,7 @@ bool schema_install_v1(
} }
result = sqlite3_exec( result = sqlite3_exec(
database, database_handle,
schema_sql, schema_sql,
NULL, NULL,
NULL, NULL,
@ -73,11 +106,19 @@ bool schema_install_v1(
if (result != SQLITE_OK) if (result != SQLITE_OK)
{ {
database_set_error(
database,
DATABASE_ERROR_SQLITE,
error_message != NULL
? error_message
: sqlite3_errmsg(database_handle)
);
g_warning( g_warning(
"Impossible d'installer le schéma SQLite V1 : %s", "Impossible d'installer le schéma SQLite V1 : %s",
error_message != NULL error_message != NULL
? error_message ? error_message
: sqlite3_errmsg(database) : sqlite3_errmsg(database_handle)
); );
sqlite3_free(error_message); sqlite3_free(error_message);
@ -87,5 +128,9 @@ bool schema_install_v1(
sqlite3_free(error_message); sqlite3_free(error_message);
database_clear_error_internal(
database
);
return true; return true;
} }

View file

@ -110,13 +110,24 @@ bool database_statement_bind_text(
) )
{ {
sqlite3 *database_handle = NULL; sqlite3 *database_handle = NULL;
const char *error_message = NULL;
int result = SQLITE_ERROR; int result = SQLITE_ERROR;
if (statement == NULL || if (statement == NULL)
statement->handle == NULL || {
return false;
}
if (statement->handle == NULL ||
index <= 0 || index <= 0 ||
value == NULL) value == NULL)
{ {
database_set_error(
statement->database,
DATABASE_ERROR_INVALID_ARGUMENT,
"Paramètres invalides pour la liaison d'un texte."
);
return false; return false;
} }
@ -134,17 +145,30 @@ bool database_statement_bind_text(
statement->database statement->database
); );
error_message =
database_handle != NULL
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result);
database_set_error(
statement->database,
DATABASE_ERROR_SQLITE,
error_message
);
g_warning( g_warning(
"Impossible de lier le paramètre texte %d : %s", "Impossible de lier le paramètre texte %d : %s",
index, index,
database_handle != NULL error_message
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result)
); );
return false; return false;
} }
database_clear_error_internal(
statement->database
);
return true; return true;
} }
@ -155,12 +179,23 @@ bool database_statement_bind_int64(
) )
{ {
sqlite3 *database_handle = NULL; sqlite3 *database_handle = NULL;
const char *error_message = NULL;
int result = SQLITE_ERROR; int result = SQLITE_ERROR;
if (statement == NULL || if (statement == NULL)
statement->handle == NULL || {
return false;
}
if (statement->handle == NULL ||
index <= 0) index <= 0)
{ {
database_set_error(
statement->database,
DATABASE_ERROR_INVALID_ARGUMENT,
"Paramètres invalides pour la liaison d'un entier."
);
return false; return false;
} }
@ -176,17 +211,30 @@ bool database_statement_bind_int64(
statement->database statement->database
); );
error_message =
database_handle != NULL
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result);
database_set_error(
statement->database,
DATABASE_ERROR_SQLITE,
error_message
);
g_warning( g_warning(
"Impossible de lier le paramètre entier %d : %s", "Impossible de lier le paramètre entier %d : %s",
index, index,
database_handle != NULL error_message
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result)
); );
return false; return false;
} }
database_clear_error_internal(
statement->database
);
return true; return true;
} }
@ -196,12 +244,23 @@ bool database_statement_bind_null(
) )
{ {
sqlite3 *database_handle = NULL; sqlite3 *database_handle = NULL;
const char *error_message = NULL;
int result = SQLITE_ERROR; int result = SQLITE_ERROR;
if (statement == NULL || if (statement == NULL)
statement->handle == NULL || {
return false;
}
if (statement->handle == NULL ||
index <= 0) index <= 0)
{ {
database_set_error(
statement->database,
DATABASE_ERROR_INVALID_ARGUMENT,
"Paramètres invalides pour la liaison de NULL."
);
return false; return false;
} }
@ -216,17 +275,30 @@ bool database_statement_bind_null(
statement->database statement->database
); );
error_message =
database_handle != NULL
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result);
database_set_error(
statement->database,
DATABASE_ERROR_SQLITE,
error_message
);
g_warning( g_warning(
"Impossible de lier le paramètre NULL %d : %s", "Impossible de lier le paramètre NULL %d : %s",
index, index,
database_handle != NULL error_message
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result)
); );
return false; return false;
} }
database_clear_error_internal(
statement->database
);
return true; return true;
} }
@ -235,25 +307,44 @@ DatabaseStatementStepResult database_statement_step(
) )
{ {
sqlite3 *database_handle = NULL; sqlite3 *database_handle = NULL;
const char *error_message = NULL;
int result = SQLITE_ERROR; int result = SQLITE_ERROR;
if (statement == NULL || if (statement == NULL)
statement->handle == NULL)
{ {
return DATABASE_STATEMENT_STEP_ERROR; return DATABASE_STATEMENT_STEP_ERROR;
} }
if (statement->handle == NULL)
{
database_set_error(
statement->database,
DATABASE_ERROR_INVALID_STATE,
"La requête préparée SQLite est absente."
);
return DATABASE_STATEMENT_STEP_ERROR;
}
result = sqlite3_step( result = sqlite3_step(
statement->handle statement->handle
); );
if (result == SQLITE_ROW) if (result == SQLITE_ROW)
{ {
database_clear_error_internal(
statement->database
);
return DATABASE_STATEMENT_STEP_ROW; return DATABASE_STATEMENT_STEP_ROW;
} }
if (result == SQLITE_DONE) if (result == SQLITE_DONE)
{ {
database_clear_error_internal(
statement->database
);
return DATABASE_STATEMENT_STEP_DONE; return DATABASE_STATEMENT_STEP_DONE;
} }
@ -261,11 +352,20 @@ DatabaseStatementStepResult database_statement_step(
statement->database statement->database
); );
g_warning( error_message =
"Impossible d'exécuter la requête SQL : %s",
database_handle != NULL database_handle != NULL
? sqlite3_errmsg(database_handle) ? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result) : sqlite3_errstr(result);
database_set_error(
statement->database,
DATABASE_ERROR_SQLITE,
error_message
);
g_warning(
"Impossible d'exécuter la requête SQL : %s",
error_message
); );
return DATABASE_STATEMENT_STEP_ERROR; return DATABASE_STATEMENT_STEP_ERROR;

View file

@ -22,17 +22,34 @@ static bool database_transaction_execute(
char *error_message = NULL; char *error_message = NULL;
int result = SQLITE_ERROR; int result = SQLITE_ERROR;
if (database == NULL || if (database == NULL)
sql == NULL ||
sql[0] == '\0')
{ {
return false; return false;
} }
database_handle = database_get_handle(database); if (sql == NULL || sql[0] == '\0')
{
database_set_error(
database,
DATABASE_ERROR_INVALID_ARGUMENT,
"La commande SQL de transaction est absente."
);
return false;
}
database_handle = database_get_handle(
database
);
if (database_handle == NULL) if (database_handle == NULL)
{ {
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
"La connexion SQLite est absente."
);
return false; return false;
} }
@ -46,6 +63,14 @@ static bool database_transaction_execute(
if (result != SQLITE_OK) if (result != SQLITE_OK)
{ {
database_set_error(
database,
DATABASE_ERROR_SQLITE,
error_message != NULL
? error_message
: sqlite3_errmsg(database_handle)
);
g_warning( g_warning(
"Impossible d'exécuter la transaction SQL : %s", "Impossible d'exécuter la transaction SQL : %s",
error_message != NULL error_message != NULL
@ -67,12 +92,22 @@ bool database_transaction_begin(
Database *database Database *database
) )
{ {
if (database == NULL || if (database == NULL)
database_get_transaction_active(database))
{ {
return false; return false;
} }
if (database_get_transaction_active(database))
{
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
"Une transaction est déjà active."
);
return false;
}
if (!database_transaction_execute( if (!database_transaction_execute(
database, database,
"BEGIN IMMEDIATE;" "BEGIN IMMEDIATE;"
@ -86,6 +121,10 @@ bool database_transaction_begin(
true true
); );
database_clear_error_internal(
database
);
return true; return true;
} }
@ -93,12 +132,22 @@ bool database_transaction_commit(
Database *database Database *database
) )
{ {
if (database == NULL || if (database == NULL)
!database_get_transaction_active(database))
{ {
return false; return false;
} }
if (!database_get_transaction_active(database))
{
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
"Aucune transaction active à valider."
);
return false;
}
if (!database_transaction_execute( if (!database_transaction_execute(
database, database,
"COMMIT;" "COMMIT;"
@ -112,6 +161,10 @@ bool database_transaction_commit(
false false
); );
database_clear_error_internal(
database
);
return true; return true;
} }
@ -119,12 +172,22 @@ bool database_transaction_rollback(
Database *database Database *database
) )
{ {
if (database == NULL || if (database == NULL)
!database_get_transaction_active(database))
{ {
return false; return false;
} }
if (!database_get_transaction_active(database))
{
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
"Aucune transaction active à annuler."
);
return false;
}
if (!database_transaction_execute( if (!database_transaction_execute(
database, database,
"ROLLBACK;" "ROLLBACK;"
@ -138,5 +201,12 @@ bool database_transaction_rollback(
false false
); );
/*
* On n'efface pas automatiquement l'erreur précédente.
*
* Un rollback est souvent exécuté à la suite d'une autre erreur.
* Cette erreur initiale doit rester disponible pour le diagnostic.
*/
return true; return true;
} }

Binary file not shown.

View file

@ -346,9 +346,169 @@ static void test_database_initialize_missing_parent(void)
); );
} }
/**
* @brief Vérifie qu'une seconde initialisation échouée est annulée.
*
* La première initialisation crée une base valide.
* La seconde tentative doit échouer sans modifier les données existantes.
*/
static void test_database_initialize_rollback(void)
{
char *temporary_directory = NULL;
char *database_path = NULL;
char *investigation_count = NULL;
char *metadata_count = NULL;
char *investigation_name = NULL;
char *investigation_root_path = NULL;
char *integrity_result = NULL;
sqlite3 *database = NULL;
GError *error = NULL;
int result = SQLITE_ERROR;
temporary_directory = g_dir_make_tmp(
"labfy-database-rollback-test-XXXXXX",
&error
);
assert(temporary_directory != NULL);
assert(error == NULL);
database_path = g_build_filename(
temporary_directory,
"Enquete.sqlite",
NULL
);
assert(database_path != NULL);
/*
* Première initialisation valide.
*/
assert(
database_initialize(
database_path,
"Enquete_Initiale",
temporary_directory
)
);
/*
* La seconde initialisation doit échouer.
*
* Elle provoquera une erreur pendant l'installation du schéma
* ou lors de l'insertion des métadonnées déjà existantes.
*/
assert(
!database_initialize(
database_path,
"Enquete_Seconde",
"/tmp/racine-seconde"
)
);
result = sqlite3_open_v2(
database_path,
&database,
SQLITE_OPEN_READONLY,
NULL
);
assert(result == SQLITE_OK);
assert(database != NULL);
investigation_count = test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM investigation;"
);
metadata_count = test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM metadata "
"WHERE key IN "
"("
" 'schema_version',"
" 'application',"
" 'created_at',"
" 'investigation_uuid'"
");"
);
investigation_name = test_database_read_single_text(
database,
"SELECT name "
"FROM investigation "
"LIMIT 1;"
);
investigation_root_path = test_database_read_single_text(
database,
"SELECT root_path "
"FROM investigation "
"LIMIT 1;"
);
integrity_result = test_database_read_single_text(
database,
"PRAGMA integrity_check;"
);
/*
* Aucune seconde enquête ne doit avoir é ajoutée.
*/
assert(strcmp(investigation_count, "1") == 0);
/*
* Les quatre métadonnées initiales doivent toujours être présentes
* sans duplication.
*/
assert(strcmp(metadata_count, "4") == 0);
/*
* Les données de la première initialisation doivent être intactes.
*/
assert(
strcmp(
investigation_name,
"Enquete_Initiale"
) == 0
);
assert(
strcmp(
investigation_root_path,
temporary_directory
) == 0
);
/*
* La base doit rester cohérente après le rollback.
*/
assert(strcmp(integrity_result, "ok") == 0);
result = sqlite3_close(database);
assert(result == SQLITE_OK);
assert(g_remove(database_path) == 0);
assert(g_rmdir(temporary_directory) == 0);
g_free(integrity_result);
g_free(investigation_root_path);
g_free(investigation_name);
g_free(metadata_count);
g_free(investigation_count);
g_free(database_path);
g_free(temporary_directory);
}
int main(void) int main(void)
{ {
test_database_initialize_valid_database(); test_database_initialize_valid_database();
test_database_initialize_rollback();
test_database_initialize_invalid_parameters(); test_database_initialize_invalid_parameters();
test_database_initialize_missing_parent(); test_database_initialize_missing_parent();

Binary file not shown.

View file

@ -9,6 +9,7 @@
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
/** /**
* @brief Vérifie l'état initial d'une connexion. * @brief Vérifie l'état initial d'une connexion.
@ -121,11 +122,333 @@ static void test_success_clears_previous_error(void)
database_close(database); database_close(database);
} }
/**
* @brief Vérifie les erreurs produites par un binding texte.
*/
static void test_statement_bind_error(void)
{
Database *database = NULL;
DatabaseStatement *statement = NULL;
const char *error_message = NULL;
database = database_open(":memory:");
assert(database != NULL);
statement = database_statement_prepare(
database,
"SELECT ?;"
);
assert(statement != NULL);
/*
* La requête ne possède qu'un seul paramètre.
*/
assert(
!database_statement_bind_text(
statement,
2,
"hors limite"
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_SQLITE
);
error_message = database_error_get_message(
database
);
assert(error_message != NULL);
assert(error_message[0] != '\0');
/*
* Un binding valide efface l'erreur précédente.
*/
assert(
database_statement_bind_text(
statement,
1,
"valeur valide"
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
/*
* Un indice invalide appartient aux erreurs d'argument.
*/
assert(
!database_statement_bind_text(
statement,
0,
"valeur"
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_ARGUMENT
);
database_statement_finalize(statement);
database_close(database);
}
/**
* @brief Vérifie les erreurs produites par les bindings entier et NULL.
*/
static void test_statement_other_bind_errors(void)
{
Database *database = NULL;
DatabaseStatement *statement = NULL;
database = database_open(":memory:");
assert(database != NULL);
statement = database_statement_prepare(
database,
"SELECT ?;"
);
assert(statement != NULL);
/*
* Indice hors limite : erreur SQLite.
*/
assert(
!database_statement_bind_int64(
statement,
2,
INT64_C(42)
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_SQLITE
);
/*
* Un binding valide efface l'erreur précédente.
*/
assert(
database_statement_bind_int64(
statement,
1,
INT64_C(42)
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
/*
* Indice invalide : erreur d'argument.
*/
assert(
!database_statement_bind_int64(
statement,
0,
INT64_C(42)
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_ARGUMENT
);
/*
* Le binding NULL valide efface l'erreur précédente.
*/
assert(
database_statement_bind_null(
statement,
1
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
/*
* Indice hors limite : erreur SQLite.
*/
assert(
!database_statement_bind_null(
statement,
2
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_SQLITE
);
/*
* Indice invalide : erreur d'argument.
*/
assert(
!database_statement_bind_null(
statement,
0
)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_ARGUMENT
);
database_statement_finalize(statement);
database_close(database);
}
/**
* @brief Vérifie l'enregistrement d'une erreur pendant sqlite3_step().
*/
static void test_statement_step_error(void)
{
Database *database = NULL;
DatabaseStatement *statement = NULL;
const char *error_message = NULL;
database = database_open(":memory:");
assert(database != NULL);
statement = database_statement_prepare(
database,
"CREATE TABLE step_error_test"
"("
" value TEXT NOT NULL UNIQUE"
");"
);
assert(statement != NULL);
assert(
database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_DONE
);
database_statement_finalize(statement);
/*
* Première insertion valide.
*/
statement = database_statement_prepare(
database,
"INSERT INTO step_error_test (value) "
"VALUES (?);"
);
assert(statement != NULL);
assert(
database_statement_bind_text(
statement,
1,
"valeur unique"
)
);
assert(
database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_DONE
);
database_statement_finalize(statement);
/*
* La seconde insertion viole la contrainte UNIQUE.
* La préparation et le binding réussissent, mais step() échoue.
*/
statement = database_statement_prepare(
database,
"INSERT INTO step_error_test (value) "
"VALUES (?);"
);
assert(statement != NULL);
assert(
database_statement_bind_text(
statement,
1,
"valeur unique"
)
);
assert(
database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_ERROR
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_SQLITE
);
error_message = database_error_get_message(
database
);
assert(error_message != NULL);
assert(error_message[0] != '\0');
database_statement_finalize(statement);
/*
* Une opération valide ultérieure efface l'erreur.
*/
statement = database_statement_prepare(
database,
"SELECT 1;"
);
assert(statement != NULL);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
assert(
database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_ROW
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
database_statement_finalize(statement);
database_close(database);
}
int main(void) int main(void)
{ {
test_initial_error_state(); test_initial_error_state();
test_statement_error(); test_statement_error();
test_success_clears_previous_error(); test_success_clears_previous_error();
test_statement_bind_error();
test_statement_other_bind_errors();
test_statement_step_error();
printf( printf(
"DatabaseError : tous les tests sont valides.\n" "DatabaseError : tous les tests sont valides.\n"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -6,6 +6,7 @@
#include "database/database.h" #include "database/database.h"
#include "database/statement.h" #include "database/statement.h"
#include "database/transaction.h" #include "database/transaction.h"
#include "database/error.h"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
@ -224,11 +225,93 @@ static void test_invalid_transaction_states(void)
database_close(database); database_close(database);
} }
/**
* @brief Vérifie l'enregistrement des erreurs de transaction.
*/
static void test_transaction_error_state(void)
{
Database *database = NULL;
const char *error_message = NULL;
database = database_open(":memory:");
assert(database != NULL);
/*
* Aucun commit n'est possible sans transaction active.
*/
assert(
!database_transaction_commit(database)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_STATE
);
error_message = database_error_get_message(
database
);
assert(error_message != NULL);
assert(error_message[0] != '\0');
/*
* Un début de transaction valide efface l'erreur précédente.
*/
assert(
database_transaction_begin(database)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
/*
* Une transaction imbriquée est refusée.
*/
assert(
!database_transaction_begin(database)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_STATE
);
database_error_clear(database);
assert(
database_transaction_rollback(database)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
/*
* Un second rollback est impossible.
*/
assert(
!database_transaction_rollback(database)
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_STATE
);
database_close(database);
}
int main(void) int main(void)
{ {
test_transaction_commit(); test_transaction_commit();
test_transaction_rollback(); test_transaction_rollback();
test_invalid_transaction_states(); test_invalid_transaction_states();
test_transaction_error_state();
printf( printf(
"DatabaseTransaction : tous les tests sont valides.\n" "DatabaseTransaction : tous les tests sont valides.\n"