feat: load investigation graph from SQLite

This commit is contained in:
grayTerminal-sh 2026-07-21 08:55:36 +02:00
parent 00dff97b15
commit c389dba0d9
4 changed files with 2076 additions and 2 deletions

View file

@ -50,6 +50,7 @@ RELATION_SERVICE_TEST_CFLAGS := \
-Wpedantic \
-DRELATION_SERVICE_ENABLE_TEST_HOOKS
INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
SRC := $(shell find src -name "*.c")
@ -100,6 +101,7 @@ TEST_RELATION_DAO := tests/test_relation_dao
TEST_RELATION_EVIDENCE_DAO := tests/test_relation_evidence_dao
TEST_RELATION_SERVICE := tests/test_relation_service
TEST_INVESTIGATION_GRAPH_MODEL := tests/test_investigation_graph_model
TEST_INVESTIGATION_GRAPH_LOADER := tests/test_investigation_graph_loader
all: $(TARGET)
@ -487,6 +489,22 @@ $(TEST_INVESTIGATION_GRAPH_MODEL): \
$(CC) $(INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS) $^ -o $@ \
$(TEST_LDFLAGS)
$(TEST_INVESTIGATION_GRAPH_LOADER): \
tests/test_investigation_graph_loader.c \
src/core/investigation_graph_loader.c \
src/dao/entity_dao.c \
src/dao/relation_dao.c \
src/models/investigation_graph_model.c \
src/models/entity_record.c \
src/models/relation_record.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
src/database/transaction.c \
src/database/error.c
$(CC) $(INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS) $^ -o $@ \
$(TEST_LDFLAGS) -lsqlite3
test: \
$(TEST_NODE) \
$(TEST_TREE_MODEL) \
@ -530,7 +548,8 @@ test: \
$(TEST_RELATION_DAO) \
$(TEST_RELATION_EVIDENCE_DAO) \
$(TEST_RELATION_SERVICE) \
$(TEST_INVESTIGATION_GRAPH_MODEL)
$(TEST_INVESTIGATION_GRAPH_MODEL) \
$(TEST_INVESTIGATION_GRAPH_LOADER)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -575,6 +594,7 @@ test: \
@$(TEST_RELATION_EVIDENCE_DAO)
@$(TEST_RELATION_SERVICE)
@$(TEST_INVESTIGATION_GRAPH_MODEL)
@$(TEST_INVESTIGATION_GRAPH_LOADER)
@echo "Tous les tests sont valides."
%.o: %.c
@ -625,6 +645,7 @@ clean:
$(TEST_RELATION_DAO) \
$(TEST_RELATION_EVIDENCE_DAO) \
$(TEST_RELATION_SERVICE) \
$(TEST_INVESTIGATION_GRAPH_MODEL)
$(TEST_INVESTIGATION_GRAPH_MODEL) \
$(TEST_INVESTIGATION_GRAPH_LOADER)
.PHONY: clean run test

View file

@ -0,0 +1,121 @@
/******************************************************************************
* @file investigation_graph_loader.h
* @brief Chargement du graphe métier d'une enquête depuis SQLite.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOADER_H
#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOADER_H
#include "database/database.h"
#include "models/investigation_graph_model.h"
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Chargeur opaque du graphe métier d'une enquête.
*
* Le chargeur emprunte la connexion Database reçue lors de sa création.
* Il possède les DAO qu'il utilise.
*/
typedef struct InvestigationGraphLoader InvestigationGraphLoader;
/**
* @brief Catégories d'erreurs produites par InvestigationGraphLoader.
*/
typedef enum
{
/**
* Un argument public est invalide.
*/
INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT,
/**
* Une allocation ou la création du graphe a échoué.
*/
INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY,
/**
* Les entités n'ont pas pu être chargées depuis SQLite.
*/
INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD,
/**
* Une entité n'a pas pu être transférée dans le graphe.
*/
INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER,
/**
* Les relations n'ont pas pu être chargées depuis SQLite.
*/
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD,
/**
* Une relation n'a pas pu être transférée dans le graphe.
*/
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER
} InvestigationGraphLoaderError;
/**
* @brief Domaine d'erreur du chargeur.
*/
#define INVESTIGATION_GRAPH_LOADER_ERROR \
investigation_graph_loader_error_quark()
/**
* @brief Retourne le domaine d'erreur du chargeur.
*/
GQuark investigation_graph_loader_error_quark(void);
/**
* @brief Crée un chargeur utilisant une connexion Database existante.
*
* La connexion est empruntée et doit rester valide pendant toute la durée de
* vie du chargeur.
*
* @param database Connexion SQLite ouverte et initialisée.
* @param error Emplacement facultatif recevant une erreur.
*
* @return Nouveau chargeur, ou NULL en cas d'échec.
*/
InvestigationGraphLoader *investigation_graph_loader_new(
Database *database,
GError **error
);
/**
* @brief Libère le chargeur.
*
* Les DAO internes sont libérés, mais la connexion Database empruntée n'est
* jamais fermée.
*
* Cette fonction accepte NULL.
*
* @param graph_loader Chargeur à libérer.
*/
void investigation_graph_loader_free(
InvestigationGraphLoader *graph_loader
);
/**
* @brief Construit un nouveau graphe depuis les données SQLite.
*
* Toutes les entités sont chargées et transférées avant les relations.
*
* Le graphe retourné appartient à l'appelant. Au moindre échec, le graphe
* partiellement construit et tous les modèles temporaires sont détruits.
*
* @param graph_loader Chargeur valide.
* @param error Emplacement facultatif recevant une erreur.
*
* @return Nouveau graphe complet, ou NULL en cas d'échec.
*/
InvestigationGraphModel *investigation_graph_loader_load(
InvestigationGraphLoader *graph_loader,
GError **error
);
G_END_DECLS
#endif

