Skip to content

docs(tutorial): use Effect-native SQL and HTTP primitives#400

Open
sam-goodwin wants to merge 7 commits into
mainfrom
claude/determined-chandrasekhar-f3c692
Open

docs(tutorial): use Effect-native SQL and HTTP primitives#400
sam-goodwin wants to merge 7 commits into
mainfrom
claude/determined-chandrasekhar-f3c692

Conversation

@sam-goodwin

Copy link
Copy Markdown
Contributor

Two tutorial fixes:

tutorial/cloudflare/neon-hyperdrive

Swap the raw pg / mysql2 snippets (wrapped in Effect.promise(async () => …)) for the Effect-native SQL clients.

+import * as Reactivity from "@effect/experimental/Reactivity";
+import * as PgClient from "@effect/sql-pg/PgClient";

   Effect.gen(function* () {
     const hd = yield* Cloudflare.Hyperdrive.bind(Hyperdrive);
+    const sql = yield* PgClient.make({ url: yield* hd.connectionString });

     return {
       fetch: Effect.gen(function* () {
-        const connectionString = yield* hd.connectionString;
-        const rows = yield* Effect.promise(async () => {
-          const client = new Client({ connectionString });
-          await client.connect();
-          try { return (await client.query("SELECT now() as now")).rows; }
-          finally { await client.end().catch(() => {}); }
-        });
+        const rows = yield* sql`SELECT now() as now`;
         return yield* HttpServerResponse.json({ ok: true, rows });
       }),
     };
-  }).pipe(Effect.provide(Cloudflare.HyperdriveConnectionLive)),
+  }).pipe(
+    Effect.provide(Reactivity.layer),
+    Effect.provide(Cloudflare.HyperdriveConnectionLive),
+  ),

Install lines change from pg + @types/pg / mysql2 to @effect/sql-pg / @effect/sql-mysql2 (the underlying driver is bundled). MySQL tab gets the equivalent MysqlClient.make treatment.

tutorial/cloudflare/queue-consumer

  • Replace return new Response("ok") with HttpServerResponse.text("ok") (added the missing import).
  • Reshape the subscribe handler so Stream.runForEach is the terminal step on stream.pipe(...) rather than wrapping the per-message effect, and drop the redundant Effect.asVoid:
   yield* Cloudflare.messages<QueueMessageBody>(queueResource).subscribe(
     (stream) =>
-      Stream.runForEach(stream, (msg) =>
-        bucket
-          .put(`/queue/${msg.body.id}`, JSON.stringify(msg.body), {
-            httpMetadata: { contentType: "application/json" },
-          })
-          .pipe(Effect.asVoid),
-      ),
+      stream.pipe(
+        Stream.runForEach((msg) =>
+          bucket.put(`/queue/${msg.body.id}`, JSON.stringify(msg.body), {
+            httpMetadata: { contentType: "application/json" },
+          }),
+        ),
+      ),
   );

- neon-hyperdrive: replace raw pg/mysql2 + Effect.promise with @effect/sql-pg / @effect/sql-mysql2 PgClient.make / MysqlClient.make
- queue-consumer: replace `new Response` with HttpServerResponse.text, and put Stream.runForEach at the end of stream.pipe instead of wrapping the per-message effect

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alchemy-version-bot

alchemy-version-bot Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/ac03071

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/ac03071

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/ac03071

@alchemy-version-bot

alchemy-version-bot Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Website Preview Deployed

URL: https://alchemyeffectwebsite-worker-pr-400-nfqvmp3ylra5rt7a.testing-2b2.workers.dev

Built from commit 669a656.


This comment updates automatically with each push.

