Porque é que este caso foi importante
Este artigo baseia-se num exemplo que ocorreu num ambiente interno de testes onde tínhamos construído:
- Servidores MCP especializados que expunham ferramentas de investigação e recuperação para Db2
- Um agente de IA capaz de utilizar essas ferramentas
- Um catálogo de injeções controladas de falhas para provocar incidentes na base de dados
O objetivo era simples: injetar falhas realistas e verificar se o agente conseguia ajudar um DBA a investigar e recuperar.
Um desses testes tornou-se mais interessante do que esperávamos. Injectámos um cenário de drop de tabela e o agente não se limitou a executar alguns comandos de recuperação. Encontrou uma limitação real de recuperação no Db2, adaptou a estratégia de forma a reconstruir os dados com base na lógica de negócio e aplicou-a. É essa a parte que não é trivial e que vale a pena documentar.
Mais tarde, identificou-se uma solução melhor que a reconstrução dos dados mas que não estava ainda sendo suportada pelos MCP: efetuar análise dos logos do Db2 para reconstruir o SQL responsável pela modificação dos dados perdidos e re-executá-lo. Adicionou-se esta capacidade e testou-se. O Agente de IA consegui recuperar todos os dados perdidos a partir do log do Db2.
A falha injetada
O incidente foi injetado com o seguinte 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"Tudo foi desenhado intencionalmente de forma a obter um incidente simples de drop de tabela:
- Uma única tabela removida, sem chaves externas ou vistas dependentes que complicassem o diagnóstico.
À primeira vista, parecia o tipo de caso que um playbook convencional de recuperação deveria conseguir resolver bem.
O pedido ao agente
Depois da injeção, o utilizador perguntou:
Parece que algumas tabelas foram eliminadas. Investigue, crie um plano e execute o plano para efetuar a recuperação.
Importa analisar a formulação porque o pedido não indica ao agente:
- Qual a tabela eliminada
- Como ocorreu a eliminação
- Qual o caminho de recuperação a seguir
O agente teve de investigar, formar um plano com base na evidência e só depois executar a recuperação. Por outras palavras, tratou-se de uma tarefa de raciocínio, não de uma simples execução de comandos.
O que o agente encontrou primeiro
A primeira descoberta útil estava no rasto deixado pela própria injeção:
- A tabela
STORE.AUDIT_LOGcontinha o marcador do incidente - O marcador identificava
STORE.PAYMENTS - A descrição confirmava o incidente
DL-01
O significado e relevância desta tabela foram inferidos pelo agente. A partir daí, o agente verificou que:
- A tabela
STORE.PAYMENTSjá não existia, tendo desaparecido a informação esperada no catálogo do Db2 - Os outros objetos do schema
STOREcontinuavam presentes
Isto levou à primeira conclusão importante: Não se tratava de uma falha ao nível do schema inteiro mas sim de uma perda de um objeto específico. A investigação focou-se então neste objecto:
O agente resumiu então a situação da seguinte forma:
| Item | Detalhe |
|---|---|
| Tabela eliminada | STORE.PAYMENTS |
| Número conhecido de linhas antes da perda | 19 linhas |
| Momento do drop | 2026-05-20 20:35:40 |
| Backup disponível | Backup completo em /database/backup |
| Momento do backup | 20:19:51, antes da criação do schema STORE |
| Archive logging | LOGARCHMETH1=LOGRETAIN |
| Localização dos logs ativos | /database/data/db2inst1/NODE0000/SQL00001/LOGSTREAM0000/ |
| Sintoma imediato | SQL0204N ao aceder a STORE.PAYMENTS |
| Evidência adicional | Marcador no AUDIT_LOG, ausência de entradas de catálogo para a tabela e os seus índices |
O primeiro caminho de recuperação parecia promissor
A pergunta seguinte era se a tabela podia ser recuperada através do mecanismo normal de restore e rollforward do Db2.
O ambiente tinha um backup completo em /database/backup e archive logging ativo com LOGARCHMETH1=LOGRETAIN
Nessa altura, a situação parecia favorável. Um padrão comum de recuperação seria:
| Passo | Ação de recuperação comum | Objetivo |
|---|---|---|
| 1 | Restaurar a base de dados para uma base de recuperação separada | Trabalhar sobre uma cópia segura em vez do sistema live |
| 2 | Fazer rollforward até um ponto imediatamente anterior ao drop | Recuperar o estado do objeto antes do incidente |
| 3 | Exportar a tabela recuperada | Extrair os dados recuperados da base lateral |
| 4 | Recriar ou importar de volta para a base live | Repor o objeto recuperado em produção |
Esta é uma abordagem técnica comum e perfeitamente razoável. Depois disso, o caso ficou mais interessante.
A primeira complicação: o backup era demasiado antigo
O backup existia, mas tinha sido feito antes de todo o schema STORE ter sido criado. O restore desse backup não devolveria por si a tabela STORE.PAYMENTS. O único caminho viável passava a depender do rollforward dos logs.
Mesmo assim, ainda parecia que a tabela e os dados eram recuperáveis porque:
- O archive logging estava ativo
- Os logs necessários para aplicar as modificações posteriores ao backup tinham sido mantidos
O plano construído pelo agente
Face a essa evidência, o agente elaborou o seguinte plano:
| Passo | Ação planeada | Resultado pretendido |
|---|---|---|
| 1 | Restaurar o backup antigo para uma base de recuperação separada | Evitar tocar na base live durante a investigação |
| 2 | Fazer rollforward dessa base para imediatamente antes do drop | Recriar o estado pré-incidente se a granularidade temporal do Db2 o permitisse |
| 3 | Recuperar aí a definição e os dados da tabela | Usar a cópia de recuperação como fonte de extração |
| 4 | Transferir o objeto recuperado para a base live | Restabelecer o serviço em LABDB |
| 5 | Validar integridade e limpar | Deixar a base de produção num estado fiável |
Isto já era um raciocínio sólido de DBA e não fora as limitações do Db2 em termos de recuperação para ponto no tempo, a história terminaria aí.
Mas não terminou.
O verdadeiro problema no Db2: o PITR não conseguia isolar as linhas
O agente restaurou o backup para uma base de dados de recuperação separada e aplicou rollforward.
O que encontrou foi uma dificuldade subtil:
- O DDL de
CREATE TABLE STORE.PAYMENTSera recuperável - No entanto, as linhas da tabela não eram recuperáveis
Porquê?
Porque o preenchimento da tabela e o drop foram ambos confirmados (commited) no mesmo segundo.
A linha do tempo relevante era:
| Hora | Evento | Significado |
|---|---|---|
20:32:09 | CREATE TABLE PAYMENTS | A definição da tabela entra nos logs muito antes do segundo crítico |
20:35:40.xxx | INSERT INTO PAYMENTS (...) | As 19 linhas de pagamentos são confirmadas |
20:35:40.995 | DROP TABLE PAYMENTS | A tabela é eliminada no mesmo segundo do insert |
O rollforward point-in-time do Db2 aceita timestamps com granularidade mínima de um segundo.
Isso significa que as opções reais de recuperação eram:
| Alvo de rollforward | Resultado |
|---|---|
20:35:39 | Tudo o que foi confirmado até 20:35:39; as linhas ainda não existem |
20:35:40 | Tudo o que foi confirmado até 20:35:40; inclui o INSERT e o DROP |
| fim dos logs | O mesmo resultado prático que 20:35:40; o drop continua a prevalecer |
Não existia nenhum alvo válido que incluísse as linhas inseridas mas excluísse o drop e aqui está o verdadeiro coração técnico do caso.
A falha já não era:
- “Como recuperar uma tabela eliminada?”
Passava a ser:
- “Como recuperar quando a recuperação física point-in-time consegue devolver a estrutura mas já não o conjunto original de linhas?”
Esse é um problema muito mais difícil.
O que o Db2 ainda conseguiu recuperar
Mesmo sem conseguir isolar as linhas, a base de dados de recuperação ainda deu ao agente algo muito valioso:
- A definição exata da tabela
- A chave primária
- A chave externa
- Os check constraints
- A definição do índice existente sobre a tabela
O agente extraiu essa definição com db2look e recriou a tabela na base de dados real. Após isso, a estrutura da tabela estava recuperada mas os dados estavam em falta.
O agente não parou. Ele não desistiu e foi mais longe. O registo de execução até aí podia ser resumido assim:
| Passo | Ação | Resultado |
|---|---|---|
| 1 | Backup completo identificado em /database/backup | Backup válido encontrado, mas demasiado antigo para recuperação direta do objeto |
| 2 | LOGARCHMETH1=LOGRETAIN confirmado | O caminho de recuperação por logs mantinha-se viável |
| 3 | Logs arquivados e ativos consolidados | A base de recuperação passou a ter o material necessário para PITR |
| 4 | Redirected restore para LABRCVR | Sucesso |
| 5 | Rollforward para imediatamente antes do drop | DDL recuperável, dados ainda ausentes |
| 6 | db2look sobre LABRCVR | DDL exato extraído, incluindo PK, FK, checks e índice |
| 7 | Tabela recriada em LABDB live | Estrutura reposta com exatidão |
O passo decisivo: reconstrução dos dados recorrendo a lógica de negócio
Esta é a parte mais importante do artigo. O agente mudou a abordagem de “raciocínio sobre como efetuar a recuperação física” para “uma vez que a recuperação física não é possível, como efetuar a reconstrução dos dados recorrendo a lógica do negócio”.
Analisou o contexto transacional sobrevivente em torno de STORE.PAYMENTS, em especial STORE.ORDERS, e inferiu quais as encomendas que logicamente deveriam ter pagamentos correspondentes.
A interpretação central foi:
- Encomendas
DELIVEREDdevem já ter pagamentos válidos - Encomendas
SHIPPEDtambém devem ter pagamentos - Encomendas
PROCESSING,PENDINGeCANCELLEDnão devem ter pagamentos
Isto produziu:
| Estado da encomenda | Encomendas | Linhas de pagamento reconstruídas |
|---|---|---|
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
O valor coincidia exatamente com o número conhecido de linhas perdidas.
O agente reconstruiu então:
AMOUNTa partir deSTORE.ORDERS.TOTAL_AMOUNT- Datas de pagamento plausíveis em relação às datas das encomendas
- Métodos de pagamento permitidos
- Valores válidos para o estado do pagamento
- Referências sintaticamente plausíveis
Em suma:
- A recuperação Db2 devolveu a definição da tabela
- O agente reconstruiu um conjunto de linhas consistente com o negócio
Uma ferramenta não baseada em IA, em regra, não conseguiria fazer sozinha.
É importante ter em atenção que o Agente, fazendo o melhor que podia, não consegue fazer milagres. A reconstrução foi uma medida de recurso e não pode ser comparada com recuperação exata e completa dos dados. Os valores originais de algumas colunas perderam-se definitivamente, incluindo:
- Timestamps exatos dos pagamentos
- Métodos de pagamento originais por encomenda
- Identificadores de referência originais
Os detalhes da recuperação e reconstrução são apresentados abaixo:
| Aspeto | Recuperação exata? | Observações |
|---|---|---|
| Definição da tabela | Sim | Recuperada dos logs e extraída com db2look |
| Chave primária | Sim | Recriada exatamente |
| Chave externa | Sim | Recriada exatamente |
| Check constraints | Sim | Recriados exatamente |
| Definição do índice | Sim | Recriada exatamente |
| Montantes originais dos pagamentos | Na prática, sim | Derivados diretamente de STORE.ORDERS.TOTAL_AMOUNT |
| Timestamps originais dos pagamentos | Não | Reconstruídos como valores plausíveis |
| Métodos originais de pagamento | Não | Reconstruídos dentro dos valores permitidos |
| Referências originais de pagamento | Não | Reconstruídas como identificadores plausíveis |
| Identidade e número original de linhas | Parcialmente | A contagem coincidiu exatamente; o conteúdo foi fiel ao negócio, não original |
Validação após a reconstrução
O agente não parou depois de inserir 19 linhas plausíveis. Validou que:
- A tabela recriada correspondia ao DDL original
- As relações de chave externa continuavam válidas
- Não existiam referências órfãs
- As estatísticas tinham sido refrescadas com
RUNSTATS
O estado final ficou assim:
STORE.PAYMENTSvoltou a existir19linhas foram repostas em forma consistente com o negócio- A integridade referencial ficou limpa
- A recuperação foi registada em
STORE.AUDIT_LOG
Do ponto de vista operacional, a tabela voltou a ser utilizável.
O resumo final da recuperação pode ser apresentado assim:
| Passo | Ação | Resultado |
|---|---|---|
| 1 | Redirected restore e PITR para LABRCVR | Definição da tabela recuperada, linhas ainda em falta |
| 2 | Extração com db2look a partir de LABRCVR | DDL exato capturado |
| 3 | Tabela recriada em LABDB live | Estrutura de produção reposta |
| 4 | 19 linhas reconstruídas a partir do padrão de negócio em STORE.ORDERS | Dados em falta repostos de forma semanticamente consistente |
| 5 | Validação de FK e verificações de integridade | Sem linhas órfãs |
| 6 | Execução de RUNSTATS | Estatísticas do otimizador refrescadas |
| 7 | Registo da recuperação e limpeza da base temporária | Incidente encerrado com rastreabilidade |
Uma abordagem técnica melhor teria sido a análise forense dos logs
Embora a reconstrução tenha tido sucesso e tenha interesse didático, esse não seria um caminho a percorrer normalmente.
O melhor caminho teria sido a análise forense dos logs do Db2, porque isso teria permitido ao agente extrair os valores originais das linhas inseridas em vez de os reconstruir com base na lógica de negócio.
Na altura do incidente, essa opção não estava disponível através dos servidores MCP. As ferramentas expostas pelo servidor suportavam investigação, restore, rollforward, inspeção de catálogo e extração de DDL, mas ainda não expunham utilitários forenses de leitura dos logs brutos.
Essa limitação foi entretanto corrigida.
O db2fmtlog pode ajudar a formatar e inspecionar conteúdo de baixo nível dos logs durante a investigação forense.
As capacidades que foram adicionadas:
| Ferramenta | Objetivo |
|---|---|
forensic_log_format | Encapsula db2fmtlog para formatar conteúdo de baixo nível dos logs de modo a que possa ser inspecionado e consumido pelo workflow de recuperação |
recover_dropped_table | Orquestra tanto o caminho standard RESTORE → ROLLFORWARD → EXPORT como o caminho forense quando é detetada uma colisão no mesmo segundo |
Para este incidente na tabela PAYMENTS, o workflow melhorado teria funcionado da seguinte maneira:
| Passo | Ação do workflow forense | Objetivo |
|---|---|---|
| 1 | Detectar que o INSERT e o DROP colidiram dentro do mesmo segundo | Reconhecer que o PITR não consegue isolar as linhas pretendidas |
| 2 | Recolher a janela exata de logs e os metadados do objeto | Delimitar a investigação forense à evidência relevante |
| 3 | Efetuar uma análise forense real de baixo nível sobre essa janela | Recuperar os valores originais inseridos, se a ferramenta subjacente, incluindo inspeção formatada com db2fmtlog, o permitir |
| 4 | Descodificar os valores recuperados com base no DDL de PAYMENTS | Mapear novamente os valores para colunas e tipos Db2 |
| 5 | Reconstruir instruções INSERT exatas com os valores originais | Recuperar as linhas reais em vez de as aproximar |
O resultado esperado seria:
| Etapa | Resultado forense |
|---|---|
| Classificação do incidente | Colisão no mesmo segundo detetada e sinalizada como não resolúvel apenas por PITR |
| Extração forense | Valores originais das linhas recuperados, se o caminho de análise de baixo nível os suportar |
| Descodificação guiada por DDL | Colunas mapeadas de novo para PAYMENT_ID, ORDER_ID, PAYMENT_DATE, AMOUNT, METHOD, STATUS e REFERENCE |
| Recuperação final | INSERT exatos reconstruídos com os timestamps, métodos e referências originais |
- A lógica standard de recuperação Db2 continua a ser o primeiro caminho
- A investigação forense baseada no
db2fmtlogpassa a ser o fallback quando o PITR do Db2 não consegue isolar as linhas pretendidas
Com esta adição, o agente deixará de ter de parar na reconstrução fiel ao negócio em cenários de colisão no mesmo segundo. Poderá tentar primeiro a recuperação exata das linhas e só recorrer à reconstrução se a extração forense também falhar.
O que aconteceu depois da melhoria do MCP
Depois da melhoria do MCP server, o incidente foi revisitado e o agente conseguiu recuperar a tabela com as 19 linhas originais exatas, em vez de apenas com uma aproximação baseada na análise de dados de negócio.
Esta segunda recuperação é importante porque mostra a diferença entre:
- Uma reconstrução estruturalmente correta
- E uma recuperação verdadeira dos valores originais das linhas
O pós-mortem pode ser resumido assim:
| Tópico | O que aconteceu |
|---|---|
| Incidente | STORE.PAYMENTS foi eliminada em 2026-05-20 20:35:40 |
| Resultado inicial | A tabela foi reconstruída com 19 linhas aproximadas com base na lógica de negócio |
| Problema remanescente | Alguns valores reconstruídos estavam errados, em especial a linha 13 e as linhas 17–19 |
| Limitação de base | O PITR continuava sem conseguir isolar o INSERT do DROP, porque ambos ocorreram no mesmo segundo |
| Caminho melhorado | Foi usada análise do conteúdo dos logs para recuperar os valores originais |
1. Identificar o ficheiro de log correto
O primeiro problema prático foi a escala dos dados:
- Existiam 26 ficheiros de log entre localizações ativas e arquivadas
- Uma passagem forense sem filtros produzia demasiado output
O agente usou as secções XHDR da saída de db2fmtlog para mapear intervalos de LFS para ficheiros físicos. Os registos INSERT de interesse tinham:
LFS = 6976
Isso permitiu mapear a atividade relevante para:
S0000007.LOG
que se revelou ser um ficheiro do log ativo.
2. Localizar exatamente os 19 registos INSERT
O passo seguinte foi encontrar os offsets exatos em bytes das 19 linhas inseridas dentro de S0000007.LOG.
O db2fmtlog forneceu a meta-informação necessária para orientar a pesquisa:
- Transaction ID
- Tipo de registo
- Identidade do objeto
A pesquisa binária passou então a procurar a combinação de:
- Os bytes de
TIDda transação000000000C6D - O marcador
INSREC_DPassociado ao objeto de destino
Isto permitiu localizar os 19 registos nos offsets exatos dentro do ficheiro de log de 16 KB.
3. Descodificar corretamente o formato da linha
Este foi o passo mais difícil, porque o formato interno da linha no Db2 não estava documentado de forma amigável.
Foi necessário corrigir vários erros de descodificação:
| Sub-problema | Erro inicial | Correção final |
|---|---|---|
| Offset de início da linha | Os primeiros offsets produziam lixo | O início correto da linha era pos + 32 |
Descodificação de DECIMAL(12,2) | O packed BCD foi lido com alinhamento de nibble errado | O primeiro nibble era padding e tinha de ser ignorado |
| Colunas de comprimento variável | A estrutura foi inicialmente assumida como tendo uma tabela de offsets no fim | A linha real usava campos explícitos de 2 bytes com comprimentos antes do texto |
| Timestamp da linha 13 | Os bytes onde se esperava o timestamp revelaram formato BCD inválido | A linha 13 continha 20 bytes extra de overhead de log antes dos dados de coluna |
Depois dessas correções, o agente conseguiu descodificar corretamente os valores originais.
4. Aplicar os valores exatos de volta à tabela live
Ainda foi preciso ultrapassar dois problemas operacionais:
| Problema | Resolução |
|---|---|
A ferramenta execute_query permitia apenas instruções SELECT | O DML foi aplicado por docker exec e pelo CLP do Db2 |
Eram necessários INSERT explícitos numa coluna identity GENERATED ALWAYS | PAYMENT_ID foi alterada temporariamente para GENERATED BY DEFAULT e depois restaurada para GENERATED ALWAYS |
O caminho final de inserção usou um ficheiro de script CLP com db2 -tvf, porque o quoting direto de INSERT multi-linha através de docker exec ... bash -c se tornou demasiado frágil.
O que a primeira reconstrução tinha assumido erradamente
A reconstrução inicial por lógica de negócio foi suficiente para repor o serviço, mas não era exata.
A recuperação forense posterior mostrou claramente as diferenças:
| Linha | Campo | Reconstruído | 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 |
A primeira reconstrução tinha acertado:
- Os padrões dos métodos
- Os valores de estado
- A estrutura das referências
Mas continuava a falhar em valores originais exatos, em pontos onde só uma análise real de baixo nível aos logs podia chegar.
Estado final depois da recuperação melhorada
Depois da melhoria do MCP e do novo caminho de recuperação:
STORE.PAYMENTSvoltou a conter 19 linhas- As linhas correspondiam aos valores originais exatos existentes antes do drop
- O comportamento da identity foi restaurado para
GENERATED ALWAYS - Não foi necessário qualquer restore de backup para a etapa final de recuperação exata dos dados
Isto torna a lição principal do caso ainda mais clara:
- A reconstrução por lógica de negócio já era valiosa
- Mas, depois da melhoria do MCP, o agente conseguiu ir além da aproximação e recuperar exatamente o conjunto original de linhas
Porque é que este caso importa
Quando a recuperação convencional encontrou um limite real do Db2, o agente de IA conseguiu continuar raciocinando sobre a semântica do negócio em vez de parar na fronteira técnica
Isto exigiu três níveis distintos de capacidade:
1. Ferramentas de investigação Db2
- Verificações de catálogo
- Inspeção de configuração
- Compreensão de backups e logs
- Execução de recuperação
2. Conhecimento de recuperação em Db2
- Redirected restore
- Limitações do rollforward
- Extração de DDL
- Passos de validação
3. Inferência por lógica de negócio
- Compreender o que representa uma tabela de pagamentos
- Inferir que encomendas deveriam ter pagamentos
- Reconstruir linhas coerentes com a evidência de negócio que sobreviveu
As ferramentas tradicionais são fortes nos dois primeiros níveis.
O valor distintivo da IA apareceu no terceiro. O LLM usado foi o Claude Sonnet 4.6 da Anthropic.
Referências
- IBM Docs: ROLLFORWARD DATABASE command
- IBM Docs: db2look command
- IBM Docs: RECOVER DROPPED TABLE command