Migration de la base en utf8mb4
Marche à suivre pour convertir la base de production de latin1 vers utf8mb4.
Cette migration ne s’exécute pas au déploiement. Plesk tire main et
ne lance aucune migration. Elle doit être lancée à la main, par le détenteur des
accès à la base.
|
Pourquoi
La base est en latin1. Tout caractère hors cp1252 saisi par un contributeur y est
remplacé par ? à l’écriture, définitivement.
| saisi | ce que la base en fait | après migration |
|---|---|---|
|
intact |
intact |
|
|
intact |
|
|
intact |
|
|
intact |
Au dernier relevé : 81 morceaux ont un titre ou un auteur détruit, 37 contributeurs sont concernés, et cinq dégâts datent de 2026. Sur 8 097 morceaux et dix-huit ans, pas un seul caractère hors cp1252 n’a survécu.
L’ordre des gestes, qui est une contrainte
Convertir les tables d’abord, changer l’encodage de connexion ensuite.
Déclarer encoding: utf8mb4 avant la conversion ferait envoyer de l’utf8mb4 vers des
colonnes latin1 — le mécanisme qui détruit aujourd’hui, en pire.
livraison 1 le script et cette page aucun effet en production
↓
À LA MAIN lancer le script les tables passent en utf8mb4
↓
livraison 2 encoding: utf8mb4 sur `all` la connexion cesse de convertir
Entre les deux, l’application lit des tables utf8mb4 sur une connexion utf8.
MySQL reconvertit, et utf8 couvre tout le plan multilingue de base : rien de
l’existant ne se perd. Seuls les emoji restent refusés, et bruyamment. C’est un
état sûr, dans lequel on peut rester des jours.
Avant de lancer
-
Dump complet, et vérifier qu’il se relit. Un dump non testé n’est pas une sauvegarde. C’est le seul retour arrière :
CONVERT TO CHARACTER SET latin1ne rend rien, il re-détruirait ce que la conversion vient de sauver.mysqldump -u<user> -p --default-character-set=latin1 <base> > avant-utf8mb4.sql mysql -u<user> -p -e "CREATE DATABASE verif_dump" mysql -u<user> -p --default-character-set=latin1 verif_dump < avant-utf8mb4.sql -
Relever un échantillon accentué et ses octets. C’est contre lui qu’on vérifiera que rien n’a bougé.
SELECT id, track_author, HEX(track_author) FROM post WHERE track_author REGEXP '[^ -~]' ORDER BY id LIMIT 5;
Lancer
mysql -u<user> -p <base> < src/data/migration/utf8mb4.sql
Le script commence par un contrôle préalable qui s’arrête si l’un des trois états
attendus est faux : rien à convertir, pas de table post, ou du double encodage
détecté. Le troisième est le décisif — voir plus bas.
Il saute les tables déjà converties : une exécution interrompue se relance sans dommage.
Après
-
Vérifier que toutes les tables portent
utf8mb4— le script l’affiche en fin d’exécution. -
Lancer la commande que le script affiche pour la base elle-même. Elle ne touche aucune donnée ; elle fixe le jeu par défaut des tables créées ensuite.
ALTER DATABASEn’est pas supporté par le protocole des requêtes préparées (erreur 1295), et le script ne connaît pas le nom de la base. D’où cette commande à part. -
Reconstruire l’index de recherche : la collation de
post_indexa changé.mysql -u<user> -p -e "TRUNCATE post_index;" <base> cd src && php -r 'require "config/ProjectConfiguration.class.php"; $c = ProjectConfiguration::getApplicationConfiguration("frontend", "prod", false); new sfDatabaseManager($c); Doctrine_Core::getTable("Post")->batchUpdateIndex();'Le TRUNCATEn’est pas une précaution, c’est la moitié de la commande.batchUpdateIndex()est incrémental : sa requête ne sélectionne que les morceaux absents de l’index. Sur un index déjà peuplé, elle ne fait rien — 0,2 s et aucun changement. C’est précisément le cas après un changement de collation, où toutes les lignes sont présentes mais mal collationnées.Mesuré sur 6 155 morceaux : 14,7 s depuis un index vide, 19 Mo de pic mémoire, 28 129 entrées produites. Compter une vingtaine de secondes pour les 8 216 morceaux de la production.
L’index local portait 28 164 entrées avant reconstruction contre 28 129 après : 35 orphelines, reliquats de morceaux supprimés ou modifiés. Vider avant de reconstruire les élimine au passage. -
Reprendre l’échantillon relevé avant. Le texte doit être identique, et le
HEX()avoir changé — un octetlatin1devient deux octets UTF-8 :avant Güyôm HEX=47FC79F46D après Güyôm HEX=47C3BC79C3B46DSi le texte a bougé, la conversion a mal tourné. Restaurer le dump. -
Seulement alors, poser
encoding: utf8mb4sur le blocalldeconfig/databases.yml-distet livrer. -
Vider le cache après le déploiement.
cd src && php symfony cache:cleardatabases.ymlest compilé danscache/. Sans cette commande, la connexion continue de négocier l’ancien encodage et le changement reste sans effet. Plesk tire le code, il ne vide pas le cache.
Vérification finale, sur le site en ligne
Ces étapes closent la migration. Elles demandent le site déployé et un compte d’admin.
-
Poster depuis l’admin un morceau au titre cyrillique et un portant un emoji, comme un contributeur le ferait. C’est le seul test qui exerce le chemin réel ; tout le reste n’en est qu’une approximation.
-
Vérifier qu’ils sont servis intacts dans la page, le JSON, le XSPF et le
max. -
Vérifier que la recherche les trouve — la collation de
post_indexa changé. -
Les retirer, ou les garder : mais le décider, pas l’oublier.
Le contrôle qui décide, et pourquoi
CONVERT TO CHARACTER SET réinterprète les octets comme du latin1 authentique. Si le
corpus portait de l’UTF-8 rangé dans des colonnes latin1 — un double encodage — la
conversion produirait du mojibake définitif.
Le contrôle cherche les formes latin1 des séquences UTF-8 les plus courantes en
français : é pour é, è pour è, à ` pour `à. S’il en trouve une, il s’arrête.
Ne pas forcer. Un corpus doublement encodé demande un traitement différent, et le forcer détruit ce qui restait.
Répétition menée le 2026-08-18
Le script a été éprouvé sur quatre copies, dont une copie des données réelles de production, jamais sur la production elle-même.
Sur les données réelles
| contrôle | résultat |
|---|---|
double encodage sur 8 216 morceaux |
0 — la conversion fera exactement ce qu’il faut |
tables converties |
12 ; les quinze tables |
|
|
|
|
nombre de morceaux |
8 216 avant, 8 216 après |
morceaux détruits |
82 avant, 82 après — ni réparés, ni aggravés |
mojibake introduit |
0 |
emoji, cyrillique, idéogrammes après conversion |
acceptés |
Sur des copies de structure
épreuve |
résultat [cols="2,3"] |
| épreuve | résultat
| conversion d’une copie latin1 complète
| 4 tables converties ; Güyôm conservé, HEX passé de 47FC79F46D à 47C3BC79C3B46D
| insertion d’un emoji après conversion | acceptée
| relance sur une base déjà convertie | arrêt avec un message explicite
| reprise après interruption à mi-parcours | 2 tables restantes converties, les 2 déjà faites intouchées
| corpus portant un double encodage | arrêt — le garde-fou mord
=== Répétée sur MariaDB, le moteur de la production L’environnement de développement a été porté de MySQL 5.7 à MariaDB 10.11, la version de la production. La répétition a donc eu lieu sur le moteur cible, et elle a révélé un défaut du contrôle préalable que MySQL masquait. [cols="2,3"] |
| contrôle sur MariaDB 10.11 | résultat
| contrôle préalable, corpus réel | passé
| tables converties | 12 ; directus_* non touchées
| Güyôm | 47FC79F46D → 47C3BC79C3B46D
| variètè_Good | E8 → C3A8, deux fois
| morceaux | 8 216 avant, 8 216 après
| morceaux détruits | 82 avant, 82 après
| corpus empoisonné | arrêt — le garde-fou mord
=== Le défaut que MariaDB a révélé La première version du contrôle cherchait Un garde-fou qui se déclenche toujours ne vaut pas mieux qu’un garde-fou qui ne se déclenche jamais. La comparaison porte désormais sur les octets, via === Un faux positif écarté Une sonde a signalé un == Ce que cette migration ne fait pas Les 81 morceaux déjà détruits le restent. Le La confondre avec une réparation ferait croire le problème réglé. L’inventaire de ce qui est perdu est un travail à part. |