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

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

  • 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

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.