Ir para o conteúdo

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.

// numa migração:
up: (op) => op.recreateTable(reflectTable(UserOld), reflectTable(UserNew)),

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):

import { runMigrationCli } from "tempest-db-js/migrations";

const config = { driver, dialect: "sqlite" as const, migrations, models: [User, Post] };
runMigrationCli(["upgrade"], config);                       // aplica pendentes
runMigrationCli(["upgrade", "--sql"], config);              // imprime SQL (offline)
runMigrationCli(["downgrade", "1"], config);                // reverte
runMigrationCli(["current"], config);                       // revisões aplicadas
runMigrationCli(["history"], config);                       // DAG
runMigrationCli(["heads"], config);                         // pontas
runMigrationCli(["check"], config);                         // drift + diff (gate de CI)
runMigrationCli(["revision", "-m", "x", "--autogenerate"], config); // gera migração

replaySchema(migrations) reconstrói a IR "atual" sem banco — é o que o --autogenerate compara com os modelos.

PostgreSQL

introspectPostgres/checkDriftPostgres (via information_schema) e o enum nomeado (CREATE TYPE ... AS ENUM) existem mas não são exercitados no CI (sem Postgres no ambiente). PoolOptions repassa tuning ao postgres.js.

Recap

  • reflectSchema(models) → IR; diffSchema(atual, alvo) → operações tipadas.
  • generateMigration(...) → arquivo TS editável com up()/down() invertido.
  • MigrationRunner.upgrade/downgrade aplica/reverte de verdade, com version table.
  • Grafo DAG (topoOrder/heads) suporta branch/merge.
  • SQL só no renderer do dialeto — nunca um .sql solto.