@effect/sql-pg 4.0.0-beta.38 → 4.0.0-beta.39

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