Migrações¶
O tempest-db-js tem um sistema de migrações inspirado no Alembic (SQLAlchemy), e
explicitamente diferente da "costura de SQL" de outras ferramentas: tudo flui
por uma Schema IR + operações tipadas, e o SQL só nasce no renderer do dialeto.
Você nunca escreve nem versiona um .sql solto.
Importe de tempest-db-js/migrations:
import {
reflectSchema, diffSchema, generateMigration,
MigrationRunner, type Migration,
} from "tempest-db-js/migrations";
Estado
Tudo nesta página funciona e é testado contra SQLite real (node:sqlite):
reflect, diff, render, codegen, grafo DAG, runner, CLI, drift
(checkDrift/introspectSqlite) e batch-mode do SQLite pra mudanças de
coluna. PostgreSQL (introspecção, enum nomeado, pool) existe estruturalmente,
mas ainda não é exercitado no CI — veja o Roadmap.
1. Do modelo ao IR¶
reflectSchema lê suas classes e produz a IR — a descrição canônica,
independente de dialeto, do schema:
const target = reflectSchema([User, Post]);
// { tables: { users: { columns: {...}, primaryKey: ["id"] }, posts: {...} } }
2. Diff → operações tipadas¶
diffSchema(atual, alvo) compara dois IR e emite operações — nunca SQL:
import { emptySchema } from "tempest-db-js/migrations";
const ops = diffSchema(emptySchema(), target);
// [ { kind: "create_table", table: {...} }, { kind: "create_table", ... } ]
Cada operação tem um inverso conhecido (invert), o que dá down() automático.
3. Autogenerate → arquivo de migração¶
generateMigration transforma as operações num arquivo TS editável, com up()
e um down() invertido:
const src = generateMigration({
revision: "a1b2c3",
downRevision: [],
label: "create users",
operations: ops,
});
// string TS: export const up/down, operações embutidas como dados
4. Aplicar / reverter¶
MigrationRunner renderiza as operações pro dialeto e executa via driver,
rastreando revisões aplicadas na tabela tempest_db_js_migrations:
import { NodeSqliteDriver } from "tempest-db-js";
const driver = NodeSqliteDriver.open("app.db");
const runner = new MigrationRunner(driver, "sqlite");
runner.upgrade(migrations, new Date().toISOString()); // aplica pendentes (ordem do DAG)
runner.downgrade(migrations, 1); // reverte a última
Migração escrita à mão usa a fachada Op:
const migration: Migration = {
revision: "m1",
downRevision: [],
up: (op) => op.createTable(reflectTable(User)),
down: (op) => op.dropTable(reflectTable(User)),
};
5. Grafo de revisões (DAG)¶
downRevision é uma lista de pais — o histórico é um DAG, não uma corrente.
Suporta branches paralelas e merge. topoOrder ordena pra aplicar (pais antes de
filhos, determinístico); heads mostra as pontas:
import { topoOrder, heads } from "tempest-db-js/migrations";
topoOrder(migrations); // ordem de aplicação
heads(migrations); // revisões sem filhos (avisa se > 1)
6. Mudanças de coluna no SQLite (batch-mode)¶
O SQLite não faz ALTER COLUMN. O tempest-db-js resolve com table-rebuild (igual ao
batch mode do Alembic): a operação recreate_table cria uma tabela nova com o
schema-alvo, copia as colunas comuns, e troca os nomes — preservando os dados.
No PostgreSQL a mesma operação vira ALTER/ADD/DROP por coluna.
7. Drift: o banco diverge dos modelos?¶
introspectSqlite lê o schema vivo do banco; checkDrift compara com os modelos e
devolve uma lista de divergências (vazia = sem drift). A comparação é no nível de
afinidade do SQLite, então varchar vs TEXT não é falso-positivo:
import { checkDrift } from "tempest-db-js/migrations";
const issues = checkDrift(driver, [User, Post]);
if (issues.length > 0) {
console.error("schema drift:", issues); // ótimo como gate de CI
}
8. CLI (programática)¶
runMigrationCli(argv, config) despacha comandos estilo Alembic e devolve linhas +
exit code (testável; um bin fino só liga em process.argv/process.exit). É
async, e aceita driver sync (SQLite) ou async (PostgreSQL, MySQL):
import { runMigrationCli } from "tempest-db-js/migrations";
const config = { driver, dialect: "sqlite" as const, migrations, models: [User, Post] };
await runMigrationCli(["upgrade"], config); // aplica pendentes
await runMigrationCli(["upgrade", "--sql"], config); // imprime SQL (offline)
await runMigrationCli(["downgrade", "1"], config); // reverte
await runMigrationCli(["current"], config); // revisões aplicadas
await runMigrationCli(["history"], config); // DAG
await runMigrationCli(["heads"], config); // pontas
await runMigrationCli(["check"], config); // drift + diff (gate de CI)
await runMigrationCli(["revision", "-m", "x", "--autogenerate"], config);
replaySchema(migrations) reconstrói a IR "atual" sem banco — é o que o
--autogenerate compara com os modelos.
PostgreSQL pelo CLI¶
O mesmo config, com um driver async — nada mais muda:
// tempest-db.config.mjs
import { defineMigrationConfig } from "tempest-db-js/migrations";
import { createEngine } from "tempest-db-js";
const engine = createEngine("postgresql://app@localhost/app");
export default defineMigrationConfig({
driver: engine.driver, // (1)!
dialect: "postgresql",
migrations,
models: [User, Post],
});
- Qualquer objeto que satisfaça
AsyncDriverserve.
Sync e async pelo mesmo caminho
O CLI adapta o driver com toAsyncDriver e roda tudo no
AsyncMigrationRunner. Um driver sync e um async têm a mesma forma — a
diferença só aparece no valor de retorno, e await normaliza os dois. Por
isso não existe flag async no config para você errar.
check por dialeto
checkDriftAsync roteia: SQLite via introspectSqliteAsync,
PostgreSQL via information_schema, e MySQL devolve uma mensagem
explícita de "não implementado" — a introspecção MySQL ainda não existe.
9. Backup e restore¶
O passo que todo runbook pede antes de rodar migração em produção:
tempest-db backup out/app-2026-09-05.dump --url "$DATABASE_URL"
tempest-db restore out/app-2026-09-05.dump --url "$DATABASE_URL"
Sem --url, usa DATABASE_URL. Também dá para chamar programaticamente
(backupDatabase(url, file) / restoreDatabase(url, file)).
| Banco | Backup | Restore |
|---|---|---|
PostgreSQL (.dump) |
pg_dump --format=custom |
pg_restore |
PostgreSQL (.sql) |
pg_dump plano |
psql --file |
| SQLite | VACUUM INTO |
cópia do arquivo |
A extensão escolhe o formato, então os dois comandos não podem discordar sobre o arquivo.
No SQLite, copiar o arquivo não é backup
Com WAL ligado, o .db sozinho não é o banco inteiro — parte dos dados está no
-wal. VACUUM INTO produz um arquivo consistente mesmo com outra conexão
escrevendo, e é por isso que ele é usado aqui em vez de cp.
A senha vai por PGPASSWORD, nunca em argv
Qualquer processo da máquina lê a linha de comando alheia. A senha da URL é passada por variável de ambiente ao processo filho.
Sufixo de driver é removido
postgresql+asyncpg://… é URL de serviço Python; o pg_dump não conhece esse
esquema. O sufixo sai antes de montar o comando.
Ferramenta ausente no PATH vira erro nomeado (BackupToolMissing), não stack trace de
spawn. backup/restore são despachados antes do carregamento do config: um banco
que ainda não migra é exatamente o que alguém precisa dumpar.
Recap¶
reflectSchema(models)→ IR;diffSchema(atual, alvo)→ operações tipadas.generateMigration(...)→ arquivo TS editável comup()/down()invertido.MigrationRunner.upgrade/downgradeaplica/reverte de verdade, com version table; o CLI usa oAsyncMigrationRunnere roda nos 3 bancos.- Grafo DAG (
topoOrder/heads) suporta branch/merge. - SQL só no renderer do dialeto — nunca um
.sqlsolto.