Imagine uma lixeira de reciclagem onde você claramente colocou uma etiqueta dizendo "Apenas Garrafas Plásticas", mas quando alguém joga um pedaço de papel lá dentro, ela o aceita silenciosamente sem uma única palavra de protesto?
Esta é a experiência de arrepiar a espinha que os desenvolvedores vivem quando conhecem o sistema de tipos do SQLite pela primeira vez.
Se você está acostumado ao estilo rigoroso de agente de alfândega do PostgreSQL (onde os tipos incorretos são rejeitados diretamente), a informalidade do SQLite pode fazer você questionar sua vida.
Ainda mais assustador, quando você quer modificar a estrutura de uma tabela, ele lhe dirá:
"A tabela não pode ser modificada diretamente. Por favor, construa uma casa nova, mude os móveis e depois exploda a casa velha."
Sob o capô, o SQLite tem apenas 5 classes de armazenamento
Não importa quais nomes de tipo elegantes você declare em CREATE TABLE (VARCHAR(255), BIGINT, DECIMAL), o SQLite sob o capô reconhece apenas estas 5 classes de armazenamento:
| Classe de armazenamento | Descrição |
|---|---|
| NULL | Valor nulo |
| INTEGER | Inteiro (ocupa automaticamente de 1 a 8 bytes de acordo com a magnitude) |
| REAL | Número de ponto flutuante (fixo de 8 bytes) |
| TEXT | String de texto (codificação UTF-8 padrão) |
| BLOB | Objeto binário grande (armazenado exatamente como inserido) |
Os tipos que você define nas colunas são meramente "sugestões" para o SQLite, não "regras obrigatórias".
Isso se chama "Type Affinity" (Afinidade de tipos). O
SQLitetentará converter seus dados para o tipo sugerido, mas se não puder, simplesmente colocará os dados originais de qualquer maneira sem lançar nenhum erro.
Você pode declarar uma coluna age INTEGER no SQLite e depois inserir a string 'para sempre dezoito'; ele o aceitará com prazer.
Três armadilhas de tipos mais fáceis de cair
Armadilha 1: Sem Boolean nativo
O SQLite não tem tipo booleano. True e False só podem ser representados pelos inteiros 1 e 0.
Quando você recupera dados do SQLite usando Node.js, obterá o número 1 ou 0, não true ou false.
Se você realizar diretamente uma verificação como if (user.is_admin === true), isso nunca será verdadeiro.
Armadilha 2: Sem tipo Date/Time
O SQLite não tem tipo de data/hora. Você só pode armazenar o tempo como:
| Método de armazenamento | Exemplo | Prós e contras |
|---|---|---|
| TEXT (string ISO-8601) | '2026-05-19T18:00:00Z' |
Mais recomendado, alta legibilidade, conversão perfeita ao passar para PostgreSQL no futuro |
| INTEGER (Unix Timestamp) | 1747656000 |
Pegada pequena, mas não legível por humanos |
Nunca armazene datas em formatos personalizados arbitrários como 19/05/2026 18:00, caso contrário, a migração de dados no futuro será un desastre.
Armadilha 3: Inserir uma string em uma coluna Integer não gerará erro
No PostgreSQL, inserir uma string em uma coluna INTEGER gera um erro imediatamente. Mas o SQLite apenas tentará convertê-la silenciosamente e, se falhar, a aceitará tal como está.
Isso significa que dados sujos podem entrar silenciosamente no seu banco de dados até que um dia seu programa falhe devido ao recebimento de um tipo inesperado, só então você descobrirá o problema.
Programação defensiva: Tratar um banco de dados informal com uma atitude rigorosa
Diante da informalidade do SQLite, você deve construir mecanismos de defesa rígidos no desenvolvimento com Node.js:
| Nível de defesa | Ferramenta | Papel |
|---|---|---|
| Guarda em tempo de compilação | TypeScript | Capturar tipos incorretos no estágio de escrita do código |
| Validação de entrada de API | Zod | Validar os dados de entrada estritamente (garantir que age seja sempre um número) |
| Conversão implícita de tipos | Prisma / Drizzle ORM | Lidar automaticamente com as diferenças de tipos entre SQLite e PostgreSQL |
Mover o "guardião da validação de dados" da camada do banco de dados para a camada da aplicação
Node.jsé uma estratégia-chave para aproveitar a velocidade de desenvolvimento do SQLite e, ao mesmo tempo, garantir a escalabilidade futura.
Ao usar um ORM, desde que você declare type: 'boolean' no seu código, o ORM o converte automaticamente para 1/0 ao salvar no SQLite, e o converte de volta para true/false ao ler, ocultando perfeitamente as diferenças de tipos subjacentes.
ALTER TABLE é capenga: O que pode e o que não pode ser modificado
O suporte do SQLite para modificar estruturas de tabelas é muito limitado:
| Operação | Suportado |
|---|---|
Adicionar coluna (ADD COLUMN) |
Sim |
Renomear coluna (RENAME COLUMN) |
Sim |
Excluir coluna (DROP COLUMN) |
Sim (em versões mais novas) |
Renomear tabela (RENAME TO) |
Sim |
| Alterar tipo de coluna | Não |
Adicionar/remover restrições UNIQUE, NOT NULL |
Não |
| Modificar chave primária | Não |
| Modificar chave estrangeira | Não |
Uma vez que precise realizar qualquer uma das modificações "não suportadas", o
SQLiteexige que você execute a estratégia de "recriar e mover".
Os quatro passos para recriar e mover: O caminho de atualização de tabelas do SQLite
Como não pode ser modificado diretamente, a prática padrão recomendada pelos documentos oficiais é construir uma casa nova, mudar os móveis, explodir a casa velha e colocar a placa nova:
| Passo | Ação | Descrição |
|---|---|---|
| 1 | Criar tabela nova | CREATE TABLE users_new (...) usando a estrutura correta |
| 2 | Copiar dados | INSERT INTO users_new SELECT ... FROM users |
| 3 | Excluir tabela velha | DROP TABLE users |
| 4 | Renombrar tabela | ALTER TABLE users_new RENAME TO users |
Esses quatro passos devem ser executados de uma só vez; qualquer queda de energia ou crash da aplicação no meio do caminho resultará na perda de dados.
Garantir que as atualizações não percam dados: Duas linhas de defesa de segurança
Linha de defesa 1: Defesa física, copiar o arquivo diretamente
O SQLite é essencialmente apenas um arquivo. Antes de executar qualquer alteração de esquema, simplesmente faça uma cópia do arquivo .db como backup.
const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');
Se as coisas derem errado, substituir o arquivo restaura tudo em un instante. Esta é uma vantagem que outros bancos de dados grandes não podem oferecer.
Linha de defesa 2: Pacote de Transaction, a máquina do tempo do banco de dados
Envolva todos os passos de migração dentro de uma única Transaction; se algum passo falhar, todo o processo será revertido automaticamente (Rollback) como se nada tivesse acontecido.
const Database = require('better-sqlite3');
const db = new Database('my_project.db');
const migrateData = db.transaction(() => {
db.prepare(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL DEFAULT 18
)
`).run();
db.prepare(`
INSERT INTO users_new (id, name, age)
SELECT id, name, COALESCE(age, 18) FROM users
`).run();
db.prepare('DROP TABLE users').run();
db.prepare('ALTER TABLE users_new RENAME TO users').run();
});
try {
migrateData();
console.log('Tabela atualizada com sucesso');
} catch (error) {
console.error('A atualização falhou, dados restaurados com segurança:', error.message);
}
"Arquivo de backup + Vinculação de Transaction" é o airbag da migração do seu banco de dados.
Controle a informalidade do SQLite com uma atitude rigorosa
Domine a informalidade do
SQLitecom uma arquitetura rígida na camada de aplicação para desfrutar da sua velocidade de desenvolvimento ultrarrápida e evitar dívidas técnicas futuras.
O sistema de tipos do SQLite é muito informal, e o ALTER TABLE tem muitas limitações.
No entanto, desde que você realize verificação de tipos com TypeScript + validação com Zod + abstração com ORM no lado do Node.js, combinado com a estratégia de segurança de backup físico + Transaction, você dapat desfrutar com segurança da eficiência de desenvolvimento oferecida pelo SQLite enquanto pavimenta o caminho para migrar para o PostgreSQL no futuro.