@effect/sql-pg 4.0.0-beta.38 → 4.0.0-beta.39
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/dist/PgClient.d.ts +57 -17
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +291 -198
- package/dist/PgClient.js.map +1 -1
- package/package.json +3 -3
- package/src/PgClient.ts +444 -285
package/src/PgClient.ts
CHANGED
|
@@ -15,11 +15,13 @@ import * as Queue from "effect/Queue"
|
|
|
15
15
|
import * as RcRef from "effect/RcRef"
|
|
16
16
|
import * as Redacted from "effect/Redacted"
|
|
17
17
|
import * as Scope from "effect/Scope"
|
|
18
|
+
import * as Semaphore from "effect/Semaphore"
|
|
18
19
|
import * as ServiceMap from "effect/ServiceMap"
|
|
19
20
|
import * as Stream from "effect/Stream"
|
|
20
21
|
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
21
22
|
import * as Client from "effect/unstable/sql/SqlClient"
|
|
22
23
|
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
|
24
|
+
import type * as SqlConnection from "effect/unstable/sql/SqlConnection"
|
|
23
25
|
import {
|
|
24
26
|
AuthenticationError,
|
|
25
27
|
AuthorizationError,
|
|
@@ -41,58 +43,6 @@ import * as Pg from "pg"
|
|
|
41
43
|
import * as PgConnString from "pg-connection-string"
|
|
42
44
|
import Cursor from "pg-cursor"
|
|
43
45
|
|
|
44
|
-
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
45
|
-
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
46
|
-
const ATTR_SERVER_ADDRESS = "server.address"
|
|
47
|
-
const ATTR_SERVER_PORT = "server.port"
|
|
48
|
-
|
|
49
|
-
const pgCodeFromCause = (cause: unknown): string | undefined => {
|
|
50
|
-
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
51
|
-
return undefined
|
|
52
|
-
}
|
|
53
|
-
const code = cause.code
|
|
54
|
-
return typeof code === "string" ? code : undefined
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const classifyError = (
|
|
58
|
-
cause: unknown,
|
|
59
|
-
message: string,
|
|
60
|
-
operation: string
|
|
61
|
-
) => {
|
|
62
|
-
const props = { cause, message, operation }
|
|
63
|
-
const code = pgCodeFromCause(cause)
|
|
64
|
-
if (code !== undefined) {
|
|
65
|
-
if (code.startsWith("08")) {
|
|
66
|
-
return new ConnectionError(props)
|
|
67
|
-
}
|
|
68
|
-
if (code.startsWith("28")) {
|
|
69
|
-
return new AuthenticationError(props)
|
|
70
|
-
}
|
|
71
|
-
if (code === "42501") {
|
|
72
|
-
return new AuthorizationError(props)
|
|
73
|
-
}
|
|
74
|
-
if (code.startsWith("42")) {
|
|
75
|
-
return new SqlSyntaxError(props)
|
|
76
|
-
}
|
|
77
|
-
if (code.startsWith("23")) {
|
|
78
|
-
return new ConstraintError(props)
|
|
79
|
-
}
|
|
80
|
-
if (code === "40P01") {
|
|
81
|
-
return new DeadlockError(props)
|
|
82
|
-
}
|
|
83
|
-
if (code === "40001") {
|
|
84
|
-
return new SerializationError(props)
|
|
85
|
-
}
|
|
86
|
-
if (code === "55P03") {
|
|
87
|
-
return new LockTimeoutError(props)
|
|
88
|
-
}
|
|
89
|
-
if (code === "57014") {
|
|
90
|
-
return new StatementTimeoutError(props)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return new UnknownError(props)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
46
|
/**
|
|
97
47
|
* @category type ids
|
|
98
48
|
* @since 1.0.0
|
|
@@ -138,14 +88,9 @@ export interface PgClientConfig {
|
|
|
138
88
|
readonly username?: string | undefined
|
|
139
89
|
readonly password?: Redacted.Redacted | undefined
|
|
140
90
|
|
|
141
|
-
readonly stream?: (() => Duplex) | undefined
|
|
142
|
-
|
|
143
|
-
readonly idleTimeout?: Duration.Input | undefined
|
|
144
91
|
readonly connectTimeout?: Duration.Input | undefined
|
|
145
92
|
|
|
146
|
-
readonly
|
|
147
|
-
readonly minConnections?: number | undefined
|
|
148
|
-
readonly connectionTTL?: Duration.Input | undefined
|
|
93
|
+
readonly stream?: (() => Duplex) | undefined
|
|
149
94
|
|
|
150
95
|
readonly applicationName?: string | undefined
|
|
151
96
|
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
@@ -160,9 +105,19 @@ export interface PgClientConfig {
|
|
|
160
105
|
* @category constructors
|
|
161
106
|
* @since 1.0.0
|
|
162
107
|
*/
|
|
163
|
-
export
|
|
164
|
-
|
|
165
|
-
|
|
108
|
+
export interface PgPoolConfig extends PgClientConfig {
|
|
109
|
+
readonly idleTimeout?: Duration.Input | undefined
|
|
110
|
+
|
|
111
|
+
readonly maxConnections?: number | undefined
|
|
112
|
+
readonly minConnections?: number | undefined
|
|
113
|
+
readonly connectionTTL?: Duration.Input | undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @category constructors
|
|
118
|
+
* @since 1.0.0
|
|
119
|
+
*/
|
|
120
|
+
export const make = (options: PgPoolConfig): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
166
121
|
fromPool({
|
|
167
122
|
...options,
|
|
168
123
|
acquire: Effect.gen(function*() {
|
|
@@ -221,6 +176,63 @@ export const make = (
|
|
|
221
176
|
})
|
|
222
177
|
})
|
|
223
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @category constructors
|
|
181
|
+
* @since 1.0.0
|
|
182
|
+
*/
|
|
183
|
+
export const makeClient = (
|
|
184
|
+
options: PgClientConfig & {
|
|
185
|
+
/**
|
|
186
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen
|
|
187
|
+
*/
|
|
188
|
+
readonly acquireForStream?: boolean | undefined
|
|
189
|
+
}
|
|
190
|
+
): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
191
|
+
fromClient({
|
|
192
|
+
...options,
|
|
193
|
+
acquire: Effect.gen(function*() {
|
|
194
|
+
const client = new Pg.Client({
|
|
195
|
+
connectionString: options.url ? Redacted.value(options.url) : undefined,
|
|
196
|
+
user: options.username,
|
|
197
|
+
host: options.host,
|
|
198
|
+
database: options.database,
|
|
199
|
+
password: options.password ? Redacted.value(options.password) : undefined,
|
|
200
|
+
ssl: options.ssl,
|
|
201
|
+
port: options.port,
|
|
202
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
203
|
+
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
204
|
+
types: options.types
|
|
205
|
+
})
|
|
206
|
+
yield* Effect.acquireRelease(
|
|
207
|
+
Effect.tryPromise({
|
|
208
|
+
try: () => client.query("SELECT 1"),
|
|
209
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
210
|
+
}),
|
|
211
|
+
() =>
|
|
212
|
+
Effect.promise(() => client.end()).pipe(
|
|
213
|
+
Effect.timeoutOption(1000)
|
|
214
|
+
)
|
|
215
|
+
).pipe(
|
|
216
|
+
Effect.timeoutOrElse({
|
|
217
|
+
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
218
|
+
onTimeout: () =>
|
|
219
|
+
Effect.fail(
|
|
220
|
+
new SqlError({
|
|
221
|
+
reason: new ConnectionError({
|
|
222
|
+
cause: new Error("Connection timed out"),
|
|
223
|
+
message: "PgClient: Connection timed out",
|
|
224
|
+
operation: "connect"
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return client
|
|
232
|
+
}),
|
|
233
|
+
acquireForStream: options.acquireForStream ?? false
|
|
234
|
+
})
|
|
235
|
+
|
|
224
236
|
/**
|
|
225
237
|
* @category constructors
|
|
226
238
|
* @since 1.0.0
|
|
@@ -238,185 +250,74 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
238
250
|
readonly types?: Pg.CustomTypesConfig | undefined
|
|
239
251
|
}
|
|
240
252
|
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
241
|
-
const compiler = makeCompiler(
|
|
242
|
-
options.transformQueryNames,
|
|
243
|
-
options.transformJson
|
|
244
|
-
)
|
|
245
|
-
const transformRows = options.transformResultNames ?
|
|
246
|
-
Statement.defaultTransforms(
|
|
247
|
-
options.transformResultNames,
|
|
248
|
-
options.transformJson
|
|
249
|
-
).array :
|
|
250
|
-
undefined
|
|
251
|
-
|
|
252
253
|
const pool = yield* options.acquire
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (this.pg !== undefined) {
|
|
262
|
-
return Effect.callback<A, SqlError>((resume) => {
|
|
263
|
-
f(this.pg!, resume)
|
|
264
|
-
return makeCancel(pool, this.pg!)
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
return Effect.callback<A, SqlError>((resume) => {
|
|
268
|
-
let done = false
|
|
269
|
-
let cancel: Effect.Effect<void> | undefined = undefined
|
|
270
|
-
let client: Pg.PoolClient | undefined = undefined
|
|
271
|
-
function onError(cause: Error) {
|
|
272
|
-
cleanup(cause)
|
|
273
|
-
resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
|
|
274
|
-
}
|
|
275
|
-
function cleanup(cause?: Error) {
|
|
276
|
-
if (!done) client?.release(cause)
|
|
277
|
-
done = true
|
|
278
|
-
client?.off("error", onError)
|
|
279
|
-
}
|
|
280
|
-
pool.connect((cause, client_) => {
|
|
281
|
-
if (cause) {
|
|
282
|
-
return resume(
|
|
283
|
-
Effect.fail(
|
|
284
|
-
new SqlError({
|
|
285
|
-
reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
|
|
286
|
-
})
|
|
287
|
-
)
|
|
288
|
-
)
|
|
289
|
-
} else if (!client_) {
|
|
290
|
-
return resume(
|
|
291
|
-
Effect.fail(
|
|
292
|
-
new SqlError({
|
|
293
|
-
reason: new ConnectionError({
|
|
294
|
-
message: "Failed to acquire connection",
|
|
295
|
-
cause: new Error("No client returned"),
|
|
296
|
-
operation: "acquireConnection"
|
|
297
|
-
})
|
|
298
|
-
})
|
|
299
|
-
)
|
|
300
|
-
)
|
|
301
|
-
} else if (done) {
|
|
302
|
-
client_.release()
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
client = client_
|
|
306
|
-
client.once("error", onError)
|
|
307
|
-
cancel = makeCancel(pool, client)
|
|
308
|
-
f(client, (eff) => {
|
|
309
|
-
cleanup()
|
|
310
|
-
resume(eff)
|
|
255
|
+
const makeConection = (client?: Pg.PoolClient) =>
|
|
256
|
+
new ConnectionImpl(
|
|
257
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
258
|
+
if (client !== undefined) {
|
|
259
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
260
|
+
f(client!, resume)
|
|
261
|
+
return makeCancel(pool, client!)
|
|
311
262
|
})
|
|
312
|
-
}
|
|
313
|
-
return Effect.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
263
|
+
}
|
|
264
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
265
|
+
let done = false
|
|
266
|
+
let cancel: Effect.Effect<void> | undefined = undefined
|
|
267
|
+
let client: Pg.PoolClient | undefined = undefined
|
|
268
|
+
function onError(cause: Error) {
|
|
269
|
+
cleanup(cause)
|
|
270
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
|
|
317
271
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
324
|
-
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
325
|
-
client.query(query, params as any, (err, result) => {
|
|
326
|
-
if (err) {
|
|
327
|
-
resume(
|
|
328
|
-
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
329
|
-
)
|
|
330
|
-
} else {
|
|
331
|
-
// Multi-statement queries return an array of results
|
|
332
|
-
resume(Effect.succeed(
|
|
333
|
-
Array.isArray(result)
|
|
334
|
-
? result.map((r) => r.rows ?? [])
|
|
335
|
-
: result.rows ?? []
|
|
336
|
-
))
|
|
272
|
+
function cleanup(cause?: Error) {
|
|
273
|
+
if (!done) client?.release(cause)
|
|
274
|
+
done = true
|
|
275
|
+
client?.off("error", onError)
|
|
337
276
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
) {
|
|
347
|
-
return transformRows
|
|
348
|
-
? Effect.map(this.run(sql, params), transformRows)
|
|
349
|
-
: this.run(sql, params)
|
|
350
|
-
}
|
|
351
|
-
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
352
|
-
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
353
|
-
client.query(sql, params as any, (err, result) => {
|
|
354
|
-
if (err) {
|
|
355
|
-
resume(
|
|
356
|
-
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
357
|
-
)
|
|
358
|
-
} else {
|
|
359
|
-
resume(Effect.succeed(result))
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
})
|
|
363
|
-
}
|
|
364
|
-
executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
|
|
365
|
-
return this.run(sql, params)
|
|
366
|
-
}
|
|
367
|
-
executeValues(sql: string, params: ReadonlyArray<unknown>) {
|
|
368
|
-
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
369
|
-
client.query(
|
|
370
|
-
{
|
|
371
|
-
text: sql,
|
|
372
|
-
rowMode: "array",
|
|
373
|
-
values: params as Array<string>
|
|
374
|
-
},
|
|
375
|
-
(err, result) => {
|
|
376
|
-
if (err) {
|
|
377
|
-
resume(
|
|
378
|
-
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
277
|
+
pool.connect((cause, client_) => {
|
|
278
|
+
if (cause) {
|
|
279
|
+
return resume(
|
|
280
|
+
Effect.fail(
|
|
281
|
+
new SqlError({
|
|
282
|
+
reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
|
|
283
|
+
})
|
|
284
|
+
)
|
|
379
285
|
)
|
|
380
|
-
} else {
|
|
381
|
-
resume(
|
|
286
|
+
} else if (!client_) {
|
|
287
|
+
return resume(
|
|
288
|
+
Effect.fail(
|
|
289
|
+
new SqlError({
|
|
290
|
+
reason: new ConnectionError({
|
|
291
|
+
message: "Failed to acquire connection",
|
|
292
|
+
cause: new Error("No client returned"),
|
|
293
|
+
operation: "acquireConnection"
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
} else if (done) {
|
|
299
|
+
client_.release()
|
|
300
|
+
return
|
|
382
301
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
sql: string,
|
|
396
|
-
params: ReadonlyArray<unknown>,
|
|
397
|
-
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
398
|
-
) {
|
|
399
|
-
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
400
|
-
const self = this
|
|
401
|
-
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
402
|
-
const client = self.pg ?? (yield* Scope.provide(reserveRaw, scope))
|
|
403
|
-
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
404
|
-
const cursor = client.query(new Cursor(sql, params as any))
|
|
405
|
-
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
406
|
-
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
407
|
-
cursor.read(128, (err, rows) => {
|
|
408
|
-
if (err) {
|
|
409
|
-
resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
|
|
410
|
-
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
411
|
-
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
412
|
-
} else {
|
|
413
|
-
resume(Cause.done())
|
|
302
|
+
client = client_
|
|
303
|
+
client.once("error", onError)
|
|
304
|
+
cancel = makeCancel(pool, client)
|
|
305
|
+
f(client, (eff) => {
|
|
306
|
+
cleanup()
|
|
307
|
+
resume(eff)
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
return Effect.suspend(() => {
|
|
311
|
+
if (!cancel) {
|
|
312
|
+
cleanup()
|
|
313
|
+
return Effect.void
|
|
414
314
|
}
|
|
315
|
+
return Effect.ensuring(cancel, Effect.sync(cleanup))
|
|
415
316
|
})
|
|
416
317
|
})
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
318
|
+
},
|
|
319
|
+
client ? Effect.succeed(client) : reserveRaw
|
|
320
|
+
)
|
|
420
321
|
|
|
421
322
|
const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
|
|
422
323
|
const fiber = Fiber.getCurrent()!
|
|
@@ -453,12 +354,12 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
453
354
|
client!.on("error", onError)
|
|
454
355
|
})
|
|
455
356
|
})
|
|
456
|
-
const reserve = Effect.map(reserveRaw,
|
|
357
|
+
const reserve = Effect.map(reserveRaw, makeConection)
|
|
457
358
|
|
|
458
359
|
const onListenClientError = (_: Error) => {
|
|
459
360
|
}
|
|
460
361
|
|
|
461
|
-
const
|
|
362
|
+
const listenAcquirer = yield* RcRef.make({
|
|
462
363
|
acquire: Effect.acquireRelease(
|
|
463
364
|
Effect.tryPromise({
|
|
464
365
|
try: async () => {
|
|
@@ -510,10 +411,127 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
510
411
|
}
|
|
511
412
|
}
|
|
512
413
|
|
|
414
|
+
return yield* makeWith({
|
|
415
|
+
acquirer: Effect.succeed(makeConection()),
|
|
416
|
+
transactionAcquirer: reserve,
|
|
417
|
+
listenAcquirer: RcRef.get(listenAcquirer),
|
|
418
|
+
config,
|
|
419
|
+
spanAttributes: options.spanAttributes,
|
|
420
|
+
transformResultNames: options.transformResultNames,
|
|
421
|
+
transformQueryNames: options.transformQueryNames,
|
|
422
|
+
transformJson: options.transformJson
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @category constructors
|
|
428
|
+
* @since 1.0.0
|
|
429
|
+
*/
|
|
430
|
+
export const fromClient = Effect.fnUntraced(function*(
|
|
431
|
+
options: {
|
|
432
|
+
readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen.
|
|
436
|
+
*/
|
|
437
|
+
readonly acquireForStream: boolean
|
|
438
|
+
|
|
439
|
+
readonly applicationName?: string | undefined
|
|
440
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
441
|
+
|
|
442
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
443
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
444
|
+
readonly transformJson?: boolean | undefined
|
|
445
|
+
readonly types?: Pg.CustomTypesConfig | undefined
|
|
446
|
+
}
|
|
447
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
448
|
+
function onError() {}
|
|
449
|
+
const acquireWithErrorHandler = options.acquire.pipe(
|
|
450
|
+
Effect.tap((client) => {
|
|
451
|
+
client.on("error", onError)
|
|
452
|
+
return Effect.addFinalizer(() => {
|
|
453
|
+
client.off("error", onError)
|
|
454
|
+
return Effect.void
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
)
|
|
458
|
+
const client = yield* acquireWithErrorHandler
|
|
459
|
+
|
|
460
|
+
const semaphore = Semaphore.makeUnsafe(1)
|
|
461
|
+
let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
|
|
462
|
+
Effect.as(semaphore.take(1), client),
|
|
463
|
+
() => semaphore.release(1)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
const makeConection = (client: Pg.Client) =>
|
|
467
|
+
new ConnectionImpl(
|
|
468
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
469
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
470
|
+
f(client, resume)
|
|
471
|
+
})
|
|
472
|
+
},
|
|
473
|
+
streamClient
|
|
474
|
+
)
|
|
475
|
+
const connection = makeConection(client)
|
|
476
|
+
const acquirer = semaphore.withPermit(Effect.succeed(connection))
|
|
477
|
+
|
|
478
|
+
const config: PgClientConfig = {
|
|
479
|
+
...options,
|
|
480
|
+
host: client.host,
|
|
481
|
+
port: client.port,
|
|
482
|
+
database: client.database,
|
|
483
|
+
username: client.user,
|
|
484
|
+
password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
|
|
485
|
+
ssl: client.ssl
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return yield* makeWith({
|
|
489
|
+
acquirer,
|
|
490
|
+
transactionAcquirer: acquirer,
|
|
491
|
+
listenAcquirer: streamClient,
|
|
492
|
+
config,
|
|
493
|
+
spanAttributes: options.spanAttributes,
|
|
494
|
+
transformResultNames: options.transformResultNames,
|
|
495
|
+
transformQueryNames: options.transformQueryNames,
|
|
496
|
+
transformJson: options.transformJson
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* @category constructors
|
|
502
|
+
* @since 1.0.0
|
|
503
|
+
*/
|
|
504
|
+
export const makeWith = Effect.fnUntraced(function*(
|
|
505
|
+
options: {
|
|
506
|
+
readonly acquirer: SqlConnection.Acquirer
|
|
507
|
+
readonly transactionAcquirer: SqlConnection.Acquirer
|
|
508
|
+
readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
509
|
+
|
|
510
|
+
readonly config: PgClientConfig
|
|
511
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
512
|
+
|
|
513
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
514
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
515
|
+
readonly transformJson?: boolean | undefined
|
|
516
|
+
}
|
|
517
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
518
|
+
const compiler = makeCompiler(
|
|
519
|
+
options.transformQueryNames,
|
|
520
|
+
options.transformJson
|
|
521
|
+
)
|
|
522
|
+
const transformRows = options.transformResultNames ?
|
|
523
|
+
Statement.defaultTransforms(
|
|
524
|
+
options.transformResultNames,
|
|
525
|
+
options.transformJson
|
|
526
|
+
).array :
|
|
527
|
+
undefined
|
|
528
|
+
|
|
529
|
+
const config = options.config
|
|
530
|
+
|
|
513
531
|
return Object.assign(
|
|
514
532
|
yield* Client.make({
|
|
515
|
-
acquirer:
|
|
516
|
-
transactionAcquirer:
|
|
533
|
+
acquirer: options.acquirer,
|
|
534
|
+
transactionAcquirer: options.transactionAcquirer,
|
|
517
535
|
compiler,
|
|
518
536
|
spanAttributes: [
|
|
519
537
|
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
|
@@ -526,11 +544,11 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
526
544
|
}),
|
|
527
545
|
{
|
|
528
546
|
[TypeId]: TypeId as TypeId,
|
|
529
|
-
config,
|
|
547
|
+
config: options.config,
|
|
530
548
|
json: (_: unknown) => Statement.fragment([PgJson(_)]),
|
|
531
549
|
listen: (channel: string) =>
|
|
532
550
|
Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
|
|
533
|
-
const client = yield*
|
|
551
|
+
const client = yield* options.listenAcquirer
|
|
534
552
|
function onNotification(msg: Pg.Notification) {
|
|
535
553
|
if (msg.channel === channel && msg.payload) {
|
|
536
554
|
Queue.offerUnsafe(queue, msg.payload)
|
|
@@ -549,19 +567,128 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
549
567
|
client.on("notification", onNotification)
|
|
550
568
|
})),
|
|
551
569
|
notify: (channel: string, payload: string) =>
|
|
552
|
-
Effect.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
} else {
|
|
557
|
-
resume(Effect.void)
|
|
558
|
-
}
|
|
559
|
-
})
|
|
560
|
-
})
|
|
570
|
+
Effect.asVoid(Effect.scoped(Effect.flatMap(
|
|
571
|
+
options.acquirer,
|
|
572
|
+
(conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
|
|
573
|
+
)))
|
|
561
574
|
}
|
|
562
575
|
)
|
|
563
576
|
})
|
|
564
577
|
|
|
578
|
+
class ConnectionImpl implements Connection {
|
|
579
|
+
constructor(
|
|
580
|
+
runWithClient: <A>(
|
|
581
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
582
|
+
) => Effect.Effect<A, SqlError>,
|
|
583
|
+
reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
584
|
+
) {
|
|
585
|
+
this.runWithClient = runWithClient
|
|
586
|
+
this.reserve = reserve
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private readonly runWithClient: <A>(
|
|
590
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
591
|
+
) => Effect.Effect<A, SqlError>
|
|
592
|
+
private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
593
|
+
|
|
594
|
+
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
595
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
596
|
+
client.query(query, params as any, (err, result) => {
|
|
597
|
+
if (err) {
|
|
598
|
+
resume(
|
|
599
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
600
|
+
)
|
|
601
|
+
} else {
|
|
602
|
+
// Multi-statement queries return an array of results
|
|
603
|
+
resume(Effect.succeed(
|
|
604
|
+
Array.isArray(result)
|
|
605
|
+
? result.map((r) => r.rows ?? [])
|
|
606
|
+
: result.rows ?? []
|
|
607
|
+
))
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
execute(
|
|
614
|
+
sql: string,
|
|
615
|
+
params: ReadonlyArray<unknown>,
|
|
616
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
617
|
+
) {
|
|
618
|
+
return transformRows
|
|
619
|
+
? Effect.map(this.run(sql, params), transformRows)
|
|
620
|
+
: this.run(sql, params)
|
|
621
|
+
}
|
|
622
|
+
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
623
|
+
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
624
|
+
client.query(sql, params as any, (err, result) => {
|
|
625
|
+
if (err) {
|
|
626
|
+
resume(
|
|
627
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
628
|
+
)
|
|
629
|
+
} else {
|
|
630
|
+
resume(Effect.succeed(result))
|
|
631
|
+
}
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
|
|
636
|
+
return this.run(sql, params)
|
|
637
|
+
}
|
|
638
|
+
executeValues(sql: string, params: ReadonlyArray<unknown>) {
|
|
639
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
640
|
+
client.query(
|
|
641
|
+
{
|
|
642
|
+
text: sql,
|
|
643
|
+
rowMode: "array",
|
|
644
|
+
values: params as Array<string>
|
|
645
|
+
},
|
|
646
|
+
(err, result) => {
|
|
647
|
+
if (err) {
|
|
648
|
+
resume(
|
|
649
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
650
|
+
)
|
|
651
|
+
} else {
|
|
652
|
+
resume(Effect.succeed(result.rows))
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
executeUnprepared(
|
|
659
|
+
sql: string,
|
|
660
|
+
params: ReadonlyArray<unknown>,
|
|
661
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
662
|
+
) {
|
|
663
|
+
return this.execute(sql, params, transformRows)
|
|
664
|
+
}
|
|
665
|
+
executeStream(
|
|
666
|
+
sql: string,
|
|
667
|
+
params: ReadonlyArray<unknown>,
|
|
668
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
669
|
+
) {
|
|
670
|
+
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
671
|
+
const self = this
|
|
672
|
+
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
673
|
+
const client = yield* Scope.provide(self.reserve, scope)
|
|
674
|
+
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
675
|
+
const cursor = client.query(new Cursor(sql, params as any))
|
|
676
|
+
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
677
|
+
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
678
|
+
cursor.read(128, (err, rows) => {
|
|
679
|
+
if (err) {
|
|
680
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
|
|
681
|
+
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
682
|
+
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
683
|
+
} else {
|
|
684
|
+
resume(Cause.done())
|
|
685
|
+
}
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
})))
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
565
692
|
const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
|
|
566
693
|
const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
567
694
|
if (cancelEffects.has(client)) {
|
|
@@ -588,57 +715,37 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
|
588
715
|
* @category layers
|
|
589
716
|
* @since 1.0.0
|
|
590
717
|
*/
|
|
591
|
-
export const
|
|
592
|
-
|
|
593
|
-
)
|
|
594
|
-
config: Config.Wrap<PgClientConfig>
|
|
595
|
-
): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
|
|
718
|
+
export const layerFrom = <E, R>(
|
|
719
|
+
acquire: Effect.Effect<PgClient, E, R>
|
|
720
|
+
): Layer.Layer<PgClient | Client.SqlClient, E, Exclude<R, Scope.Scope | Reactivity.Reactivity>> =>
|
|
596
721
|
Layer.effectServices(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
)
|
|
603
|
-
)
|
|
604
|
-
)
|
|
605
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
722
|
+
Effect.map(acquire, (client) =>
|
|
723
|
+
ServiceMap.make(PgClient, client).pipe(
|
|
724
|
+
ServiceMap.add(Client.SqlClient, client)
|
|
725
|
+
))
|
|
726
|
+
).pipe(Layer.provide(Reactivity.layer)) as any
|
|
606
727
|
|
|
607
728
|
/**
|
|
608
729
|
* @category layers
|
|
609
730
|
* @since 1.0.0
|
|
610
731
|
*/
|
|
611
|
-
export const
|
|
612
|
-
config:
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
)
|
|
732
|
+
export const layerConfig: (
|
|
733
|
+
config: Config.Wrap<PgPoolConfig>
|
|
734
|
+
) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
|
|
735
|
+
config: Config.Wrap<PgPoolConfig>
|
|
736
|
+
): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
|
|
737
|
+
layerFrom(Effect.flatMap(
|
|
738
|
+
Config.unwrap(config).asEffect(),
|
|
739
|
+
make
|
|
740
|
+
))
|
|
620
741
|
|
|
621
742
|
/**
|
|
622
743
|
* @category layers
|
|
623
744
|
* @since 1.0.0
|
|
624
745
|
*/
|
|
625
|
-
export const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
readonly applicationName?: string | undefined
|
|
629
|
-
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
630
|
-
|
|
631
|
-
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
632
|
-
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
633
|
-
readonly transformJson?: boolean | undefined
|
|
634
|
-
readonly types?: Pg.CustomTypesConfig | undefined
|
|
635
|
-
}): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
|
|
636
|
-
Layer.effectServices(
|
|
637
|
-
Effect.map(fromPool(options), (client) =>
|
|
638
|
-
ServiceMap.make(PgClient, client).pipe(
|
|
639
|
-
ServiceMap.add(Client.SqlClient, client)
|
|
640
|
-
))
|
|
641
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
746
|
+
export const layer = (
|
|
747
|
+
config: PgPoolConfig
|
|
748
|
+
): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
|
|
642
749
|
|
|
643
750
|
/**
|
|
644
751
|
* @category constructor
|
|
@@ -705,3 +812,55 @@ interface PgJson extends Custom<"PgJson", unknown> {}
|
|
|
705
812
|
* @since 1.0.0
|
|
706
813
|
*/
|
|
707
814
|
const PgJson = Statement.custom<PgJson>("PgJson")
|
|
815
|
+
|
|
816
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
817
|
+
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
818
|
+
const ATTR_SERVER_ADDRESS = "server.address"
|
|
819
|
+
const ATTR_SERVER_PORT = "server.port"
|
|
820
|
+
|
|
821
|
+
const pgCodeFromCause = (cause: unknown): string | undefined => {
|
|
822
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
823
|
+
return undefined
|
|
824
|
+
}
|
|
825
|
+
const code = cause.code
|
|
826
|
+
return typeof code === "string" ? code : undefined
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const classifyError = (
|
|
830
|
+
cause: unknown,
|
|
831
|
+
message: string,
|
|
832
|
+
operation: string
|
|
833
|
+
) => {
|
|
834
|
+
const props = { cause, message, operation }
|
|
835
|
+
const code = pgCodeFromCause(cause)
|
|
836
|
+
if (code !== undefined) {
|
|
837
|
+
if (code.startsWith("08")) {
|
|
838
|
+
return new ConnectionError(props)
|
|
839
|
+
}
|
|
840
|
+
if (code.startsWith("28")) {
|
|
841
|
+
return new AuthenticationError(props)
|
|
842
|
+
}
|
|
843
|
+
if (code === "42501") {
|
|
844
|
+
return new AuthorizationError(props)
|
|
845
|
+
}
|
|
846
|
+
if (code.startsWith("42")) {
|
|
847
|
+
return new SqlSyntaxError(props)
|
|
848
|
+
}
|
|
849
|
+
if (code.startsWith("23")) {
|
|
850
|
+
return new ConstraintError(props)
|
|
851
|
+
}
|
|
852
|
+
if (code === "40P01") {
|
|
853
|
+
return new DeadlockError(props)
|
|
854
|
+
}
|
|
855
|
+
if (code === "40001") {
|
|
856
|
+
return new SerializationError(props)
|
|
857
|
+
}
|
|
858
|
+
if (code === "55P03") {
|
|
859
|
+
return new LockTimeoutError(props)
|
|
860
|
+
}
|
|
861
|
+
if (code === "57014") {
|
|
862
|
+
return new StatementTimeoutError(props)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return new UnknownError(props)
|
|
866
|
+
}
|