fix(query-runner): "conx_db_id inválido" al correr un .sql, y el parser que se comía el script
Dos bugs encadenados. El segundo es mucho peor que el que se veía.
1. EL ERROR VISIBLE. RunBatchQuery leía conx_db_id del formulario y lo guardaba
bien, y después lo pisaba con cero. El struct del body solo tenía etiquetas
json, pero en multipart Fiber mapea por la etiqueta form: no encontraba
conx_db_id, dejaba el campo en cero — y BodyParser NO devuelve error cuando
no mapea nada, así que la rama se ejecutaba igual y sobreescribía el valor
bueno. De ahí el "conx_db_id inválido" con la conexión bien elegida en
pantalla. Reproducido con un test antes de tocar nada.
2. EL QUE NO SE VEÍA. El script se partía con split(';') y se descartaba todo
trozo que empezara con "--". En un script documentado, donde cada sentencia
va debajo de su encabezado en comentarios, eso se saltaba casi todo el
archivo en silencio. Con el .sql que lo destapó: de 10 sentencias reales
pasaban 3 — y una de esas tres era un fragmento corrupto, porque un punto y
coma dentro de un comentario había partido una sentencia al medio. Se
perdían los dos INSERT de alta y el ALTER TABLE, que era el único cambio
estructural del script.
Es decir: arreglar solo el bug 1 habría sido peor que dejarlo. El usuario
habría leído "3 consultas ejecutadas" y se habría ido tranquilo con la
migración a medio aplicar.
DividirSentenciasSQL parte respetando dónde el punto y coma no separa nada:
comentarios de línea (-- y #), de bloque, y cadenas con ' " ` incluyendo
escapes con barra y comillas duplicadas. Y en vez de descartar lo que empieza
con "--", pregunta si al trozo le queda SQL después de sacarle los comentarios
— un "--" adentro de una cadena ya no oculta la sentencia.
Mismo criterio en los dos lados: el front arma las sentencias y el backend
divide los archivos subidos, y las dos implementaciones dan las mismas 10 sobre
el archivo real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b7c52f174a
commit
93f5fdec7a
@@ -0,0 +1,100 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// El caso real que lo destapó: un script de migración bien documentado, donde
|
||||
// cada sentencia viene debajo de su encabezado en comentarios y uno de esos
|
||||
// comentarios contiene un punto y coma.
|
||||
const scriptReal = `-- ============================================================
|
||||
-- PARAMETROS WEBMASTER
|
||||
-- ============================================================
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 1. EL SUBMODULO
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO submodulo (nombre, url) VALUES ('parametroswebmaster', 'p.php');
|
||||
|
||||
-- 2. ACCESO
|
||||
INSERT INTO usuario_has_submodulo (submodulo_id, usuario_id)
|
||||
SELECT S.id, 1 FROM submodulo S WHERE S.nombre = 'parametroswebmaster';
|
||||
|
||||
-- 4. COLUMNAS DE IA
|
||||
ALTER TABLE bot_tipos_config
|
||||
ADD COLUMN IF NOT EXISTS ia_activo TINYINT(1) NOT NULL DEFAULT 0;
|
||||
|
||||
-- 5. VERIFICACION
|
||||
-- (cargar_menu() excluye el modulo 6 en duro; si sale 6, no aparecera)
|
||||
SELECT S.id, S.nombre FROM submodulo S WHERE S.nombre = 'parametroswebmaster';
|
||||
|
||||
SHOW COLUMNS FROM bot_tipos_config;
|
||||
`
|
||||
|
||||
func TestDividirSentenciasScriptReal(t *testing.T) {
|
||||
sentencias := DividirSentenciasSQL(scriptReal)
|
||||
|
||||
if len(sentencias) != 5 {
|
||||
t.Fatalf("salieron %d sentencias, esperaba 5:\n%s", len(sentencias), strings.Join(sentencias, "\n---\n"))
|
||||
}
|
||||
|
||||
// Cada una tiene que terminar conteniendo su SQL, aunque venga precedida
|
||||
// de comentarios. El parser viejo descartaba todas estas.
|
||||
esperados := []string{"INSERT INTO submodulo", "INSERT INTO usuario_has_submodulo",
|
||||
"ALTER TABLE bot_tipos_config", "SELECT S.id", "SHOW COLUMNS"}
|
||||
for i, esperado := range esperados {
|
||||
if !strings.Contains(sentencias[i], esperado) {
|
||||
t.Errorf("sentencia %d no contiene %q:\n%s", i, esperado, sentencias[i])
|
||||
}
|
||||
}
|
||||
|
||||
// El punto y coma dentro del comentario no puede haber partido nada: si
|
||||
// partiera, quedaría un fragmento que empieza con el resto del comentario.
|
||||
for _, s := range sentencias {
|
||||
if strings.HasPrefix(strings.TrimSpace(s), "si sale 6") {
|
||||
t.Error("se partió dentro de un comentario: quedó un fragmento corrupto")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDividirSentenciasCasosBorde(t *testing.T) {
|
||||
casos := []struct {
|
||||
nombre string
|
||||
entrada string
|
||||
cuantas int
|
||||
}{
|
||||
{"punto y coma dentro de una cadena", `INSERT INTO t VALUES ('hola; chau'); SELECT 1;`, 2},
|
||||
{"comilla escapada con barra", `INSERT INTO t VALUES ('a\'; b'); SELECT 1;`, 2},
|
||||
{"comilla duplicada", `INSERT INTO t VALUES ('a''; b'); SELECT 1;`, 2},
|
||||
{"comentario de bloque con punto y coma", `/* uno; dos */ SELECT 1;`, 1},
|
||||
{"solo comentarios no es una sentencia", "-- nada que ver\n-- tampoco esto\n", 0},
|
||||
{"sin punto y coma final", "SELECT 1", 1},
|
||||
{"vacío", "", 0},
|
||||
{"puntos y coma de más", "SELECT 1;;; SELECT 2;", 2},
|
||||
{"backticks con punto y coma", "SELECT `col;raro` FROM t; SELECT 2;", 2},
|
||||
{"almohadilla como comentario", "# comentario; con punto y coma\nSELECT 1;", 1},
|
||||
}
|
||||
for _, c := range casos {
|
||||
got := DividirSentenciasSQL(c.entrada)
|
||||
if len(got) != c.cuantas {
|
||||
t.Errorf("%s: salieron %d, esperaba %d → %q", c.nombre, len(got), c.cuantas, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// La regresión concreta: el filtro viejo tiraba toda sentencia que empezara
|
||||
// con un comentario, que en un script documentado son casi todas.
|
||||
func TestSentenciaConEncabezadoNoSeDescarta(t *testing.T) {
|
||||
s := "-- Crea la tabla\n-- Es re-ejecutable\nCREATE TABLE t (id INT)"
|
||||
if !TieneSQLReal(s) {
|
||||
t.Error("una sentencia precedida por comentarios se está descartando")
|
||||
}
|
||||
if TieneSQLReal("-- solo un comentario\n") {
|
||||
t.Error("un comentario suelto no es una sentencia")
|
||||
}
|
||||
// Un "--" adentro de una cadena no convierte la sentencia en comentario.
|
||||
if !TieneSQLReal(`SELECT '-- esto no es comentario'`) {
|
||||
t.Error("un guión doble dentro de una cadena no debe ocultar la sentencia")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user