#!/usr/bin/env bash
#
# El freno de `scripts/consultar-pruebas.sh` frena lo que no se deshace.
#
#   pruebas/prueba_freno_consultar_pruebas.sh
#
# ⚠️ POR QUÉ EXISTE. (1) La primera versión del freno miraba con qué palabra empezaba
# TODO el texto, así que `SELECT 1; DROP DATABASE x;` lo saltaba. (2) La segunda buscaba
# ` WHERE ` con un grep, y el 28-sep-2026 un UPDATE sin WHERE pisó `dic_campo.descripcion`
# (1,995 de 1,995 filas) y `dic_tabla.descripcion` (65 de 65) en pruebas (#24601). Medido
# el 01-oct: ese grep deja pasar un UPDATE sin WHERE real si la palabra está en un
# comentario, una subconsulta o un texto, o si el WHERE es `1=1`. Un freno que se salta
# sin querer es PEOR que no tenerlo: da una confianza que no corresponde.
#
# ⚠️ NO TOCA NINGUNA BASE NI LA RED: `mysqlsh` es un guion de mentira (puesto de primero en
# el PATH) que contesta con los números que cada caso le dicta y ANOTA todo lo que le
# llega. Así se puede comprobar, además de si frena, QUÉ habría ejecutado el motor: que el
# SQL multilínea llega ENTERO y de una sola vez, que se contó antes de escribir, y que se
# guardó el respaldo.
# ⚠️ Sin `timeout`: no existe en macOS y devolvía 127, así que TODAS las comprobaciones
# daban «pasa» y parecía que el guion estaba roto. Lo estaba la prueba.
# ⚠️ Cada prueba de «frena» se vio fallar contra el guion anterior (ver el PR de #24603).
set -u
cd "$(dirname "$0")/.." || exit 1
G=scripts/consultar-pruebas.sh
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
mkdir -p "$TMP/bin"
printf 'BD_USUARIO=nadie\nBD_CLAVE=nada\n' > "$TMP/env"; chmod 600 "$TMP/env"

# mysqlsh de mentira. Variables que lee: FALSO_FILAS (las que toca el WHERE), FALSO_TOTAL,
# FALSO_SQLMODE, FALSO_CASCADA (tablas hijas) y FALSO_CONTEO_RARO. Anota los argumentos
# (FALSO_ARGS) y cada consulta que le llega (FALSO_LOG).
cat > "$TMP/bin/mysqlsh" <<'FAKE'
#!/usr/bin/env bash
sql=""
echo "$*" >> "$FALSO_ARGS"
while [ $# -gt 0 ]; do [ "$1" = "-e" ] && { sql="$2"; break; }; shift; done
printf '%s\n---\n' "$sql" >> "$FALSO_LOG"
case "$sql" in
  "SELECT COUNT(*) FROM "*)
      if [ -n "${FALSO_CONTEO_RARO:-}" ]; then echo "$FALSO_CONTEO_RARO"; exit 0; fi
      case "$sql" in *WHERE*|"SELECT COUNT(*) FROM (SELECT 1 FROM"*) printf 'COUNT\n%s\n' "${FALSO_FILAS:-1}" ;;
                     *) printf 'COUNT\n%s\n' "${FALSO_TOTAL:-5000}" ;; esac ;;
  "SELECT @@SESSION.sql_mode") printf 'sql_mode\n%s\n' "${FALSO_SQLMODE:-STRICT_TRANS_TABLES}" ;;
  "SELECT DISTINCT CONSTRAINT_SCHEMA, TABLE_NAME FROM information_schema.REFERENTIAL_CONSTRAINTS"*) if [ -n "${FALSO_CASCADA:-}" ]; then printf 'CONSTRAINT_SCHEMA\tTABLE_NAME\n%s\n' "$FALSO_CASCADA"; fi ;;
  "SELECT DISTINCT TRIGGER_NAME FROM information_schema.TRIGGERS"*) if [ -n "${FALSO_TRIGGERS:-}" ]; then printf 'TRIGGER_NAME\n%s\n' "$FALSO_TRIGGERS"; fi ;;
  "SELECT DISTINCT ROUTINE_NAME FROM information_schema.ROUTINES"*) if [ -n "${FALSO_RUTINAS:-}" ]; then printf 'ROUTINE_NAME\n%s\n' "$FALSO_RUTINAS"; fi ;;
  "SELECT COUNT(*) AS vistas FROM information_schema.VIEWS"*) printf 'vistas\n%s\n' "${FALSO_VISTAS:-0}" ;;
  "SELECT COUNT(*) AS invisibles FROM information_schema.COLUMNS"*) printf 'invisibles\n%s\n' "${FALSO_INVISIBLES:-0}" ;;
  "SELECT COLUMN_NAME FROM information_schema.STATISTICS"*) printf 'COLUMN_NAME\n%s\n' "${FALSO_PK:-id}" ;;
  "SELECT * FROM "*)
      if [ -n "${FALSO_SNAP_WARNING:-}" ]; then
          echo '{"warning":"Using a password on the command line interface can be insecure."}'
          echo '{"hasData":true,"rows":[{"warning":"valor propio"}]}'
      else echo '{"id":1,"descripcion":"valor anterior"}'; fi ;;
  *) printf '%s' "$sql" > "$FALSO_EJECUTADO"; echo ejecutado ;;
esac
FAKE
chmod +x "$TMP/bin/mysqlsh"

export MB_PRUEBAS_ENV="$TMP/env" MB_CARPETA_CONFIG="$TMP/cfg" FALSO_EJECUTADO="$TMP/ejecutado" FALSO_ARGS="$TMP/args" FALSO_LOG="$TMP/log"
export PATH="$TMP/bin:$PATH"
fallos=0
ok()    { echo "  OK    $1"; }
falla() { echo "  FALLA $1"; fallos=$((fallos+1)); }

# probar <descripción> <FRENA|PASA> <filas que toca> <total de la tabla> [args del guion...]
probar() {
  local desc="$1" esperado="$2"; export FALSO_FILAS="$3" FALSO_TOTAL="$4"; shift 4
  rm -f "$FALSO_EJECUTADO" "$FALSO_LOG"
  salida=$("$G" "$@" 2>&1); cod=$?
  if [ $cod -eq 2 ]; then real=FRENA; else real=PASA; fi
  if [ "$real" != "$esperado" ]; then falla "$desc (esperaba $esperado, dio $real: $(echo "$salida" | head -1))"; return; fi
  if [ "$esperado" = FRENA ] && [ -e "$FALSO_EJECUTADO" ]; then falla "$desc — FRENÓ pero igual ejecutó"; return; fi
  if [ "$esperado" = PASA ] && [ ! -e "$FALSO_EJECUTADO" ]; then falla "$desc — dijo PASA pero no ejecutó nada"; return; fi
  ok "$desc"
}

