@effect/sql-pg 4.0.0-beta.4 → 4.0.0-beta.40

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/src/PgClient.ts CHANGED
@@ -10,16 +10,31 @@ import * as Effect from "effect/Effect"
10
10
  import * as Fiber from "effect/Fiber"
11
11
  import * as Layer from "effect/Layer"
12
12
  import * as Number from "effect/Number"
13
+ import * as Option from "effect/Option"
13
14
  import * as Queue from "effect/Queue"
14
15
  import * as RcRef from "effect/RcRef"
15
16
  import * as Redacted from "effect/Redacted"
16
17
  import * as Scope from "effect/Scope"
18
+ import * as Semaphore from "effect/Semaphore"
17
19
  import * as ServiceMap from "effect/ServiceMap"
18
20
  import * as Stream from "effect/Stream"
19
21
  import * as Reactivity from "effect/unstable/reactivity/Reactivity"
20
22
  import * as Client from "effect/unstable/sql/SqlClient"
21
23
  import type { Connection } from "effect/unstable/sql/SqlConnection"
22
- import { SqlError } from "effect/unstable/sql/SqlError"
24
+ import type * as SqlConnection from "effect/unstable/sql/SqlConnection"
25
+ import {
26
+ AuthenticationError,
27
+ AuthorizationError,
28
+ ConnectionError,
29
+ ConstraintError,
30
+ DeadlockError,
31
+ LockTimeoutError,
32
+ SerializationError,
33
+ SqlError,
34
+ SqlSyntaxError,
35
+ StatementTimeoutError,
36
+ UnknownError
37
+ } from "effect/unstable/sql/SqlError"
23
38
  import type { Custom, Fragment } from "effect/unstable/sql/Statement"
24
39
  import * as Statement from "effect/unstable/sql/Statement"
25
40
  import type { Duplex } from "node:stream"
@@ -28,11 +43,6 @@ import * as Pg from "pg"
28
43
  import * as PgConnString from "pg-connection-string"
29
44
  import Cursor from "pg-cursor"
30
45
 
31
- const ATTR_DB_SYSTEM_NAME = "db.system.name"
32
- const ATTR_DB_NAMESPACE = "db.namespace"
33
- const ATTR_SERVER_ADDRESS = "server.address"
34
- const ATTR_SERVER_PORT = "server.port"
35
-
36
46
  /**
37
47
  * @category type ids
38
48
  * @since 1.0.0
@@ -78,14 +88,9 @@ export interface PgClientConfig {
78
88
  readonly username?: string | undefined
79
89
  readonly password?: Redacted.Redacted | undefined
80
90
 
81
- readonly stream?: (() => Duplex) | undefined
82
-
83
- readonly idleTimeout?: Duration.DurationInput | undefined
84
- readonly connectTimeout?: Duration.DurationInput | undefined
91
+ readonly connectTimeout?: Duration.Input | undefined
85
92
 
86
- readonly maxConnections?: number | undefined
87
- readonly minConnections?: number | undefined
88
- readonly connectionTTL?: Duration.DurationInput | undefined
93
+ readonly stream?: (() => Duplex) | undefined
89
94
 
90
95
  readonly applicationName?: string | undefined
91
96
  readonly spanAttributes?: Record<string, unknown> | undefined
@@ -100,9 +105,19 @@ export interface PgClientConfig {
100
105
  * @category constructors
101
106
  * @since 1.0.0
102
107
  */
