Depois de uma cheia catastrófica ter destruído o nosso armazém e nos ter obrigado a reconstruir do zero, decidi que desta vez ia fazer as coisas como deve ser — migrar das folhas de cálculo para uma base de dados a sério, construir um ERP em condições e nunca mais perder o controlo dos nossos dados. Esta é a história dessa migração.
Contexto: A Cheia Que Mudou Tudo
Em abril de 2024, mudámo-nos para um armazém industrial em Alfafar, Valencia, para escalar as operações. Seis meses depois, em outubro de 2024, a DANA — um fenómeno meteorológico catastrófico — atingiu Valencia. O nosso armazém ficava numa das zonas mais afetadas. A água das cheias destruiu o stock, os equipamentos e meses de dinâmica operacional.
O que se seguiu foram dois meses de gestão de crise que nada tinham a ver com código. Nas semanas a seguir à cheia, voluntários e militares vieram ajudar-nos a recuperar as operações do armazém — inventário, equipamentos, limpeza e logística. Depois vieram os pedidos de indemnização ao seguro, os orçamentos de emergência sem qualquer receita e o reset mental de ver tudo o que tínhamos construído debaixo de água. Foi o período mais difícil da minha vida.
Mas foi também um recomeço. Quando reconstruímos — e reconstruímos mesmo — tomei uma decisão: desta vez íamos fazer as coisas como deve ser. Acabou-se a fita-cola. Acabou-se o "já chega". Reconstruímos a equipa de 2 para 5 colaboradores e comprometi-me a construir uma infraestrutura em condições: uma base de dados a sério, um ERP a sério, documentação a sério. A era do Google Sheets tinha terminado.
O Ponto de Partida: Porque é Que o Google Sheets Tinha de Desaparecer
Quando fundei a empresa, o Google Sheets foi a escolha óbvia para gerir o inventário. É colaborativo, visual, toda a gente sabe usá-lo e é gratuito. A minha equipa do armazém conseguia atualizar as localizações do stock. Eu conseguia escrever fórmulas. Toda a gente conseguia ver tudo em tempo real.
O problema é que o Google Sheets nunca foi concebido para ser uma base de dados. E quando se está a processar milhares de SKUs de liquidação da Amazon em várias contas de marketplace, começa a falhar de formas difíceis de prever:
- Limites de taxa da API — a API do Google Sheets tem uma quota de cerca de 60 pedidos por minuto por utilizador. Quando se tem 5 scripts automatizados a aceder à mesma folha, esgota-se isso num instante
- Pesadelos de concorrência — dois scripts a atualizar a mesma linha ao mesmo tempo. Um colaborador a editar enquanto um script escreve. Recálculos de fórmulas durante importações de dados. A corrupção de dados era um risco constante
- Sem integridade referencial — uma "venda" era uma linha num separador. O "artigo" a que se referia era uma linha noutro separador. A ligação? Uma cadeia de texto (o código LPN). Um erro de escrita no LPN? Venda órfã. Nenhuma chave estrangeira para salvar o dia
- Degradação do desempenho — com cerca de 8000 linhas com fórmulas, a folha demorava mais de 15 segundos a carregar. Os meus colaboradores estavam à espera. As operações estavam a abrandar
- Sem capacidade de consulta — "Mostra-me todos os artigos do camião A2Z33838 que ainda estão em stock e que ainda não foram anunciados no eBay" exige uma fórmula QUERY de 200 caracteres que rebenta se alguém inserir uma coluna
Tinha de migrar. Mas não podia parar o negócio para o fazer.
A Restrição Que Mudou Tudo
Isto não foi a migração de um projeto paralelo de fim de semana. Era um negócio em produção com restrições reais:
- 5 colaboradores que dependiam do Google Sheets todos os dias — operários de armazém a digitalizar artigos, administrativos a processar encomendas, logística a coordenar envios
- Encomendas a entrar diariamente através do Wallapop (o nosso canal principal), cada uma precisando de ser processada em poucas horas
- Dados financeiros que tinham de estar certos ao cêntimo — registamos os custos por camião da Amazon (códigos A2Z) para efeitos fiscais e de relatórios de rentabilidade
- Sem um segundo engenheiro — eu era o único programador E geria as operações do negócio em simultâneo
Não podia simplesmente carregar num interruptor. Precisava de uma estratégia de migração que me permitisse avançar de forma incremental, validar em cada passo e reverter se algo corresse mal.
O Pipeline de 5 Fases
Concebi a migração como um pipeline de 5 fases, em que cada fase podia ser executada de forma independente em modo dry-run ou execute:
Phase 1: EXTRACT & ANALYZE
└─ Pull all data from 3 Google Sheets tabs → CSVs
└─ Generate analysis report (row counts, data types, anomalies)
Phase 2: VALIDATE
└─ Parse every row against the target PostgreSQL schema
└─ Type conversions (dates, decimals, booleans, enums)
└─ Flag invalid rows with specific error reasons
└─ Generate validation report
Phase 3: MIGRATE
└─ Load validated data into PostgreSQL
└─ Respect foreign key ordering (items → listings → sales)
└─ Dry-run mode: simulate without writing
└─ Execute mode: actual INSERT with transaction safety
Phase 4: SYNC OPERATIONAL DATA
└─ Pull "Prep de Pedidos" (order preparation) tab
└─ Match warehouse status to database records
└─ Update pick/pack states
Phase 5: CONTINUOUS SYNC
└─ Daily: Sheet → DB delta sync
└─ Handles new rows, updates, and edge cases
└─ Runs alongside the database as source of truth transitions
As Partes Difíceis de Que Ninguém Fala
1. A Qualidade dos Dados Nunca é a Que Pensamos
O Google Sheets é "flexível" — o que significa que os teus dados são uma confusão. Eis o que encontrei quando tentei processar mais de 8000 linhas de inventário:
- Preços guardados como texto com símbolos de euro:
"12,50 €","12.50","12,5"e""(vazio) — todos a significar coisas diferentes - Datas em 4 formatos diferentes, consoante quem as introduziu e a localização configurada no navegador
- Códigos LPN (o nosso identificador principal) com capitalização inconsistente, espaços no final e duplicados ocasionais
- Campos de estado que tinham evoluído de forma orgânica:
"vendido","VENDIDO","Vendido (devuelto)","vendido?"
Passei mais tempo a escrever código de limpeza e normalização de dados do que na lógica de migração propriamente dita. Cada caso-limite que corrigia revelava mais dois.
2. Reconciliação Financeira por Camião
Compramos liquidações da Amazon ao camião. Cada camião tem um código (A2Z33838, A2Z34102, etc.) e um custo. Para efeitos fiscais, preciso de saber: para cada camião, quanto pagámos, quanto vendemos, o que ainda está em stock e qual é a margem?
No Google Sheets, isto era um separador com fórmulas SUMIFS. No PostgreSQL, precisei de reconstruir isto como uma vista (v_resumen_a2z) e validar que os números coincidiam ao cêntimo.
Truckload | Sheet Total | DB Total | Diff | Status
A2Z33838 | €14,230.50 | €14,230.50| €0.00 | ✓ MATCH
A2Z34102 | €8,445.00 | €8,445.00 | €0.00 | ✓ MATCH
A2Z34567 | €11,200.75 | €11,198.25| €2.50 | ✗ INVESTIGATE
| | | | → 1 sale missing
| | | | → LPN XU-20240312
Gerei centenas destes relatórios de reconciliação. Cada discrepância tinha de ser investigada. A maioria era causada por problemas de timing (uma venda registada na folha mas ainda não extraída da API do Wallapop) ou por colaboradores meus a fazer correções manuais diretamente na folha.
3. O Período de Operação em Paralelo
Durante cerca de 3 semanas, mantive os dois sistemas a funcionar em simultâneo. O Google Sheets continuou a ser a fonte de verdade para os meus colaboradores, enquanto a base de dados era preenchida e validada em segundo plano.
Todas as manhãs:
- Executar o script de sincronização para puxar as alterações do Sheets para o PostgreSQL
- Verificar o relatório de reconciliação à procura de discrepâncias
- Investigar e corrigir quaisquer problemas (normalmente 2 a 5 por dia)
- Testar os dashboards do Retool com dados reais
- Processar as encomendas do dia (as operações reais do negócio)
Todas as noites:
- Rever o que os colaboradores alteraram no Sheets ao longo do dia
- Executar a sincronização novamente
- Verificar que os totais financeiros continuavam a coincidir
Isto era exaustivo. Estava essencialmente a gerir dois sistemas, a fazer o meu trabalho como operador do negócio E a depurar casos-limite da migração. Mas era a única forma de garantir confiança antes do cutover.
4. Os Scripts de Correção
Nenhuma migração é limpa. Escrevi mais de 15 scripts de correção especializados para problemas descobertos durante ou depois do período de operação em paralelo:
fix_listing_prices_from_sheet.py— alguns preços de anúncios na BD vinham da API do Wallapop (que mostra o preço atual), mas a folha tinha o preço de anúncio original. Estes precisavam de reconciliaçãofix_cash_transaction_duplicates.py— transações em numerário que tinham sido introduzidas duas vezes (uma pelo script de sincronização, outra pela interface do Retool depois do cutover)fix_incidents_create_returned_sales.py— devoluções que existiam como "incidentes" no Sheets mas precisavam dos registos de "venda" correspondentes na base de dadosborrar_ventas_no_cuadran_sheet.py— vendas que existiam na BD mas não tinham correspondência na folha (dados fantasma de erros iniciais na extração da API)fix_zero_price_sales.py— vendas importadas com preço de €0,00 devido a um erro no extrator que não capturava o campo do preço para certos estados de encomenda
Cada script de correção seguia o mesmo padrão: diagnosticar → dry-run → rever → executar → verificar. Nada de correções às cegas.
5. Explicar à Equipa
A parte mais difícil não foi técnica. Foi explicar a 4 colaboradores não técnicos porque é que estávamos a mudar a forma como trabalhavam.
"A folha de cálculo funciona bem" é uma coisa perfeitamente razoável para um operário de armazém dizer. E, do ponto de vista deles, funcionava mesmo bem. Eles não viam os limites de taxa da API, os riscos de corrupção de dados ou os scripts a rebentar às 3 da manhã por causa de um recálculo de fórmula.
Tive de:
- Construir interfaces no Retool que fossem pelo menos tão fáceis como a folha de cálculo (caso contrário, a adoção iria falhar)
- Formar cada pessoa individualmente no novo fluxo de trabalho
- Manter a folha disponível como cópia de segurança apenas de leitura durante as primeiras 2 semanas após o cutover
- Ser recetivo ao feedback do tipo "isto não funciona como antes" (a maior parte do qual era válido)
O Cutover
Depois de 3 semanas de operação em paralelo, mais de 200 relatórios de reconciliação e 15 scripts de correção, documentei o plano de cutover:
CUTOVER CHECKLIST
─────────────────
[✓] All truckloads reconciled to €0.00 difference
[✓] All active listings matched between Sheet and DB
[✓] All pending orders present in both systems
[✓] Retool modules tested: orders, items, sales, invoicing
[✓] Employees trained on new workflow
[✓] Sheet set to read-only with banner: "USE RETOOL"
[✓] Automated extractors pointed to DB (not Sheet)
[✓] Backup snapshot of all table counts taken
[✓] Rollback procedure documented (Sheet restore)
O cutover em si foi anticlimático — que é exatamente o que se quer. Mudei os extratores para escreverem diretamente no PostgreSQL, defini o Google Sheets como apenas de leitura e disse à minha equipa: "a partir de hoje, usem o Retool para tudo".
O Que Construí (A Stack Técnica)
A migração não foram apenas scripts. Exigiu construir toda a infraestrutura de destino:
- 16 ficheiros DDL modulares a definir o esquema do PostgreSQL (tabelas, vistas, vistas materializadas, funções, triggers)
- Mais de 35 ficheiros de migração incremental para a evolução do esquema após o cutover
- Mais de 25 modelos de dados SQLModel em Python para o backend em FastAPI
- Vistas materializadas como
mv_portalhero_feedpara a geração de feeds de marketplace - Vistas como
v_resumen_a2zpara relatórios financeiros por camião - Funções de trigger para o registo automático de vendas e atualizações de inventário
Resultados
6 meses após a migração:
- Tempos de consulta: de mais de 15 segundos (carregamento da folha) para menos de 50 ms (consulta no PostgreSQL)
- Concorrência: 5 scripts automatizados + 5 colaboradores + dashboards do Retool, todos a aceder à BD em simultâneo com zero conflitos
- Integridade dos dados: chaves estrangeiras, restrições e triggers detetam erros que teriam corrompido silenciosamente a folha
- Escala operacional: expandimos de apenas Wallapop para Wallapop + eBay + PortalHero — algo que teria sido impossível com o Sheets como espinha dorsal
- Rigor financeiro: a reconciliação por camião, que antes me demorava 2 horas com SUMIFS, corre agora em 3 segundos como uma vista materializada
O Que Faria de Forma Diferente
Se voltasse a fazer isto:
- Começar logo com a base de dados desde o primeiro dia. O imposto do "migramos mais tarde" é real e caro. Mesmo uma configuração simples de PostgreSQL + um frontend de ERP personalizado no Retool desde o início ter-me-ia poupado centenas de horas
- Automatizar as verificações de reconciliação em vez de as executar manualmente todas as manhãs. Um pipeline ao estilo de CI que me enviasse um e-mail apenas quando fossem encontradas discrepâncias
- Construir a interface do Retool primeiro e só depois migrar. Construí-as em paralelo, o que significou que os colaboradores viram interfaces meio-acabadas durante o período de transição. Se a interface estivesse polida antes do cutover, a adoção teria sido mais suave
A Conclusão
Migrar um sistema em produção não é um problema técnico. É um problema operacional que exige soluções técnicas. O código é talvez 30% do trabalho. Os outros 70% são:
- Compreender todos os casos-limite dos teus dados existentes
- Construir confiança através da operação em paralelo e da reconciliação
- Gerir o lado humano — formação, comunicação, feedback
- Ter a disciplina de não cortar caminho quando estás exausto e a contagem de discrepâncias está finalmente a zero
Fiz tudo isto como engenheiro a solo a gerir um negócio com 5 colaboradores. Sem uma equipa de DBAs. Sem QA dedicado. Apenas engenharia sistemática e muito café.
O pipeline de migração é open-source: github.com/AspiranteD/sheets-to-postgres-migration