echo "— Lo que ya frenaba (no se puede perder) —"
probar "DROP DATABASE suelto"                       FRENA 1 5 "DROP DATABASE mbinvlachimalteca;"
probar "⚠️ DROP DATABASE escondido tras un SELECT"   FRENA 1 5 "SELECT 1; DROP DATABASE mbinvlachimalteca;"
probar "⚠️ DROP DATABASE en otra línea"              FRENA 1 5 "SELECT 1;
DROP DATABASE mbinvlachimalteca;"
probar "DROP DATABASE tras un comentario"           FRENA 1 5 "/* nada */ DROP DATABASE x;"
probar "DROP SCHEMA"                                FRENA 1 5 "DROP SCHEMA x"
probar "DELETE sin WHERE"                           FRENA 1 5 "DELETE FROM maeplu"
probar "⚠️ DELETE sin WHERE detrás de un SELECT"     FRENA 1 5 "SELECT 1; DELETE FROM maeplu"
probar "UPDATE sin WHERE"                           FRENA 1 5 "UPDATE maeplu SET plu=1"

echo "— Los huecos de #24601: parece que tiene WHERE y NO lo tiene —"
probar "⚠️ WHERE en un comentario --"                FRENA 1 5000 "UPDATE dic_campo SET descripcion='x'
-- WHERE id=1"
probar "⚠️ WHERE en un comentario #"                 FRENA 1 5000 "UPDATE dic_campo SET descripcion='x'
# WHERE id=1"
probar "⚠️ WHERE en un comentario /* */"             FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' /* WHERE id=1 */"
probar "⚠️ WHERE sólo dentro de una subconsulta"     FRENA 1 5000 "UPDATE dic_campo SET descripcion=(SELECT 'x' FROM dic_tabla WHERE tabla='a')"
probar "⚠️ WHERE dentro de un texto"                 FRENA 1 5000 "UPDATE dic_campo SET descripcion='aplica WHERE se use'"
probar "⚠️ WHERE 1=1 (tautología)"                   FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE 1=1"
probar "⚠️ WHERE TRUE"                               FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE TRUE"
probar "⚠️ WHERE id=1 OR 1=1"                        FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE id=1 OR 1=1"
probar "WHERE vacío"                                FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE"
probar "⚠️ /*! código */ que el motor SÍ ejecuta"    FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' /*!50000 -- */"
probar "TRUNCATE"                                   FRENA 1 5 "TRUNCATE TABLE dic_campo"
probar "⚠️ PREPARE con SQL armado"                   FRENA 1 5 "SET @q=CONCAT('UPDATE dic_campo SET d=1'); PREPARE s FROM @q; EXECUTE s;"
probar "⚠️ PREPARE con un UPDATE sin WHERE adentro"  FRENA 1 5 "PREPARE s FROM 'UPDATE dic_campo SET d=1'; EXECUTE s;"
probar "WITH ... UPDATE no se puede analizar"       FRENA 1 5 "WITH c AS (SELECT 1) UPDATE dic_campo SET d=1 WHERE id=1"
probar "comando del cliente \\!"                     FRENA 1 5 "\\! echo hola"
probar "texto sin cerrar"                           FRENA 1 5 "UPDATE dic_campo SET d='abc WHERE id=1"

echo "— Cuántas filas toca (el WHERE es real, pero ancho) —"
probar "⚠️ WHERE real que toca TODA la tabla (65 de 65)" FRENA 65 65 "UPDATE dic_tabla SET descripcion='x' WHERE tabla LIKE '%'"
probar "WHERE real que toca 500 de 5,000 (> umbral)"      FRENA 500 5000 "UPDATE dic_campo SET descripcion='x' WHERE tabla='maeplu'"
probar "DELETE que toca 101 de 5,000"                     FRENA 101 5000 "DELETE FROM dic_campo WHERE tabla='maeplu'"
probar "UPDATE que toca 3 de 5,000: pasa"                 PASA 3 5000 "UPDATE dic_campo SET descripcion='x' WHERE tabla='maeart' AND campo IN ('a','b','c')"
probar "UPDATE que toca 100 de 5,000 (= umbral): pasa"    PASA 100 5000 "UPDATE dic_campo SET descripcion='x' WHERE tabla='maeplu'"
probar "⚠️ tocar la ÚNICA fila de una tabla también cuenta como entera" FRENA 1 1 "UPDATE parametros_web SET x=1 WHERE id=1"

echo "— Lo que tiene que seguir pasando —"
probar "DELETE CON where: pasa"                     PASA 1 5000 "DELETE FROM maeplu WHERE id=1"
probar "UPDATE CON where: pasa"                     PASA 1 5000 "UPDATE maeplu SET plu=1 WHERE id=1"
probar "un SELECT normal: pasa"                     PASA 1 5000 "SELECT descripcion FROM maeplu"
probar "un SELECT con WHERE y UPDATE en un texto"   PASA 1 5000 "SELECT 'UPDATE x SET a=1' AS t FROM maeplu WHERE id=1"
probar "texto con ; y WHERE real"                   PASA 1 5000 "UPDATE dic_campo SET descripcion='a; b' WHERE id=1"
probar "UPDATE con subconsulta Y WHERE propio"      PASA 1 5000 "UPDATE dic_campo SET d=(SELECT 1 FROM t WHERE id=1) WHERE id=2"
probar "where en minúscula"                         PASA 1 5000 "update dic_campo set descripcion='x' where id=1"
probar "WHERE en la línea siguiente"                PASA 1 5000 "UPDATE dic_campo SET descripcion='x'
WHERE
 id=1"
probar "INSERT: pasa"                               PASA 1 5000 "INSERT INTO zz_temporal VALUES (1)"
probar "DROP TABLE (se deshace con el respaldo): pasa" PASA 1 5000 "DROP TABLE zz_temporal"

echo "— Hallazgos de la revisión del PR —"
probar "⚠️ -- seguido de tabulación vertical (\v) es comentario"  FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' --$(printf '\v') WHERE id=1"
probar "⚠️ -- seguido de salto de página (\f) es comentario"       FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' --$(printf '\f') WHERE id=1"
probar "⚠️ SET @v; UPDATE ... WHERE @v (el conteo vería otro estado)" FRENA 0 5000 "SET @all=1; UPDATE dic_campo SET descripcion='x' WHERE @all=1"
probar "⚠️ WHERE con variable @x"                                      FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE id=@x"
probar "⚠️ UPDATE después de un SET en el mismo envío"                 FRENA 1 5000 "SET NAMES utf8; UPDATE dic_campo SET descripcion='x' WHERE id=1"
probar "⚠️ dos UPDATE en el mismo envío (el 2.º vería otro estado)"    FRENA 1 5000 "UPDATE dic_campo SET a=1 WHERE id=1; UPDATE dic_campo SET b=2 WHERE a=1"
probar "SELECT y luego UPDATE: pasa (la lectura no cambia nada)"       PASA 1 5000 "SELECT 1; UPDATE dic_campo SET descripcion='x' WHERE id=1"
probar "⚠️ UPDATE con alias AS que toca TODA la tabla"                 FRENA 5000 5000 "UPDATE dic_campo AS d SET descripcion='x' WHERE d.tabla LIKE '%'"
probar "⚠️ UPDATE con alias sin AS que toca TODA la tabla"             FRENA 5000 5000 "UPDATE dic_campo d SET descripcion='x' WHERE d.tabla LIKE '%'"
probar "⚠️ DELETE con alias AS que toca 500"                           FRENA 500 5000 "DELETE FROM dic_campo AS d WHERE d.id > 0"
probar "UPDATE con alias que toca 2: pasa"                             PASA 2 5000 "UPDATE dic_campo AS d SET descripcion='x' WHERE d.id IN (1,2)"
probar "⚠️ PREPARE literal con WHERE real que toca TODA la tabla"      FRENA 5000 5000 "PREPARE s FROM 'UPDATE dic_campo SET d=1 WHERE tabla LIKE ''%'''; EXECUTE s;"
probar "⚠️ PREPARE literal con ? no se puede contar"                    FRENA 1 5000 "PREPARE s FROM 'UPDATE dic_campo SET d=1 WHERE id=?'; EXECUTE s;"
FALSO_SQLMODE=NO_BACKSLASH_ESCAPES probar "⚠️ NO_BACKSLASH_ESCAPES con una barra en el SQL" FRENA 1 5000 "SELECT 'x\'; UPDATE dic_campo SET d=1; -- ';"
probar "una barra en el texto con sql_mode normal: pasa"               PASA 1 5000 "UPDATE dic_campo SET descripcion='a\\b' WHERE id=1"
FALSO_CASCADA=$'mbinv\thija_detalle' probar "⚠️ DELETE de una tabla con ON DELETE CASCADE" FRENA 1 5000 "DELETE FROM interfaz_inventario_log WHERE id=1"
probar "DELETE de una tabla sin hijas en cascada: pasa"                PASA 1 5000 "DELETE FROM interfaz_inventario_log WHERE id=1"
probar "UPDATE con LIMIT 1: se cuenta lo que el LIMIT deja, no todo lo que calza" PASA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE tabla LIKE '%' ORDER BY id LIMIT 1"
if grep -q "LIMIT 1" "$FALSO_LOG" && grep -q "SELECT COUNT(\*) FROM (SELECT 1 FROM" "$FALSO_LOG"; then ok "el conteo respeta el LIMIT"; else falla "el conteo NO respeta el LIMIT"; fi
FALSO_CONTEO_RARO="Rows: 1000 (warning: 500)" probar "⚠️ un conteo que no es un número no se adivina" FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE id=1"
printf "BD_USUARIO=admin@dom\nBD_CLAVE='p@ss:w%%rd'\n" > "$TMP/env2"; chmod 600 "$TMP/env2"
rm -f "$FALSO_ARGS"; MB_PRUEBAS_ENV="$TMP/env2" probar "usuario/clave con @ : %" PASA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE id=1"
# Octava revisión: no basta con que ALGUNA llamada lleve la URI escapada (el conteo previo, que
# sale de Python). La ejecución final del shell (`correr`) tiene que escaparla igual: se revisa
# CADA --uri registrado y ninguno puede traer el usuario o la clave crudos.
if [ -s "$FALSO_ARGS" ] && ! grep -q -- "admin@dom\|p@ss:w%rd" "$FALSO_ARGS" && ! grep -- "--uri" "$FALSO_ARGS" | grep -qv "admin%40dom:p%40ss%3Aw%25rd@"; then
  ok "TODAS las URI (conteo y ejecución final) llevan usuario y clave escapados"
else
  falla "alguna URI (probablemente la de la ejecución final) lleva usuario/clave sin escapar"
fi

echo "— Octava revisión del PR —"
probar "⚠️ \\. (ejecutar archivo) en una línea posterior, sin ; antes" FRENA 1 5000 "SELECT 1
\\. /tmp/destructivo.sql"
probar "⚠️ \\source en una línea posterior tras un INSERT"            FRENA 1 5000 "INSERT INTO zz_temporal VALUES (1)
\\source /tmp/destructivo.sql"
probar "un \\G al final de la línea de un SELECT: sigue pasando"       PASA 1 5000 "SELECT 1 \\G"
probar "⚠️ BINLOG con eventos de filas crudos"                           FRENA 1 5000 "BINLOG 'abc='"
rm -rf "$MB_CARPETA_CONFIG"
probar "con tiquete, un UPDATE tras un INSERT igual se cuenta y respalda" PASA 1 5000 --se-lo-que-hago 24603 "INSERT INTO control VALUES (1); UPDATE datos SET x=1 WHERE activo=1"
if grep -q "FROM datos" "$FALSO_LOG" 2>/dev/null; then ok "el UPDATE posterior al INSERT se contó/respaldó con la excepción"; else falla "el UPDATE posterior al INSERT quedó fuera del conteo y del respaldo"; fi

