14 KiB
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
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
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
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
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
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
class MigrationExecutor {
constructor(options: ExecutorOptions)
execute(plan: MigrationPlan, direction: MigrationDirection, options?: RunOptions): Promise<MigrationResult>
}
Dry-run reporter
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
class MigrationStatusDashboard {
constructor(config: StatusConfig)
getAll(): Promise<ExtendedMigrationStatus[]>
getSummary(): Promise<MigrationSummary>
healthCheck(): Promise<MigrationHealthCheck>
getHistory(id: string): Promise<MigrationHistoryEntry[]>
}
Batch migrator
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
class SurrealDbAdapter implements DatabaseAdapter {
constructor(config: SurrealDbAdapterConfig)
query<T>(sql: string, params?: unknown[]): Promise<T[]>
}
interface SurrealDbAdapterConfig { db: unknown /* Surreal instance */ }
Events
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
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
MigrationRunnerConfigSchemaRunOptionsSchemaBatchMigratorConfigSchemaDatabaseStoreConfigSchemaDistributedLockConfigSchemaRedisLockConfigSchema
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)
planUpidempotency: applyingplanUpto an already fully-applied set returns an emptytoApplylist.sortMigrationsstability: for any permutation of N migrations,sortMigrationsreturns the same order.checksumMigrationdeterminism: calling checksum on the same migration object twice returns the same string.BatchMigratorpartial 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 againstMemoryMigrationStore.checksumMigration_1k: 1000 checksum computations.
File layout
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.