-- 019_status_constraints_and_retention.sql -- -- A) Contraintes de domaine sur les statuts -- -- Dix-huit migrations, zéro CHECK. crawl_run.status, job_triggers.job_type et -- band_locations.geocode_status sont du texte libre dont les valeurs admises ne -- vivaient que dans des commentaires. C'est le dénominateur commun de plusieurs -- bugs de ce dépôt : un statut 'error' que plus personne n'écrivait mais qu'un -- bouton d'admin ciblait encore, des lignes 'processing' qu'aucune requête ne -- reprenait, un job_type réclamé par le mauvais daemon. Une faute de frappe -- écrivait une ligne durablement invisible, sans jamais rien faire échouer. -- -- Les valeurs sont relevées sur le code qui écrit réellement ces colonnes -- (crawler/src/db.py, geocoder/src/*.py, api/src/adminRoutes.js). -- -- NOT VALID est délibéré et DÉFINITIF ici : la contrainte s'applique à toutes -- les écritures futures, mais l'historique n'est pas relu. Cette migration -- s'exécute au démarrage de chaque conteneur API ; valider exigerait un scan -- complet, et une seule ligne héritée hors domaine ferait échouer la migration, -- donc le démarrage de l'API. Le but est d'arrêter la dérive, pas de réécrire -- le passé. Pour valider un jour, à froid : -- ALTER TABLE crawl_run VALIDATE CONSTRAINT crawl_run_status_check; ALTER TABLE crawl_run DROP CONSTRAINT IF EXISTS crawl_run_status_check; ALTER TABLE crawl_run ADD CONSTRAINT crawl_run_status_check CHECK (status IN ('running', 'done', 'error', 'cancelled')) NOT VALID; ALTER TABLE job_triggers DROP CONSTRAINT IF EXISTS job_triggers_status_check; ALTER TABLE job_triggers ADD CONSTRAINT job_triggers_status_check CHECK (status IN ('pending', 'running', 'done', 'error', 'cancelled')) NOT VALID; ALTER TABLE job_triggers DROP CONSTRAINT IF EXISTS job_triggers_type_check; ALTER TABLE job_triggers ADD CONSTRAINT job_triggers_type_check CHECK (job_type IN ('enrich', 'incremental', 'full_crawl', 'geocoder_enqueue')) NOT VALID; ALTER TABLE band_locations DROP CONSTRAINT IF EXISTS band_locations_geocode_status_check; ALTER TABLE band_locations ADD CONSTRAINT band_locations_geocode_status_check CHECK (geocode_status IN ( 'queued', 'processing', 'done', 'country_only', 'error', 'llm_needed', 'manual' )) NOT VALID; -- ------------------------------------------------------------------ -- B) Table morte -- -- geocode_queue date du pipeline Nominatim, remplacé par band_locations. Plus -- aucune ligne de code ne la référence ; elle ne subsistait que dans le schéma. -- ------------------------------------------------------------------ DROP TABLE IF EXISTS geocode_queue; -- ------------------------------------------------------------------ -- C) Rétention -- -- crawl_log, admin_audit_log et admin_login_attempts grossissent sans fin. La -- migration 015 avait pourtant explicitement conçu service_health pour éviter -- « une table qui grossit » — la leçon n'avait pas été appliquée aux autres. -- crawl_log prend une ligne par échec d'enrichissement, et GET /admin/api/activity -- relit admin_audit_log en entier à chaque affichage. -- -- La purge n'est pas automatique (pas de pg_cron ici) : elle est exposée par -- l'API, qui l'appelle au démarrage. Les durées sont volontairement longues, -- l'objectif est de borner la croissance, pas d'effacer l'historique utile. -- ------------------------------------------------------------------ CREATE OR REPLACE FUNCTION purge_historique( jours_logs INT DEFAULT 90, jours_audit INT DEFAULT 365, jours_tentatives INT DEFAULT 30 ) RETURNS TABLE (table_purgee TEXT, lignes BIGINT) AS $$ BEGIN RETURN QUERY WITH d AS ( DELETE FROM crawl_log WHERE created_at < now() - make_interval(days => jours_logs) RETURNING 1 ) SELECT 'crawl_log'::TEXT, count(*) FROM d; RETURN QUERY WITH d AS ( DELETE FROM admin_audit_log WHERE created_at < now() - make_interval(days => jours_audit) RETURNING 1 ) SELECT 'admin_audit_log'::TEXT, count(*) FROM d; RETURN QUERY WITH d AS ( DELETE FROM admin_login_attempts WHERE created_at < now() - make_interval(days => jours_tentatives) RETURNING 1 ) SELECT 'admin_login_attempts'::TEXT, count(*) FROM d; END; $$ LANGUAGE plpgsql; -- Index manquant : la purge et le filtre `since` de /admin/api/logs balayaient -- crawl_log en entier faute d'index sur created_at seul (celui de la 006 est -- DESC, utilisable, mais on garde l'intention explicite ici). CREATE INDEX IF NOT EXISTS idx_admin_login_attempts_created ON admin_login_attempts (created_at); CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created ON admin_audit_log (created_at);