echo "— Novena revisión del PR —"
# (1) Con tiquete, un rechazo al INICIO del envío no debe dejar sin contar lo que viene después.
rm -rf "$MB_CARPETA_CONFIG"
probar "con tiquete, CALL seguido de UPDATE: el UPDATE igual se cuenta y respalda" PASA 1 5000 --se-lo-que-hago 24603 "CALL p(); UPDATE tabla_nueve SET x=1 WHERE id=1"
if grep -q "FROM tabla_nueve" "$FALSO_LOG" 2>/dev/null; then ok "el UPDATE posterior al rechazo se contó/respaldó con la excepción"; else falla "el UPDATE posterior al rechazo quedó fuera del conteo y del respaldo"; fi
# (2) INSERT masivo a una tabla con triggers de INSERT: sin tiquete frena; un INSERT de una fila sólo avisa.
export FALSO_TRIGGERS=detmov_insert
probar "⚠️ INSERT ... SELECT a una tabla con triggers de INSERT"       FRENA 1 5000 "INSERT INTO detmov (a) SELECT a FROM otra"
probar "⚠️ INSERT de varias filas a una tabla con triggers de INSERT"  FRENA 1 5000 "INSERT INTO detmov (a) VALUES (1),(2)"
probar "INSERT de UNA fila a una tabla con triggers: pasa"            PASA 1 5000 "INSERT INTO detmov (a) VALUES (1)"
probar "con tiquete, INSERT masivo con triggers: pasa"                PASA 1 5000 --se-lo-que-hago 24603 "INSERT INTO detmov (a) SELECT a FROM otra"
unset FALSO_TRIGGERS
probar "INSERT ... SELECT a una tabla SIN triggers: pasa"             PASA 1 5000 "INSERT INTO detmov (a) SELECT a FROM otra"
# (3) WHERE tautológico pero acotado por LIMIT con ORDER BY por la llave: se cuenta lo que el LIMIT deja.
probar "UPDATE ... WHERE 1=1 ORDER BY id LIMIT 1 pasa (1 fila)"       PASA 1 5000 "UPDATE jobs SET processed=1 WHERE 1=1 ORDER BY id LIMIT 1"
probar "⚠️ WHERE 1=1 sin LIMIT sigue frenando"                         FRENA 1 5000 "UPDATE jobs SET processed=1 WHERE 1=1 ORDER BY id"
# (4) \G es un delimitador: lo que viene después es OTRA sentencia y se analiza.
probar "⚠️ SELECT 1 \\G seguido de un UPDATE sin WHERE"                 FRENA 1 5000 "SELECT 1 \\G
UPDATE dic_campo SET d=1;"
probar "SELECT 1 \\G seguido de otro SELECT: pasa"                      PASA 1 5000 "SELECT 1 \\G
SELECT 2 \\G"

echo "— Décima revisión del PR —"
# (1) `source` sin barra al empezar una línea (mysqlsh lo ejecuta sin `;`).
probar "⚠️ source sin barra en una línea posterior"                  FRENA 1 5000 "SELECT 1
source /tmp/destructivo.sql"
probar "⚠️ DELIMITER en una línea posterior"                         FRENA 1 5000 "SELECT 1
DELIMITER //"
probar "una columna llamada source al empezar línea: pasa"          PASA 1 5000 "SELECT id,
source
FROM dic_campo"
# (2) ON UPDATE CASCADE / SET NULL: un UPDATE de un padre cambia filas hijas.
FALSO_CASCADA=$'mbinv\thija' probar "⚠️ UPDATE de una tabla con hijas ON UPDATE CASCADE" FRENA 1 5000 "UPDATE dic_campo SET id=2 WHERE id=1"
FALSO_CASCADA=$'mbinv\thija' probar "con tiquete, UPDATE con hijas ON UPDATE CASCADE pasa"  PASA 1 5000 --se-lo-que-hago 24603 "UPDATE dic_campo SET id=2 WHERE id=1"
probar "UPDATE de una tabla sin hijas: pasa"                         PASA 1 5000 "UPDATE dic_campo SET id=2 WHERE id=1"
# (3) un /*! ... */ que cruza el borde del WHERE: el conteo lleva SQL válido.
probar "UPDATE con WHERE a medias dentro de /*! */"                  PASA 1 5000 "UPDATE dic_campo SET x=1 WHERE a=1 /*! AND b=2 */"
if grep -q "^WHERE a=1 AND b=2$" "$FALSO_LOG" 2>/dev/null && ! grep "^WHERE" "$FALSO_LOG" | grep -q '/\*\|\*/'; then ok "el conteo cruza el borde del comentario sin dejar delimitadores sueltos"; else falla "el conteo trae /* o */ sueltos"; fi
# (4) funciones almacenadas en el WHERE: el conteo previo las ejecutaría.
FALSO_RUTINAS=f probar "⚠️ WHERE con una función almacenada"           FRENA 1 5000 "UPDATE dic_campo SET x=1 WHERE cliente.f(id)=0"
FALSO_RUTINAS=f probar "⚠️ WHERE con una función almacenada, con tiquete" FRENA 1 5000 --se-lo-que-hago 24603 "UPDATE dic_campo SET x=1 WHERE f(id)=0"
probar "WHERE con función nativa (LOWER): pasa"                      PASA 1 5000 "UPDATE dic_campo SET x=1 WHERE LOWER(d)='a'"

echo "— Undécima revisión del PR —"
# (1) LOAD DATA también dispara los triggers de INSERT, fila por fila.
export FALSO_TRIGGERS=detmov_insert
probar "⚠️ LOAD DATA INTO TABLE a una tabla con triggers de INSERT"      FRENA 1 5000 "LOAD DATA LOCAL INFILE '/tmp/x.csv' INTO TABLE detmov"
probar "⚠️ LOAD DATA con esquema, PARTITION y columnas"                  FRENA 1 5000 "LOAD DATA LOCAL INFILE '/tmp/x.csv' IGNORE INTO TABLE mbinv.detmov FIELDS TERMINATED BY ',' (a, b)"
probar "con tiquete, LOAD DATA con triggers: pasa"                      PASA 1 5000 --se-lo-que-hago 24603 "LOAD DATA LOCAL INFILE '/tmp/x.csv' INTO TABLE detmov"
unset FALSO_TRIGGERS
probar "LOAD DATA a una tabla SIN triggers: pasa"                       PASA 1 5000 "LOAD DATA LOCAL INFILE '/tmp/x.csv' INTO TABLE detmov"
# (2) `--` seguido de DEL (0x7f) es comentario para MySQL.
probar "⚠️ -- seguido de DEL (0x7f) esconde el WHERE"                    FRENA 1 5000 "UPDATE dic_campo SET d=1 --"$'\x7f'" WHERE id=1"
probar "-- seguido de espacio sigue siendo comentario: pasa"            PASA 1 5000 "UPDATE dic_campo SET d=1 WHERE id=1 -- listo"

