@effect/sql-pg 0.50.3 → 4.0.0-beta.0

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
@@ -1,27 +1,27 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import * as Reactivity from "@effect/experimental/Reactivity"
5
- import * as Client from "@effect/sql/SqlClient"
6
- import type { Connection } from "@effect/sql/SqlConnection"
7
- import { SqlError } from "@effect/sql/SqlError"
8
- import type { Custom, Fragment } from "@effect/sql/Statement"
9
- import * as Statement from "@effect/sql/Statement"
10
4
  import * as Arr from "effect/Array"
11
- import * as Chunk from "effect/Chunk"
5
+ import * as Cause from "effect/Cause"
6
+ import * as Channel from "effect/Channel"
12
7
  import * as Config from "effect/Config"
13
- import type * as ConfigError from "effect/ConfigError"
14
- import * as Context from "effect/Context"
15
8
  import * as Duration from "effect/Duration"
16
9
  import * as Effect from "effect/Effect"
17
10
  import * as Fiber from "effect/Fiber"
18
11
  import * as Layer from "effect/Layer"
19
12
  import * as Number from "effect/Number"
20
- import * as Option from "effect/Option"
13
+ import * as Queue from "effect/Queue"
21
14
  import * as RcRef from "effect/RcRef"
22
15
  import * as Redacted from "effect/Redacted"
23
16
  import * as Scope from "effect/Scope"
17
+ import * as ServiceMap from "effect/ServiceMap"
24
18
  import * as Stream from "effect/Stream"
19
+ import * as Reactivity from "effect/unstable/reactivity/Reactivity"
20
+ import * as Client from "effect/unstable/sql/SqlClient"
21
+ import type { Connection } from "effect/unstable/sql/SqlConnection"
22
+ import { SqlError } from "effect/unstable/sql/SqlError"
23
+ import type { Custom, Fragment } from "effect/unstable/sql/Statement"
24
+ import * as Statement from "effect/unstable/sql/Statement"
25
25
  import type { Duplex } from "node:stream"
26
26
  import type { ConnectionOptions } from "node:tls"
27
27
  import * as Pg from "pg"
@@ -61,7 +61,7 @@ export interface PgClient extends Client.SqlClient {
61
61
  * @category tags
62
62
  * @since 1.0.0
63
63
  */
64
- export const PgClient = Context.GenericTag<PgClient>("@effect/sql-pg/PgClient")
64
+ export const PgClient = ServiceMap.Service<PgClient>("@effect/sql-pg/PgClient")
65
65
 
66
66
  /**
67
67
  * @category constructors
@@ -96,381 +96,277 @@ export interface PgClientConfig {
96
96
  readonly types?: Pg.CustomTypesConfig | undefined
97
97
  }
98
98
 
99
- type ClientOptions = {
100
- readonly spanAttributes?: Record<string, unknown> | undefined
101
- readonly transformResultNames?: ((str: string) => string) | undefined
102
- readonly transformQueryNames?: ((str: string) => string) | undefined
103
- readonly transformJson?: boolean | undefined
104
- }
105
-
106
- const makeClient = (
107
- pool: Pg.Pool,
108
- config: PgClientConfig,
109
- options: ClientOptions
99
+ /**
100
+ * @category constructors
101
+ * @since 1.0.0
102
+ */
103
+ export const make = (
104
+ options: PgClientConfig
110
105
  ): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
111
- Effect.gen(function*() {
112
- const compiler = makeCompiler(
113
- options.transformQueryNames,
106
+ fromPool({
107
+ ...options,
108
+ acquire: Effect.gen(function*() {
109
+ const pool = new Pg.Pool({
110
+ connectionString: options.url ? Redacted.value(options.url) : undefined,
111
+ user: options.username,
112
+ host: options.host,
113
+ database: options.database,
114
+ password: options.password ? Redacted.value(options.password) : undefined,
115
+ ssl: options.ssl,
116
+ port: options.port,
117
+ ...(options.stream ? { stream: options.stream } : {}),
118
+ connectionTimeoutMillis: options.connectTimeout
119
+ ? Duration.toMillis(Duration.fromDurationInputUnsafe(options.connectTimeout))
120
+ : undefined,
121
+ idleTimeoutMillis: options.idleTimeout
122
+ ? Duration.toMillis(Duration.fromDurationInputUnsafe(options.idleTimeout))
123
+ : undefined,
124
+ max: options.maxConnections,
125
+ min: options.minConnections,
126
+ maxLifetimeSeconds: options.connectionTTL
127
+ ? Duration.toSeconds(Duration.fromDurationInputUnsafe(options.connectionTTL))
128
+ : undefined,
129
+ application_name: options.applicationName ?? "@effect/sql-pg",
130
+ types: options.types
131
+ })
132
+
133
+ pool.on("error", (_err) => {})
134
+
135
+ yield* Effect.acquireRelease(
136
+ Effect.tryPromise({
137
+ try: () => pool.query("SELECT 1"),
138
+ catch: (cause) => new SqlError({ cause, message: "PgClient: Failed to connect" })
139
+ }),
140
+ () =>
141
+ Effect.promise(() => pool.end()).pipe(
142
+ Effect.timeoutOption(1000)
143
+ )
144
+ ).pipe(
145
+ Effect.timeoutOrElse({
146
+ duration: options.connectTimeout ?? Duration.seconds(5),
147
+ onTimeout: () =>
148
+ Effect.fail(
149
+ new SqlError({
150
+ cause: new Error("Connection timed out"),
151
+ message: "PgClient: Connection timed out"
152
+ })
153
+ )
154
+ })
155
+ )
156
+
157
+ return pool
158
+ })
159
+ })
160
+
161
+ /**
162
+ * @category constructors
163
+ * @since 1.0.0
164
+ */
165
+ export const fromPool = Effect.fnUntraced(function*(
166
+ options: {
167
+ readonly acquire: Effect.Effect<Pg.Pool, SqlError, Scope.Scope>
168
+
169
+ readonly applicationName?: string | undefined
170
+ readonly spanAttributes?: Record<string, unknown> | undefined
171
+
172
+ readonly transformResultNames?: ((str: string) => string) | undefined
173
+ readonly transformQueryNames?: ((str: string) => string) | undefined
174
+ readonly transformJson?: boolean | undefined
175
+ readonly types?: Pg.CustomTypesConfig | undefined
176
+ }
177
+ ): 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,
114
185
  options.transformJson
115
- )
116
- const transformRows = options.transformResultNames ?
117
- Statement.defaultTransforms(
118
- options.transformResultNames,
119
- options.transformJson
120
- ).array :
121
- undefined
122
-
123
- class ConnectionImpl implements Connection {
124
- readonly pg: Pg.PoolClient | undefined
125
- constructor(pg?: Pg.PoolClient) {
126
- this.pg = pg
127
- }
186
+ ).array :
187
+ undefined
128
188
 
129
- private runWithClient<A>(f: (client: Pg.PoolClient, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
130
- if (this.pg !== undefined) {
131
- return Effect.async<A, SqlError>((resume) => {
132
- f(this.pg!, resume)
133
- return makeCancel(pool, this.pg!)
134
- })
189
+ const pool = yield* options.acquire
190
+
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" })))
135
211
  }
136
- return Effect.async<A, SqlError>((resume) => {
137
- let done = false
138
- let cancel: Effect.Effect<void> | undefined = undefined
139
- let client: Pg.PoolClient | undefined = undefined
140
- function onError(cause: Error) {
141
- cleanup(cause)
142
- resume(Effect.fail(new SqlError({ cause, message: "Connection error" })))
143
- }
144
- function cleanup(cause?: Error) {
145
- if (!done) client?.release(cause)
146
- done = true
147
- client?.off("error", onError)
148
- }
149
- pool.connect((cause, client_) => {
150
- if (cause) {
151
- return resume(Effect.fail(new SqlError({ cause, message: "Failed to acquire connection" })))
152
- } else if (!client_) {
153
- return resume(
154
- Effect.fail(
155
- new SqlError({ message: "Failed to acquire connection", cause: new Error("No client returned") })
156
- )
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") })
157
224
  )
158
- } else if (done) {
159
- client_.release()
160
- return
161
- }
162
- client = client_
163
- client.once("error", onError)
164
- cancel = makeCancel(pool, client)
165
- f(client, (eff) => {
166
- cleanup()
167
- resume(eff)
168
- })
169
- })
170
- return Effect.suspend(() => {
171
- if (!cancel) {
172
- cleanup()
173
- return Effect.void
174
- }
175
- return Effect.ensuring(cancel, Effect.sync(cleanup))
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)
176
236
  })
177
237
  })
178
- }
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
+ }
179
247
 
180
- private run(query: string, params: ReadonlyArray<unknown>) {
181
- return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
182
- client.query(query, params as any, (err, result) => {
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
+ ))
260
+ }
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))
281
+ }
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) => {
183
297
  if (err) {
184
298
  resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
185
299
  } else {
186
- // Multi-statement queries return an array of results
187
- resume(Effect.succeed(
188
- Array.isArray(result)
189
- ? result.map((r) => r.rows ?? [])
190
- : result.rows ?? []
191
- ))
300
+ resume(Effect.succeed(result.rows))
192
301
  }
193
- })
194
- })
195
- }
196
-
197
- execute(
198
- sql: string,
199
- params: ReadonlyArray<unknown>,
200
- transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
201
- ) {
202
- return transformRows
203
- ? Effect.map(this.run(sql, params), transformRows)
204
- : this.run(sql, params)
205
- }
206
- executeRaw(sql: string, params: ReadonlyArray<unknown>) {
207
- return this.runWithClient<Pg.Result>((client, resume) => {
208
- client.query(sql, params as any, (err, result) => {
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) => {
209
327
  if (err) {
210
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))
211
331
  } else {
212
- resume(Effect.succeed(result))
332
+ resume(Cause.done())
213
333
  }
214
334
  })
215
335
  })
