Files
suaveplan/openspec/archive/2026-04-24-enterprise-migration/proposal.md
2026-05-27 23:29:53 +01:00

391 lines
14 KiB
Markdown

# OpenSpec Change: enterprise-migration
- **Change ID**: enterprise-migration
- **Package**: `@suaveplan/migration`
- **Category**: universal
- **Target path**: `universal/database/migration`
- **Status**: PROPOSED
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/migration.md`
---
## Summary
Deliver `@suaveplan/migration` on the genesis branch from scratch at enterprise quality. The package provides a database-agnostic schema migration runner: a `MigrationRunner` that orchestrates up/down execution, a pluggable `MigrationStoreAdapter` for ledger persistence, a `MigrationLockAdapter` interface (memory + distributed), a dry-run reporter, a status dashboard, an optional batch migrator for multi-target rollouts, and a SurrealDB adapter. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all public types, OTel instrumentation, `AbortSignal` cancellation, logger injection via `@suaveplan/logger`, event bus integration via `@suaveplan/events`, property-based tests (fast-check), and a mitata benchmark suite. The migration runner previously bundled inside `@suaveplan/surrealdb` is now a standalone package consuming `@suaveplan/db`'s `DatabaseAdapter` interface.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | Base class for `MigrationError`; `defineCodes` for `MIGRATION.*` codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `DatabaseLike` |
| `@suaveplan/db` (`workspace:^`) | `DatabaseAdapter` interface consumed by `DatabaseMigrationStore` and `SurrealDbAdapter` |
| `@suaveplan/logger` (`workspace:^`) | `LoggerLike` structured logging throughout the runner |
| `@suaveplan/events` (`workspace:^`) | `EventBus` for migration lifecycle events (before/after each step) |
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on runner, planner, executor |
| `tslib` (`catalog:build`) | Runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`), `@suaveplan/db-sqlite workspace:^` (integration test store backend).
---
## Preserved public API surface (from main branch)
### Runner
```ts
class MigrationRunner {
constructor(config: MigrationRunnerConfig)
up(options?: RunOptions): Promise<MigrationResult>
down(options?: RunOptions & { steps?: number }): Promise<MigrationResult>
status(): Promise<MigrationStatus[]>
validate(): Promise<ValidationResult>
}
interface MigrationRunnerConfig {
migrations: Migration[]
store: MigrationStoreAdapter
lock?: MigrationLockAdapter
logger?: LoggerLike
dryRun?: boolean
checksumValidation?: boolean
}
interface RunOptions { signal?: AbortSignal; target?: string }
```
### Types
```ts
interface Migration {
id: string
name: string
up: (ctx: MigrationContext) => Promise<void>
down?: (ctx: MigrationContext) => Promise<void>
checksum?: string
}
interface MigrationContext { db: DatabaseAdapter; logger: LoggerLike }
type MigrationDirection = 'up' | 'down'
type MigrationStatus = 'pending' | 'applied' | 'failed' | 'rolled-back'
interface MigrationRecord { id: string; name: string; appliedAt: number; checksum?: string; direction: MigrationDirection }
interface MigrationPlan { toApply: Migration[]; toRollback: Migration[] }
interface MigrationResult { applied: string[]; skipped: string[]; failed: string[]; direction: MigrationDirection; dryRun: boolean }
interface ValidationError { migrationId: string; message: string }
interface ValidationResult { valid: boolean; errors: ValidationError[] }
```
### Store adapters
```ts
interface MigrationStoreAdapter {
getApplied(): Promise<MigrationRecord[]>
record(record: MigrationRecord): Promise<void>
remove(id: string): Promise<void>
clear(): Promise<void>
}
class MemoryMigrationStore implements MigrationStoreAdapter
class DatabaseMigrationStore implements MigrationStoreAdapter {
constructor(config: DatabaseStoreConfig)
}
interface DatabaseStoreConfig { adapter: DatabaseAdapter; tableName?: string }
interface DatabaseAdapter { query<T>(sql: string, params?: unknown[]): Promise<T[]> }
```
### Lock adapters
```ts
interface MigrationLockAdapter {
acquire(timeout?: number): Promise<boolean>
release(): Promise<void>
isLocked(): Promise<boolean>
}
class MemoryMigrationLock implements MigrationLockAdapter
class DistributedMigrationLock implements MigrationLockAdapter {
constructor(config: DistributedLockConfig)
}
interface DistributedLockAdapter { acquire(key: string, ttl: number): Promise<boolean>; release(key: string): Promise<void> }
interface DistributedLockConfig { adapter: DistributedLockAdapter; key?: string; ttl?: number }
class InMemoryDistributedLockAdapter implements DistributedLockAdapter
class RedisDistributedLock implements MigrationLockAdapter {
constructor(config: RedisLockConfig)
}
interface RedisLockClient { set(key: string, value: string, options: object): Promise<string | null>; del(key: string): Promise<number> }
interface RedisLockConfig { client: RedisLockClient; key?: string; ttl?: number }
```
### Planner
```ts
function planUp(all: Migration[], applied: MigrationRecord[]): MigrationPlan
function planDown(all: Migration[], applied: MigrationRecord[], steps?: number): MigrationPlan
function sortMigrations(migrations: Migration[]): Migration[]
function checksumMigration(migration: Migration): string
```
### Executor
```ts
class MigrationExecutor {
constructor(options: ExecutorOptions)
execute(plan: MigrationPlan, direction: MigrationDirection, options?: RunOptions): Promise<MigrationResult>
}
```
### Dry-run reporter
```ts
class DryRunReporter {
constructor()
record(entry: DryRunEntry): void
report(): DryRunReport
}
interface DryRunEntry { migrationId: string; direction: MigrationDirection; riskLevel: RiskLevel; reason?: string }
interface DryRunReport { entries: DryRunEntry[]; totalRisk: RiskLevel; summary: string }
type RiskLevel = 'low' | 'medium' | 'high'
```
### Status dashboard
```ts
class MigrationStatusDashboard {
constructor(config: StatusConfig)
getAll(): Promise<ExtendedMigrationStatus[]>
getSummary(): Promise<MigrationSummary>
healthCheck(): Promise<MigrationHealthCheck>
getHistory(id: string): Promise<MigrationHistoryEntry[]>
}
```
### Batch migrator
```ts
class BatchMigrator {
constructor(config: BatchMigratorConfig)
run(options?: RunOptions): Promise<BatchMigrationResult>
}
interface BatchMigratorConfig { targets: Array<{ name: string; runner: MigrationRunner }>; concurrency?: number }
interface BatchMigrationResult { results: Map<string, MigrationResult>; failed: string[]; succeeded: string[] }
```
### SurrealDB adapter
```ts
class SurrealDbAdapter implements DatabaseAdapter {
constructor(config: SurrealDbAdapterConfig)
query<T>(sql: string, params?: unknown[]): Promise<T[]>
}
interface SurrealDbAdapterConfig { db: unknown /* Surreal instance */ }
```
### Events
```ts
interface MigrationEventMap {
'migration:before': { migration: Migration; direction: MigrationDirection }
'migration:after': { migration: Migration; direction: MigrationDirection; durationMs: number }
'migration:failed': { migration: Migration; direction: MigrationDirection; error: Error }
'migration:plan': { plan: MigrationPlan; direction: MigrationDirection }
}
const migrationEvents: EventBus<MigrationEventMap>
```
### Error codes
```ts
type MigrationErrorCode =
| 'MIGRATION_FAILED'
| 'MIGRATION_LOCK_TIMEOUT'
| 'MIGRATION_CHECKSUM_MISMATCH'
| 'MIGRATION_NOT_FOUND'
| 'MIGRATION_STORE_ERROR'
| 'MIGRATION_PLAN_INVALID'
| 'MIGRATION_ABORTED'
| 'MIGRATION_DRY_RUN'
```
---
## New additions for genesis
### 1. Structured `MigrationError`
`MigrationError` extends `SuaveplanError` from `@suaveplan/error` carrying a `MigrationErrorCode` discriminant. Internal throw sites use named factory helpers.
### 2. Zod schemas
- `MigrationRunnerConfigSchema`
- `RunOptionsSchema`
- `BatchMigratorConfigSchema`
- `DatabaseStoreConfigSchema`
- `DistributedLockConfigSchema`
- `RedisLockConfigSchema`
### 3. Logger injection
`MigrationRunnerConfig.logger` is now typed as `LoggerLike` (from `@suaveplan/types`). All runner, planner, and executor operations emit structured log entries at `debug` / `info` / `error` levels with migration ID, direction, and duration.
### 4. Event bus integration
`MigrationRunnerConfig` gains an optional `events?: EventBus<MigrationEventMap>`. When provided the runner emits `migration:before`, `migration:after`, `migration:failed`, `migration:plan` events. The exported `migrationEvents` singleton is available for packages that do not control runner construction.
### 5. OTel instrumentation
| Operation | Signal |
|---|---|
| `runner.up` / `runner.down` | span `migration.run`, attribute `migration.direction`, `migration.count` |
| per-migration execution | span `migration.execute`, attribute `migration.id`, histogram `migration.duration_ms` |
| `runner.status` | counter `migration.status_query_total` |
| `batchMigrator.run` | span `migration.batch`, attribute `migration.target_count` |
### 6. Checksum validation
`checksumMigration` hashes the serialised `up` function body. When `checksumValidation: true` (default on `MigrationRunner`), mismatched checksums between the registered migration and the store record throw `MigrationError(MIGRATION_CHECKSUM_MISMATCH)` before applying.
### 7. Property-based tests (fast-check)
- `planUp` idempotency: applying `planUp` to an already fully-applied set returns an empty `toApply` list.
- `sortMigrations` stability: for any permutation of N migrations, `sortMigrations` returns the same order.
- `checksumMigration` determinism: calling checksum on the same migration object twice returns the same string.
- `BatchMigrator` partial failure: if K of N targets fail, the result has exactly K failed entries.
### 8. Benchmark suite
- `planUp_100`: plan for 100 pending migrations.
- `runner_up_10_memory`: run 10 no-op migrations against `MemoryMigrationStore`.
- `checksumMigration_1k`: 1000 checksum computations.
---
## File layout
```text
universal/database/migration/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts # Public barrel
error-codes.ts # MIGRATION.* codes
migration-events.ts # migrationEvents singleton + MigrationEventMap
constants/
index.ts # DEFAULT_TABLE_NAME, DEFAULT_LOCK_TTL
types/
migration.types.ts # All public TS interfaces
migration.types.md
schemas.ts # Zod schemas
schemas.md
planner/
migration-planner/
migration-planner.ts # planUp, planDown, sortMigrations, checksumMigration
migration-planner.md
migration-planner.test.ts
index.ts
runner/
migration-runner/
migration-runner.ts # MigrationRunner
migration-runner.md
migration-runner.types.ts
migration-runner.test.ts
index.ts
migration-executor/
migration-executor.ts # MigrationExecutor
migration-executor.md
migration-executor.test.ts
index.ts
dry-run-reporter/
dry-run-reporter.ts
dry-run-reporter.md
dry-run-reporter.test.ts
index.ts
store/
memory-store/
memory-store.ts # MemoryMigrationStore
memory-store.md
memory-store.test.ts
index.ts
database-store/
database-store.ts # DatabaseMigrationStore
database-store.md
database-store.types.ts
database-store.test.ts
index.ts
lock/
memory-lock/
memory-lock.ts
memory-lock.md
memory-lock.test.ts
index.ts
distributed-lock/
distributed-lock.ts
distributed-lock.md
distributed-lock.types.ts
distributed-lock.test.ts
index.ts
redis-lock/
redis-lock.ts
redis-lock.md
redis-lock.types.ts
redis-lock.test.ts
index.ts
status/
migration-status/
migration-status.ts # MigrationStatusDashboard
migration-status.md
migration-status.types.ts
migration-status.test.ts
index.ts
batch/
batch-migrator/
batch-migrator.ts
batch-migrator.md
batch-migrator.types.ts
batch-migrator.test.ts
index.ts
adapters/
surrealdb-adapter/
surrealdb-adapter.ts
surrealdb-adapter.md
surrealdb-adapter.types.ts
surrealdb-adapter.test.ts
index.ts
__benchmarks__/
migration.bench.ts
tests/
integration.test.ts # Full up/down cycle with SQLite store (db-sqlite)
property.test.ts # fast-check property tests
```
---
## Impact
**Downstream packages**: any package that manages database schema evolution. `@suaveplan/surrealdb` provides a `SurrealAdapter` that implements `DatabaseAdapter`; `@suaveplan/migration`'s `SurrealDbAdapter` wraps that for the migration store.
**Breaking changes from main-branch API**: none. New optional fields (`events`, `signal`) are additive.
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/db`, `@suaveplan/logger`, `@suaveplan/events` must be published to Verdaccio before this package can be built or tested.