echo "— Duodécima revisión del PR —"
# (1) Funciones almacenadas en CUALQUIER expresión, no sólo en el WHERE.
FALSO_RUTINAS=f probar "⚠️ función almacenada en el SET"              FRENA 1 5000 "UPDATE dic_campo SET d=cliente.f() WHERE id=1"
FALSO_RUTINAS=f probar "⚠️ función almacenada en los VALUES de un INSERT" FRENA 1 5000 "INSERT INTO dic_campo (d) VALUES (f(1))"
FALSO_RUTINAS=f probar "⚠️ SELECT de una función almacenada"           FRENA 1 5000 "SELECT f(1)"
FALSO_RUTINAS=f probar "con tiquete, función almacenada en el SET: pasa" PASA 1 5000 --se-lo-que-hago 24603 "UPDATE dic_campo SET d=f() WHERE id=1"
probar "función nativa en el SET (NOW): pasa"                PASA 1 5000 "UPDATE dic_campo SET d=NOW() WHERE id=1"
probar "SELECT con función nativa: pasa"                     PASA 1 5000 "SELECT NOW()"
# (2) USE <base>; UPDATE ...: el conteo y el respaldo corren con esa base por omisión.
rm -f "$FALSO_ARGS"
probar "USE base; UPDATE sin calificar: pasa"                PASA 1 5000 "USE client_db; UPDATE dic_campo SET d=1 WHERE id=1"
if grep -q -- "--schema client_db" "$FALSO_ARGS"; then ok "los pasos previos se conectan con --schema client_db"; else falla "los pasos previos NO usan la base del USE"; fi
probar "⚠️ USE base; UPDATE sin WHERE sigue frenando"          FRENA 1 5000 "USE client_db; UPDATE dic_campo SET d=1"

echo "— Decimotercera revisión del PR —"
# (1) `source from`: el nombre del archivo puede ser una palabra de SQL; sólo es columna tras `,` o SELECT.
probar "⚠️ source from (archivo llamado from)"                   FRENA 1 5000 "SELECT 1
source from"
probar "⚠️ source and"                                            FRENA 1 5000 "SELECT 1
source and"
probar "columna source tras una coma: pasa"                      PASA 1 5000 "SELECT id,
source FROM dic_campo"
# (2) Tope de respaldo por debajo del umbral: sin tiquete NO se escribe sin respaldo.
salida=$(MB_SNAPSHOT_MAX_FILAS=5 FALSO_FILAS=10 FALSO_TOTAL=5000 "$G" "UPDATE dic_campo SET d=1 WHERE id < 10" 2>&1); cod=$?
if [ $cod -eq 2 ]; then ok "sin tiquete, más filas que el tope de respaldo: frena"; else falla "sin tiquete y sin respaldo igual escribió"; fi
# (3) `db . tabla` con espacios es lo mismo que `db.tabla`.
export FALSO_TRIGGERS=detmov_insert
probar "⚠️ INSERT ... SELECT a mbinv . detmov (con espacios)"     FRENA 1 5000 "INSERT INTO mbinv . detmov (a) SELECT a FROM otra"
unset FALSO_TRIGGERS
# (4) Nombre de función entre comillas con signos raros.
FALSO_RUTINAS=write-all probar "⚠️ función almacenada con nombre entre comillas y guion" FRENA 1 5000 "SELECT cliente.\`write-all\`()"
# (5) Columnas invisibles: el respaldo con SELECT * no las guarda.
FALSO_INVISIBLES=1 probar "⚠️ UPDATE de una tabla con columnas invisibles"    FRENA 1 5000 "UPDATE dic_campo SET d=1 WHERE id=1"
FALSO_INVISIBLES=1 probar "con tiquete, tabla con columnas invisibles: pasa"   PASA 1 5000 --se-lo-que-hago 24603 "UPDATE dic_campo SET d=1 WHERE id=1"

echo "— Decimocuarta revisión del PR —"
# (1) Un paréntesis que se abre dentro de /*! */ y se cierra fuera cambia qué WHERE es de quién.
probar "⚠️ paréntesis abierto dentro de /*! */ y cerrado afuera"   FRENA 1 5000 "UPDATE t SET x = /*! ( */ SELECT 1 WHERE t.id=1 ORDER BY 1)"
probar "paréntesis balanceados dentro de /*! */: pasa"             PASA 1 5000 "UPDATE dic_campo SET x=1 WHERE /*! (id=1) */"
# (2) CLONE INSTANCE borra todo lo del servidor receptor.
probar "⚠️ CLONE INSTANCE"                                          FRENA 1 5000 "CLONE INSTANCE FROM 'u'@'host':3306 IDENTIFIED BY 'x'"
probar "con tiquete, CLONE INSTANCE pasa"                          PASA 1 5000 --se-lo-que-hago 24603 "CLONE INSTANCE FROM 'u'@'host':3306 IDENTIFIED BY 'x'"
# (3) Un UPDATE/DELETE a través de una VISTA: el respaldo sólo trae las columnas de la vista.
FALSO_VISTAS=1 probar "⚠️ DELETE a través de una vista"             FRENA 1 5000 "DELETE FROM v_clientes WHERE id=1"
FALSO_VISTAS=1 probar "con tiquete, DELETE a través de una vista pasa" PASA 1 5000 --se-lo-que-hago 24603 "DELETE FROM v_clientes WHERE id=1"

echo "— Decimoquinta revisión del PR —"
# (1) Con ANSI_QUOTES las comillas dobles son identificadores, no textos: se mira el modo con SÓLO verlas.
FALSO_SQLMODE=ANSI_QUOTES probar "⚠️ comillas dobles con ANSI_QUOTES activo"       FRENA 1 5000 'UPDATE t SET x=1 WHERE "cliente"."f"()=1'
probar "comillas dobles con el modo normal: pasa"                                  PASA 1 5000 'UPDATE dic_campo SET d="x" WHERE id=1'
# (2) DISCARD TABLESPACE tira el .ibd de la tabla.
probar "⚠️ ALTER TABLE ... DISCARD TABLESPACE"                                      FRENA 1 5000 "ALTER TABLE customer_orders DISCARD TABLESPACE"
probar "con tiquete, DISCARD TABLESPACE pasa"                                      PASA 1 5000 --se-lo-que-hago 24603 "ALTER TABLE customer_orders DISCARD TABLESPACE"
probar "ALTER TABLE normal: pasa"                                                  PASA 1 5000 "ALTER TABLE zz_temporal ADD COLUMN c INT"

