Por qué este caso fue importante
Este artículo se basa en un ejemplo ocurrido en un entorno interno de pruebas donde habíamos construido:
- Servidores MCP especializados que exponían herramientas de investigación y recuperación para Db2
- Un agente de IA capaz de utilizar esas herramientas
- Un catálogo de inyecciones controladas de fallos para provocar incidentes en la base de datos
El objetivo era simple: inyectar fallos realistas y comprobar si el agente podía ayudar a un DBA a investigar y recuperar.
Una de esas pruebas se volvió más interesante de lo esperado. Inyectamos un escenario de drop de tabla y el agente no se limitó a ejecutar algunos comandos de recuperación. Encontró una limitación real de recuperación en Db2, adaptó la estrategia para reconstruir los datos basándose en la lógica de negocio y aplicó esa estrategia. Esa es la parte no trivial que merece ser documentada.
Más tarde se identificó una solución mejor que la reconstrucción de los datos, pero todavía no estaba soportada por el MCP: analizar el contenido de los logs de Db2 para reconstruir el SQL responsable de modificar los datos perdidos y volver a ejecutarlo. Esa capacidad fue añadida y probada. El agente de IA consiguió entonces recuperar todos los datos perdidos a partir del log de Db2.
El fallo inyectado
El incidente fue inyectado con el siguiente script:
#!/bin/bash
# DL-01 — DROP TABLE (no dependents)
# Drops STORE.PAYMENTS — no views or FKs depend on it.
# Symptom: SQL0204N on any query referencing PAYMENTS.
set -e
CONTAINER=db2-primary
DB=LABDB
echo "==> [DL-01] Injecting: DROP TABLE STORE.PAYMENTS"
docker exec "$CONTAINER" su - db2inst1 -c "
db2 connect to $DB > /dev/null
db2 \"INSERT INTO STORE.AUDIT_LOG (EVENT_TYPE, TABLE_NAME, DESCRIPTION)
VALUES ('INCIDENT_INJECTED', 'STORE.PAYMENTS', 'DL-01: PAYMENTS table dropped — no cascade')\"
db2 \"DROP TABLE STORE.PAYMENTS\"
db2 commit
db2 connect reset > /dev/null
"
echo ""
echo "==> [DL-01] Done."
echo " Symptom : SQL0204N when querying STORE.PAYMENTS"
echo " Evidence: SYSCAT.TABLES has no entry for PAYMENTS"
echo " SYSCAT.INDEXES missing PAYMENTS indexes"
echo " AUDIT_LOG has the incident marker"Todo fue diseñado intencionadamente para obtener un incidente simple de drop de tabla:
- Una única tabla eliminada, sin claves foráneas ni vistas dependientes que complicaran el diagnóstico.
A primera vista, parecía el tipo de caso que un playbook convencional de recuperación debería resolver bien.
La petición al agente
Después de la inyección, el usuario preguntó:
Parece que algunas tablas fueron eliminadas. Investiga, crea un plan y ejecuta el plan para efectuar la recuperación.
La formulación importa porque la petición no le dice al agente:
- Qué tabla fue eliminada
- Cómo ocurrió la eliminación
- Qué camino de recuperación debía seguir
El agente tuvo que investigar, formar un plan a partir de la evidencia y solo después ejecutar la recuperación. En otras palabras, se trató de una tarea de razonamiento, no de una simple ejecución de comandos.
Lo primero que encontró el agente
El primer hallazgo útil estaba en la huella dejada por la propia inyección:
- La tabla
STORE.AUDIT_LOGcontenía el marcador del incidente - El marcador identificaba
STORE.PAYMENTS - La descripción confirmaba el incidente
DL-01
El significado y la relevancia de esa tabla fueron inferidos por el agente. A partir de ahí, el agente comprobó que:
- La tabla
STORE.PAYMENTSya no existía y la información esperada en el catálogo de Db2 había desaparecido - Los demás objetos del esquema
STOREseguían presentes
Esto llevó a la primera conclusión importante: no se trataba de un fallo a nivel de todo el esquema, sino de la pérdida de un objeto específico. La investigación se centró entonces en ese objeto:
El agente resumió la situación así:
| Ítem | Detalle |
|---|---|
| Tabla eliminada | STORE.PAYMENTS |
| Número conocido de filas antes de la pérdida | 19 filas |
| Momento del drop | 2026-05-20 20:35:40 |
| Backup disponible | Backup completo en /database/backup |
| Momento del backup | 20:19:51, antes de la creación del esquema STORE |
| Archive logging | LOGARCHMETH1=LOGRETAIN |
| Ubicación de los logs activos | /database/data/db2inst1/NODE0000/SQL00001/LOGSTREAM0000/ |
| Síntoma inmediato | SQL0204N al acceder a STORE.PAYMENTS |
| Evidencia adicional | Marcador en AUDIT_LOG, ausencia de entradas de catálogo para la tabla y sus índices |
La primera vía de recuperación parecía prometedora
La siguiente pregunta era si la tabla podía recuperarse mediante el mecanismo normal de restore y rollforward de Db2.
El entorno tenía un backup completo en /database/backup y archive logging activo con LOGARCHMETH1=LOGRETAIN.
En ese momento, la situación parecía favorable. Un patrón común de recuperación sería:
| Paso | Acción de recuperación común | Objetivo |
|---|---|---|
| 1 | Restaurar la base de datos en una base de recuperación separada | Trabajar sobre una copia segura en lugar del sistema live |
| 2 | Hacer rollforward hasta un punto inmediatamente anterior al drop | Recuperar el estado del objeto antes del incidente |
| 3 | Exportar la tabla recuperada | Extraer los datos recuperados de la base lateral |
| 4 | Recrearla o importarla de nuevo en la base live | Reponer el objeto recuperado en producción |
Este es un enfoque técnico común y perfectamente razonable. Después de eso, el caso se volvió más interesante.
La primera complicación: el backup era demasiado antiguo
El backup existía, pero se había tomado antes de que todo el esquema STORE hubiera sido creado. Restaurar ese backup por sí solo no devolvería la tabla STORE.PAYMENTS. La única vía viable pasaba a depender del rollforward de los logs.
Aun así, seguía pareciendo que la tabla y los datos eran recuperables porque:
- El archive logging estaba activo
- Los logs necesarios para aplicar las modificaciones posteriores al backup se habían conservado
El plan construido por el agente
Con esa evidencia, el agente elaboró el siguiente plan:
| Paso | Acción planificada | Resultado pretendido |
|---|---|---|
| 1 | Restaurar el backup antiguo en una base de recuperación separada | Evitar tocar la base live durante la investigación |
| 2 | Hacer rollforward de esa base hasta inmediatamente antes del drop | Recrear el estado previo al incidente si la granularidad temporal de Db2 lo permitía |
| 3 | Recuperar allí la definición y los datos de la tabla | Usar la copia de recuperación como fuente de extracción |
| 4 | Transferir el objeto recuperado a la base live | Restablecer el servicio en LABDB |
| 5 | Validar integridad y limpiar | Dejar la base de producción en un estado fiable |
Esto ya era un razonamiento sólido de DBA y, de no haber existido las limitaciones de Db2 en recuperación point-in-time, la historia habría terminado ahí.
Pero no terminó.
El verdadero problema en Db2: el PITR no conseguía aislar las filas
El agente restauró el backup en una base de datos de recuperación separada y aplicó rollforward.
Lo que encontró fue una dificultad sutil:
- El DDL de
CREATE TABLE STORE.PAYMENTSera recuperable - Sin embargo, las filas de la tabla no eran recuperables
¿Por qué?
Porque la carga de la tabla y el drop fueron ambos confirmados (committed) dentro del mismo segundo.
La línea temporal relevante era:
| Hora | Evento | Significado |
|---|---|---|
20:32:09 | CREATE TABLE PAYMENTS | La definición de la tabla entra en los logs mucho antes del segundo crítico |
20:35:40.xxx | INSERT INTO PAYMENTS (...) | Las 19 filas de pagos son confirmadas |
20:35:40.995 | DROP TABLE PAYMENTS | La tabla es eliminada en el mismo segundo del insert |
El rollforward point-in-time de Db2 acepta timestamps con granularidad mínima de un segundo.
Eso significa que las opciones reales de recuperación eran:
| Objetivo de rollforward | Resultado |
|---|---|
20:35:39 | Todo lo confirmado hasta 20:35:39; las filas aún no existen |
20:35:40 | Todo lo confirmado hasta 20:35:40; incluye el INSERT y el DROP |
| fin de los logs | El mismo resultado práctico que 20:35:40; el drop sigue prevaleciendo |
No existía ningún objetivo válido que incluyera las filas insertadas pero excluyera el drop, y aquí está el verdadero corazón técnico del caso.
El fallo ya no era:
- “¿Cómo recuperar una tabla eliminada?”
Pasaba a ser:
- “¿Cómo recuperar cuando la recuperación física point-in-time puede devolver la estructura pero ya no el conjunto original de filas?”
Ese es un problema mucho más difícil.
Lo que Db2 todavía pudo recuperar
Incluso sin conseguir aislar las filas, la base de datos de recuperación todavía dio al agente algo muy valioso:
- La definición exacta de la tabla
- La clave primaria
- La clave foránea
- Los check constraints
- La definición del índice existente sobre la tabla
El agente extrajo esa definición con db2look y recreó la tabla en la base de datos real. Después de eso, la estructura de la tabla estaba recuperada, pero los datos seguían faltando.
El agente no se detuvo. Fue más allá. El registro de ejecución hasta ese momento puede resumirse así:
| Paso | Acción | Resultado |
|---|---|---|
| 1 | Backup completo identificado en /database/backup | Backup válido encontrado, pero demasiado antiguo para recuperación directa del objeto |
| 2 | LOGARCHMETH1=LOGRETAIN confirmado | La vía de recuperación por logs seguía siendo viable |
| 3 | Logs archivados y activos consolidados | La base de recuperación pasó a tener el material necesario para PITR |
| 4 | Redirected restore a LABRCVR | Éxito |
| 5 | Rollforward hasta inmediatamente antes del drop | DDL recuperable, datos aún ausentes |
| 6 | db2look sobre LABRCVR | DDL exacto extraído, incluyendo PK, FK, checks e índice |
| 7 | Tabla recreada en LABDB live | Estructura restaurada con exactitud |
El paso decisivo: reconstrucción de los datos recurriendo a la lógica de negocio
Esta es la parte más importante del artículo. El agente cambió el enfoque de “razonar sobre cómo efectuar la recuperación física” a “dado que la recuperación física no es posible, cómo efectuar la reconstrucción de los datos recurriendo a la lógica del negocio”.
Analizó el contexto transaccional superviviente en torno a STORE.PAYMENTS, en especial STORE.ORDERS, e infirió qué pedidos debían tener lógicamente pagos correspondientes.
La interpretación central fue:
- Los pedidos
DELIVEREDya deberían tener pagos válidos - Los pedidos
SHIPPEDtambién deberían tener pagos - Los pedidos
PROCESSING,PENDINGyCANCELLEDno deberían tener pagos
Eso produjo:
| Estado del pedido | Pedidos | Filas de pago reconstruidas |
|---|---|---|
DELIVERED | 1–12, 31–33 | 15 |
SHIPPED | 13–16 | 4 |
PROCESSING | 17–20, 36–37 | 0 |
PENDING | 21–26, 38–40 | 0 |
CANCELLED | 27–30 | 0 |
Total:
15 + 4 = 19
El valor coincidía exactamente con el número conocido de filas perdidas.
El agente reconstruyó entonces:
AMOUNTa partir deSTORE.ORDERS.TOTAL_AMOUNT- Fechas de pago plausibles en relación con las fechas de los pedidos
- Métodos de pago permitidos
- Valores válidos para el estado del pago
- Referencias sintácticamente plausibles
En resumen:
- La recuperación Db2 devolvió la definición de la tabla
- El agente reconstruyó un conjunto de filas consistente con el negocio
Como regla general, una herramienta no basada en IA no conseguiría hacerlo por sí sola.
Es importante tener en cuenta que el agente, haciendo lo mejor que podía, no puede hacer milagros. La reconstrucción fue una medida de recurso y no puede compararse con una recuperación exacta y completa de los datos. Los valores originales de algunas columnas se perdieron definitivamente, entre ellos:
- Timestamps exactos de los pagos
- Métodos de pago originales por pedido
- Identificadores de referencia originales
Los detalles de la recuperación y reconstrucción se muestran a continuación:
| Aspecto | ¿Recuperación exacta? | Observaciones |
|---|---|---|
| Definición de la tabla | Sí | Recuperada de los logs y extraída con db2look |
| Clave primaria | Sí | Recreada exactamente |
| Clave foránea | Sí | Recreada exactamente |
| Check constraints | Sí | Recreados exactamente |
| Definición del índice | Sí | Recreada exactamente |
| Importes originales de los pagos | En la práctica, sí | Derivados directamente de STORE.ORDERS.TOTAL_AMOUNT |
| Timestamps originales de los pagos | No | Reconstruidos como valores plausibles |
| Métodos originales de pago | No | Reconstruidos dentro de los valores permitidos |
| Referencias originales de pago | No | Reconstruidas como identificadores plausibles |
| Identidad y número original de filas | Parcialmente | El recuento coincidió exactamente; el contenido fue fiel al negocio, no original |
Validación después de la reconstrucción
El agente no se detuvo después de insertar 19 filas plausibles. Validó que:
- La tabla recreada correspondía al DDL original
- Las relaciones de clave foránea seguían siendo válidas
- No existían referencias huérfanas
- Las estadísticas habían sido refrescadas con
RUNSTATS
El estado final quedó así:
STORE.PAYMENTSvolvió a existir19filas fueron repuestas de forma consistente con el negocio- La integridad referencial quedó limpia
- La recuperación fue registrada en
STORE.AUDIT_LOG
Desde el punto de vista operativo, la tabla volvió a ser utilizable.
El resumen final de la recuperación puede presentarse así:
| Paso | Acción | Resultado |
|---|---|---|
| 1 | Redirected restore y PITR a LABRCVR | Definición de la tabla recuperada, filas aún ausentes |
| 2 | Extracción con db2look desde LABRCVR | DDL exacto capturado |
| 3 | Tabla recreada en LABDB live | Estructura de producción repuesta |
| 4 | 19 filas reconstruidas a partir del patrón de negocio en STORE.ORDERS | Datos faltantes repuestos de forma semánticamente consistente |
| 5 | Validación de FK y verificaciones de integridad | Sin filas huérfanas |
| 6 | Ejecución de RUNSTATS | Estadísticas del optimizador refrescadas |
| 7 | Registro de la recuperación y limpieza de la base temporal | Incidente cerrado con trazabilidad |
Una vía técnica mejor habría sido el análisis forense de logs
Aunque la reconstrucción tuvo éxito y tiene interés didáctico, ese no sería el camino habitual a seguir.
La mejor vía habría sido el análisis forense de los logs de Db2, porque eso habría permitido al agente extraer los valores originales de las filas insertadas en lugar de reconstruirlos a partir de la lógica de negocio.
En el momento del incidente, esa opción no estaba disponible a través de los servidores MCP. Las herramientas expuestas por el servidor soportaban investigación, restore, rollforward, inspección del catálogo y extracción de DDL, pero todavía no exponían utilidades forenses para leer el contenido bruto de los logs.
Esa limitación ya ha sido corregida.
db2fmtlog puede ayudar a formatear e inspeccionar contenido de bajo nivel de los logs durante la investigación forense.
Las capacidades que se añadieron son:
| Herramienta | Objetivo |
|---|---|
forensic_log_format | Envuelve db2fmtlog para formatear contenido de bajo nivel de los logs, de modo que pueda ser inspeccionado y consumido por el workflow de recuperación |
recover_dropped_table | Orquesta tanto la ruta estándar RESTORE → ROLLFORWARD → EXPORT como la ruta forense cuando se detecta una colisión en el mismo segundo |
Para este incidente en la tabla PAYMENTS, el workflow mejorado habría funcionado de la siguiente manera:
| Paso | Acción del workflow forense | Objetivo |
|---|---|---|
| 1 | Detectar que el INSERT y el DROP colisionaron dentro del mismo segundo | Reconocer que el PITR no consigue aislar las filas deseadas |
| 2 | Reunir la ventana exacta de logs y los metadatos del objeto | Delimitar la investigación forense a la evidencia relevante |
| 3 | Realizar un análisis forense real de bajo nivel sobre esa ventana | Recuperar los valores originales insertados si la herramienta subyacente, incluida la inspección formateada con db2fmtlog, lo permite |
| 4 | Decodificar los valores recuperados basándose en el DDL de PAYMENTS | Volver a mapear los valores a columnas y tipos Db2 |
| 5 | Reconstruir sentencias INSERT exactas con los valores originales | Recuperar las filas reales en lugar de aproximarlas |
El resultado esperado sería:
| Etapa | Resultado forense |
|---|---|
| Clasificación del incidente | Colisión en el mismo segundo detectada y señalada como no resoluble solo mediante PITR |
| Extracción forense | Valores originales de las filas recuperados, si la vía de análisis de bajo nivel los soporta |
| Decodificación guiada por DDL | Columnas mapeadas de nuevo a PAYMENT_ID, ORDER_ID, PAYMENT_DATE, AMOUNT, METHOD, STATUS y REFERENCE |
| Recuperación final | INSERT exactos reconstruidos con los timestamps, métodos y referencias originales |
La mejora esencial que ahora se ha añadido es:
- la lógica estándar de recuperación Db2 sigue siendo la primera ruta
- la investigación forense basada en
db2fmtlogpasa a ser el fallback cuando el PITR de Db2 no puede aislar las filas deseadas
Con esta adición, el agente ya no necesita detenerse en la reconstrucción fiel al negocio en escenarios de colisión dentro del mismo segundo. Puede intentar primero la recuperación exacta de las filas y recurrir a la reconstrucción solo si la extracción forense también falla.
Lo que ocurrió después de la mejora del MCP
Después de la mejora del MCP server, el incidente fue revisitado y el agente consiguió recuperar la tabla con las 19 filas originales exactas, en lugar de solo con una aproximación basada en el análisis de datos de negocio.
Esta segunda recuperación es importante porque muestra la diferencia entre:
- Una reconstrucción estructuralmente correcta
- Y una recuperación verdadera de los valores originales de las filas
El post-mortem puede resumirse así:
| Tema | Lo que ocurrió |
|---|---|
| Incidente | STORE.PAYMENTS fue eliminada en 2026-05-20 20:35:40 |
| Resultado inicial | La tabla fue reconstruida con 19 filas aproximadas basadas en la lógica de negocio |
| Problema restante | Algunos valores reconstruidos eran incorrectos, en especial la fila 13 y las filas 17–19 |
| Limitación de base | El PITR seguía sin poder aislar el INSERT del DROP, porque ambos ocurrieron dentro del mismo segundo |
| Camino mejorado | Se utilizó análisis del contenido de los logs para recuperar los valores originales |
1. Identificar el fichero de log correcto
El primer problema práctico fue la escala de los datos:
- Existían 26 ficheros de log entre ubicaciones activas y archivadas
- Una pasada forense sin filtros producía demasiado output
El agente utilizó las secciones XHDR de la salida de db2fmtlog para mapear intervalos de LFS a ficheros físicos. Los registros INSERT de interés tenían:
LFS = 6976
Eso permitió mapear la actividad relevante a:
S0000007.LOG
que resultó ser un fichero del log activo.
2. Localizar exactamente los 19 registros INSERT
El siguiente paso fue encontrar los offsets exactos en bytes de las 19 filas insertadas dentro de S0000007.LOG.
db2fmtlog proporcionó la metainformación necesaria para orientar la búsqueda:
- Transaction ID
- Tipo de registro
- Identidad del objeto
La búsqueda binaria pasó entonces a buscar la combinación de:
- Los bytes de
TIDde la transacción000000000C6D - El marcador
INSREC_DPasociado al objeto de destino
Esto permitió localizar los 19 registros en offsets exactos dentro del fichero de log de 16 KB.
3. Decodificar correctamente el formato de la fila
Este fue el paso más difícil, porque el formato interno de la fila en Db2 no estaba documentado de una manera amigable.
Fue necesario corregir varios errores de decodificación:
| Sub-problema | Error inicial | Corrección final |
|---|---|---|
| Offset de inicio de la fila | Los primeros offsets producían basura | El inicio correcto de la fila era pos + 32 |
Decodificación de DECIMAL(12,2) | El packed BCD se leyó con alineación de nibble incorrecta | El primer nibble era padding y debía ignorarse |
| Columnas de longitud variable | Inicialmente se asumió una tabla de offsets al final | La fila real usaba campos explícitos de 2 bytes con longitudes antes del texto |
| Timestamp de la fila 13 | Los bytes donde se esperaba el timestamp mostraban BCD inválido | La fila 13 contenía 20 bytes extra de overhead de log antes de los datos de columna |
Después de estas correcciones, el agente consiguió decodificar correctamente los valores originales.
4. Aplicar los valores exactos de vuelta a la tabla live
Todavía fue necesario resolver dos problemas operativos:
| Problema | Resolución |
|---|---|
La herramienta execute_query solo permitía instrucciones SELECT | El DML se aplicó mediante docker exec y el CLP de Db2 |
Eran necesarios INSERT explícitos en una columna identity GENERATED ALWAYS | PAYMENT_ID fue cambiada temporalmente a GENERATED BY DEFAULT y luego restaurada a GENERATED ALWAYS |
La vía final de inserción utilizó un fichero de script CLP con db2 -tvf, porque el quoting directo de INSERT multilínea mediante docker exec ... bash -c se volvió demasiado frágil.
Lo que la primera reconstrucción había supuesto erróneamente
La reconstrucción inicial basada en lógica de negocio fue suficiente para restaurar el servicio, pero no era exacta.
La recuperación forense posterior mostró claramente las diferencias:
| Fila | Campo | Reconstruido | Real |
|---|---|---|---|
| 13 | PAYMENT_DATE | 09:00 | 17:00 |
| 13 | AMOUNT | 99.98 | 179.99 |
| 17 | ORDER_ID | 17 | 31 |
| 18 | ORDER_ID | 18 | 32 |
| 19 | ORDER_ID | 19 | 33 |
La primera reconstrucción había acertado:
- Los patrones de los métodos
- Los valores de estado
- La estructura de las referencias
Pero seguía fallando en valores originales exactos, en puntos a los que solo un análisis real de bajo nivel sobre los logs podía llegar.
Estado final después de la recuperación mejorada
Después de la mejora del MCP y de la nueva ruta de recuperación:
STORE.PAYMENTSvolvió a contener 19 filas- Las filas correspondían a los valores originales exactos existentes antes del drop
- El comportamiento de la identity fue restaurado a
GENERATED ALWAYS - No fue necesario ningún restore de backup para la etapa final de recuperación exacta de los datos
Esto hace que la lección principal del caso sea todavía más clara:
- La reconstrucción mediante lógica de negocio ya era valiosa
- Pero, después de la mejora del MCP, el agente consiguió ir más allá de la aproximación y recuperar exactamente el conjunto original de filas
Por qué este caso importa
Cuando la recuperación convencional encontró un límite real de Db2, el agente de IA consiguió seguir razonando sobre la semántica del negocio en lugar de detenerse en la frontera técnica.
Esto exigió tres niveles distintos de capacidad:
1. Herramientas de investigación Db2
- Verificaciones de catálogo
- Inspección de configuración
- Comprensión de backups y logs
- Ejecución de recuperación
2. Conocimiento de recuperación en Db2
- Redirected restore
- Limitaciones del rollforward
- Extracción de DDL
- Pasos de validación
3. Inferencia mediante lógica de negocio
- Comprender qué representa una tabla de pagos
- Inferir qué pedidos deberían tener pagos
- Reconstruir filas coherentes con la evidencia de negocio superviviente
Cuando el MCP server mejoró, esa pila de capacidades se amplió con análisis forense real de bajo nivel. La combinación se volvió aún más fuerte:
- conocimiento de recuperación Db2 para restaurar la estructura
- razonamiento de lógica de negocio para proporcionar una vía de fallback
- análisis forense de logs para recuperar los valores exactos cuando la vía estándar no podía
Eso es lo que hizo que el resultado final fuera especialmente relevante:
- el agente primero restauró el servicio mediante reconstrucción
- y más tarde, una vez mejorada la herramienta MCP, recuperó exactamente las filas originales
Referencias
- IBM Docs: ROLLFORWARD DATABASE command
- IBM Docs: db2look command
- IBM Docs: RECOVER DROPPED TABLE command