3.8 KiB
migration
(no submodules)
Purpose
MigrationRunner applies forward-only SQL migration files to a SQLite database. It maintains a ledger table (__sqlite_migrations by default) that records which migration files have been applied, making runs idempotent — re-running the same set of migration files produces the same database state regardless of how many times it is called.
Each migration is executed inside a BEGIN IMMEDIATE transaction. A failure mid-migration triggers an automatic ROLLBACK, ensuring the database is never left in a partially-applied state. A dryRun() mode reads and validates files but makes no database changes, letting callers preview what would be applied.
Features
- Forward-only, filename-ordered migration application (
.sqlfiles sorted lexicographically) - Idempotent: skips already-applied migrations via ledger table lookup
- Transactional: each migration runs in
BEGIN IMMEDIATE/COMMIT/ROLLBACK - Dry-run mode: previews pending migrations without modifying the database
- Injectable
Clockfor deterministicapplied_attimestamps in tests - Configurable
migrationsFolder,migrationsTable, anddryRunviaMigrationRunnerOptionsSchema - Throws
SqliteError(SQLITE.MIGRATION_FAILED)with original cause on any failure
Usage: Basic Example
import { createBunDriver } from "@suaveplan/db-sqlite/drivers/bun-driver";
import { MigrationRunner } from "@suaveplan/db-sqlite/migration/migration";
const { sqlite, close } = await createBunDriver({ filename: "app.db" });
try {
const runner = new MigrationRunner(sqlite, { migrationsFolder: "./db/migrations" });
const result = await runner.run();
console.log(`Applied ${result.applied} migration(s):`, result.migrations);
} finally {
await close();
}
Usage: Advanced Example
import { MigrationRunner } from "@suaveplan/db-sqlite/migration/migration";
// Dry-run to preview without applying
const runner = new MigrationRunner(sqlite, {
migrationsFolder: "./db/migrations",
migrationsTable: "custom_ledger",
});
const preview = await runner.dryRun();
console.log("Would apply:", preview.migrations);
if (preview.applied > 0) {
const result = await runner.run();
console.log("Applied:", result.migrations);
}
API Reference
new MigrationRunner(sqlite, options?, clock?)
Constructs a MigrationRunner. Validates options via MigrationRunnerOptionsSchema.
| Parameter | Type | Description |
|---|---|---|
sqlite |
bun:sqlite Database |
Database instance to migrate |
options |
Partial<MigrationRunnerOptions> |
Folder, table, dry-run settings |
clock |
Clock |
Injectable clock for applied_at (default: Date.now) |
run(options?: Partial<MigrationRunnerOptions>): Promise<MigrationResult>
Reads .sql files from migrationsFolder, skips already-applied entries, applies each in order, records to ledger. Returns { applied: number, migrations: string[] }.
dryRun(options?: Partial<MigrationRunnerOptions>): Promise<MigrationResult>
Same as run() but makes no database changes. Useful for change-preview UIs and CI checks.
Implementation Notes
Migrations are sorted lexicographically by filename. Conventional naming (001_init.sql, 002_add_users.sql) produces chronological order. Non-.sql files in the folder are silently ignored.
The #isMigrationApplied check uses a raw SQL query rather than a prepared statement to avoid circular dependency with PreparedStatementCache — the migration runner runs before the adapter is fully connected and the cache is available.
The applied_at column stores a Unix millisecond timestamp as INTEGER rather than ISO 8601 text. This keeps sorting and comparison simple and avoids timezone ambiguity. Consumers that need a Date object divide by 1000 or use new Date(applied_at).