echo "— Segunda revisión del PR —"
probar "⚠️ destino \`db\`.\`tabla\` entre comillas que toca TODA la tabla"  FRENA 5000 5000 "UPDATE \`client_db\`.\`dic_campo\` SET descripcion='x' WHERE tabla LIKE '%'"
probar "⚠️ destino db.tabla con alias AS que toca 500"                     FRENA 500 5000 "DELETE FROM client_db.dic_campo AS d WHERE d.id > 0"
probar "destino \`db\`.\`tabla\` que toca 2: pasa"                          PASA 2 5000 "UPDATE \`client_db\`.\`dic_campo\` SET descripcion='x' WHERE id IN (1,2)"
probar "⚠️ CALL de un procedimiento almacenado"                           FRENA 1 5000 "CALL sp_calcular_promedios_movil_v18(1)"
probar "⚠️ EXPLAIN ANALYZE ejecuta el UPDATE"                              FRENA 1 5000 "EXPLAIN ANALYZE UPDATE dic_campo d JOIN dic_tabla t ON 1=1 SET d.descripcion='x'"
probar "EXPLAIN normal: pasa (no ejecuta)"                                 PASA 1 5000 "EXPLAIN SELECT * FROM dic_campo WHERE id=1"
FALSO_CASCADA=$'mbinv\thija_detalle' probar "⚠️ cascada: la hija se informa con su esquema"  FRENA 1 5000 "DELETE FROM mbinv.interfaz_inventario_log WHERE id=1"
probar "la búsqueda de cascadas se acota al esquema del destino"           PASA 1 5000 "DELETE FROM cliente_a.padre WHERE id=1"
if grep -q "UNIQUE_CONSTRAINT_SCHEMA = 'cliente_a'" "$FALSO_LOG"; then ok "las cascadas se buscan sólo en el esquema cliente_a"; else falla "las cascadas NO se acotan al esquema del destino"; fi

echo "— Tercera revisión del PR —"
FALSO_SQLMODE=ANSI_QUOTES,STRICT_TRANS_TABLES probar "⚠️ ANSI_QUOTES con una barra en el SQL"  FRENA 1 5000 'SELECT "x\"; UPDATE dic_campo SET d=1; -- "'
probar "⚠️ UPDATE con FORCE INDEX (no se puede contar): frena"             FRENA 500 5000 "UPDATE dic_campo FORCE INDEX(PRIMARY) SET descripcion='x' WHERE id > 0"
probar "⚠️ UPDATE con PARTITION: frena"                                    FRENA 1 5000 "UPDATE dic_campo PARTITION(p0) SET descripcion='x' WHERE id = 1"
probar "⚠️ UPDATE de varias tablas con JOIN: frena"                        FRENA 1 5000 "UPDATE dic_campo c JOIN dic_tabla t ON t.tabla=c.tabla SET c.descripcion='x' WHERE t.tabla='maeplu'"
probar "con tiquete, el UPDATE de varias tablas pasa"                      PASA 1 5000 --se-lo-que-hago 24603 "UPDATE dic_campo c JOIN dic_tabla t ON t.tabla=c.tabla SET c.descripcion='x' WHERE t.tabla='maeplu'"
probar "⚠️ PREPARE de una escritura ejecutado DOS veces"                    FRENA 100 5000 "PREPARE s FROM 'UPDATE dic_campo SET d=1 WHERE d=0 ORDER BY id LIMIT 100'; EXECUTE s; EXECUTE s;"
probar "PREPARE de una escritura ejecutado UNA vez: pasa"                  PASA 100 5000 "PREPARE s FROM 'UPDATE dic_campo SET d=1 WHERE d=0 ORDER BY id LIMIT 100'; EXECUTE s;"
probar "⚠️ WHERE con RAND(): otro subconjunto cada vez"                     FRENA 50 5000 "UPDATE dic_campo SET descripcion='x' WHERE RAND() < 0.5"
probar "⚠️ WHERE con UUID()"                                                FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' WHERE id = 1 OR UUID() = 'a'"
rm -rf "$MB_CARPETA_CONFIG"
LARGO=$(printf 'SELECT 1 AS x /* %0600d */; ' 0)
probar "con tiquete, la bitácora guarda tabla destino y huella aunque el SQL sea largo" PASA 65 65 --se-lo-que-hago 24603 "${LARGO}UPDATE dic_tabla SET descripcion='x'"
grep -q '"tablas": \["dic_tabla"\]' "$MB_CARPETA_CONFIG/excepciones-sql.log" && ok "la bitácora registra la tabla destino" || falla "la bitácora no registra la tabla destino"
grep -q '"sql_sha256": "[0-9a-f]\{64\}"' "$MB_CARPETA_CONFIG/excepciones-sql.log" && ok "la bitácora registra el SHA-256 del SQL completo" || falla "la bitácora no registra el SHA-256"
export FALSO_TRIGGERS=trg_auditoria; FALSO_FILAS=1; FALSO_TOTAL=5000; export FALSO_FILAS FALSO_TOTAL
salida=$("$G" "UPDATE detuni SET precio=1 WHERE id=1" 2>&1); unset FALSO_TRIGGERS; echo "$salida" | grep -q "trigger" && ok "avisa que la tabla tiene triggers (no se cuentan)" || falla "no avisa de los triggers"

