Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

SQLite Session no Node.js: sincronize mudanças

Atualizado em: 9 de outubro de 2026

Terminal Linux usado em desenvolvimento e administração de aplicações Node.js

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:

  1. valide assinatura e origem;
  2. confirme schemaVersion;
  3. verifique tamanho máximo;
  4. decodifique para Uint8Array;
  5. aplique dentro do fluxo controlado;
  6. 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

  1. crie Session por tabela ou lote;
  2. execute alterações locais;
  3. gere changeset;
  4. feche a Session;
  5. envolva com ID, schema e assinatura;
  6. persista na fila local;
  7. envie com retry;
  8. valide no destino;
  9. aplique com política conservadora;
  10. 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita