O SQLite Session no Node.js permite registrar alterações feitas em tabelas e empacotá-las como um changeset ou patchset binário. Esse arquivo pode ser aplicado depois a outro banco SQLite com o mesmo schema e estado inicial compatível. O recurso é útil para sincronização local, exportação incremental, replicação controlada, colaboração offline e auditoria técnica de mudanças.
O módulo nativo node:sqlite expõe database.createSession(), session.changeset(), session.patchset() e database.applyChangeset(). A API é síncrona e trabalha sobre uma conexão DatabaseSync, então deve ser usada em fluxos curtos, workers ou processos onde o bloqueio da thread principal foi avaliado.
Neste guia, você aprenderá a capturar mudanças, diferenciar changeset de patchset, aplicar alterações com resolução de conflitos, limitar tabelas, validar schemas, combinar o recurso com transações e evitar perda de dados.
O que é uma Session?
Uma Session observa alterações realizadas por uma conexão SQLite em uma ou mais tabelas. Ela não cria uma cópia completa do banco. Em vez disso, registra chaves primárias e valores necessários para representar operações de INSERT, UPDATE e DELETE.
Depois, a aplicação solicita um blob binário:
- changeset: inclui valores antigos suficientes para detectar mais conflitos;
- patchset: é menor, mas guarda menos valores anteriores e possui detecção de conflito mais limitada.
O banco de destino precisa possuir tabelas compatíveis, mesmas colunas e definição de chave primária coerente.
Pré-requisitos
O recurso depende de uma versão do Node.js com suporte a node:sqlite e Session. Verifique a versão usada em produção e mantenha testes específicos. Um exemplo de abertura:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('data/app.db', {
enableForeignKeyConstraints: true,
defensive: true,
timeout: 5_000
});O modo defensivo reduz recursos SQL que poderiam corromper deliberadamente o arquivo. O timeout permite aguardar brevemente por locks antes de falhar.
Para fundamentos do módulo, consulte SQLite no Node.js.
Schema com chave primária
A extensão Session exige uma chave primária declarada. Evite campos de chave com NULL:
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
updated_at TEXT NOT NULL
) STRICT;
`);Tabelas virtuais não são capturadas pela extensão. Triggers também merecem testes, porque aplicar um changeset pode disparar efeitos adicionais dependendo do schema do destino.
Criando uma Session
const session = db.createSession({
table: 'notes',
db: 'main'
});Quando table é omitida, alterações de todas as tabelas elegíveis são observadas. Em aplicações reais, prefira limitar por tabela para reduzir tamanho, superfície de conflito e risco de sincronizar dados que não deveriam sair do dispositivo.
Capturando alterações
const insert = db.prepare(`
INSERT INTO notes (id, title, body, updated_at)
VALUES (?, ?, ?, ?)
`);
insert.run(
crypto.randomUUID(),
'Planejamento',
'Conteúdo inicial',
new Date().toISOString()
);
const changeset = session.changeset();
console.log(changeset.byteLength);O resultado é um Uint8Array. Ele pode ser salvo em arquivo, enviado por API, armazenado em uma fila ou anexado a um registro de sincronização.
Changeset não é resetado
Solicitar session.changeset() não zera a Session. Chamadas posteriores incluem todas as mudanças observadas desde a criação. Para criar lotes independentes, feche a Session e abra outra depois de persistir o primeiro lote.
const changeset = session.changeset();
session.close();
await saveChangeset(changeset);
const nextSession = db.createSession({ table: 'notes' });Mudanças são consolidadas
Se uma linha for atualizada várias vezes durante a mesma Session, o changeset representa o estado inicial e o estado final relevante. Uma inserção seguida de exclusão pode não produzir mudança alguma. Esse comportamento reduz ruído, mas significa que a Session não é um log de eventos detalhado.
Para registrar cada evento de negócio, use um padrão como Outbox Pattern no Node.js ou Event Sourcing, em vez de tratar o changeset como trilha de auditoria.
Gerando patchset
const patchset = session.patchset();Patchsets ocupam menos espaço porque omitem valores antigos de campos não chave em DELETE e UPDATE. Em troca, não conseguem detectar todos os casos em que o destino mudou desde a base original.
Use changeset quando conflito e consistência importam mais que tamanho. Use patchset somente quando o modelo de sincronização garante que o destino não terá alterações concorrentes relevantes.
Aplicando em outro banco
import { DatabaseSync } from 'node:sqlite';
const target = new DatabaseSync('data/replica.db', {
enableForeignKeyConstraints: true,
defensive: true
});
const applied = target.applyChangeset(changeset);
console.log({ applied });Por padrão, um conflito aborta a aplicação. Isso é mais seguro que ignorar silenciosamente inconsistências.
Tipos de conflito
Conflitos comuns incluem:
- INSERT com chave primária já existente;
- UPDATE ou DELETE cuja linha não existe;
- valores atuais diferentes dos valores antigos esperados;
- violação de FOREIGN KEY;
- violação de UNIQUE, CHECK ou NOT NULL.
Tratando conflitos
import { constants } from 'node:sqlite';
const result = target.applyChangeset(changeset, {
onConflict(conflictType) {
if (conflictType === constants.SQLITE_CHANGESET_NOTFOUND) {
return constants.SQLITE_CHANGESET_OMIT;
}
return constants.SQLITE_CHANGESET_ABORT;
}
});As decisões possíveis incluem:
SQLITE_CHANGESET_ABORT: cancela e reverte;SQLITE_CHANGESET_OMIT: ignora a alteração conflitante;SQLITE_CHANGESET_REPLACE: substitui em conflitos permitidos.
Não use REPLACE como regra genérica. Ele pode sobrescrever mudanças válidas feitas no destino.
Filtro por tabela
target.applyChangeset(changeset, {
filter(tableName) {
return tableName === 'notes';
},
onConflict() {
return constants.SQLITE_CHANGESET_ABORT;
}
});O filtro cria uma allowlist de tabelas. Valide também o tipo de changeset, origem, versão do schema e tamanho antes de aplicar.
Transações e atomicidade
applyChangeset() reverte quando o handler aborta. Ainda assim, operações externas como upload, chamada HTTP ou publicação em fila não fazem parte da transação SQLite.
Se a sincronização também precisa gerar eventos, grave tudo no banco primeiro e use um relay posterior. A combinação com Outbox evita afirmar que uma alteração foi publicada quando a transação local falhou.
Versionamento de schema
Um changeset só é seguro quando origem e destino possuem schemas compatíveis. Inclua um envelope:
const envelope = {
schemaVersion: 7,
sourceDevice: deviceId,
createdAt: new Date().toISOString(),
changeset: Buffer.from(changeset).toString('base64')
};No destino:
- valide assinatura e origem;
- confirme schemaVersion;
- verifique tamanho máximo;
- decodifique para Uint8Array;
- aplique dentro do fluxo controlado;
- registre o resultado.
Idempotência
Reaplicar o mesmo changeset pode gerar conflitos ou duplicação. Atribua um ID único ao lote e registre a aplicação:
CREATE TABLE applied_changesets (
id TEXT PRIMARY KEY,
applied_at TEXT NOT NULL,
source TEXT NOT NULL
) STRICT;Antes de aplicar, confirme que o ID ainda não existe. Para princípios gerais, consulte Idempotência em APIs Node.js.
Assinatura e integridade
Changesets são dados binários e não devem ser aceitos de origem desconhecida. Assine o envelope com HMAC ou assinatura assimétrica e valide antes de abrir a transação.
const signature = createHmac('sha256', key)
.update(changeset)
.digest('base64url');Veja Assinaturas HMAC no Node.js.
Limites de tamanho
Defina limite para evitar uso excessivo de memória:
const MAX_CHANGESET_BYTES = 10 * 1024 * 1024;
if (changeset.byteLength > MAX_CHANGESET_BYTES) {
throw new Error('Changeset muito grande');
}Divida sincronizações grandes por intervalo, tabela ou checkpoint. Não mantenha uma Session aberta por semanas acumulando alterações sem controle.
Fila de sincronização
Uma tabela local pode armazenar lotes pendentes:
CREATE TABLE sync_queue (
id TEXT PRIMARY KEY,
payload BLOB NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
sent_at TEXT
) STRICT;O worker envia em ordem, usa retry com backoff e marca somente após confirmação remota. Consulte Retry com Backoff no Node.js.
Concorrência
DatabaseSync bloqueia a thread durante operações. Para aplicações HTTP, mova sincronização pesada para Worker Thread ou processo separado. Defina timeout de lock e não execute changesets grandes no caminho crítico da requisição.
Backup antes de aplicar
Em alterações críticas, use a API de backup:
import { backup } from 'node:sqlite';
await backup(target, 'backup/pre-sync.db');Backup não substitui validação, mas facilita recuperação operacional.
Testes
Crie bancos em memória com o mesmo schema:
const source = new DatabaseSync(':memory:');
const target = new DatabaseSync(':memory:');
source.exec(schemaSql);
target.exec(schemaSql);
const session = source.createSession({ table: 'notes' });
source.prepare('INSERT INTO notes VALUES (?, ?, ?, ?)')
.run('1', 'Teste', 'Corpo', '2026-01-01T00:00:00Z');
const changeset = session.changeset();
target.applyChangeset(changeset);
const note = target.prepare('SELECT * FROM notes WHERE id = ?').get('1');
assert.equal(note.title, 'Teste');Cenários de conflito
Teste explicitamente:
- INSERT duplicado;
- DELETE de linha ausente;
- UPDATE com valor alterado no destino;
- violação de chave estrangeira;
- schema incompatível;
- changeset truncado;
- reaplicação do mesmo lote;
- handler que lança exceção.
Observabilidade
Registre sem expor conteúdo sensível:
- ID do lote;
- origem;
- tamanho em bytes;
- tempo de aplicação;
- resultado;
- tipo e quantidade de conflitos;
- versão do schema.
Não grave o blob inteiro em logs.
Erros comuns
- usar tabelas sem PRIMARY KEY;
- tratar changeset como event log completo;
- usar patchset onde conflito importa;
- aplicar dados não autenticados;
- ignorar versionamento de schema;
- usar REPLACE para todo conflito;
- reaplicar lote sem idempotência;
- manter Session aberta indefinidamente;
- executar sincronização grande na thread HTTP.
Fluxo recomendado
- crie Session por tabela ou lote;
- execute alterações locais;
- gere changeset;
- feche a Session;
- envolva com ID, schema e assinatura;
- persista na fila local;
- envie com retry;
- valide no destino;
- aplique com política conservadora;
- registre idempotência e conflitos.
Conclusão
O SQLite Session no Node.js oferece sincronização incremental sem copiar o banco inteiro. Changesets preservam mais contexto para detectar conflitos, enquanto patchsets economizam espaço quando a consistência concorrente é simples.
Use chaves primárias, envelope assinado, schema versionado, idempotência e política de conflito explícita. Com filas, backups e testes adversariais, a Session Extension se torna uma base útil para aplicações offline e replicação controlada.
Consulte a documentação oficial de node:sqlite e a introdução oficial à SQLite Session Extension.



