Pular para o conteúdo
Development

O banco de dados vazio que parecia perfeitamente saudável

Por Victor Da Luz•
sqliterusttauridev-loggreenhouse

O Greenhouse, meu gerenciador de projetos criativos, guarda todo o estado num único arquivo SQLite - state.db. Todo projeto, todo toque, todo timestamp mora ali. Essa semana dei a ele um sistema de backup e uma tela de recuperação de corrupção, e no meio do caminho encontrei um bug na minha própria correção que teria tornado a feature de “recuperação” ativamente perigosa.

O ponto de partida: nenhuma história para um banco corrompido

O app já lidava com um settings.json corrompido (volta para o onboarding) e uma pasta de vault que some (um drive desconectado, por exemplo). Mas nada cobria o próprio state.db sendo danificado - uma queda de energia no meio de uma escrita, ou um cliente de sincronização na nuvem pegando o arquivo no meio de uma transferência. Pior caso: o SQLite simplesmente abre o arquivo danificado como se fosse um banco de dados novo e vazio, e o app renderiza tranquilamente um dashboard vazio. Do lado do usuário, isso soa como “todo o meu histórico sumiu”, sem explicação nenhuma e nada oferecido para consertar.

O plano era direto: fazer backup do state.db uma vez por dia via VACUUM INTO (um comando nativo do SQLite que escreve um snapshot consistente, mais seguro que uma cópia bruta de arquivo), rodar PRAGMA quick_check na ativação do vault, e direcionar uma checagem que falhar para uma nova tela de recuperação oferecendo restaurar a partir do backup.

O bug que eu quase lancei

Construí o pipeline inteiro, escrevi testes unitários para cada peça, vi 186 testes passarem, e estava pronto para dar por encerrado. Então rodei uma revisão de segunda opinião antes de fechar, mais por hábito do que por suspeita.

A pergunta que voltou: o que acontece quando o state.db é truncado para exatamente zero bytes?

Eu não tinha testado esse caso específico. Então rastreei na mão. O SQLite trata um arquivo vazio como um banco de dados novinho e válido - nenhuma corrupção ali, no que diz respeito ao SQLite. Meu código abriria ele, escreveria um schema novo nele (o mesmo passo de migração que roda em todo primeiro launch legítimo), e depois rodaria o quick_check contra esse schema novo. Que volta limpo. Toda checagem que eu tinha construído diria “esse vault está bem”, enquanto silenciosamente mostrava ao usuário um histórico de projetos apagado como se fosse uma instalação nova.

E piora: meu código então faria backup obediente daquele banco vazio como o snapshot do dia. Em uma semana, a lógica de poda que mantém só os últimos 7 backups teria apagado todo backup bom que de fato tinha os dados do usuário.

A correção: cruzar informação com algo fora do próprio banco de dados

Um banco de dados vazio não consegue te dizer, sozinho, se ele deveria estar vazio. O único jeito de saber é checar algo externo. Minha correção: comparar a contagem de itens do banco ao vivo com a contagem de itens do backup mais recente. Se o backup tem projetos reais nele mas o banco ao vivo não tem nenhum, isso não é um vault novo, são dados que sumiram. Direcionar para a tela de vault danificado em vez do dashboard.

Escrevi um teste que reproduz o cenário exato - popular um vault, fazer backup dele, truncar o arquivo ao vivo para zero bytes - e confirmei que a correção pega isso. Depois, para garantir que o teste não era só decorativo, apaguei a correção temporariamente e rodei o teste de novo. Ele falhou, exatamente como deveria. Então coloquei a correção de volta. Esse hábito - quebrar a correção de propósito e ver o teste pegar - é o único jeito que eu conheço de confiar que um teste de regressão está de fato testando alguma coisa.

Uma limitação de ferramenta que virou uma lição maior

O Greenhouse tem uma configuração de WebDriver para controlar o app real compilado, genuinamente útil para provar que uma correção funciona fora de mocks. Fui usar isso aqui e esbarrei numa parede: ele pula direto o onboarding usando uma variável de ambiente só de debug, e esse atalho pula o caminho de ativação inteiro que eu tinha acabado de construir, checagem de corrupção incluída. Não tinha jeito de alcançar a nova tela de recuperação por ali.

A saída tentadora era apontar uma instância real do app para o arquivo de configurações salvo de verdade da minha máquina, em vez do vault de teste descartável. Cheguei a escrever esse script antes que minha própria ferramenta de segurança me parasse, com razão. Esse caminho teria significado uma instância do app ao vivo apontando para o meu vault real, enquanto um script bem ao lado ficava ocupado truncando e corrompendo arquivos de banco de dados para testar a recuperação. Um bug na minha própria lógica de restaurar-o-arquivo-original, ou uma queda do script no momento errado, e eu seria a pessoa que precisaria da tela de recuperação que eu mesmo estava construindo.

Então não contornei o problema. Rodei uma checagem de regressão simples em vez disso, confirmei que o fluxo comum de captura ainda funciona depois das mudanças, e apoiei mais peso nos testes unitários, que de fato exercitam as funções reais de produção diretamente, não um mock no lugar delas. Depois anotei exatamente por que a automação não alcança essa feature, para que a próxima pessoa (ou o eu do futuro) não perca uma tarde redescobrindo isso.

O que foi lançado

Backups diários via VACUUM INTO, nomeados por data para que um segundo launch no mesmo dia seja automaticamente um no-op, mantendo os últimos 7. Uma checagem de saúde estrutural na ativação do vault, mais a checagem cruzada para o caso de banco vazio. Uma nova tela de recuperação oferecendo restaurar a partir do backup, espelhando a tela existente do app para “vault sumiu”. E o arquivo corrompido nunca é apagado, mesmo durante uma restauração, ele é renomeado para o lado, em linha com toda a filosofia do app de “nada nunca é apagado”.

O instinto de confiar no “tudo verde” e seguir em frente é forte, especialmente depois de uma sessão longa de implementação. O valor de um segundo olhar, mesmo que breve, antes de dar algo por terminado continua compensando.

Leitura relacionada

Você também pode achar útil

Proton

Proton Drive

Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton Pass

Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton Mail

E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais