first commit
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# migration
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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<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)`.
|
||||
Reference in New Issue
Block a user