backendpostgres

TEXT Array Antigo Quebra o Dashboard Administrativo

· 5 min de leitura

O dashboard administrativo do Lima ficou fora do ar recentemente, retornando erros 500 em todas as partes. A causa principal foi uma coluna TEXT[] legada, de antes do nosso rebrand, a habits.completed_dates, que ainda armazenava datas como strings no formato ‘YYYY-MM-DD’. Uma query do banco de dados, countHabitsCompletedSince, estava comparando este array de texto com um timestamp, levando a uma incompatibilidade de tipo e a um erro de servidor.

O Problema: Um Dashboard em Crise

Usuários tentando acessar qualquer parte do endpoint /admin/stats se deparavam com o temido erro 500. Isso significava nenhuma visibilidade sobre métricas importantes, uma interrupção significativa para qualquer pessoa que gerenciava dados dentro do app Lima. O sintoma imediato foi uma falha na query countHabitsCompletedSince, que alimenta uma parte central da visualização administrativa.

O grande problema é que essa questão passou por todos os nossos testes. Nossos dados de teste, como a maioria dos datasets bem mantidos, aderiam às nossas premissas de esquema atuais. Eles não carregavam o formato antigo da coluna habits.completed_dates. Apenas linhas de produção específicas, que haviam sido migradas diretamente do aplicativo antigo antes do rebrand MoWave One, continham as strings de data TEXT[] legadas. Isso significava que o bug era uma mina terrestre silenciosa, esperando a forma de dado exata para ser acionada.

A Causa Técnica: Desvio de Formato dos Dados

O cerne do problema estava em como habits.completed_dates estava sendo tratado. Antes do rebrand, essa coluna era um TEXT[] armazenando datas como strings. Pós-rebrand, nossa aplicação esperava e geralmente trabalhava com tipos de data ou timestamp reais para comparações. A query countHabitsCompletedSince tentava comparar elementos desta coluna TEXT[] diretamente com um timestamp. O Postgres, quando confrontado com a comparação de uma string de texto como ‘2023-01-15’ com um timestamp verdadeiro, não a converte implicitamente de uma forma que permita uma operação < ou >= válida. Essa incompatibilidade de tipo fez com que a query lançasse um erro, que então se propagou para um 500 no endpoint da API.

Aqui está uma versão simplificada de como a query problemática se parecia conceitualmente:

SELECT count(*) FROM habits WHERE completed_dates @> ARRAY[CAST(? AS TEXT)] AND d IN (SELECT UNNEST(completed_dates) FROM habits WHERE UNNEST(completed_dates) >= CAST(? AS TIMESTAMP));

O problema estava especificamente na comparação: UNNEST(completed_dates) >= CAST(? AS TIMESTAMP). Comparar um elemento TEXT de completed_dates diretamente com um TIMESTAMP sem a conversão (casting) adequada era o ponto de falha.

A Correção: Casting de Tipo Adequado e Testes de Regressão

A solução envolveu a conversão explícita (casting) das strings de data de texto para o tipo DATE antes da comparação. Isso permitiu que o Postgres realizasse uma comparação de data válida com o timestamp fornecido. A gente modificou a query para garantir que cada elemento d do array completed_dates fosse convertido para o tipo DATE. A lógica corrigida ficou parecida com isso:

SELECT count(*) FROM habits WHERE completed_dates @> ARRAY[CAST(? AS TEXT)] AND EXISTS (SELECT 1 FROM UNNEST(completed_dates) AS d WHERE d::date >= CAST(? AS date));

Essa pequena, mas crucial, mudança garantiu que a comparação d::date >= CAST(? AS date) fosse sempre entre dois tipos DATE, resolvendo o erro. Antes do deploy, a gente validou essa correção contra uma cópia dos nossos dados de produção que continha as linhas legadas problemáticas, confirmando que o dashboard administrativo estava totalmente restaurado.

Para prevenir esse tipo específico de regressão, adicionamos um novo teste de integração, AdminStatsQueryIT. Este teste popula dados explicitamente com o formato de string de data TEXT[] legado na coluna habits.completed_dates. Isso garante que quaisquer futuras mudanças na query ou no esquema serão testadas contra o formato de dados real que causou essa interrupção. Este novo teste fecha a lacuna que permitiu que esse bug chegasse à produção.

Lições Aprendidas

Rebrands e migrações são excelentes oportunidades para minas terrestres de formato de dados. Se uma coluna é anterior às suas premissas de esquema atuais, é muito provável que seus testes existentes não contemplem seu formato antigo. Sempre considere o ciclo de vida completo dos seus dados, especialmente quando ele abrange grandes versões de aplicativos ou rebrands como a transição do MoWave One para o app Lima (https://getlima.app). Construir testes de regressão específicos para esses formatos de dados legados, uma vez identificados, é fundamental para prevenir futuras interrupções. Dados de teste devem idealmente refletir todo o espectro dos dados de produção reais, incluindo anomalias históricas, e não apenas os dados impecáveis que estão em conformidade com o esquema mais recente.