Migrando banco local SQLite para Cloudflare D1 sem ORM

Tinha um banco SQLite num servidor tradicional e precisei levar pro D1. Sem ORM no caminho, a migração é SQL puro via Wrangler — e tem pegadinha.

O problema

Imagine a seguinte situação: eu tinha um programa (um simulador) rodando com um banco de dados SQLite local dentro de um VPS tradicional (pense no VPS como um computador alugado lá nos Estados Unidos, na Virgínia). Ele funcionava, mas para os usuários do Brasil, a navegação ficava lenta por causa da latência — que é aquele “atraso” ou demorinha na resposta provocada pela distância física entre o servidor lá fora e o usuário aqui. Além disso, cuidar desse computador alugado dava um certo trabalho de manutenção.

Por isso, decidi mudar para o Cloudflare D1 — que é como se fosse o mesmo SQLite, só que distribuído no edge (ou seja, espalhado em vários servidores pelo mundo todo, pertinho do usuário final). De início, a migração parecia muito simples: “pô, é SQLite para SQLite, a linguagem de consulta (sintaxe) é a mesma, né?”.

Errado pela metade! O D1 é sim compatível com a linguagem do SQLite, mas ele funciona de um jeito diferente nos bastidores (o chamado runtime: em vez de rodar em um programa fixo e contínuo no servidor, ele roda no “edge”, em pedacinhos executados sob demanda chamados V8 Isolates). Além disso, o fluxo de mudança tem algumas pegadinhas que me custaram um bom tempo. Este artigo é exatamente tudo o que eu gostaria de ter sabido antes de começar!

A diferença que importa

No SQLite tradicional da sua máquina, o banco de dados roda em um processo persistente: é como uma ligação telefônica que fica aberta. Você se conecta, faz suas perguntas (queries), recebe as respostas e fecha.

Já no Cloudflare D1, a coisa roda em V8 Isolates no edge. Pense nesses Isolates como pequenas tarefas independentes executadas instantaneamente na nuvem: não existe uma “conexão fixa e aberta”. O acesso é feito através de uma ponte chamada binding (usando o comando env.DB.prepare(...)), e cada pergunta ao banco é totalmente independente da anterior.

A sintaxe das consultas em SQL continua sendo a mesma. O que muda de verdade é:

  • Sem a função ATTACH DATABASE: No SQLite local, a gente usava essa função para “juntar” dois bancos de dados na mão e copiar informações de um para o outro. No D1, isso não existe.
  • Sem acesso direto aos arquivos: Você não pode simplesmente mandar o comando sqlite3 .dump > arquivo no seu terminal e importar o arquivo gerado direto na nuvem. O D1 só aceita receber essas mudanças (as chamadas migrations) através de uma ferramenta própria chamada Wrangler.
  • Tipos e funções de dados: A grande maioria funciona normal, mas algumas funções bem específicas de versões (builds) do SQLite local podem não estar disponíveis no D1.

Ou seja: migrar não é simplesmente “copiar e colar o arquivo .db”. Trata-se de exportar a estrutura do banco (o schema, que é o molde das tabelas) e os dados (as informações em si) em um formato que o Wrangler consiga entender e aplicar na nuvem.

O procedimento que funcionou

1. Exportar o schema

No SQLite da sua máquina local, rode no terminal:

sqlite3 meu_banco.db .schema > schema.sql

Esse comando vai gerar um arquivo schema.sql com todos os comandos CREATE TABLE (as instruções para criar cada tabela do zero). Dê uma revisada nesse arquivo antes de levar para a nuvem — às vezes existem configurações como WITHOUT ROWID ou tipos que o D1 trata de forma diferente. No meu caso, precisei ajustar um comando AUTOINCREMENT que o D1 recomenda não usar (ele já garante IDs numéricos crescentes automaticamente por padrão).

2. Exportar os dados

sqlite3 meu_banco.db .dump > dump_completo.sql