216
- }
217
- executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
218
- return this.run(sql, params)
219
- }
220
- executeValues(sql: string, params: ReadonlyArray<unknown>) {
221
- return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
222
- client.query(
223
- {
224
- text: sql,
225
- rowMode: "array",
226
- values: params as Array<string>
227
- },
228
- (err, result) => {
229
- if (err) {
230
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
231
- } else {
232
- resume(Effect.succeed(result.rows))
233
- }
234
- }
235
- )
236
- })
237
- }
238
- executeUnprepared(
239
- sql: string,
240
- params: ReadonlyArray<unknown>,
241
- transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
242
- ) {
243
- return this.execute(sql, params, transformRows)
244
- }
245
- executeStream(
246
- sql: string,
247
- params: ReadonlyArray<unknown>,
248
- transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
249
- ) {
250
- // eslint-disable-next-line @typescript-eslint/no-this-alias
251
- const self = this
252
- return Effect.gen(function*() {
253
- const scope = yield* Effect.scope
254
- const client = self.pg ?? (yield* reserveRaw)
255
- yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
256
- const cursor = client.query(new Cursor(sql, params as any))
257
- const pull = Effect.async<Chunk.Chunk<any>, Option.Option<SqlError>>((resume) => {
258
- cursor.read(128, (err, rows) => {
259
- if (err) {
260
- resume(Effect.fail(Option.some(new SqlError({ cause: err, message: "Failed to execute statement" }))))
261
- } else if (Arr.isNonEmptyArray(rows)) {
262
- resume(Effect.succeed(Chunk.unsafeFromArray(transformRows ? transformRows(rows) as any : rows)))
263
- } else {
264
- resume(Effect.fail(Option.none()))
265
- }
266
- })
267
- })
268
- return Stream.repeatEffectChunkOption(pull)
269
- }).pipe(
270
- Stream.unwrapScoped
271
- )
272
- }
336
+ })))
273
337
  }
338
+ }
274
339
 
