Charger le graphe d'enquête depuis SQLite #59

Closed
opened 2026-07-21 08:40:14 +02:00 by fy59 · 0 comments
Owner

Charger le graphe d'enquête depuis SQLite

Contexte

Le projet possède désormais :

  • EntityRecord et EntityDao ;
  • RelationRecord et RelationDao ;
  • InvestigationGraphModel.

InvestigationGraphModel représente en mémoire :

  • les entités de l'enquête ;
  • les relations orientées ;
  • les index entrants et sortants.

SQLite reste la source de vérité.

La future interface GTK et le futur tableau blanc ne doivent pas charger
directement les DAO ni gérer eux-mêmes le transfert de propriété des modèles.

Une couche dédiée doit construire un graphe complet et cohérent depuis la base
de données.

Objectif

Créer un composant InvestigationGraphLoader capable de :

  1. charger toutes les entités depuis EntityDao ;
  2. les transférer dans un nouveau InvestigationGraphModel ;
  3. charger toutes les relations depuis RelationDao ;
  4. les transférer dans le graphe ;
  5. retourner uniquement un graphe entièrement construit ;
  6. libérer toutes les ressources en cas d'échec.

Le composant doit rester indépendant de GTK.

Fichiers

Créer :

include/core/investigation_graph_loader.h
src/core/investigation_graph_loader.c
tests/test_investigation_graph_loader.c

Type opaque

typedef struct InvestigationGraphLoader InvestigationGraphLoader;

Le chargeur doit :

  • emprunter une connexion Database existante ;
  • créer et posséder ses DAO internes ;
  • ne jamais fermer la connexion empruntée ;
  • libérer ses DAO lors de sa destruction.

Domaine d'erreur

Créer :

typedef enum
{
    INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT,
    INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY,
    INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD,
    INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER,
    INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD,
    INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER
} InvestigationGraphLoaderError;

Ajouter :

#define INVESTIGATION_GRAPH_LOADER_ERROR \
    investigation_graph_loader_error_quark()

GQuark investigation_graph_loader_error_quark(void);

Les erreurs provenant des DAO ou du graphe doivent être contextualisées sans
masquer leur message d'origine.

Construction

InvestigationGraphLoader *investigation_graph_loader_new(
    Database *database,
    GError **error
);

La fonction doit :

  • refuser une connexion NULL ;
  • créer EntityDao ;
  • créer RelationDao ;
  • nettoyer correctement les créations intermédiaires en cas d'échec ;
  • accepter un GError ** facultatif.

Destruction

void investigation_graph_loader_free(
    InvestigationGraphLoader *graph_loader
);

La fonction doit :

  • accepter NULL ;
  • libérer les DAO internes ;
  • ne jamais fermer la connexion Database.

Chargement complet

InvestigationGraphModel *investigation_graph_loader_load(
    InvestigationGraphLoader *graph_loader,
    GError **error
);

La fonction doit retourner un nouveau graphe possédé par l'appelant.

Ordre obligatoire

Le chargement doit suivre cet ordre :

création du graphe vide
    ↓
chargement de toutes les entités
    ↓
transfert des entités dans le graphe
    ↓
chargement de toutes les relations
    ↓
transfert des relations dans le graphe
    ↓
retour du graphe complet

Les relations ne doivent jamais être ajoutées avant les entités.

Transfert de propriété

Les tableaux retournés par EntityDao et RelationDao possèdent leurs
modèles.

Lorsqu'un modèle est ajouté avec succès au graphe :

  • le graphe devient propriétaire du modèle ;
  • le tableau du DAO ne doit plus tenter de le libérer.

Lorsqu'un ajout échoue :

  • le modèle refusé reste possédé par le tableau ou doit être libéré
    explicitement ;
  • aucun double free ne doit être possible.

L'implémentation doit donc transférer chaque pointeur de manière explicite.

Une stratégie valide consiste à :

  1. lire le pointeur à l'index courant ;
  2. appeler la fonction d'ajout du graphe ;
  3. remplacer l'élément du tableau par NULL uniquement après le succès ;
  4. libérer normalement le tableau à la fin.

Aucun pointeur ne doit être abandonné.

Comportement en cas d'échec