O .dump faz uma foto completa: ele traz tanto a estrutura (schema) quanto as informações reais escritas em forma de comandos INSERT. Para fazer a migração sem dor de cabeça, eu prefiro separar: o schema em um arquivo e os dados em outro. Assim, primeiro nós preparamos a estrutura (criamos as tabelas) e depois inserimos os dados para popular o banco.

Para extrair apenas as linhas de inserção de dados (sem os comandos CREATE de tabela), use:

# Só inserts (sem CREATE)
sqlite3 meu_banco.db .dump | grep '^INSERT' > dados.sql

3. Aplicar no D1 via Wrangler

Agora usamos o Wrangler, a ferramenta da Cloudflare para gerenciar projetos pelo terminal.

Primeiro, criamos a estrutura das tabelas na nuvem:

npx wrangler d1 execute meu-banco --remote --file=schema.sql

Depois, enviamos os dados:

npx wrangler d1 execute meu-banco --remote --file=dados.sql

O parâmetro --remote é crucial. Sem ele, o Wrangler vai aplicar essas mudanças em um banco de dados local de testes (um SQLite de desenvolvimento que ele guarda dentro da pasta .wrangler/state no seu computador). Você vai achar que migrou tudo, vai testar no aplicativo e ver tudo funcionando — sem perceber que na verdade está lendo os dados do teste local. Quando subir a aplicação para o ar (produção), os dados não estarão lá. Aconteceu exatamente isso comigo: perdi uma tarde inteira achando que era algum bug na minha consulta SQL.

4. Verificar contagem

Sempre confirme se a quantidade de dados bateu certinho:

npx wrangler d1 execute meu-banco --remote \
  --command="SELECT COUNT(*) FROM candidatos"

Se a contagem total for idêntica à do seu banco local original, a migração foi concluída com sucesso!

As pegadinhas que encontrei

Tabelas grandes em inserts únicos. O comando .dump gera uma linha de INSERT para cada registro do banco. Se você tiver 10 mil linhas, serão 10 mil comandos individuais. O Wrangler vai tentar aplicar um por um, e isso fica extremamente lento. Solução: agrupar esses inserts em blocos chamados transações, ou dividir o arquivo dados.sql em lotes menores. Para bancos pequenos (com apenas alguns milhares de linhas), isso não chega a ser um problema.

Tipos de data. No SQLite local, é muito comum salvar datas como texto (string ISO). Funciona no D1 também, mas é melhor padronizar para o formato INTEGER (usando Unix timestamp, que é uma contagem em segundos) se você quer fazer ordenações e comparações mais rápidas. Esse é um ajuste que deve ser feito no arquivo de schema antes de migrar, e não depois.

Chaves estrangeiras (Foreign Keys). O D1 suporta chaves estrangeiras (as regras de conexão entre tabelas), mas por padrão ele não fica validando essas chaves a cada INSERT (exatamente como faz o SQLite local). Se você quiser uma validação rigorosa dos dados, precisa rodar o comando PRAGMA foreign_keys = ON; em cada conexão — e, como no D1 cada consulta é independente, isso significa que você precisa habilitar a regra nas suas queries, e não de forma global.

O que aprendi

A grande lição que fica desse processo é: “mesma sintaxe” não significa “mesmo runtime”. O SQLite tradicional e o D1 usam a mesma linguagem de consulta SQL, mas funcionam em ambientes completamente diferentes. Assumir que migrar é apenas “copiar o arquivo” só porque o SQL é igual faz você cair em várias pegadinhas (ficar sem o ATTACH, perder o acesso direto aos arquivos e esquecer o parâmetro --remote obrigatório).

Por isso, a pergunta certa a se fazer antes de migrar não é “a sintaxe é compatível?”, mas sim “o ambiente de execução (runtime) é compatível?”.

E o segundo aprendizado, mais prático: sempre confirme a contagem de dados após a migração. Fazer uma migração sem verificar o resultado é pura fé. Rodar um SELECT COUNT(*) antes e depois para comparar leva 10 segundos e evita aquele tipo de erro chato que só aparece em produção, semanas depois, quando algum usuário reclama que “estão faltando dados”.

Leia também

Comentários

Carregando comentários…

Deixe um comentário