echo "— Cuarta revisión del PR —"
probar "⚠️ PREPARE con comillas dobles: \\\" y \"\" se decodifican y el WHERE queda comentado" FRENA 1 5000 'PREPARE s FROM "UPDATE dic_campo SET d=\"x\" -- "" WHERE id=1"; EXECUTE s;'
probar "PREPARE con comillas dobles y un WHERE real chico: pasa"                             PASA 1 5000 'PREPARE s FROM "UPDATE dic_campo SET d=1 WHERE id=1"; EXECUTE s;'
probar "⚠️ LIMIT sin ORDER BY: respaldo y escritura podrían ser filas distintas"             FRENA 10 5000 "DELETE FROM dic_campo WHERE d=1 LIMIT 10"
probar "⚠️ LIMIT con ORDER BY por una columna que no es la llave primaria"                   FRENA 1 5000 "UPDATE dic_campo SET d=1 WHERE d=0 ORDER BY creado LIMIT 1"
probar "LIMIT con ORDER BY por la llave primaria: pasa"                                      PASA 1 5000 "UPDATE dic_campo SET d=1 WHERE d=0 ORDER BY id DESC LIMIT 1"
FALSO_PK=$'tabla\ncampo' probar "⚠️ LIMIT con ORDER BY por una parte de la llave compuesta"  FRENA 1 5000 "UPDATE dic_campo SET d=1 WHERE d=0 ORDER BY tabla LIMIT 1"
FALSO_PK=$'tabla\ncampo' probar "LIMIT con ORDER BY por toda la llave compuesta: pasa"       PASA 1 5000 "UPDATE dic_campo SET d=1 WHERE d=0 ORDER BY tabla, campo LIMIT 1"
probar "con tiquete, LIMIT sin ORDER BY pasa"                                                PASA 10 5000 --se-lo-que-hago 24603 "DELETE FROM dic_campo WHERE d=1 LIMIT 10"
salida=$(MB_SNAPSHOT_MAX_FILAS=5 FALSO_FILAS=10 FALSO_TOTAL=5000 "$G" --se-lo-que-hago 24603 "UPDATE dic_campo SET d=1 WHERE id < 10" 2>&1); echo "$salida" | grep -q "NO se respaldó" && ok "con tiquete, más filas que el tope: no intenta respaldar (y lo dice)" || falla "con tiquete y más filas que el tope sí intentó respaldar"
probar "⚠️ INSERT ... ON DUPLICATE KEY UPDATE pisa filas existentes"                          FRENA 1 5000 "INSERT INTO dic_campo (id, descripcion) SELECT id, 'x' FROM copia ON DUPLICATE KEY UPDATE descripcion='x'"
probar "⚠️ REPLACE INTO borra y reinserta"                                                    FRENA 1 5000 "REPLACE INTO dic_campo (id, descripcion) VALUES (1,'x')"
probar "INSERT simple: pasa"                                                                 PASA 1 5000 "INSERT INTO dic_campo (id, descripcion) VALUES (1,'x')"
probar "con tiquete, INSERT ... ON DUPLICATE KEY UPDATE pasa"                                PASA 1 5000 --se-lo-que-hago 24603 "INSERT INTO dic_campo (id) VALUES (1) ON DUPLICATE KEY UPDATE id=1"

echo "— Quinta revisión del PR —"
probar "⚠️ SET sql_mode en el mismo envío, antes de un texto con barra" FRENA 1 5000 "SET sql_mode='NO_BACKSLASH_ESCAPES'; SELECT 'x\'; UPDATE dic_campo SET d=1; -- ';"
probar "⚠️ SET @@session.sql_mode"                                       FRENA 1 5000 "SET @@session.sql_mode=''"
probar "SET NAMES solo: pasa"                                            PASA 1 5000 "SET NAMES utf8"
probar "⚠️ LOAD DATA ... REPLACE"                                         FRENA 1 5000 "LOAD DATA LOCAL INFILE '/tmp/x.csv' REPLACE INTO TABLE dic_campo"
probar "LOAD DATA sin REPLACE: pasa"                                     PASA 1 5000 "LOAD DATA LOCAL INFILE '/tmp/x.csv' INTO TABLE dic_campo"
probar "⚠️ CREATE EVENT con un UPDATE adentro"                            FRENA 1 5000 "CREATE EVENT e ON SCHEDULE AT CURRENT_TIMESTAMP DO UPDATE dic_campo SET descripcion='x'"
probar "⚠️ CREATE DEFINER=... EVENT"                                      FRENA 1 5000 "CREATE DEFINER=root@localhost EVENT e ON SCHEDULE AT CURRENT_TIMESTAMP DO UPDATE dic_campo SET descripcion='x'"
probar "⚠️ ALTER EVENT"                                                   FRENA 1 5000 "ALTER EVENT e DO UPDATE dic_campo SET descripcion='x'"
probar "CREATE TABLE: pasa"                                              PASA 1 5000 "CREATE TABLE zz_temporal (id INT)"
probar "WHERE dentro de /*! */: el conteo lleva SQL válido"              PASA 1 5000 "UPDATE dic_campo SET descripcion='x' /*! WHERE id=1 */"
if grep -q "^WHERE id=1$" "$FALSO_LOG" && ! grep -B1 -A0 "^WHERE id=1$" "$FALSO_LOG" | grep -q '/\*'; then ok "el conteo se armó con la posición real de los tokens, sin el comentario"; else falla "el conteo del WHERE dentro de /*! */ salió mal"; fi
rm -rf "$MB_CARPETA_CONFIG"
export FALSO_SNAP_WARNING=1
probar "respaldo de una tabla con una columna llamada warning"           PASA 1 5000 "UPDATE dic_campo SET warning='x' WHERE id=1"
unset FALSO_SNAP_WARNING
grep -q "valor propio" "$MB_CARPETA_CONFIG"/snapshots/*.jsonl 2>/dev/null && ok "el respaldo conserva la fila cuya columna se llama warning" || falla "el respaldo perdió la fila cuya columna se llama warning"
! grep -q "Using a password" "$MB_CARPETA_CONFIG"/snapshots/*.jsonl 2>/dev/null && ok "y descarta el aviso de mysqlsh" || falla "el respaldo trae el aviso de mysqlsh"

echo "— Sexta revisión del PR —"
probar "⚠️ SELECT LAST_INSERT_ID(1) y luego un UPDATE que lo usa"       FRENA 0 5000 "SELECT LAST_INSERT_ID(1); UPDATE dic_campo SET d=1 WHERE LAST_INSERT_ID()=1"
probar "⚠️ WHERE con LAST_INSERT_ID()"                                   FRENA 0 5000 "UPDATE dic_campo SET d=1 WHERE id = LAST_INSERT_ID()"
probar "⚠️ SELECT que asigna con := y luego un UPDATE"                    FRENA 1 5000 "SELECT 1 INTO @x; UPDATE dic_campo SET d=1 WHERE id=1"
probar "⚠️ SELECT GET_LOCK y luego un UPDATE"                             FRENA 1 5000 "SELECT GET_LOCK('a',1); UPDATE dic_campo SET d=1 WHERE id=1"
probar "SELECT común y luego un UPDATE: sigue pasando"                   PASA 1 5000 "SELECT descripcion FROM maeplu; UPDATE dic_campo SET d=1 WHERE id=1"

echo "— Séptima revisión del PR —"
probar "⚠️ CREATE TRIGGER con un UPDATE adentro"                          FRENA 1 5000 "CREATE TRIGGER tr AFTER INSERT ON jobs FOR EACH ROW UPDATE dic_campo SET descripcion='x'"
probar "⚠️ CREATE PROCEDURE"                                               FRENA 1 5000 "CREATE PROCEDURE p() UPDATE dic_campo SET descripcion='x'"
probar "⚠️ CREATE FUNCTION"                                                FRENA 1 5000 "CREATE FUNCTION f() RETURNS INT RETURN 1"
probar "DROP TRIGGER: pasa (no guarda SQL nuevo)"                          PASA 1 5000 "DROP TRIGGER tr"
probar "⚠️ DROP /*!99999 TEMPORARY */ DATABASE (el motor lo lee como DROP DATABASE)" FRENA 1 5000 "DROP /*!99999 TEMPORARY */ DATABASE client_db"
probar "comentario /*!50000 ... */ de versión baja: se sigue leyendo"      FRENA 1 5000 "UPDATE dic_campo SET descripcion='x' /*!50000 -- */"
probar "⚠️ ALTER TABLE ... TRUNCATE PARTITION"                             FRENA 1 5000 "ALTER TABLE orders TRUNCATE PARTITION p2025"
probar "⚠️ ALTER TABLE ... DROP PARTITION"                                 FRENA 1 5000 "ALTER TABLE orders DROP PARTITION p2025"
probar "ALTER TABLE ... ADD COLUMN: pasa"                                  PASA 1 5000 "ALTER TABLE zz_temporal ADD COLUMN x INT"
probar "WHERE con NOW(): pasa (límite documentado: la limpieza por antigüedad es normal)" PASA 1 5000 "UPDATE dic_campo SET d=1 WHERE ts <= NOW() - INTERVAL 7 DAY"