Au moindre échec :

  • le graphe partiellement construit doit être détruit ;
  • les modèles non transférés doivent être détruits ;
  • les tableaux temporaires doivent être libérés ;
  • la fonction doit retourner NULL ;
  • aucun graphe incomplet ne doit être rendu à l'appelant.

Exemples :

Échec pendant le chargement des entités

EntityDao échoue
    ↓
aucun graphe retourné

Échec pendant le transfert d'une entité

entités déjà transférées → détruites avec le graphe
entité refusée           → détruite avec le tableau temporaire
entités restantes        → détruites avec le tableau temporaire

Échec pendant le transfert d'une relation

graphe partiel           → entièrement détruit
relations non transférées → détruites avec le tableau temporaire

Base vide

Une base contenant :

  • aucune entité ;
  • aucune relation ;

doit produire un graphe valide et vide.

Ce cas ne constitue pas une erreur.

Cohérence des données

Si SQLite contient une relation dont :

  • l'entité source est absente ;
  • ou l'entité cible est absente ;

le chargement doit échouer avec
INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER.

Le chargeur ne doit pas ignorer silencieusement la relation incohérente.

Le message doit inclure l'erreur produite par InvestigationGraphModel.

Absence de modification de SQLite

Le chargeur est strictement en lecture.

Il ne doit :

  • insérer aucune donnée ;
  • modifier aucune donnée ;
  • supprimer aucune donnée ;
  • corriger automatiquement aucune incohérence.

Tests

Créer :

tests/test_investigation_graph_loader.c

Les tests doivent utiliser une base SQLite temporaire initialisée avec le
schéma réel.

Les données valides doivent être préparées avec les DAO existants.

Les incohérences impossibles à créer via les DAO peuvent être introduites par
SQL direct uniquement lorsque le test vise précisément leur détection.

Scénarios minimaux

  1. refus d'une connexion NULL ;
  2. création et destruction du chargeur ;
  3. destruction avec NULL ;
  4. la destruction du chargeur ne ferme pas la connexion ;
  5. chargement d'une base vide ;
  6. graphe vide avec un comptage nul ;
  7. chargement d'une entité ;
  8. chargement de plusieurs entités ;
  9. chargement d'entités sans relation ;
  10. chargement d'une relation valide ;
  11. chargement de plusieurs relations ;
  12. conservation de l'orientation source vers cible ;
  13. vérification des index entrants ;
  14. vérification des index sortants ;
  15. vérification des listes incidentes ;
  16. recherche des entités après chargement ;
  17. recherche des relations après chargement ;
  18. vérification du nombre d'entités ;
  19. vérification du nombre de relations ;
  20. refus d'un chargeur NULL ;
  21. GError ** facultatif ;
  22. échec lorsque la table entites est absente ;
  23. échec lorsque la table relations est absente ;
  24. aucun graphe partiel retourné après une erreur ;
  25. échec lorsqu'une relation référence une source absente ;
  26. échec lorsqu'une relation référence une cible absente ;
  27. message d'erreur contenant le contexte du graphe ;
  28. possibilité de recharger après un échec corrigé ;
  29. deux chargements successifs retournent deux graphes indépendants ;
  30. la destruction d'un graphe ne modifie pas l'autre ;
  31. tous les anciens tests restent valides.

Test d'indépendance des graphes

Le test doit appeler deux fois :

investigation_graph_loader_load();

Il doit vérifier que :

  • les deux pointeurs de graphe sont différents ;
  • les deux graphes contiennent les mêmes données métier ;
  • les pointeurs de EntityRecord correspondants sont différents ;
  • les pointeurs de RelationRecord correspondants sont différents ;
  • la destruction du premier graphe ne rend pas le second inutilisable.

Chaque chargement doit donc reconstruire ses propres modèles depuis SQLite.

Test d'une relation incohérente

Pour tester une relation dont une entité est absente, les contraintes de clés
étrangères doivent être désactivées uniquement pour la préparation du cas de
test, puis réactivées avant le chargement.

Le test doit :

  1. créer une base temporaire valide ;
  2. insérer une relation incohérente par SQL direct ;
  3. réactiver les contraintes ;
  4. appeler le chargeur ;
  5. vérifier le retour NULL ;
  6. vérifier RELATION_TRANSFER ;
  7. vérifier que le message mentionne la source ou la cible absente.

Cette manipulation ne doit jamais être utilisée dans le code de production.

Makefile

