# 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 ```ts 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 ```ts 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` | Folder, table, dry-run settings | | `clock` | `Clock` | Injectable clock for `applied_at` (default: `Date.now`) | ### `run(options?: Partial): Promise` Reads `.sql` files from `migrationsFolder`, skips already-applied entries, applies each in order, records to ledger. Returns `{ applied: number, migrations: string[] }`. ### `dryRun(options?: Partial): Promise` 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)`.