1import * as Cloudflare from "@/Cloudflare";
2import * as Planetscale from "@/Planetscale";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { exerciseSqlSurface, postJson } from "./exercise.ts";
8import type { UserRow } from "./fixtures/routes.ts";
9import Stack from "./fixtures/mysql-stack.ts";
10
11const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
12 providers: Layer.merge(Cloudflare.providers(), Planetscale.providers()),
13 state: Cloudflare.state(),
14});
15
16// PlanetScale database provisioning dominates the deploy.
17const HOOK_TIMEOUT = 900_000;
18const TEST_TIMEOUT = 120_000;
19
20/**
21 * End-to-end test of `SQL.MySQL` — the raw `@effect/sql-mysql2` client
22 * wrapped in alchemy's deferred `proxyChain` — against a real Cloudflare
23 * Worker fronting a PlanetScale MySQL origin through Hyperdrive (caching
24 * disabled for read-after-write assertions).
25 *
26 * Runs the same shared exercise as the D1/Postgres suites, plus the
27 * transaction surface: `withTransaction` (commit + rollback). Also pins the
28 * Workers-specific client defaults — text protocol (Hyperdrive's MySQL
29 * proxy has no `COM_STMT_PREPARE`) and eval-free row parsing (workerd
30 * forbids runtime code generation) — since every route would fail without
31 * them.
32 */
33describe.skipIf(!process.env.PLANETSCALE_TEST)(
34 "SQL.MySQL",
35 {
36 tags: [
37 "provider:cloudflare",
38 "provider:cloudflare:hyperdrive",
39 "provider:cloudflare:worker",
40 "provider:planetscale",
41 "provider:planetscale:mysql",
42 "live",
43 ],
44 },
45 () => {
46 const stack = beforeAll(deploy(Stack), { timeout: HOOK_TIMEOUT });
47 afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), {
48 timeout: HOOK_TIMEOUT,
49 });
50
51 test(
52 "SQL.MySQL exercises the full client surface over a deployed Worker",
53 Effect.gen(function* () {
54 const { url } = yield* stack;
55 yield* exerciseSqlSurface(url);
56
57 // withTransaction — both inserts commit atomically.
58 const dave: UserRow = {
59 id: 10,
60 name: "dave",
61 email: "dave@example.com",
62 };
63 const erin: UserRow = {
64 id: 11,
65 name: "erin",
66 email: "erin@example.com",
67 };
68 const committed = (yield* postJson(`${url}/tx/commit`, [
69 dave,
70 erin,
71 ])) as {
72 rows: UserRow[];
73 };
74 expect(committed.rows).toEqual([dave, erin]);
75
76 // withTransaction — a failing effect rolls the insert back.
77 const rolledBack = (yield* postJson(`${url}/tx/rollback`, {
78 id: 12,
79 name: "frank",
80 email: "frank@example.com",
81 })) as { error: string; rows: unknown[] };
82 expect(rolledBack.error).toBe("Rollback");
83 expect(rolledBack.rows).toEqual([]);
84 }),
85 { timeout: TEST_TIMEOUT },
86 );
87 },
88);