Ajouter :

INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS := \
    $(TEST_CFLAGS) \
    -Wpedantic

TEST_INVESTIGATION_GRAPH_LOADER := \
    tests/test_investigation_graph_loader

Ajouter une cible dédiée compilant uniquement :

  • 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 ;
  • les modules database nécessaires.

La cible ne doit inclure aucun autre fichier tests/test_*.c.

Ajouter $(TEST_INVESTIGATION_GRAPH_LOADER) :

  • aux dépendances de make test ;
  • aux exécutables exécutés par make test ;
  • aux fichiers supprimés par make clean.

La compilation doit conserver :

-std=c17 -Wall -Wextra -Wpedantic -Werror

Hors périmètre

Ce ticket ne couvre pas :

  • GTK ;
  • Cairo ;
  • le tableau blanc interactif ;
  • le chargement asynchrone ;
  • une barre de progression ;
  • l'annulation du chargement ;
  • les preuves associées aux relations ;
  • les coordonnées des nœuds ;
  • le zoom ;
  • les filtres d'affichage ;
  • la modification du graphe ;
  • la synchronisation en temps réel après une écriture ;
  • une transaction d'écriture ;
  • une migration SQLite.

Le chargement asynchrone destiné à l'interface GTK fera l'objet d'un ticket
séparé après validation de ce chargeur synchrone.

Critères d'acceptation

  • InvestigationGraphLoader est opaque.
  • Le chargeur emprunte la connexion Database.
  • Les DAO internes sont possédés par le chargeur.
  • La connexion n'est jamais fermée par le chargeur.
  • Toutes les entités sont chargées avant les relations.
  • Les modèles sont transférés sans fuite ni double libération.
  • Une base vide produit un graphe vide valide.
  • Une base valide produit un graphe complet.
  • Une incohérence relationnelle provoque un échec explicite.
  • Aucun graphe partiel n'est retourné.
  • Les erreurs des DAO sont contextualisées.
  • Les erreurs du graphe sont contextualisées.
  • Deux chargements produisent deux graphes indépendants.
  • Le chargeur ne modifie jamais SQLite.
  • Aucun changement GTK n'est introduit.
  • Les tests ciblés passent.
  • make clean && make && make test réussit.
  • git diff --check ne retourne aucune erreur.
