@effect/sql-pg 4.0.0-beta.6 → 4.0.0-beta.62

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,20 +150,24 @@ 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(
142
157
  Effect.timeoutOption(1000)
143
- )
158
+ ),
159
+ { interruptible: true }
144
160
  ).pipe(
145
161
  Effect.timeoutOrElse({
146
162
  duration: options.connectTimeout ?? Duration.seconds(5),
147
- onTimeout: () =>
163
+ orElse: () =>
148
164
  Effect.fail(
149
165
  new SqlError({
150
- cause: new Error("Connection timed out"),
151
- message: "PgClient: Connection timed out"
166
+ reason: new ConnectionError({
167
+ cause: new Error("Connection timed out"),
168
+ message: "PgClient: Connection timed out",
169
+ operation: "connect"
170
+ })
152
171
  })
153
172
  )
154
173
  })
@@ -158,6 +177,64 @@ export const make = (
158
177
  })
159
178
  })
160
179
 
180
+ /**
181
+ * @category constructors
182
+ * @since 1.0.0
183
+ */
184
+ export const makeClient = (
185
+ options: PgClientConfig & {
186
+ /**
187
+ * Whether to acquire a separate client for each sql.stream / sql.listen
188
+ */
189
+ readonly acquireForStream?: boolean | undefined
190
+ }
191
+ ): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
192
+ fromClient({
193
+ ...options,
194
+ acquire: Effect.gen(function*() {
195
+ const client = new Pg.Client({
196
+ connectionString: options.url ? Redacted.value(options.url) : undefined,
197
+ user: options.username,
198
+ host: options.host,
199
+ database: options.database,
200
+ password: options.password ? Redacted.value(options.password) : undefined,
201
+ ssl: options.ssl,
202
+ port: options.port,
203
+ ...(options.stream ? { stream: options.stream } : {}),
204
+ application_name: options.applicationName ?? "@effect/sql-pg",
205
+ types: options.types
206
+ })
207
+ yield* Effect.acquireRelease(
208
+ Effect.tryPromise({
209
+ try: () => client.query("SELECT 1"),
210
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
211
+ }),
212
+ () =>
213
+ Effect.promise(() => client.end()).pipe(
214
+ Effect.timeoutOption(1000)
215
+ ),
216
+ { interruptible: true }
217
+ ).pipe(
218
+ Effect.timeoutOrElse({
219
+ duration: options.connectTimeout ?? Duration.seconds(5),
220
+ orElse: () =>
221
+ Effect.fail(
222
+ new SqlError({
223
+ reason: new ConnectionError({
224
+ cause: new Error("Connection timed out"),
225
+ message: "PgClient: Connection timed out",
226
+ operation: "connect"
227
+ })
228
+ })
229
+ )
230
+ })
231
+ )
232
+
233
+ return client
234
+ }),
235
+ acquireForStream: options.acquireForStream ?? false
236
+ })
237
+
161
238
  /**
162
239
  * @category constructors
163
240
  * @since 1.0.0
@@ -175,197 +252,149 @@ export const fromPool = Effect.fnUntraced(function*(
175
252
  readonly types?: Pg.CustomTypesConfig | undefined
176
253
  }
177
254
  ): 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
255
  const pool = yield* options.acquire
190
256
 
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)
257
+ const makeConection = (client?: Pg.PoolClient) =>
258
+ new ConnectionImpl(
259
+ function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
260
+ if (client !== undefined) {
261
+ return Effect.callback<A, SqlError>((resume) => {
262
+ f(client!, resume)
263
+ return makeCancel(pool, client!)
236
264
  })
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
- ))
265
+ }
266
+ return Effect.callback<A, SqlError>((resume) => {
267
+ let done = false
268
+ let cancel: Effect.Effect<void> | undefined = undefined
269
+ let client: Pg.PoolClient | undefined = undefined
270
+ function onError(cause: Error) {
271
+ cleanup(cause)
272
+ resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
260
273
  }
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))
274
+ function cleanup(cause?: Error) {
275
+ if (!done) client?.release(cause)
276
+ done = true
277
+ client?.off("error", onError)
281
278
  }
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))
279
+ pool.connect((cause, client_) => {
280
+ if (cause) {
281
+ return resume(
282
+ Effect.fail(
283
+ new SqlError({
284
+ reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
285
+ })
286
+ )
287
+ )
288
+ } else if (!client_) {
289
+ return resume(
290
+ Effect.fail(
291
+ new SqlError({
292
+ reason: new ConnectionError({
293
+ message: "Failed to acquire connection",
294
+ cause: new Error("No client returned"),
295
+ operation: "acquireConnection"
296
+ })
297
+ })
298
+ )
299
+ )
300
+ } else if (done) {
301
+ client_.release()
302
+ return
301
303
  }
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())
304
+ client = client_
305
+ client.once("error", onError)
306
+ cancel = makeCancel(pool, client)
307
+ f(client, (eff) => {
308
+ cleanup()
309
+ resume(eff)
310
+ })
311
+ })
312
+ return Effect.suspend(() => {
313
+ if (!cancel) {
314
+ cleanup()
315
+ return Effect.void
333
316
  }
317
+ return Effect.ensuring(cancel, Effect.sync(cleanup))
334
318
  })
335
319
  })
336
- })))
337
- }
338
- }
320
+ },
321
+ client ? Effect.succeed(client) : reserveRaw
322
+ )
339
323
 
340
324
  const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
341
325
  const fiber = Fiber.getCurrent()!
342
- const scope = ServiceMap.getUnsafe(fiber.services, Scope.Scope)
326
+ const scope = Context.getUnsafe(fiber.context, Scope.Scope)
343
327
  let cause: Error | undefined = undefined
328
+ function onError(cause_: Error) {
329
+ cause = cause_
330
+ }
344
331
  pool.connect((err, client, release) => {
345
332
  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)
333
+ return resume(
334
+ Effect.fail(
335
+ new SqlError({
336
+ reason: classifyError(
337
+ err,
338
+ "Failed to acquire connection for transaction",
339
+ "acquireConnection"
340
+ )
354
341
  })
355
- ),
356
- client!
357
- ))
358
- }
359
- function onError(cause_: Error) {
360
- cause = cause_
342
+ )
343
+ )
344
+ } else if (!client) {
345
+ return resume(
346
+ Effect.fail(
347
+ new SqlError({
348
+ reason: new ConnectionError({
349
+ message: "Failed to acquire connection for transaction",
350
+ cause: new Error("No client returned"),
351
+ operation: "acquireConnection"
352
+ })
353
+ })
354
+ )
355
+ )
361
356
  }
362
- client!.on("error", onError)
357
+ client.on("error", onError)
358
+ resume(Effect.as(
359
+ Scope.addFinalizer(
360
+ scope,
361
+ Effect.sync(() => {
362
+ client.off("error", onError)
363
+ release(cause)
364
+ })
365
+ ),
366
+ client
367
+ ))
363
368
  })
364
369
  })
365
- const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client))
370
+ const reserve = Effect.map(reserveRaw, makeConection)
366
371
 
367
- const listenClient = yield* RcRef.make({
368
- acquire: reserveRaw
372
+ const onListenClientError = (_: Error) => {
373
+ }
374
+
375
+ const listenAcquirer = yield* RcRef.make({
376
+ acquire: Effect.acquireRelease(
377
+ Effect.tryPromise({
378
+ try: async () => {
379
+ const client = new Pg.Client(pool.options)
380
+ await client.connect()
381
+ client.on("error", onListenClientError)
382
+ return client
383
+ },
384
+ catch: (cause) =>
385
+ new SqlError({
386
+ reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
387
+ })
388
+ }),
389
+ (client) =>
390
+ Effect.promise(() => {
391
+ client.off("error", onListenClientError)
392
+ return client.end()
393
+ }).pipe(
394
+ Effect.timeoutOption(1000)
395
+ ),
396
+ { interruptible: true }
397
+ )
369
398
  })
370
399
 
371
400
  let config: PgClientConfig = {
@@ -386,7 +415,7 @@ export const fromPool = Effect.fnUntraced(function*(
386
415
  config = {
387
416
  ...config,
388
417
  host: config.host ?? parsed.host ?? undefined,
389
- port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
418
+ port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
390
419
  username: config.username ?? parsed.user ?? undefined,
391
420
  password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
392
421
  database: config.database ?? parsed.database ?? undefined
@@ -396,10 +425,127 @@ export const fromPool = Effect.fnUntraced(function*(
396
425
  }
397
426
  }
398
427
 
428
+ return yield* makeWith({
429
+ acquirer: Effect.succeed(makeConection()),
430
+ transactionAcquirer: reserve,
431
+ listenAcquirer: RcRef.get(listenAcquirer),
432
+ config,
433
+ spanAttributes: options.spanAttributes,
434
+ transformResultNames: options.transformResultNames,
435
+ transformQueryNames: options.transformQueryNames,
436
+ transformJson: options.transformJson
437
+ })
438
+ })
439
+
440
+ /**
441
+ * @category constructors
442
+ * @since 1.0.0
443
+ */
444
+ export const fromClient = Effect.fnUntraced(function*(
445
+ options: {
446
+ readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
447
+
448
+ /**
449
+ * Whether to acquire a separate client for each sql.stream / sql.listen.
450
+ */
451
+ readonly acquireForStream: boolean
452
+
453
+ readonly applicationName?: string | undefined
454
+ readonly spanAttributes?: Record<string, unknown> | undefined
455
+
456
+ readonly transformResultNames?: ((str: string) => string) | undefined
457
+ readonly transformQueryNames?: ((str: string) => string) | undefined
458
+ readonly transformJson?: boolean | undefined
459
+ readonly types?: Pg.CustomTypesConfig | undefined
460
+ }
461
+ ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
462
+ function onError() {}
463
+ const acquireWithErrorHandler = options.acquire.pipe(
464
+ Effect.tap((client) => {
465
+ client.on("error", onError)
466
+ return Effect.addFinalizer(() => {
467
+ client.off("error", onError)
468
+ return Effect.void
469
+ })
470
+ })
471
+ )
472
+ const client = yield* acquireWithErrorHandler
473
+
474
+ const semaphore = Semaphore.makeUnsafe(1)
475
+ let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
476
+ Effect.as(semaphore.take(1), client),
477
+ () => semaphore.release(1)
478
+ )
479
+
480
+ const makeConection = (client: Pg.Client) =>
481
+ new ConnectionImpl(
482
+ function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
483
+ return Effect.callback<A, SqlError>((resume) => {
484
+ f(client, resume)
485
+ })
486
+ },
487
+ streamClient
488
+ )
489
+ const connection = makeConection(client)
490
+ const acquirer = semaphore.withPermit(Effect.succeed(connection))
491
+
492
+ const config: PgClientConfig = {
493
+ ...options,
494
+ host: client.host,
495
+ port: client.port,
496
+ database: client.database,
497
+ username: client.user,
498
+ password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
499
+ ssl: client.ssl
500
+ }
501
+
502
+ return yield* makeWith({
503
+ acquirer,
504
+ transactionAcquirer: acquirer,
505
+ listenAcquirer: streamClient,
506
+ config,
507
+ spanAttributes: options.spanAttributes,
508
+ transformResultNames: options.transformResultNames,
509
+ transformQueryNames: options.transformQueryNames,
510
+ transformJson: options.transformJson
511
+ })
512
+ })
513
+
514
+ /**
515
+ * @category constructors
516
+ * @since 1.0.0
517
+ */
518
+ export const makeWith = Effect.fnUntraced(function*(
519
+ options: {
520
+ readonly acquirer: SqlConnection.Acquirer
521
+ readonly transactionAcquirer: SqlConnection.Acquirer
522
+ readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
523
+
524
+ readonly config: PgClientConfig
525
+ readonly spanAttributes?: Record<string, unknown> | undefined
526
+
527
+ readonly transformResultNames?: ((str: string) => string) | undefined
528
+ readonly transformQueryNames?: ((str: string) => string) | undefined
529
+ readonly transformJson?: boolean | undefined
530
+ }
531
+ ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
532
+ const compiler = makeCompiler(
533
+ options.transformQueryNames,
534
+ options.transformJson
535
+ )
536
+ const transformRows = options.transformResultNames ?
537
+ Statement.defaultTransforms(
538
+ options.transformResultNames,
539
+ options.transformJson
540
+ ).array :
541
+ undefined
542
+
543
+ const config = options.config
544
+
399
545
  return Object.assign(
400
546
  yield* Client.make({
401
- acquirer: Effect.succeed(new ConnectionImpl()),
402
- transactionAcquirer: reserve,
547
+ acquirer: options.acquirer,
548
+ transactionAcquirer: options.transactionAcquirer,
403
549
  compiler,
404
550
  spanAttributes: [
405
551
  ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
@@ -412,11 +558,11 @@ export const fromPool = Effect.fnUntraced(function*(
412
558
  }),
413
559
  {
414
560
  [TypeId]: TypeId as TypeId,
415
- config,
561
+ config: options.config,
416
562
  json: (_: unknown) => Statement.fragment([PgJson(_)]),
417
563
  listen: (channel: string) =>
418
564
  Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
419
- const client = yield* RcRef.get(listenClient)
565
+ const client = yield* options.listenAcquirer
420
566
  function onNotification(msg: Pg.Notification) {
421
567
  if (msg.channel === channel && msg.payload) {
422
568
  Queue.offerUnsafe(queue, msg.payload)
@@ -430,24 +576,133 @@ export const fromPool = Effect.fnUntraced(function*(
430
576
  )
431
577
  yield* Effect.tryPromise({
432
578
  try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
433
- catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
579
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
434
580
  })
435
581
  client.on("notification", onNotification)
436
582
  })),
437
583
  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
- })
584
+ Effect.asVoid(Effect.scoped(Effect.flatMap(
585
+ options.acquirer,
586
+ (conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
587
+ )))
447
588
  }
448
589
  )
449
590
  })
450
591
 
592
+ class ConnectionImpl implements Connection {
593
+ constructor(
594
+ runWithClient: <A>(
595
+ f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
596
+ ) => Effect.Effect<A, SqlError>,
597
+ reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
598
+ ) {
599
+ this.runWithClient = runWithClient
600
+ this.reserve = reserve
601
+ }
602
+
603
+ private readonly runWithClient: <A>(
604
+ f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
605
+ ) => Effect.Effect<A, SqlError>
606
+ private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
607
+
608
+ private run(query: string, params: ReadonlyArray<unknown>) {
609
+ return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
610
+ client.query(query, params as any, (err, result) => {
611
+ if (err) {
612
+ resume(
613
+ Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
614
+ )
615
+ } else {
616
+ // Multi-statement queries return an array of results
617
+ resume(Effect.succeed(
618
+ Array.isArray(result)
619
+ ? result.map((r) => r.rows ?? [])
620
+ : result.rows ?? []
621
+ ))
622
+ }
623
+ })
624
+ })
625
+ }
626
+
627
+ execute(
628
+ sql: string,
629
+ params: ReadonlyArray<unknown>,
630
+ transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
631
+ ) {
632
+ return transformRows
633
+ ? Effect.map(this.run(sql, params), transformRows)
634
+ : this.run(sql, params)
635
+ }
636
+ executeRaw(sql: string, params: ReadonlyArray<unknown>) {
637
+ return this.runWithClient<Pg.Result>((client, resume) => {
638
+ client.query(sql, params as any, (err, result) => {
639
+ if (err) {
640
+ resume(
641
+ Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
642
+ )
643
+ } else {
644
+ resume(Effect.succeed(result))
645
+ }
646
+ })
647
+ })
648
+ }
649
+ executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
650
+ return this.run(sql, params)
651
+ }
652
+ executeValues(sql: string, params: ReadonlyArray<unknown>) {
653
+ return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
654
+ client.query(
655
+ {
656
+ text: sql,
657
+ rowMode: "array",
658
+ values: params as Array<string>
659
+ },
660
+ (err, result) => {
661
+ if (err) {
662
+ resume(
663
+ Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
664
+ )
665
+ } else {
666
+ resume(Effect.succeed(result.rows))
667
+ }
668
+ }
669
+ )
670
+ })
671
+ }
672
+ executeUnprepared(
673
+ sql: string,
674
+ params: ReadonlyArray<unknown>,
675
+ transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
676
+ ) {
677
+ return this.execute(sql, params, transformRows)
678
+ }
679
+ executeStream(
680
+ sql: string,
681
+ params: ReadonlyArray<unknown>,
682
+ transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
683
+ ) {
684
+ // oxlint-disable-next-line @typescript-eslint/no-this-alias
685
+ const self = this
686
+ return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
687
+ const client = yield* Scope.provide(self.reserve, scope)
688
+ yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
689
+ const cursor = client.query(new Cursor(sql, params as any))
690
+ // @effect-diagnostics-next-line returnEffectInGen:off
691
+ return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
692
+ cursor.read(128, (err, rows) => {
693
+ if (err) {
694
+ resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
695
+ } else if (Arr.isArrayNonEmpty(rows)) {
696
+ resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
697
+ } else {
698
+ resume(Cause.done())
699
+ }
700
+ })
701
+ })
702
+ })))
703
+ }
704
+ }
705
+
451
706
  const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
