@effect/sql-pg 4.0.0-beta.5 → 4.0.0-beta.50

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
@@ -5,21 +5,36 @@ import * as Arr from "effect/Array"
5
5
  import * as Cause from "effect/Cause"
6
6
  import * as Channel from "effect/Channel"
7
7
  import * as Config from "effect/Config"
8
+ import * as Context from "effect/Context"
8
9
  import * as Duration from "effect/Duration"
9
10
  import * as Effect from "effect/Effect"
10
11
  import * as Fiber from "effect/Fiber"
11
12
  import * as Layer from "effect/Layer"
12
13
  import * as Number from "effect/Number"
14
+ import * as Option from "effect/Option"
13
15
  import * as Queue from "effect/Queue"
14
16
  import * as RcRef from "effect/RcRef"
15
17
  import * as Redacted from "effect/Redacted"
16
18
  import * as Scope from "effect/Scope"
17
- import * as ServiceMap from "effect/ServiceMap"
19
+ import * as Semaphore from "effect/Semaphore"
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
@@ -61,7 +71,7 @@ export interface PgClient extends Client.SqlClient {
61
71
  * @category tags
62
72
  * @since 1.0.0
63
73
  */
64
- export const PgClient = ServiceMap.Service<PgClient>("@effect/sql-pg/PgClient")
74
+ export const PgClient = Context.Service<PgClient>("@effect/sql-pg/PgClient")
65
75
 
66
76
  /**
67
77
  * @category constructors
@@ -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,197 +250,148 @@ 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()!
342
- const scope = ServiceMap.getUnsafe(fiber.services, Scope.Scope)
324
+ const scope = Context.getUnsafe(fiber.context, Scope.Scope)
343
325
  let cause: Error | undefined = undefined
326
+ function onError(cause_: Error) {
327
+ cause = cause_
328
+ }
344
329
  pool.connect((err, client, release) => {
345
330
  if (err) {
346
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to acquire connection for transaction" })))
347
- } else {
348
- resume(Effect.as(
349
- Scope.addFinalizer(
350
- scope,
351
- Effect.sync(() => {
352
- client!.off("error", onError)
353
- release(cause)
331
+ return resume(
332
+ Effect.fail(
333
+ new SqlError({
334
+ reason: classifyError(
335
+ err,
336
+ "Failed to acquire connection for transaction",
337
+ "acquireConnection"
338
+ )
354
339
  })
355
- ),
356
- client!
357
- ))
358
- }
359
- function onError(cause_: Error) {
360
- cause = cause_
340
+ )
341
+ )
342
+ } else if (!client) {
343
+ return resume(
344
+ Effect.fail(
345
+ new SqlError({
346
+ reason: new ConnectionError({
347
+ message: "Failed to acquire connection for transaction",
348
+ cause: new Error("No client returned"),
349
+ operation: "acquireConnection"
350
+ })
351
+ })
352
+ )
353
+ )
361
354
  }
362
- client!.on("error", onError)
355
+ client.on("error", onError)
356
+ resume(Effect.as(
357
+ Scope.addFinalizer(
358
+ scope,
359
+ Effect.sync(() => {
360
+ client.off("error", onError)
361
+ release(cause)
362
+ })
363
+ ),
364
+ client
365
+ ))
363
366
  })
364
367
  })
365
- const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client))
368
+ const reserve = Effect.map(reserveRaw, makeConection)
369
+
370
+ const onListenClientError = (_: Error) => {
371
+ }
366
372
 
367
- const listenClient = yield* RcRef.make({
368
- acquire: reserveRaw
373
+ const listenAcquirer = yield* RcRef.make({
374
+ acquire: Effect.acquireRelease(
375
+ Effect.tryPromise({
376
+ try: async () => {
377
+ const client = new Pg.Client(pool.options)
378
+ await client.connect()
379
+ client.on("error", onListenClientError)
380
+ return client
381
+ },
382
+ catch: (cause) =>
383
+ new SqlError({
384
+ reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
385
+ })
386
+ }),
387
+ (client) =>
388
+ Effect.promise(() => {
389
+ client.off("error", onListenClientError)
390
+ return client.end()
391
+ }).pipe(
392
+ Effect.timeoutOption(1000)
393
+ )
394
+ )
369
395
  })
370
396
 
371
397
  let config: PgClientConfig = {
@@ -386,7 +412,7 @@ export const fromPool = Effect.fnUntraced(function*(
386
412
  config = {
387
413
  ...config,
388
414
  host: config.host ?? parsed.host ?? undefined,
389
- port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
415
+ port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
390
416
  username: config.username ?? parsed.user ?? undefined,
391
417
  password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
392
418
  database: config.database ?? parsed.database ?? undefined
@@ -396,10 +422,127 @@ export const fromPool = Effect.fnUntraced(function*(
396
422
  }
397
423
  }
398
424
 
425
+ return yield* makeWith({
426
+ acquirer: Effect.succeed(makeConection()),
427
+ transactionAcquirer: reserve,
428
+ listenAcquirer: RcRef.get(listenAcquirer),
429
+ config,
430
+ spanAttributes: options.spanAttributes,
431
+ transformResultNames: options.transformResultNames,
432
+ transformQueryNames: options.transformQueryNames,
433
+ transformJson: options.transformJson
434
+ })
435
+ })
436
+
437
+ /**
438
+ * @category constructors
439
+ * @since 1.0.0
440
+ */
441
+ export const fromClient = Effect.fnUntraced(function*(
442
+ options: {
443
+ readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
444
+
445
+ /**
446
+ * Whether to acquire a separate client for each sql.stream / sql.listen.
447
+ */
448
+ readonly acquireForStream: boolean
449
+
450
+ readonly applicationName?: string | undefined
451
+ readonly spanAttributes?: Record<string, unknown> | undefined
452
+
453
+ readonly transformResultNames?: ((str: string) => string) | undefined
454
+ readonly transformQueryNames?: ((str: string) => string) | undefined
455
+ readonly transformJson?: boolean | undefined
456
+ readonly types?: Pg.CustomTypesConfig | undefined
457
+ }
458
+ ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
459
+ function onError() {}
460
+ const acquireWithErrorHandler = options.acquire.pipe(
461
+ Effect.tap((client) => {
462
+ client.on("error", onError)
463
+ return Effect.addFinalizer(() => {
464
+ client.off("error", onError)
465
+ return Effect.void
466
+ })
467
+ })
468
+ )
469
+ const client = yield* acquireWithErrorHandler
470
+
471
+ const semaphore = Semaphore.makeUnsafe(1)
472
+ let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
473
+ Effect.as(semaphore.take(1), client),
474
+ () => semaphore.release(1)
475
+ )
476
+
477
+ const makeConection = (client: Pg.Client) =>
478
+ new ConnectionImpl(
479
+ function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
480
+ return Effect.callback<A, SqlError>((resume) => {
481
+ f(client, resume)
482
+ })
483
+ },
484
+ streamClient
485
+ )
486
+ const connection = makeConection(client)
487
+ const acquirer = semaphore.withPermit(Effect.succeed(connection))
488
+
489
+ const config: PgClientConfig = {
490
+ ...options,
491
+ host: client.host,
492
+ port: client.port,
493
+ database: client.database,
494
+ username: client.user,
495
+ password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
496
+ ssl: client.ssl
497
+ }
498
+
499
+ return yield* makeWith({
500
+ acquirer,
501
+ transactionAcquirer: acquirer,
502
+ listenAcquirer: streamClient,
503
+ config,
504
+ spanAttributes: options.spanAttributes,
505
+ transformResultNames: options.transformResultNames,
506
+ transformQueryNames: options.transformQueryNames,
507
+ transformJson: options.transformJson
508
+ })
509
+ })
510
+
511
+ /**
512
+ * @category constructors
513
+ * @since 1.0.0
514
+ */
515
+ export const makeWith = Effect.fnUntraced(function*(
516
+ options: {
517
+ readonly acquirer: SqlConnection.Acquirer
518
+ readonly transactionAcquirer: SqlConnection.Acquirer
519
+ readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
520
+
521
+ readonly config: PgClientConfig
522
+ readonly spanAttributes?: Record<string, unknown> | undefined
523
+
524
+ readonly transformResultNames?: ((str: string) => string) | undefined
525
+ readonly transformQueryNames?: ((str: string) => string) | undefined
526
+ readonly transformJson?: boolean | undefined
527
+ }
528
+ ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
529
+ const compiler = makeCompiler(
530
+ options.transformQueryNames,
531
+ options.transformJson
532
+ )
533
+ const transformRows = options.transformResultNames ?
534
+ Statement.defaultTransforms(
535
+ options.transformResultNames,
536
+ options.transformJson
537
+ ).array :
538
+ undefined
539
+
540
+ const config = options.config
541
+
399
542
  return Object.assign(
400
543
  yield* Client.make({
401
- acquirer: Effect.succeed(new ConnectionImpl()),
402
- transactionAcquirer: reserve,
544
+ acquirer: options.acquirer,
545
+ transactionAcquirer: options.transactionAcquirer,
403
546
  compiler,
404
547
  spanAttributes: [
405
548
  ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
@@ -412,11 +555,11 @@ export const fromPool = Effect.fnUntraced(function*(
412
555
  }),
413
556
  {
414
557
  [TypeId]: TypeId as TypeId,
415
- config,
558
+ config: options.config,
416
559
  json: (_: unknown) => Statement.fragment([PgJson(_)]),
417
560
  listen: (channel: string) =>
418
561
  Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
419
- const client = yield* RcRef.get(listenClient)
562
+ const client = yield* options.listenAcquirer
420
563
  function onNotification(msg: Pg.Notification) {
421
564
  if (msg.channel === channel && msg.payload) {
422
565
  Queue.offerUnsafe(queue, msg.payload)
@@ -430,24 +573,133 @@ export const fromPool = Effect.fnUntraced(function*(
430
573
  )
431
574
  yield* Effect.tryPromise({
432
575
  try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
433
- catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
576
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
434
577
  })
435
578
  client.on("notification", onNotification)
436
579
  })),
437
580
  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
- })
581
+ Effect.asVoid(Effect.scoped(Effect.flatMap(
582
+ options.acquirer,
583
+ (conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
584
+ )))
447
585
  }
448
586
  )
449
587
  })
450
588
 
589
+ class ConnectionImpl implements Connection {
590
+ constructor(
591
+ runWithClient: <A>(
592
+ f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
593
+ ) => Effect.Effect<A, SqlError>,
594
+ reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
595
+ ) {
596
+ this.runWithClient = runWithClient
597
+ this.reserve = reserve
598
+ }
599
+
600
+ private readonly runWithClient: <A>(
601
+ f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
602
+ ) => Effect.Effect<A, SqlError>
603
+ private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
604
+
605
+ private run(query: string, params: ReadonlyArray<unknown>) {
606
+ return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
607
+ client.query(query, params as any, (err, result) => {
608
+ if (err) {
609
+ resume(
610
+ Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
611
+ )
612
+ } else {
613
+ // Multi-statement queries return an array of results
614
+ resume(Effect.succeed(
615
+ Array.isArray(result)
616
+ ? result.map((r) => r.rows ?? [])
617
+ : result.rows ?? []
618
+ ))
619
+ }
620
+ })
621
+ })
622
+ }
623
+
624
+ execute(
625
+ sql: string,
626
+ params: ReadonlyArray<unknown>,
627
+ transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
628
+ ) {
629
+ return transformRows
630
+ ? Effect.map(this.run(sql, params), transformRows)
631
+ : this.run(sql, params)
632
+ }
633
+ executeRaw(sql: string, params: ReadonlyArray<unknown>) {
634
+ return this.runWithClient<Pg.Result>((client, resume) => {
635
+ client.query(sql, params as any, (err, result) => {
636
+ if (err) {
637
+ resume(
638
+ Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
639
+ )
640
+ } else {
641
+ resume(Effect.succeed(result))
642
+ }
643
+ })
644
+ })
645
+ }
646
+ executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
647
+ return this.run(sql, params)
648
+ }
649
+ executeValues(sql: string, params: ReadonlyArray<unknown>) {
650
+ return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
651
+ client.query(
652
+ {
653
+ text: sql,
654
+ rowMode: "array",
655
+ values: params as Array<string>
656
+ },
657
+ (err, result) => {
658
+ if (err) {
659
+ resume(
660
+ Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
661
+ )
662
+ } else {
663
+ resume(Effect.succeed(result.rows))
664
+ }
665
+ }
666
+ )
667
+ })
668
+ }
669
+ executeUnprepared(
670
+ sql: string,
671
+ params: ReadonlyArray<unknown>,
672
+ transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
673
+ ) {
674
+ return this.execute(sql, params, transformRows)
675
+ }
676
+ executeStream(
677
+ sql: string,
678
+ params: ReadonlyArray<unknown>,
679
+ transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
680
+ ) {
681
+ // oxlint-disable-next-line @typescript-eslint/no-this-alias
682
+ const self = this
683
+ return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
684
+ const client = yield* Scope.provide(self.reserve, scope)
685
+ yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
686
+ const cursor = client.query(new Cursor(sql, params as any))
687
+ // @effect-diagnostics-next-line returnEffectInGen:off
688
+ return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
689
+ cursor.read(128, (err, rows) => {
690
+ if (err) {
691
+ resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
692
+ } else if (Arr.isArrayNonEmpty(rows)) {
693
+ resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
694
+ } else {
695
+ resume(Cause.done())
696
+ }
697
+ })
698
+ })
699
+ })))
700
+ }
701
+ }
702
+
451
703
  const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
452
704
  const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
453
705
  if (cancelEffects.has(client)) {
@@ -474,57 +726,37 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
474
726
  * @category layers
475
727
  * @since 1.0.0
476
728
  */
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> =>
482
- 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))
729
+ export const layerFrom = <E, R>(
730
+ acquire: Effect.Effect<PgClient, E, R>
731
+ ): Layer.Layer<PgClient | Client.SqlClient, E, Exclude<R, Scope.Scope | Reactivity.Reactivity>> =>
732
+ Layer.effectContext(
733
+ Effect.map(acquire, (client) =>
734
+ Context.make(PgClient, client).pipe(
735
+ Context.add(Client.SqlClient, client)
736
+ ))
737
+ ).pipe(Layer.provide(Reactivity.layer)) as any
492
738
 
493
739
  /**
494
740
  * @category layers
495
741
  * @since 1.0.0
496
742
  */
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))
743
+ export const layerConfig: (
744
+ config: Config.Wrap<PgPoolConfig>
745
+ ) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
746
+ config: Config.Wrap<PgPoolConfig>
747
+ ): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
748
+ layerFrom(Effect.flatMap(
749
+ Config.unwrap(config).asEffect(),
750
+ make
751
+ ))
506
752
 
507
753
  /**
508
754
  * @category layers
509
755
  * @since 1.0.0
510
756
  */
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))
757
+ export const layer = (
758
+ config: PgPoolConfig
759
+ ): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
528
760
 
