db : amorce la zone or, hypertable « mesure » et compression au-delà de 7 jours #106

Merged
lenaic merged 2 commits from olivier/29-hypertables-compression into develop 2026-09-03 09:28:07 +00:00
Member

Amorce la zone or pour débloquer le ticket #29.

Le problème. #29 demande de convertir les tables de mesures en hypertables et d'y poser une compression. Il n'y avait rien à convertir : enervision_prod et enervision_preprod comptaient zéro table dans public, seuls les catalogues internes de TimescaleDB étaient présents. C'est le constat que fait aussi #105.

Ce que fait cette migration. Le strict nécessaire pour que #29 ait un objet, pas plus :

  • site, le référentiel des sept sites, peuplé depuis docs/GLOSSAIRE.md ;
  • mesure, convertie en hypertable de fragments d'un jour ;
  • une politique de compression au-delà de sept jours, segmentée par site.

Les cinq autres tables de la zone or (prevision, alerte, qualite_jour, recommandation, acces_site) restent au #105.

Le modèle est celui du dossier d'architecture collectif, et non la forme esquissée dans docs/POSTGRESQL.md. Celle-ci est écartée pour trois raisons, consignées dans le document corrigé : sa clé capteur_id n'est pas celle des zones bronze et argent (donc pas d'auditabilité sans table de correspondance) ; sa colonne de valeur unique ne peut pas porter la brute à côté de la retenue, ce que l'ADR 0006 exige ; et son data_quality par défaut à 'ok' n'appartient à aucune des deux énumérations de qualité retenues.

Relevé après application sur le serveur

enervision_prod enervision_preprod
site 7 lignes 7 lignes
mesure hypertable, 0 ligne 352 807 lignes, 35 jours au pas de la minute
Fragments 0 36, comprimés 28/36
Compression politique > 7 j 35 Mo → 5 488 kio, facteur 6,5

La production reste vide volontairement : la conversion d'une table remplie est longue, elle passe donc avant le chargement de la collecte (#33). Le jeu de test vit en préproduction seule.

Latence, agrégat horaire sur 24 h et les 7 sites, 20 exécutions : médiane 3,67 ms, p95 4,09 ms, max 10,31 ms — cible 400 ms. Une fenêtre de 24 h prise dans les fragments comprimés reste à 4,9 ms. Le plan montre ChunkAppend ne touchant que 2 fragments sur 36.

Un écart d'installation corrigé au passage. pg_default_acl était vide sur enervision_preprod alors que enervision_prod portait bien grafana=r sur public : les mêmes tables auraient été lisibles par Grafana en production et muettes en préproduction. La clause manquante a été posée sur le serveur, et le grant select est explicite dans la migration pour ne plus dépendre d'elle.

La migration se rejoue sans effet de bord — vérifié en transaction annulée sur préproduction, hypertable déjà comprimée : uniquement des NOTICE.

Amorce la zone or pour débloquer le ticket #29. **Le problème.** #29 demande de convertir les tables de mesures en hypertables et d'y poser une compression. Il n'y avait rien à convertir : `enervision_prod` et `enervision_preprod` comptaient zéro table dans `public`, seuls les catalogues internes de TimescaleDB étaient présents. C'est le constat que fait aussi #105. **Ce que fait cette migration.** Le strict nécessaire pour que #29 ait un objet, pas plus : - `site`, le référentiel des sept sites, peuplé depuis `docs/GLOSSAIRE.md` ; - `mesure`, convertie en hypertable de fragments d'un jour ; - une politique de compression au-delà de sept jours, segmentée par site. Les cinq autres tables de la zone or (`prevision`, `alerte`, `qualite_jour`, `recommandation`, `acces_site`) **restent au #105**. **Le modèle est celui du dossier d'architecture collectif**, et non la forme esquissée dans `docs/POSTGRESQL.md`. Celle-ci est écartée pour trois raisons, consignées dans le document corrigé : sa clé `capteur_id` n'est pas celle des zones bronze et argent (donc pas d'auditabilité sans table de correspondance) ; sa colonne de valeur unique ne peut pas porter la brute à côté de la retenue, ce que l'ADR 0006 exige ; et son `data_quality` par défaut à `'ok'` n'appartient à aucune des deux énumérations de qualité retenues. **Relevé après application sur le serveur** | | `enervision_prod` | `enervision_preprod` | |---|---|---| | `site` | 7 lignes | 7 lignes | | `mesure` | hypertable, **0 ligne** | 352 807 lignes, 35 jours au pas de la minute | | Fragments | 0 | 36, comprimés 28/36 | | Compression | politique > 7 j | 35 Mo → 5 488 kio, **facteur 6,5** | La production reste **vide volontairement** : la conversion d'une table remplie est longue, elle passe donc avant le chargement de la collecte (#33). Le jeu de test vit en préproduction seule. **Latence**, agrégat horaire sur 24 h et les 7 sites, 20 exécutions : médiane **3,67 ms**, p95 **4,09 ms**, max 10,31 ms — cible 400 ms. Une fenêtre de 24 h prise *dans* les fragments comprimés reste à 4,9 ms. Le plan montre `ChunkAppend` ne touchant que 2 fragments sur 36. **Un écart d'installation corrigé au passage.** `pg_default_acl` était vide sur `enervision_preprod` alors que `enervision_prod` portait bien `grafana=r` sur `public` : les mêmes tables auraient été lisibles par Grafana en production et muettes en préproduction. La clause manquante a été posée sur le serveur, et le `grant select` est explicite dans la migration pour ne plus dépendre d'elle. La migration se rejoue sans effet de bord — vérifié en transaction annulée sur préproduction, hypertable déjà comprimée : uniquement des `NOTICE`.
db : amorce la zone or, hypertable « mesure » et compression au-delà de 7 jours
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 45s
Intégration / Tests unitaires et couverture (pull_request) Successful in 53s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 9s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m16s
741e1948a6
Le ticket #29 demandait de convertir les tables de mesures en hypertables et
d'y poser une compression. Il n'y avait rien à convertir : les deux bases
comptaient zéro table dans « public », seuls les catalogues internes de
TimescaleDB étaient là. Le #105 le constate d'ailleurs explicitement.

Cette migration pose donc le strict nécessaire pour que #29 ait un objet :
« site », le référentiel des sept sites du GLOSSAIRE, et « mesure » convertie
en hypertable de fragments d'un jour, comprimée au-delà de sept jours. Les
cinq autres tables de la zone or restent au #105.

Le modèle suit le dossier d'architecture collectif, et non la forme esquissée
dans docs/POSTGRESQL.md : cette dernière portait une clé « capteur_id » qui
n'est pas celle des zones bronze et argent, une seule colonne de valeur là où
l'ADR 0006 exige la brute à côté de la retenue, et un « data_quality » par
défaut à « ok » qui n'appartient à aucune des deux énumérations retenues. Le
document est corrigé, avec les motifs de l'écart.

La conversion passe avant le chargement : la production reste vide, elle
attend la collecte du #33. Le jeu de test d'un mois vit en préproduction.

Le « grant select » est explicite plutôt que laissé à la clause de droits par
défaut du schéma : cette clause existait sur enervision_prod mais pas sur
enervision_preprod, où pg_default_acl était vide. Les mêmes tables auraient
été lisibles par Grafana en production et muettes en préproduction. La clause
manquante a été posée sur le serveur en plus de ce grant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
marvin requested review from marvin 2026-09-03 09:09:47 +00:00
marvin approved these changes 2026-09-03 09:10:30 +00:00
Merge branch 'develop' into olivier/29-hypertables-compression
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 47s
Intégration / Tests unitaires et couverture (pull_request) Successful in 1m3s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 8s
Intégration / Images épinglées par version (pull_request) Successful in 2s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m21s
45ff0101b6
marvin requested review from lenaic 2026-09-03 09:16:25 +00:00
lenaic merged commit da87c92aaa into develop 2026-09-03 09:28:07 +00:00
lenaic deleted branch olivier/29-hypertables-compression 2026-09-03 09:28:07 +00:00
Author
Member

Le lot complet de la zone or est ouvert en #110 (ticket #105). Il ne défait rien ici : la 0007 reste la première pose, et le lot passe au-dessus en 0008 à 0015. Les deux demandes fusionnent dans n'importe quel ordre — les fichiers 0008 et 0010 créent la forme complète sur une base neuve et complètent une table déjà posée.

Trois choses relevées depuis, dont deux qui touchent cette demande.

1. Sur les deux bases, site et mesure appartiennent à postgres — et sur la production, le schéma ops et ops.schema_migrations aussi. La 0007 y a été appliquée en superutilisateur. Le grant select on site, mesure to grafana explicite sauve la lecture pour ces deux tables, et c'était bien vu ; mais deux effets restent :

  • le rôle applicatif ne peut plus modifier ces tables — les alter table de mon lot échouent sur un refus de droits tant que la propriété n'est pas reprise ;
  • la clause ALTER DEFAULT PRIVILEGES FOR ROLE enervision_prod ne couvre que les objets créés par ce rôle : la prochaine table posée en postgres sans grant explicite serait muette pour Grafana, sans message.

La reprise (alter ... owner to) est dans db/migrations/README.md de #110, et le test d'intégration vérifie désormais le propriétaire.

Sur l'argument du grant explicite : il tient, mais son motif — « la clause existe sur enervision_prod mais pas sur enervision_preprod » — est plus faible qu'il n'y paraît. grafana n'a pas CONNECT sur enervision_preprod (10-roles-bases-droits.sh ne l'accorde que sur la production) : il ne lira jamais la préproduction, l'asymétrie est donc sans conséquence. Ça ne rend pas le grant nuisible, seulement facultatif.

2. La méthode d'imputation. check (methode_imputation in ('interpolated', 'forward_fill', 'none')) avec none par défaut fait porter à none deux sens distincts — « valeur mesurée » et « trou assumé ». Le §9 de docs/data/etl-pipeline.md et l'ADR 0006 nomment measured le régime de la valeur présente et valide. Les confondre interdit de compter les valeurs réellement mesurées, ce que la répartition par méthode de qualite_jour demande. La 0010 de #110 élargit donc la contrainte à quatre valeurs et remet le défaut à measured. Dis-moi si tu vois une raison de garder trois régimes, je la retirerai.

3. Pour information : les six migrations d'authentification n'ont jamais été jouées sur enervision_prodops n'y contient que schema_migrations. Elles passeront avec #110, dont la 0015 référence ops.users.

Rien à changer ici de mon point de vue. Je relis la #110 avec toi si tu veux, et l'inverse.

Le lot complet de la zone or est ouvert en **#110** (ticket #105). Il **ne défait rien ici** : la `0007` reste la première pose, et le lot passe au-dessus en `0008` à `0015`. Les deux demandes fusionnent dans n'importe quel ordre — les fichiers `0008` et `0010` créent la forme complète sur une base neuve *et* complètent une table déjà posée. Trois choses relevées depuis, dont deux qui touchent cette demande. **1. Sur les deux bases, `site` et `mesure` appartiennent à `postgres`** — et sur la production, le schéma `ops` et `ops.schema_migrations` aussi. La `0007` y a été appliquée en superutilisateur. Le `grant select on site, mesure to grafana` explicite sauve la lecture pour ces deux tables, et c'était bien vu ; mais deux effets restent : - le rôle applicatif ne peut plus **modifier** ces tables — les `alter table` de mon lot échouent sur un refus de droits tant que la propriété n'est pas reprise ; - la clause `ALTER DEFAULT PRIVILEGES FOR ROLE enervision_prod` ne couvre que les objets créés **par** ce rôle : la prochaine table posée en `postgres` sans `grant` explicite serait muette pour Grafana, sans message. La reprise (`alter ... owner to`) est dans `db/migrations/README.md` de #110, et le test d'intégration vérifie désormais le propriétaire. Sur l'argument du `grant` explicite : il tient, mais son motif — « la clause existe sur `enervision_prod` mais pas sur `enervision_preprod` » — est plus faible qu'il n'y paraît. `grafana` n'a **pas** `CONNECT` sur `enervision_preprod` (`10-roles-bases-droits.sh` ne l'accorde que sur la production) : il ne lira jamais la préproduction, l'asymétrie est donc sans conséquence. Ça ne rend pas le `grant` nuisible, seulement facultatif. **2. La méthode d'imputation.** `check (methode_imputation in ('interpolated', 'forward_fill', 'none'))` avec `none` par défaut fait porter à `none` deux sens distincts — « valeur mesurée » et « trou assumé ». Le §9 de `docs/data/etl-pipeline.md` et l'ADR 0006 nomment `measured` le régime de la valeur présente et valide. Les confondre interdit de compter les valeurs réellement mesurées, ce que la répartition par méthode de `qualite_jour` demande. La `0010` de #110 élargit donc la contrainte à quatre valeurs et remet le défaut à `measured`. Dis-moi si tu vois une raison de garder trois régimes, je la retirerai. **3. Pour information** : les six migrations d'authentification n'ont jamais été jouées sur `enervision_prod` — `ops` n'y contient que `schema_migrations`. Elles passeront avec #110, dont la `0015` référence `ops.users`. Rien à changer ici de mon point de vue. Je relis la #110 avec toi si tu veux, et l'inverse.
lenaic left a comment

Relu et vérifié sur le serveur, tout ce qui est annoncé est exact : 7 sites des deux côtés, 0 mesure en production, 352 807 en préproduction, 36 fragments dont 28 comprimés, politique à 7 jours, grafana lit site et mesure dans les deux bases.

J'ai poussé deux vérifications de plus. La chaîne complète 0001 à 0007 rejouée dans l'ordre sur une base neuve passe sans une erreur, extension comprise. Et 0007 rejoué sur une base où il est déjà appliqué est idempotent, cinq avis, aucune erreur.

Le choix de garder la production vide et de convertir avant le chargement est le bon, et le motif est écrit. Les trois raisons d'écarter l'ancienne forme sont justes, celle sur la clé commune du bronze à l'or particulièrement. La section du manuel sur la décompression avant reprise servira plus tôt que prévu, voir ma relecture de la #110.

Une remarque en commentaire de ligne, non bloquante.

Hors périmètre, pour information : ops.schema_migrations dans enervision_prod ne contient que 0007. Les migrations 0001 à 0006 n'y sont jamais passées et ops.users n'existe pas en production. La chaîne se rattrapera au premier démarrage de l'API contre la production, mais ce chemin n'a encore jamais été parcouru.

À fusionner avant la #110, qui la complète.

Relu et vérifié sur le serveur, tout ce qui est annoncé est exact : 7 sites des deux côtés, 0 mesure en production, 352 807 en préproduction, 36 fragments dont 28 comprimés, politique à 7 jours, `grafana` lit `site` et `mesure` dans les deux bases. J'ai poussé deux vérifications de plus. La chaîne complète 0001 à 0007 rejouée dans l'ordre sur une base neuve passe sans une erreur, extension comprise. Et 0007 rejoué sur une base où il est déjà appliqué est idempotent, cinq avis, aucune erreur. Le choix de garder la production vide et de convertir avant le chargement est le bon, et le motif est écrit. Les trois raisons d'écarter l'ancienne forme sont justes, celle sur la clé commune du bronze à l'or particulièrement. La section du manuel sur la décompression avant reprise servira plus tôt que prévu, voir ma relecture de la #110. Une remarque en commentaire de ligne, non bloquante. Hors périmètre, pour information : `ops.schema_migrations` dans `enervision_prod` ne contient que 0007. Les migrations 0001 à 0006 n'y sont jamais passées et `ops.users` n'existe pas en production. La chaîne se rattrapera au premier démarrage de l'API contre la production, mais ce chemin n'a encore jamais été parcouru. À fusionner avant la #110, qui la complète.
@ -0,0 +57,4 @@
('SITE005', 'Hôpital Toulouse Purpan', 'hospital', 600),
('SITE006', 'Bureau Bordeaux Centre', 'office', 180),
('SITE007', 'Usine Nantes Rezé', 'factory', 950)
on conflict (site_id) do nothing;
Owner

Le commentaire au-dessus dit qu'une capacité corrigée à la main sur le serveur ne doit pas être écrasée par un rejeu. Je le prendrais dans l'autre sens : le GLOSSAIRE se déclare source unique de ces valeurs, donc c'est la correction manuelle qui devrait céder, pas le dépôt.

Avec do nothing, une capacité corrigée ici n'atteindra jamais une base déjà peuplée, et rien ne le signalera. Un do update set libelle = excluded.libelle, type = excluded.type, capacite_kw = excluded.capacite_kw garde les deux alignés et reste rejouable.

Pas bloquant pour cette fusion.

Le commentaire au-dessus dit qu'une capacité corrigée à la main sur le serveur ne doit pas être écrasée par un rejeu. Je le prendrais dans l'autre sens : le `GLOSSAIRE` se déclare source unique de ces valeurs, donc c'est la correction manuelle qui devrait céder, pas le dépôt. Avec `do nothing`, une capacité corrigée ici n'atteindra jamais une base déjà peuplée, et rien ne le signalera. Un `do update set libelle = excluded.libelle, type = excluded.type, capacite_kw = excluded.capacite_kw` garde les deux alignés et reste rejouable. Pas bloquant pour cette fusion.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision!106
No description provided.