1import * as Data from "effect/Data";
2import type * as Effect from "effect/Effect";
3
4export type MigrationDialect = "postgres" | "mysql" | "sqlite";
5
6/**
7 * A single migration read from disk, normalized across directory layouts.
8 */
9export interface MigrationRecord {
10 /**
11 * The bookkeeping key used for applied-detection. Layout-dependent: the
12 * directory name for drizzle-kit/Prisma-layout dirs
13 * (`20260721033159_init`), the relative file path for flat dirs
14 * (`0001_users.sql`).
15 */
16 name: string;
17 /** sha256 hex of the raw file content. */
18 hash: string;
19 /**
20 * Millis derived from a 14-digit `YYYYMMDDHHMMSS` prefix when present.
21 */
22 createdAtMillis: number | undefined;
23 /** Raw file content. */
24 sql: string;
25 /** Individual statements (split on `--> statement-breakpoint`). */
26 statements: string[];
27}
28
29/**
30 * The minimal query surface the migration flow needs from its target
31 * database. Each database resource adapts its native access path (the D1
32 * HTTP API, a `pg.Client` over a Planetscale temp role, a `mysql2`
33 * connection, the local workerd D1 tunnel) into this shape once.
34 */
35export interface SqlExecutor {
36 readonly dialect: MigrationDialect;
37 /**
38 * Run a single query and return its rows as objects. `params` bind as
39 * `?`/`$n` placeholders; adapters without native parameter support inline
40 * them as SQL literals (see `inlineSqlParams`).
41 */
42 readonly query: (
43 sql: string,
44 params?: ReadonlyArray<unknown>,
45 ) => Effect.Effect<Array<Record<string, unknown>>, MigrationError>;
46 /**
47 * Execute one or more statements as a unit — a transaction where the
48 * target supports one (pg/mysql), a single batched query on D1 (which has
49 * no transactions over HTTP).
50 */
51 readonly batch: (
52 statements: ReadonlyArray<string>,
53 ) => Effect.Effect<void, MigrationError>;
54}
55
56/** A migration failed to read, convert, or apply. */
57export class MigrationError extends Data.TaggedError("MigrationError")<{
58 message: string;
59 cause?: unknown;
60}> {}
61
62/**
63 * The migrations directory uses drizzle-kit's pre-v1 layout
64 * (`meta/_journal.json`). The fix is upstream: `drizzle-kit up`.
65 */
66export class DrizzleV0LayoutError extends Data.TaggedError(
67 "DrizzleV0LayoutError",
68)<{
69 dir: string;
70 message: string;
71}> {}
72
73/**
74 * A row recorded in an applied-migrations table (ours, or a foreign one
75 * being converted) matches no local migration file. Guessing would corrupt
76 * history, so this is a hard error — migrations were applied to the
77 * database that the local environment does not have.
78 */
79export class MigrationHistoryConflictError extends Data.TaggedError(
80 "MigrationHistoryConflictError",
81)<{
82 table: string;
83 unmatched: ReadonlyArray<string>;
84 message: string;
85}> {}
86
87export type MigrationApplyError =
88 | MigrationError
89 | MigrationHistoryConflictError;