@vibeorm/migrate
v5.0.2
Published
Migration engine for VibeORM v2 — IR differ, per-dialect DDL, migration runner, introspection
Maintainers
Readme
@vibeorm/migrate
Part of VibeORM — a type-safe TypeScript ORM for Bun and Node. Prisma-schema or TypeScript-DSL input, a canonical schema IR, a generated client, and a dialect-aware SQL layer over PostgreSQL, PGlite, SQLite and MySQL.
The migration engine: it diffs two schema IRs into dialect-neutral steps, renders those steps as per-dialect DDL, applies them with bookkeeping, and introspects a live database back into IR. It is its own package so that the CLI and tests can run schema changes without pulling in the query runtime, and so every SQL-text decision stays in one reviewable place.
bun add @vibeorm/migrateStable.
2.0.0, published under thelatestdist-tag. The v1 line stays installable asvibeorm@1.
Most users never install this directly — it ships as a dependency of vibeorm and of the generated client. Reach for it when you are writing an adapter or tooling against the runtime.
What it does
Three stages, each usable on its own.
Diff. diffSchemas({ from, to, dialect }) returns ordered MigrationStep values — createTable, addColumn, alterColumn, createIndex, addForeignKey, createJoinTable, dropView and the rest. Steps carry IR fragments, never SQL text, and are emitted in dependency-safe order. Both sides normalize before comparison, so diffSchemas({ from: introspect(push(X)), to: X }) is empty: pushing twice is a no-op. There is no rename detection — a renamed table diffs as drop plus create.
Render. renderSteps({ steps, dialect, from, to }) produces the SQL strings for postgres, sqlite (including the table-rebuild recipe for ALTERs sqlite cannot express) or mysql. describeStep({ step }) gives the one-line human description used in CLI output and refusal messages.
Run. applyMigrations executes MigrationFiles in order under an advisory lock, recording each in the _vibe_migrations bookkeeping table with a sha256 checksum over its statements. Re-applying a migration whose SQL changed is a VIBE_MIGRATION error, not a silent divergence. On postgres and sqlite each migration runs inside one transaction with its bookkeeping row; on mysql, where DDL auto-commits, progress is recorded per statement and a re-run resumes from where it stopped. migrationStatus reports applied, pending, mismatched and mid-rollback migrations, rollbackMigration runs a migration's downSql, and resolveMigration repairs the bookkeeping table without running SQL.
Introspect. introspectPostgres, introspectSqlite and introspectMysql read a live database back into SchemaIR; pull picks the right one for a dialect.
push vs migrate
push goes straight from the live database to your schema — introspect, diff, render, apply — with no files kept. It suits prototyping and disposable databases. generateMigration plus applyMigrations keeps named, checksummed SQL files you can review and replay, which is what shared and production databases need. Details in docs/migrations.md.
Destructive steps
Every step carries a destructive flag: drops of tables, columns, enums and join tables, plus narrowing type changes. push refuses to run when any step is destructive unless you pass acceptDataLoss: true, and the thrown VibeError (VIBE_MIGRATION) lists each offending step by description. generateMigration surfaces the same information as MigrationFile.destructive so callers can require confirmation before applying. Constructs a dialect cannot express — scalar lists on sqlite and mysql, partial indexes on mysql, removing a value from a surviving enum — throw VIBE_UNSUPPORTED_CAPABILITY instead of degrading.
Usage
import { applyMigrations, generateMigration, push, pull } from "@vibeorm/migrate";
import { toSqlExecutor } from "@vibeorm/runtime";
const executor = toSqlExecutor({ adapter });
// Prototyping: make the database match the schema.
const result = await push({ executor, schema, acceptDataLoss: false });
console.log(result.steps, result.applied);
// Versioned: render a named migration from a diff, then apply it.
const migration = generateMigration({ from: previous, to: schema, name: "add_posts", dialect: "postgres" });
await applyMigrations({ executor, migrations: [migration], dialect: "postgres" });
// Introspect the live database back into IR.
const live = await pull({ executor, dialect: "postgres" });Objects another tool owns are declared as plain ExternalObjectOwner data. parseExternalObjects validates declarations without a database, inspectExternalObjects({ executor, dialect, external }) resolves them against the catalog, and push accepts that resolution as external; applyMigrations takes the plain declarations as external and resolves them under its lock. PostgreSQL and PGlite only. See external objects.
SqlExecutor calls must all reach the same database session — the engine issues BEGIN/COMMIT, PRAGMA and advisory-lock statements through it. Pool-backed executors must pin one connection for the whole operation.
Links
- Documentation
- Query API
- Repository and issues
- MIT licensed
