TypeDrop

2026-04-21 Challenge

2026-04-21 Hard

Typed Schema Migration Engine

You're building the schema migration engine for a multi-tenant database platform. Raw migration manifests arrive as `unknown` JSON from a CI/CD pipeline; your engine must validate them, compile each migration into a strongly-typed dependency graph, execute migrations in topological order with concurrency limits and rollback support, and emit a discriminated-union result per migration — with zero `any`.

Goals

  • Implement `validateManifest` to narrow `unknown` input into `ValidMigration[]`, accumulating ALL `ValidationError`s (field errors, duplicates, unknown deps) without short-circuiting, and brand valid ids as `MigrationId` without using `as`.
  • Implement `compileGraph` to build a typed dependency graph, detect cycles with a path-reporting DFS or Kahn's algorithm, and produce topologically sorted `waves` of concurrently-runnable migrations.
  • Implement `runMigrations` to process waves sequentially with bounded concurrency (`Promise.allSettled`), skip already-applied migrations, and on failure optionally roll back all applied migrations in reverse order — emitting a discriminated-union `MigrationResult` per migration.
  • Wire everything together in `runEngine` with proper `Partial<EngineOptions>` default-filling and correct `EngineReport` propagation from each stage.
challenge.ts

// Key types & main orchestrator signature

export type MigrationId = string & { readonly __brand: "MigrationId" };

export type MigrationResult =
  | { status: "applied";    id: MigrationId; durationMs: number }
  | { status: "skipped";    id: MigrationId; reason: string }
  | { status: "failed";     id: MigrationId; error: ExecutionError }
  | { status: "rolledBack"; id: MigrationId; error: ExecutionError };

export type EngineReport =
  | { outcome: "success";         results: MigrationResult[]; totalMs: number }
  | { outcome: "partial_failure"; results: MigrationResult[]; totalMs: number; firstFailure: MigrationId }
  | { outcome: "validation_error"; errors: ValidationError[] }
  | { outcome: "graph_error";      error: GraphError };

export type ExecutorFn = (
  id: MigrationId,
  sql: string,
  direction: "up" | "down"
) => Promise<{ ok: true } | { ok: false; reason: string }>;

/** Validates → compiles graph → executes waves → returns structured report. */
export async function runEngine(
  rawManifest: unknown,
  executor: ExecutorFn,
  options?: Partial<EngineOptions>
): Promise<EngineReport> { /* TODO */ }
Hints (click to reveal)

Hints

  • For branding `MigrationId` without `as`, write a type predicate `function isMigrationId(s: string): s is MigrationId` that validates the string, then call it inside `validateManifest` — the narrowed type flows automatically.
  • Kahn's algorithm (in-degree queue) naturally produces topological waves: all nodes with in-degree 0 form wave 0; decrement neighbors and collect the next zero-in-degree batch for wave 1, and so on — cycle detection is free (remaining non-zero in-degree nodes).
  • Track applied migrations in a local `MigrationId[]` (push on success); on rollback iterate with `.reverse()` and call the executor with `direction: "down"` — `Promise.allSettled` ensures you capture every settled result even when some reject.

Or clone locally

git clone -b challenge/2026-04-21 https://github.com/niltonheck/typedrop.git