Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View 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.