Villa Aurora, Florencia
Ni un carácter perdido
La base vieja guardaba texto en varios juegos de caracteres, a veces dos dentro de la misma tabla. Libretto no podía salir a producción hasta que cada nombre, materia y nota llegara exactamente como debía.
El problema
Cada columna declaraba su propio juego de caracteres —latin1, latin2, UTF-16— y una misma tabla de usuarios mezclaba dos: latin2 para nombre y apellido, latin1 para todo lo demás. Leído ingenuamente por una conexión UTF-8, un nombre como Nicolás sale como Nicolás; o peor, los caracteres desaparecen sin dejar rastro.
Las personas estaban repartidas en tres fuentes —fichas personales, logins y asignaciones docentes— sin una clave común, y el número de matrícula era único solo dentro de cada carrera.
Restricciones
- El sistema viejo siguió en uso hasta el corte, así que la migración tenía que poder correr una y otra vez sin duplicar nada.
- Producción ya tenía datos cargados por scripts anteriores; la sincronización debía adoptarlos, no borrarlos.
- Una persona no técnica tenía que poder ejecutarla con un solo comando.
- Sin cambios de esquema en la aplicación solo para soportar la migración.
Qué construí
Un motor de sincronización en Node.js que lee la base legacy, planifica cada escritura como una transformación pura, valida el plan completo contra un control de calidad y recién entonces escribe: en orden de claves foráneas, una transacción por unidad lógica, para que una corrida interrumpida se pueda simplemente volver a ejecutar.
Solo inserta: nunca actualiza ni borra. Una tabla de mapeo (entidad, clave legacy → id nuevo) recuerda lo que ya se migró. Para cada ítem: si su clave está en el mapa, se saltea; si ya existe una fila con la misma clave natural —un email, un usuario y curso, un código de catálogo— se adopta en el mapa; si no, se inserta. Esa regla del medio permitió que el motor tomara una base de producción que scripts anteriores ya habían llenado.
Tres modos para tres momentos: una vista previa que no escribe nada, un bootstrap que solo arma el mapa, y corridas reales que exigen un --allow-write explícito. Un wrapper para el operador abre las conexiones, lo ejecuta e imprime el resultado en lenguaje claro.
Lo difícil
Bytes en la base vieja
6C926573616D65
Decodificado como UTF-8
l�esame
1 carácter de reemplazo. El control se niega a escribir.
Decodificado como latin1
lCarácter de control invisible U+0092esame
Ningún carácter de reemplazo, pero falta un carácter
Decodificado como Windows-1252
l’esame
Correcto
Leer bytes, no texto
Cada columna de texto se lee como hexadecimal: ASCII puro, así que ni el driver ni el charset de la conexión pueden corromperla. Después los bytes se decodifican en Node con el códec que corresponde al charset declarado de la columna, consultado en information_schema y cacheado por tabla: UTF-16 como UTF-16BE, latin2 como Windows-1250, latin1 como Windows-1252.
Por qué Windows-1252 y no latin1
En Windows-1252, los bytes 0x92 a 0x96 son comillas tipográficas y guiones. En latin1 estricto son caracteres de control invisibles: decodificar como latin1 los pierde en silencio, sin generar un solo carácter de reemplazo. Un control que solo buscara caracteres rotos habría pasado. Probalo en la figura de arriba.
Un control que se niega a escribir
Cada decodificación cuenta los U+FFFD, el carácter de reemplazo. Si el total es mayor que cero, el motor lanza un error antes de su primera escritura y termina con código distinto de cero. En el prototipo, decodificar dos tablas de muestra como UTF-8 producía 54 y 284; con los códecs correctos, cero. Más tarde, cuando el servidor productivo convirtió una tabla de exámenes a UTF-8 real a mitad del proyecto, la decodificación pasó de ser por tabla a ser por columna.
Quién es quién
Tres fuentes se fusionaron en 436 usuarios canónicos, incluidos 51 marcadores para personas que solo existían en tablas de exámenes: sin ellos se habrían perdido 280 registros de examen. Los números de matrícula chocaban entre personas; resolverlos por identidad canónica e inscripción más reciente asignó número a 357 de 364 personas y dejó 7 para que decida una persona, en lugar de adivinar.
Resultado
- Cero caracteres de reemplazo en todas las tablas migradas.
- 436 usuarios canónicos a partir de tres fuentes, y 280 registros de examen conservados que una fusión ingenua habría perdido.
- 357 de 364 matrículas resueltas automáticamente; 7 marcadas para revisión en lugar de adivinadas.
- Idempotente por diseño: volver a correrlo después de una carga no encuentra nada que hacer. El mapa tiene 14.227 registros.
- El 13 de octubre de 2026 se congeló el sistema viejo y Libretto tomó la posta.
El código pertenece al cliente y es privado. Las figuras son ilustraciones con datos de ejemplo; en esta página no aparece ningún registro real.