275
- const reserveRaw = Effect.async<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
276
- const fiber = Option.getOrThrow(Fiber.getCurrentFiber())
277
- const scope = Context.unsafeGet(fiber.currentContext, Scope.Scope)
278
- let cause: Error | undefined = undefined
279
- function onError(cause_: Error) {
280
- cause = cause_
281
- }
282
- pool.connect((err, client, release) => {
283
- if (err) {
284
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to acquire connection for transaction" })))
285
- return
286
- } else if (!client) {
287
- resume(
288
- Effect.fail(
289
- new SqlError({
290
- message: "Failed to acquire connection for transaction",
291
- cause: new Error("No client returned")
292
- })
293
- )
294
- )
295
- return
296
- }
297
-
298
- // Else we know we have client defined, so we can proceed with the connection
299
- client.on("error", onError)
340
+ const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
341
+ const fiber = Fiber.getCurrent()!
342
+ const scope = ServiceMap.getUnsafe(fiber.services, Scope.Scope)
343
+ let cause: Error | undefined = undefined
344
+ pool.connect((err, client, release) => {
345
+ if (err) {
346
+ resume(Effect.fail(new SqlError({ cause: err, message: "Failed to acquire connection for transaction" })))
347
+ } else {
300
348
  resume(Effect.as(
301
349
  Scope.addFinalizer(
302
350
  scope,
303
351
  Effect.sync(() => {
304
- client.off("error", onError)
352
+ client!.off("error", onError)
305
353
  release(cause)
306
354
  })
307
355
  ),
308
- client
356
+ client!
309
357
  ))
310
- })
311
- })
312
- const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client))
313
-
314
- const listenClient = yield* RcRef.make({
315
- acquire: reserveRaw
316
- })
317
-
318
- return Object.assign(
319
- yield* Client.make({
320
- acquirer: Effect.succeed(new ConnectionImpl()),
321
- transactionAcquirer: reserve,
322
- compiler,
323
- spanAttributes: [
324
- ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
325
- [ATTR_DB_SYSTEM_NAME, "postgresql"],
326
- [ATTR_DB_NAMESPACE, config.database ?? config.username ?? "postgres"],
327
- [ATTR_SERVER_ADDRESS, config.host ?? "localhost"],
328
- [ATTR_SERVER_PORT, config.port ?? 5432]
329
- ],
330
- transformRows
331
- }),
332
- {
333
- [TypeId]: TypeId as TypeId,
334
- config,
335
- json: (_: unknown) => PgJson(_),
336
- listen: (channel: string) =>
337
- Stream.asyncPush<string, SqlError>(Effect.fnUntraced(function*(emit) {
338
- const client = yield* RcRef.get(listenClient)
339
- function onNotification(msg: Pg.Notification) {
340
- if (msg.channel === channel && msg.payload) {
341
- emit.single(msg.payload)
342
- }
343
- }
344
- yield* Effect.addFinalizer(() =>
345
- Effect.promise(() => {
346
- client.off("notification", onNotification)
347
- return client.query(`UNLISTEN ${Pg.escapeIdentifier(channel)}`)
348
- })
349
- )
350
- yield* Effect.tryPromise({
351
- try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
352
- catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
353
- })
354
- client.on("notification", onNotification)
355
- })),
356
- notify: (channel: string, payload: string) =>
357
- Effect.async<void, SqlError>((resume) => {
358
- pool.query(`NOTIFY ${Pg.escapeIdentifier(channel)}, $1`, [payload], (err) => {
359
- if (err) {
360
- resume(Effect.fail(new SqlError({ cause: err, message: "Failed to notify" })))
361
- } else {
362
- resume(Effect.void)
363
- }
364
- })
365
- })
366
358
  }
367
- )
368
- })
369
-
370
- /**
371
- * @category constructors
372
- * @since 1.0.0
373
- */
374
- export const make = (
375
- options: PgClientConfig
376
- ): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
377
- Effect.gen(function*() {
378
- const pool = new Pg.Pool({
379
- connectionString: options.url ? Redacted.value(options.url) : undefined,
380
- user: options.username,
381
- host: options.host,
382
- database: options.database,
383
- password: options.password ? Redacted.value(options.password) : undefined,
384
- ssl: options.ssl,
385
- port: options.port,
386
- ...(options.stream ? { stream: options.stream } : {}),
387
- connectionTimeoutMillis: options.connectTimeout
388
- ? Duration.toMillis(options.connectTimeout)
389
- : undefined,
390
- idleTimeoutMillis: options.idleTimeout
391
- ? Duration.toMillis(options.idleTimeout)
392
- : undefined,
393
- max: options.maxConnections,
394
- min: options.minConnections,
395
- maxLifetimeSeconds: options.connectionTTL
396
- ? Duration.toSeconds(options.connectionTTL)
397
- : undefined,
398
- application_name: options.applicationName ?? "@effect/sql-pg",
399
- types: options.types
400
- })
401
-
402
- pool.on("error", (_err) => {
403
- })
404
-
405
- yield* Effect.acquireRelease(
406
- Effect.tryPromise({
407
- try: () => pool.query("SELECT 1"),
408
- catch: (cause) => new SqlError({ cause, message: "PgClient: Failed to connect" })
409
- }),
410
- () =>
411
- Effect.promise(() => pool.end()).pipe(
412
- Effect.interruptible,
413
- Effect.timeoutOption(1000)
414
- )
415
- ).pipe(
416
- Effect.timeoutFail({
417
- duration: options.connectTimeout ?? Duration.seconds(5),
418
- onTimeout: () =>
419
- new SqlError({
420
- cause: new Error("Connection timed out"),
421
- message: "PgClient: Connection timed out"
422
- })
423
- })
424
- )
425
-
426
- let config = options
427
- if (pool.options.connectionString) {
428
- try {
429
- const parsed = PgConnString.parse(pool.options.connectionString)
430
- config = {
431
- ...config,
432
- host: config.host ?? parsed.host ?? undefined,
433
- port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
434
- username: config.username ?? parsed.user ?? undefined,
435
- password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
436
- database: config.database ?? parsed.database ?? undefined
437
- }
438
- } catch {
439
- //
359
+ function onError(cause_: Error) {
360
+ cause = cause_
440
361
  }
441
- }
442
-
443
- return yield* makeClient(pool, config, options)
362
+ client!.on("error", onError)
363
+ })
444
364
  })