echo "— La excepción exige tiquete, y deja rastro —"
probar "⚠️ --se-lo-que-hago SIN número de tiquete"    FRENA 1 5 --se-lo-que-hago "UPDATE maeplu SET plu=1"
probar "⚠️ --se-lo-que-hago con un texto, no un número" FRENA 1 5 --se-lo-que-hago "porque si" "UPDATE maeplu SET plu=1"
rm -rf "$MB_CARPETA_CONFIG"
probar "--se-lo-que-hago 24603 deja pasar la tabla entera" PASA 65 65 --se-lo-que-hago 24603 "UPDATE dic_tabla SET descripcion='x' WHERE tabla LIKE '%'"
if grep -q '"tiquete": "24603"' "$MB_CARPETA_CONFIG/excepciones-sql.log" 2>/dev/null; then ok "la excepción quedó anotada con el tiquete"; else falla "la excepción NO quedó anotada"; fi
[ "$(stat -c '%a' "$MB_CARPETA_CONFIG/excepciones-sql.log" 2>/dev/null || stat -f '%Lp' "$MB_CARPETA_CONFIG/excepciones-sql.log")" = 600 ] && ok "la bitácora de excepciones es 600" || falla "la bitácora no es 600"
probar "con tiquete, un UPDATE sin WHERE real pasa pero queda anotado" PASA 5000 5000 --se-lo-que-hago 24603 "UPDATE dic_campo SET d='x' -- WHERE id=1"
grep -q 'frenado_por' "$MB_CARPETA_CONFIG/excepciones-sql.log" && ok "se anotó POR QUÉ el guion lo habría frenado" || falla "no se anotó el motivo del freno saltado"

rm -rf "$MB_CARPETA_CONFIG"
probar "con tiquete, UPDATE sin WHERE: pasa" PASA 65 65 --se-lo-que-hago 24603 "UPDATE dic_tabla SET descripcion='x'"
[ "$(ls "$MB_CARPETA_CONFIG"/snapshots/*.jsonl 2>/dev/null | wc -l | tr -d ' ')" = 1 ] && ok "con tiquete y sin WHERE, igual se respaldó la tabla" || falla "con tiquete y sin WHERE NO se respaldó la tabla"

echo "— Respaldo antes de escribir —"
rm -rf "$MB_CARPETA_CONFIG"
probar "UPDATE con WHERE" PASA 2 5000 "UPDATE dic_campo SET descripcion='nueva' WHERE id IN (1,2)"
n=$(ls "$MB_CARPETA_CONFIG"/snapshots/*.jsonl 2>/dev/null | wc -l | tr -d ' ')
[ "$n" = 1 ] && ok "se guardó el respaldo de las filas antes de escribir" || falla "no se guardó el respaldo ($n archivos)"
grep -q 'valor anterior' "$MB_CARPETA_CONFIG"/snapshots/*.jsonl 2>/dev/null && ok "el respaldo trae el valor anterior" || falla "el respaldo no trae el valor anterior"
[ "$(stat -c '%a' "$MB_CARPETA_CONFIG"/snapshots/*.jsonl 2>/dev/null || stat -f '%Lp' "$MB_CARPETA_CONFIG"/snapshots/*.jsonl)" = 600 ] && ok "el respaldo es 600" || falla "el respaldo no es 600"

echo "— Regresión del incidente: SQL multilínea llega ENTERO, de una sola vez —"
SQL="UPDATE dic_campo
   SET descripcion = 'Campo heredado, sin uso activo conocido en el BO actual.'
 WHERE tabla = 'maeart'
   AND campo IN ('ruedo');"
probar "el UPDATE multilínea del incidente, con su WHERE" PASA 1 5000 "$SQL"
if [ "$(cat "$FALSO_EJECUTADO")" = "$SQL" ]; then ok "al motor le llegó byte por byte, con su WHERE"; else falla "lo que llegó al motor NO es lo que se pidió"; fi
SQL2="UPDATE dic_campo
   SET descripcion = 'Campo heredado, sin uso activo conocido en el BO actual.';"
probar "el MISMO UPDATE sin la línea del WHERE" FRENA 1 5000 "$SQL2"

echo "— Sin el analizador no se ejecuta nada (falla cerrado) —"
cp "$G" "$TMP/solo-guion.sh"   # sin sql_guarda.py al lado
export FALSO_FILAS=1 FALSO_TOTAL=5000; rm -f "$FALSO_EJECUTADO"
"$TMP/solo-guion.sh" "UPDATE dic_campo SET d=1 WHERE id=1" >/dev/null 2>&1; cod=$?
if [ $cod -ne 0 ] && [ ! -e "$FALSO_EJECUTADO" ]; then ok "sin sql_guarda.py no ejecuta"; else falla "sin sql_guarda.py EJECUTÓ igual (código $cod)"; fi

echo
echo "  $fallos con problema"
exit $((fallos>0))
