1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import * as crypto from "node:crypto";
5import { listSqlFiles, splitSqlStatements } from "../SqlFile.ts";
6import { detectLayout } from "./Detect.ts";
7import { MigrationError, type MigrationRecord } from "./Format.ts";
8import { DRIZZLE_DIR_PATTERN, timestampPrefixMillis } from "./Utils.ts";
9
10export {
11 DRIZZLE_DIR_PATTERN,
12 inlineSqlParams,
13 quoteIdentifier,
14 sqlLiteral,
15 timestampPrefixMillis,
16} from "./Utils.ts";
17
18/** Map filesystem failures into the migration error channel. */
19export const mapPlatformError = <A, E, R>(
20 effect: Effect.Effect<A, E, R>,
21 context: string,
22): Effect.Effect<A, MigrationError, R> =>
23 effect.pipe(
24 Effect.mapError(
25 (cause) =>
26 new MigrationError({
27 message: `${context}: ${String(cause)}`,
28 cause,
29 }),
30 ),
31 );
32
33const sha256 = (content: string) =>
34 Effect.sync(() => crypto.createHash("sha256").update(content).digest("hex"));
35
36/**
37 * Read a drizzle-v1-layout directory (`{ts}_{name}/migration.sql`) into
38 * records keyed the way drizzle keys them: `name` = the directory name,
39 * sorted by name (drizzle's own sort), hash = sha256 of `migration.sql`.
40 */
41export const readDrizzleDirRecords = (dir: string) =>
42 mapPlatformError(
43 Effect.gen(function* () {
44 const fs = yield* FileSystem.FileSystem;
45 const path = yield* Path.Path;
46 const entries = yield* fs.readDirectory(dir);
47 const names: string[] = [];
48 for (const entry of entries) {
49 if (!DRIZZLE_DIR_PATTERN.test(entry)) continue;
50 const sqlPath = path.join(dir, entry, "migration.sql");
51 if (yield* fs.exists(sqlPath)) names.push(entry);
52 }
53 names.sort((a, b) => a.localeCompare(b));
54 const records: MigrationRecord[] = [];
55 for (const name of names) {
56 const sql = yield* fs.readFileString(
57 path.join(dir, name, "migration.sql"),
58 );
59 records.push({
60 name,
61 hash: yield* sha256(sql),
62 createdAtMillis: timestampPrefixMillis(name),
63 sql,
64 statements: splitSqlStatements(sql),
65 });
66 }
67 return records;
68 }),
69 `Failed to read drizzle migrations from ${dir}`,
70 );
71
72/**
73 * Read a flat directory of `.sql` files into records keyed by relative file
74 * path — the convention wrangler and legacy Alchemy state share. Nested
75 * `dir/migration.sql` paths are included (via `listSqlFiles`'s recursive
76 * listing) so legacy state written against drizzle-layout dirs keeps
77 * resolving.
78 */
79export const readFlatRecords = (dir: string) =>
80 mapPlatformError(
81 listSqlFiles(dir).pipe(
82 Effect.map((files) =>
83 files.map((file): MigrationRecord => ({
84 name: file.id,
85 hash: file.hash,
86 createdAtMillis: timestampPrefixMillis(file.id),
87 sql: file.sql,
88 statements: splitSqlStatements(file.sql),
89 })),
90 ),
91 ),
92 `Failed to read migrations from ${dir}`,
93 );
94
95/** Read migration records using the directory's detected layout. */
96export const readMigrationRecords = (dir: string) =>
97 detectLayout(dir).pipe(
98 Effect.flatMap((layout) =>
99 layout === "flat" ? readFlatRecords(dir) : readDrizzleDirRecords(dir),
100 ),
101 );