365
+ const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client))
445
366
 
446
- /**
447
- * @category constructors
448
- * @since 1.0.0
449
- */
450
- export interface PgClientFromPoolOptions {
451
- readonly acquire: Effect.Effect<Pg.Pool, SqlError, Scope.Scope>
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
-
462
- /**
463
- * Create a `PgClient` from an existing `pg` pool.
464
- *
465
- * You control the pool lifecycle via `acquire` (typically `Effect.acquireRelease`).
466
- *
467
- * @category constructors
468
- * @since 1.0.0
469
- */
470
- export const fromPool = Effect.fnUntraced(function*(
471
- options: PgClientFromPoolOptions
472
- ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
473
- const pool = yield* options.acquire
367
+ const listenClient = yield* RcRef.make({
368
+ acquire: reserveRaw
369
+ })
474
370
 
475
371
  let config: PgClientConfig = {
476
372
  url: pool.options.connectionString ? Redacted.make(pool.options.connectionString) : undefined,
@@ -480,17 +376,17 @@ export const fromPool = Effect.fnUntraced(function*(
480
376
  username: pool.options.user,
481
377
  password: typeof pool.options.password === "string" ? Redacted.make(pool.options.password) : undefined,
482
378
  ssl: pool.options.ssl,
483
- applicationName: (pool.options as any).application_name,
379
+ applicationName: pool.options.application_name,
484
380
  types: pool.options.types
485
381
  }
486
-
487
382
  if (pool.options.connectionString) {
383
+ // @effect-diagnostics-next-line tryCatchInEffectGen:off
488
384
  try {
489
385
  const parsed = PgConnString.parse(pool.options.connectionString)
490
386
  config = {
491
387
  ...config,
492
388
  host: config.host ?? parsed.host ?? undefined,
493
- port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
389
+ port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
494
390
  username: config.username ?? parsed.user ?? undefined,
495
391
  password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
496
392
  database: config.database ?? parsed.database ?? undefined
@@ -500,7 +396,56 @@ export const fromPool = Effect.fnUntraced(function*(
500
396
  }
501
397
  }
502
398
 
503
- return yield* makeClient(pool, config, options)
399
+ return Object.assign(
400
+ yield* Client.make({
401
+ acquirer: Effect.succeed(new ConnectionImpl()),
402
+ transactionAcquirer: reserve,
403
+ compiler,
404
+ spanAttributes: [
405
+ ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
406
+ [ATTR_DB_SYSTEM_NAME, "postgresql"],
407
+ [ATTR_DB_NAMESPACE, config.database ?? config.username ?? "postgres"],
408
+ [ATTR_SERVER_ADDRESS, config.host ?? "localhost"],
409
+ [ATTR_SERVER_PORT, config.port ?? 5432]
410
+ ],
411
+ transformRows
412
+ }),
413
+ {
414
+ [TypeId]: TypeId as TypeId,
415
+ config,
416
+ json: (_: unknown) => Statement.fragment([PgJson(_)]),
417
+ listen: (channel: string) =>
418
+ Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
419
+ const client = yield* RcRef.get(listenClient)
420
+ function onNotification(msg: Pg.Notification) {
421
+ if (msg.channel === channel && msg.payload) {
422
+ Queue.offerUnsafe(queue, msg.payload)
423
+ }
424
+ }
425
+ yield* Effect.addFinalizer(() =>
426
+ Effect.promise(() => {
427
+ client.off("notification", onNotification)
428
+ return client.query(`UNLISTEN ${Pg.escapeIdentifier(channel)}`)
429
+ })
430
+ )
431
+ yield* Effect.tryPromise({
432
+ try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
433
+ catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
434
+ })
435
+ client.on("notification", onNotification)
436
+ })),
437
+ 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
+ })
447
+ }
448
+ )
504
449
  })
505
450
 
506
451
  const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
@@ -511,7 +456,7 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
511
456
  const processId = (client as any).processID
512
457
  const eff = processId !== undefined
513
458
  // query cancelation is best-effort, so we don't fail if it doesn't work
514
- ? Effect.async<void>((resume) => {
459
+ ? Effect.callback<void>((resume) => {
515
460
  if (pool.ending) return resume(Effect.void)
516
461
  pool.query(`SELECT pg_cancel_backend(${processId})`, () => {
517
462
  resume(Effect.void)
@@ -529,15 +474,17 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
529
474
  * @category layers
530
475
  * @since 1.0.0
531
476
  */
532
- export const layerConfig = (
533
- config: Config.Config.Wrap<PgClientConfig>
534
- ): Layer.Layer<PgClient | Client.SqlClient, ConfigError.ConfigError | SqlError> =>
535
- Layer.scopedContext(
536
- Config.unwrap(config).pipe(
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(
537
484
  Effect.flatMap(make),
538
485
  Effect.map((client) =>
539
- Context.make(PgClient, client).pipe(
540
- Context.add(Client.SqlClient, client)
486
+ ServiceMap.make(PgClient, client).pipe(
487
+ ServiceMap.add(Client.SqlClient, client)
541
488
  )
542
489
  )
543
490
  )
@@ -550,10 +497,10 @@ export const layerConfig = (
550
497
  export const layer = (
551
498
  config: PgClientConfig
552
499
  ): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
553
- Layer.scopedContext(
500
+ Layer.effectServices(
554
501
  Effect.map(make(config), (client) =>
555
- Context.make(PgClient, client).pipe(
556
- Context.add(Client.SqlClient, client)
502
+ ServiceMap.make(PgClient, client).pipe(
503
+ ServiceMap.add(Client.SqlClient, client)
557
504
  ))
558
505
  ).pipe(Layer.provide(Reactivity.layer))
559
506
 
@@ -561,13 +508,21 @@ export const layer = (
561
508
  * @category layers
562
509
  * @since 1.0.0
563
510
  */
564
- export const layerFromPool = (
565
- options: PgClientFromPoolOptions
566
- ): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
567
- Layer.scopedContext(
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(
568
523
  Effect.map(fromPool(options), (client) =>
569
- Context.make(PgClient, client).pipe(
570
- Context.add(Client.SqlClient, client)
524
+ ServiceMap.make(PgClient, client).pipe(
525
+ ServiceMap.add(Client.SqlClient, client)
571
526
  ))
572
527
  ).pipe(Layer.provide(Reactivity.layer))
573
528
 
@@ -608,8 +563,8 @@ export const makeCompiler = (
608
563
  placeholder(undefined),
609
564
  [
610
565
  withoutTransform || transformValue === undefined
611
- ? type.i0
612
- : transformValue(type.i0)
566
+ ? type.paramA
567
+ : transformValue(type.paramA)
613
568
  ]
614
569
  ]
615
570
  }