t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/SqlMigrations.unit.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

654 lines
23 KiB
TypeScript

import { purePlugin } from "@/Bundle/PurePlugin.ts";
import type { DurableObjectState } from "@/Cloudflare/Workers/DurableObjectState.ts";
import {
SqlMigrations,
type SqlMigrationsInput,
} from "@/Cloudflare/Workers/SqlMigrations.ts";
import {
SqlMigrationsRuntime,
type SqlMigrationSnapshot,
} from "@/Cloudflare/Workers/SqlMigrationsRuntime.ts";
import { makeEffectVirtualEntry } from "@/Cloudflare/Workers/Sources/Rolldown.ts";
import { Worker } from "@/Cloudflare/Workers/Worker.ts";
import {
makeWorkerRuntimeContext,
type WorkerExport,
type WorkerRuntimeContext,
} from "@/Cloudflare/Workers/WorkerRuntimeContext.ts";
import type { RuntimeContext } from "@/RuntimeContext.ts";
import {
DrizzleV0LayoutError,
MigrationError,
type MigrationHistoryConflictError,
} from "@/SQL/Migrations/Format.ts";
import { readMigrationRecords } from "@/SQL/Migrations/Records.ts";
import { sha256 } from "@/Util/sha256.ts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, layer } from "alchemy-test";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Stream from "effect/Stream";
import * as ChildProcess from "effect/process/ChildProcess";
import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
import { nodePath, nodeSupportsDevMode } from "../../nodeProbe.ts";
type Assert<T extends true> = T;
type Application = ReturnType<
Effect.Success<ReturnType<typeof SqlMigrations>>["apply"]
>;
type _RequiresRuntimeContext = Assert<
RuntimeContext extends Effect.Services<Application> ? true : false
>;
type _RequiresDurableObjectState = Assert<
DurableObjectState extends Effect.Services<Application> ? true : false
>;
type _NotConstructionOnly = Assert<
Application extends Effect.Effect<
void,
MigrationError | MigrationHistoryConflictError,
Effect.Services<ReturnType<typeof SqlMigrations>>
>
? false
: true
>;
type _RuntimeRequiredEvenWithState = Assert<
Application extends Effect.Effect<
void,
MigrationError | MigrationHistoryConflictError,
DurableObjectState
>
? false
: true
>;
type _SnapshotHasNoMethod = Assert<
"apply" extends keyof SqlMigrationSnapshot ? false : true
>;
// Worker resolves its host through the resource's Self key.
const WorkerHost = Context.Service<Worker, WorkerRuntimeContext>(
Worker.Self.key,
);
const writeMigrations = Effect.fn(function* (files: Record<string, string>) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({
prefix: "alchemy-sql-migrations-unit-",
});
for (const [name, contents] of Object.entries(files)) {
const file = path.join(dir, name);
yield* fs.makeDirectory(path.dirname(file), { recursive: true });
yield* fs.writeFileString(file, contents);
}
return dir;
});
const capture = Effect.fn(function* (input: SqlMigrationsInput) {
const worker = makeWorkerRuntimeContext("sql-migrations-unit");
const snapshot = yield* SqlMigrations(input).pipe(
Effect.provideService(WorkerHost, worker),
);
const { default: _default, ...exports } = yield* worker.exports;
const migrationExports: Record<string, WorkerExport> = exports;
return { worker, snapshot, exports: migrationExports };
});
const generatedEntry = (exports: Record<string, WorkerExport>) =>
makeEffectVirtualEntry(exports, {
name: "sql-migrations-unit",
stage: "test",
})("./worker.ts");
layer(NodeServices.layer)("Cloudflare.SqlMigrations construction", (it) => {
it.effect(
"captures ordered flat files and their raw-content hashes",
() =>
Effect.gen(function* () {
const first = "CREATE TABLE users (id INTEGER PRIMARY KEY);\n";
const second = "CREATE TABLE posts (id INTEGER PRIMARY KEY);\n";
const dir = yield* writeMigrations({
"10_posts.sql": second,
"2_users.sql": first,
"ignored.txt": "not a migration",
});
const { worker, snapshot, exports } = yield* capture(dir);
expect(snapshot._tag).toBe("Cloudflare.SqlMigrations");
expect(snapshot.table).toBe("__alchemy_migrations");
expect(snapshot.records).toEqual(yield* readMigrationRecords(dir));
expect(snapshot.records.map((record) => record.name)).toEqual([
"2_users.sql",
"10_posts.sql",
]);
expect(snapshot.records.map((record) => record.hash)).toEqual([
yield* sha256(first),
yield* sha256(second),
]);
expect(snapshot.records.map((record) => record.sql)).toEqual([
first,
second,
]);
expect(
snapshot.records.map((record) => record.createdAtMillis),
).toEqual([undefined, undefined]);
expect(typeof snapshot.apply).toBe("function");
expect(Effect.isEffect(snapshot.apply())).toBe(true);
expect(Object.values(exports)).toEqual([
{
kind: "sqlMigrations",
snapshot: {
_tag: snapshot._tag,
table: snapshot.table,
records: snapshot.records,
},
},
]);
for (const exported of Object.values(exports)) {
if (exported.kind === "sqlMigrations") {
expect(exported.snapshot).not.toHaveProperty("apply");
expect(exported.snapshot).not.toBe(snapshot);
}
}
expect(worker.env).toEqual({});
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"captures modern directories with timestamps and a custom table",
() =>
Effect.gen(function* () {
const first =
"CREATE TABLE users (id INTEGER PRIMARY KEY);\n--> statement-breakpoint\nINSERT INTO users VALUES (1);\n";
const second = "CREATE TABLE posts (id INTEGER PRIMARY KEY);\n";
const dir = yield* writeMigrations({
"20240102000000_posts/migration.sql": second,
"20240101000000_users/migration.sql": first,
"20240101000000_users/snapshot.json": "{}",
});
const { snapshot, exports, worker } = yield* capture({
dir,
table: "application_history",
});
expect(snapshot.table).toBe("application_history");
expect(snapshot.records).toEqual(yield* readMigrationRecords(dir));
expect(snapshot.records.map((record) => record.name)).toEqual([
"20240101000000_users",
"20240102000000_posts",
]);
expect(
snapshot.records.map((record) => record.createdAtMillis),
).toEqual([1704067200000, 1704153600000]);
expect(snapshot.records.map((record) => record.hash)).toEqual([
yield* sha256(first),
yield* sha256(second),
]);
expect(snapshot.records[0]!.statements).toEqual([
"CREATE TABLE users (id INTEGER PRIMARY KEY);",
"INSERT INTO users VALUES (1);",
]);
expect(Object.values(exports)).toEqual([
{
kind: "sqlMigrations",
snapshot: {
_tag: snapshot._tag,
table: snapshot.table,
records: snapshot.records,
},
},
]);
expect(worker.env).toEqual({});
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"reconstructs apply from the plain runtime snapshot without reading the directory",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* writeMigrations({
"migrations/0001_users.sql":
"CREATE TABLE users (id INTEGER PRIMARY KEY);",
});
const dir = path.join(root, "migrations");
const input = { dir, table: "runtime_history" };
const captured = yield* capture(input);
const bundles: Record<string, SqlMigrationSnapshot> = {};
for (const [key, exported] of Object.entries(captured.exports)) {
if (exported.kind === "sqlMigrations") {
expect(exported.snapshot).not.toHaveProperty("apply");
bundles[key] = exported.snapshot;
}
}
expect(Object.keys(bundles)).toHaveLength(1);
yield* fs.remove(dir, { recursive: true });
const reconstructed = yield* Effect.acquireUseRelease(
Effect.sync(() => {
const previous = globalThis.__ALCHEMY_RUNTIME__;
globalThis.__ALCHEMY_RUNTIME__ = true;
return previous;
}),
() =>
SqlMigrations(input).pipe(
Effect.provideService(SqlMigrationsRuntime, bundles),
Effect.provideService(WorkerHost, captured.worker),
),
(previous) =>
Effect.sync(() => {
globalThis.__ALCHEMY_RUNTIME__ = previous;
}),
);
expect(reconstructed._tag).toBe(captured.snapshot._tag);
expect(reconstructed.table).toBe(captured.snapshot.table);
expect(reconstructed.records).toEqual(captured.snapshot.records);
expect(typeof reconstructed.apply).toBe("function");
expect(Effect.isEffect(reconstructed.apply())).toBe(true);
expect(reconstructed.apply).not.toBe(captured.snapshot.apply);
for (const snapshot of Object.values(bundles)) {
expect(snapshot).not.toHaveProperty("apply");
}
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
exclusive: true,
},
);
it.effect(
"fails construction for a missing directory before registering exports",
() =>
Effect.gen(function* () {
const path = yield* Path.Path;
const root = yield* writeMigrations({});
const dir = path.join(root, "missing");
const worker = makeWorkerRuntimeContext("missing-sql-migrations");
const exit = yield* SqlMigrations(dir).pipe(
Effect.provideService(WorkerHost, worker),
Effect.exit,
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const error = Cause.squash(exit.cause);
expect(error).toBeInstanceOf(MigrationError);
expect(error).toMatchObject({
message: expect.stringContaining(dir),
});
}
expect(Object.keys(yield* worker.exports)).toEqual(["default"]);
expect(worker.env).toEqual({});
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"rejects legacy journals even when modern migration files exist",
() =>
Effect.gen(function* () {
const dir = yield* writeMigrations({
"meta/_journal.json": '{"version":"7","entries":[]}',
"20240101000000_users/migration.sql":
"CREATE TABLE users (id INTEGER);",
});
const worker = makeWorkerRuntimeContext("legacy-sql-migrations");
const exit = yield* SqlMigrations(dir).pipe(
Effect.provideService(WorkerHost, worker),
Effect.exit,
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const error = Cause.squash(exit.cause);
expect(error).toBeInstanceOf(DrizzleV0LayoutError);
expect(error).toMatchObject({
dir,
message: expect.stringContaining("drizzle-kit up"),
});
}
expect(Object.keys(yield* worker.exports)).toEqual(["default"]);
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"embeds SQL larger than 5 KB without file imports or environment bindings",
() =>
Effect.gen(function* () {
const sql = `CREATE TABLE messages (body TEXT);\n--> statement-breakpoint\nINSERT INTO messages VALUES ('${"large-payload-".repeat(600)}');\n`;
expect(sql.length).toBeGreaterThan(5 * 1024);
const dir = yield* writeMigrations({ "0001_messages.sql": sql });
const { snapshot, exports, worker } = yield* capture({
dir,
table: "large_history",
});
const entry = generatedEntry(exports);
const snapshots = Object.fromEntries(
Object.entries(exports).map(([key]) => [key, snapshot]),
);
expect(snapshot.records[0]!.sql).toBe(sql);
expect(entry).toContain(
`withSqlMigrations(entrypoint, ${JSON.stringify(snapshots)})`,
);
expect(entry).toContain(JSON.stringify(sql));
expect(entry).toContain(snapshot.records[0]!.hash);
expect(entry).toContain('"table":"large_history"');
expect(entry).not.toMatch(
/\b(?:from\s*|import\s*(?:\(\s*)?)(["'])[^"']*\.sql(?:\?[^"']*)?\1/,
);
expect(entry).not.toContain("migrations.js");
expect(entry).not.toMatch(/\benv\s*(?:\.|\[)/);
expect(entry).not.toContain("process.env");
expect(entry).not.toContain("Records.ts");
expect(worker.env).toEqual({});
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"changes the generated entry when SQL changes under the same capture key",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const firstSql = "CREATE TABLE first_version (id INTEGER);\n";
const secondSql = "CREATE TABLE second_version (id INTEGER);\n";
const dir = yield* writeMigrations({ "0001_schema.sql": firstSql });
const input = { dir, table: "stable_history" };
const first = yield* capture(input);
const firstEntry = generatedEntry(first.exports);
yield* fs.writeFileString(path.join(dir, "0001_schema.sql"), secondSql);
const second = yield* capture(input);
const secondEntry = generatedEntry(second.exports);
expect(Object.keys(second.exports)).toEqual(Object.keys(first.exports));
expect(second.snapshot.records[0]!.name).toBe(
first.snapshot.records[0]!.name,
);
expect(second.snapshot.records[0]!.hash).toBe(yield* sha256(secondSql));
expect(second.snapshot.records[0]!.hash).not.toBe(
first.snapshot.records[0]!.hash,
);
expect(secondEntry).not.toBe(firstEntry);
expect(firstEntry).toContain(JSON.stringify(firstSql));
expect(secondEntry).toContain(JSON.stringify(secondSql));
expect(secondEntry).not.toContain(JSON.stringify(firstSql));
expect(first.snapshot.records[0]!.sql).toBe(firstSql);
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"keeps deploy-time readers out of the emitted Durable Object migrator graph",
() =>
Effect.gen(function* () {
const path = yield* Path.Path;
const input = yield* path.fromFileUrl(
new URL(
"../../../src/Cloudflare/Workers/SqlMigrationsApply.ts",
import.meta.url,
),
);
const { rolldown } = yield* Effect.promise(() => import("rolldown"));
const graph = new Set<string>();
const output = yield* Effect.acquireUseRelease(
Effect.promise(() =>
rolldown({
input,
platform: "neutral",
external: [/^effect\//, /^node:/],
plugins: [
{
name: "sql-migrations-runtime-boundary",
moduleParsed(info) {
graph.add(info.id);
for (const id of [
...info.importedIds,
...info.dynamicallyImportedIds,
]) {
graph.add(id);
}
},
},
],
}),
),
(bundle) => Effect.promise(() => bundle.generate({ format: "esm" })),
(bundle) => Effect.promise(() => bundle.close()),
);
const chunks = output.output.filter((item) => item.type === "chunk");
const modules = [
...graph,
...chunks.flatMap((chunk) => Object.keys(chunk.modules)),
];
const code = chunks.map((chunk) => chunk.code).join("\n");
const imports = chunks.flatMap((chunk) => [
...chunk.imports,
...chunk.dynamicImports,
]);
expect(chunks.flatMap((chunk) => chunk.exports)).toContain(
"applySqlMigrations",
);
expect(
modules.some((id) => id.endsWith("/SQL/Migrations/AlchemyFormat.ts")),
).toBe(true);
expect(
modules.some((id) => id.endsWith("/SQL/Migrations/Convert.ts")),
).toBe(true);
expect(
modules.some((id) => id.endsWith("/SQL/Migrations/Utils.ts")),
).toBe(true);
const forbidden =
/(?:SQL\/Migrations\/(?:Records|Registry|Detect)\.ts|SQL\/SqlFile\.ts|node:(?:crypto|fs)|effect\/(?:FileSystem|Path))/;
expect(modules.filter((id) => forbidden.test(id))).toEqual([]);
expect(imports.filter((id) => forbidden.test(id))).toEqual([]);
expect(code).not.toContain("node:crypto");
expect(code).not.toContain("readMigrationRecords");
}),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect(
"emits the SqlMigrations factory and apply method without filesystem imports",
() =>
Effect.gen(function* () {
const path = yield* Path.Path;
const input = yield* path.fromFileUrl(
new URL(
"../../../src/Cloudflare/Workers/SqlMigrations.ts",
import.meta.url,
),
);
const { rolldown } = yield* Effect.promise(() => import("rolldown"));
const output = yield* Effect.acquireUseRelease(
Effect.promise(() =>
rolldown({
input,
platform: "neutral",
external: (id) => !id.startsWith(".") && !path.isAbsolute(id),
transform: {
define: { "globalThis.__ALCHEMY_RUNTIME__": "true" },
},
plugins: [purePlugin()],
}),
),
(bundle) =>
Effect.promise(() =>
bundle.generate({ format: "esm", minify: "dce-only" }),
),
(bundle) => Effect.promise(() => bundle.close()),
);
const chunks = output.output.filter((item) => item.type === "chunk");
const modules = chunks.flatMap((chunk) =>
Object.entries(chunk.modules)
.filter(([, module]) => module.renderedLength > 0)
.map(([id]) => id),
);
const imports = chunks.flatMap((chunk) => [
...chunk.imports,
...chunk.dynamicImports,
]);
const code = chunks.map((chunk) => chunk.code).join("\n");
expect(chunks.flatMap((chunk) => chunk.exports)).toContain(
"SqlMigrations",
);
expect(
modules.some((id) => id.endsWith("/Workers/SqlMigrations.ts")),
).toBe(true);
expect(
modules.some((id) => id.endsWith("/Workers/SqlMigrationsApply.ts")),
).toBe(true);
const forbidden =
/(?:SQL\/Migrations\/(?:Records|Registry|Detect)\.ts|SQL\/SqlFile\.ts|node:(?:crypto|fs)|effect\/(?:FileSystem|Path)|@effect\/platform-(?:node|bun))/;
expect(modules.filter((id) => forbidden.test(id))).toEqual([]);
expect(imports.filter((id) => forbidden.test(id))).toEqual([]);
expect(code).not.toContain("readMigrationRecords");
expect(code).not.toContain("node:fs");
expect(code).not.toContain("node:crypto");
}),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
},
);
it.effect.skipIf(!nodeSupportsDevMode)(
"imports a SqlMigrations Durable Object through Node's Oxc loader and captures SQL",
() =>
Effect.gen(function* () {
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner;
const sql =
"CREATE TABLE node_loader_probe (id INTEGER PRIMARY KEY);\n";
const root = yield* writeMigrations({
"migrations/0001_probe.sql": sql,
});
const loader = yield* path.fromFileUrl(
new URL("../../../bin/register-dev-mode.js", import.meta.url),
);
const fixture = yield* path.fromFileUrl(
new URL(
"./fixtures/sql-migrations-unit/node-import.ts",
import.meta.url,
),
);
const handle = yield* spawner.spawn(
ChildProcess.make(nodePath!, ["--import", loader, fixture], {
cwd: root,
env: { NO_COLOR: "1", npm_execpath: "", npm_config_user_agent: "" },
extendEnv: true,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
killSignal: "SIGKILL",
}),
);
const [stdout, stderr, exitCode] = yield* Effect.all(
[
handle.stdout.pipe(
Stream.decodeText,
Stream.runCollect,
Effect.map((chunks) => chunks.join("")),
),
handle.stderr.pipe(
Stream.decodeText,
Stream.runCollect,
Effect.map((chunks) => chunks.join("")),
),
handle.exitCode,
],
{ concurrency: 3 },
);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
expect(stdout).toContain('"runtime":"node"');
expect(stdout).toContain('"object":"SqlMigrationsUnitObject"');
expect(stdout).toContain('"table":"node_loader_history"');
expect(stdout).toContain(JSON.stringify(sql));
expect(stdout).toContain(yield* sha256(sql));
expect(stdout).toContain('"capturedHasApply":true');
expect(stdout).toContain('"exportedHasApply":false');
expect(stdout).toContain('"exports":1');
expect(stdout).toContain('"env":{}');
}).pipe(Effect.scoped),
{
tags: [
"unit",
"provider:cloudflare",
"provider:cloudflare:worker",
"local",
],
timeout: 60_000,
},
);
});