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