+
return {
fetch: Effect.gen(function* () {
+ const connectionString = yield* hd.connectionString;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't yield* hd.connectionString here. PgClient needs to be lazily evaluated (maybe provided as a Layer to the fetch effect)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 25145a3. Replaced PgClient.make({ url: yield* hd.connectionString }) at init with a Layer.unwrapEffect(hd.connectionString.pipe(Effect.map(url => PgClient.layer({ url })))) provided to the fetch effect — connectionString is now read lazily under the request's RuntimeContext.

},
Effect.gen(function* () {
+ const hd = yield* Cloudflare.Hyperdrive.bind(Hyperdrive);
+ const sql = yield* PgClient.make({ url: yield* hd.connectionString });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work. Perhaps we need PgClient.layer and Layer.unwrap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — now using PgClient.layer / MysqlClient.layer wrapped in Layer.unwrapEffect, provided to the fetch boundary. See 25145a3.

Comment on lines +524 to +535
Three things to notice (regardless of engine):

- The `nodejs_compat` flag is required because the underlying
driver (`pg` / `mysql2`) uses Node's `net` and `crypto` APIs.
- `PgClient.make` / `MysqlClient.make` returns a scoped Effect that
builds the client once per Worker isolate. Subsequent requests
reuse it — no per-request connect/disconnect — while Hyperdrive
handles the pooling on the Cloudflare side of the wire.
- Queries are written as tagged templates (`` sql`...` ``) that are
`Effect`s. You `yield*` them directly inside `fetch` — no
`Effect.promise(async () => ...)` wrappers, and errors flow into
the typed error channel as `SqlError`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use bullet points like this to point of "things to notice". Instead, break the code into diff/delta snippets and explain step by step, drawing the user's attention to the appropriate code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restructured in 25145a3: dropped the trailing bullet list and split the section into three delta diff snippets (bind at init → derive SqlLive via Layer.unwrapEffect → query in fetch + provide), each with one prose paragraph drawing attention to the new code.

sam-goodwin and others added 6 commits May 21, 2026 00:13
PR feedback:
- yield* hd.connectionString at init doesn't work — connectionString requires RuntimeContext, only available at request time
- Replace PgClient.make / MysqlClient.make at init with Layer.unwrapEffect over PgClient.layer / MysqlClient.layer, provided at the fetch boundary
- Break the "Bind Hyperdrive to your Worker" mega-snippet into delta diff steps with prose per step, drop the trailing "things to notice" bullet list

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the tabbed Neon/PlanetScale Postgres/PlanetScale MySQL guide with three provider-specific tutorials sharing the same structure:

- neon-hyperdrive (Neon Postgres, order 7)
- planetscale-postgres-hyperdrive (new, order 8)
- planetscale-mysql-hyperdrive (new, order 9)

Bumped drizzle (10), branch-from-shared-database (11), and queue-consumer (12) to keep the chapter sequence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nodejs_compat is on by default for Effect-native Cloudflare Workers, so the explicit flag (and its accompanying comments/bullets) is just noise. Removed from the three hyperdrive tutorials, drizzle, and artifacts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per the tutorial standard, each new concept gets its own heading, diff snippet, and prose paragraph instead of dumping the full Db.ts at once.

- neon-hyperdrive: split into "Create the project" + "Fork a branch"
- planetscale-postgres-hyperdrive: "Create the cluster" + "Fork a branch" + "Materialize a role for Hyperdrive"
- planetscale-mysql-hyperdrive: "Create the cluster" + "Fork a branch" + "Materialize a password for Hyperdrive"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…drive

Adds Worker + Hyperdrive + SQL-client fixtures so the three tutorial
guides are actually exercised against a real cluster:

- Planetscale Postgres: restructured fixtures into `hyperdrive-drizzle/`
  and `hyperdrive-effect-sql/`. Existing Hyperdrive.test.ts → Hyperdrive.drizzle.test.ts.
  New Hyperdrive.effect-sql.test.ts uses `@effect/sql-pg` PgClient.layer + Layer.unwrapEffect.
- Planetscale MySQL: new Stack + Hyperdrive.effect-sql.test.ts using `@effect/sql-mysql2`.
  Skipped the Drizzle variant — drizzle-orm has no `effect-mysql2` adapter.
- Neon: new Stack + Hyperdrive.drizzle.test.ts (Drizzle.postgres) and
  Hyperdrive.effect-sql.test.ts (`@effect/sql-pg`).

Each effect-sql worker uses a separate `*_sql` table so it can share the
same Hyperdrive cluster as the drizzle variant without colliding.

Drive-by fixes:
- `SqlClient` import path corrected to `effect/unstable/sql/SqlClient`
  (it lives under effect's unstable tree in v4, not `@effect/sql`).
  Updated in the three tutorial docs to match.
- Tutorial docs now reference `Cloudflare.HyperdriveBindingLive`
  (the actual export name) instead of `HyperdriveConnectionLive`.
- Added `@effect/sql-mysql2` to the catalog + alchemy optional peer deps.

Tests are gated behind `NEON_TEST=1` / `PLANETSCALE_TEST=1`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant