# Migrations PostgreSQL — jouées à la main sur le VPS

Ce dossier n'est **pas** vu par Doctrine Migrations. C'est voulu.

## Pourquoi

L'application vit sur deux machines, avec deux familles de bases :

| Machine | Base | Contenu | Migrations |
|---|---|---|---|
| **OVH mutualisé** | MySQL `default` | cœur du système (utilisateurs, conversations, `api_usage`…) | **Doctrine**, `doctrine:migrations:migrate` |
| **VPS** `search.lintello.ai` | PostgreSQL `ldico` et `prospect` | Tesseract / L'dicO : recherche web, crawl, leads | **à la main**, ce dossier |

> ⚠️ **Le nom de la connexion Doctrine n'est pas le nom de la base.** La connexion
> `search` (`SEARCH_DATABASE_URL`) pointe sur la base **`ldico`** ; il n'existe aucune base
> nommée `search`. Le nom d'hôte `search.lintello.ai` ajoute à la confusion : c'est le
> domaine du VPS, pas celui de la base. Vérifié le 30/07 — `psql -d search` répond
> `FATAL: database "search" does not exist`. Les cinq bases du conteneur sont `ldico`,
> `prospect`, `postgres`, `template0`, `template1`.

Doctrine Migrations ne tient **qu'un seul registre** (`doctrine_migration_versions`), celui
de la connexion `default`. Une migration PostgreSQL posée dans `migrations/` était donc
comptabilisée dans le registre MySQL, puis tentée sur MySQL.

C'est arrivé, et ça a coûté trois semaines : la file s'arrêtait sur
`Version20260703120000` (`ALTER TABLE search_result`, table qui n'existe pas côté MySQL),
donc **aucune migration MySQL postérieure au 03/07 ne pouvait plus passer**. La table
`api_usage` est restée absente et le compteur de coût de revient a écrit dans le vide
sans que rien ne remonte.

**Règle : une modification de schéma PostgreSQL ne prend jamais la forme d'un fichier
`VersionXXX.php`.** Elle vient ici, en `.sql`.

## Comment jouer un fichier

Depuis le VPS — l'OVH mutualisé ne joint pas le PostgreSQL du VPS en SSH :

```bash
docker exec -i ldico-postgres psql -U ldico -d prospect < 20260728140000-prospect-target-profile-curseur-sirene.sql
```

La base cible — `prospect` ou `search` — est **dans le nom du fichier** et rappelée dans
son en-tête. Les deux bases coexistent dans le même conteneur : c'est le seul endroit où
la confusion est possible, et elle est silencieuse.

Chaque fichier est encadré par `BEGIN; … COMMIT;`. PostgreSQL gère le DDL en transaction :
si une instruction échoue, **rien** n'est appliqué. On peut donc tenter un fichier sans
risquer de laisser la base à moitié migrée.

## Convention de nommage

```
<horodatage>-<base>-<intitulé>.sql
20260728140000-prospect-target-profile-curseur-sirene.sql
```

L'horodatage reprend celui de la migration Doctrine d'origine, pour que l'ordre
chronologique reste lisible et que l'historique Git se raccorde.

## Ce qui n'est pas suivi

Il n'y a **aucun registre** : rien n'enregistre ce qui a été joué sur quelle base. Deux
conséquences pratiques :

- avant de rejouer un fichier, vérifier l'état réel (`\d target_profile`). Les fichiers
  récents sont écrits en `IF NOT EXISTS` / `IF EXISTS` et se rejouent sans dommage ; les
  plus anciens (le `CREATE INDEX` nu de `20260703120000`) échouent sur un objet déjà là ;
- pour une **installation à blanc**, ne pas rejouer la série : partir de
  [`../prospect_schema_reference.sql`](../prospect_schema_reference.sql), qui décrit l'état
  cible complet des 11 tables — sans oublier son addendum de contrainte `CHECK`, que
  Doctrine ne sait pas déclarer.