103
- export const make = (
104
- options: PgClientConfig
105
- ): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
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> =>
106
121
  fromPool({
107
122
  ...options,
108
123
  acquire: Effect.gen(function*() {
@@ -116,15 +131,15 @@ export const make = (
116
131
  port: options.port,
117
132
  ...(options.stream ? { stream: options.stream } : {}),
118
133
  connectionTimeoutMillis: options.connectTimeout
119
- ? Duration.toMillis(Duration.fromDurationInputUnsafe(options.connectTimeout))
134
+ ? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout))
120
135
  : undefined,
121
136
  idleTimeoutMillis: options.idleTimeout
122
- ? Duration.toMillis(Duration.fromDurationInputUnsafe(options.idleTimeout))
137
+ ? Duration.toMillis(Duration.fromInputUnsafe(options.idleTimeout))
123
138
  : undefined,
124
139
  max: options.maxConnections,
125
140
  min: options.minConnections,
126
141
  maxLifetimeSeconds: options.connectionTTL
127
- ? Duration.toSeconds(Duration.fromDurationInputUnsafe(options.connectionTTL))
142
+ ? Duration.toSeconds(Duration.fromInputUnsafe(options.connectionTTL))
128
143
  : undefined,
129
144
  application_name: options.applicationName ?? "@effect/sql-pg",
130
145
  types: options.types
@@ -135,7 +150,7 @@ export const make = (
135
150
  yield* Effect.acquireRelease(
136
151
  Effect.tryPromise({
137
152
  try: () => pool.query("SELECT 1"),
138
- catch: (cause) => new SqlError({ cause, message: "PgClient: Failed to connect" })
153
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
139
154
  }),
140
155
  () =>
141
156
  Effect.promise(() => pool.end()).pipe(
@@ -144,11 +159,14 @@ export const make = (
144
159
  ).pipe(
145
160
  Effect.timeoutOrElse({
146
161
  duration: options.connectTimeout ?? Duration.seconds(5),
147
- onTimeout: () =>
162
+ orElse: () =>
148
163
  Effect.fail(
149
164
  new SqlError({
150
- cause: new Error("Connection timed out"),
151
- message: "PgClient: Connection timed out"
165
+ reason: new ConnectionError({
166
+ cause: new Error("Connection timed out"),
167
+ message: "PgClient: Connection timed out",
168
+ operation: "connect"
169
+ })
152
170
  })
153
171
  )
154
172
  })
@@ -158,6 +176,63 @@ export const make = (
158
176
  })
159
177
  })
160
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
+ orElse: () =>
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
+
161
236
  /**
162
237
  * @category constructors
163
238
  * @since 1.0.0
@@ -175,167 +250,74 @@ export const fromPool = Effect.fnUntraced(function*(
175
250
  readonly types?: Pg.CustomTypesConfig | undefined
176
251
  }
177
252
  ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
178
- const compiler = makeCompiler(
179
- options.transformQueryNames,
180
- options.transformJson
181
- )
182
- const transformRows = options.transformResultNames ?
183
- Statement.defaultTransforms(
184
- options.transformResultNames,
185
- options.transformJson
186
- ).array :
187
- undefined
188
-
189
253
  const pool = yield* options.acquire
190
254
 
191
- class ConnectionImpl implements Connection {
192
- readonly pg: Pg.PoolClient | undefined
193
- constructor(pg?: Pg.PoolClient) {
194
- this.pg = pg
195
- }
196
-
197
- private runWithClient<A>(f: (client: Pg.PoolClient, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
198
- if (this.pg !== undefined) {
199
- return Effect.callback<A, SqlError>((resume) => {
200
- f(this.pg!, resume)
201
- return makeCancel(pool, this.pg!)
202
- })
203
- }
204
- return Effect.callback<A, SqlError>((resume) => {
205
- let done = false
206
- let cancel: Effect.Effect<void> | undefined = undefined
207
- let client: Pg.PoolClient | undefined = undefined
208
- function onError(cause: Error) {
209
- cleanup(cause)
210
- resume(Effect.fail(new SqlError({ cause, message: "Connection error" })))
211
- }
212
- function cleanup(cause?: Error) {
213
- if (!done) client?.release(cause)
214
- done = true
215
- client?.off("error", onError)
216
- }
217
- pool.connect((cause, client_) => {
218
- if (cause) {
219
- return resume(Effect.fail(new SqlError({ cause, message: "Failed to acquire connection" })))
220
- } else if (!client_) {
221
- return resume(
222
- Effect.fail(
223
- new SqlError({ message: "Failed to acquire connection", cause: new Error("No client returned") })
224
- )
225
- )
226
- } else if (done) {
227
- client_.release()
228
- return
229
- }
230
- client = client_
231
- client.once("error", onError)
232
- cancel = makeCancel(pool, client)
233
- f(client, (eff) => {
234
- cleanup()
235
- 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!)
236
262
  })
237
- })
238
- return Effect.suspend(() => {
239
- if (!cancel) {
240
- cleanup()
241
- return Effect.void
242
- }
243
- return Effect.ensuring(cancel, Effect.sync(cleanup))
244
- })
245
- })
246
- }
247
-
248
- private run(query: string, params: ReadonlyArray<unknown>) {
249
- return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
250
- client.query(query, params as any, (err, result) => {
251
- if (err) {
252
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
253
- } else {
254
- // Multi-statement queries return an array of results
255
- resume(Effect.succeed(
256
- Array.isArray(result)
257
- ? result.map((r) => r.rows ?? [])
258
- : result.rows ?? []
259
- ))
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") })))
260
271
  }
261
- })
262
- })
263
- }
264
-
265
- execute(
266
- sql: string,
267
- params: ReadonlyArray<unknown>,
268
- transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
269
- ) {
270
- return transformRows
271
- ? Effect.map(this.run(sql, params), transformRows)
272
- : this.run(sql, params)
273
- }
274
- executeRaw(sql: string, params: ReadonlyArray<unknown>) {
275
- return this.runWithClient<Pg.Result>((client, resume) => {
276
- client.query(sql, params as any, (err, result) => {
277
- if (err) {
278
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
279
- } else {
280
- resume(Effect.succeed(result))
272
+ function cleanup(cause?: Error) {
273
+ if (!done) client?.release(cause)
274
+ done = true
275
+ client?.off("error", onError)
281
276
  }
282
- })
283
- })
284
- }
285
- executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
286
- return this.run(sql, params)
287
- }
288
- executeValues(sql: string, params: ReadonlyArray<unknown>) {
289
- return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
290
- client.query(
291
- {
292
- text: sql,
293
- rowMode: "array",
294
- values: params as Array<string>
295
- },
296
- (err, result) => {
297
- if (err) {
298
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
299
- } else {
300
- resume(Effect.succeed(result.rows))
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
+ )
285
+ )
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
301
301
  }
302
- }
303
- )
304
- })
305
- }
306
- executeUnprepared(
307
- sql: string,
308
- params: ReadonlyArray<unknown>,
309
- transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
310
- ) {
311
- return this.execute(sql, params, transformRows)
312
- }
313
- executeStream(
314
- sql: string,
315
- params: ReadonlyArray<unknown>,
316
- transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
317
- ) {
318
- // oxlint-disable-next-line @typescript-eslint/no-this-alias
319
- const self = this
320
- return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
321
- const client = self.pg ?? (yield* Scope.provide(reserveRaw, scope))
322
- yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
323
- const cursor = client.query(new Cursor(sql, params as any))
324
- // @effect-diagnostics-next-line returnEffectInGen:off
325
- return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
326
- cursor.read(128, (err, rows) => {
327
- if (err) {
328
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
329
- } else if (Arr.isArrayNonEmpty(rows)) {
330
- resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
331
- } else {
332
- 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
333
314
  }
315
+ return Effect.ensuring(cancel, Effect.sync(cleanup))
334
316
  })
335
317
  })
336
- })))
337
- }
338
- }
318
+ },
319
+ client ? Effect.succeed(client) : reserveRaw
320
+ )
339
321
 
340
322
  const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
341
323
  const fiber = Fiber.getCurrent()!
@@ -343,7 +325,17 @@ export const fromPool = Effect.fnUntraced(function*(
343
325
  let cause: Error | undefined = undefined
344
326
  pool.connect((err, client, release) => {
345
327
  if (err) {
346
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to acquire connection for transaction" })))
328
+ resume(
329
+ Effect.fail(
330
+ new SqlError({
331
+ reason: classifyError(
332
+ err,
333
+ "Failed to acquire connection for transaction",
334
+ "acquireConnection"
335
+ )
336
+ })
337
+ )
338
+ )
347
339
  } else {
348
340
  resume(Effect.as(
349
341
  Scope.addFinalizer(
@@ -362,10 +354,33 @@ export const fromPool = Effect.fnUntraced(function*(
362
354
  client!.on("error", onError)
363
355
  })
364
356
  })
365
- const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client))
357
+ const reserve = Effect.map(reserveRaw, makeConection)
366
358
 
367
- const listenClient = yield* RcRef.make({
368
- acquire: reserveRaw
359
+ const onListenClientError = (_: Error) => {
360
+ }
361
+
362
+ const listenAcquirer = yield* RcRef.make({
363
+ acquire: Effect.acquireRelease(
364
+ Effect.tryPromise({
365
+ try: async () => {
366
+ const client = new Pg.Client(pool.options)
367
+ await client.connect()
368
+ client.on("error", onListenClientError)
369
+ return client
370
+ },
371
+ catch: (cause) =>
372
+ new SqlError({
373
+ reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
374
+ })
375
+ }),
376
+ (client) =>
377
+ Effect.promise(() => {
378
+ client.off("error", onListenClientError)
379
+ return client.end()
380
+ }).pipe(
381
+ Effect.timeoutOption(1000)
382
+ )
383
+ )
369
384
  })
370
385
 
371
386
  let config: PgClientConfig = {
@@ -386,7 +401,7 @@ export const fromPool = Effect.fnUntraced(function*(
386
401
  config = {
387
402
  ...config,
388
403
  host: config.host ?? parsed.host ?? undefined,
389
- port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
404
+ port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
390
405
  username: config.username ?? parsed.user ?? undefined,
391
406
  password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
392
407
  database: config.database ?? parsed.database ?? undefined
@@ -396,10 +411,127 @@ export const fromPool = Effect.fnUntraced(function*(
396
411
  }
397
412
  }
398
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
+
399
531
  return Object.assign(
400
532
  yield* Client.make({
401
- acquirer: Effect.succeed(new ConnectionImpl()),
402
- transactionAcquirer: reserve,
533
+ acquirer: options.acquirer,
534
+ transactionAcquirer: options.transactionAcquirer,
403
535
  compiler,
404
536
  spanAttributes: [
405
537
  ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
@@ -412,11 +544,11 @@ export const fromPool = Effect.fnUntraced(function*(
412
544
  }),
413
545
  {
414
546
  [TypeId]: TypeId as TypeId,
415
- config,
547
+ config: options.config,
416
548
  json: (_: unknown) => Statement.fragment([PgJson(_)]),
417
549
  listen: (channel: string) =>
418
550
  Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
419
- const client = yield* RcRef.get(listenClient)
551
+ const client = yield* options.listenAcquirer
420
552
  function onNotification(msg: Pg.Notification) {
421
553
  if (msg.channel === channel && msg.payload) {
422
554
  Queue.offerUnsafe(queue, msg.payload)
@@ -430,24 +562,133 @@ export const fromPool = Effect.fnUntraced(function*(
430
562
  )
431
563
  yield* Effect.tryPromise({
432
564
  try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
433
- catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
565
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
434
566
  })
435
567
  client.on("notification", onNotification)
436
568
  })),
437
569
  notify: (channel: string, payload: string) =>
438
- Effect.callback<void, SqlError>((resume) => {
439
- pool.query(`NOTIFY ${Pg.escapeIdentifier(channel)}, $1`, [payload], (err) => {
440
- if (err) {
441
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to notify" })))
442
- } else {
443
- resume(Effect.void)
444
- }
445
- })
446
- })
570
+ Effect.asVoid(Effect.scoped(Effect.flatMap(
571
+ options.acquirer,
572
+ (conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
573
+ )))
447
574
  }
448
575
  )
449
576
  })
450
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
+
451
692
  const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
452
693
  const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
453
694
  if (cancelEffects.has(client)) {
@@ -474,57 +715,37 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
474
715
  * @category layers
475
716
  * @since 1.0.0
476
717
  */
477
- export const layerConfig: (
478
- config: Config.Wrap<PgClientConfig>
479
- ) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
480
- config: Config.Wrap<PgClientConfig>
481
- ): 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>> =>
482
721
  Layer.effectServices(
483
- Config.unwrap(config).asEffect().pipe(
484
- Effect.flatMap(make),
485
- Effect.map((client) =>
486
- ServiceMap.make(PgClient, client).pipe(
487
- ServiceMap.add(Client.SqlClient, client)
488
- )
489
- )
490
- )
491
- ).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
492
727
 
493
728
  /**
494
729
  * @category layers
495
730
  * @since 1.0.0
496
731
  */
497
- export const layer = (
498
- config: PgClientConfig
499
- ): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
500
- Layer.effectServices(
501
- Effect.map(make(config), (client) =>
502
- ServiceMap.make(PgClient, client).pipe(
503
- ServiceMap.add(Client.SqlClient, client)
504
- ))
505
- ).pipe(Layer.provide(Reactivity.layer))
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
+ ))
506
741
 
507
742
  /**
508
743
  * @category layers
509
744
  * @since 1.0.0
510
745
  */
511
- export const layerFromPool = (options: {
512
- readonly acquire: Effect.Effect<Pg.Pool, SqlError, Scope.Scope>
513
-
514
- readonly applicationName?: string | undefined
515
- readonly spanAttributes?: Record<string, unknown> | undefined
516
-
517
- readonly transformResultNames?: ((str: string) => string) | undefined
518
- readonly transformQueryNames?: ((str: string) => string) | undefined
519
- readonly transformJson?: boolean | undefined
520
- readonly types?: Pg.CustomTypesConfig | undefined
521
- }): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
522
- Layer.effectServices(
523
- Effect.map(fromPool(options), (client) =>
524
- ServiceMap.make(PgClient, client).pipe(
525
- ServiceMap.add(Client.SqlClient, client)
526
- ))
527
- ).pipe(Layer.provide(Reactivity.layer))
746
+ export const layer = (
747
+ config: PgPoolConfig
748
+ ): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
528
749
 
529
750
  /**
530
751
  * @category constructor
@@ -591,3 +812,55 @@ interface PgJson extends Custom<"PgJson", unknown> {}
591
812
  * @since 1.0.0
592
813
  */
593
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
+ }