Skip to main content

Porteo de SQL crudo de MySQL a Postgres

El problema: la migración a Postgres es parcial, y queda mucho SQL crudo en dialecto MySQL. Cada vez que uno de esos endpoints aparece roto, hay que portarlo. Esta es la receta.

Antes de tocar el SQL: descartá la conexión

Primero mirá la consola del servidor

Si el endpoint devuelve {success:false, err:{}}, el problema puede no ser el SQL. En el caso que originó esta receta, el service leía de MSSQLConnection.dataSource, que es undefined: portar el dialecto no habría cambiado nada, porque las queries nunca llegaban a la base.

Ver el gotcha 9 de datos y persistencia.

La tabla de traducción

MySQLPostgresPor qué importa
schema.TABLAschema."TABLA"Sin comillas, PG baja a minúsculas y la tabla no existe
?$1..$nEl driver pg solo entiende posicionales
a <=> ba IS NOT DISTINCT FROM bIgualdad NULL-safe
SUBSTRING_INDEX(x, sep, 1)split_part(x, sep, 1)Con -1 (el último) → split_part(x, sep, 2) si son 2 partes
DAY(LAST_DAY(STR_TO_DATE(x,'%d-%b-%y')))EXTRACT(DAY FROM date_trunc('month', to_date(x,'DD-Mon-YY')) + INTERVAL '1 month - 1 day')Mon parsea inglés sin depender del locale; TMMon sí depende
COLLATE utf8mb4_unicode_ci(eliminar)En MySQL reconciliaba collations distintas entre tablas; en PG todas comparten la de la base
IFNULLCOALESCE
CAST(x AS DECIMAL(14,4))CAST(x AS numeric(14,4))

Las cuatro trampas que no se ven en la tabla

1. Los alias camelCase hay que quotearlos

La peor de todas: no tira error, "funciona" mostrando ceros

AS cantidadRegistros sin comillas vuelve como cantidadregistros, así que row.cantidadRegistros es undefined y el indicador da 0.

Vale para el alias y para cada referencia (u."nroOp").

2. concat() de PG ignora los NULL; CONCAT de MySQL los propaga

Si el valor alimenta un COUNT(DISTINCT …), la diferencia cambia el resultado. Usá ||, que sí propaga NULL.

3. Postgres lanza error donde MySQL devolvía NULL

STR_TO_DATE con basura daba NULL; to_date tira y se cae la query entera. Igual con castear un varchar no numérico. Poné una guarda:

CASE WHEN x ~ '^[0-9]{1,2}-[A-Za-z]{3}-[0-9]{2}$' THEN to_date(x, 'DD-Mon-YY') END

4. Traducí ?$n en UN solo lugar

Cuando los params se ensamblan concatenando builders, renumerar a mano es la forma más fácil de desalinear un parámetro sin que nada avise:

private toPositional(sql: string): string {
let i = 0;
return sql.replace(/\?/g, () => `$${++i}`);
}

Caveat: reemplaza todo ?, así que ninguna query puede tener un ? dentro de un literal.

Cómo verificarlo sin datos y sin ejecutar

EXPLAIN (GENERIC_PLAN) (Postgres 16+) parsea y planifica una consulta con $n sin ejecutarla y sin bindear params. Caza:

  • tabla o columna inexistente,
  • errores de sintaxis,
  • función que no existe,
  • tipos incompatibles.

O sea: el 100 % de los errores de un porteo. Y no necesita que las tablas tengan datos.

El patrón: mockear el dataSource para capturar el SQL que produce cada método, y pasar cada uno por EXPLAIN (GENERIC_PLAN). Molde real: tests/integration/control-dashboard/control-dashboard-sql.test.ts, que se saltea si no hay base de test.

En el porteo original este test encontró un column "usuario" does not exist que la revisión a ojo se había comido, en 12 de 42 queries.

Lo que NO valida: los números

Con las tablas vacías se verifica que las queries ejecuten, no que la aritmética sea correcta. Para eso hace falta un smoke test con datos reales.

El gotcha del final

CustomResponse.error(e) guarda el Error en err, y un Error serializa a {}. Si estás porteando algo que falla, el mensaje no te va a llegar al front: está en la consola del servidor.

Al portear, completá también message para que la próxima falla se vea en la respuesta. Ver Contrato de respuesta.