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>
153 lines
3.6 KiB
Go
153 lines
3.6 KiB
Go
package services
|
|
|
|
import "strings"
|
|
|
|
// DividirSentenciasSQL parte un script en sentencias por punto y coma,
|
|
// respetando dónde ese punto y coma NO es un separador.
|
|
//
|
|
// Partir con strings.Split(texto, ";") parece que funciona hasta que el script
|
|
// trae un punto y coma dentro de un comentario o de una cadena — ahí corta una
|
|
// sentencia al medio y produce SQL corrupto que igual se manda a ejecutar.
|
|
//
|
|
// Las sentencias se devuelven con sus comentarios: MySQL los acepta, y sirven
|
|
// para que el usuario reconozca en los resultados qué se ejecutó.
|
|
func DividirSentenciasSQL(texto string) []string {
|
|
var sentencias []string
|
|
var actual strings.Builder
|
|
r := []rune(texto)
|
|
|
|
agregar := func() {
|
|
if s := strings.TrimSpace(actual.String()); TieneSQLReal(s) {
|
|
sentencias = append(sentencias, s)
|
|
}
|
|
actual.Reset()
|
|
}
|
|
|
|
for i := 0; i < len(r); i++ {
|
|
c := r[i]
|
|
var sig rune
|
|
if i+1 < len(r) {
|
|
sig = r[i+1]
|
|
}
|
|
|
|
switch {
|
|
// Comentario de línea: -- … o # …
|
|
case (c == '-' && sig == '-') || c == '#':
|
|
for i < len(r) && r[i] != '\n' {
|
|
actual.WriteRune(r[i])
|
|
i++
|
|
}
|
|
if i < len(r) {
|
|
actual.WriteRune('\n')
|
|
}
|
|
|
|
// Comentario de bloque: /* … */
|
|
case c == '/' && sig == '*':
|
|
actual.WriteString("/*")
|
|
i += 2
|
|
for i < len(r) && !(r[i] == '*' && i+1 < len(r) && r[i+1] == '/') {
|
|
actual.WriteRune(r[i])
|
|
i++
|
|
}
|
|
actual.WriteString("*/")
|
|
i++
|
|
|
|
// Cadenas y nombres citados: ' " `
|
|
case c == '\'' || c == '"' || c == '`':
|
|
comilla := c
|
|
actual.WriteRune(c)
|
|
i++
|
|
for i < len(r) {
|
|
if r[i] == '\\' && i+1 < len(r) {
|
|
// Escape con barra: se copia el par entero.
|
|
actual.WriteRune(r[i])
|
|
actual.WriteRune(r[i+1])
|
|
i += 2
|
|
continue
|
|
}
|
|
if r[i] == comilla {
|
|
actual.WriteRune(r[i])
|
|
// Comilla duplicada ('') es una comilla literal, no el cierre.
|
|
if i+1 < len(r) && r[i+1] == comilla {
|
|
actual.WriteRune(r[i+1])
|
|
i += 2
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
actual.WriteRune(r[i])
|
|
i++
|
|
}
|
|
|
|
case c == ';':
|
|
agregar()
|
|
|
|
default:
|
|
actual.WriteRune(c)
|
|
}
|
|
}
|
|
agregar()
|
|
return sentencias
|
|
}
|
|
|
|
// TieneSQLReal dice si al trozo le queda algo ejecutable una vez sacados los
|
|
// comentarios. Es lo que distingue "esta sentencia viene precedida por un
|
|
// encabezado en comentarios" de "esto es solo un comentario suelto".
|
|
//
|
|
// El filtro viejo descartaba todo trozo que EMPEZARA con "--", y como en un
|
|
// script bien documentado casi todas las sentencias vienen debajo de su
|
|
// comentario, se saltaba la mayoría del archivo sin decir nada.
|
|
func TieneSQLReal(s string) bool {
|
|
limpio := quitarComentarios(s)
|
|
return strings.TrimSpace(limpio) != ""
|
|
}
|
|
|
|
func quitarComentarios(s string) string {
|
|
var out strings.Builder
|
|
r := []rune(s)
|
|
for i := 0; i < len(r); i++ {
|
|
c := r[i]
|
|
var sig rune
|
|
if i+1 < len(r) {
|
|
sig = r[i+1]
|
|
}
|
|
switch {
|
|
case (c == '-' && sig == '-') || c == '#':
|
|
for i < len(r) && r[i] != '\n' {
|
|
i++
|
|
}
|
|
out.WriteRune('\n')
|
|
case c == '/' && sig == '*':
|
|
i += 2
|
|
for i < len(r) && !(r[i] == '*' && i+1 < len(r) && r[i+1] == '/') {
|
|
i++
|
|
}
|
|
i++
|
|
out.WriteRune(' ')
|
|
case c == '\'' || c == '"' || c == '`':
|
|
// Una cadena cuenta como contenido real; se copia entera para no
|
|
// confundir un "--" que viva adentro con un comentario.
|
|
comilla := c
|
|
out.WriteRune(c)
|
|
i++
|
|
for i < len(r) {
|
|
if r[i] == '\\' && i+1 < len(r) {
|
|
out.WriteRune(r[i])
|
|
out.WriteRune(r[i+1])
|
|
i += 2
|
|
continue
|
|
}
|
|
if r[i] == comilla {
|
|
out.WriteRune(r[i])
|
|
break
|
|
}
|
|
out.WriteRune(r[i])
|
|
i++
|
|
}
|
|
default:
|
|
out.WriteRune(c)
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|