452
707
  const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
453
708
  if (cancelEffects.has(client)) {
@@ -474,57 +729,37 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
474
729
  * @category layers
475
730
  * @since 1.0.0
476
731
  */
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))
732
+ export const layerFrom = <E, R>(
733
+ acquire: Effect.Effect<PgClient, E, R>
734
+ ): Layer.Layer<PgClient | Client.SqlClient, E, Exclude<R, Scope.Scope | Reactivity.Reactivity>> =>
735
+ Layer.effectContext(
736
+ Effect.map(acquire, (client) =>
737
+ Context.make(PgClient, client).pipe(
738
+ Context.add(Client.SqlClient, client)
739
+ ))
740
+ ).pipe(Layer.provide(Reactivity.layer)) as any
492
741
 
493
742
  /**
494
743
  * @category layers
495
744
  * @since 1.0.0
496
745
  */
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))
746
+ export const layerConfig: (
747
+ config: Config.Wrap<PgPoolConfig>
748
+ ) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
749
+ config: Config.Wrap<PgPoolConfig>
750
+ ): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
751
+ layerFrom(Effect.flatMap(
752
+ Config.unwrap(config).asEffect(),
753
+ make
754
+ ))
506
755
 
507
756
  /**
508
757
  * @category layers
509
758
  * @since 1.0.0
510
759
  */
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))
760
+ export const layer = (
761
+ config: PgPoolConfig
762
+ ): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
528
763
 
529
764
  /**
530
765
  * @category constructor
@@ -591,3 +826,55 @@ interface PgJson extends Custom<"PgJson", unknown> {}
591
826
  * @since 1.0.0
592
827
  */
593
828
  const PgJson = Statement.custom<PgJson>("PgJson")
829
+
830
+ const ATTR_DB_SYSTEM_NAME = "db.system.name"
831
+ const ATTR_DB_NAMESPACE = "db.namespace"
832
+ const ATTR_SERVER_ADDRESS = "server.address"
833
+ const ATTR_SERVER_PORT = "server.port"
834
+
835
+ const pgCodeFromCause = (cause: unknown): string | undefined => {
836
+ if (typeof cause !== "object" || cause === null || !("code" in cause)) {
837
+ return undefined
838
+ }
839
+ const code = cause.code
840
+ return typeof code === "string" ? code : undefined
841
+ }
842
+
843
+ const classifyError = (
844
+ cause: unknown,
845
+ message: string,
846
+ operation: string
847
+ ) => {
848
+ const props = { cause, message, operation }
849
+ const code = pgCodeFromCause(cause)
850
+ if (code !== undefined) {
851
+ if (code.startsWith("08")) {
852
+ return new ConnectionError(props)
853
+ }
854
+ if (code.startsWith("28")) {
855
+ return new AuthenticationError(props)
856
+ }
857
+ if (code === "42501") {
858
+ return new AuthorizationError(props)
859
+ }
860
+ if (code.startsWith("42")) {
861
+ return new SqlSyntaxError(props)
862
+ }
863
+ if (code.startsWith("23")) {
864
+ return new ConstraintError(props)
865
+ }
866
+ if (code === "40P01") {
867
+ return new DeadlockError(props)
868
+ }
869
+ if (code === "40001") {
870
+ return new SerializationError(props)
871
+ }
872
+ if (code === "55P03") {
873
+ return new LockTimeoutError(props)
874
+ }
875
+ if (code === "57014") {
876
+ return new StatementTimeoutError(props)
877
+ }
878
+ }
879
+ return new UnknownError(props)
880
+ }