# Rédiger une migration pour le gestionnaire sécurisé

Ce document explique comment Claude ou Codex doivent produire une migration compatible avec `admin/database-migrations.php`. **Aucune migration n'est exécutée automatiquement** — elle doit être déposée dans `sql/migrations/`, enregistrée dans `sql/migrations/manifest.php`, puis appliquée manuellement depuis l'administration par un super-administrateur authentifié.

## Principe

Une migration est une **classe PHP**, pas un fichier `.sql` opaque. Elle étend `AbstractMigration` (`sql/migrations/AbstractMigration.php`) et implémente `MigrationInterface`. Le navigateur n'envoie jamais de SQL — seulement un `migration_id`, résolu côté serveur contre le manifeste.

Partir de `tools/templates/migration-template.php`.

## Nommage

`sql/migrations/AAAA-MM-JJ_NNN_description_courte.php`, avec une classe `Migration_AAAA_MM_JJ_NNN_DescriptionCourte`. `NNN` est un compteur à 3 chiffres pour départager plusieurs migrations le même jour. L'`id()` retourné par la classe doit être **identique** au nom de fichier sans l'extension.

## Contrat obligatoire

- `id()`, `title()`, `description()`, `author()`, `date()` (`Y-m-d`), `riskLevel()` (`low`/`medium`/`high`), `affectedTables()`, `preconditions()`.
- `supportsTransaction()` : `true` uniquement si toutes les instructions de `up()` sont réellement transactionnelles sur ce moteur (MySQL/InnoDB : les `CREATE`/`ALTER TABLE` provoquent un commit implicite — ne jamais annoncer `true` pour une migration qui en contient).
- `supportsRollback()` : `true` seulement si `down()` est réellement implémentée et sûre. Par défaut `false` (héritée d'`AbstractMigration`).
- `supportsDryRun()` : `true` par défaut. Si `up()` ne peut pas raisonnablement être simulée (ex. opération qui dépend d'un état externe), la surcharger à `false` et l'expliquer dans `description()`.
- `checksum()` : ne pas surcharger — la méthode héritée calcule un SHA-256 du fichier source lui-même. Toute modification du fichier après application change le checksum et bloque automatiquement toute réexécution silencieuse.

## `preflight(PDO $pdo): MigrationCheckResult`

**Lecture seule, jamais d'écriture.** Utiliser les helpers hérités : `tableExists()`, `columnExists()`, `indexExists()`, `rowCount()`, `engineOf()`, `serverVersion()`. Retourner un `MigrationCheckResult` avec la liste de tous les contrôles effectués (même ceux qui passent) et la liste des `blockers` (vide si tout est vert). Exemples de blockers obligatoires selon le cas : colonne déjà existante qu'on tente de recréer, doublons empêchant une contrainte `UNIQUE`, table absente, migration dépendante non appliquée (le moteur vérifie déjà cette dernière automatiquement, inutile de la dupliquer).

## `up(PDO $pdo, bool $dryRun): array`

- Rappeler `preflight()` en tout début et lever `MigrationBlockedException` si non `ok`.
- Construire la liste des instructions SQL réellement nécessaires (ignorer ce qui existe déjà — **idempotence obligatoire**, une migration relancée ne doit jamais échouer ni dupliquer).
- Valider la liste avec `MigrationSqlGuard::assertAllStatementsAllowed($statements)` avant toute exécution.
- Si `$dryRun` est vrai : ne **jamais** exécuter de DDL réel. Utiliser `$this->skipStep($sql, $raison)` pour chaque instruction planifiée, avec une raison honnête (`"DDL non transactionnel - verifie et affiche, non execute"`, pas `"simule"` si ce n'est pas vraiment simulé).
- Si `$dryRun` est faux : exécuter chaque instruction avec `$this->execGuarded($pdo, $sql, $params)`. Pour du DDL non transactionnel, exécuter étape par étape — une exception arrête tout net, le moteur enregistre l'étape exacte atteinte via `getStepLog()`.
- Retourner `['steps' => $this->stepLog, ...]` au minimum.

## `down(PDO $pdo): array`

Uniquement si `supportsRollback()` retourne `true`. Doit être l'inverse exact et sûr de `up()` — jamais un `DROP TABLE`/`DROP COLUMN` générique sur des données qui pourraient contenir de l'information (le garde-fou `MigrationSqlGuard` refuse de toute façon `DROP COLUMN`).

## Interdictions absolues (refusées par `MigrationSqlGuard`, même si présentes dans le fichier)

`DROP DATABASE`, `DROP TABLE`, `TRUNCATE`, `DELETE`/`UPDATE` sans `WHERE`, `GRANT`/`REVOKE`, `CREATE`/`ALTER USER`, accès à `mysql.*`, `LOAD DATA INFILE`, `INTO OUTFILE`/`DUMPFILE`, plusieurs instructions empilées dans une seule chaîne, commandes système. Seuls les préfixes `CREATE TABLE`, `ALTER TABLE` (avec `ADD COLUMN`/`ADD INDEX`/`ADD UNIQUE KEY`/`ADD CONSTRAINT`/`MODIFY COLUMN`/`CHANGE COLUMN`), `CREATE INDEX`, `INSERT INTO` (config versionnée uniquement), et les instructions `SELECT`/`SHOW`/`DESCRIBE`/`EXPLAIN` (préflight) sont acceptés.

## Migrations DDL vs migrations de données

- **DDL** (colonnes, tables, index) : `supportsTransaction() => false` en général sous MySQL. Documenter le risque dans `description()`, exécuter étape par étape.
- **Données** (ex. seed de configuration versionnée via `INSERT INTO`) : `supportsTransaction() => true`, toujours avec préconditions vérifiant l'absence de la donnée avant insertion (éviter les doublons).

## Tests

Aucun test automatisé n'est exécuté par le moteur lui-même. Documenter dans la description les scénarios attendus (rejeu, doublon, dépendance manquante) — voir `docs/database-migration-manager.md` §22 pour la liste de référence.

## Procédure de validation avant dépôt

1. Ajouter le fichier dans `sql/migrations/`.
2. Ajouter une entrée dans `sql/migrations/manifest.php` (id, file, class).
3. Vérifier manuellement l'équilibre des accolades/parenthèses (pas de `php -l` disponible dans cet environnement).
4. Ne jamais exécuter la migration soi-même — c'est un super-administrateur, depuis `admin/database-migrations.php`, qui lance preflight puis dry-run puis apply.

## Ce que Claude/Codex doivent livrer avec chaque nouvelle migration

1. Le fichier de migration lui-même.
2. Le rapport preflight attendu (quels contrôles, quels blockers possibles).
3. Le niveau de risque et sa justification.
4. Si rollback supporté : la logique de `down()` et ses limites.
5. Les scénarios de test pertinents (voir liste de référence).
6. Rien d'autre n'est requis pour le checksum — il est calculé automatiquement depuis le fichier.
