From 142088da87da1b2c85e9d58c493975e2deb9434e Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 15 Jul 2026 23:10:24 +0200 Subject: [PATCH] feat(database): finalize database architecture v1 --- Makefile | 6 +- database/init.sql | 0 database/schema.sql | 0 database/schema_v1.sql | 1231 ++++++++++++ docs/CONVENTIONS.md | 302 +++ docs/database/SCHEMA_AUDIT_V1.md | 2909 +++++++++++++++++++++++++++++ docs/tickets/closed/TICKET-023.md | 643 +++++++ include/database/database.h | 15 +- include/database/schema.h | 34 + labfy-investigation | Bin 127528 -> 129216 bytes resources/database.gresource.xml | 8 + src/database/database.c | 77 +- src/database/schema.c | 91 + tests/test_database | Bin 26856 -> 27000 bytes tests/test_database.c | 27 +- tests/test_investigation_project | Bin 45072 -> 45120 bytes 16 files changed, 5287 insertions(+), 56 deletions(-) delete mode 100644 database/init.sql delete mode 100644 database/schema.sql create mode 100644 database/schema_v1.sql create mode 100644 docs/CONVENTIONS.md create mode 100644 docs/database/SCHEMA_AUDIT_V1.md create mode 100644 docs/tickets/closed/TICKET-023.md create mode 100644 include/database/schema.h create mode 100644 resources/database.gresource.xml create mode 100644 src/database/schema.c diff --git a/Makefile b/Makefile index 83cd829..a6a646d 100644 --- a/Makefile +++ b/Makefile @@ -62,12 +62,14 @@ $(TEST_TREE_BUILDER): \ $(TEST_PROJECT): \ tests/test_investigation_project.c \ src/core/investigation_project.c \ - src/database/database.c + src/database/database.c \ + src/database/schema.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(TEST_DATABASE): \ tests/test_database.c \ - src/database/database.c + src/database/database.c \ + src/database/schema.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 test: \ diff --git a/database/init.sql b/database/init.sql deleted file mode 100644 index e69de29..0000000 diff --git a/database/schema.sql b/database/schema.sql deleted file mode 100644 index e69de29..0000000 diff --git a/database/schema_v1.sql b/database/schema_v1.sql new file mode 100644 index 0000000..010e433 --- /dev/null +++ b/database/schema_v1.sql @@ -0,0 +1,1231 @@ +/****************************************************************************** + * Labfy Investigation + * + * Schéma SQLite officiel V1 + ******************************************************************************/ + +PRAGMA foreign_keys = ON; + +CREATE TABLE metadata +( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +CREATE TABLE investigation +( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + root_path TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE categories +( + id TEXT PRIMARY KEY, + + nom TEXT NOT NULL COLLATE NOCASE, + description TEXT, + + icone TEXT, + couleur TEXT, + + system INTEGER NOT NULL DEFAULT 0, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE (nom), + + CHECK ( + length(trim(nom)) > 0 + ), + + CHECK ( + description IS NULL + OR length(trim(description)) > 0 + ), + + CHECK ( + icone IS NULL + OR length(trim(icone)) > 0 + ), + + CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) + ), + + CHECK ( + system IN (0, 1) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_categories_status +ON categories(status); + +CREATE TABLE tags +( + id TEXT PRIMARY KEY, + + nom TEXT NOT NULL COLLATE NOCASE, + description TEXT, + couleur TEXT, + + system INTEGER NOT NULL DEFAULT 0, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE (nom), + + CHECK ( + length(trim(nom)) > 0 + ), + + CHECK ( + description IS NULL + OR length(trim(description)) > 0 + ), + + CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) + ), + + CHECK ( + system IN (0, 1) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_tags_status +ON tags(status); + +CREATE TABLE types_preuve +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +INSERT INTO types_preuve +(id, code, label) +VALUES +(1, 'screenshot', 'Capture d''écran'), +(2, 'photo', 'Photographie'), +(3, 'video', 'Vidéo'), +(4, 'document', 'Document'), +(5, 'email', 'Courrier électronique'), +(6, 'archive', 'Archive'), +(7, 'audio', 'Audio'), +(8, 'text', 'Texte'), +(9, 'other', 'Autre'); + +CREATE TABLE types_entite +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +CREATE TABLE tag_preuves +( + tag_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, preuve_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_preuves_preuve_id +ON tag_preuves(preuve_id); + +CREATE TABLE tag_recherches +( + tag_id TEXT NOT NULL, + recherche_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, recherche_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_recherches_recherche_id +ON tag_recherches(recherche_id); + +CREATE TABLE tag_entites +( + tag_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, entite_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_entites_entite_id +ON tag_entites(entite_id); + +CREATE TABLE tag_relations +( + tag_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, relation_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_relations_relation_id +ON tag_relations(relation_id); + +CREATE TABLE tag_hypotheses +( + tag_id TEXT NOT NULL, + hypothese_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, hypothese_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_hypotheses_hypothese_id +ON tag_hypotheses(hypothese_id); + +CREATE TABLE tag_chronologie +( + tag_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, chronologie_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_chronologie_chronologie_id +ON tag_chronologie(chronologie_id); + +INSERT INTO types_entite +(id, code, label) +VALUES +(1, 'email_address', 'Adresse email'), +(2, 'bank_account', 'Compte bancaire'), +(3, 'facebook_account', 'Compte Facebook'), +(4, 'instagram_account', 'Compte Instagram'), +(5, 'identity_document', 'Document d''identité'), +(6, 'iban', 'IBAN'), +(7, 'person', 'Personne'), +(8, 'pseudonym', 'Pseudonyme'), +(9, 'phone_number', 'Numéro de téléphone'), +(10, 'website', 'Site web'), +(11, 'domain_name', 'Nom de domaine'), +(12, 'ip_address', 'Adresse IP'), +(13, 'organization', 'Organisation'), +(14, 'other', 'Autre'); + +CREATE TABLE types_source +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +INSERT INTO types_source +(id, code, label) +VALUES +(1, 'website', 'Site web'), +(2, 'social_network', 'Réseau social'), +(3, 'email', 'Courrier électronique'), +(4, 'document', 'Document'), +(5, 'testimony', 'Témoignage'), +(6, 'phone_export', 'Export de téléphone'), +(7, 'disk_export', 'Export de disque'), +(8, 'osint_tool', 'Outil OSINT'), +(9, 'manual_entry', 'Saisie manuelle'), +(10, 'other', 'Autre'); + +CREATE TABLE sources +( + id TEXT PRIMARY KEY, + + type_id INTEGER NOT NULL, + + nom TEXT NOT NULL, + reference TEXT, + description TEXT, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + FOREIGN KEY (type_id) + REFERENCES types_source(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK (length(trim(nom)) > 0), + + CHECK ( + reference IS NULL + OR length(trim(reference)) > 0 + ) +); + +CREATE INDEX idx_sources_type_id +ON sources(type_id); + +CREATE INDEX idx_sources_reference +ON sources(reference); + +CREATE TABLE preuves +( + id TEXT PRIMARY KEY, + + name TEXT NOT NULL, + relative_path TEXT NOT NULL UNIQUE, + + type_id INTEGER NOT NULL, + + size_bytes INTEGER, + sha256 TEXT, + mime_type TEXT, + + description TEXT, + commentaire TEXT, + categorie_id TEXT, + + file_created_at TEXT, + imported_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + locked INTEGER NOT NULL DEFAULT 0, + + FOREIGN KEY (type_id) + REFERENCES types_preuve(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (categorie_id) + REFERENCES categories(id) + ON UPDATE CASCADE + ON DELETE SET NULL + + CHECK (length(trim(name)) > 0), + + CHECK (length(trim(relative_path)) > 0), + + CHECK ( + size_bytes IS NULL + OR size_bytes >= 0 + ), + + CHECK ( + sha256 IS NULL + OR ( + length(sha256) = 64 + AND sha256 = lower(sha256) + ) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ), + + CHECK ( + locked IN (0, 1) + ) +); + +CREATE INDEX idx_preuves_type_id +ON preuves(type_id); + +CREATE INDEX idx_preuves_status +ON preuves(status); + +CREATE INDEX idx_preuves_sha256 +ON preuves(sha256); + +CREATE INDEX idx_preuves_categorie_id +ON preuves(categorie_id); + +CREATE TABLE recherches +( + id TEXT PRIMARY KEY, + + source_id TEXT, + type_outil_id INTEGER NOT NULL, + + outil_nom TEXT NOT NULL, + requete TEXT, + resultat TEXT, + observations TEXT, + categorie_id TEXT, + + started_at TEXT NOT NULL, + completed_at TEXT, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'planned', + + FOREIGN KEY (source_id) + REFERENCES sources(id) + ON UPDATE CASCADE + ON DELETE SET NULL, + + FOREIGN KEY (type_outil_id) + REFERENCES types_outil(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (categorie_id) + REFERENCES categories(id) + ON UPDATE CASCADE + ON DELETE SET NULL, + + CHECK (length(trim(outil_nom)) > 0), + + CHECK ( + requete IS NULL + OR length(trim(requete)) > 0 + ), + + CHECK ( + completed_at IS NULL + OR completed_at >= started_at + ), + + CHECK ( + status IN ( + 'planned', + 'running', + 'completed', + 'failed', + 'cancelled', + 'archived' + ) + ) +); + +CREATE INDEX idx_recherches_source_id +ON recherches(source_id); + +CREATE INDEX idx_recherches_type_outil_id +ON recherches(type_outil_id); + +CREATE INDEX idx_recherches_status +ON recherches(status); + +CREATE INDEX idx_recherches_started_at +ON recherches(started_at); + +CREATE INDEX idx_recherches_categorie_id +ON recherches(categorie_id); + +CREATE TABLE recherche_preuves +( + recherche_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + preuve_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'input', + 'output' + ) + ) +); + +CREATE INDEX idx_recherche_preuves_preuve_id +ON recherche_preuves(preuve_id); + +CREATE TABLE entites +( + id TEXT PRIMARY KEY, + + type_id INTEGER NOT NULL, + + valeur TEXT NOT NULL, + label TEXT, + description TEXT, + + confiance INTEGER NOT NULL DEFAULT 50, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE (type_id, valeur), + + FOREIGN KEY (type_id) + REFERENCES types_entite(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK (length(trim(valeur)) > 0), + + CHECK ( + label IS NULL + OR length(trim(label)) > 0 + ), + + CHECK ( + confiance BETWEEN 0 AND 100 + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_entites_type_id +ON entites(type_id); + +CREATE INDEX idx_entites_valeur +ON entites(valeur); + +CREATE INDEX idx_entites_status +ON entites(status); + +CREATE INDEX idx_entites_confiance +ON entites(confiance); + +CREATE TABLE types_outil +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +CREATE TABLE recherche_entites +( + recherche_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + entite_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'discovered', + 'enriched', + 'validated', + 'contradicted' + ) + ) +); + +CREATE INDEX idx_recherche_entites_entite_id +ON recherche_entites(entite_id); + +CREATE TABLE preuve_entites +( + preuve_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + PRIMARY KEY ( + preuve_id, + entite_id + ), + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_preuve_entites_entite_id +ON preuve_entites(entite_id); + +CREATE TABLE relations +( + id TEXT PRIMARY KEY, + + entite_source_id TEXT NOT NULL, + entite_cible_id TEXT NOT NULL, + + type_relation TEXT NOT NULL, + label TEXT, + justification TEXT, + + confiance INTEGER NOT NULL DEFAULT 50, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE ( + entite_source_id, + entite_cible_id, + type_relation + ), + + FOREIGN KEY (entite_source_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (entite_cible_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + entite_source_id <> entite_cible_id + ), + + CHECK ( + length(trim(type_relation)) > 0 + ), + + CHECK ( + label IS NULL + OR length(trim(label)) > 0 + ), + + CHECK ( + confiance BETWEEN 0 AND 100 + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted', + 'disputed' + ) + ) +); + +CREATE INDEX idx_relations_entite_source_id +ON relations(entite_source_id); + +CREATE INDEX idx_relations_entite_cible_id +ON relations(entite_cible_id); + +CREATE INDEX idx_relations_type_relation +ON relations(type_relation); + +CREATE INDEX idx_relations_status +ON relations(status); + +CREATE INDEX idx_relations_confiance +ON relations(confiance); + +CREATE TABLE relation_preuves +( + relation_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + PRIMARY KEY ( + relation_id, + preuve_id + ), + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_relation_preuves_preuve_id +ON relation_preuves(preuve_id); + +CREATE TABLE recherche_relations +( + recherche_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + relation_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'discovered', + 'confirmed', + 'contradicted' + ) + ) +); + +CREATE INDEX idx_recherche_relations_relation_id +ON recherche_relations(relation_id); + +CREATE TABLE chronologie +( + id TEXT PRIMARY KEY, + + event_time TEXT NOT NULL, + + titre TEXT NOT NULL, + description TEXT, + + origine TEXT NOT NULL DEFAULT 'manual', + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + CHECK ( + length(trim(titre)) > 0 + ), + + CHECK ( + description IS NULL + OR length(trim(description)) > 0 + ), + + CHECK ( + origine IN ( + 'manual', + 'automatic', + 'imported' + ) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_chronologie_event_time +ON chronologie(event_time); + +CREATE INDEX idx_chronologie_origine +ON chronologie(origine); + +CREATE INDEX idx_chronologie_status +ON chronologie(status); + +CREATE TABLE recherche_chronologie +( + recherche_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + chronologie_id + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE TABLE preuve_chronologie +( + preuve_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + preuve_id, + chronologie_id + ), + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE TABLE entite_chronologie +( + entite_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + entite_id, + chronologie_id + ), + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE TABLE relation_chronologie +( + relation_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + relation_id, + chronologie_id + ), + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE INDEX idx_recherche_chronologie_chronologie_id +ON recherche_chronologie(chronologie_id); + +CREATE INDEX idx_preuve_chronologie_chronologie_id +ON preuve_chronologie(chronologie_id); + +CREATE INDEX idx_entite_chronologie_chronologie_id +ON entite_chronologie(chronologie_id); + +CREATE INDEX idx_relation_chronologie_chronologie_id +ON relation_chronologie(chronologie_id); + +CREATE TABLE journal +( + id TEXT PRIMARY KEY, + + event_time TEXT NOT NULL, + action TEXT NOT NULL, + + objet_type TEXT, + objet_id TEXT, + + resultat TEXT NOT NULL, + + details TEXT, + acteur TEXT, + + created_at TEXT NOT NULL, + + CHECK ( + length(trim(action)) > 0 + ), + + CHECK ( + objet_type IS NULL + OR length(trim(objet_type)) > 0 + ), + + CHECK ( + objet_id IS NULL + OR length(trim(objet_id)) > 0 + ), + + CHECK ( + ( + objet_type IS NULL + AND objet_id IS NULL + ) + OR + ( + objet_type IS NOT NULL + AND objet_id IS NOT NULL + ) + ), + + CHECK ( + resultat IN ( + 'success', + 'failure', + 'partial', + 'cancelled' + ) + ), + + CHECK ( + details IS NULL + OR length(trim(details)) > 0 + ), + + CHECK ( + acteur IS NULL + OR length(trim(acteur)) > 0 + ) +); + +CREATE INDEX idx_journal_event_time +ON journal(event_time); + +CREATE INDEX idx_journal_action +ON journal(action); + +CREATE INDEX idx_journal_objet +ON journal(objet_type, objet_id); + +CREATE INDEX idx_journal_resultat +ON journal(resultat); + +CREATE TABLE hypotheses +( + id TEXT PRIMARY KEY, + + titre TEXT NOT NULL, + description TEXT NOT NULL, + categorie_id TEXT, + + confiance INTEGER NOT NULL DEFAULT 50, + evaluation TEXT, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'proposed', + + FOREIGN KEY (categorie_id) + REFERENCES categories(id) + ON UPDATE CASCADE + ON DELETE SET NULL + + CHECK ( + length(trim(titre)) > 0 + ), + + CHECK ( + length(trim(description)) > 0 + ), + + CHECK ( + confiance BETWEEN 0 AND 100 + ), + + CHECK ( + evaluation IS NULL + OR length(trim(evaluation)) > 0 + ), + + CHECK ( + status IN ( + 'proposed', + 'under_review', + 'supported', + 'contradicted', + 'confirmed', + 'rejected', + 'archived' + ) + ) +); + +CREATE INDEX idx_hypotheses_status +ON hypotheses(status); + +CREATE INDEX idx_hypotheses_confiance +ON hypotheses(confiance); + +CREATE INDEX idx_hypotheses_updated_at +ON hypotheses(updated_at); + +CREATE INDEX idx_hypotheses_categorie_id +ON hypotheses(categorie_id); + +CREATE TABLE hypothese_preuves +( + hypothese_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + hypothese_id, + preuve_id, + role + ), + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'supports', + 'contradicts', + 'confirms' + ) + ) +); + +CREATE INDEX idx_hypothese_preuves_preuve_id +ON hypothese_preuves(preuve_id); + +CREATE TABLE hypothese_entites +( + hypothese_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + PRIMARY KEY ( + hypothese_id, + entite_id + ), + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_hypothese_entites_entite_id +ON hypothese_entites(entite_id); + +CREATE TABLE hypothese_relations +( + hypothese_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + hypothese_id, + relation_id, + role + ), + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'supports', + 'contradicts' + ) + ) +); + +CREATE INDEX idx_hypothese_relations_relation_id +ON hypothese_relations(relation_id); + +CREATE TABLE recherche_hypotheses +( + recherche_id TEXT NOT NULL, + hypothese_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + hypothese_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'created', + 'enriched', + 'confirmed', + 'contradicted' + ) + ) +); + +CREATE INDEX idx_recherche_hypotheses_hypothese_id +ON recherche_hypotheses(hypothese_id); + +INSERT INTO types_outil +(id, code, label) +VALUES +(1, 'web_browser', 'Navigateur web'), +(2, 'search_engine', 'Moteur de recherche'), +(3, 'dns_tool', 'Outil DNS'), +(4, 'whois_tool', 'Outil WHOIS'), +(5, 'social_network', 'Réseau social'), +(6, 'email_tool', 'Outil email'), +(7, 'metadata_tool', 'Outil de métadonnées'), +(8, 'hash_tool', 'Outil de calcul de hash'), +(9, 'ocr_tool', 'Outil OCR'), +(10, 'custom_script', 'Script personnalisé'), +(11, 'manual_process', 'Procédure manuelle'), +(12, 'other', 'Autre'); + diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md new file mode 100644 index 0000000..8ab7432 --- /dev/null +++ b/docs/CONVENTIONS.md @@ -0,0 +1,302 @@ +# Conventions de développement + +Version : 1.0 + +--- + +# Philosophie + +Labfy Investigation est développé comme un logiciel d'enquête numérique. + +Les choix d'architecture privilégient : + +- la lisibilité ; +- la maintenabilité ; +- la robustesse ; +- la traçabilité ; +- la simplicité. + +Une solution simple et cohérente est toujours préférée à une solution +complexe. + +--- + +# Langue + +Le domaine métier est écrit en français. + +Exemples : + +- preuve +- entite +- personne +- source +- chronologie +- journal +- hypothese +- recherche + +Les standards techniques conservent leur nom d'origine. + +Exemples : + +- uuid +- sha256 +- mime_type +- sqlite +- created_at +- updated_at +- relative_path + +--- + +# Architecture + +Le projet est organisé par domaine métier. + +Exemple : + +``` +preuve + ↓ +preuve.h +preuve.c + +entite + ↓ +entite.h +entite.c +``` + +Chaque module possède une responsabilité unique. + +--- + +# SQL + +Une table représente un objet métier. + +Une table importante possède : + +- une structure C ; +- un module C ; +- des tests. + +Les objets métier utilisent : + +``` +TEXT PRIMARY KEY +``` + +contenant un UUID. + +Les tables de référence utilisent : + +``` +INTEGER PRIMARY KEY +``` + +--- + +# UUID + +Tous les objets métier utilisent un UUID. + +Exemple : + +``` +preuve +entite +personne +source +chronologie +journal +``` + +Il n'existe pas simultanément : + +``` +id +uuid +``` + +La colonne : + +``` +id +``` + +contient directement l'UUID. + +--- + +# Dates + +Toutes les dates sont enregistrées en UTC. + +Format : + +``` +YYYY-MM-DDTHH:MM:SSZ +``` + +--- + +# Chemins + +Les chemins sont toujours relatifs à la racine de l'enquête. + +Autorisé : + +``` +01_Preuves_Originales/Documents/facture.pdf +``` + +Interdit : + +``` +/home/user/Documents/... +``` + +--- + +# Suppression + +Les objets métier importants ne sont jamais supprimés immédiatement. + +Une suppression est une modification d'état. + +Exemple : + +``` +active +archived +deleted +``` + +Une purge physique éventuelle devra être explicite. + +--- + +# Nommage + +Le SQL utilise : + +- snake_case +- minuscules +- aucun accent +- aucun espace + +--- + +# Code C + +Norme : + +``` +C17 +``` + +Variables : + +Toujours explicites. + +Autorisé : + +```c +relative_path +description +commentaire +created_at +``` + +Interdit : + +```c +path +desc +comm +crt +``` + +Les fonctions utilisent le nom du module. + +Exemple : + +```c +preuve_create() +preuve_load() +preuve_update() +preuve_delete() +``` + +--- + +# Structures + +Une structure représente un objet métier. + +Exemple : + +```c +typedef struct Preuve Preuve; +``` + +Les structures publiques sont opaques lorsque cela est possible. + +--- + +# Tests + +Toute nouvelle fonctionnalité importante possède un test. + +Le développement suit toujours l'ordre : + +``` +Conception + +↓ + +Implémentation + +↓ + +Tests + +↓ + +Commit +``` + +--- + +# Git + +Un ticket terminé correspond à un commit. + +Le message de commit est rédigé en anglais. + +Exemple : + +``` +feat(database): initialize investigation database +``` + +--- + +# Documentation + +Les décisions importantes sont documentées. + +Les changements de schéma SQLite sont précédés d'un audit. + +Aucune décision importante ne doit exister uniquement dans le code. + +--- + +# Objectif + +Le projet doit rester compréhensible plusieurs années après sa création. + +La priorité est donnée à la qualité du code plutôt qu'à la rapidité de +développement. diff --git a/docs/database/SCHEMA_AUDIT_V1.md b/docs/database/SCHEMA_AUDIT_V1.md new file mode 100644 index 0000000..5e3ba25 --- /dev/null +++ b/docs/database/SCHEMA_AUDIT_V1.md @@ -0,0 +1,2909 @@ +# Audit du schéma SQLite V1 + +Version : 1.0 + +Date : 2026-07-15 + +--- + +# Objectif + +Ce document analyse le schéma historique de Labfy Investigation afin de +définir le schéma métier officiel V1. + +Chaque table historique reçoit l'une des décisions suivantes : + +- `CONSERVER` +- `MODIFIER` +- `RENOMMER` +- `FUSIONNER` +- `SUPPRIMER` + +Le schéma historique sert de fondation. Les concepts utiles sont conservés, +mais les noms, contraintes et relations peuvent être corrigés. + +--- + +# Conventions générales + +## Identifiants métier + +Tous les objets métier utilisent un UUID stocké sous forme de texte : + +```sql +id TEXT PRIMARY KEY +``` + +L'UUID est généré par l'application avec : + +```c +g_uuid_string_random() +``` + +Les tables de référence utilisent des identifiants entiers. + +--- + +## Dates + +Les dates techniques sont stockées en UTC au format ISO 8601 : + +```text +YYYY-MM-DDTHH:MM:SSZ +``` + +--- + +## Chemins + +Tous les chemins enregistrés dans la base sont relatifs à la racine de +l'enquête. + +Exemple valide : + +```text +01_Preuves_Originales/Documents/facture.pdf +``` + +Exemple interdit : + +```text +/home/utilisateur/Enquetes/Test/01_Preuves_Originales/Documents/facture.pdf +``` + +--- + +## Nommage SQL + +Les noms utilisent : + +- des minuscules ; +- le `snake_case` ; +- aucun accent ; +- aucun espace. + +--- + +## Suppression logique + +Les objets importants ne sont pas supprimés immédiatement de la base. + +Un statut métier permet de conserver leur historique et leurs relations. + +Une purge physique éventuelle devra être une opération explicite et +journalisée. + +--- + +# Audit des tables + +## Table historique `preuves` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +preuves +``` + +### Justification + +Le concept est central et doit être conservé. + +La table historique doit cependant être renforcée afin de garantir : + +- l'identification unique des preuves ; +- la portabilité des chemins ; +- l'intégrité des fichiers ; +- la traçabilité de l'import ; +- la suppression logique ; +- la cohérence des types et statuts. + +--- + +## Définition métier + +Une preuve représente un élément collecté dans le cadre d'une enquête. + +Elle peut correspondre notamment à : + +- une capture d'écran ; +- une photographie ; +- une vidéo ; +- un document ; +- un email exporté ; +- une archive ; +- un fichier audio ; +- un journal technique ; +- un résultat produit par un outil. + +Le contenu d'une preuve originale ne doit jamais être modifié. + +Les annotations, conversions, extractions et autres transformations produisent +de nouveaux fichiers dans `02_Preuves_Traitees`. + +--- + +## Identifiant + +La colonne : + +```text +id +``` + +contient directement l'UUID de la preuve. + +Il n'est pas nécessaire d'avoir deux colonnes distinctes `id` et `uuid`. + +Exemple : + +```text +550e8400-e29b-41d4-a716-446655440000 +``` + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la preuve | +| `name` | `TEXT` | oui | Nom visible du fichier ou de l'élément | +| `relative_path` | `TEXT` | oui | Chemin relatif dans l'enquête | +| `type_id` | `INTEGER` | oui | Référence vers `types_preuve` | +| `source_id` | `TEXT` | non | Référence vers la source principale | +| `size_bytes` | `INTEGER` | non | Taille du fichier en octets | +| `sha256` | `TEXT` | non | Empreinte SHA-256 | +| `mime_type` | `TEXT` | non | Type MIME détecté | +| `description` | `TEXT` | non | Description synthétique | +| `comment` | `TEXT` | non | Notes détaillées | +| `file_created_at` | `TEXT` | non | Date de création connue du fichier | +| `imported_at` | `TEXT` | oui | Date d'ajout dans l'enquête | +| `updated_at` | `TEXT` | oui | Dernière modification des métadonnées | +| `status` | `TEXT` | oui | État logique de la preuve | +| `locked` | `INTEGER` | oui | Verrouillage logique, `0` ou `1` | + +--- + +## Statuts autorisés + +Première version : + +```text +active +archived +deleted +``` + +Une preuve marquée : + +```text +deleted +``` + +reste présente dans la base afin de préserver : + +- le journal ; +- les relations ; +- les références dans les rapports ; +- la traçabilité. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +UNIQUE (relative_path) +``` + +```sql +FOREIGN KEY (type_id) + REFERENCES types_preuve(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +FOREIGN KEY (source_id) + REFERENCES sources(id) + ON UPDATE CASCADE + ON DELETE SET NULL +``` + +```sql +CHECK (length(trim(name)) > 0) +``` + +```sql +CHECK (length(trim(relative_path)) > 0) +``` + +```sql +CHECK (size_bytes IS NULL OR size_bytes >= 0) +``` + +```sql +CHECK ( + sha256 IS NULL + OR ( + length(sha256) = 64 + AND sha256 = lower(sha256) + ) +) +``` + +```sql +CHECK (status IN ('active', 'archived', 'deleted')) +``` + +```sql +CHECK (locked IN (0, 1)) +``` + +--- + +## Index prévus + +La contrainte `UNIQUE` crée déjà un index sur : + +```text +relative_path +``` + +Ajouter également : + +```sql +CREATE INDEX idx_preuves_type_id +ON preuves(type_id); +``` + +```sql +CREATE INDEX idx_preuves_source_id +ON preuves(source_id); +``` + +```sql +CREATE INDEX idx_preuves_status +ON preuves(status); +``` + +```sql +CREATE INDEX idx_preuves_sha256 +ON preuves(sha256); +``` + +L'index sur `sha256` permettra notamment de détecter rapidement les fichiers +déjà présents dans l'enquête. + +--- + +## Relation avec les sources + +Une preuve peut posséder une source principale via : + +```text +source_id +``` + +Ce champ ne remplace pas nécessairement toutes les relations possibles. + +Une future table de liaison pourra être ajoutée si une preuve doit être reliée +à plusieurs sources : + +```text +preuve_sources +``` + +Pour le schéma V1, une source principale est suffisante tant que le besoin +multi-source n'est pas confirmé. + +--- + +## Relation avec les tags + +La relation entre les preuves et les tags sera stockée dans : + +```text +preuve_tags +``` + +La table historique : + +```text +preuves_tag +``` + +sera donc renommée et consolidée. + +--- + +## Structure C cible + +La structure C envisagée est : + +```c +typedef struct InvestigationEvidence InvestigationEvidence; +``` + +Sa représentation privée contiendra conceptuellement : + +```c +struct InvestigationEvidence +{ + char *id; + char *name; + char *relative_path; + + int type_id; + char *source_id; + + guint64 size_bytes; + + char *sha256; + char *mime_type; + char *description; + char *comment; + + char *file_created_at; + char *imported_at; + char *updated_at; + + char *status; + gboolean locked; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +Elle sert uniquement à vérifier que le schéma SQL correspond bien au futur +modèle C. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `preuves` ; +- adoption d'un UUID comme clé primaire ; +- utilisation exclusive d'un chemin relatif ; +- ajout de contraintes `NOT NULL` ; +- ajout de la taille en octets ; +- ajout du hash SHA-256 ; +- ajout du type MIME ; +- séparation entre date du fichier et date d'import ; +- ajout d'une date de mise à jour ; +- ajout d'un statut de suppression logique ; +- ajout du verrouillage ; +- ajout d'une relation optionnelle vers `sources` ; +- ajout d'index métier ; +- renommage futur de `preuves_tag` en `preuve_tags`. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `preuves` +``` +--- + +## Table historique `sources` + +### Décision + +```text +MODIFIER +``` +--- + +### Nom final + +```text +sources +``` +--- + +### Justification + +Le concept doit être conservé. + +Une source représente l'origine d'une information ou d'une preuve. Elle ne +représente pas la preuve elle-même. + +Exemples : + +- site web ; +- réseau social ; +- témoin ; +- téléphone ; +- disque ; +- document ; +- email ; +- outil externe. + +La table historique doit être renforcée avec : + +- un UUID ; +- un type obligatoire ; +- une référence technique générique ; +- des dates techniques ; +- des contraintes explicites. + +--- + +### Colonnes finales proposées + + Colonnes finales proposées + +|Colonne|Type|Obligatoire|Description| +|-|-|-|-| +|id|TEXT|oui|UUID de la source| +|type_id|INTEGER|oui|Référence vers types_source| +|nom|TEXT|oui|Nom visible de la source| +|reference|TEXT|non|URL, identifiant, numéro de série, Message-ID ou autre référence technique| +|description|TEXT|non|Description libre| +|created_at|TEXT|oui|Date de création dans l'enquête| +|updated_at|TEXT|oui|Dernière modification des métadonnées| + +--- + +### Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (type_id) + REFERENCES types_source(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (length(trim(nom)) > 0) +``` + +```sql +CHECK ( + reference IS NULL + OR length(trim(reference)) > 0 +) +``` +--- + +### Relation avec les preuves + +Le modèle prévu est : + +``` +source + ↓ +recherche + ↓ +preuve +``` + +Une recherche documentera : + +- la source interrogée ; +- l'outil utilisé ; +- la requête ; +- les résultats ; +- les preuves produites. + +--- + +### Index prévus + +```sql +CREATE INDEX idx_sources_type_id +ON sources(type_id); +``` + +```sql +CREATE INDEX idx_sources_reference +ON sources(reference); +``` +--- + +### Structure C cible + +```C +typedef struct Source Source; +``` + +```C +struct Source +{ + char *id; + + int type_id; + + char *nom; + char *reference; + char *description; + + char *created_at; + char *updated_at; +}; +``` +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +### Modifications par rapport au schéma historique + +- conservation du concept sources ; +- UUID comme clé primaire ; +- type obligatoire ; +- remplacement des champs trop spécialisés par reference ; +- ajout des dates techniques ; +- ajout des contraintes ; +- ajout des index ; +- absence de relation directe avec preuves. + +--- + +### Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `sources` +``` + +## Table historique `recherche` + +### Décision + +```text +RENOMMER ET MODIFIER +``` + +### Nom final + +```text +recherches +``` + +### Justification + +Le concept doit être conservé, mais la table est renommée au pluriel afin de +respecter les conventions SQL du projet. + +Une recherche représente une opération documentée réalisée au cours d'une +enquête. + +Elle peut correspondre à : + +- une requête sur un moteur de recherche ; +- une consultation de profil social ; +- une requête DNS ou WHOIS ; +- une analyse de fichier ; +- une extraction de métadonnées ; +- une recherche dans une base publique ; +- une procédure manuelle ; +- l'exécution d'un script ou d'un outil OSINT. + +La recherche conserve le contexte de l'opération afin que son résultat puisse +être compris et reproduit. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la recherche | +| `source_id` | `TEXT` | non | Source principalement interrogée | +| `type_outil_id` | `INTEGER` | oui | Catégorie de l'outil utilisé | +| `outil_nom` | `TEXT` | oui | Nom précis de l'outil ou de la procédure | +| `requete` | `TEXT` | non | Requête, commande ou opération exécutée | +| `resultat` | `TEXT` | non | Résultat synthétique | +| `observations` | `TEXT` | non | Notes et interprétations | +| `started_at` | `TEXT` | oui | Début de l'opération en UTC | +| `completed_at` | `TEXT` | non | Fin de l'opération en UTC | +| `created_at` | `TEXT` | oui | Création de l'enregistrement | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de la recherche | + +--- + +## Statuts autorisés + +```text +planned +running +completed +failed +cancelled +archived +``` + +Une recherche échouée reste enregistrée. + +Un échec peut être important pour comprendre : + +- les méthodes déjà essayées ; +- les limitations rencontrées ; +- les changements d'une cible ; +- les erreurs techniques ; +- les étapes non reproductibles. + +--- + +## Relation avec les sources + +La colonne : + +```text +source_id +``` + +est facultative. + +Certaines recherches interrogent une source identifiable : + +```text +profil Instagram +site web +export téléphonique +document +``` + +D'autres opérations n'en possèdent pas directement : + +```text +calcul de hash +OCR local +analyse de métadonnées +script interne +``` + +La suppression logique ou physique d'une source ne doit pas supprimer +l'historique des recherches. + +La relation utilisera donc : + +```sql +ON DELETE SET NULL +``` + +--- + +## Relation avec les outils + +`types_outil` représente une catégorie technique : + +```text +dns_tool +whois_tool +web_browser +ocr_tool +custom_script +``` + +La colonne : + +```text +outil_nom +``` + +contient le nom concret utilisé : + +```text +dig +Firefox +ExifTool +Tesseract +labfy-whois +``` + +Cela évite de créer immédiatement une table `outils` alors que le besoin +d'un catalogue complet d'outils n'est pas encore établi. + +--- + +## Relation avec les preuves + +Une recherche peut : + +- utiliser une preuve comme donnée d'entrée ; +- produire une preuve comme résultat. + +Une preuve peut également être liée à plusieurs recherches. + +La relation est donc plusieurs-à-plusieurs et sera stockée dans : + +```text +recherche_preuves +``` + +Cette table comportera un rôle : + +```text +input +output +``` + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (source_id) + REFERENCES sources(id) + ON UPDATE CASCADE + ON DELETE SET NULL +``` + +```sql +FOREIGN KEY (type_outil_id) + REFERENCES types_outil(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (length(trim(outil_nom)) > 0) +``` + +```sql +CHECK ( + requete IS NULL + OR length(trim(requete)) > 0 +) +``` + +```sql +CHECK ( + completed_at IS NULL + OR completed_at >= started_at +) +``` + +```sql +CHECK ( + status IN ( + 'planned', + 'running', + 'completed', + 'failed', + 'cancelled', + 'archived' + ) +) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_recherches_source_id +ON recherches(source_id); +``` + +```sql +CREATE INDEX idx_recherches_type_outil_id +ON recherches(type_outil_id); +``` + +```sql +CREATE INDEX idx_recherches_status +ON recherches(status); +``` + +```sql +CREATE INDEX idx_recherches_started_at +ON recherches(started_at); +``` + +--- + +## Structure C cible + +```c +typedef struct Recherche Recherche; +``` + +Représentation privée envisagée : + +```c +struct Recherche +{ + char *id; + char *source_id; + + int type_outil_id; + char *outil_nom; + + char *requete; + char *resultat; + char *observations; + + char *started_at; + char *completed_at; + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure ne sera pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- renommage de `recherche` en `recherches` ; +- adoption d'un UUID comme clé primaire ; +- ajout d'une source optionnelle ; +- distinction entre catégorie et nom concret de l'outil ; +- ajout du contexte de la requête ; +- ajout des résultats et observations ; +- ajout des dates de début et de fin ; +- ajout d'un statut ; +- création d'une relation plusieurs-à-plusieurs avec les preuves. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +RENOMMER EN `recherches` +MODIFIER LE SCHÉMA +``` + +## Table historique `entites` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +entites +``` + +### Justification + +Le concept est central et doit être conservé. + +Une entité représente une information identifiable découverte au cours de +l'enquête. + +Elle peut être : + +- extraite d'une preuve ; +- produite par une recherche ; +- ajoutée manuellement ; +- enrichie progressivement ; +- reliée à d'autres entités. + +La table historique doit être renforcée avec : + +- un UUID ; +- un type obligatoire ; +- une valeur canonique ; +- une valeur d'affichage ; +- un statut ; +- des dates techniques ; +- des contraintes explicites. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'entité | +| `type_id` | `INTEGER` | oui | Référence vers `types_entite` | +| `valeur` | `TEXT` | oui | Valeur canonique utilisée pour les comparaisons | +| `label` | `TEXT` | non | Valeur lisible ou nom d'affichage | +| `description` | `TEXT` | non | Description libre | +| `confiance` | `INTEGER` | oui | Niveau de confiance de 0 à 100 | +| `created_at` | `TEXT` | oui | Date de création dans l'enquête | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de l'entité | + +--- + +## Différence entre `valeur` et `label` + +La colonne : + +```text +valeur +``` + +contient la forme canonique utilisée pour les recherches et les doublons. + +Exemples : + +```text +contact@example.com ++33612345678 +arnaque_123 +example.com +192.0.2.10 +``` + +La colonne : + +```text +label +``` + +contient une forme d'affichage facultative. + +Exemples : + +```text +Adresse principale +Téléphone du vendeur +Compte Instagram suspect +Site de la société +``` + +--- + +## Niveau de confiance + +La colonne : + +```text +confiance +``` + +utilise une valeur entière comprise entre : + +```text +0 et 100 +``` + +Exemples : + +```text +100 = vérifié +80 = fortement probable +50 = à confirmer +20 = faible +0 = inconnu +``` + +La valeur par défaut est : + +```text +50 +``` + +--- + +## Statuts autorisés + +```text +active +archived +deleted +``` + +Une entité supprimée logiquement reste dans la base afin de préserver : + +- les recherches ; +- les relations ; +- les rapports ; +- la chronologie ; +- le journal. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (type_id) + REFERENCES types_entite(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (length(trim(valeur)) > 0) +``` + +```sql +CHECK ( + label IS NULL + OR length(trim(label)) > 0 +) +``` + +```sql +CHECK (confiance BETWEEN 0 AND 100) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Unicité + +Une même valeur peut exister pour plusieurs types différents. + +Exemple : + +```text +valeur = "example.com" +``` + +pourrait être interprétée comme : + +- nom de domaine ; +- site web. + +L'unicité doit donc porter sur : + +```sql +UNIQUE (type_id, valeur) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_entites_type_id +ON entites(type_id); +``` + +```sql +CREATE INDEX idx_entites_valeur +ON entites(valeur); +``` + +```sql +CREATE INDEX idx_entites_status +ON entites(status); +``` + +```sql +CREATE INDEX idx_entites_confiance +ON entites(confiance); +``` + +--- + +## Relation avec les recherches + +Une recherche peut découvrir ou enrichir plusieurs entités. + +Une entité peut être liée à plusieurs recherches. + +La relation sera stockée dans : + +```text +recherche_entites +``` + +avec un rôle : + +```text +discovered +enriched +validated +contradicted +``` + +--- + +## Relation avec les preuves + +Une entité peut être extraite d'une ou plusieurs preuves. + +Une preuve peut contenir plusieurs entités. + +Cette relation sera stockée dans : + +```text +preuve_entites +``` + +--- + +## Structure C cible + +```c +typedef struct Entite Entite; +``` + +Représentation privée envisagée : + +```c +struct Entite +{ + char *id; + + int type_id; + + char *valeur; + char *label; + char *description; + + int confiance; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `entites` ; +- UUID comme clé primaire ; +- type obligatoire ; +- séparation entre valeur canonique et label d'affichage ; +- ajout d'un niveau de confiance ; +- ajout d'un statut logique ; +- ajout des dates techniques ; +- ajout d'une contrainte d'unicité ; +- ajout de relations avec les recherches et les preuves. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `entites` +``` + +## Table historique `associations` + +### Décision + +```text +RENOMMER ET MODIFIER +``` + +### Nom final + +```text +relations +``` + +### Justification + +Le concept de liaison entre objets métier doit être conservé, mais la table +historique `associations` est trop générique. + +Une relation représente un lien documenté entre deux entités. + +Exemples : + +- une personne utilise une adresse email ; +- un pseudonyme appartient probablement à une personne ; +- un compte social utilise un numéro de téléphone ; +- un IBAN est associé à une adresse email ; +- un domaine pointe vers une adresse IP ; +- une organisation possède un site web. + +Une relation n'est pas un simple raccord technique. Elle constitue une +information d'enquête à part entière. + +Elle doit donc posséder : + +- un UUID ; +- une entité source ; +- une entité cible ; +- un type de relation ; +- un niveau de confiance ; +- une justification ; +- des dates ; +- un statut. + +--- + +## Nom et orientation + +Une relation est orientée : + +```text +entite_source + │ + ▼ +entite_cible +``` + +Exemple : + +```text +Personne + │ utilise + ▼ +Adresse email +``` + +L'orientation doit être choisie de manière cohérente lors de la création. + +La relation inverse ne doit pas être créée automatiquement dans la base. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la relation | +| `entite_source_id` | `TEXT` | oui | Entité à l'origine de la relation | +| `entite_cible_id` | `TEXT` | oui | Entité ciblée par la relation | +| `type_relation` | `TEXT` | oui | Code stable décrivant la relation | +| `label` | `TEXT` | non | Libellé d'affichage facultatif | +| `justification` | `TEXT` | non | Explication du raisonnement | +| `confiance` | `INTEGER` | oui | Niveau de confiance de 0 à 100 | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de la relation | + +--- + +## Type de relation + +La colonne : + +```text +type_relation +``` + +contient un code technique stable. + +Exemples : + +```text +uses +owns +belongs_to +linked_to +communicates_with +resolves_to +registered_with +paid_to +same_as +possibly_same_as +``` + +Le libellé humain pourra être traduit plus tard. + +Pour la V1, aucune table de référence `types_relation` n'est imposée. + +Le type reste une chaîne contrôlée par l'application. + +Une table dédiée pourra être introduite si le catalogue devient important. + +--- + +## Niveau de confiance + +La colonne : + +```text +confiance +``` + +est comprise entre : + +```text +0 et 100 +``` + +Exemples : + +```text +100 = relation vérifiée +80 = fortement probable +50 = hypothèse raisonnable +20 = faible indice +0 = inconnu +``` + +--- + +## Statuts autorisés + +```text +active +archived +deleted +disputed +``` + +Le statut : + +```text +disputed +``` + +indique que la relation est contestée ou qu'un élément contradictoire a été +découvert. + +La relation reste conservée pour préserver l'historique du raisonnement. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (entite_source_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +FOREIGN KEY (entite_cible_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (entite_source_id <> entite_cible_id) +``` + +```sql +CHECK (length(trim(type_relation)) > 0) +``` + +```sql +CHECK ( + label IS NULL + OR length(trim(label)) > 0 +) +``` + +```sql +CHECK (confiance BETWEEN 0 AND 100) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted', + 'disputed' + ) +) +``` + +--- + +## Unicité + +Un même couple d'entités peut posséder plusieurs types de relations. + +Exemple : + +```text +Personne A + owns +Compte X + +Personne A + uses +Compte X +``` + +L'unicité doit donc porter sur : + +```sql +UNIQUE ( + entite_source_id, + entite_cible_id, + type_relation +) +``` + +Cela empêche un doublon exact sans interdire plusieurs relations distinctes. + +--- + +## Relation avec les preuves + +Une relation peut être soutenue par plusieurs preuves. + +Une preuve peut soutenir plusieurs relations. + +La liaison sera stockée dans : + +```text +relation_preuves +``` + +--- + +## Relation avec les recherches + +Une relation peut être découverte, confirmée ou contredite par plusieurs +recherches. + +La liaison sera stockée dans : + +```text +recherche_relations +``` + +avec un rôle : + +```text +discovered +confirmed +contradicted +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_relations_entite_source_id +ON relations(entite_source_id); +``` + +```sql +CREATE INDEX idx_relations_entite_cible_id +ON relations(entite_cible_id); +``` + +```sql +CREATE INDEX idx_relations_type_relation +ON relations(type_relation); +``` + +```sql +CREATE INDEX idx_relations_status +ON relations(status); +``` + +```sql +CREATE INDEX idx_relations_confiance +ON relations(confiance); +``` + +--- + +## Structure C cible + +```c +typedef struct Relation Relation; +``` + +Représentation privée envisagée : + +```c +struct Relation +{ + char *id; + + char *entite_source_id; + char *entite_cible_id; + + char *type_relation; + char *label; + char *justification; + + int confiance; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- remplacement de `associations` par `relations` ; +- UUID comme clé primaire ; +- relation orientée entre deux entités ; +- ajout d'un type de relation ; +- ajout d'un niveau de confiance ; +- ajout d'une justification ; +- ajout d'un statut contestable ; +- ajout des dates techniques ; +- ajout de liaisons avec les preuves et les recherches ; +- ajout de contraintes et d'index. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +RENOMMER `associations` EN `relations` +MODIFIER PROFONDÉMENT LE SCHÉMA +``` + +## Table historique `chronologie` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +chronologie +``` + +### Justification + +Le concept doit être conservé. + +Une entrée de chronologie représente un événement important de l'enquête. + +Elle peut être : + +- saisie manuellement ; +- produite par une recherche ; +- associée à une preuve ; +- liée à une entité ; +- liée à une relation ; +- générée automatiquement par l'application. + +La chronologie ne doit pas être confondue avec le journal technique. + +La chronologie raconte les événements utiles à la compréhension de l'enquête. + +Le journal enregistre les actions réalisées dans le logiciel. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'événement | +| `event_time` | `TEXT` | oui | Date et heure de l'événement décrit | +| `titre` | `TEXT` | oui | Titre court | +| `description` | `TEXT` | non | Description détaillée | +| `origine` | `TEXT` | oui | Origine manuelle ou automatique | +| `created_at` | `TEXT` | oui | Date de création de l'enregistrement | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique | + +--- + +## Origines autorisées + +```text +manual +automatic +imported +``` + +`manual` : + +- saisie directe par l'enquêteur. + +`automatic` : + +- génération par Labfy Investigation. + +`imported` : + +- événement importé depuis une source externe. + +--- + +## Statuts autorisés + +```text +active +archived +deleted +``` + +Une entrée supprimée logiquement reste conservée afin de préserver : + +- les rapports ; +- les références ; +- les liens avec les preuves ; +- l'historique de l'enquête. + +--- + +## Relation avec les objets métier + +Une entrée de chronologie peut être liée à plusieurs objets métier. + +Les relations sont stockées dans des tables spécialisées : + +```text +recherche_chronologie +preuve_chronologie +entite_chronologie +relation_chronologie +``` + +Cette solution est préférée à un couple polymorphe : + +```text +objet_type +objet_id +``` + +car elle permet de conserver de vraies clés étrangères SQLite. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +CHECK (length(trim(titre)) > 0) +``` + +```sql +CHECK ( + description IS NULL + OR length(trim(description)) > 0 +) +``` + +```sql +CHECK ( + origine IN ( + 'manual', + 'automatic', + 'imported' + ) +) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_chronologie_event_time +ON chronologie(event_time); +``` + +```sql +CREATE INDEX idx_chronologie_origine +ON chronologie(origine); +``` + +```sql +CREATE INDEX idx_chronologie_status +ON chronologie(status); +``` + +--- + +## Structure C cible + +```c +typedef struct EvenementChronologie EvenementChronologie; +``` + +Représentation privée envisagée : + +```c +struct EvenementChronologie +{ + char *id; + + char *event_time; + + char *titre; + char *description; + + char *origine; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `chronologie` ; +- UUID comme clé primaire ; +- séparation entre date de l'événement et dates techniques ; +- ajout d'un titre obligatoire ; +- ajout d'une origine ; +- ajout d'un statut logique ; +- ajout de relations explicites vers les objets métier ; +- ajout de contraintes et d'index. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `chronologie` +``` + +## Table historique `journal` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +journal +``` + +### Justification + +Le concept doit être conservé. + +Le journal constitue la trace d'audit des actions significatives réalisées +dans Labfy Investigation. + +Il ne décrit pas directement les événements de l'enquête. + +Il décrit les opérations effectuées dans le logiciel. + +Exemples : + +- création d'une enquête ; +- import d'une preuve ; +- modification d'une entité ; +- création d'une relation ; +- changement du niveau de confiance ; +- archivage d'un objet ; +- génération d'un rapport ; +- calcul ou vérification d'un hash. + +Le journal doit permettre de répondre aux questions suivantes : + +- quelle action a été effectuée ; +- quand elle a été effectuée ; +- sur quel objet ; +- avec quel résultat ; +- avec quels détails complémentaires. + +--- + +## Différence avec `chronologie` + +La table : + +```text +chronologie +``` + +raconte les événements utiles à la compréhension de l'enquête. + +La table : + +```text +journal +``` + +raconte les actions effectuées dans l'application. + +Exemple : + +```text +Chronologie : +Le compte Instagram suspect a été découvert à 14 h 32. + +Journal : +Une entité de type compte Instagram a été créée à 14 h 34. +``` + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'entrée du journal | +| `event_time` | `TEXT` | oui | Date et heure de l'action | +| `action` | `TEXT` | oui | Code technique de l'action | +| `objet_type` | `TEXT` | non | Type de l'objet concerné | +| `objet_id` | `TEXT` | non | UUID de l'objet concerné | +| `resultat` | `TEXT` | oui | Résultat de l'action | +| `details` | `TEXT` | non | Informations complémentaires | +| `acteur` | `TEXT` | non | Utilisateur, processus ou composant ayant réalisé l'action | +| `created_at` | `TEXT` | oui | Date de création de l'enregistrement | + +--- + +## Actions prévues + +Les actions utilisent un code technique stable. + +Exemples : + +```text +create +update +archive +restore +delete +import +export +open +close +hash_compute +hash_verify +report_generate +relation_create +relation_dispute +``` + +La liste n'est pas limitée par une contrainte SQL afin de permettre +l'évolution du logiciel sans migration du schéma. + +L'application reste responsable de l'utilisation de codes cohérents. + +--- + +## Types d'objets possibles + +Exemples : + +```text +investigation +preuve +source +recherche +entite +relation +chronologie +hypothese +rapport +``` + +Une action peut ne concerner aucun objet précis. + +Exemple : + +```text +application_start +application_stop +``` + +Dans ce cas : + +```text +objet_type = NULL +objet_id = NULL +``` + +--- + +## Cohérence entre `objet_type` et `objet_id` + +Les deux colonnes doivent être : + +- toutes les deux nulles ; +- ou toutes les deux renseignées. + +Contrainte prévue : + +```sql +CHECK ( + ( + objet_type IS NULL + AND objet_id IS NULL + ) + OR + ( + objet_type IS NOT NULL + AND objet_id IS NOT NULL + ) +) +``` + +Le journal utilise volontairement une relation polymorphe. + +Aucune clé étrangère ne peut garantir automatiquement l'existence de +l'objet référencé. + +L'application doit vérifier la cohérence lors de l'écriture. + +--- + +## Résultats autorisés + +```text +success +failure +partial +cancelled +``` + +Une opération échouée doit rester enregistrée. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +CHECK (length(trim(action)) > 0) +``` + +```sql +CHECK ( + objet_type IS NULL + OR length(trim(objet_type)) > 0 +) +``` + +```sql +CHECK ( + objet_id IS NULL + OR length(trim(objet_id)) > 0 +) +``` + +```sql +CHECK ( + ( + objet_type IS NULL + AND objet_id IS NULL + ) + OR + ( + objet_type IS NOT NULL + AND objet_id IS NOT NULL + ) +) +``` + +```sql +CHECK ( + resultat IN ( + 'success', + 'failure', + 'partial', + 'cancelled' + ) +) +``` + +```sql +CHECK ( + details IS NULL + OR length(trim(details)) > 0 +) +``` + +```sql +CHECK ( + acteur IS NULL + OR length(trim(acteur)) > 0 +) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_journal_event_time +ON journal(event_time); +``` + +```sql +CREATE INDEX idx_journal_action +ON journal(action); +``` + +```sql +CREATE INDEX idx_journal_objet +ON journal(objet_type, objet_id); +``` + +```sql +CREATE INDEX idx_journal_resultat +ON journal(resultat); +``` + +--- + +## Structure C cible + +```c +typedef struct EntreeJournal EntreeJournal; +``` + +Représentation privée envisagée : + +```c +struct EntreeJournal +{ + char *id; + + char *event_time; + char *action; + + char *objet_type; + char *objet_id; + + char *resultat; + char *details; + char *acteur; + + char *created_at; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Suppression + +Les entrées du journal ne doivent pas être modifiées ni supprimées par les +fonctions métier ordinaires. + +Une éventuelle purge devra être : + +- explicite ; +- réservée à une opération administrative ; +- documentée ; +- elle-même journalisée lorsque cela est possible. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `journal` ; +- UUID comme clé primaire ; +- séparation entre action, objet, résultat et détails ; +- ajout de l'acteur ; +- ajout de contraintes de cohérence ; +- ajout d'index ; +- affirmation du caractère append-only du journal. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `journal` +``` + +## Table historique `hypotheses` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +hypotheses +``` + +### Justification + +Le concept doit être conservé. + +Une hypothèse représente une proposition de travail formulée pendant +l'enquête. + +Elle ne doit pas être confondue avec : + +- une relation ; +- une preuve ; +- une note libre ; +- une conclusion définitive. + +Une hypothèse peut évoluer à mesure que de nouveaux éléments apparaissent. + +Elle doit pouvoir être : + +- proposée ; +- étudiée ; +- soutenue ; +- contredite ; +- confirmée ; +- rejetée ; +- archivée. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'hypothèse | +| `titre` | `TEXT` | oui | Titre court | +| `description` | `TEXT` | oui | Description détaillée | +| `confiance` | `INTEGER` | oui | Niveau de confiance de 0 à 100 | +| `evaluation` | `TEXT` | non | Justification du niveau de confiance | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de l'hypothèse | + +--- + +## Niveau de confiance + +La colonne : + +```text +confiance +``` + +est comprise entre : + +```text +0 et 100 +``` + +Elle représente l'évaluation actuelle de l'enquêteur. + +Exemples : + +```text +0 = aucune confiance +25 = faible +50 = plausible +75 = fortement probable +100 = confirmé +``` + +La confiance ne doit pas être calculée automatiquement dans la V1. + +L'application pourra proposer des aides à l'évaluation plus tard, mais +l'enquêteur reste responsable de la valeur enregistrée. + +--- + +## Évaluation + +La colonne : + +```text +evaluation +``` + +explique pourquoi le niveau de confiance a été choisi. + +Exemple : + +```text +Deux preuves indépendantes, +un numéro de téléphone commun +et une relation confirmée. +``` + +Cette colonne est facultative, mais fortement recommandée lorsque la confiance +est modifiée. + +--- + +## Statuts autorisés + +```text +proposed +under_review +supported +contradicted +confirmed +rejected +archived +``` + +Signification : + +```text +proposed +``` + +Hypothèse nouvellement formulée. + +```text +under_review +``` + +Hypothèse en cours d'étude. + +```text +supported +``` + +Plusieurs éléments la soutiennent, sans confirmation définitive. + +```text +contradicted +``` + +Des éléments la contredisent. + +```text +confirmed +``` + +Hypothèse considérée comme confirmée. + +```text +rejected +``` + +Hypothèse écartée. + +```text +archived +``` + +Hypothèse conservée uniquement pour l'historique. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +CHECK (length(trim(titre)) > 0) +``` + +```sql +CHECK (length(trim(description)) > 0) +``` + +```sql +CHECK (confiance BETWEEN 0 AND 100) +``` + +```sql +CHECK ( + evaluation IS NULL + OR length(trim(evaluation)) > 0 +) +``` + +```sql +CHECK ( + status IN ( + 'proposed', + 'under_review', + 'supported', + 'contradicted', + 'confirmed', + 'rejected', + 'archived' + ) +) +``` + +--- + +## Relation avec les preuves + +Une hypothèse peut être : + +- soutenue ; +- contredite ; +- confirmée ; + +par plusieurs preuves. + +Une preuve peut intervenir dans plusieurs hypothèses. + +La relation est stockée dans : + +```text +hypothese_preuves +``` + +avec un rôle : + +```text +supports +contradicts +confirms +``` + +--- + +## Relation avec les entités + +Une hypothèse peut concerner plusieurs entités. + +La liaison est stockée dans : + +```text +hypothese_entites +``` + +--- + +## Relation avec les relations + +Une hypothèse peut s'appuyer sur des relations existantes ou les remettre en +cause. + +La liaison est stockée dans : + +```text +hypothese_relations +``` + +avec un rôle : + +```text +supports +contradicts +``` + +--- + +## Relation avec les recherches + +Une recherche peut : + +- produire une hypothèse ; +- l'enrichir ; +- la confirmer ; +- la contredire. + +La liaison est stockée dans : + +```text +recherche_hypotheses +``` + +avec un rôle : + +```text +created +enriched +confirmed +contradicted +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_hypotheses_status +ON hypotheses(status); +``` + +```sql +CREATE INDEX idx_hypotheses_confiance +ON hypotheses(confiance); +``` + +```sql +CREATE INDEX idx_hypotheses_updated_at +ON hypotheses(updated_at); +``` + +--- + +## Structure C cible + +```c +typedef struct Hypothese Hypothese; +``` + +Représentation privée envisagée : + +```c +struct Hypothese +{ + char *id; + + char *titre; + char *description; + + int confiance; + char *evaluation; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `hypotheses` ; +- UUID comme clé primaire ; +- ajout d'un titre obligatoire ; +- ajout d'une description obligatoire ; +- ajout d'un niveau de confiance ; +- ajout d'une évaluation explicative ; +- ajout d'un statut détaillé ; +- ajout des dates techniques ; +- ajout de relations vers les preuves, entités, relations et recherches ; +- ajout de contraintes et d'index. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `hypotheses` +``` + +## Nouvelle table `categories` + +### Décision + +```text +AJOUTER +``` + +### Nom final + +```text +categories +``` + +### Justification + +Le schéma historique ne distinguait pas clairement la classification +structurante des annotations libres. + +Une catégorie sert à classer un objet métier dans un domaine principal. + +Exemples : + +- OSINT ; +- finance ; +- réseaux sociaux ; +- téléphonie ; +- documents ; +- infrastructure. + +Une catégorie est stable, peu nombreuse et généralement administrée. + +Elle ne doit pas être confondue avec un tag, qui constitue une annotation +libre et potentiellement multiple. + +--- + +## Objets catégorisables dans la V1 + +Les catégories peuvent être attribuées à : + +```text +preuves +recherches +hypotheses +``` + +Chaque objet possède au maximum une catégorie. + +Les entités disposent déjà de `type_id`. + +Les relations disposent déjà de `type_relation`. + +Aucune catégorie supplémentaire n'est donc prévue pour elles dans la V1. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la catégorie | +| `nom` | `TEXT` | oui | Nom visible | +| `description` | `TEXT` | non | Description libre | +| `icone` | `TEXT` | non | Nom d'icône du thème GTK | +| `couleur` | `TEXT` | non | Couleur hexadécimale | +| `system` | `INTEGER` | oui | Catégorie fournie par l'application | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique | + +--- + +## Colonne `system` + +```text +system = 1 +``` + +indique une catégorie livrée avec Labfy Investigation. + +```text +system = 0 +``` + +indique une catégorie créée par l'utilisateur. + +L'application pourra empêcher la suppression ordinaire d'une catégorie +système. + +--- + +## Couleur + +La couleur facultative utilise le format : + +```text +#RRGGBB +``` + +Exemple : + +```text +#FF0000 +``` + +Le schéma vérifie uniquement la longueur et le préfixe. + +La validation complète des caractères hexadécimaux sera aussi effectuée dans +le code C. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +UNIQUE (nom COLLATE NOCASE) +``` + +```sql +CHECK (length(trim(nom)) > 0) +``` + +```sql +CHECK ( + description IS NULL + OR length(trim(description)) > 0 +) +``` + +```sql +CHECK ( + icone IS NULL + OR length(trim(icone)) > 0 +) +``` + +```sql +CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) +) +``` + +```sql +CHECK (system IN (0, 1)) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Structure C cible + +```c +typedef struct Categorie Categorie; +``` + +Représentation privée envisagée : + +```c +struct Categorie +{ + char *id; + + char *nom; + char *description; + char *icone; + char *couleur; + + gboolean system; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +--- + +## Décision finale + +```text +AJOUTER LA TABLE `categories` +``` + +## Table historique `tags` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +tags +``` + +### Justification + +Le concept doit être conservé. + +Un tag est une annotation libre et transversale. + +Contrairement à une catégorie : + +- plusieurs tags peuvent être appliqués au même objet ; +- les utilisateurs peuvent en créer librement ; +- ils ne structurent pas le modèle métier principal. + +Exemples : + +- urgent ; +- à vérifier ; +- escroquerie ; +- Telegram ; +- fraude ; +- prioritaire. + +--- + +## Objets pouvant recevoir des tags + +Dans la V1 : + +```text +preuves +recherches +entites +relations +hypotheses +chronologie +``` + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID du tag | +| `nom` | `TEXT` | oui | Nom visible | +| `description` | `TEXT` | non | Description libre | +| `couleur` | `TEXT` | non | Couleur hexadécimale | +| `system` | `INTEGER` | oui | Tag fourni par l'application | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique | + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +UNIQUE (nom COLLATE NOCASE) +``` + +```sql +CHECK (length(trim(nom)) > 0) +``` + +```sql +CHECK ( + description IS NULL + OR length(trim(description)) > 0 +) +``` + +```sql +CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) +) +``` + +```sql +CHECK (system IN (0, 1)) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Structure C cible + +```c +typedef struct Tag Tag; +``` + +Représentation privée envisagée : + +```c +struct Tag +{ + char *id; + + char *nom; + char *description; + char *couleur; + + gboolean system; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept ; +- UUID comme clé primaire ; +- unicité du nom sans tenir compte de la casse ; +- ajout d'une description ; +- ajout d'une couleur ; +- distinction entre tags système et utilisateur ; +- ajout des dates techniques ; +- ajout de la suppression logique ; +- ajout de tables de liaison explicites. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `tags` +``` + diff --git a/docs/tickets/closed/TICKET-023.md b/docs/tickets/closed/TICKET-023.md new file mode 100644 index 0000000..377d2f9 --- /dev/null +++ b/docs/tickets/closed/TICKET-023.md @@ -0,0 +1,643 @@ +# Ticket #023 + +## Titre + +Consolider le schéma métier initial de l'enquête. + +--- + +## Objectif + +Transformer le schéma SQLite préparé lors des premières phases du projet en +un schéma métier officiel, cohérent, documenté et testable. + +La base existante sert de fondation. + +Le travail ne consiste pas à repartir de zéro, mais à : + +- conserver les concepts métier déjà définis ; +- corriger les incohérences ; +- renforcer les contraintes ; +- uniformiser les conventions ; +- intégrer le schéma au module `Database`. + +--- + +## Base de travail + +La base historique contient notamment les tables suivantes : + +```text +preuves +entites +personnes +chronologie +sources +recherche +hypotheses +journal +tags +types_preuve +types_entite +types_source +types_outils +associations +personnes_entites +entite_tags +preuves_tag +``` + +Les tables créées au ticket #022 restent également obligatoires : + +```text +metadata +investigation +``` + +Le schéma officiel doit donc préserver ces concepts tout en les consolidant. + +--- + +## Architecture + +```text +InvestigationProject + │ + ▼ +Database + │ + ▼ +Schéma SQLite versionné + │ + ├── Métadonnées + ├── Enquête + ├── Preuves + ├── Entités + ├── Personnes + ├── Sources + ├── Chronologie + ├── Recherches + ├── Hypothèses + ├── Journal + └── Tags et relations +``` + +Le module `Database` reste le seul autorisé à exécuter du SQL. + +--- + +## Décisions de conception à officialiser + +### Identifiants + +Les entités métier utiliseront des identifiants texte de type UUID. + +Exemple : + +```text +550e8400-e29b-41d4-a716-446655440000 +``` + +Les identifiants sont générés par l'application avec : + +```c +g_uuid_string_random() +``` + +Les tables de référence simples peuvent utiliser des identifiants entiers. + +--- + +### Dates + +Les dates techniques sont stockées en UTC au format ISO 8601 : + +```text +YYYY-MM-DDTHH:MM:SSZ +``` + +Exemple : + +```text +2026-07-15T08:42:17Z +``` + +--- + +### Nommage + +Les noms SQL utilisent uniquement : + +- des minuscules ; +- du `snake_case` ; +- aucun accent ; +- aucun espace. + +Les noms des tables de liaison sont uniformisés. + +Exemple : + +```text +preuve_tags +entite_tags +personne_entites +``` + +Aucune forme singulier/pluriel incohérente ne doit subsister. + +--- + +### Clés étrangères + +Toutes les relations métier doivent utiliser des clés étrangères explicites. + +Le module `Database` doit activer : + +```sql +PRAGMA foreign_keys = ON; +``` + +à chaque ouverture de connexion. + +Les comportements `ON DELETE` doivent être décidés pour chaque relation. + +--- + +### Tables de liaison + +Chaque table de liaison doit empêcher les doublons grâce à : + +- une clé primaire composite ; +- ou une contrainte `UNIQUE`. + +Exemple : + +```sql +PRIMARY KEY (preuve_id, tag_id) +``` + +--- + +## Tables métier à consolider + +### `preuves` + +Responsabilité : + +- représenter un élément de preuve ; +- mémoriser son nom ; +- son chemin relatif ; +- son type ; +- sa description ; +- ses dates ; +- son hash éventuel ; +- son statut. + +Contraintes minimales : + +- identifiant obligatoire ; +- nom obligatoire ; +- chemin relatif obligatoire ; +- type obligatoire ; +- date de création obligatoire ; +- chemin unique au sein d'une enquête. + +--- + +### `entites` + +Responsabilité : + +- représenter une entité identifiée pendant l'enquête. + +Exemples : + +- adresse email ; +- compte bancaire ; +- profil social ; +- pseudonyme ; +- téléphone ; +- document d'identité. + +Contraintes minimales : + +- identifiant obligatoire ; +- type obligatoire ; +- valeur obligatoire ; +- date de création obligatoire. + +--- + +### `personnes` + +Responsabilité : + +- représenter une personne physique ou un profil humain étudié. + +Le modèle ne doit pas obliger à connaître l'identité civile complète. + +Une personne peut n'avoir qu'un pseudonyme ou une désignation temporaire. + +--- + +### `sources` + +Responsabilité : + +- représenter l'origine d'une information ou d'une preuve. + +Exemples : + +- site web ; +- email ; +- réseau social ; +- document ; +- témoignage ; +- outil OSINT. + +--- + +### `chronologie` + +Responsabilité : + +- mémoriser les événements importants de l'enquête ; +- relier éventuellement un événement à une preuve, une entité ou une source. + +--- + +### `recherches` + +Responsabilité : + +- documenter une opération de recherche ; +- mémoriser la requête ; +- l'outil employé ; +- la date ; +- le résultat ; +- les observations. + +--- + +### `hypotheses` + +Responsabilité : + +- enregistrer une hypothèse de travail ; +- mémoriser son statut ; +- sa justification ; +- les éléments qui la soutiennent ou la contredisent. + +--- + +### `journal` + +Responsabilité : + +- conserver la trace des actions significatives réalisées dans l'enquête. + +Exemples : + +- création d'une preuve ; +- modification d'une entité ; +- calcul d'un hash ; +- génération d'un rapport ; +- import d'un fichier. + +--- + +### `tags` + +Responsabilité : + +- permettre une classification libre des preuves et des entités. + +--- + +## Tables de référence + +Les tables suivantes doivent être consolidées : + +```text +types_preuve +types_entite +types_source +types_outils +``` + +Elles doivent comporter au minimum : + +```sql +id INTEGER PRIMARY KEY +code TEXT NOT NULL UNIQUE +label TEXT NOT NULL +description TEXT +``` + +Les valeurs initiales sont insérées lors de la création de la base. + +--- + +## Tables de liaison + +Les relations suivantes doivent être prévues : + +```text +preuve_tags +entite_tags +personne_entites +``` + +Le schéma existant contient également une table générique : + +```text +associations +``` + +Cette table doit être auditée avant conservation. + +Elle ne doit être gardée que si son rôle est clairement défini et ne fait pas +doublon avec des relations spécialisées. + +--- + +## Audit obligatoire + +Avant d'écrire le schéma définitif, chaque table de la base historique doit +être classée dans l'une des catégories suivantes : + +```text +CONSERVER +MODIFIER +RENOMMER +FUSIONNER +SUPPRIMER +``` + +La décision doit être documentée dans : + +```text +docs/database/SCHEMA_AUDIT_V1.md +``` + +Le document doit contenir au minimum : + +```text +Nom historique +Décision +Nom final +Justification +Modifications prévues +``` + +--- + +## Organisation du module Database + +Le code SQL est organisé par domaine métier. + +``` +include/ +└── database/ + ├── database.h + ├── schema.h + ├── preuve.h + ├── entite.h + ├── personne.h + ├── source.h + ├── chronologie.h + ├── recherche.h + ├── hypothese.h + ├── journal.h + ├── tag.h + ├── type_preuve.h + ├── type_entite.h + ├── type_source.h + └── type_outil.h + +src/ +└── database/ + ├── database.c + ├── schema.c + ├── preuve.c + ├── entite.c + ├── personne.c + ├── source.c + ├── chronologie.c + ├── recherche.c + ├── hypothese.c + ├── journal.c + ├── tag.c + ├── type_preuve.c + ├── type_entite.c + ├── type_source.c + └── type_outil.c + +database/ +├── schema_v1.sql +├── indexes.sql +├── triggers.sql +└── reference_data.sql +``` + +--- + +### Responsabilités + +`database.c` + +- ouverture de SQLite ; +- fermeture ; +- transactions ; +- erreurs ; +- point d'entrée du module Database. + +`schema.c` + +- installation du schéma ; +- migrations ; +- exécution des scripts SQL. + +Chaque module métier est responsable exclusivement de son objet métier. + +Exemple : + +``` +preuve.c +``` + +gère uniquement : + +- création ; +- lecture ; +- modification ; +- suppression logique ; +- gestion des tags de la preuve. + +Les tables de liaison simples ne possèdent pas de module dédié. + +Exemples : + +``` +preuve_tags +personne_entites +entite_tags +``` + +Leur manipulation est réalisée par le module métier correspondant. + +--- + +## Intégration dans `Database` + +Le module `database_initialize()` doit exécuter le schéma officiel contenu +dans : + +```text +database/schema_v1.sql +``` + +Deux approches sont acceptables : + +1. intégrer le schéma au binaire via une ressource GLib ; +2. générer une chaîne C depuis le fichier SQL lors de la compilation. + +Le logiciel installé ne doit pas dépendre d'un chemin relatif fragile vers le +dépôt source. + +--- + +## Principe d'organisation + +Une table métier importante implique systématiquement : + +- une structure C ; +- un module C ; +- des tests ; +- une documentation. + +La correspondance est la suivante : + +| SQL | C | +|-----|---| +| preuves | preuve.c | +| entites | entite.c | +| personnes | personne.c | +| sources | source.c | +| chronologie | chronologie.c | +| journal | journal.c | +| recherches | recherche.c | +| hypotheses | hypothese.c | + +--- + +## Version du schéma + +Le numéro de version reste : + +```text +1 +``` + +tant que Labfy Investigation n'a pas produit de base officiellement publiée. + +Une fois le schéma V1 considéré comme stable et distribué, toute modification +incompatible nécessitera : + +- une nouvelle version ; +- une migration dédiée ; +- une mise à jour de `schema_version`. + +--- + +## Hors périmètre + +Ce ticket ne doit pas : + +- développer les fonctions CRUD ; +- créer les structures C `Preuve`, `Entite` ou `Personne` ; +- connecter les tables à GTK ; +- importer les anciennes données automatiquement ; +- créer les migrations V2 ; +- produire un rapport ; +- ajouter la recherche plein texte. + +--- + +## Contraintes techniques + +- C17 ; +- SQLite3 ; +- GLib ; +- aucune dépendance GTK ; +- toutes les tables utilisent des contraintes explicites ; +- toutes les clés étrangères sont documentées ; +- toutes les tables de liaison empêchent les doublons ; +- aucun SQL métier hors du module `Database` ; +- aucun identifiant généré par SQLite pour les objets métier en UUID ; +- compilation sans warning. + +--- + +## Tests + +Faire évoluer : + +```text +tests/test_database.c +``` + +Les tests doivent vérifier : + +- la présence de toutes les tables officielles ; +- la présence des tables de référence ; +- l'insertion des valeurs initiales ; +- l'activation des clés étrangères ; +- le refus d'une clé étrangère invalide ; +- le refus d'un doublon dans une table de liaison ; +- le refus des colonnes obligatoires manquantes ; +- l'unicité des codes des tables de référence ; +- la valeur `schema_version = 1` ; +- l'intégrité SQLite avec : + +```sql +PRAGMA integrity_check; +``` + +--- + +## Critères d'acceptation + +- [ ] Le schéma historique a été audité. +- [ ] Chaque table possède une décision documentée. +- [ ] Un schéma V1 officiel existe. +- [ ] Les conventions de nommage sont uniformes. +- [ ] Toutes les clés primaires sont explicites. +- [ ] Toutes les clés étrangères sont explicites. +- [ ] Les relations empêchent les doublons. +- [ ] Les colonnes obligatoires utilisent `NOT NULL`. +- [ ] Les tables de référence sont initialisées. +- [ ] `database_initialize()` installe le schéma complet. +- [ ] `PRAGMA integrity_check` retourne `ok`. +- [ ] Tous les tests restent valides. +- [ ] Aucune dépendance GTK. + +--- + +## Livrables + +```text +docs/database/SCHEMA_AUDIT_V1.md +database/schema_v1.sql +include/database/database.h +src/database/database.c +tests/test_database.c +``` + +--- + +## Commit attendu + +```text +feat(database): consolidate initial investigation schema +``` diff --git a/include/database/database.h b/include/database/database.h index c254658..74301e8 100644 --- a/include/database/database.h +++ b/include/database/database.h @@ -11,19 +11,10 @@ /** * @brief Initialise la base SQLite d'une enquête. * - * La fonction crée ou ouvre le fichier SQLite indiqué, puis initialise - * transactionnellement le schéma minimal de l'enquête. + * La fonction crée ou ouvre le fichier SQLite indiqué, installe le schéma + * courant, puis enregistre les métadonnées de l'enquête. * - * Les informations suivantes sont enregistrées : - * - * - version du schéma ; - * - nom de l'application ; - * - date de création UTC ; - * - UUID de l'enquête ; - * - nom de l'enquête ; - * - chemin racine de l'enquête. - * - * Aucun handle sqlite3 n'est exposé au code appelant. + * Aucun handle SQLite n'est exposé au code appelant. * * @param database_path * Chemin complet du fichier Enquete.sqlite. diff --git a/include/database/schema.h b/include/database/schema.h new file mode 100644 index 0000000..e3f5295 --- /dev/null +++ b/include/database/schema.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * @file schema.h + * @brief Interface interne d'installation du schéma SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_SCHEMA_H +#define LABFY_INVESTIGATION_SCHEMA_H + +#include + +#include + +/** + * @brief Installe le schéma SQLite V1 dans une base ouverte. + * + * La connexion doit être valide et une transaction doit déjà être active. + * + * Cette fonction crée : + * + * - les tables ; + * - les index ; + * - les valeurs de référence. + * + * Elle ne réalise ni COMMIT ni ROLLBACK. + * + * @param database Connexion SQLite ouverte. + * + * @return true si le schéma a été correctement installé, sinon false. + */ +bool schema_install_v1( + sqlite3 *database +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index 93e2c018e10e31da0ef9ee32c33dc2e34153e5b6..e7b631749a454c3433a9f0ef6dc72602cbb72792 100755 GIT binary patch delta 19971 zcmZ{M34Baf`~N+6W|GOiCo>^3No*w~L1L*ADw+hX)Y4cYf=KLJq9`4EMWhl(UHYPE ztEj&sNP-eoVqdDYD_V4j60O&(y3GH3&bfE8=xDveM`uo{wn65tQOpBa?$Wt1o4Ylfo>~Td+BcV=2eBW;)3XUM}VGnOt`tULK)yK)u58&fLy1 z*oopX{YF#ZgCFMzqUSw>h=J?TFCvW@$- zk=L7WE5lgS%Un!k7jH`3neTXgH&;KJ2js^}YH~M`44#OK%sD#LJ(?_}Meb4L5_c-q z$3(924tnlkBBOY+EU4MJ%L-X;>mEfjEUL$2=v&$*3DZ{nI(bNn})Gn`xdFQY0| z2Z)LM%GiseeGd_zk)y_Dre|g5qz|7kJ|}a0&XnRuez)A|O}&A>XYr#Ji;>Q@v{P2v zh^LiEI;qSofLExA1>WmFA(0f3DR>FZ=vVIFjm67U7|M#&Ui|~gJ>c!vsKJc8UxHU zV7^u{vuQ?nkh!pq&i|>@b+iclmzmB_h0bz%2pD68w@bE)xkaA<(=Ni>yh6oDr)s$_ zt*03gK`#4Ls)eRR1ev4j@H|zi@pMi^5IqL{QJ)DdL+K7+o&(cY#muI+foa;%+hvZ5 zSx0>u2GLYtK2b5{vk3lV$XWR((7c zsXjPEMkx+!ebRkdyc;Pic8`y9aqF5dN!dl9+&@QOOX_ZPK|8U)Ak+M$rp7CaStIs4 zx-aR~W*?y;<+kpZIPbL$3s6nWyPM;YnE%>T;Ozm0K-q@d?e?RFvDA`0Oy2TeI%-fD z{X97;F!7n)o|u>LJTX7Pb^wwPt*~9750c}8&-LXNE(4UQlu`z5pVAIJ#*-Ynxkp>l zk>2jnniy$7&$(m)UD-2>ge>bd*>l6&T$A8$L1*_0H!bF=8ZqX4k}{3{?Vwr=3NrXmOruE=goR3Lt&D_`Xp(lHrNh-bDyKM{U zH^%Y}QiwDnffrd0^B*(hp~U=tw)}*rdGPV9EeJR|XM#UXOBqA@(&H&P=XK^-y#x7-|e48TGE^S(@7`V`JIVmT=DjICJ~ZP!v{2zXWQud{y{WzfQ2Y@;ecVJ z4ZS~LsHx8zOvj-Q-bzo+HY_uP9I<=kLc3DsU()#1~)|(ebPeZr*F`( zmsbG_6gL{wKRVFX3rr18!1F z!6VvvNE8k)6Nb#jK2SBJk#o>(?gX8bZXvhmYw3;UKkm@X^hl&*S^7J&?hZFMHp57~ zsmIVxWIgRUG*%vTiyll5peu)l$*q5OaKzIaL!%mW?Z!r3Y*{?-2GIuySB$-4+AO1q z{GXp3GMi{_MoZbh75zLT5~r;1GMdR%w>7H#_fIrzSYzJ+IEgrH2eder(*eU0L-KDh z-*cV0>dL8{L$7xB1bSdtOIgaMkA{_yAi8mQH0e$+4Oil~L5PjGgmZgk@aCOiUGWOf zLT+P*PRx%tV%D5t{=eMgXX&;Py>P_)3*`lxoY|fXpz|}^kP);jGcNQ4w|WcWFws6nvLv3KB4cA4011lXNh?~GJopR^&{ilZ+BuY@QE^- z{yefhW>rYmbNP)MbW*mzx#%)uXA|mHBAYpvY!j~A?Y3Y}HH~mkZ7Oxy6;OYr6SD*S z+JboCh3d%YtH2-OcrS(r8dfW8I~o5Pj{oHn@S5WT5s59A;}6!vzx^Zdy%;`3sfoV| zJeCyNQK+q?T}O2f+y&+E{4$blIly)cW-*Gc9Ti4y(DG3M=3g#4l5vWK!g*6Iwyzk~ zy{wwLmU@j&AzOw02dR};GbTNQn-8f^jkxQZ^TMmI$_A)r)~ z&A+C>cyHC&2GtgI%x}SMvEAdTva$)m@}aR;opCt$!#M|gQ(Gx#ugm>SHTM_Q+}^ea zwYassE$7^a5XTAJNwd7>IUyBiZL4XQ$>Xf=bN6@(+VRcYHntiQFE9hEF^K|`SdHl| zFmctGR5jdowjze)J;|btR+ICe(~f9ow`7SD*@?Fe|5x^L)$DK6K~sYL-T=Ze(@#Kw zEr1T45nZNtBEZ>{Dos^(6$<@_u6`_DKS6oa;0-(LCELyd~R^#^NdLh@r|g?oAsHL>j4vsqM3<=O_gqFET%qF8~7D) zRyL?MsWW^K?K(BK(eF`UEeL4NC8|$iLpXcF_R4zD5KLE3ZRz*hab}EZ*sf|Qr#Gfr zd@7g*KA*)#dXsv-+rV1r;4W3UhcHZj!b*&ZR7XdaA>zseTUSTUI9VMsJyjhAhE#_Q zTTo3k+Kw<8YHVqI$S?<2Cekn8bq#WM$bZ~Y^U)T5g6-ih@QQC=QS{Nf(egj1sAXCY zvV)GD7E9{U4bvK0pHNm9Nc9wkm=I1>lN((LISn#w?+dojD%e^$lzfd4pg1hWA)kdZ5F zI(lMyllWrZ;%oXjKB^7t879Bq^AU;7{Tnq!3Uds$Bel&J7`8?VCD&=pj2QBO4xf=t zb`+nQF@li6G-hTNX-HSitk=2SQ5J1Nl~@i&^RN=0+r+@EnW>wHd0O%f-J46&NU!nb z>{u|6{ywwuoAVsWz>>^6rARO%s48{vOgMrL9{3Vtwu2GL<}r4sS+k;jV;Lt-OpYVN zw{-KY$dG_na5L5$hbx{rq^LLcUS!Q3S@WU4%?ig;p@8?BShpc7_wVCafs~&>9qd|X z2EY+?2J#$GTAT9cw+>w!nEYlT|5a_SA2?Uen~?vwHf5inv`H+m1}v;iS}#b<=%m_| z52}r(fpR}q7#&@kE2EmLALH^8T*GQ}y{d6p3kVG$(Pru>QC zKyDC0Kx}QwZ9!oHgx021a7wNVdcXyg%4uM+)~n6)nUH1{ooZ7SS6h72q*gyothRW) zHl=^H#R^c^a%w1VEJ@^H6MhbY_wj5o!X4H1K7gY{WF`LW6D9EH_P_Y0&n9BJfUQ6p!?^LP#5?a zG-B>7@{q2d+k&j6XXiFe`TQHrz%A92;;$K|!gi8*#B}FG*{lwKwb@m`xxPWz#~LzU zxXY{!f2#b?K5AKD^6$o-J#71{ z+UZHO=Yn`Ll@=^81u6TWj;~la0Tl#Hdx2&_Y%fmBu1y>G7pFxqnos+hGz2%D`Y-H= zyX%yNqy5NUyPe-fJ2sS?^vuFAa{AM%g#?ezpL{Tk9H)I3y%v13j9Kqjnpp7Ke}&~q zp?tvD=$YPu=;3b`MKy9O1ydedwn}S6yt{;9`9bmo?9s@%!8AH=gEbkBReEVl$zbHX zk`y|&49kebd_7-Yu|7VJDNn3Ou<`AxBB6@If_}Ie!vbFLnz;e%hVy$lI`61$JrrYA z>JQabSk!Bt09-i^y;13GMF;*b1d;^-ge!RI8GtLig$b;o6}Fb>HgPwq=N{Kt|2rc_ zhwIVU?JmFUzHnO&qava29jqb}KcnQja@F^50Ac&*AC@jyoj0pW(kO=+YXJ_N`MFiF zf7$T}M8sTSOW)0&3f!SxmWNuepsy<3d8F{X5+ifotUFT9JKXWw`M`-N!Nz$p8!K#` z+0ZTs@MdXw`40Iu>s~ej!5Irqkq;jY70ytIDd45u(Pa<1{g=FBq?m4B+@kYiHX;iG z{$KrnwNk`}3*WgaYMt!E8KL@xB zBlqg@hbz1G(5v~){*`|h>tzT3M$Z3gPk1c)g!LL!V~MD+#T9GzGOO)n)RLda<=g&& z?hx{8YRE_I(&VL_wwk}5T%Ii_BkIxhOHFOIXtI?ln&CJ8P4F8wCG(hIZ!Rbg*-fJh zvgK}O`f))BowL;6oj6!3f|+i4%`W<+z|ZAEFdI$*XM(B6hh~>=MC)uYjruS|-XBcg z{4he^5KO0h*rpx~V@qPr@&OX4&hP@mSfOxdF}?m_1IzXr!@oBh{)u_H=MwY!9ZSsX zRo*i%fley&qvIC4#~n?m6k|T8J_h(XDJKBnn3UrQP%h;-1C%;OeHGtlW4{POK=s3R z@a#Wi4>)r=Xbwg4#f2qdubD;Yc5J7X`9<{Gr52xpYALS;73d85WNC;`dQI$VhOJi^ zVvVWB=H|izrdq$!qzddbb~`eOghd+=_0f93Y-FMPj}E?iz3@>gd|@>_{vai?(4)k5`dTcE?c1c)Dkfb-JO?kecr6=Qcj%6Is`rVNJ8@$d=G{+2^3KfKN!KqAbb7pn&F|ua%l|g+T$gAZFd`!-V_3$N z%%~~DvoptKq)&}$Ic3sVDRc7V%-qSAQRDH}aeQtjdU4#u2~(zw8a6i5G9uG5JUe4@ zR_0{Oze`&>SQ<^C8C(4$Mvb46lQDKII3WiMhc}PQu=GjkIVva9(mzJPU}ohZf;5~TLOzcw}k7gmoX*>qVKy=yWL%f zd$-kgdmF&{fKNf2wZ?AmiS{YLv49)a+U-jK(?7A>w*tNocnWYW;6uRQ0Nt@FB(JmE zBLPPN_5@tD9`b+-Hb5RQ>r=?SUFq?=f3X z0a1pL^AKaZ{;8DO`R#Y}(J-rNg1n@av1kT|myipzk*bcA+a4?`9 za3*K)tIZT?1?gDBvz(l~$F4^tbfN_`Y_C+3nv0M%=+*1zdj5Zuj>@$^qMW(q~_Vhb&rUw>L&E zq;AgaPDyHr|9BEBMHixXXzccg_U*udtUE58!lWGhf8}Gl{Vj&=;_1Cme%-@KPIh+& zBcm_J|5RGEJv7YoKX&`qwYW+hT;Bu#B|X1A6rVOc;ib=xQ1T;<*%2xSuBQL&4o=8n z=R-`jXh4S{U30>8a6`a1T7!XE%fv4Z6T3ivLU%xIF+C5pZ+=2Ymj)aBF>~{&&(2Wx zWysFZu=<#NH*2Z5>QIpb-fMIcc(2hSlvoo{UZLk9*LeemUM;zI9CH4c?e9^a;!sjR zV|bZ@asr)%@)KHA9GVe=DcGV`U`-u?eGlnoo9*^zwX{ERXnzX2gjM!#E&447-5*QE z5$dxm6d&!x>4#+v@NL)s19=fW55foZ3F=uiU@!AG zc5g)2+nDxyYb9*ABgHeZe7uL{Ag)$-$2husHRwg>>A=0gPWw^4aGuUU^*XBSzoQ?c z`WLE;zNdTl1~;ycrKAa#j!77p)pKI3%#5))(Gm1%m+6bW!9jyjEyk+Sj*S*=p6XiD z=KH#sEm#t9)RC?i*!jigIGg>~>4JTO$i(8?`yPAJ-WH3i_I-|&TzvJ!Gh)>~6wy8m zVIi^WG*&2fY3*xu+P~Fl>#-u$g>%s(^wk}rb=z}vY z$xv!O8|o8j5kwpTC$L&W8rHgyoW|FKfjiGF())Bz5_GLxNsGR32X)iP%ccqGET0;24H~ME|Hu>MDpG@d0R9Tw z_wxlg*o~2C#HCQY;D7y6D{{1W>ZKjTJW%Zsi(r$;>Q^u!ozh#-_$&3gzp7I4(2HMW zb()~!gci&g<)UoKw>c^#!|DAVf{AaGsFKx-RaNV+gpf`2;FZ8;M_WizrmSapVlGxE71AO#i<2 z7D=GJuD>Ctw8oM33BUL;j&J?)D(B)9&}0;UmjQ_qvl@T6%8;ctd`fMjjWh z_+_??&sKvTw87^YJs(7o$8_d{*YK6)-Ur=CGPMKKoJLhf$!>k=(8}f{k}j!aO#3U@ zCow(`8{%`5xQ97pP4SlxrxH0gvN+<=0KMsxR1rLTPrxp+njJ&Ej?(47O~5BJ`rq%t zcIEH=Wm!5&Uw;-sLW{GWy-Tq3-27i7a-ABUhmw1=(eqa1BbxerGQI#h`Me>Vu6o`G zyH4OA%`j)X{4tp9rECA_fW7$29}V%v&_91PAbV)Yi)L^z@x@|DU3jq$WzL`3WJU3f zKU;GLeg96=lU2$@LbAx8%4I^9l6^|HOrpsOWtB|2ke(&iWb&aiX|IfSB{8zo7-fws z=|D~@mt9FRS*b+0foZyu=|)l@>5mKzY6--_s+mz|SV7;b% z6--(Xs@w=BGs&`&f%S-oGZ|Tu9Zm|J0ynXv7B1HV5t7uHyn)f;qT62L`3iYpAQO~7 zT9QWOLnSl{&c3Q-M8Wm8%GM~doS2j@(WEW$Ri;OiNHSa57!A&S%Bg4s`MKg8LspVB zWnBz;m82-wW5^N`uZ(I%ZjysaQfp|LqAYJs7LW)fxD6Rgeo{VYLqf?z<%>3?gRFZ` z`MC{gLpCa=SkjIxRZ?O}GcsS98H;dQC_7`3tCh;nu_WBJRCT}2SBh60i6KvvE^(wW z>7iuDfvK^w3P47hPsSmW*OfmRwnT|+3$RQ{YfHkc>6jP>i5O2{$8%$^V?uO~{B%+d zMjBi3tCs?vCTSq1t!#9D11(wa%%-xtQ6^5qP;q%3g$Kn7Ec+1?JN$X_hS9JdY9Z7W zC|wQ;uVpCMf6@iFQ4-;KOW!BR%7pZIDJvP; zcr#lrGSm1T3PW3O=9u2b>g~M!m?FKt5Q)*(hp=IsR~L|Vp1|w4&?I-Pa*cf+Fris6 z<=osf6hxnysCzrNFfK*IcpXepiKt1&4X9e9nO?my7io)moiRwdz5qH_*ReCMj*0mi z^voAyJHov&;BJu^>cJJ+a1N&xy}KW8>{kry+ysS7vRf3xw1TMQ9Lz91 zUAt=-@(mD@>q3ZcBRpvnHF3g}5w;s~8=`}qr@2GR6W6&iZsW;Xu9H1FFpl#1gb8*6 zh6l=P0aD;E(Cj+RH5B-79SqAE*D6+U4M9JEi8ObzKvD5x<`C{1L1hV z(D_WmL}TOp-zM4=-~)?Lu>T_Yyta}xSs?{so>|#MV^7wrCz|^PG}YN=I)J(u9XfW) z<8cn#>sjxRv#5Kry2sy`JN3=pjIZX@)Cnbge!7D8f6Y&8%?awt=AmB9Lw5^HoS1K} zY`*E$`4$O0{;&lDVj|cLGwKaQ-E#u!W`mwZ=+usU29z_OB+h)2IP*#3%qNL6pCrzF zk~j~xVqC;K^H~xsW{E4CB~IP_)0v$&Q5f@>?%*$=+C}I1Has8-4|G(d_F=ewBRJ&| z#GASC45H)~DpEZ*GMyVZxg!epUtqV|=WTbcdOap3LnRKAh%; zLX^F-D#&BmxyUtRcoC}ZRMkL1P{AE!}1+Zl?5%Lfpzg-Cj~D#}#rR$3dms)u$g@nZvv`NOgir)I-i zG?<6qRttX+4}UGW#Fl<`dAEo3E#_?eQh_V0OmeSt8?HfnZ})r&NcqOUt=dXeyy8jAlFOaQzMv+7IBj)sd)2MITaq7!SyMFGBOj+W3?D`WK{hGYE~G)E zGn!%ENq+6X;nG-Q60R*tXES4!Up2IMYhq6F>u4sxyD976A||W5m*fU!57i7VTWDFRTZc zkc_~VL^&K!{7H}!^EL@oe(X$~d`*TL0n1=BD^Yzx9G6Vol=u6SPRiTy#Mh+d!B{sB zOWz;?%Afs7^FSVw2)jm1O2>D|hh0U~fz{CkRYzxHSQeG}<*0(SsOl+);^9^Bo5ago zgcQO;YQTb3NPql2U8wSUDp^Pdl$=N<8HCIzi5!U4l_Zo*8bqEG^0;Jl8W}8;H6<14 z88-<@n0sPvX|Q&PzWy+)Z}X*=b?= zlmjI~-e$fatdT6NK{dl-A$+%Q+UHJvnZ`P5*9a|X{(SIi(`#;zlE9v$&diI_pE-1ds- zI~rWyQexgC_1rHy5IQ#{^*vJGc}Pp0OGh+jzDF7w4_HTbGZ5WtC7a(P$0DtG(>Y;s z`nb#~Q!=tLCGCZ#goOq#DO;y!PRkswH8hw$a&l&-q`oj6J0W8P-kPSfmB}GAdGfd^ zSsDkYh_|ci`%v&pS?T;QJKbykX{R!A6ZxxNRyunJnw~RiTxNRi_{o_gnOCB%lx-#n zO3N+8tn}MLs!FoIBt=dk!jqQpbMo-0x)CV%caVC%Sxg1f*fLvnOKG^1bX3xIl0KwR z*|C!ZC6y#iU#sMOO4x3=@Tby# zH%Z6yn^n6>lv#UnsWA^?!ymdzl-poVx;{-P(a7^vGMQc*ze2?~u7lqMJj?ASrOO_8 zzFkc*W+1=VIgeTEp-f`r`7;C>n+Cr+gO5p+~i7LMC)8bKFb`mRN zm#~A1T+gv&Xb!%MTXUvU`{|3uDq$t~EqJ)^hu4VAkF%^yx}{p`FhQ^lP`Q6)c-d3Z z-V|xte^>b%_YxS*;{f(M0U?cCgl*pig6gD>+b+N(V%WwfJx)RRRRUHn=H&`jnm4PG zE7{hLtF-{qx18rHFCTMGof==;4+M)|wb6+G;6NlV6ImJ4PvuPRD>yY@Cjsv*)sqU< zl|VHmz0ah~t5m7>V+4MbO5VbCviFtVH5O`3_LwU5(GY>TfmMZFcezeIRuk4Ir*i~$ zrb@N&A1{bwr8!yUInz_{qzZsPEJEwUsGrL1#Cg=N>uSCbv|i+jS{FThFx_6zWX06%kJwzK<6jEC;3 zWo^atQ!*-W$2hbC$>_oBWHJXaQ(Uov|I4~n=r1(YeL}uLNFU_Da0Xm_{g1J!{E>Y` z!fvX0!?)>tN6~)kLPL8fDaR0R1PA8s;9`E)eIq+ ztUm>pr_$v(cI5hj!oaKQ($hxuqlbo97f5YLWC^h)D*16=!LJ@rrOhfnNj*j;sp?9( z7&*g%`4^Q-dqw|3U7`Oc$OgPgV?hs6mxSf2l0YTn1X9yLfY%rZY3?D_eZ@UM6`!HH zuDU2q6>>{Bi1||G8l{pKsz#2f_#XWPqLqq&B&6*ExGE_p;fh(A!$2zz%-wLXVSZ0m zPh&?_(cyyYT>-GFK*qdIWnQnIMuw=}QzFPmRdSuDozhjIp`K#Bn4 z1enFap3@}Asd4pvmbja%dF!X$cC{R?jNd{Ulq|l0$F08N9h6(ggg*+f_ z9UsGJAH`JZl;j6^r?p+ldqAutT5+i)QOdMR;;kfBl4!>-kZH%UPkSkgD@jXN?O>%> zly53YcloNj;`)%Z^wG|n4yYtOgma}|2`@A_Q^|Ws9=mA2E>xw=`IWSF(SB6uxpD|i zSMA;opAt;61S+OS#Nz5W=bl!&J|b~0+s_Kc+m$(wa96lVIsOPiYe%C(g*=8{ZA)9H zba_lVlXc1*0GEB=3(OV delta 17596 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 QaJy~P6~AcjR8fQf13j7yI{*Lx diff --git a/resources/database.gresource.xml b/resources/database.gresource.xml new file mode 100644 index 0000000..1f4c1a1 --- /dev/null +++ b/resources/database.gresource.xml @@ -0,0 +1,8 @@ + + + + + ../database/schema_v1.sql + + + diff --git a/src/database/database.c b/src/database/database.c index af750a8..04af0e0 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -4,6 +4,7 @@ ******************************************************************************/ #include "database/database.h" +#include "database/schema.h" #include #include @@ -18,28 +19,6 @@ */ #define DATABASE_APPLICATION_NAME "Labfy Investigation" -/** - * @brief Crée la table contenant les métadonnées de l'enquête. - */ -static const char *const database_create_metadata_table_sql = - "CREATE TABLE metadata" - "(" - " key TEXT PRIMARY KEY," - " value TEXT NOT NULL" - ");"; - -/** - * @brief Crée la table représentant l'enquête courante. - */ -static const char *const database_create_investigation_table_sql = - "CREATE TABLE investigation" - "(" - " id INTEGER PRIMARY KEY CHECK (id = 1)," - " name TEXT NOT NULL," - " root_path TEXT NOT NULL," - " created_at TEXT NOT NULL" - ");"; - /** * @brief Requête d'insertion d'une métadonnée. */ @@ -55,11 +34,13 @@ static const char *const database_insert_investigation_sql = " id," " name," " root_path," - " created_at" + " created_at," + " updated_at" ")" "VALUES" "(" - " 1," + " ?," + " ?," " ?," " ?," " ?" @@ -319,6 +300,7 @@ static bool database_insert_all_metadata( */ static bool database_insert_investigation( sqlite3 *database, + const char *investigation_uuid, const char *investigation_name, const char *investigation_root_path, const char *created_at @@ -329,6 +311,7 @@ static bool database_insert_investigation( bool success = false; if (database == NULL || + investigation_uuid == NULL || investigation_name == NULL || investigation_root_path == NULL || created_at == NULL) @@ -357,7 +340,7 @@ static bool database_insert_investigation( result = sqlite3_bind_text( statement, 1, - investigation_name, + investigation_uuid, -1, SQLITE_TRANSIENT ); @@ -370,7 +353,7 @@ static bool database_insert_investigation( result = sqlite3_bind_text( statement, 2, - investigation_root_path, + investigation_name, -1, SQLITE_TRANSIENT ); @@ -383,6 +366,32 @@ static bool database_insert_investigation( result = sqlite3_bind_text( statement, 3, + investigation_root_path, + -1, + SQLITE_TRANSIENT + ); + + if (result != SQLITE_OK) + { + goto cleanup; + } + + result = sqlite3_bind_text( + statement, + 4, + created_at, + -1, + SQLITE_TRANSIENT + ); + + if (result != SQLITE_OK) + { + goto cleanup; + } + + result = sqlite3_bind_text( + statement, + 5, created_at, -1, SQLITE_TRANSIENT @@ -530,22 +539,11 @@ bool database_initialize( transaction_started = true; - if (!database_execute_sql( - database, - database_create_metadata_table_sql - )) + if (!schema_install_v1(database)) { goto rollback; } - - if (!database_execute_sql( - database, - database_create_investigation_table_sql - )) - { - goto rollback; - } - + if (!database_insert_all_metadata( database, created_at, @@ -557,6 +555,7 @@ bool database_initialize( if (!database_insert_investigation( database, + investigation_uuid, investigation_name, investigation_root_path, created_at diff --git a/src/database/schema.c b/src/database/schema.c new file mode 100644 index 0000000..b237b9c --- /dev/null +++ b/src/database/schema.c @@ -0,0 +1,91 @@ +/****************************************************************************** + * @file schema.c + * @brief Installation du schéma SQLite de Labfy Investigation. + ******************************************************************************/ + +#include "database/schema.h" + +#include + +/** + * @brief Charge le schéma SQL V1 depuis les ressources intégrées. + * + * @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(), + * ou NULL en cas d'échec. + */ +static char *schema_load_v1_sql(void) +{ + char *schema_sql = NULL; + GError *error = NULL; + + if (!g_file_get_contents( + "database/schema_v1.sql", + &schema_sql, + NULL, + &error + )) + { + g_warning( + "Impossible de charger database/schema_v1.sql : %s", + error != NULL + ? error->message + : "erreur inconnue" + ); + + g_clear_error(&error); + + return NULL; + } + + return schema_sql; +} + +bool schema_install_v1( + sqlite3 *database +) +{ + char *schema_sql = NULL; + char *error_message = NULL; + + int result = SQLITE_ERROR; + + if (database == NULL) + { + return false; + } + + schema_sql = schema_load_v1_sql(); + + if (schema_sql == NULL) + { + return false; + } + + result = sqlite3_exec( + database, + schema_sql, + NULL, + NULL, + &error_message + ); + + g_free(schema_sql); + + if (result != SQLITE_OK) + { + g_warning( + "Impossible d'installer le schéma SQLite V1 : %s", + error_message != NULL + ? error_message + : sqlite3_errmsg(database) + ); + + sqlite3_free(error_message); + + return false; + } + + sqlite3_free(error_message); + + return true; +} diff --git a/tests/test_database b/tests/test_database index bad3911669e2822fe9f4e0cffa68ac982991e7ec..a6f4d909841d87d36b2f694cc9d5d02d56205acd 100755 GIT binary patch literal 27000 zcmeHweRLefm2XS(7sj>(M&R&aHv(aqSVHm_#E!9KdF&BLvauus3D6ozEqmh8jF=C{ zCOFYP18YTSBOqaUJd$-d%f@7R`wSr*$D0I!d@REzdt?GcaZFYu4$MS`h=hQ`I%t3Q zR#i`Tt2^uDk9W>{$90b8_PurQty{NleRNGt=}Upy^|?7Yno4=v^%`ND&KHzQnXxk@ z9{?(~>$H4)>e>Qr8t^Gh6Zw?_AXlVdo)gZHbRv`7Q!?KTFO_@>DhmmcoL?%*7dZ-Q z1p+6zsq`puHw-KgdoBLu2}GOT1OwYf;{^7OTWJ_$;GT|!k} z!>^NVe@%Itq}<)t3x#g@RauULs=P)iw@KE|4eyt73aa|u1UYJlGfgV2Hp}w-+PzD* zpMn%$l*rx-@JG$Jp}tA`#j|E{`-8_{==*M0;3vm_`_Y&G`CtC#nn<{1#j;%IDee zk5{=RK6);KoEJao_M%_mqyG*c{ZIPPZ}Op+pGd@X_H5Ob$# zvnCcvbwr&7w?<+Khbq+Hl5Q0O;b^w>&bZkbikoC!X9|8aC68||TR1cXo` zVInLfhS{Q321-XL9M$4xM{Ea0Q0vYR+C|kR8E-{ngL!K>R=T9DoaU2q#$zp?Gh35R z8u%Op={ZZNm7Z2;8)|FTRvSyomM*g%vrkLPmLtmT37v$Kf`r(gMJ|2j+312p%=9_P zc>)AA4?p|B!JUDcH`hHdoom{WX=*yzPJ zy4Ea^=>QngtTdxpJQ7{Hh6x2Wx>~YQ#ylHct+|M<+vvRRCL&TvGXstE)1OM^&Ekn@ z%11=Gu9-dsFB;5BmCfRj_-9&FO$*xS=i2C-Z1h<+`Ykp(0>nzqHu@Zks%hJ7bo>0= zX`|1z@$a?sSF?B``wJBjaBs6fh<>4X%uEhnodLbX`31=SZ_ah<>?v9{F>uSs+BeLOk;4c(Xu=ex-O6 zpRzv^{VE%MhK;_^M*siy@%Q=jKML+SH6z$F_34f1KKJ$~b0>y^dk)VS;3`ine;MS7 z;veBNe?cXl2;W9|nUM*k;ui_eA$*AOLxj`Nk?Cjr8N%}jKfw6638$eVvybt|38x_< z)5rLugws%%>0i7I#{2DM z`3vrW2m-Em_5Xx|VDHoi34CfiS%^?90~3YfITIuK3%ZDJKtk|`nSXf;fkht&yT^hr zyt^j&!gyXV=Sc9_$H@zz;19Jz!HkKK_WT9wvhrkK*Xm-(YN@47!9A;A0jC!1{&jM8 zuxIr&&?gG}K}xrxu1BVxh9Nn(4`l6y9hF*p7AJj~t>A5f4s@i-VE68$!R}OlTd-$J zaS7`Vj92sr2Lr=EG{6xF(*y?tuTtL$?%91*OBDxu3s+&54)!j;2Q37rJVc=T*dZF~ zg587Z*>Ev<`vq^l;Jrog?v%VV{1NY)r@`CXG#uX(h9VVAnX~>;TZkZ=CUrW(WDSEH1(3kLRH}^>)`J#|~mjKL7O353+-3Mn;Y={8@v2h{nRfSBgMs_G1$qOIU@Z6q*Z4@V z`={yQad@~pun!ToPej->QN{njv;^mzBc1aglu&=!M*Ka{f)SjCm0_LOmOm}HKb%Kg z#|}LtxmOZ*?`-x{Phj66kdgoIP;`(U(<{l<-GTimeZMGuug>~HLf>Jj?|-q}VEQ*7 zQt3_mp!0dk>VYV$OE17IeNR7+T>+tDzEJTksp9KW#kW{RV80cS7s3V^+vf_&lbE4V z^&d#dC!}NtN%p3wf%kt8H0=<(6i#VHPIut`J-hE0J!$|`DxC6|&~uakobny1r&w6f zr+U=a1b0+$@0Q%pO70IopehIY;Oh_@^g1=e17aW|YGaoAg)DVbmU_K#@&T!3M>gk5 zk#kVyd^MXhQ{>RxLS;OY%^4E`b5!OGXLDW`IYTn%t!&QsMGj^w*2ev=o9pqo$QhA2 z=X11i&R&s&p_XOzY)(|{O4569m66E#eG}w&y)Orl>AEt z|IsY}HwFJ)g8yT7)v-e#N&Y-x-U%|kCom+)rmiw`ktz}s>7XozX z_s%YHKuC5A$?p(P3P<#1SsNc0;$INrq6Z&4G*gOCA#pKz2KS6lBjhQGbCkDKMx6Kp0GiAuwr$@hn*63Zu8MR7@jFc^WzLJi4Zq z&c!S9p0OM`KbkZ;^)CMyuWShTiRKgqJt~x3fm~}j7IUb1g*s60^6f&+1yW6!RP+9C*`x+x(l>;* z*9eHY{|bk$`-H9qLRW#*wLxS;oBV`;`ur zZwQqugvvEiWusL2{JWOQ^p`NCrcdDYGW{qGBfV3<%*=!7&w%M5g@`P>d-phATo7qZ z^fEQBCC|kWGcLx;>C8BKEi%SL#+V~xJ~GlGLyoReV(LMdq)OOxs{Ai_JxWf8QoQ-h zLb?gBu93?@WTSA;ts>5rQe}k9=-?D--KdO|ML{-?}$V-bH5B0LryLDxwh!)sS)@T zW!4Q122QgOJF5`Y8_|+;0ERVJW&5cid*~HEFyi?twVEq?de83DTJm~u@rW`EBI*Kk z7&ZGg+PS9ts7K`rRLf^>5#^9Wo#o_G-O%G-J4H@)mDA}c=X1X0>?bcl5q6l^RkW%Y zOh0I;G_$5(=1{r8S7no>^6NI0UmDF?d9Cus%P1$<{URd1gd_eK5O*_AQ|d-r@0S9~v4(tYCbFa(FM$sRUNk0`#izU-hFttx&dPXo! z=gu}dmCf5580|7gb6^fni$%ZT>SM?R(qN?SjuG_1sb5vDFr{q5u77?!dt_ZN^JQGg4naDjKz4H0nXCQOBqcV*caxpkJe|hDB;G!fMoXM%DC- z6cL^^y%K_=QTK9RhtK@BXEJH4jQo2CMfW1^qmB_sjST;4=YA~9-S6PeqYN~5ZA~{=aob{fZ^qm6f{FIGzp^AoO7ST)6uZWW6DrUX`DG@Uf@pj_TzmF4-jJbb#n`3U7lE1@}=l2!QmUt9A)W5v~?p^4! z0(v7D1W^N!HlendtVi_|6_Kmxn zJYsl+N<-<6?WDoL2}F)|B=B?m9j9ptdj@s=&~Jl_aa*pe9`L&Qp88qNh!U<`q$J8)vp z?h{&)LTH3Gtmg0oa-Si6GkXFj@cb}Yf(S?d8^tD4O*b5a@#1&xdEyt~%jlv5jll0G z!7R-yQkBoYml}&p=`DOC%aD_0I4=B1L)QoydJC!`*5=!p)I;6B4iy(}#XgM4-6V2% zV?(44&ToSAQ)4yVVYKY9avi)*KZ_Da1A3 z5=#~#6W%FY&Y6>zNk`ymQf&1t)&*}1r7-XCud;2iLOA_$2#IA1Y0iUY?`2Aul_TQ3 zUxzXHb9_90QpN|*2J29skAMoOJCy(kgXy^tK$koYw>q}>rPPpMZ(#$=PxAC_P!sGGGv3fI z)k%$_xB(PN43yP-sF}0 zQVIaMAbuNKdGZ^jl{+{0t3=}3y4rg?w7xW;S>Kym-;|@e{|}}d$0?u56`%upiEHSS z4%~%WLJ701Ra1}p<*W7jrrKIMw`M)#&|E5#)K{<88*ZwtX$%+}Z`5#9EopY({GUJ9 z=8ZRNbTZGy!H4E78Z5whLONS$k%Y|+f!aWIqh7tSslKt~>P7nc%^T}O)^S|rrczN=Ju zp_JTHdJ891p`$Yv55;%UVM-jIi^X?Yrt;xT(!f3oMDI+QNwZ88Y5COX3?;W&B9h5y zq=S-3s!Sss4JX5V9I-^Vh$OmLFR}|2E!MqES|r?tbCi5SlWH(Y&jw?CO>Mwv3^X(v zH4Vn*z=o#Us?GW$t;0-)s7x&!-2uVy)=)AWi?T-94mhltGDRaOX6gU!<}Px7qIH|u z5i)j|@dQK{lI#B>`A}zPB;3m7vhbOf+Zs0!iETzGDat%kIY&+TysAT7(7^54W8eUv*VOV~N#Ssv7jhz%7lk*Eq^p zrB!gd;BYFAiIzB<4WU+1#9g9XtEDcX*SG^h%GsKWdoLtC1eC69tv_3SJ9UMHx0Nq+ z1{*hlJy(Pe1{${0Q3q9`qgzO`J^8Ix^6WcOQWWSZpS6Fno_&^bIKj*ZaZ4P%RwMO%P?23j5rzAoV6AH@RC>U zu}&^N^p$!tmP+UmGl6*xYZEJhUwR~QbA7(;dce7Hx>R*MO1)coz z0KHgcm^<;~iG;=J{os_(_5ty81#8XZEexu!JbEns-~p3G9IG9yU%V~xm6r`p^3>op z^$mf|je1Re<3`<@kxMWcFDBom%tiW^s@kSNgI=;`vA$-JH6PpM9D_)SjudWNOn_!T zey|OkAZLd@&P;V8w0P=WB+6nHC=SWv6E+0maWfUCWh8#nftD{$Xf;?}A(F8Q)Z0uw z62`hN64F~E15fG;ixUe46a4DUhpLi7NIIBZ{0qa;#K4nrp}J$>Ni23^(dfXFb}6U+ z_`s8@>G2B;vM6renHu;uthSuZ<~!_KXtuY9T8SUQ%bI#hHQ?+e^*0Sy9G3SyoNKSjiuSx!!s(Z-!)dSTsu{vf4 zdiX8dK5n)%vda<2!bioomKJxE7PmDP2Uix?tt@W%3|s50=fYTOM?B2UOYcFp7}~VC zYC~O>j^9L?;jK{v-70}a?#B96+SJ()LXZO;#*OGx3pcqJsv0|lc7k2BU?v-15b8@R4?9A zVTt8jlDDc4wqa*S`^RGw6Ndp`{mI0{Q-D8ybz*|PUU>iYiHTWw4=?!H#KdC2INr-^ z0hiU1_x}$!% zR{icq{`+XZ=K(GI@(x+{KL+|k*kQfXAs?~izmNQrXy?y5%HLv@e**an@LkAt^lgKx zU*6I@Vz?Bf)5yOT--_{`oXXGrt|d?Hat!%&P9Q5^!S+#+lz#b(ys3HGIRB2De#C%- z%ACsa`Ru>b37v@v3LN@@g2Hv^#n;Q)VZ;z6^?MYmCVsOvV+!_Ys7NiZV49XUFBcsK zD)MHW3j+O0%Rf*2uAIuIFGJ{e<+)v2KYfM5G7NJZIJ7x%;w5{DpX4bKjsDK2vmQ9> zfwLYs>w&W#IO~D49ysfPvmQ9>fxpZH>YfR8uY|fsLfspo*6GdSWx!guce?O^q z_!APJrOcy6J<@CewCk=B4>Fi0`2TxO{EcUhcuSHied ztmt@o;-u^z6R38MgsOa;n&J9@%&a)+AnGR#$Lkv7s-19}o$;MAA7^tISALM{|E~je zf1JDE6*3OiOSnbCb_sV#c%Ot1OZZO`J}2Q33164+EeSu6a2jp$AYCZoRT8d{aJ__E zBy5*(hlKY@_^^ckB;j)s9+B{M3Ez_N0|}?$*d&)P;Z+i@kZ`?(f(!8%5>Fedt5@nJ zc=KJPSF9+jD67zylrLFPzGBI8#pw+x9;RsTO}KL@zH+p=nsr~EWWWJEPyPj<=W4UG zE|-0mOZov9o%W)LUyUa~Io*KI9Ng@o%X*Q0l(yh62fa-7XC~rfH~vVE8lM!s8xJ|y z4O8QTqVLCpSAFSN4e6<&3L^2|7vN9vm!r+mhOQ%O_D+y@@J#%jIFn0>_rd`4VKDJ) zpJK9g_eU}4#D70+pdj(y7+{^`uax{HoU3WAlK)+DEE4aP0e(gDcS-)moU3V1NdBiU z7X;or!{d)7f4@ur|3;pdAAZIB7i#W%gJ3-Bb)n|IH>e17s#j@|D7QrbzAujGUy*d) zYeSwP`FCF-7sp0WElKw1j0%{uXrGb77bgz0H zm;4*(mH;H)I|F>jhyO#+bFiZntYUV@hnIeArXl5P>b^%MFc0(`+-#%f0Yxuk`bC<1 zyo+PQO2_O~5K^21UJHI#7vhW?edwEg=*>)@rR9_fy}U<=$G8vwy*~6Eu=9Mh%O+`* z+6#OL{JK!>_JE4oVNJzcY4LaFh;u;6u@!_WhtCv6Lf=+Rz=2PW|i+%VjeCYK)^hO_g zGt*~jQ)KYUVOleZPQT%l>n&xE*3cSHR+Po)cEFafkqm9c*SpCUTnrg)05y9@B(8kK zrHQeqNjD3&#f+_ySW75kwBZ0_!U&~yYOS%3&Ik@7wBdF@PY$|MFl>b4@z5>$=I(9+ChVkh|2dm>815TJa@X6;m3}e&gKx5;r#`>oE>c*Ol^@dTl%q8S? zn8QnMsY_0MlYbZPTI3@b>f2A$VRETU{G~mPaCm8`@Ymov#Nj1fZhf^2r}FiW>coLT zU+^m1Ri@*+Zzl&=mTvsCIly-M#L`4JTc*I~rCyaisa$*=Z1`q6jLM3Na|m>;CtpH} z@04*-rTG3D5hB0Hj>K@pXGaBFgUB|cZ4}prv3_$^UBC#`ufr{=hJgjV5eQ0RaNTCj z*igH1ZB?zYasB#+K%>!EwH7B`HDlea^;LB>)$U`!e0E6Qz$%XG(hZ}0f2TOdD!=iD zA#PFo6`tZcQP&l&beU(Rx-XNi#>^H#7rI(knEGAyYQOc4Wp-ElvuxLWql zUK+a*SBc^-RU?(aMX&yM$YNx`NiA{hDEw}}YSdwz>#kW1J{ulr%u<+Yx-%)(rG$F2PuS^M>}UN_wuMoS`L)e@0uyNK1Q=Q4S^ z(jrjnTHPqy%d9%#GzQL-+EKb*_Ka@T#faM`Z+LYSr`%<`J=QSE2B@|BL!A^wo4M|d zwbbD**U5MH^3Ae5VrW`fVpj)_&H*Ol0&Y`J(HIW8Z;htP_=4b4T;yAZj$y_-HKuM0 zCAMj0ZM&k-ARyepIr4zG12K>nHzOeukWZbFBxwxelly8J%|*zIaaGIAZF2V6)&^OH z!BC!)v@**)I!S}mRJ$apYHdv^O_{7(%vTz#R?noKY(YtEcc>%W3R`2a10Q8IIQ9`xvtFo0H3jYEg?DD#l zQ;^R>DP};IdhI@!yn5eN@NE@9QiqaPD(`p6tNm*Q)xNgkciaD2?IqkK)hXf7Jejf?sv)LruK`u`mH2?nDqO+0A)9ZLSNoTH z>p(j6$Eh#w#FLKGNeX)X@gpjgRsZeqXzYOw3Q}tKx z!Mc=p^SjK^ie&laN}+^G-aY;(c{_A<5!O&C%ikvrz#VNGZE-4l)cisF@XB@;lpiCv eiihpiz$kE4uHvPy6};qcY!)1!bP2dn?Y{sEk&)#9 literal 26856 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 diff --git a/tests/test_database.c b/tests/test_database.c index b227363..7912035 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -131,6 +131,8 @@ static void test_database_initialize_valid_database(void) char *investigation_name = NULL; char *investigation_root_path = NULL; char *investigation_created_at = NULL; + char *investigation_id = NULL; + char *investigation_count = NULL; sqlite3 *database = NULL; GError *error = NULL; @@ -214,19 +216,31 @@ static void test_database_initialize_valid_database(void) investigation_name = test_database_read_single_text( database, "SELECT name FROM investigation " - "WHERE id = 1;" + "LIMIT 1;" ); investigation_root_path = test_database_read_single_text( database, "SELECT root_path FROM investigation " - "WHERE id = 1;" + "LIMIT 1;" ); investigation_created_at = test_database_read_single_text( database, "SELECT created_at FROM investigation " - "WHERE id = 1;" + "LIMIT 1;" + ); + + investigation_id = test_database_read_single_text( + database, + "SELECT id FROM investigation " + "LIMIT 1;" + ); + + investigation_count = test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM investigation;" ); assert(strcmp(schema_version, "1") == 0); @@ -246,6 +260,11 @@ static void test_database_initialize_valid_database(void) assert(g_remove(database_path) == 0); assert(g_rmdir(temporary_directory) == 0); + assert(investigation_id[0] != '\0'); + assert(g_uuid_string_is_valid(investigation_id)); + + assert(strcmp(investigation_count, "1") == 0); + g_free(investigation_created_at); g_free(investigation_root_path); g_free(investigation_name); @@ -255,6 +274,8 @@ static void test_database_initialize_valid_database(void) g_free(schema_version); g_free(database_path); g_free(temporary_directory); + g_free(investigation_id); + g_free(investigation_count); } /** diff --git a/tests/test_investigation_project b/tests/test_investigation_project index 6bca42ee51eb845c72aec687ce4f7186e6d07189..18cee8edc37fe77fd96bf664654544a4e73493b0 100755 GIT binary patch delta 10250 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 delta 9804 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 LqOks|cXs+e