@effect/sql-pg 4.0.0-beta.7 → 4.0.0-beta.71
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/README.md +1 -1
- package/dist/PgClient.d.ts +108 -36
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +430 -194
- package/dist/PgClient.js.map +1 -1
- package/dist/PgMigrator.d.ts +53 -9
- package/dist/PgMigrator.d.ts.map +1 -1
- package/dist/PgMigrator.js +93 -60
- package/dist/PgMigrator.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/package.json +9 -9
- package/src/PgClient.ts +637 -267
- package/src/PgMigrator.ts +119 -65
- package/src/index.ts +3 -3
package/dist/PgClient.js
CHANGED
|
@@ -1,45 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* PostgreSQL driver for Effect SQL, backed by the `pg` package.
|
|
3
|
+
*
|
|
4
|
+
* Use this module to provide a {@link PgClient} and the generic `SqlClient`
|
|
5
|
+
* service from pool settings, a single managed `pg.Client`, an existing
|
|
6
|
+
* `pg.Pool`, or custom connection acquirers. The client uses Effect SQL's
|
|
7
|
+
* PostgreSQL compiler, classifies common PostgreSQL failures as `SqlError`s,
|
|
8
|
+
* and adds PostgreSQL-specific JSON fragments plus LISTEN/NOTIFY operations.
|
|
9
|
+
*
|
|
10
|
+
* ## Mental model
|
|
11
|
+
*
|
|
12
|
+
* Pool-backed clients acquire a connection for each operation. Transactions and
|
|
13
|
+
* cursor streams keep a dedicated connection for their scope, so they consume
|
|
14
|
+
* pool capacity while active. Clients built from one `pg.Client` serialize
|
|
15
|
+
* query access through that client; set `acquireForStream` in
|
|
16
|
+
* {@link makeClient} when streams or listeners need separate clients.
|
|
17
|
+
*
|
|
18
|
+
* ## Common tasks
|
|
19
|
+
*
|
|
20
|
+
* - Use {@link layer} with concrete pool settings, or {@link layerConfig} when
|
|
21
|
+
* settings should come from `Config`.
|
|
22
|
+
* - Use {@link make} for a scoped pool-backed client without immediately
|
|
23
|
+
* turning it into a layer.
|
|
24
|
+
* - Use {@link fromPool} or {@link fromClient} when another component owns
|
|
25
|
+
* acquisition of the underlying `pg` resources.
|
|
26
|
+
* - Use `client.json`, `client.listen`, and `client.notify` for
|
|
27
|
+
* PostgreSQL-specific values and notifications.
|
|
28
|
+
*
|
|
29
|
+
* ## Gotchas
|
|
30
|
+
*
|
|
31
|
+
* LISTEN opens a scoped long-lived client and issues `UNLISTEN` when the stream
|
|
32
|
+
* scope closes, so keep listener streams scoped for exactly the period
|
|
33
|
+
* notifications are needed. Long-running transactions, streams, and listeners
|
|
34
|
+
* can hold onto database connections even while other fibers continue to use
|
|
35
|
+
* the same `PgClient`.
|
|
36
|
+
*
|
|
37
|
+
* @since 4.0.0
|
|
3
38
|
*/
|
|
4
39
|
import * as Arr from "effect/Array";
|
|
5
40
|
import * as Cause from "effect/Cause";
|
|
6
41
|
import * as Channel from "effect/Channel";
|
|
7
42
|
import * as Config from "effect/Config";
|
|
43
|
+
import * as Context from "effect/Context";
|
|
8
44
|
import * as Duration from "effect/Duration";
|
|
9
45
|
import * as Effect from "effect/Effect";
|
|
10
46
|
import * as Fiber from "effect/Fiber";
|
|
11
47
|
import * as Layer from "effect/Layer";
|
|
12
48
|
import * as Number from "effect/Number";
|
|
49
|
+
import * as Option from "effect/Option";
|
|
13
50
|
import * as Queue from "effect/Queue";
|
|
14
51
|
import * as RcRef from "effect/RcRef";
|
|
15
52
|
import * as Redacted from "effect/Redacted";
|
|
16
53
|
import * as Scope from "effect/Scope";
|
|
17
|
-
import * as
|
|
54
|
+
import * as Semaphore from "effect/Semaphore";
|
|
18
55
|
import * as Stream from "effect/Stream";
|
|
19
56
|
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
20
57
|
import * as Client from "effect/unstable/sql/SqlClient";
|
|
21
|
-
import { SqlError } from "effect/unstable/sql/SqlError";
|
|
58
|
+
import { AuthenticationError, AuthorizationError, ConnectionError, ConstraintError, DeadlockError, LockTimeoutError, SerializationError, SqlError, SqlSyntaxError, StatementTimeoutError, UniqueViolation, UnknownError } from "effect/unstable/sql/SqlError";
|
|
22
59
|
import * as Statement from "effect/unstable/sql/Statement";
|
|
23
60
|
import * as Pg from "pg";
|
|
24
61
|
import * as PgConnString from "pg-connection-string";
|
|
25
62
|
import Cursor from "pg-cursor";
|
|
26
|
-
const ATTR_DB_SYSTEM_NAME = "db.system.name";
|
|
27
|
-
const ATTR_DB_NAMESPACE = "db.namespace";
|
|
28
|
-
const ATTR_SERVER_ADDRESS = "server.address";
|
|
29
|
-
const ATTR_SERVER_PORT = "server.port";
|
|
30
63
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
64
|
+
* Runtime type identifier used to mark `PgClient` values.
|
|
65
|
+
*
|
|
66
|
+
* @category type IDs
|
|
67
|
+
* @since 4.0.0
|
|
33
68
|
*/
|
|
34
69
|
export const TypeId = "~@effect/sql-pg/PgClient";
|
|
35
70
|
/**
|
|
71
|
+
* Context tag used to access the `PgClient` service.
|
|
72
|
+
*
|
|
36
73
|
* @category tags
|
|
37
|
-
* @since
|
|
74
|
+
* @since 4.0.0
|
|
38
75
|
*/
|
|
39
|
-
export const PgClient = /*#__PURE__*/
|
|
76
|
+
export const PgClient = /*#__PURE__*/Context.Service("@effect/sql-pg/PgClient");
|
|
40
77
|
/**
|
|
78
|
+
* Creates a scoped PostgreSQL client backed by a managed `pg` connection pool.
|
|
79
|
+
*
|
|
41
80
|
* @category constructors
|
|
42
|
-
* @since
|
|
81
|
+
* @since 4.0.0
|
|
43
82
|
*/
|
|
44
83
|
export const make = options => fromPool({
|
|
45
84
|
...options,
|
|
@@ -67,193 +106,178 @@ export const make = options => fromPool({
|
|
|
67
106
|
yield* Effect.acquireRelease(Effect.tryPromise({
|
|
68
107
|
try: () => pool.query("SELECT 1"),
|
|
69
108
|
catch: cause => new SqlError({
|
|
70
|
-
cause,
|
|
71
|
-
message: "PgClient: Failed to connect"
|
|
109
|
+
reason: classifyError(cause, "PgClient: Failed to connect", "connect")
|
|
72
110
|
})
|
|
73
|
-
}), () => Effect.promise(() => pool.end()).pipe(Effect.timeoutOption(1000))
|
|
111
|
+
}), () => Effect.promise(() => pool.end()).pipe(Effect.timeoutOption(1000)), {
|
|
112
|
+
interruptible: true
|
|
113
|
+
}).pipe(Effect.timeoutOrElse({
|
|
74
114
|
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
orElse: () => Effect.fail(new SqlError({
|
|
116
|
+
reason: new ConnectionError({
|
|
117
|
+
cause: new Error("Connection timed out"),
|
|
118
|
+
message: "PgClient: Connection timed out",
|
|
119
|
+
operation: "connect"
|
|
120
|
+
})
|
|
78
121
|
}))
|
|
79
122
|
}));
|
|
80
123
|
return pool;
|
|
81
124
|
})
|
|
82
125
|
});
|
|
83
126
|
/**
|
|
127
|
+
* Creates a scoped PostgreSQL client backed by a managed single `pg` client, optionally acquiring a separate client for streaming and LISTEN operations.
|
|
128
|
+
*
|
|
84
129
|
* @category constructors
|
|
85
|
-
* @since
|
|
130
|
+
* @since 4.0.0
|
|
131
|
+
*/
|
|
132
|
+
export const makeClient = options => fromClient({
|
|
133
|
+
...options,
|
|
134
|
+
acquire: Effect.gen(function* () {
|
|
135
|
+
const client = new Pg.Client({
|
|
136
|
+
connectionString: options.url ? Redacted.value(options.url) : undefined,
|
|
137
|
+
user: options.username,
|
|
138
|
+
host: options.host,
|
|
139
|
+
database: options.database,
|
|
140
|
+
password: options.password ? Redacted.value(options.password) : undefined,
|
|
141
|
+
ssl: options.ssl,
|
|
142
|
+
port: options.port,
|
|
143
|
+
...(options.stream ? {
|
|
144
|
+
stream: options.stream
|
|
145
|
+
} : {}),
|
|
146
|
+
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
147
|
+
types: options.types
|
|
148
|
+
});
|
|
149
|
+
yield* Effect.acquireRelease(Effect.tryPromise({
|
|
150
|
+
try: () => client.query("SELECT 1"),
|
|
151
|
+
catch: cause => new SqlError({
|
|
152
|
+
reason: classifyError(cause, "PgClient: Failed to connect", "connect")
|
|
153
|
+
})
|
|
154
|
+
}), () => Effect.promise(() => client.end()).pipe(Effect.timeoutOption(1000)), {
|
|
155
|
+
interruptible: true
|
|
156
|
+
}).pipe(Effect.timeoutOrElse({
|
|
157
|
+
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
158
|
+
orElse: () => Effect.fail(new SqlError({
|
|
159
|
+
reason: new ConnectionError({
|
|
160
|
+
cause: new Error("Connection timed out"),
|
|
161
|
+
message: "PgClient: Connection timed out",
|
|
162
|
+
operation: "connect"
|
|
163
|
+
})
|
|
164
|
+
}))
|
|
165
|
+
}));
|
|
166
|
+
return client;
|
|
167
|
+
}),
|
|
168
|
+
acquireForStream: options.acquireForStream ?? false
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* Builds a PostgreSQL client from a scoped `pg` pool acquisition effect, deriving transaction, streaming, and LISTEN/NOTIFY support from that pool.
|
|
172
|
+
*
|
|
173
|
+
* @category constructors
|
|
174
|
+
* @since 4.0.0
|
|
86
175
|
*/
|
|
87
176
|
export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
88
|
-
const compiler = makeCompiler(options.transformQueryNames, options.transformJson);
|
|
89
|
-
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames, options.transformJson).array : undefined;
|
|
90
177
|
const pool = yield* options.acquire;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
178
|
+
const makeConection = client => new ConnectionImpl(function runWithClient(f) {
|
|
179
|
+
if (client !== undefined) {
|
|
180
|
+
return Effect.callback(resume => {
|
|
181
|
+
f(client, resume);
|
|
182
|
+
return makeCancel(pool, client);
|
|
183
|
+
});
|
|
95
184
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
185
|
+
return Effect.callback(resume => {
|
|
186
|
+
let done = false;
|
|
187
|
+
let cancel = undefined;
|
|
188
|
+
let client = undefined;
|
|
189
|
+
function onError(cause) {
|
|
190
|
+
cleanup(cause);
|
|
191
|
+
resume(Effect.fail(new SqlError({
|
|
192
|
+
reason: classifyError(cause, "Connection error", "acquireConnection")
|
|
193
|
+
})));
|
|
102
194
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
195
|
+
function cleanup(cause) {
|
|
196
|
+
if (!done) client?.release(cause);
|
|
197
|
+
done = true;
|
|
198
|
+
client?.off("error", onError);
|
|
199
|
+
}
|
|
200
|
+
pool.connect((cause, client_) => {
|
|
201
|
+
if (cause) {
|
|
202
|
+
return resume(Effect.fail(new SqlError({
|
|
203
|
+
reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
|
|
112
204
|
})));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
done = true;
|
|
117
|
-
client?.off("error", onError);
|
|
118
|
-
}
|
|
119
|
-
pool.connect((cause, client_) => {
|
|
120
|
-
if (cause) {
|
|
121
|
-
return resume(Effect.fail(new SqlError({
|
|
122
|
-
cause,
|
|
123
|
-
message: "Failed to acquire connection"
|
|
124
|
-
})));
|
|
125
|
-
} else if (!client_) {
|
|
126
|
-
return resume(Effect.fail(new SqlError({
|
|
205
|
+
} else if (!client_) {
|
|
206
|
+
return resume(Effect.fail(new SqlError({
|
|
207
|
+
reason: new ConnectionError({
|
|
127
208
|
message: "Failed to acquire connection",
|
|
128
|
-
cause: new Error("No client returned")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return Effect.suspend(() => {
|
|
143
|
-
if (!cancel) {
|
|
144
|
-
cleanup();
|
|
145
|
-
return Effect.void;
|
|
146
|
-
}
|
|
147
|
-
return Effect.ensuring(cancel, Effect.sync(cleanup));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
run(query, params) {
|
|
152
|
-
return this.runWithClient((client, resume) => {
|
|
153
|
-
client.query(query, params, (err, result) => {
|
|
154
|
-
if (err) {
|
|
155
|
-
resume(Effect.fail(new SqlError({
|
|
156
|
-
cause: err,
|
|
157
|
-
message: "Failed to execute statement"
|
|
158
|
-
})));
|
|
159
|
-
} else {
|
|
160
|
-
// Multi-statement queries return an array of results
|
|
161
|
-
resume(Effect.succeed(Array.isArray(result) ? result.map(r => r.rows ?? []) : result.rows ?? []));
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
execute(sql, params, transformRows) {
|
|
167
|
-
return transformRows ? Effect.map(this.run(sql, params), transformRows) : this.run(sql, params);
|
|
168
|
-
}
|
|
169
|
-
executeRaw(sql, params) {
|
|
170
|
-
return this.runWithClient((client, resume) => {
|
|
171
|
-
client.query(sql, params, (err, result) => {
|
|
172
|
-
if (err) {
|
|
173
|
-
resume(Effect.fail(new SqlError({
|
|
174
|
-
cause: err,
|
|
175
|
-
message: "Failed to execute statement"
|
|
176
|
-
})));
|
|
177
|
-
} else {
|
|
178
|
-
resume(Effect.succeed(result));
|
|
179
|
-
}
|
|
209
|
+
cause: new Error("No client returned"),
|
|
210
|
+
operation: "acquireConnection"
|
|
211
|
+
})
|
|
212
|
+
})));
|
|
213
|
+
} else if (done) {
|
|
214
|
+
client_.release();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
client = client_;
|
|
218
|
+
client.once("error", onError);
|
|
219
|
+
cancel = makeCancel(pool, client);
|
|
220
|
+
f(client, eff => {
|
|
221
|
+
cleanup();
|
|
222
|
+
resume(eff);
|
|
180
223
|
});
|
|
181
224
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
client.query({
|
|
189
|
-
text: sql,
|
|
190
|
-
rowMode: "array",
|
|
191
|
-
values: params
|
|
192
|
-
}, (err, result) => {
|
|
193
|
-
if (err) {
|
|
194
|
-
resume(Effect.fail(new SqlError({
|
|
195
|
-
cause: err,
|
|
196
|
-
message: "Failed to execute statement"
|
|
197
|
-
})));
|
|
198
|
-
} else {
|
|
199
|
-
resume(Effect.succeed(result.rows));
|
|
200
|
-
}
|
|
201
|
-
});
|
|
225
|
+
return Effect.suspend(() => {
|
|
226
|
+
if (!cancel) {
|
|
227
|
+
cleanup();
|
|
228
|
+
return Effect.void;
|
|
229
|
+
}
|
|
230
|
+
return Effect.ensuring(cancel, Effect.sync(cleanup));
|
|
202
231
|
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return this.execute(sql, params, transformRows);
|
|
206
|
-
}
|
|
207
|
-
executeStream(sql, params, transformRows) {
|
|
208
|
-
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
209
|
-
const self = this;
|
|
210
|
-
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function* (_, scope) {
|
|
211
|
-
const client = self.pg ?? (yield* Scope.provide(reserveRaw, scope));
|
|
212
|
-
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()));
|
|
213
|
-
const cursor = client.query(new Cursor(sql, params));
|
|
214
|
-
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
215
|
-
return Effect.callback(resume => {
|
|
216
|
-
cursor.read(128, (err, rows) => {
|
|
217
|
-
if (err) {
|
|
218
|
-
resume(Effect.fail(new SqlError({
|
|
219
|
-
cause: err,
|
|
220
|
-
message: "Failed to execute statement"
|
|
221
|
-
})));
|
|
222
|
-
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
223
|
-
resume(Effect.succeed(transformRows ? transformRows(rows) : rows));
|
|
224
|
-
} else {
|
|
225
|
-
resume(Cause.done());
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
})));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
+
});
|
|
233
|
+
}, client ? Effect.succeed(client) : reserveRaw);
|
|
232
234
|
const reserveRaw = Effect.callback(resume => {
|
|
233
235
|
const fiber = Fiber.getCurrent();
|
|
234
|
-
const scope =
|
|
236
|
+
const scope = Context.getUnsafe(fiber.context, Scope.Scope);
|
|
235
237
|
let cause = undefined;
|
|
238
|
+
function onError(cause_) {
|
|
239
|
+
cause = cause_;
|
|
240
|
+
}
|
|
236
241
|
pool.connect((err, client, release) => {
|
|
237
242
|
if (err) {
|
|
238
|
-
resume(Effect.fail(new SqlError({
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
return resume(Effect.fail(new SqlError({
|
|
244
|
+
reason: classifyError(err, "Failed to acquire connection for transaction", "acquireConnection")
|
|
245
|
+
})));
|
|
246
|
+
} else if (!client) {
|
|
247
|
+
return resume(Effect.fail(new SqlError({
|
|
248
|
+
reason: new ConnectionError({
|
|
249
|
+
message: "Failed to acquire connection for transaction",
|
|
250
|
+
cause: new Error("No client returned"),
|
|
251
|
+
operation: "acquireConnection"
|
|
252
|
+
})
|
|
241
253
|
})));
|
|
242
|
-
} else {
|
|
243
|
-
resume(Effect.as(Scope.addFinalizer(scope, Effect.sync(() => {
|
|
244
|
-
client.off("error", onError);
|
|
245
|
-
release(cause);
|
|
246
|
-
})), client));
|
|
247
|
-
}
|
|
248
|
-
function onError(cause_) {
|
|
249
|
-
cause = cause_;
|
|
250
254
|
}
|
|
251
255
|
client.on("error", onError);
|
|
256
|
+
resume(Effect.as(Scope.addFinalizer(scope, Effect.sync(() => {
|
|
257
|
+
client.off("error", onError);
|
|
258
|
+
release(cause);
|
|
259
|
+
})), client));
|
|
252
260
|
});
|
|
253
261
|
});
|
|
254
|
-
const reserve = Effect.map(reserveRaw,
|
|
255
|
-
const
|
|
256
|
-
|
|
262
|
+
const reserve = Effect.map(reserveRaw, makeConection);
|
|
263
|
+
const onListenClientError = _ => {};
|
|
264
|
+
const listenAcquirer = yield* RcRef.make({
|
|
265
|
+
acquire: Effect.acquireRelease(Effect.tryPromise({
|
|
266
|
+
try: async () => {
|
|
267
|
+
const client = new Pg.Client(pool.options);
|
|
268
|
+
await client.connect();
|
|
269
|
+
client.on("error", onListenClientError);
|
|
270
|
+
return client;
|
|
271
|
+
},
|
|
272
|
+
catch: cause => new SqlError({
|
|
273
|
+
reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
|
|
274
|
+
})
|
|
275
|
+
}), client => Effect.promise(() => {
|
|
276
|
+
client.off("error", onListenClientError);
|
|
277
|
+
return client.end();
|
|
278
|
+
}).pipe(Effect.timeoutOption(1000)), {
|
|
279
|
+
interruptible: true
|
|
280
|
+
})
|
|
257
281
|
});
|
|
258
282
|
let config = {
|
|
259
283
|
url: pool.options.connectionString ? Redacted.make(pool.options.connectionString) : undefined,
|
|
@@ -273,7 +297,7 @@ export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
|
273
297
|
config = {
|
|
274
298
|
...config,
|
|
275
299
|
host: config.host ?? parsed.host ?? undefined,
|
|
276
|
-
port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
|
|
300
|
+
port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
|
|
277
301
|
username: config.username ?? parsed.user ?? undefined,
|
|
278
302
|
password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
|
|
279
303
|
database: config.database ?? parsed.database ?? undefined
|
|
@@ -282,18 +306,84 @@ export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
|
282
306
|
//
|
|
283
307
|
}
|
|
284
308
|
}
|
|
285
|
-
return
|
|
286
|
-
acquirer: Effect.succeed(
|
|
309
|
+
return yield* makeWith({
|
|
310
|
+
acquirer: Effect.succeed(makeConection()),
|
|
287
311
|
transactionAcquirer: reserve,
|
|
312
|
+
listenAcquirer: RcRef.get(listenAcquirer),
|
|
313
|
+
config,
|
|
314
|
+
spanAttributes: options.spanAttributes,
|
|
315
|
+
transformResultNames: options.transformResultNames,
|
|
316
|
+
transformQueryNames: options.transformQueryNames,
|
|
317
|
+
transformJson: options.transformJson
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
/**
|
|
321
|
+
* Builds a PostgreSQL client from a scoped `pg` client acquisition effect, serializing access when sharing the client and optionally using separate clients for streams and LISTEN.
|
|
322
|
+
*
|
|
323
|
+
* @category constructors
|
|
324
|
+
* @since 4.0.0
|
|
325
|
+
*/
|
|
326
|
+
export const fromClient = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
327
|
+
function onError() {}
|
|
328
|
+
const acquireWithErrorHandler = options.acquire.pipe(Effect.tap(client => {
|
|
329
|
+
client.on("error", onError);
|
|
330
|
+
return Effect.addFinalizer(() => {
|
|
331
|
+
client.off("error", onError);
|
|
332
|
+
return Effect.void;
|
|
333
|
+
});
|
|
334
|
+
}));
|
|
335
|
+
const client = yield* acquireWithErrorHandler;
|
|
336
|
+
const semaphore = Semaphore.makeUnsafe(1);
|
|
337
|
+
let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(Effect.as(semaphore.take(1), client), () => semaphore.release(1));
|
|
338
|
+
const makeConection = client => new ConnectionImpl(function runWithClient(f) {
|
|
339
|
+
return Effect.callback(resume => {
|
|
340
|
+
f(client, resume);
|
|
341
|
+
});
|
|
342
|
+
}, streamClient);
|
|
343
|
+
const connection = makeConection(client);
|
|
344
|
+
const acquirer = semaphore.withPermit(Effect.succeed(connection));
|
|
345
|
+
const config = {
|
|
346
|
+
...options,
|
|
347
|
+
host: client.host,
|
|
348
|
+
port: client.port,
|
|
349
|
+
database: client.database,
|
|
350
|
+
username: client.user,
|
|
351
|
+
password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
|
|
352
|
+
ssl: client.ssl
|
|
353
|
+
};
|
|
354
|
+
return yield* makeWith({
|
|
355
|
+
acquirer,
|
|
356
|
+
transactionAcquirer: acquirer,
|
|
357
|
+
listenAcquirer: streamClient,
|
|
358
|
+
config,
|
|
359
|
+
spanAttributes: options.spanAttributes,
|
|
360
|
+
transformResultNames: options.transformResultNames,
|
|
361
|
+
transformQueryNames: options.transformQueryNames,
|
|
362
|
+
transformJson: options.transformJson
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
/**
|
|
366
|
+
* Low-level constructor for a `PgClient` from SQL connection acquirers, a LISTEN acquirer, client configuration, and transformation options.
|
|
367
|
+
*
|
|
368
|
+
* @category constructors
|
|
369
|
+
* @since 4.0.0
|
|
370
|
+
*/
|
|
371
|
+
export const makeWith = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
372
|
+
const compiler = makeCompiler(options.transformQueryNames, options.transformJson);
|
|
373
|
+
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames, options.transformJson).array : undefined;
|
|
374
|
+
const config = options.config;
|
|
375
|
+
return Object.assign(yield* Client.make({
|
|
376
|
+
acquirer: options.acquirer,
|
|
377
|
+
transactionAcquirer: options.transactionAcquirer,
|
|
288
378
|
compiler,
|
|
289
379
|
spanAttributes: [...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), [ATTR_DB_SYSTEM_NAME, "postgresql"], [ATTR_DB_NAMESPACE, config.database ?? config.username ?? "postgres"], [ATTR_SERVER_ADDRESS, config.host ?? "localhost"], [ATTR_SERVER_PORT, config.port ?? 5432]],
|
|
290
380
|
transformRows
|
|
291
381
|
}), {
|
|
292
382
|
[TypeId]: TypeId,
|
|
293
|
-
config,
|
|
383
|
+
config: options.config,
|
|
294
384
|
json: _ => Statement.fragment([PgJson(_)]),
|
|
295
385
|
listen: channel => Stream.callback(Effect.fnUntraced(function* (queue) {
|
|
296
|
-
const client = yield*
|
|
386
|
+
const client = yield* options.listenAcquirer;
|
|
297
387
|
function onNotification(msg) {
|
|
298
388
|
if (msg.channel === channel && msg.payload) {
|
|
299
389
|
Queue.offerUnsafe(queue, msg.payload);
|
|
@@ -306,26 +396,98 @@ export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
|
306
396
|
yield* Effect.tryPromise({
|
|
307
397
|
try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
|
|
308
398
|
catch: cause => new SqlError({
|
|
309
|
-
cause,
|
|
310
|
-
message: "Failed to listen"
|
|
399
|
+
reason: classifyError(cause, "Failed to listen", "listen")
|
|
311
400
|
})
|
|
312
401
|
});
|
|
313
402
|
client.on("notification", onNotification);
|
|
314
403
|
})),
|
|
315
|
-
notify: (channel, payload) => Effect.
|
|
316
|
-
|
|
404
|
+
notify: (channel, payload) => Effect.asVoid(Effect.scoped(Effect.flatMap(options.acquirer, conn => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload]))))
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
class ConnectionImpl {
|
|
408
|
+
constructor(runWithClient, reserve) {
|
|
409
|
+
this.runWithClient = runWithClient;
|
|
410
|
+
this.reserve = reserve;
|
|
411
|
+
}
|
|
412
|
+
runWithClient;
|
|
413
|
+
reserve;
|
|
414
|
+
run(query, params) {
|
|
415
|
+
return this.runWithClient((client, resume) => {
|
|
416
|
+
client.query(query, params, (err, result) => {
|
|
317
417
|
if (err) {
|
|
318
418
|
resume(Effect.fail(new SqlError({
|
|
319
|
-
|
|
320
|
-
message: "Failed to notify"
|
|
419
|
+
reason: classifyError(err, "Failed to execute statement", "execute")
|
|
321
420
|
})));
|
|
322
421
|
} else {
|
|
323
|
-
|
|
422
|
+
// Multi-statement queries return an array of results
|
|
423
|
+
resume(Effect.succeed(Array.isArray(result) ? result.map(r => r.rows ?? []) : result.rows ?? []));
|
|
324
424
|
}
|
|
325
425
|
});
|
|
326
|
-
})
|
|
327
|
-
}
|
|
328
|
-
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
execute(sql, params, transformRows) {
|
|
429
|
+
return transformRows ? Effect.map(this.run(sql, params), transformRows) : this.run(sql, params);
|
|
430
|
+
}
|
|
431
|
+
executeRaw(sql, params) {
|
|
432
|
+
return this.runWithClient((client, resume) => {
|
|
433
|
+
client.query(sql, params, (err, result) => {
|
|
434
|
+
if (err) {
|
|
435
|
+
resume(Effect.fail(new SqlError({
|
|
436
|
+
reason: classifyError(err, "Failed to execute statement", "execute")
|
|
437
|
+
})));
|
|
438
|
+
} else {
|
|
439
|
+
resume(Effect.succeed(result));
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
executeWithoutTransform(sql, params) {
|
|
445
|
+
return this.run(sql, params);
|
|
446
|
+
}
|
|
447
|
+
executeValues(sql, params) {
|
|
448
|
+
return this.runWithClient((client, resume) => {
|
|
449
|
+
client.query({
|
|
450
|
+
text: sql,
|
|
451
|
+
rowMode: "array",
|
|
452
|
+
values: params
|
|
453
|
+
}, (err, result) => {
|
|
454
|
+
if (err) {
|
|
455
|
+
resume(Effect.fail(new SqlError({
|
|
456
|
+
reason: classifyError(err, "Failed to execute statement", "execute")
|
|
457
|
+
})));
|
|
458
|
+
} else {
|
|
459
|
+
resume(Effect.succeed(result.rows));
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
executeUnprepared(sql, params, transformRows) {
|
|
465
|
+
return this.execute(sql, params, transformRows);
|
|
466
|
+
}
|
|
467
|
+
executeStream(sql, params, transformRows) {
|
|
468
|
+
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
469
|
+
const self = this;
|
|
470
|
+
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function* (_, scope) {
|
|
471
|
+
const client = yield* Scope.provide(self.reserve, scope);
|
|
472
|
+
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()));
|
|
473
|
+
const cursor = client.query(new Cursor(sql, params));
|
|
474
|
+
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
475
|
+
return Effect.callback(resume => {
|
|
476
|
+
cursor.read(128, (err, rows) => {
|
|
477
|
+
if (err) {
|
|
478
|
+
resume(Effect.fail(new SqlError({
|
|
479
|
+
reason: classifyError(err, "Failed to execute statement", "stream")
|
|
480
|
+
})));
|
|
481
|
+
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
482
|
+
resume(Effect.succeed(transformRows ? transformRows(rows) : rows));
|
|
483
|
+
} else {
|
|
484
|
+
resume(Cause.done());
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
})));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
329
491
|
const cancelEffects = /*#__PURE__*/new WeakMap();
|
|
330
492
|
const makeCancel = (pool, client) => {
|
|
331
493
|
if (cancelEffects.has(client)) {
|
|
@@ -344,23 +506,31 @@ const makeCancel = (pool, client) => {
|
|
|
344
506
|
return eff;
|
|
345
507
|
};
|
|
346
508
|
/**
|
|
509
|
+
* Creates a layer from an effect that acquires a `PgClient`, providing both `PgClient` and `SqlClient`.
|
|
510
|
+
*
|
|
347
511
|
* @category layers
|
|
348
|
-
* @since
|
|
512
|
+
* @since 4.0.0
|
|
349
513
|
*/
|
|
350
|
-
export const
|
|
514
|
+
export const layerFrom = acquire => Layer.effectContext(Effect.map(acquire, client => Context.make(PgClient, client).pipe(Context.add(Client.SqlClient, client)))).pipe(Layer.provide(Reactivity.layer));
|
|
351
515
|
/**
|
|
516
|
+
* Creates a layer from a `Config`-wrapped PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
|
|
517
|
+
*
|
|
352
518
|
* @category layers
|
|
353
|
-
* @since
|
|
519
|
+
* @since 4.0.0
|
|
354
520
|
*/
|
|
355
|
-
export const
|
|
521
|
+
export const layerConfig = config => layerFrom(Effect.flatMap(Config.unwrap(config), make));
|
|
356
522
|
/**
|
|
523
|
+
* Creates a layer from a concrete PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
|
|
524
|
+
*
|
|
357
525
|
* @category layers
|
|
358
|
-
* @since
|
|
526
|
+
* @since 4.0.0
|
|
359
527
|
*/
|
|
360
|
-
export const
|
|
528
|
+
export const layer = config => layerFrom(make(config));
|
|
361
529
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
530
|
+
* Creates the PostgreSQL statement compiler, using `$1` placeholders, double-quoted identifiers, PostgreSQL returning clauses, and optional JSON value transformation.
|
|
531
|
+
*
|
|
532
|
+
* @category constructors
|
|
533
|
+
* @since 4.0.0
|
|
364
534
|
*/
|
|
365
535
|
export const makeCompiler = (transform, transformJson = true) => {
|
|
366
536
|
const transformValue = transformJson && transform ? Statement.defaultTransforms(transform).value : undefined;
|
|
@@ -388,7 +558,73 @@ export const makeCompiler = (transform, transformJson = true) => {
|
|
|
388
558
|
const escape = /*#__PURE__*/Statement.defaultEscape("\"");
|
|
389
559
|
/**
|
|
390
560
|
* @category custom types
|
|
391
|
-
* @since
|
|
561
|
+
* @since 4.0.0
|
|
392
562
|
*/
|
|
393
563
|
const PgJson = /*#__PURE__*/Statement.custom("PgJson");
|
|
564
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name";
|
|
565
|
+
const ATTR_DB_NAMESPACE = "db.namespace";
|
|
566
|
+
const ATTR_SERVER_ADDRESS = "server.address";
|
|
567
|
+
const ATTR_SERVER_PORT = "server.port";
|
|
568
|
+
const pgCodeFromCause = cause => {
|
|
569
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
const code = cause.code;
|
|
573
|
+
return typeof code === "string" ? code : undefined;
|
|
574
|
+
};
|
|
575
|
+
const pgConstraintFromCause = cause => {
|
|
576
|
+
if (typeof cause !== "object" || cause === null || !("constraint" in cause)) {
|
|
577
|
+
return "unknown";
|
|
578
|
+
}
|
|
579
|
+
const constraint = cause.constraint;
|
|
580
|
+
if (typeof constraint !== "string") {
|
|
581
|
+
return "unknown";
|
|
582
|
+
}
|
|
583
|
+
const normalized = constraint.trim();
|
|
584
|
+
return normalized.length === 0 ? "unknown" : normalized;
|
|
585
|
+
};
|
|
586
|
+
const classifyError = (cause, message, operation) => {
|
|
587
|
+
const props = {
|
|
588
|
+
cause,
|
|
589
|
+
message,
|
|
590
|
+
operation
|
|
591
|
+
};
|
|
592
|
+
const code = pgCodeFromCause(cause);
|
|
593
|
+
if (code !== undefined) {
|
|
594
|
+
if (code.startsWith("08")) {
|
|
595
|
+
return new ConnectionError(props);
|
|
596
|
+
}
|
|
597
|
+
if (code.startsWith("28")) {
|
|
598
|
+
return new AuthenticationError(props);
|
|
599
|
+
}
|
|
600
|
+
if (code === "42501") {
|
|
601
|
+
return new AuthorizationError(props);
|
|
602
|
+
}
|
|
603
|
+
if (code.startsWith("42")) {
|
|
604
|
+
return new SqlSyntaxError(props);
|
|
605
|
+
}
|
|
606
|
+
if (code === "23505") {
|
|
607
|
+
return new UniqueViolation({
|
|
608
|
+
...props,
|
|
609
|
+
constraint: pgConstraintFromCause(cause)
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (code.startsWith("23")) {
|
|
613
|
+
return new ConstraintError(props);
|
|
614
|
+
}
|
|
615
|
+
if (code === "40P01") {
|
|
616
|
+
return new DeadlockError(props);
|
|
617
|
+
}
|
|
618
|
+
if (code === "40001") {
|
|
619
|
+
return new SerializationError(props);
|
|
620
|
+
}
|
|
621
|
+
if (code === "55P03") {
|
|
622
|
+
return new LockTimeoutError(props);
|
|
623
|
+
}
|
|
624
|
+
if (code === "57014") {
|
|
625
|
+
return new StatementTimeoutError(props);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return new UnknownError(props);
|
|
629
|
+
};
|
|
394
630
|
//# sourceMappingURL=PgClient.js.map
|