Migrate
File-based migrations with version control for production — each migration stored as a SQL file committable to git
When to Use Migrate
- Production deployments - Versioned, repeatable schema changes
- Team collaboration - Migration files in version control
- CI/CD pipelines - Automated migration deployment
- Rollback support - Down migrations for reverting changes
Workflow Overview
1. Make changes to your VibORM models
2. Generate a migration file
3. Review the generated SQL
4. Apply the migration to database
Generate Migrations
Generate a new migration by comparing your current models against the previous snapshot:
# Generate a migration with auto-generated name
npx viborm migrate generate
# Generate with a custom name
npx viborm migrate generate --name add-users-table
# Preview without creating files
npx viborm migrate generate --dry-runimport { createMigrationClient } from "viborm/migrations";
import { createFsStorageDriver } from "viborm/migrations/storage/fs";
import { client } from "./client";
const migrations = createMigrationClient(client, {
storageDriver: createFsStorageDriver("./migrations"),
});
// Generate with custom name
const result = await migrations.generate({
name: "add-users-table",
});
if (result.entry) {
console.log(result.message); // e.g. "Generated migration 0003_add-users-table"
console.log(`Operations: ${result.operations.length}`);
console.log(`Files written: ${result.written}`);
} else {
console.log("No changes detected");
}
// Preview without creating files
const preview = await migrations.preview();Generate Options
| Option | CLI | API | Description |
|---|---|---|---|
| Name | --name <name> |
name: string |
Custom migration name |
| Output directory | --out <dir> |
dir: string |
Output directory (default: ./migrations) |
| Dry run | --dry-run |
dryRun: boolean |
Preview without creating files |
| Resolver | - | resolver: Resolver |
Handle ambiguous changes |
| Enum resolver | - | enumValueResolver: EnumValueResolver |
Handle enum value removals |
Check Migration Status
View which migrations have been applied:
# Show migration status
npx viborm migrate statusOutput:
Migration Status:
[x] 0000_initial.sql (applied 2024-01-15)
[x] 0001_add-users.sql (applied 2024-01-16)
[ ] 0002_add-posts.sql (pending)const statuses = await migrations.status();
for (const status of statuses) {
const state = status.appliedAt ? "applied" : "pending";
const date = status.appliedAt?.toLocaleDateString() ?? "";
console.log(`${status.entry.name}: ${state} ${date}`);
}
// Get only pending migrations
const pending = await migrations.pending();
console.log(`${pending.length} migrations pending`);Status Options
| Option | CLI | API | Description |
|---|---|---|---|
| Directory | --dir <dir> |
- | Migrations directory (default: ./migrations) |
| Table name | --table-name <name> |
tableName: string |
Migrations tracking table name |
Apply Migrations
Apply pending migrations to the database:
# Apply all pending migrations
npx viborm migrate apply
# Apply up to a specific migration index
npx viborm migrate apply --to 5
# Preview without applying
npx viborm migrate apply --dry-run// Apply all pending
const result = await migrations.apply();
console.log(`Applied ${result.applied.length} migrations`);
// Apply up to a specific migration index
await migrations.apply({ to: 5 });
// Preview without applying
await migrations.apply({ dryRun: true });Apply Options
| Option | CLI | API | Description |
|---|---|---|---|
| To | --to <index> |
to: number |
Apply up to this migration index |
| Directory | --dir <dir> |
- | Migrations directory (default: ./migrations) |
| Table name | --table-name <name> |
tableName: string |
Migrations tracking table name |
| Dry run | --dry-run |
dryRun: boolean |
Preview without applying |
| Force | --force |
- | Skip confirmation prompts |
Roll Back Migrations
Every generated migration includes a down migration (stored in
meta/_down/) that reverses its operations. Lossy operations (dropped
tables or columns) are annotated with warnings in the down file — the
structure is restored, but not the data.
# Roll back the last migration
npx viborm migrate down
# Roll back the last 3 migrations
npx viborm migrate down --steps 3
# Roll back to a specific migration (by name or index)
npx viborm migrate down --to add-users
# Preview without executing
npx viborm migrate down --dry-run// Roll back the last migration
const result = await migrations.down({ steps: 1 });
console.log(`Rolled back: ${result.rolledBack.length} migrations`);
// Roll back to a specific point
await migrations.down({ to: "add-users" });
// Preview
await migrations.down({ steps: 1, dryRun: true });Down Options
| Option | CLI | API | Description |
|---|---|---|---|
| Steps | --steps <n> |
steps: number |
Number of migrations to roll back |
| To | --to <migration> |
to: string | number |
Roll back to this migration (exclusive) |
| Directory | --dir <dir> |
- | Migrations directory (default: ./migrations) |
| Table name | --table-name <name> |
tableName: string |
Migrations tracking table name |
| Dry run | --dry-run |
dryRun: boolean |
Preview without executing |
| Force | --force |
- | Skip confirmation prompts |
Drop Migration Tracking
migrate drop removes the last N migrations (default: 1) from the tracking table. It does not revert any database changes — to undo schema changes, use migrate down instead:
# Remove the last migration from tracking
npx viborm migrate drop
# Remove the last 3 migrations from tracking
npx viborm migrate drop --count 3// Removes from tracking only (doesn't touch tables or data)
await migrations.rollback({ count: 1 });Drop Options
| Option | CLI | API | Description |
|---|---|---|---|
| Last | --last |
- | Drop only the last migration (the default) |
| Count | --count <n> |
count: number |
Drop the last N migrations |
| Directory | --dir <dir> |
- | Migrations directory (default: ./migrations) |
| Table name | --table-name <name> |
tableName: string |
Migrations tracking table name |
| Force | --force |
- | Skip confirmation prompts |
Reset Database (API only)
reset() drops all tables and enums, clears the migration tracking table, and re-applies every migration from the journal. There is no CLI equivalent:
await migrations.reset();
Squash Migrations
Combine multiple migrations into one:
// List migrations to review
const entries = await migrations.list();
console.log(entries.map(e => `${e.idx}: ${e.name}`));
// Squash development migrations
const result = await migrations.squash({
from: 0,
to: 5,
name: "initial-schema",
cleanup: true, // Delete original files
});
console.log(`Squashed into: ${result.newEntry.name}`);
Squash Options
| Option | Type | Description |
|---|---|---|
from |
number |
Starting migration index |
to |
number |
Ending migration index (inclusive) |
name |
string |
Name for the squashed migration |
cleanup |
boolean |
Delete original migration files |
dryRun |
boolean |
Preview without squashing |
Migration File Structure
migrations/
├── 0000_initial.sql
├── 0001_add-users.sql
├── 0002_add-posts.sql
└── meta/
├── _journal.json
├── _snapshot.json
└── _down/
├── 0000_initial.sql
├── 0001_add-users.sql
└── 0002_add-posts.sql
Up Migration
The main SQL file contains the forward migration:
CREATE TABLE "users" (
"id" serial PRIMARY KEY,
"email" text NOT NULL,
"created_at" timestamp DEFAULT NOW()
);
CREATE UNIQUE INDEX "users_email_idx" ON "users" ("email");
Down Migration
The down file contains the rollback SQL:
DROP INDEX "users_email_idx";
DROP TABLE "users";
Journal
Tracks migration metadata:
{
"version": "1",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "20240115120000",
"name": "initial",
"when": 1705320000000,
"checksum": "sha256:abc123..."
}
]
}
Snapshot
Stores the schema state after migrations:
{
"tables": [...],
"enums": [...]
}
Reading Migration Data
Access migration data programmatically:
// List all migrations
const entries = await migrations.list();
// Get full journal
const journal = await migrations.journal();
// Get schema snapshot
const snapshot = await migrations.snapshot();
// Read migration SQL content
const sql = await migrations.read(entries[0]);
Error Handling
import { MigrationError, isMigrationError } from "viborm/migrations";
try {
await migrations.apply();
} catch (error) {
if (isMigrationError(error)) {
console.error(`Migration error: ${error.code}`);
console.error(error.message);
}
}
| Error Code | Description |
|---|---|
MIGRATION_STORAGE_REQUIRED |
Operation requires a storage driver but none was provided |
MIGRATION_CHECKSUM_MISMATCH |
Migration file was modified after being applied |
MIGRATION_NOT_FOUND |
Referenced migration doesn’t exist |
MIGRATION_ALREADY_APPLIED |
Migration was already applied |
CI/CD Integration
Example GitHub Actions workflow:
- name: Apply migrations
run: npx viborm migrate apply
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
For programmatic deployments:
import { createMigrationClient } from "viborm/migrations";
import { createFsStorageDriver } from "viborm/migrations/storage/fs";
import { client } from "./db/client";
const migrations = createMigrationClient(client, {
storageDriver: createFsStorageDriver("./migrations"),
});
const pending = await migrations.pending();
if (pending.length > 0) {
console.log(`Applying ${pending.length} migrations...`);
await migrations.apply();
console.log("Done!");
}