first commit
This commit is contained in:
101
docs/persistence/database/db/src/migration/migration-runner.md
Normal file
101
docs/persistence/database/db/src/migration/migration-runner.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# MigrationRunner
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`MigrationRunner` is the interface that `DatabaseAdapter.migrate` delegates to in concrete adapter implementations. Separating the migration contract from the adapter contract lets adapters swap migration strategies without changing their top-level API — for example, a SQLite adapter might use Drizzle's migration runner while a SurrealDB adapter uses a custom `.surql` file runner, yet both expose the same `DatabaseAdapter.migrate` signature to callers.
|
||||
|
||||
The three-method design mirrors the principle of least surprise: `pendingMigrations` is a read-only query (safe to call for status checks without making changes), `applyMigration` is the atomic unit of work, and `run` is the high-level orchestrator that wires them together.
|
||||
|
||||
## Features
|
||||
|
||||
- `pendingMigrations` — read-only: lists unapplied migrations without side effects
|
||||
- `applyMigration` — atomic unit: applies a single named migration by SQL string
|
||||
- `run` — orchestrator: discovers, orders, and applies all pending migrations
|
||||
- `MigrationError` — thrown with the exact migration name on failure
|
||||
- `AbortSignal` on both `applyMigration` and `run` via `DbOperationOptions`
|
||||
- Accepts optional `MigrationConfig` to override folder/table defaults
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { MigrationRunner } from "@suaveplan/db";
|
||||
import { MigrationError } from "@suaveplan/db";
|
||||
|
||||
class FileMigrationRunner implements MigrationRunner {
|
||||
async pendingMigrations(config?) {
|
||||
const applied = await this.loadApplied(config);
|
||||
const all = await this.discoverFiles(config);
|
||||
return all
|
||||
.filter(m => !applied.has(m.name))
|
||||
.map(m => ({ name: m.name, applied: false }));
|
||||
}
|
||||
|
||||
async applyMigration(name, sql, opts?) {
|
||||
try {
|
||||
await this.db.execute(sql, [], opts);
|
||||
await this.recordApplied(name);
|
||||
} catch (err) {
|
||||
throw new MigrationError(name, err instanceof Error ? err.message : String(err), { cause: err });
|
||||
}
|
||||
}
|
||||
|
||||
async run(config?, opts?) {
|
||||
const pending = await this.pendingMigrations(config);
|
||||
const names: string[] = [];
|
||||
for (const { name } of pending) {
|
||||
const sql = await this.loadSql(name, config);
|
||||
await this.applyMigration(name, sql, opts);
|
||||
names.push(name);
|
||||
}
|
||||
return { applied: names.length, migrations: names };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { MigrationRunner } from "@suaveplan/db";
|
||||
import { MigrationError, withDbSpan } from "@suaveplan/db";
|
||||
|
||||
// Wrapping run with OTel tracing
|
||||
async function runMigrations(runner: MigrationRunner): Promise<void> {
|
||||
const result = await withDbSpan("db.migrate", () =>
|
||||
runner.run({ migrationsFolder: "./db/migrations" })
|
||||
);
|
||||
console.log(`Applied ${result.applied} migration(s):`, result.migrations);
|
||||
}
|
||||
|
||||
// Status check without applying
|
||||
async function checkMigrationStatus(runner: MigrationRunner): Promise<void> {
|
||||
const pending = await runner.pendingMigrations();
|
||||
if (pending.length > 0) {
|
||||
console.warn(`${pending.length} unapplied migrations:`, pending.map(m => m.name));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `MigrationRunner`
|
||||
|
||||
```ts
|
||||
interface MigrationRunner {
|
||||
pendingMigrations(config?: MigrationConfig): Promise<MigrationStatus[]>;
|
||||
applyMigration(name: string, sql: string, opts?: DbOperationOptions): Promise<void>;
|
||||
run(config?: MigrationConfig, opts?: DbOperationOptions): Promise<MigrationResult>;
|
||||
}
|
||||
```
|
||||
|
||||
- **`pendingMigrations`** — returns only unapplied migrations; never mutates state
|
||||
- **`applyMigration`** — applies one migration; throws `MigrationError` on failure with the migration's name in `error.migration`
|
||||
- **`run`** — discovers pending migrations via `pendingMigrations` and applies them in order via `applyMigration`; returns aggregate `MigrationResult`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`MigrationRunner` is intentionally an interface rather than an abstract class. This keeps the dependency graph clean — the runner itself has no implementation, so it imposes no runtime cost. Concrete adapters in `@suaveplan/db-sqlite` and future packages supply the implementation.
|
||||
|
||||
The `AbortSignal` is only threaded through `applyMigration` and `run` — `pendingMigrations` is a read-only operation and is not expected to take long enough to warrant cancellation in typical use cases. Implementors may add signal support to `pendingMigrations` as a non-breaking extension.
|
||||
Reference in New Issue
Block a user