| 1 | import * as Data from "effect/Data"; |
| 2 | import type * as Effect from "effect/Effect"; |
| 3 | |
| 4 | export type MigrationDialect = "postgres" | "mysql" | "sqlite"; |
| 5 | |
| 6 | /** |
| 7 | * A single migration read from disk, normalized across directory layouts. |
| 8 | */ |
| 9 | export 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 | */ |
| 35 | export 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. */ |
| 57 | export 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 | */ |
| 66 | export 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 | */ |
| 79 | export class MigrationHistoryConflictError extends Data.TaggedError( |
| 80 | "MigrationHistoryConflictError", |
| 81 | )<{ |
| 82 | table: string; |
| 83 | unmatched: ReadonlyArray<string>; |
| 84 | message: string; |
| 85 | }> {} |
| 86 | |
| 87 | export type MigrationApplyError = |
| 88 | | MigrationError |
| 89 | | MigrationHistoryConflictError; |