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.
Useful resources
Or clone locally
git clone -b challenge/2026-04-21 https://github.com/niltonheck/typedrop.git