View file

@ -0,0 +1,562 @@
/******************************************************************************
* @file investigation_graph_loader.c
* @brief Chargement du graphe métier d'une enquête depuis SQLite.
******************************************************************************/
#include "core/investigation_graph_loader.h"
#include "dao/entity_dao.h"
#include "dao/relation_dao.h"
#include "models/entity_record.h"
#include "models/relation_record.h"
#include <glib.h>
/**
* @brief Représentation privée du chargeur.
*
* La connexion Database est empruntée.
* Les DAO sont possédés.
*/
struct InvestigationGraphLoader
{
Database *database;
EntityDao *entity_dao;
RelationDao *relation_dao;
};
/**
* @brief Enregistre une erreur littérale du chargeur.
*/
static void investigation_graph_loader_set_error_literal(
GError **error,
InvestigationGraphLoaderError error_code,
const char *message
)
{
if (error == NULL ||
*error != NULL)
{
return;
}
g_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR,
error_code,
message
);
}
/**
* @brief Conserve le contexte d'une erreur provenant d'un autre composant.
*/
static void investigation_graph_loader_set_nested_error(
GError **error,
InvestigationGraphLoaderError error_code,
const char *context,
const GError *nested_error
)
{
if (error == NULL ||
*error != NULL)
{
return;
}
g_set_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR,
error_code,
"%s : %s",
context,
nested_error != NULL
? nested_error->message
: "erreur inconnue"
);
}
/**
* @brief Transfère toutes les entités d'un tableau vers le graphe.
*
* Le tableau possède initialement tous ses EntityRecord.
*
* Chaque élément est retiré du tableau avant son ajout. En cas de succès, le
* graphe en devient propriétaire. En cas d'échec, le modèle retiré est libéré
* explicitement et les éléments restants restent possédés par le tableau.
*/
static gboolean investigation_graph_loader_transfer_entities(
InvestigationGraphModel *graph_model,
GPtrArray *entity_records,
GError **error
)
{
EntityRecord *entity_record =
NULL;
GError *graph_error =
NULL;
if (graph_model == NULL ||
entity_records == NULL)
{
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT,
"Le graphe ou la liste des entités est absent."
);
return FALSE;
}
while (entity_records->len > 0)
{
entity_record =
g_ptr_array_steal_index(
entity_records,
0
);
if (entity_record == NULL)
{
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER,
"La liste des entités contient un modèle absent."
);
return FALSE;
}
if (!investigation_graph_model_add_entity(
graph_model,
entity_record,
&graph_error
))
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER,
"Impossible de transférer une entité dans le graphe",
graph_error
);
g_clear_error(
&graph_error
);
entity_record_free(
entity_record
);
return FALSE;
}
entity_record =
NULL;
}
return TRUE;
}
/**
* @brief Transfère toutes les relations d'un tableau vers le graphe.
*
* Le tableau possède initialement tous ses RelationRecord.
*
* Chaque élément est retiré du tableau avant son ajout. En cas de succès, le
* graphe en devient propriétaire. En cas d'échec, le modèle retiré est libéré
* explicitement et les éléments restants restent possédés par le tableau.
*/
static gboolean investigation_graph_loader_transfer_relations(
InvestigationGraphModel *graph_model,
GPtrArray *relation_records,
GError **error
)
{
RelationRecord *relation_record =
NULL;
GError *graph_error =
NULL;
if (graph_model == NULL ||
relation_records == NULL)
{
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT,
"Le graphe ou la liste des relations est absent."
);
return FALSE;
}
while (relation_records->len > 0)
{
relation_record =
g_ptr_array_steal_index(
relation_records,
0
);
if (relation_record == NULL)
{
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER,
"La liste des relations contient un modèle absent."
);
return FALSE;
}
if (!investigation_graph_model_add_relation(
graph_model,
relation_record,
&graph_error
))
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER,
"Impossible de transférer une relation dans le graphe",
graph_error
);
g_clear_error(
&graph_error
);
relation_record_free(
relation_record
);
return FALSE;
}
relation_record =
NULL;
}
return TRUE;
}
GQuark investigation_graph_loader_error_quark(void)
{
return g_quark_from_static_string(
"investigation-graph-loader-error-quark"
);
}
InvestigationGraphLoader *investigation_graph_loader_new(
Database *database,
GError **error
)
{
InvestigationGraphLoader *graph_loader =
NULL;
EntityDao *entity_dao =
NULL;
RelationDao *relation_dao =
NULL;
GError *dao_error =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (database == NULL)
{
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT,
"La connexion Database est obligatoire."
);
return NULL;
}
entity_dao =
entity_dao_new(
database,
&dao_error
);
if (entity_dao == NULL)
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD,
"Impossible de créer le DAO des entités",
dao_error
);
g_clear_error(
&dao_error
);
return NULL;
}
relation_dao =
relation_dao_new(
database,
&dao_error
);
if (relation_dao == NULL)
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD,
"Impossible de créer le DAO des relations",
dao_error
);
g_clear_error(
&dao_error
);
entity_dao_free(
entity_dao
);
return NULL;
}
graph_loader =
g_try_new0(
InvestigationGraphLoader,
1
);
if (graph_loader == NULL)
{
relation_dao_free(
relation_dao
);
entity_dao_free(
entity_dao
);
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY,
"Impossible d'allouer le chargeur du graphe d'enquête."
);
return NULL;
}
graph_loader->database =
database;
graph_loader->entity_dao =
entity_dao;
graph_loader->relation_dao =
relation_dao;
return graph_loader;
}
void investigation_graph_loader_free(
InvestigationGraphLoader *graph_loader
)
{
if (graph_loader == NULL)
{
return;
}
relation_dao_free(
graph_loader->relation_dao
);
entity_dao_free(
graph_loader->entity_dao
);
graph_loader->relation_dao =
NULL;
graph_loader->entity_dao =
NULL;
graph_loader->database =
NULL;
g_free(
graph_loader
);
}
InvestigationGraphModel *investigation_graph_loader_load(
InvestigationGraphLoader *graph_loader,
GError **error
)
{
InvestigationGraphModel *graph_model =
NULL;
GPtrArray *entity_records =
NULL;
GPtrArray *relation_records =
NULL;
GError *component_error =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (graph_loader == NULL ||
graph_loader->database == NULL ||
graph_loader->entity_dao == NULL ||
graph_loader->relation_dao == NULL)
{
investigation_graph_loader_set_error_literal(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT,
"Le chargeur du graphe d'enquête est invalide."
);
return NULL;
}
graph_model =
investigation_graph_model_new(
&component_error
);
if (graph_model == NULL)
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY,
"Impossible de créer le graphe d'enquête",
component_error
);
g_clear_error(
&component_error
);
return NULL;
}
entity_records =
entity_dao_list_all(
graph_loader->entity_dao,
&component_error
);
if (entity_records == NULL)
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD,
"Impossible de charger les entités depuis SQLite",
component_error
);
goto failure;
}
g_clear_error(
&component_error
);
if (!investigation_graph_loader_transfer_entities(
graph_model,
entity_records,
error
))
{
goto failure;
}
g_ptr_array_unref(
entity_records
);
entity_records =
NULL;
relation_records =
relation_dao_list_all(
graph_loader->relation_dao,
&component_error
);
if (relation_records == NULL)
{
investigation_graph_loader_set_nested_error(
error,
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD,
"Impossible de charger les relations depuis SQLite",
component_error
);
goto failure;
}
g_clear_error(
&component_error
);
if (!investigation_graph_loader_transfer_relations(
graph_model,
relation_records,
error
))
{
goto failure;
}
g_ptr_array_unref(
relation_records
);
relation_records =
NULL;
return graph_model;
failure:
g_clear_error(
&component_error
);
if (relation_records != NULL)
{
g_ptr_array_unref(
relation_records
);
}
if (entity_records != NULL)
{
g_ptr_array_unref(
entity_records
);
}
investigation_graph_model_free(
graph_model
);
return NULL;
}

File diff suppressed because it is too large Load diff