Migrations¶
tempest-db-js has a migration system inspired by Alembic (SQLAlchemy), and
explicitly different from the "SQL-stitching" of other tools: everything flows
through a Schema IR + typed operations, and SQL is only born in the dialect
renderer. You never write nor version a loose .sql file.
Import from tempest-db-js/migrations:
import {
reflectSchema, diffSchema, generateMigration,
MigrationRunner, type Migration,
} from "tempest-db-js/migrations";
State
Everything on this page works and is tested against real SQLite (node:sqlite):
reflect, diff, render, codegen, DAG graph, runner, CLI, drift
(checkDrift/introspectSqlite), and SQLite's batch-mode for column changes.
PostgreSQL (introspection, named enum, pool) exists structurally, but is not yet
exercised in CI — see the Roadmap.
1. From the model to the IR¶
reflectSchema reads your classes and produces the IR — the canonical,
dialect-independent description of the schema:
const target = reflectSchema([User, Post]);
// { tables: { users: { columns: {...}, primaryKey: ["id"] }, posts: {...} } }
2. Diff → typed operations¶
diffSchema(current, target) compares two IRs and emits operations — never SQL:
import { emptySchema } from "tempest-db-js/migrations";
const ops = diffSchema(emptySchema(), target);
// [ { kind: "create_table", table: {...} }, { kind: "create_table", ... } ]
Each operation has a known inverse (invert), which gives you down() automatically.
3. Autogenerate → migration file¶
generateMigration turns the operations into an editable TS file, with up()
and an inverted down():
const src = generateMigration({
revision: "a1b2c3",
downRevision: [],
label: "create users",
operations: ops,
});
// TS string: export const up/down, operations embedded as data
4. Apply / revert¶
MigrationRunner renders the operations for the dialect and executes them via the
driver, tracking applied revisions in the tempest_db_js_migrations table:
import { NodeSqliteDriver } from "tempest-db-js";
const driver = NodeSqliteDriver.open("app.db");
const runner = new MigrationRunner(driver, "sqlite");
runner.upgrade(migrations, new Date().toISOString()); // apply pending (DAG order)
runner.downgrade(migrations, 1); // revert the last
A hand-written migration uses the Op facade:
const migration: Migration = {
revision: "m1",
downRevision: [],
up: (op) => op.createTable(reflectTable(User)),
down: (op) => op.dropTable(reflectTable(User)),
};
5. Revision graph (DAG)¶
downRevision is a list of parents — the history is a DAG, not a chain.
It supports parallel branches and merges. topoOrder orders them for applying
(parents before children, deterministic); heads shows the tips:
import { topoOrder, heads } from "tempest-db-js/migrations";
topoOrder(migrations); // application order
heads(migrations); // revisions with no children (warns if > 1)
6. Column changes on SQLite (batch-mode)¶
SQLite doesn't do ALTER COLUMN. tempest-db-js solves this with a table-rebuild (just
like Alembic's batch mode): the recreate_table operation creates a new table with
the target schema, copies the common columns, and swaps the names — preserving the
data. On PostgreSQL the same operation turns into per-column ALTER/ADD/DROP.
7. Drift: does the DB diverge from the models?¶
introspectSqlite reads the live schema from the database; checkDrift compares it
with the models and returns a list of divergences (empty = no drift). The comparison
is at the level of SQLite's affinity, so varchar vs TEXT is not a false
positive:
import { checkDrift } from "tempest-db-js/migrations";
const issues = checkDrift(driver, [User, Post]);
if (issues.length > 0) {
console.error("schema drift:", issues); // great as a CI gate
}
8. CLI (programmatic)¶
runMigrationCli(argv, config) dispatches Alembic-style commands and returns lines +
an exit code (testable; a thin bin just wires it to 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); // apply pending
runMigrationCli(["upgrade", "--sql"], config); // print SQL (offline)
runMigrationCli(["downgrade", "1"], config); // revert
runMigrationCli(["current"], config); // applied revisions
runMigrationCli(["history"], config); // DAG
runMigrationCli(["heads"], config); // tips
runMigrationCli(["check"], config); // drift + diff (CI gate)
runMigrationCli(["revision", "-m", "x", "--autogenerate"], config); // generate migration
replaySchema(migrations) reconstructs the "current" IR without a database — it's what
--autogenerate compares against the models.
PostgreSQL
introspectPostgres/checkDriftPostgres (via information_schema) and the named
enum (CREATE TYPE ... AS ENUM) exist but are not exercised in CI (no Postgres in
the environment). PoolOptions passes tuning through to postgres.js.
Recap¶
reflectSchema(models)→ IR;diffSchema(current, target)→ typed operations.generateMigration(...)→ editable TS file with invertedup()/down().MigrationRunner.upgrade/downgradereally applies/reverts, with a version table.- DAG graph (
topoOrder/heads) supports branch/merge. - SQL only in the dialect renderer — never a loose
.sqlfile.