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