Files
ENI-projet-piscine/docs/architecture/40-data.md
T

187 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Données
PostgreSQL 17 avec l'extension TimescaleDB. Le choix, ses alternatives et ses conséquences sont
dans l'[ADR 0001](../adr/0001-postgresql-timescaledb.md), qui fait foi. Ce document décrit le
système qui en découle.
## Avertissement
**Aucune table applicative n'existe à ce jour.** `Base.metadata` est vide, `app/models/` ne
contient qu'un commentaire, l'unique révision Alembic ne crée aucune table, et aucune hypertable
n'a été déclarée. Tout ce qui suit sous le statut `Cible` est une proposition de structure, pas un
relevé du code. Le modèle sera arrêté au jalon J2.
## Trois emplacements, trois rôles
C'est la règle que l'ADR 0001 existe surtout pour fixer. La confondre coûte cher : un script placé
au mauvais endroit ne s'exécute jamais, ou s'exécute deux fois.
| Emplacement | Contenu | Quand ça s'exécute |
|---|---|---|
| `db/init/` | Extensions, bases annexes | **Une seule fois**, à la première initialisation du conteneur, quand `PGDATA` est vide. Ne rejoue jamais |
| `db/migrations/` | SQL versionné qui ne découle pas du schéma applicatif : rétention, compression | À la main, aujourd'hui vide |
| `apps/backend/alembic/` | Le schéma exposé par l'API, et lui seul | `alembic upgrade head`, c'est `Base.metadata` qui fait foi |
Une hypertable relève des deux derniers : **Alembic crée la table, et le `create_hypertable()`
vit dans la même révision**. Les séparer rendrait le schéma irreproductible depuis un seul
`alembic upgrade head`.
Détail de `db/init/` et du piège de montage : [`db/README.md`](../../db/README.md).
## Ce qui existe
Statut : `Fait`.
- `db/init/100-extensions.sql` crée l'extension `timescaledb`.
- `db/init/110-test-database.sql` crée `enervision_test`, dont le nom est attendu en dur par
`apps/backend/tests/conftest.py`.
- Une révision Alembic, `5353c0e4f094`, qui **ne crée aucune table**. Elle établit
`alembic_version` et refuse de s'appliquer si l'extension manque :
```sql
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN
RAISE EXCEPTION 'extension timescaledb absente, voir db/init et db/README.md';
END IF;
```
Cette garde forme paire avec le 503 de `/api/v1/health/ready`. Un bootstrap sauté ne se voit pas
au démarrage de l'API : ces deux gardes le rendent visible tôt, des deux côtés.
## Cycle de vie d'une mesure
Statut : `Cible`. Aucun de ces maillons n'existe.
```mermaid
flowchart LR
src["Source de mesures"] -.-> ing["Ingestion Airflow"]
ing -.-> hy[("Hypertable mesure")]
hy -.-> agg[("Agrégat continu")]
hy -.-> comp["Compression"]
hy -.-> ret["Rétention"]
agg -.-> api["API FastAPI"]
agg -.-> graf["Grafana"]
```
Les lectures de l'API et de Grafana visent l'agrégat continu, pas la table brute : c'est tout
l'intérêt de TimescaleDB, et cela doit rester vrai quand les volumes augmenteront.
## Modèle
Statut : `Cible`. Les entités ci-dessous sont des **candidates**, à valider en J2. Elles
s'appuient sur les gabarits de [`apps/backend/TESTING.md`](../../apps/backend/TESTING.md), qui
évoquent déjà un modèle `Site`, un `SiteRepository` et un `ConsumptionService` exposant un
`total_kwh(site_id)`.
```mermaid
erDiagram
SITE ||--o{ POINT_DE_MESURE : porte
POINT_DE_MESURE ||--o{ MESURE : produit
SITE {
int id PK
string nom
}
POINT_DE_MESURE {
int id PK
int site_id FK
string libelle
string unite
}
MESURE {
timestamptz horodatage PK
int point_id PK
double valeur
}
```
`MESURE` est la table destinée à devenir une hypertable, partitionnée sur `horodatage`. Sa clé
primaire doit inclure la colonne de temps : TimescaleDB l'exige, une clé sur le seul identifiant
de point serait refusée.
## Gabarit de révision créant une hypertable
Conforme à la règle de l'ADR 0001 : table et hypertable dans la même révision.
```python
def upgrade() -> None:
op.create_table(
"mesure",
sa.Column("horodatage", sa.DateTime(timezone=True), nullable=False),
sa.Column("point_id", sa.Integer(), sa.ForeignKey("point_de_mesure.id"), nullable=False),
sa.Column("valeur", sa.Float(), nullable=False),
sa.PrimaryKeyConstraint("horodatage", "point_id"),
)
op.execute("SELECT create_hypertable('mesure', by_range('horodatage'))")
def downgrade() -> None:
op.drop_table("mesure")
```
`drop_table` suffit au retour arrière : supprimer la table supprime l'hypertable et ses partitions.
## Conventions
- **Noms au singulier**, en minuscules, sans préfixe de table.
- **Toute colonne de temps en `timestamptz`.** Jamais de `timestamp` nu : une mesure sans fuseau
devient ininterprétable dès le premier changement d'heure.
- **La colonne de partitionnement s'appelle `horodatage`** et entre dans la clé primaire.
- **Les politiques de rétention et de compression** vont dans `db/migrations/`, pas dans Alembic :
elles ne découlent pas du schéma applicatif.
- **Tout modèle doit être importé dans `app/models/__init__.py`**, sans quoi
`alembic revision --autogenerate` ne le voit pas et génère un `drop` de sa table.
## Questions ouvertes
Elles relèvent du jalon J2, « valider le périmètre retenu », et bloquent le modèle définitif.
- **Quelles sources de mesures**, et selon quel protocole elles sont collectées.
- **Quelle granularité** à l'ingestion : la seconde, la minute, le quart d'heure.
- **Quels agrégats continus**, et sur quelles fenêtres.
- **Quelle profondeur de rétention** en données brutes, et à partir de quand on compresse.
- **Quelles unités** sont manipulées, et si une même table les mélange.
- **Multi-tenant ou non** : un site appartient-il à un client, et faut-il cloisonner les lectures.
## Modélisation détaillée des données
Cette modélisation prend en compte les fichiers CSV historiques,
leurs métadonnées JSON et les données de l’API Mock.
Elle comprend six tables, depuis le stockage des mesures
jusqu’aux recommandations proposées à l’utilisateur.
### Schéma de données
Le diagramme ci-dessous présente les tables et leurs relations.
Il décrit une structure de conception ; les migrations correspondantes
restent à implémenter.
![Schéma de données EnerVision](images/EnerVision-schema-donnees.png)
*Figure — Modélisation des données EnerVision.*
### Description des tables
Chaque table remplit un rôle précis dans le traitement et l’exploitation
des données.
| Table | Rôle | Origine des informations |
|---|---|---|
| `datasets` | Identifier les jeux historiques, retrouver leurs fichiers et conserver leurs métadonnées | Archive CSV/JSON et informations ajoutées lors de l’import |
| `sites` | Regrouper les informations des sites : identifiant, nom, type et caractéristiques disponibles | CSV et API Mock `/api/v1/sites` |
| `readings` | Stocker les mesures, leur provenance, leur qualité et les éventuelles valeurs imputées | CSV et API Mock `/current` et `/readings` |
| `predictions` | Conserver les prévisions, leur période cible et la référence du modèle utilisé | Traitements ML d’EnerVision |
| `alerts` | Enregistrer les alertes, leur type, leur gravité et leur message | API Mock `/alerts` et détections EnerVision |
| `recommendations` | Proposer des actions et expliquer la règle qui les motive | Règles métier d’EnerVision |
Les anomalies historiques décrites dans les JSON sont conservées
dans `datasets.metadata`. Elles servent à l’analyse des données
et ne sont pas considérées comme des alertes actuelles.
### Relations entre les tables
- Un site possède plusieurs mesures, prévisions et alertes.
- Un jeu de données historique contient plusieurs mesures CSV.
- Les mesures API ne sont pas rattachées à un dataset historique.
- Une alerte peut être associée à une prévision du même site.
- Une alerte peut donner lieu à plusieurs recommandations.