Files
skills/docs/persistence/database/db-sqlite/src/migration/migration.md
2026-05-29 08:20:56 +01:00

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 (.sql files 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 Clock for deterministic applied_at timestamps in tests
  • Configurable migrationsFolder, migrationsTable, and dryRun via MigrationRunnerOptionsSchema
  • 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).