first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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)`.