1import * as D1Client from "@effect/sql-d1/D1Client";
2import type { AnyRelations, EmptyRelations } from "drizzle-orm";
3import type { EffectSQLiteD1Database } from "drizzle-orm/effect-d1";
4import * as SQLiteD1Drizzle from "drizzle-orm/effect-d1";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import type { D1DatabaseSource } from "../SQL/D1.ts";
8import { makeExecutionMemo } from "../Runtime/ExecutionMemo.ts";
9import { proxyChain } from "../Util/proxy-chain.ts";
10
11/**
12 * Open a Drizzle database over a Cloudflare D1 binding using the
13 * `drizzle-orm/effect-d1` integration (which drives queries through
14 * `@effect/sql-d1`'s `D1Client`).
15 *
16 * Accepts the client returned by `Cloudflare.D1.QueryDatabase(db)` — or its
17 * `raw` effect directly — and returns a chainable Proxy over
18 * `EffectSQLiteD1Database` (via `proxyChain`): every property read records a
19 * step, every call records args, and the chain is replayed against the
20 * resolved drizzle db when it's finally yielded as an Effect. Callers don't
21 * need a separate `yield* conn` step:
22 *
23 * ```typescript
24 * const d1 = yield* Cloudflare.D1.QueryDatabase(Db);
25 * const db = yield* Drizzle.D1(d1, { relations });
26 *
27 * fetch: Effect.gen(function* () {
28 * const rows = yield* db.select().from(users);
29 * });
30 * ```
31 *
32 * The client build is deferred until the first query and memoized on the
33 * current execution's `Scope` (via {@link makeExecutionMemo}), so the
34 * `D1Client` (and its prepared-statement cache) is built at most once per
35 * execution — a Worker `fetch`/`queue`/`scheduled` event, a Durable Object
36 * call, or a Workflow run — and reused across every query in that execution.
37 * Resolving the binding is likewise deferred, so deploy / plan-time
38 * invocations (where `WorkerEnvironment` isn't provided) never touch D1.
39 *
40 * The client is built against that same execution scope, so its finalizer
41 * fires when the scope closes — when the request / run settles, not when the
42 * Worker's isolate-lifetime init completes. Wrapping queries in a nested
43 * `Effect.scoped` narrows both the memo and the client's lifetime to that
44 * block: memo key and finalizer target are always the same scope object, so
45 * they cannot disagree.
46 *
47 * @binding
48 */
49export const D1 = <
50 TRelations extends AnyRelations = EmptyRelations,
51 E = never,
52 R = never,
53>(
54 database: D1DatabaseSource<E, R>,
55 config?: SQLiteD1Drizzle.EffectDrizzleSQLiteD1Config<TRelations>,
56) =>
57 Effect.map(
58 makeExecutionMemo(
59 Effect.gen(function* () {
60 const db = yield* Effect.isEffect(database) ? database : database.raw;
61 const d1Ctx = yield* Layer.build(D1Client.layer({ db }));
62 return yield* SQLiteD1Drizzle.makeWithDefaults(
63 config ??
64 ({} as SQLiteD1Drizzle.EffectDrizzleSQLiteD1Config<TRelations>),
65 ).pipe(Effect.provideContext(d1Ctx));
66 }),
67 ),
68 (db) =>
69 proxyChain<
70 EffectSQLiteD1Database<TRelations> & {
71 $client: D1Client.D1Client;
72 }
73 >(
74 db as Effect.Effect<
75 EffectSQLiteD1Database<TRelations> & {
76 $client: D1Client.D1Client;
77 }
78 >,
79 ),
80 );