@thomasfosterau/effect-drizzle-ext-pg 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +442 -2
- package/dist/index.d.ts +159 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations-Dp67FrXh.d.ts +68 -0
- package/dist/migrations-Dp67FrXh.d.ts.map +1 -0
- package/dist/migrations.d.ts +2 -0
- package/dist/migrations.js +136 -0
- package/dist/migrations.js.map +1 -0
- package/dist/testing.d.ts +270 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +408 -0
- package/dist/testing.js.map +1 -0
- package/dist/tooling/index.d.ts +217 -0
- package/dist/tooling/index.d.ts.map +1 -0
- package/dist/tooling/index.js +479 -0
- package/dist/tooling/index.js.map +1 -0
- package/package.json +76 -4
- package/src/index.ts +231 -0
- package/src/migrations.ts +173 -0
- package/src/provisioning.ts +407 -0
- package/src/testing.ts +447 -0
- package/src/tooling/index.ts +46 -0
- package/src/tooling/migration-drift.ts +640 -0
- package/src/tooling/view-dependencies.ts +168 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Foster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,443 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@thomasfosterau/effect-drizzle-ext-pg`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `@effect/sql-pg` binding for `@thomasfosterau/effect-drizzle-ext`: `Database` / `CurrentTransaction` services over `drizzle-orm/effect-postgres`, `PgClient` layers, a sentinel-rollback test runner with test-database provisioning, a fresh-database migration runner, and migration-drift / view-dependency guards.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@thomasfosterau/effect-drizzle-ext-pg)
|
|
6
|
+
|
|
7
|
+
**Disclaimer:** This repository and package have been created using Claude Code. The package's API and functionality is in flux, so expect churn and breaking changes.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @thomasfosterau/effect-drizzle-ext-pg @thomasfosterau/effect-drizzle-ext effect drizzle-orm @effect/sql-pg
|
|
13
|
+
# optional: ./tooling's schema guards, ./testing's container fallback
|
|
14
|
+
pnpm add --save-dev drizzle-kit @testcontainers/postgresql
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`effect` (`^4.0.0`), `drizzle-orm` (`>=1.0.0-rc.5`) and `@effect/sql-pg` (`^4.0.0`) are peer dependencies. The client constructors below are written against `@effect/sql-pg`'s own wire protocol, and `pg` is no longer a peer. `drizzle-kit` (`>=1.0.0-rc.5`, for `./tooling`) and `@testcontainers/postgresql` (`>=12`, for `./testing`'s container fallback) are **optional** peers. `@thomasfosterau/effect-drizzle-ext` is a regular dependency, not a peer — it supplies the driver-agnostic `makeDatabaseServices` factory this package specialises. Node.js 22 or newer is required.
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
`drizzle-orm/effect-postgres` already ships the Effect-native Drizzle driver for Postgres; this package binds it to `@thomasfosterau/effect-drizzle-ext`'s driver-agnostic `makeDatabaseServices` so an application writes the wiring once. `makePgDatabase({ relations, key })` returns:
|
|
22
|
+
|
|
23
|
+
- **`Database`** / **`CurrentTransaction`** — the two `Context.Service` keys, typed over the caller's own `relations` graph.
|
|
24
|
+
- **`transact`** — the write boundary: always-open, never join-if-present (see below).
|
|
25
|
+
- **`readHandle`** — the read resolver: the ambient `CurrentTransaction` when one is open, `Database` otherwise.
|
|
26
|
+
- **`make`** — the `Effect` that builds the Drizzle client (`drizzle-orm/effect-postgres`'s `make` plus its `DefaultServices`) over an ambient `PgClient`.
|
|
27
|
+
- **`layer`** — `make` installed under the `Database` key, requiring only `PgClient`.
|
|
28
|
+
|
|
29
|
+
Call it once, at module scope, and export what it returns:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const database = makePgDatabase({ relations, key: "Database" });
|
|
33
|
+
export const { Database, CurrentTransaction, transact, readHandle } = database;
|
|
34
|
+
export type Database = typeof database.Database.Identifier; // "Database"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`Database`/`CurrentTransaction`/`transact`/`readHandle` are otherwise identical to the driver-agnostic surface documented in [the core package's `./database`](https://github.com/thomasfosterau/effect-drizzle-ext/blob/trunk/packages/core/README.md#database) — this package adds nothing to their semantics, only the driver behind them.
|
|
38
|
+
|
|
39
|
+
Both `make` and `layer` carry `PgClient` in their requirement channel and nothing else: **where the pool comes from is the consuming application's decision**, made by providing one of:
|
|
40
|
+
|
|
41
|
+
- **`pgClientFromUrl`** — `@effect/sql-pg`'s `PgClient.make` plus one `SELECT 1`. **Pings the database at layer build**, failing the build loudly with a `SqlError` if the ping doesn't answer within `connectTimeout`. Right for a long-lived process: a misconfigured URL fails once, at startup, instead of on the first query.
|
|
42
|
+
- **`pgClientFromPool`** — `@effect/sql-pg`'s own `PgClient.layer`, whose pool opens connections on first use. **Never connects at layer build**; the pool is shut down with the layer's own scope, so a runtime built per request disposes of its pool with that request, and a connection failure surfaces on the first query instead. Use this wherever a runtime is built per request or per event — `pgClientFromUrl` there would open a connection on every build, including ones that never touch the database. A pool built this way **must never outlive the scope that created it**.
|
|
43
|
+
- a client layer of the caller's own (any `Layer<PgClient>`).
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export const pgClientFromUrl: (config: {
|
|
47
|
+
readonly url: Redacted.Redacted<string>;
|
|
48
|
+
readonly maxConnections?: number | undefined;
|
|
49
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
50
|
+
readonly types?: PgTypes.Registry | undefined;
|
|
51
|
+
readonly prepare?: boolean | undefined;
|
|
52
|
+
}) => Layer.Layer<PgClient.PgClient | SqlClient, SqlError>;
|
|
53
|
+
|
|
54
|
+
export const pgClientFromPool: (options: {
|
|
55
|
+
readonly connectionString: Redacted.Redacted<string>;
|
|
56
|
+
readonly maxConnections?: number | undefined;
|
|
57
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
58
|
+
readonly types?: PgTypes.Registry | undefined;
|
|
59
|
+
readonly prepare?: boolean | undefined;
|
|
60
|
+
}) => Layer.Layer<PgClient.PgClient | SqlClient, SqlError>;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both output `PgClient | SqlClient` — that is what `@effect/sql-pg`'s own `PgClient.layer` / `PgClient.layerFrom` provide, and `Layer`'s output is covariant, so either satisfies a `PgClient`-only requirement (`testDatabaseLayer` below is built from one).
|
|
64
|
+
|
|
65
|
+
### Binary codecs and Drizzle (read this before upgrading the driver)
|
|
66
|
+
|
|
67
|
+
`@effect/sql-pg` rc.113 and later talks to PostgreSQL in the **binary** protocol only, and decodes each column by OID. Drizzle's `effect-postgres` driver was written for the text protocol, so under the `1.0.0-rc.5-ab785fc` commit build some column types broke or were silently corrupted: user enums and enum arrays, `vector`, `interval`, `tsvector`, `time`, `macaddr`, and `bigint({ mode: "string" })` (the driver handed back a JS `bigint` where Drizzle's type says `string`). The measured table and a tested codec shim are in [website#2027](https://github.com/thomasfosterau/website/issues/2027). The published `drizzle-orm@1.0.0-rc.5`, which this package is built against, reads most of them itself: it selects enums and their arrays, `vector`, `interval`, `tsvector` and `regconfig` as `::text`, and returns a string-mode `bigint` as a string.
|
|
68
|
+
|
|
69
|
+
For a column type it still doesn't cover, use a per-client codec registry. Both helpers forward two optional `@effect/sql-pg` options for it:
|
|
70
|
+
|
|
71
|
+
- `types` — a `PgTypes.Registry` (`PgTypes.makeRegistry()` plus `register(oid, codec, { arrayOid })` calls), for example an `int8` codec that returns a decimal string.
|
|
72
|
+
- `prepare` — `false` to use unnamed statements. Set it behind a pooler that cannot keep named prepared statements across queries, such as Cloudflare Hyperdrive.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const types = PgTypes.makeRegistry();
|
|
76
|
+
types.register(20, int8StringCodec, { arrayOid: 1016 });
|
|
77
|
+
const client = pgClientFromUrl({ url, types, prepare: false });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`tests/integration/round-trip.test.ts` pins an enum, a string-mode `bigint` and a `timestamptz` through Drizzle against a live server, with and without the registry, and checks both read back the same row. This package ships no codecs of its own: which types need one depends on the application's schema.
|
|
81
|
+
|
|
82
|
+
Deployment topology stays with the consuming application: resolving a connection string from a platform binding (a Cloudflare Hyperdrive binding, an environment variable), degrading a failed binding to a proxy client, and any wrapper around the concrete client are all things only the application knows about. This package has no Hyperdrive binding and no Cloudflare/Alchemy integration.
|
|
83
|
+
|
|
84
|
+
## Quick start
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { makePgDatabase, pgClientFromPool } from "@thomasfosterau/effect-drizzle-ext-pg";
|
|
88
|
+
import * as Effect from "effect/Effect";
|
|
89
|
+
import * as Layer from "effect/Layer";
|
|
90
|
+
import * as Redacted from "effect/Redacted";
|
|
91
|
+
import { relations } from "./relations.ts"; // this application's drizzle relations graph
|
|
92
|
+
|
|
93
|
+
const database = makePgDatabase({ relations, key: "Database" });
|
|
94
|
+
export const { Database, CurrentTransaction, transact, readHandle } = database;
|
|
95
|
+
|
|
96
|
+
// Wire the client: a pool that lives and dies with the runtime that built it.
|
|
97
|
+
const DatabaseLive = database.layer.pipe(
|
|
98
|
+
Layer.provide(pgClientFromPool({ connectionString: Redacted.make(process.env.DATABASE_URL!) })),
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
An operation opens its own write boundary with `transact`, yielding `CurrentTransaction` for every statement inside it:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const createWidget = (name: string) =>
|
|
106
|
+
transact(
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
const tx = yield* CurrentTransaction;
|
|
109
|
+
return yield* tx.insert(widgets).values({ name }).returning();
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A read prefers whichever transaction is already open, so a helper called from inside a handler's `transact` sees its uncommitted writes without being told about the transaction at all:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const findWidget = (name: string) =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const handle = yield* readHandle;
|
|
120
|
+
return yield* handle.select().from(widgets).where(eq(widgets.name, name));
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A handler wires the two together — a request path that reads what it just wrote, in one boundary:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const program = transact(
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
yield* createWidget("cog");
|
|
130
|
+
return yield* findWidget("cog"); // sees the insert above; nothing has committed yet
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**`transact` is always-open, never join-if-present.** Calling it while a transaction is already open deliberately opens a _nested_ one — a SAVEPOINT on the same connection (`tx.transaction(...)`, not `db.transaction(...)`), never a silent join of the ambient one. A join-if-present variant is easy to write and is the wrong call: it would delete every SAVEPOINT the calling code currently takes, changing rollback granularity without changing a line of that code. To reuse the open transaction instead of nesting, yield `CurrentTransaction` directly and don't call `transact` again. This is adjudicated (website #1883), not something to "improve" into join-if-present.
|
|
136
|
+
|
|
137
|
+
Reads prefer the ambient transaction for the same reason nesting is deliberate: a read issued against `Database` from inside an open transaction takes its own pooled connection and is not guaranteed to see that transaction's uncommitted writes, so `readHandle` joins the open `CurrentTransaction` whenever there is one and falls back to `Database` — the client — only when there isn't.
|
|
138
|
+
|
|
139
|
+
## `./migrations`
|
|
140
|
+
|
|
141
|
+
The ordered runner for a **brand-new** database. It reads a drizzle-kit `migrations/` directory (one `<digits>_<slug>/` per migration, each holding a `migration.sql`) and applies every migration in directory-name order, each in its own transaction, with no ledger. It mirrors the apply order and transaction shape of alchemy's deploy-time runner, minus the bookkeeping table: the database is always fresh, so every migration applies unconditionally. A directory without a `migration.sql` is skipped. The first failure stops the run with a `MigrationError` naming the migration directory; that migration's transaction rolls back and the ones before it stay applied.
|
|
142
|
+
|
|
143
|
+
`@effect/sql-pg` speaks the extended query protocol, which refuses more than one statement per query. So each `migration.sql` is split on drizzle-kit's `--> statement-breakpoint` marker, exactly as drizzle's own migrator does, and the statements run in order inside the migration's transaction. A hand-written migration must keep that marker between its statements.
|
|
144
|
+
|
|
145
|
+
The directory is read through the `FileSystem` / `Path` services (a Node program provides `@effect/platform-node`'s `NodeServices.layer`), and the statements run on the ambient `SqlClient`.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
export class MigrationError /* Schema.TaggedError: migration, cause */ {}
|
|
149
|
+
export interface Migration {
|
|
150
|
+
readonly name: string;
|
|
151
|
+
readonly statements: ReadonlyArray<string>;
|
|
152
|
+
}
|
|
153
|
+
export const STATEMENT_BREAKPOINT: "--> statement-breakpoint";
|
|
154
|
+
export const splitStatements: (sql: string) => ReadonlyArray<string>;
|
|
155
|
+
export const readMigrations: (
|
|
156
|
+
dir: string,
|
|
157
|
+
) => Effect.Effect<ReadonlyArray<Migration>, PlatformError, FileSystem | Path>;
|
|
158
|
+
export const applyMigrationList: (
|
|
159
|
+
migrations: ReadonlyArray<Migration>,
|
|
160
|
+
) => Effect.Effect<number, MigrationError, SqlClient>;
|
|
161
|
+
export const applyMigrations: (
|
|
162
|
+
dir: string,
|
|
163
|
+
) => Effect.Effect<number, MigrationError | PlatformError, SqlClient | FileSystem | Path>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const applied = applyMigrations(MIGRATIONS_DIR).pipe(
|
|
168
|
+
Effect.provide(PgClient.layer({ url, maxConnections: 1 })),
|
|
169
|
+
Effect.provide(NodeServices.layer),
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## `./testing`
|
|
174
|
+
|
|
175
|
+
An always-rollback test transaction runner: every spec runs inside a real database transaction that is rolled back once it completes, so a suite needs no truncation, no fixture reset and no ordering discipline. The library owns no schema, so a spec creates whatever `TEMPORARY` tables it needs inside that transaction — DDL is transactional in PostgreSQL, so the always-rollback disposes of them with everything else.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
export const isRetryableConnectFailure: (error: unknown) => boolean;
|
|
179
|
+
|
|
180
|
+
export interface TestTransaction<Key extends string> {
|
|
181
|
+
readonly runInTestTransaction: <R, A, E>(
|
|
182
|
+
runtime: ManagedRuntime.ManagedRuntime<R, SqlError>,
|
|
183
|
+
effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>,
|
|
184
|
+
) => Promise<A>;
|
|
185
|
+
readonly withTestTransaction: <R>(
|
|
186
|
+
runtime: ManagedRuntime.ManagedRuntime<R, SqlError>,
|
|
187
|
+
) => <A, E>(effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>) => Promise<A>;
|
|
188
|
+
readonly attemptInSavepoint: <A, E, R>(
|
|
189
|
+
effect: Effect.Effect<A, E, R>,
|
|
190
|
+
) => Effect.Effect<Exit.Exit<A, E | SqlError>, never, R | `${Key}/CurrentTransaction`>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const makeTestTransaction: <Relations extends AnyRelations, Key extends string>(
|
|
194
|
+
database: PgDatabase<Relations, Key>,
|
|
195
|
+
) => TestTransaction<Key>;
|
|
196
|
+
|
|
197
|
+
export const testDatabaseLayer: <Relations extends AnyRelations, Key extends string>(
|
|
198
|
+
database: PgDatabase<Relations, Key>,
|
|
199
|
+
options: {
|
|
200
|
+
readonly url: Redacted.Redacted<string>;
|
|
201
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
202
|
+
},
|
|
203
|
+
) => Layer.Layer<Key, SqlError>;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`testDatabaseLayer(database, { url })` is the suite's `Database` layer: the application's own `database.layer` over a `PgClient` built from `url`, with `maxConnections` **pinned to 1**. That pin is load-bearing, not a default to override: a fork runs its tests strictly sequentially and the runner holds exactly one transaction-bound connection at a time, so 1 is the real per-fork demand — and it's what forces an operation-opened `transact` to nest as a SAVEPOINT on that same connection (and so see the outer test transaction's uncommitted writes) rather than silently acquiring a second connection that cannot. Unlike production, `testDatabaseLayer` uses the pinging `pgClientFromUrl` on purpose: a suite that cannot reach its database should fail once at layer build with one legible `SqlError`, not once per spec on the first query.
|
|
207
|
+
|
|
208
|
+
`makeTestTransaction(database)` builds the runner for one application's `PgDatabase`. A full spec:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { pgTable, integer } from "drizzle-orm/pg-core";
|
|
212
|
+
import { sql } from "drizzle-orm";
|
|
213
|
+
import * as Effect from "effect/Effect";
|
|
214
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
215
|
+
import * as Redacted from "effect/Redacted";
|
|
216
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
217
|
+
import { database } from "./database.ts"; // this application's makePgDatabase() result
|
|
218
|
+
import {
|
|
219
|
+
makeTestTransaction,
|
|
220
|
+
testDatabaseLayer,
|
|
221
|
+
} from "@thomasfosterau/effect-drizzle-ext-pg/testing";
|
|
222
|
+
|
|
223
|
+
const url = Redacted.make(process.env.DATABASE_URL_TEST!);
|
|
224
|
+
const runtime = ManagedRuntime.make(testDatabaseLayer(database, { url }));
|
|
225
|
+
const { runInTestTransaction } = makeTestTransaction(database);
|
|
226
|
+
|
|
227
|
+
const { Database, CurrentTransaction, transact } = database;
|
|
228
|
+
|
|
229
|
+
describe("widgets", () => {
|
|
230
|
+
it("creates a widget", async () => {
|
|
231
|
+
await runInTestTransaction(
|
|
232
|
+
runtime,
|
|
233
|
+
Effect.gen(function* () {
|
|
234
|
+
const db = yield* Database;
|
|
235
|
+
yield* db.execute(sql`CREATE TEMPORARY TABLE widget (id integer PRIMARY KEY)`);
|
|
236
|
+
yield* transact(
|
|
237
|
+
Effect.flatMap(CurrentTransaction, (tx) =>
|
|
238
|
+
tx.execute(sql`INSERT INTO widget (id) VALUES (1)`),
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
// …assert against it here…
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("sees nothing from the spec above", async () => {
|
|
247
|
+
await runInTestTransaction(
|
|
248
|
+
runtime,
|
|
249
|
+
// `widget` doesn't exist here: the previous spec's transaction, table included, rolled back.
|
|
250
|
+
Effect.void,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`withTestTransaction(runtime)` is `runInTestTransaction` with the runtime bound once — the form a suite exports as its own helper. Both wrap the spec's effect in the application's own `transact`, so the spec opens its write boundary exactly as a request handler does in production; binding `CurrentTransaction` straight from the harness instead would make specs the one place in the system where a write runs without anything having called `transact`.
|
|
257
|
+
|
|
258
|
+
**A constraint-rejection assertion needs `attemptInSavepoint`, not a bare `Effect.exit`.** A statement that raises aborts the _whole_ PostgreSQL transaction, so `Effect.exit(tx.insert(...))` alone captures the expected failure but then poisons every later statement in the test with `current transaction is aborted, commands ignored until end of transaction block` — surfacing several assertions away from the real cause, on whatever query runs next. `attemptInSavepoint` nests the attempt in a SAVEPOINT on the same pinned connection, so the driver rolls back to it on failure and the enclosing transaction — and every fixture the test already wrote — carries on:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
it("rejects a duplicate slug without poisoning the rest of the test", async () => {
|
|
262
|
+
await runInTestTransaction(
|
|
263
|
+
runtime,
|
|
264
|
+
Effect.gen(function* () {
|
|
265
|
+
yield* transact(insertWidget({ slug: "cog" }));
|
|
266
|
+
|
|
267
|
+
const rejected = yield* attemptInSavepoint(transact(insertWidget({ slug: "cog" })));
|
|
268
|
+
expect(rejected._tag).toBe("Failure");
|
|
269
|
+
|
|
270
|
+
// Without the savepoint, this next statement would fail with
|
|
271
|
+
// "current transaction is aborted" instead of running.
|
|
272
|
+
yield* transact(insertWidget({ slug: "sprocket" }));
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Provisioning the test database
|
|
279
|
+
|
|
280
|
+
`provisionTestDatabase(options)` is the half of an integration suite's global setup that every application carried as its own `local-database.ts`; preferenceflow's copy is the reference. It resolves where the run's database comes from, then provisions it:
|
|
281
|
+
|
|
282
|
+
0. **An explicit `DATABASE_URL_TEST` / `DATABASE_URL`** wins and is used as-is. It must already be migrated; nothing is provisioned or torn down.
|
|
283
|
+
1. **A reachable local server.** This is whatever the standard `PG*` variables point at, defaulting to a trust-auth `postgres@127.0.0.1:5432`. This checkout's database is dropped, recreated and migrated on it.
|
|
284
|
+
2. **A Testcontainers container** when nothing answers and no `PG*` variable is set. It needs a Docker daemon and the optional peer `@testcontainers/postgresql`, which is imported only on this path. The container stops when the provisioning scope closes.
|
|
285
|
+
|
|
286
|
+
An explicit `PG*` target that does not answer fails with `DatabaseUnreachable`; the run never falls back silently. The database name is `<dbNamePrefix>_test_<sha256(migrationsDir)[:8]>`, so parallel worktrees sharing one server never drop each other's database; pass `migrationsDir` as an absolute path. The container `image` and `dbNamePrefix` are parameters: `pgvector/pgvector:pg17` in website and gardeneria, `postgres:17` in preferenceflow. By default the database is migrated with `./migrations`' `applyMigrations` over `migrationsDir`; a harness whose migrations are not a drizzle-kit directory passes its own `applyMigrations: (url) => Effect<number, …>`.
|
|
287
|
+
|
|
288
|
+
The resolution order is `resolveDatabaseSource(options)`. It is pure apart from one server probe, and the probe is injectable, so the order is unit-testable without a server. Fixture users and seed data stay with the application: the harness → database edge runs one way.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
export const provisionTestDatabase: <E, R>(
|
|
292
|
+
options: ProvisionOptions<E, R>, // { env, migrationsDir, dbNamePrefix, image, probe?, applyMigrations? }
|
|
293
|
+
) => Effect.Effect<
|
|
294
|
+
ProvisionedDatabase, // { url: Redacted<string>, source: DatabaseSource }
|
|
295
|
+
| E
|
|
296
|
+
| DatabaseUnreachable
|
|
297
|
+
| InvalidDatabasePrefix
|
|
298
|
+
| ContainerStartFailed
|
|
299
|
+
| SqlError
|
|
300
|
+
| MigrationError
|
|
301
|
+
| PlatformError,
|
|
302
|
+
R | FileSystem | Path | Scope
|
|
303
|
+
>;
|
|
304
|
+
export const resolveDatabaseSource: (
|
|
305
|
+
options: DatabaseSourceOptions,
|
|
306
|
+
) => Effect.Effect<DatabaseSource, DatabaseUnreachable | InvalidDatabasePrefix>;
|
|
307
|
+
export type DatabaseSource =
|
|
308
|
+
| { readonly _tag: "Explicit"; readonly url: Redacted<string> }
|
|
309
|
+
| {
|
|
310
|
+
readonly _tag: "LocalServer";
|
|
311
|
+
readonly adminUrl: Redacted<string>;
|
|
312
|
+
readonly database: string;
|
|
313
|
+
readonly url: Redacted<string>;
|
|
314
|
+
}
|
|
315
|
+
| { readonly _tag: "Container"; readonly image: string };
|
|
316
|
+
export type ServerProbe = (adminUrl: Redacted<string>) => Effect.Effect<void, ServerUnreachable>;
|
|
317
|
+
export const probeServer: ServerProbe;
|
|
318
|
+
export const testDatabaseName: (options: {
|
|
319
|
+
readonly prefix: string;
|
|
320
|
+
readonly migrationsDir: string;
|
|
321
|
+
}) => Effect.Effect<string, InvalidDatabasePrefix>;
|
|
322
|
+
export const connectionUrl: (env: ResolutionEnvironment, database: string) => Redacted<string>;
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
A vitest global setup holds a `Scope` across the run and closes it in the teardown it returns. This is this repository's own `packages/pg/tests/integration/global-setup.ts`:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
export default async function setup() {
|
|
329
|
+
const scope = Effect.runSync(Scope.make());
|
|
330
|
+
const database = await Effect.runPromise(
|
|
331
|
+
provisionTestDatabase({
|
|
332
|
+
env: process.env,
|
|
333
|
+
migrationsDir: MIGRATIONS_DIR,
|
|
334
|
+
dbNamePrefix: "effect_drizzle_ext",
|
|
335
|
+
image: "pgvector/pgvector:pg17",
|
|
336
|
+
}).pipe(Scope.provide(scope), Effect.provide(NodeServices.layer)),
|
|
337
|
+
);
|
|
338
|
+
process.env.DATABASE_URL_TEST = Redacted.value(database.url);
|
|
339
|
+
return () => Effect.runPromise(Scope.close(scope, Exit.void));
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Retries
|
|
344
|
+
|
|
345
|
+
`isRetryableConnectFailure` decides what the runner retries: a `SqlError` that failed at the connect/acquire seam (`operation` of `"connect"` / `"acquireConnection"`), whether or not the driver itself flagged it retryable — this catches a pool-acquisition timeout or an `ECONNRESET` under fork-startup contention, neither of which carries the SQLSTATE `@effect/sql-pg` requires to flag `isRetryable`. It never retries a failure raised by the test's own query (constraint violations, syntax errors), and never retries once the test body has run — only the _open_ of the outer transaction is retried, never test logic.
|
|
346
|
+
|
|
347
|
+
Failures surface through `Cause.squash`, so the underlying error — not an opaque `Cause` — reaches the rejected `Promise`, and `.rejects.toThrow(/…/)` matches it directly:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
await expect(runInTestTransaction(runtime, Effect.fail(new Error("boom")))).rejects.toThrow(/boom/);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Errors against a live database
|
|
354
|
+
|
|
355
|
+
`classifyError` from [`@thomasfosterau/effect-drizzle-ext/errors`](https://github.com/thomasfosterau/effect-drizzle-ext/blob/trunk/packages/core/README.md#errors) recovers a tagged, fact-carrying error from the drizzle / `@effect/sql-pg` error graph a real query raises — `constraint`, `table` and `column` are populated the same way against this driver as they are documented in the core package. `catchUniqueViolation` is the combinator most operations reach for, typically wrapping a `transact`ed write:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { catchUniqueViolation } from "@thomasfosterau/effect-drizzle-ext/errors";
|
|
359
|
+
|
|
360
|
+
const createWidget = (slug: string) =>
|
|
361
|
+
transact(
|
|
362
|
+
Effect.flatMap(CurrentTransaction, (tx) => tx.insert(widgets).values({ slug }).returning()),
|
|
363
|
+
).pipe(
|
|
364
|
+
catchUniqueViolation((violation) =>
|
|
365
|
+
violation.constraint._tag === "Some" && violation.constraint.value === "widget_slug_key"
|
|
366
|
+
? Effect.fail(new SlugTaken({ slug }))
|
|
367
|
+
: Effect.die(violation),
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`violation.constraint` / `.table` / `.column` / `.detail` are `Option`s, populated per-SQLSTATE as documented in the core README; for a violation raised against a `TEMPORARY` table, `schema` is the session's temporary schema (`pg_temp_N`), not `public`.
|
|
373
|
+
|
|
374
|
+
## `./tooling`
|
|
375
|
+
|
|
376
|
+
Two schema guards for a drizzle-kit `migrations/` directory. Neither needs a database. Both are **bin-less** Effects: an application's own `db:migration-drift` / `db:view-dependencies` task runs them with its schema module and reports the typed error. They are ported from the applications' `packages/database/scripts/check-*.ts` (website #1200, #1471, #1228). `drizzle-kit` is an optional peer that only this subpath imports, through `drizzle-kit/api-postgres`.
|
|
377
|
+
|
|
378
|
+
**`checkMigrationDrift({ schema, migrationsDir, base? })`** runs three checks, each guarding "snapshot history is a DAG, not a chain" from a different side. All three problems are reported together as one `MigrationDriftError { issues }`.
|
|
379
|
+
|
|
380
|
+
1. **DAG integrity** (`checkDagIntegrity`, pure):
|
|
381
|
+
- every snapshot id is unique;
|
|
382
|
+
- every `prevIds` entry resolves, or is drizzle-kit's all-zero root sentinel;
|
|
383
|
+
- there is exactly one head, and everything is reachable from it;
|
|
384
|
+
- there is no cycle.
|
|
385
|
+
2. **Zero drift.** The schema is diffed against the **lexicographically-latest** snapshot with `generateDrizzleJson` / `generateMigration`: the same "latest by directory name" pick alchemy's `Drizzle.Schema.regenerate` makes at deploy time, not a DAG-aware one. The result must be zero statements. A diff drizzle-kit could only resolve interactively (rename or recreate) counts as drift too. This check runs only on a structurally sound DAG.
|
|
386
|
+
3. **Base ancestry**, when `base` is `Some`. Every migration the branch adds must descend from the base branch's current head. `readBaseMigrations` reads the base through `git ls-tree` / `git show` (fetching it into a shallow checkout if needed). `resolveBaseRef(env)` takes the ref from `MIGRATION_BASE_REF`, else from `origin/$GITHUB_BASE_REF`, which GitHub Actions sets only on pull requests.
|
|
387
|
+
|
|
388
|
+
**`checkViewDependencies({ schema })`** fails with `ViewDependencyError { edges }` when any view selects from another view (`DB-VIEW-001`). drizzle-kit emits `DROP VIEW` / `CREATE VIEW` with no topological order and no cascade, so a migration touching either end of such an edge is one PostgreSQL rejects. It reads the graph out of drizzle-kit's own `CREATE VIEW` DDL.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
export const checkMigrationDrift: (options: {
|
|
392
|
+
readonly schema: Record<string, unknown>;
|
|
393
|
+
readonly migrationsDir: string;
|
|
394
|
+
readonly base?: Option.Option<GitBase> | undefined; // { ref, migrationsPath, cwd }
|
|
395
|
+
}) => Effect.Effect<
|
|
396
|
+
MigrationDriftReport, // { snapshots, baseRef }
|
|
397
|
+
MigrationDriftError | MalformedSnapshot | SchemaDiffError | PlatformError,
|
|
398
|
+
FileSystem | Path | ChildProcessSpawner
|
|
399
|
+
>;
|
|
400
|
+
export const checkViewDependencies: (options: {
|
|
401
|
+
readonly schema: Record<string, unknown>;
|
|
402
|
+
}) => Effect.Effect<{ readonly viewCount: number }, ViewDependencyError | SchemaDiffError>;
|
|
403
|
+
// Pure pieces, for a caller composing its own report:
|
|
404
|
+
export const checkDagIntegrity: (entries: ReadonlyArray<SnapshotEntry>) => ReadonlyArray<string>;
|
|
405
|
+
export const checkBaseAncestry: (
|
|
406
|
+
entries: ReadonlyArray<SnapshotEntry>,
|
|
407
|
+
base: BaseMigrations,
|
|
408
|
+
) => ReadonlyArray<string>;
|
|
409
|
+
export const findViewEdges: (views: ReadonlyArray<ViewDefinition>) => ReadonlyArray<ViewEdge>;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
An application's script:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
// packages/database/scripts/check-migration-drift.ts — run by the `db:migration-drift` task
|
|
416
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
417
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
418
|
+
import { checkMigrationDrift, resolveBaseRef } from "@thomasfosterau/effect-drizzle-ext-pg/tooling";
|
|
419
|
+
import * as schema from "../src/schema.ts";
|
|
420
|
+
|
|
421
|
+
const repoRoot = path.resolve(import.meta.dirname, "../../..");
|
|
422
|
+
checkMigrationDrift({
|
|
423
|
+
schema,
|
|
424
|
+
migrationsDir: path.join(repoRoot, "packages/database/migrations"),
|
|
425
|
+
base: Option.map(resolveBaseRef(process.env), (ref) => ({
|
|
426
|
+
ref,
|
|
427
|
+
migrationsPath: "packages/database/migrations",
|
|
428
|
+
cwd: repoRoot,
|
|
429
|
+
})),
|
|
430
|
+
}).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
This repository proves both guards against a committed fixture chain, `packages/pg/tests/fixtures/` (a schema module plus the drizzle-kit migrations generated from it), in the unit suite CI runs. The integration suite provisions its database from the same chain.
|
|
434
|
+
|
|
435
|
+
## Limitations
|
|
436
|
+
|
|
437
|
+
- **No Hyperdrive / Cloudflare layer.** Resolving a connection string from a platform binding, and degrading a failed binding to a proxy client, are deployment-topology decisions that stay in the consuming application.
|
|
438
|
+
- **No migrator for a live database.** `./migrations` applies everything to a database that is always brand new; it keeps no ledger, so it is not a deploy-time migrator.
|
|
439
|
+
- **Single driver.** This package binds `@effect/sql-pg` only; a different Postgres driver, or a different database entirely, needs its own binding package built the same way against the core package's `makeDatabaseServices`.
|
|
440
|
+
|
|
441
|
+
## License
|
|
442
|
+
|
|
443
|
+
MIT © Thomas Foster. Repository: [github.com/thomasfosterau/effect-drizzle-ext](https://github.com/thomasfosterau/effect-drizzle-ext), package directory `packages/pg`. Extracted from the `thomasfosterau/website`, `thomasfosterau/gardeneria` and `thomasfosterau/preferenceflow` applications. See [`@thomasfosterau/effect-drizzle-ext`](https://github.com/thomasfosterau/effect-drizzle-ext/blob/trunk/packages/core/README.md) for the driver-agnostic half.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
2
|
+
import { DatabaseServices } from "@thomasfosterau/effect-drizzle-ext/database";
|
|
3
|
+
import { EffectPgDatabase, EffectPgQueryResultHKT, EffectPgTransaction } from "drizzle-orm/effect-postgres";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
7
|
+
import * as Duration from "effect/Duration";
|
|
8
|
+
import { SqlError } from "effect/sql/SqlError";
|
|
9
|
+
import * as Redacted from "effect/Redacted";
|
|
10
|
+
import * as PgTypes from "@effect/sql-pg/PgTypes";
|
|
11
|
+
import { AnyRelations } from "drizzle-orm/relations";
|
|
12
|
+
/**
|
|
13
|
+
* The Effect-native Drizzle client an application's `Database` service holds,
|
|
14
|
+
* typed over its relations graph.
|
|
15
|
+
*
|
|
16
|
+
* `$client` is the `PgClient` the driver was built against; it is part of the
|
|
17
|
+
* value `drizzle-orm/effect-postgres`'s `make` returns, and is named here so
|
|
18
|
+
* the service payload type is self-contained rather than
|
|
19
|
+
* `Effect.Success<typeof make>` of a local binding.
|
|
20
|
+
*/
|
|
21
|
+
export type PgDatabaseClient<Relations extends AnyRelations> = EffectPgDatabase<Relations> & {
|
|
22
|
+
readonly $client: PgClient.PgClient;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The transaction handle the driver passes to a `db.transaction(...)`
|
|
26
|
+
* callback — the payload of the `CurrentTransaction` service.
|
|
27
|
+
*
|
|
28
|
+
* The query-result HKT is pinned to `EffectPgQueryResultHKT` (the `@effect/
|
|
29
|
+
* sql-pg` one) because that is what `EffectPgDatabase` opens its transactions
|
|
30
|
+
* with; a consumer never writes the parameter.
|
|
31
|
+
*/
|
|
32
|
+
export type PgTransaction<Relations extends AnyRelations> = EffectPgTransaction<EffectPgQueryResultHKT, Relations>;
|
|
33
|
+
/**
|
|
34
|
+
* One application's database services: the driver-agnostic
|
|
35
|
+
* {@link DatabaseServices} surface, plus the two things that are specific to
|
|
36
|
+
* having chosen a driver — the `Effect` that builds the client and the
|
|
37
|
+
* `Layer` that installs it under the `Database` key.
|
|
38
|
+
*
|
|
39
|
+
* Both carry `PgClient` in their requirement channel and nothing else: WHERE
|
|
40
|
+
* the pool comes from is the consuming application's decision, made by
|
|
41
|
+
* providing {@link pgClientFromUrl}, {@link pgClientFromPool}, or a client
|
|
42
|
+
* layer of its own.
|
|
43
|
+
*/
|
|
44
|
+
export interface PgDatabase<Relations extends AnyRelations, Key extends string> extends DatabaseServices<PgDatabaseClient<Relations>, PgTransaction<Relations>, Key> {
|
|
45
|
+
/**
|
|
46
|
+
* Builds the Drizzle client over an ambient `PgClient`.
|
|
47
|
+
*
|
|
48
|
+
* `DefaultServices` supplies the no-op logger and cache the driver
|
|
49
|
+
* requires; an application that wants real ones builds its own client and
|
|
50
|
+
* provides it under {@link DatabaseServices.Database} instead of using
|
|
51
|
+
* {@link layer}.
|
|
52
|
+
*/
|
|
53
|
+
readonly make: Effect.Effect<PgDatabaseClient<Relations>, never, PgClient.PgClient>;
|
|
54
|
+
/**
|
|
55
|
+
* {@link make} installed under the `Database` key — the stanza every
|
|
56
|
+
* consuming application was repeating once per runtime graph.
|
|
57
|
+
*
|
|
58
|
+
* Requires a `PgClient`, which is the seam the deployment topology plugs
|
|
59
|
+
* into: a request-scoped pool over a platform binding in production, a
|
|
60
|
+
* URL-configured pool in a harness.
|
|
61
|
+
*/
|
|
62
|
+
readonly layer: Layer.Layer<Key, never, PgClient.PgClient>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Builds one application's {@link PgDatabase} from its relations graph and a
|
|
66
|
+
* service key.
|
|
67
|
+
*
|
|
68
|
+
* Call it once, at module scope, and export what it returns; the keys are
|
|
69
|
+
* derived from `key`, so calling it twice with the same key produces services
|
|
70
|
+
* that share a runtime slot.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { makePgDatabase } from "@thomasfosterau/effect-drizzle-ext-pg";
|
|
75
|
+
*
|
|
76
|
+
* const relations = {}; // the application's drizzle relations graph
|
|
77
|
+
* const database = makePgDatabase({ relations, key: "Database" });
|
|
78
|
+
* export const { Database, CurrentTransaction, transact, readHandle } = database;
|
|
79
|
+
* export type Database = typeof database.Database.Identifier; // "Database"
|
|
80
|
+
*
|
|
81
|
+
* console.log(Database.key); // "Database"
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare const makePgDatabase: <Relations extends AnyRelations, const Key extends string>(options: {
|
|
85
|
+
readonly relations: Relations;
|
|
86
|
+
readonly key: Key;
|
|
87
|
+
}) => PgDatabase<Relations, Key>;
|
|
88
|
+
/**
|
|
89
|
+
* A `PgClient` layer over a connection URL that **pings the database
|
|
90
|
+
* (`SELECT 1`) at layer BUILD**, failing the build with a `SqlError` if the
|
|
91
|
+
* ping does not answer within `connectTimeout`.
|
|
92
|
+
*
|
|
93
|
+
* That is the right trade for a long-lived process — a misconfigured URL
|
|
94
|
+
* fails once, loudly, at startup rather than on the first query — and the
|
|
95
|
+
* wrong one for a runtime that is built per request, where it opens a
|
|
96
|
+
* connection even for requests that never touch the database. Use
|
|
97
|
+
* {@link pgClientFromPool} there.
|
|
98
|
+
*
|
|
99
|
+
* `@effect/sql-pg` 4.0.0-rc.115 replaced the `pg` driver with its own wire
|
|
100
|
+
* protocol, and its `PgClient.layer` no longer pings on its own: the pool
|
|
101
|
+
* opens connections on first use. The ping is therefore this function's, run
|
|
102
|
+
* once over the freshly built client.
|
|
103
|
+
*
|
|
104
|
+
* The output includes `SqlClient` as well as `PgClient` because that is what
|
|
105
|
+
* `PgClient.layerFrom` provides; a consumer that only needs `PgClient` can
|
|
106
|
+
* ignore the extra service.
|
|
107
|
+
*/
|
|
108
|
+
export declare const pgClientFromUrl: (config: {
|
|
109
|
+
readonly url: Redacted.Redacted<string>;
|
|
110
|
+
readonly maxConnections?: number | undefined;
|
|
111
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* A per-client codec registry (`PgTypes.makeRegistry()` plus your
|
|
114
|
+
* `register` calls), forwarded to `@effect/sql-pg`. See the README's
|
|
115
|
+
* "Binary codecs" section for why Drizzle needs one.
|
|
116
|
+
*/
|
|
117
|
+
readonly types?: PgTypes.Registry | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Named prepared statements (`@effect/sql-pg` defaults this to `true`).
|
|
120
|
+
* Set `false` behind a pooler that cannot keep them across queries, such
|
|
121
|
+
* as Cloudflare Hyperdrive.
|
|
122
|
+
*/
|
|
123
|
+
readonly prepare?: boolean | undefined;
|
|
124
|
+
}) => Layer.Layer<PgClient.PgClient | SqlClient, SqlError>;
|
|
125
|
+
/**
|
|
126
|
+
* A `PgClient` layer over a connection URL that **does not connect at layer
|
|
127
|
+
* build** — the lazy counterpart to {@link pgClientFromUrl}.
|
|
128
|
+
*
|
|
129
|
+
* The pool is `@effect/sql-pg`'s own: it opens connections on first use (up
|
|
130
|
+
* to `maxConnections`) and is shut down, every session released, when the
|
|
131
|
+
* layer's scope closes. A runtime built per request therefore disposes of its
|
|
132
|
+
* pool with that request, and a connection failure surfaces on the first
|
|
133
|
+
* query as a `SqlError` instead of at build. Platforms that pin I/O to the
|
|
134
|
+
* event that created it require exactly this — a pool must never outlive
|
|
135
|
+
* that event (website #455).
|
|
136
|
+
*
|
|
137
|
+
* Until `@effect/sql-pg` 4.0.0-rc.115 this wrapped a bare `pg.Pool` via
|
|
138
|
+
* `PgClient.fromPool`, because `PgClient.layer` pinged at build; rc.115
|
|
139
|
+
* dropped both the `pg` driver and `fromPool`, and its pool is lazy by
|
|
140
|
+
* construction. The name is kept for the contract, not the mechanism.
|
|
141
|
+
*/
|
|
142
|
+
export declare const pgClientFromPool: (options: {
|
|
143
|
+
readonly connectionString: Redacted.Redacted<string>;
|
|
144
|
+
readonly maxConnections?: number | undefined;
|
|
145
|
+
readonly connectTimeout?: Duration.Input | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* A per-client codec registry (`PgTypes.makeRegistry()` plus your
|
|
148
|
+
* `register` calls), forwarded to `@effect/sql-pg`. See the README's
|
|
149
|
+
* "Binary codecs" section for why Drizzle needs one.
|
|
150
|
+
*/
|
|
151
|
+
readonly types?: PgTypes.Registry | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Named prepared statements (`@effect/sql-pg` defaults this to `true`).
|
|
154
|
+
* Set `false` behind a pooler that cannot keep them across queries, such
|
|
155
|
+
* as Cloudflare Hyperdrive.
|
|
156
|
+
*/
|
|
157
|
+
readonly prepare?: boolean | undefined;
|
|
158
|
+
}) => Layer.Layer<PgClient.PgClient | SqlClient, SqlError>;
|
|
159
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;YAwDY,iBAAiB,kBAAkB,gBAAgB,iBAAiB;WACrE,SAAS,SAAS;;;;;;;;;;YAWjB,cAAc,kBAAkB,gBAAgB,oBAC1D,wBACA;;;;;;;;;;;;iBAce,WACf,kBAAkB,cAClB,4BACQ,iBAAiB,iBAAiB,YAAY,cAAc,YAAY;;;;;;;;;WASvE,MAAM,OAAO,OAAO,iBAAiB,mBAAmB,SAAS;;;;;;;;;WAUjE,OAAO,MAAM,MAAM,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;qBAuBtC,iBAAkB,kBAAkB,oBAAoB,oBAAkB;WAC5E,WAAW;WACX,KAAK;MACZ,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;qBAqCb,kBAAe;WACjB,KAAK,SAAS;WACd;WACA,iBAAiB,SAAS;;;;;;WAM1B,QAAQ,QAAQ;;;;;;WAMhB;MACP,MAAM,MAAM,SAAS,WAAW,WAAW;;;;;;;;;;;;;;;;;;qBAqBlC,mBAAgB;WAClB,kBAAkB,SAAS;WAC3B;WACA,iBAAiB,SAAS;;;;;;WAM1B,QAAQ,QAAQ;;;;;;WAMhB;MACP,MAAM,MAAM,SAAS,WAAW,WAAW"}
|