# Charger le graphe d'enquête depuis SQLite ## Contexte Le projet possède désormais : - `EntityRecord` et `EntityDao` ; - `RelationRecord` et `RelationDao` ; - `InvestigationGraphModel`. `InvestigationGraphModel` représente en mémoire : - les entités de l'enquête ; - les relations orientées ; - les index entrants et sortants. SQLite reste la source de vérité. La future interface GTK et le futur tableau blanc ne doivent pas charger directement les DAO ni gérer eux-mêmes le transfert de propriété des modèles. Une couche dédiée doit construire un graphe complet et cohérent depuis la base de données. ## Objectif Créer un composant `InvestigationGraphLoader` capable de : 1. charger toutes les entités depuis `EntityDao` ; 2. les transférer dans un nouveau `InvestigationGraphModel` ; 3. charger toutes les relations depuis `RelationDao` ; 4. les transférer dans le graphe ; 5. retourner uniquement un graphe entièrement construit ; 6. libérer toutes les ressources en cas d'échec. Le composant doit rester indépendant de GTK. ## Fichiers Créer : ```text include/core/investigation_graph_loader.h src/core/investigation_graph_loader.c tests/test_investigation_graph_loader.c ``` ## Type opaque ```c typedef struct InvestigationGraphLoader InvestigationGraphLoader; ``` Le chargeur doit : - emprunter une connexion `Database` existante ; - créer et posséder ses DAO internes ; - ne jamais fermer la connexion empruntée ; - libérer ses DAO lors de sa destruction. ## Domaine d'erreur Créer : ```c typedef enum { INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT, INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY, INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD, INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER, INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD, INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER } InvestigationGraphLoaderError; ``` Ajouter : ```c #define INVESTIGATION_GRAPH_LOADER_ERROR \ investigation_graph_loader_error_quark() GQuark investigation_graph_loader_error_quark(void); ``` Les erreurs provenant des DAO ou du graphe doivent être contextualisées sans masquer leur message d'origine. ## Construction ```c InvestigationGraphLoader *investigation_graph_loader_new( Database *database, GError **error ); ``` La fonction doit : - refuser une connexion `NULL` ; - créer `EntityDao` ; - créer `RelationDao` ; - nettoyer correctement les créations intermédiaires en cas d'échec ; - accepter un `GError **` facultatif. ## Destruction ```c void investigation_graph_loader_free( InvestigationGraphLoader *graph_loader ); ``` La fonction doit : - accepter `NULL` ; - libérer les DAO internes ; - ne jamais fermer la connexion `Database`. ## Chargement complet ```c InvestigationGraphModel *investigation_graph_loader_load( InvestigationGraphLoader *graph_loader, GError **error ); ``` La fonction doit retourner un nouveau graphe possédé par l'appelant. ### Ordre obligatoire Le chargement doit suivre cet ordre : ```text création du graphe vide ↓ chargement de toutes les entités ↓ transfert des entités dans le graphe ↓ chargement de toutes les relations ↓ transfert des relations dans le graphe ↓ retour du graphe complet ``` Les relations ne doivent jamais être ajoutées avant les entités. ## Transfert de propriété Les tableaux retournés par `EntityDao` et `RelationDao` possèdent leurs modèles. Lorsqu'un modèle est ajouté avec succès au graphe : - le graphe devient propriétaire du modèle ; - le tableau du DAO ne doit plus tenter de le libérer. Lorsqu'un ajout échoue : - le modèle refusé reste possédé par le tableau ou doit être libéré explicitement ; - aucun double `free` ne doit être possible. L'implémentation doit donc transférer chaque pointeur de manière explicite. Une stratégie valide consiste à : 1. lire le pointeur à l'index courant ; 2. appeler la fonction d'ajout du graphe ; 3. remplacer l'élément du tableau par `NULL` uniquement après le succès ; 4. libérer normalement le tableau à la fin. Aucun pointeur ne doit être abandonné. ## Comportement en cas d'échec Au moindre échec : - le graphe partiellement construit doit être détruit ; - les modèles non transférés doivent être détruits ; - les tableaux temporaires doivent être libérés ; - la fonction doit retourner `NULL` ; - aucun graphe incomplet ne doit être rendu à l'appelant. Exemples : ### Échec pendant le chargement des entités ```text EntityDao échoue ↓ aucun graphe retourné ``` ### Échec pendant le transfert d'une entité ```text entités déjà transférées → détruites avec le graphe entité refusée → détruite avec le tableau temporaire entités restantes → détruites avec le tableau temporaire ``` ### Échec pendant le transfert d'une relation ```text graphe partiel → entièrement détruit relations non transférées → détruites avec le tableau temporaire ``` ## Base vide Une base contenant : - aucune entité ; - aucune relation ; doit produire un graphe valide et vide. Ce cas ne constitue pas une erreur. ## Cohérence des données Si SQLite contient une relation dont : - l'entité source est absente ; - ou l'entité cible est absente ; le chargement doit échouer avec `INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER`. Le chargeur ne doit pas ignorer silencieusement la relation incohérente. Le message doit inclure l'erreur produite par `InvestigationGraphModel`. ## Absence de modification de SQLite Le chargeur est strictement en lecture. Il ne doit : - insérer aucune donnée ; - modifier aucune donnée ; - supprimer aucune donnée ; - corriger automatiquement aucune incohérence. ## Tests Créer : ```text tests/test_investigation_graph_loader.c ``` Les tests doivent utiliser une base SQLite temporaire initialisée avec le schéma réel. Les données valides doivent être préparées avec les DAO existants. Les incohérences impossibles à créer via les DAO peuvent être introduites par SQL direct uniquement lorsque le test vise précisément leur détection. ### Scénarios minimaux 1. refus d'une connexion `NULL` ; 2. création et destruction du chargeur ; 3. destruction avec `NULL` ; 4. la destruction du chargeur ne ferme pas la connexion ; 5. chargement d'une base vide ; 6. graphe vide avec un comptage nul ; 7. chargement d'une entité ; 8. chargement de plusieurs entités ; 9. chargement d'entités sans relation ; 10. chargement d'une relation valide ; 11. chargement de plusieurs relations ; 12. conservation de l'orientation source vers cible ; 13. vérification des index entrants ; 14. vérification des index sortants ; 15. vérification des listes incidentes ; 16. recherche des entités après chargement ; 17. recherche des relations après chargement ; 18. vérification du nombre d'entités ; 19. vérification du nombre de relations ; 20. refus d'un chargeur `NULL` ; 21. `GError **` facultatif ; 22. échec lorsque la table `entites` est absente ; 23. échec lorsque la table `relations` est absente ; 24. aucun graphe partiel retourné après une erreur ; 25. échec lorsqu'une relation référence une source absente ; 26. échec lorsqu'une relation référence une cible absente ; 27. message d'erreur contenant le contexte du graphe ; 28. possibilité de recharger après un échec corrigé ; 29. deux chargements successifs retournent deux graphes indépendants ; 30. la destruction d'un graphe ne modifie pas l'autre ; 31. tous les anciens tests restent valides. ## Test d'indépendance des graphes Le test doit appeler deux fois : ```c investigation_graph_loader_load(); ``` Il doit vérifier que : - les deux pointeurs de graphe sont différents ; - les deux graphes contiennent les mêmes données métier ; - les pointeurs de `EntityRecord` correspondants sont différents ; - les pointeurs de `RelationRecord` correspondants sont différents ; - la destruction du premier graphe ne rend pas le second inutilisable. Chaque chargement doit donc reconstruire ses propres modèles depuis SQLite. ## Test d'une relation incohérente Pour tester une relation dont une entité est absente, les contraintes de clés étrangères doivent être désactivées uniquement pour la préparation du cas de test, puis réactivées avant le chargement. Le test doit : 1. créer une base temporaire valide ; 2. insérer une relation incohérente par SQL direct ; 3. réactiver les contraintes ; 4. appeler le chargeur ; 5. vérifier le retour `NULL` ; 6. vérifier `RELATION_TRANSFER` ; 7. vérifier que le message mentionne la source ou la cible absente. Cette manipulation ne doit jamais être utilisée dans le code de production. ## Makefile Ajouter : ```make INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS := \ $(TEST_CFLAGS) \ -Wpedantic TEST_INVESTIGATION_GRAPH_LOADER := \ tests/test_investigation_graph_loader ``` Ajouter une cible dédiée compilant uniquement : - `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` ; - les modules `database` nécessaires. La cible ne doit inclure aucun autre fichier `tests/test_*.c`. Ajouter `$(TEST_INVESTIGATION_GRAPH_LOADER)` : - aux dépendances de `make test` ; - aux exécutables exécutés par `make test` ; - aux fichiers supprimés par `make clean`. La compilation doit conserver : ```text -std=c17 -Wall -Wextra -Wpedantic -Werror ``` ## Hors périmètre Ce ticket ne couvre pas : - GTK ; - Cairo ; - le tableau blanc interactif ; - le chargement asynchrone ; - une barre de progression ; - l'annulation du chargement ; - les preuves associées aux relations ; - les coordonnées des nœuds ; - le zoom ; - les filtres d'affichage ; - la modification du graphe ; - la synchronisation en temps réel après une écriture ; - une transaction d'écriture ; - une migration SQLite. Le chargement asynchrone destiné à l'interface GTK fera l'objet d'un ticket séparé après validation de ce chargeur synchrone. ## Critères d'acceptation - [x] `InvestigationGraphLoader` est opaque. - [x] Le chargeur emprunte la connexion `Database`. - [x] Les DAO internes sont possédés par le chargeur. - [x] La connexion n'est jamais fermée par le chargeur. - [x] Toutes les entités sont chargées avant les relations. - [x] Les modèles sont transférés sans fuite ni double libération. - [x] Une base vide produit un graphe vide valide. - [x] Une base valide produit un graphe complet. - [x] Une incohérence relationnelle provoque un échec explicite. - [x] Aucun graphe partiel n'est retourné. - [x] Les erreurs des DAO sont contextualisées. - [x] Les erreurs du graphe sont contextualisées. - [x] Deux chargements produisent deux graphes indépendants. - [x] Le chargeur ne modifie jamais SQLite. - [x] Aucun changement GTK n'est introduit. - [x] Les tests ciblés passent. - [x] `make clean && make && make test` réussit. - [x] `git diff --check` ne retourne aucune erreur.
fy59 closed this issue 2026-07-21 08:53:55 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: fy59/labfy-investigation#59
No description provided.