529
761
  /**
530
762
  * @category constructor
@@ -591,3 +823,55 @@ interface PgJson extends Custom<"PgJson", unknown> {}
591
823
  * @since 1.0.0
592
824
  */
593
825
  const PgJson = Statement.custom<PgJson>("PgJson")
826
+
827
+ const ATTR_DB_SYSTEM_NAME = "db.system.name"
828
+ const ATTR_DB_NAMESPACE = "db.namespace"
829
+ const ATTR_SERVER_ADDRESS = "server.address"
830
+ const ATTR_SERVER_PORT = "server.port"
831
+
832
+ const pgCodeFromCause = (cause: unknown): string | undefined => {
833
+ if (typeof cause !== "object" || cause === null || !("code" in cause)) {
834
+ return undefined
835
+ }
836
+ const code = cause.code
837
+ return typeof code === "string" ? code : undefined
838
+ }
839
+
840
+ const classifyError = (
841
+ cause: unknown,
842
+ message: string,
843
+ operation: string
844
+ ) => {
845
+ const props = { cause, message, operation }
846
+ const code = pgCodeFromCause(cause)
847
+ if (code !== undefined) {
848
+ if (code.startsWith("08")) {
849
+ return new ConnectionError(props)
850
+ }
851
+ if (code.startsWith("28")) {
852
+ return new AuthenticationError(props)
853
+ }
854
+ if (code === "42501") {
855
+ return new AuthorizationError(props)
856
+ }
857
+ if (code.startsWith("42")) {
858
+ return new SqlSyntaxError(props)
859
+ }
860
+ if (code.startsWith("23")) {
861
+ return new ConstraintError(props)
862
+ }
863
+ if (code === "40P01") {
864
+ return new DeadlockError(props)
865
+ }
866
+ if (code === "40001") {
867
+ return new SerializationError(props)
868
+ }
869
+ if (code === "55P03") {
870
+ return new LockTimeoutError(props)
871
+ }
872
+ if (code === "57014") {
873
+ return new StatementTimeoutError(props)
874
+ }
875
+ }
876
+ return new UnknownError(props)
877
+ }