1import { applyMigrations } from "@/SQL/Migrations/index.ts";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { Database } from "bun:sqlite";
4import { expect, layer } from "alchemy-test";
5import { drizzle } from "drizzle-orm/bun-sqlite";
6import { migrate as drizzleMigrate } from "drizzle-orm/bun-sqlite/migrator";
7import * as Effect from "effect/Effect";
8import { makeSqliteExecutor, tableNames } from "./sqlite-executor.ts";
9
10/**
11 * Adopting a drizzle-kit-migrated database (the Seth case): the user ran
12 * `drizzle-kit generate` + `drizzle-kit migrate` before Alchemy ever saw
13 * the database. Migrating to Alchemy is a ONE-WAY conversion — drizzle's
14 * REAL migrator (bun-sqlite driver) produces the starting state, our flow
15 * copies its history into `__alchemy_migrations` and owns bookkeeping from
16 * then on; `__drizzle_migrations` is frozen, never written or dropped.
17 */
18
19const fixturesDir = new URL("./fixtures/drizzle-v1", import.meta.url).pathname;
20
21const describe = layer(NodeServices.layer);
22
23const rowsOf = (db: Database, table: string) =>
24 db.query(`SELECT hash, name FROM ${table} ORDER BY id;`).all() as Array<{
25 hash: string;
26 name: string;
27 }>;
28
29describe("drizzle adoption (one-way conversion)", (it) => {
30 it.effect(
31 "converts drizzle-kit history into __alchemy_migrations without replaying",
32 () =>
33 Effect.gen(function* () {
34 const db = new Database(":memory:");
35 // The user's pre-Alchemy state, produced by drizzle's own migrator.
36 yield* Effect.sync(() =>
37 drizzleMigrate(drizzle({ client: db }), {
38 migrationsFolder: fixturesDir,
39 }),
40 );
41 const drizzleRows = rowsOf(db, "__drizzle_migrations");
42 expect(drizzleRows.length).toBe(2);
43
44 // First Alchemy deploy: adopt. A replay would throw on the bare
45 // CREATE TABLEs, so passing proves conversion-not-reapplication.
46 const executor = makeSqliteExecutor(db);
47 yield* applyMigrations({
48 resolved: { dir: fixturesDir, table: "__alchemy_migrations" },
49 executor,
50 });
51
52 const ours = rowsOf(db, "__alchemy_migrations");
53 expect(ours.map((r) => r.name)).toEqual([
54 "20240101000000_init",
55 "20240102000000_add_posts",
56 ]);
57 // drizzle's hashes are sha256 of migration.sql — carried verbatim.
58 expect(ours.map((r) => r.hash)).toEqual(drizzleRows.map((r) => r.hash));
59 // The drizzle table is frozen, not dropped.
60 expect(rowsOf(db, "__drizzle_migrations")).toEqual(drizzleRows);
61 expect(tableNames(db)).toEqual(
62 expect.arrayContaining(["users", "posts", "__drizzle_migrations"]),
63 );
64 }),
65 { tags: ["unit", "local"] },
66 );
67
68 it.effect(
69 "after conversion, drizzle's table is never written again",
70 () =>
71 Effect.gen(function* () {
72 const db = new Database(":memory:");
73 yield* Effect.sync(() =>
74 drizzleMigrate(drizzle({ client: db }), {
75 migrationsFolder: fixturesDir,
76 }),
77 );
78 const frozen = rowsOf(db, "__drizzle_migrations");
79
80 const executor = makeSqliteExecutor(db);
81 yield* applyMigrations({
82 resolved: { dir: fixturesDir, table: "__alchemy_migrations" },
83 executor,
84 });
85 yield* applyMigrations({
86 resolved: { dir: fixturesDir, table: "__alchemy_migrations" },
87 executor,
88 });
89
90 // One-way: drizzle's table never moves again.
91 expect(rowsOf(db, "__drizzle_migrations")).toEqual(frozen);
92 expect(rowsOf(db, "__alchemy_migrations").length).toBe(2);
93 }),
94 { tags: ["unit", "local"] },
95 );
96
97 it.effect(
98 "our flow is idempotent across repeated deploys",
99 () =>
100 Effect.gen(function* () {
101 const db = new Database(":memory:");
102 const executor = makeSqliteExecutor(db);
103 yield* applyMigrations({
104 resolved: { dir: fixturesDir, table: "__alchemy_migrations" },
105 executor,
106 });
107 const first = rowsOf(db, "__alchemy_migrations");
108 yield* applyMigrations({
109 resolved: { dir: fixturesDir, table: "__alchemy_migrations" },
110 executor,
111 });
112 expect(rowsOf(db, "__alchemy_migrations")).toEqual(first);
113 // No drizzle table was ever created on a greenfield database.
114 expect(tableNames(db)).not.toContain("__drizzle_migrations");
115 }),
116 { tags: ["unit", "local"] },
117 );
118});