1// `@effect/sql-pg` (and pg underneath) are optional peers — value imports
2// are deferred to first use so `alchemy/Drizzle` resolves without them
3// installed.
4import type * as PgClient from "@effect/sql-pg/PgClient";
5import type { AnyRelations, EmptyRelations } from "drizzle-orm";
6import type { EffectPgDatabase } from "drizzle-orm/effect-postgres";
7import type { EffectDrizzlePgConfig } from "drizzle-orm/pg-core/effect/utils";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import type * as Redacted from "effect/Redacted";
11import { makeExecutionMemo } from "../Runtime/ExecutionMemo.ts";
12import { resolveConnectionOptions } from "../SQL/PostgresTls.ts";
13import { proxyChain } from "../Util/proxy-chain.ts";
14
15/**
16 * Open a Drizzle/Postgres database from a connection URL using the
17 * `drizzle-orm/effect-postgres` integration.
18 *
19 * Returns a chainable Proxy over `EffectPgDatabase` (via `proxyChain`) —
20 * every property read records a step, every call records args, and the
21 * chain is replayed against the resolved drizzle db when it's finally
22 * yielded as an Effect. Callers don't need a separate `yield* conn` step:
23 *
24 * ```typescript
25 * const db = yield* Drizzle.Postgres(hd.connectionString);
26 *
27 * fetch: Effect.gen(function* () {
28 * const rows = yield* db.select().from(users);
29 * });
30 * ```
31 *
32 * The connect work is deferred until the first query and memoized on the
33 * current execution's `Scope` (via {@link makeExecutionMemo}), so the pool
34 * is built at most once per execution — a Worker `fetch`/`queue`/`scheduled`
35 * event, a Durable Object call, a Workflow run, or a Lambda invocation — and
36 * reused across every query and `task` step in that execution. Yielding the
37 * connection string is likewise deferred, so deploy / plan-time invocations
38 * (where `WorkerEnvironment` isn't provided) never trigger a real connection
39 * attempt.
40 *
41 * The pool is built against that same execution scope, so its `end`
42 * finalizer fires when the scope closes — when the request / run settles,
43 * not when the Worker's isolate-lifetime init completes. Wrapping queries in
44 * a nested `Effect.scoped` narrows both the memo and the pool's lifetime to
45 * that block: memo key and finalizer target are always the same scope
46 * object, so they cannot disagree.
47 *
48 * @binding
49 */
50
51export const Postgres = <
52 TRelations extends AnyRelations = EmptyRelations,
53 E = never,
54 R = never,
55>(
56 connectionString: Effect.Effect<Redacted.Redacted<string>, E, R>,
57 config?: EffectDrizzlePgConfig<TRelations>,
58) =>
59 Effect.map(
60 makeExecutionMemo(
61 Effect.gen(function* () {
62 const [PgClient, PgDrizzle] = yield* Effect.promise(() =>
63 Promise.all([
64 import("@effect/sql-pg/PgClient"),
65 import("drizzle-orm/effect-postgres"),
66 ]),
67 );
68 const url = yield* connectionString;
69 const pgCtx = yield* Layer.build(
70 PgClient.layer(resolveConnectionOptions(url)),
71 );
72 return yield* PgDrizzle.makeWithDefaults(config).pipe(
73 Effect.provideContext(pgCtx),
74 );
75 }),
76 ),
77 (db) =>
78 proxyChain<
79 EffectPgDatabase<TRelations> & {
80 $client: PgClient.PgClient;
81 }
82 >(
83 db as Effect.Effect<
84 EffectPgDatabase<TRelations> & {
85 $client: PgClient.PgClient;
86 }
87 >,
88 ),
89 );