Open History to see the last commit
1import { resolvePoolConfig } from "@alchemy.run/sql/PostgresTls";
2// `@effect/sql-pg` (and pg underneath) are optional peers — value imports
3// are deferred to first use so `@alchemy.run/drizzle` resolves without them
4// installed.
5import type * as PgClient from "@effect/sql-pg/PgClient";
6import { makeExecutionMemo } from "alchemy/Runtime/ExecutionMemo";
7import { proxyChain } from "alchemy/Util/proxy-chain";
8import type { AnyRelations, EmptyRelations } from "drizzle-orm";
9import type { EffectPgDatabase } from "drizzle-orm/effect-postgres";
10import type { EffectDrizzlePgConfig } from "drizzle-orm/pg-core/effect/utils";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
13import type * as Redacted from "effect/Redacted";
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 * `@effect/sql-pg` client options are passed via `config.client`. Behind a
49 * transaction-mode pooler such as Hyperdrive, turn off named prepared
50 * statements, since a later query can reach a backend session that never
51 * prepared them:
52 *
53 * ```typescript
54 * const db = yield* Drizzle.Postgres(hd.connectionString, {
55 * client: { prepare: false },
56 * });
57 * ```
58 *
59 * @binding
60 */
61
62export const Postgres = <TRelations extends AnyRelations = EmptyRelations, E = never, R = never>(
63 connectionString: Effect.Effect<Redacted.Redacted<string>, E, R>,
64 config?: EffectDrizzlePgConfig<TRelations> & {
65 /**
66 * Overrides for the underlying `@effect/sql-pg` client — `prepare`,
67 * `ssl`, pool limits, and friends. The URL comes from `connectionString`.
68 */
69 readonly client?: Omit<PgClient.PgPoolConfig, "url">;
70 },
71) =>
72 Effect.map(
73 makeExecutionMemo(
74 Effect.gen(function* () {
75 const [PgClient, PgDrizzle] = yield* Effect.promise(() =>
76 Promise.all([import("@effect/sql-pg/PgClient"), import("drizzle-orm/effect-postgres")]),
77 );
78 const url = yield* connectionString;
79 const { client, ...drizzleConfig } = config ?? {};
80 const pgCtx = yield* Layer.build(PgClient.layer(resolvePoolConfig(url, client)));
81 return yield* PgDrizzle.makeWithDefaults(
82 drizzleConfig as EffectDrizzlePgConfig<TRelations>,
83 ).pipe(Effect.provideContext(pgCtx));
84 }),
85 ),
86 (db) =>
87 proxyChain<
88 EffectPgDatabase<TRelations> & {
89 $client: PgClient.PgClient;
90 }
91 >(
92 db as Effect.Effect<
93 EffectPgDatabase<TRelations> & {
94 $client: PgClient.PgClient;
95 },
96 any,
97 any
98 >,
99 ),
100 );