@effect/sql-pg 4.0.0-rc.112 → 4.0.0-rc.113

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.
Files changed (60) hide show
  1. package/AGENTS.md +24 -9
  2. package/CLAUDE.md +24 -9
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
  5. package/ai-docs/src/01_effect/01_basics/index.md +5 -3
  6. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
  7. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
  8. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
  9. package/ai-docs/src/03_stream/30_encoding.ts +5 -7
  10. package/ai-docs/src/08_observability/10_logging.ts +1 -1
  11. package/ai-docs/src/70_cli/10_basics.ts +7 -7
  12. package/ai-docs/src/71_ai/10_language-model.ts +2 -2
  13. package/ai-docs/src/71_ai/20_tools.ts +1 -1
  14. package/ai-docs/src/71_ai/30_chat.ts +1 -1
  15. package/dist/PgAuth.d.ts +24 -0
  16. package/dist/PgAuth.d.ts.map +1 -1
  17. package/dist/PgAuth.js +29 -2
  18. package/dist/PgAuth.js.map +1 -1
  19. package/dist/PgClient.d.ts +49 -82
  20. package/dist/PgClient.d.ts.map +1 -1
  21. package/dist/PgClient.js +58 -510
  22. package/dist/PgClient.js.map +1 -1
  23. package/dist/PgConnection.d.ts +177 -0
  24. package/dist/PgConnection.d.ts.map +1 -0
  25. package/dist/PgConnection.js +1786 -0
  26. package/dist/PgConnection.js.map +1 -0
  27. package/dist/PgPool.d.ts +107 -0
  28. package/dist/PgPool.d.ts.map +1 -0
  29. package/dist/PgPool.js +124 -0
  30. package/dist/PgPool.js.map +1 -0
  31. package/dist/PgProtocol.d.ts +63 -30
  32. package/dist/PgProtocol.d.ts.map +1 -1
  33. package/dist/PgProtocol.js +64 -22
  34. package/dist/PgProtocol.js.map +1 -1
  35. package/dist/PgTypes.d.ts +85 -17
  36. package/dist/PgTypes.d.ts.map +1 -1
  37. package/dist/PgTypes.js +166 -169
  38. package/dist/PgTypes.js.map +1 -1
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/connection.d.ts +2 -0
  44. package/dist/internal/connection.d.ts.map +1 -0
  45. package/dist/internal/connection.js +5 -0
  46. package/dist/internal/connection.js.map +1 -0
  47. package/dist/internal/sqlError.d.ts +10 -0
  48. package/dist/internal/sqlError.d.ts.map +1 -0
  49. package/dist/internal/sqlError.js +57 -0
  50. package/dist/internal/sqlError.js.map +1 -0
  51. package/package.json +6 -14
  52. package/src/PgAuth.ts +33 -2
  53. package/src/PgClient.ts +138 -681
  54. package/src/PgConnection.ts +2294 -0
  55. package/src/PgPool.ts +231 -0
  56. package/src/PgProtocol.ts +93 -50
  57. package/src/PgTypes.ts +254 -186
  58. package/src/index.ts +10 -0
  59. package/src/internal/connection.ts +26 -0
  60. package/src/internal/sqlError.ts +75 -0
package/src/PgClient.ts CHANGED
@@ -1,61 +1,33 @@
1
1
  /**
2
- * Connects Effect SQL to PostgreSQL using the `pg` package.
3
- *
4
- * This module provides constructors and layers for building a PostgreSQL
5
- * client from pool settings, a managed `pg.Client`, an existing `pg.Pool`, or
6
- * custom connection code. The client runs Effect SQL queries against
7
- * PostgreSQL, including transactions and streamed results, and adds helpers for
8
- * JSON values and LISTEN/NOTIFY messages. It also maps common PostgreSQL
9
- * failures, such as connection, authentication, constraint, timeout, and
10
- * deadlock errors, into Effect SQL errors.
2
+ * PostgreSQL support for Effect SQL, backed by the native wire protocol client.
11
3
  *
12
4
  * @since 4.0.0
13
5
  */
14
- import * as Arr from "effect/Array"
15
- import * as Cause from "effect/Cause"
16
- import * as Channel from "effect/Channel"
6
+ import type * as Arr from "effect/Array"
17
7
  import * as Config from "effect/Config"
18
8
  import * as Context from "effect/Context"
19
- import * as Duration from "effect/Duration"
9
+ import type * as Duration from "effect/Duration"
20
10
  import * as Effect from "effect/Effect"
21
- import * as Fiber from "effect/Fiber"
22
11
  import * as Layer from "effect/Layer"
23
- import * as Number from "effect/Number"
24
- import * as Option from "effect/Option"
25
- import * as Queue from "effect/Queue"
26
- import * as RcRef from "effect/RcRef"
27
- import * as Redacted from "effect/Redacted"
28
- import * as Scope from "effect/Scope"
29
- import * as Semaphore from "effect/Semaphore"
12
+ import type * as Queue from "effect/Queue"
13
+ import type * as Redacted from "effect/Redacted"
14
+ import type * as Scope from "effect/Scope"
30
15
  import * as Stream from "effect/Stream"
31
16
  import * as Reactivity from "effect/unstable/reactivity/Reactivity"
32
17
  import * as Client from "effect/unstable/sql/SqlClient"
33
- import type { Connection } from "effect/unstable/sql/SqlConnection"
34
- import type * as SqlConnection from "effect/unstable/sql/SqlConnection"
35
- import {
36
- AuthenticationError,
37
- AuthorizationError,
38
- ConnectionError,
39
- ConstraintError,
40
- DeadlockError,
41
- LockTimeoutError,
42
- SerializationError,
43
- SqlError,
44
- SqlSyntaxError,
45
- StatementTimeoutError,
46
- UniqueViolation,
47
- UnknownError
48
- } from "effect/unstable/sql/SqlError"
18
+ import type { Borrower, Connection } from "effect/unstable/sql/SqlConnection"
19
+ import type { SqlError } from "effect/unstable/sql/SqlError"
49
20
  import type { Custom, Fragment } from "effect/unstable/sql/Statement"
50
21
  import * as Statement from "effect/unstable/sql/Statement"
51
22
  import type { Duplex } from "node:stream"
52
23
  import type { ConnectionOptions } from "node:tls"
53
- import * as Pg from "pg"
54
- import * as PgConnString from "pg-connection-string"
55
- import Cursor from "pg-cursor"
24
+ import { validateChannelName } from "./internal/sqlError.ts"
25
+ import * as PgConnection from "./PgConnection.ts"
26
+ import * as PgPool from "./PgPool.ts"
27
+ import * as PgTypes from "./PgTypes.ts"
56
28
 
57
29
  /**
58
- * Runtime type identifier used to mark `PgClient` values.
30
+ * The runtime type identifier for `PgClient`.
59
31
  *
60
32
  * @category type IDs
61
33
  * @since 4.0.0
@@ -63,7 +35,7 @@ import Cursor from "pg-cursor"
63
35
  export const TypeId: TypeId = "~@effect/sql-pg/PgClient"
64
36
 
65
37
  /**
66
- * Type-level identifier used to mark `PgClient` values.
38
+ * The type-level identifier for `PgClient`.
67
39
  *
68
40
  * @category type IDs
69
41
  * @since 4.0.0
@@ -71,7 +43,7 @@ export const TypeId: TypeId = "~@effect/sql-pg/PgClient"
71
43
  export type TypeId = "~@effect/sql-pg/PgClient"
72
44
 
73
45
  /**
74
- * PostgreSQL client service, extending `SqlClient` with JSON parameter fragments and LISTEN/NOTIFY helpers.
46
+ * A PostgreSQL `SqlClient` with JSON and `LISTEN`/`NOTIFY` helpers.
75
47
  *
76
48
  * @category services
77
49
  * @since 4.0.0
@@ -80,16 +52,19 @@ export interface PgClient extends Client.SqlClient {
80
52
  readonly [TypeId]: TypeId
81
53
  readonly config: PgClientConfig
82
54
  readonly json: (_: unknown) => Fragment
83
- readonly listen: (channel: string) => Stream.Stream<string, SqlError>
55
+ /**
56
+ * Registers a channel listener and returns its non-empty payload queue after
57
+ * PostgreSQL confirms `LISTEN`. The listener holds a connection until the
58
+ * scope closes.
59
+ */
60
+ readonly listen: (
61
+ channel: string
62
+ ) => Effect.Effect<Queue.Dequeue<PgConnection.Notification>, SqlError, Scope.Scope>
84
63
  readonly notify: (channel: string, payload: string) => Effect.Effect<void, SqlError>
85
64
  }
86
65
 
87
66
  /**
88
- * Service tag for the PostgreSQL client service.
89
- *
90
- * **When to use**
91
- *
92
- * Use to access or provide a PostgreSQL client through the Effect context.
67
+ * The service tag for `PgClient`.
93
68
  *
94
69
  * @category services
95
70
  * @since 4.0.0
@@ -97,7 +72,7 @@ export interface PgClient extends Client.SqlClient {
97
72
  export const PgClient = Context.Service<PgClient>("@effect/sql-pg/PgClient")
98
73
 
99
74
  /**
100
- * Configuration for a PostgreSQL client, including connection, TLS, custom stream, application name, type parser, JSON transform, and query/result name transform options.
75
+ * Connection and query settings for a PostgreSQL client.
101
76
  *
102
77
  * @category models
103
78
  * @since 4.0.0
@@ -123,11 +98,32 @@ export interface PgClientConfig {
123
98
  readonly transformResultNames?: ((str: string) => string) | undefined
124
99
  readonly transformQueryNames?: ((str: string) => string) | undefined
125
100
  readonly transformJson?: boolean | undefined
126
- readonly types?: Pg.CustomTypesConfig | undefined
101
+ readonly types?: PgTypes.Registry | undefined
102
+ /**
103
+ * Pipelines queries from multiple fibers on each pooled connection.
104
+ * Transactions, streams, and listeners still reserve a connection.
105
+ */
106
+ readonly multiplex?: boolean | undefined
107
+ /**
108
+ * How many statements may share one connection when `multiplex` is on.
109
+ * Defaults to `32`. Higher values trade tail latency for throughput: the
110
+ * statements sharing a connection are pipelined into one write, and they
111
+ * also queue behind the slowest of them.
112
+ */
113
+ readonly multiplexConcurrency?: number | undefined
114
+ /**
115
+ * Caches prepared statements by name. Enabled by default. Disable it for
116
+ * poolers that cannot preserve named statements between queries.
117
+ */
118
+ readonly prepare?: boolean | undefined
119
+ /** How many statements a connection keeps prepared. Defaults to `100`. */
120
+ readonly preparedStatementCacheSize?: number | undefined
121
+ /** Maximum backend message size in bytes. Defaults to 16 MiB. */
122
+ readonly maxMessageSize?: number | undefined
127
123
  }
128
124
 
129
125
  /**
130
- * PostgreSQL pool configuration, extending `PgClientConfig` with idle timeout, pool size, and connection lifetime settings.
126
+ * PostgreSQL client settings with connection pool limits and timeouts.
131
127
  *
132
128
  * @category models
133
129
  * @since 4.0.0
@@ -141,73 +137,23 @@ export interface PgPoolConfig extends PgClientConfig {
141
137
  }
142
138
 
143
139
  /**
144
- * Creates a scoped PostgreSQL client backed by a managed `pg` connection pool.
140
+ * Creates a scoped PostgreSQL client backed by a connection pool.
145
141
  *
146
142
  * @category constructors
147
143
  * @since 4.0.0
148
144
  */
149
145
  export const make = (options: PgPoolConfig): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
150
- fromPool({
151
- ...options,
152
- acquire: Effect.gen(function*() {
153
- const pool = new Pg.Pool({
154
- connectionString: options.url ? Redacted.value(options.url) : undefined,
155
- user: options.username,
156
- host: options.host,
157
- database: options.database,
158
- password: options.password ? Redacted.value(options.password) : undefined,
159
- ssl: options.ssl,
160
- port: options.port,
161
- ...(options.stream ? { stream: options.stream } : {}),
162
- connectionTimeoutMillis: options.connectTimeout
163
- ? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout))
164
- : undefined,
165
- idleTimeoutMillis: options.idleTimeout
166
- ? Duration.toMillis(Duration.fromInputUnsafe(options.idleTimeout))
167
- : undefined,
168
- max: options.maxConnections,
169
- min: options.minConnections,
170
- maxLifetimeSeconds: options.connectionTTL
171
- ? Duration.toSeconds(Duration.fromInputUnsafe(options.connectionTTL))
172
- : undefined,
173
- application_name: options.applicationName ?? "@effect/sql-pg",
174
- types: options.types
175
- })
176
-
177
- pool.on("error", (_err) => {})
178
-
179
- yield* Effect.acquireRelease(
180
- Effect.tryPromise({
181
- try: () => pool.query("SELECT 1"),
182
- catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
183
- }),
184
- () =>
185
- Effect.promise(() => pool.end()).pipe(
186
- Effect.timeoutOption(1000)
187
- ),
188
- { interruptible: true }
189
- ).pipe(
190
- Effect.timeoutOrElse({
191
- duration: options.connectTimeout ?? Duration.seconds(5),
192
- orElse: () =>
193
- Effect.fail(
194
- new SqlError({
195
- reason: new ConnectionError({
196
- cause: new Error("Connection timed out"),
197
- message: "PgClient: Connection timed out",
198
- operation: "connect"
199
- })
200
- })
201
- )
202
- })
203
- )
204
-
205
- return pool
206
- })
207
- })
146
+ Effect.flatMap(PgPool.make(options), (pool) =>
147
+ makeImpl({
148
+ acquirer: Effect.map(pool.get, makeConnection),
149
+ borrower: (f) => pool.use((connection) => f(makeConnection(connection))),
150
+ transactionAcquirer: Effect.map(pool.reserve, makeConnection),
151
+ listenAcquirer: pool.reserve,
152
+ config: options
153
+ }))
208
154
 
209
155
  /**
210
- * Creates a scoped PostgreSQL client backed by a managed single `pg` client, optionally acquiring a separate client for streaming and LISTEN operations.
156
+ * Creates a scoped PostgreSQL client backed by one connection.
211
157
  *
212
158
  * @category constructors
213
159
  * @since 4.0.0
@@ -215,398 +161,62 @@ export const make = (options: PgPoolConfig): Effect.Effect<PgClient, SqlError, S
215
161
  export const makeClient = (
216
162
  options: PgClientConfig & {
217
163
  /**
218
- * Whether to acquire a separate client for each sql.stream / sql.listen
164
+ * Opens a separate connection for each stream and listener when enabled.
219
165
  */
220
166
  readonly acquireForStream?: boolean | undefined
221
167
  }
222
- ): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> => {
223
- function onError() {}
224
- return fromClient({
225
- ...options,
226
- acquire: Effect.acquireRelease(
227
- Effect.tryPromise({
228
- try: async () => {
229
- const client = new Pg.Client({
230
- connectionString: options.url ? Redacted.value(options.url) : undefined,
231
- user: options.username,
232
- host: options.host,
233
- database: options.database,
234
- password: options.password ? Redacted.value(options.password) : undefined,
235
- ssl: options.ssl,
236
- port: options.port,
237
- ...(options.stream ? { stream: options.stream } : {}),
238
- application_name: options.applicationName ?? "@effect/sql-pg",
239
- types: options.types
240
- })
241
- client.on("error", onError)
242
- await client.connect()
243
- return client
244
- },
245
- catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
246
- }),
247
- (client) =>
248
- Effect.promise(() => {
249
- client.off("error", onError)
250
- return client.end()
251
- }).pipe(
252
- Effect.timeoutOption(1000)
253
- ),
254
- { interruptible: true }
255
- ).pipe(
256
- Effect.timeoutOrElse({
257
- duration: options.connectTimeout ?? Duration.seconds(5),
258
- orElse: () =>
259
- Effect.fail(
260
- new SqlError({
261
- reason: new ConnectionError({
262
- cause: new Error("Connection timed out"),
263
- message: "PgClient: Connection timed out",
264
- operation: "connect"
265
- })
266
- })
267
- )
268
- })
269
- ),
270
- acquireForStream: options.acquireForStream ?? false
271
- })
272
- }
273
-
274
- /**
275
- * Builds a PostgreSQL client from a scoped `pg` pool acquisition effect, deriving transaction, streaming, and LISTEN/NOTIFY support from that pool.
276
- *
277
- * @category constructors
278
- * @since 4.0.0
279
- */
280
- export const fromPool = Effect.fnUntraced(function*(
281
- options: {
282
- readonly acquire: Effect.Effect<Pg.Pool, SqlError, Scope.Scope>
283
-
284
- readonly applicationName?: string | undefined
285
- readonly spanAttributes?: Record<string, unknown> | undefined
286
-
287
- readonly transformResultNames?: ((str: string) => string) | undefined
288
- readonly transformQueryNames?: ((str: string) => string) | undefined
289
- readonly transformJson?: boolean | undefined
290
- readonly types?: Pg.CustomTypesConfig | undefined
291
- }
292
- ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
293
- const pool = yield* options.acquire
294
-
295
- const makeConection = (client?: Pg.PoolClient) =>
296
- new ConnectionImpl(
297
- function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
298
- if (client !== undefined) {
299
- return Effect.callback<A, SqlError>((resume) => {
300
- f(client!, resume)
301
- return makeCancel(pool, client!)
302
- })
303
- }
304
- return Effect.callback<A, SqlError>((resume) => {
305
- let done = false
306
- let cancel: Effect.Effect<void> | undefined = undefined
307
- let client: Pg.PoolClient | undefined = undefined
308
- function onError(cause: Error) {
309
- cleanup(cause)
310
- resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
311
- }
312
- function cleanup(cause?: Error) {
313
- if (!done) client?.release(cause)
314
- done = true
315
- client?.off("error", onError)
316
- }
317
- pool.connect((cause, client_) => {
318
- if (cause) {
319
- return resume(
320
- Effect.fail(
321
- new SqlError({
322
- reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
323
- })
324
- )
325
- )
326
- } else if (!client_) {
327
- return resume(
328
- Effect.fail(
329
- new SqlError({
330
- reason: new ConnectionError({
331
- message: "Failed to acquire connection",
332
- cause: new Error("No client returned"),
333
- operation: "acquireConnection"
334
- })
335
- })
336
- )
337
- )
338
- } else if (done) {
339
- client_.release()
340
- return
341
- }
342
- client = client_
343
- client.once("error", onError)
344
- cancel = makeCancel(pool, client)
345
- f(client, (eff) => {
346
- cleanup()
347
- resume(eff)
348
- })
349
- })
350
- return Effect.suspend(() => {
351
- if (!cancel) {
352
- cleanup()
353
- return Effect.void
354
- }
355
- return Effect.ensuring(cancel, Effect.sync(cleanup))
356
- })
357
- })
358
- },
359
- client ? Effect.succeed(client) : reserveRaw
360
- )
361
-
362
- const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
363
- const fiber = Fiber.getCurrent()!
364
- const scope = Context.getUnsafe(fiber.context, Scope.Scope)
365
- let cause: Error | undefined = undefined
366
- function onError(cause_: Error) {
367
- cause = cause_
368
- }
369
- pool.connect((err, client, release) => {
370
- if (err) {
371
- return resume(
372
- Effect.fail(
373
- new SqlError({
374
- reason: classifyError(
375
- err,
376
- "Failed to acquire connection for transaction",
377
- "acquireConnection"
378
- )
379
- })
380
- )
381
- )
382
- } else if (!client) {
383
- return resume(
384
- Effect.fail(
385
- new SqlError({
386
- reason: new ConnectionError({
387
- message: "Failed to acquire connection for transaction",
388
- cause: new Error("No client returned"),
389
- operation: "acquireConnection"
390
- })
391
- })
392
- )
393
- )
394
- }
395
- client.on("error", onError)
396
- resume(Effect.as(
397
- Scope.addFinalizer(
398
- scope,
399
- Effect.sync(() => {
400
- client.off("error", onError)
401
- release(cause)
402
- })
403
- ),
404
- client
405
- ))
406
- })
407
- })
408
- const reserve = Effect.map(reserveRaw, makeConection)
409
-
410
- const onListenClientError = (_: Error) => {
411
- }
412
-
413
- const listenAcquirer = yield* RcRef.make({
414
- acquire: Effect.acquireRelease(
415
- Effect.tryPromise({
416
- try: async () => {
417
- const client = new Pg.Client(pool.options)
418
- await client.connect()
419
- client.on("error", onListenClientError)
420
- return client
421
- },
422
- catch: (cause) =>
423
- new SqlError({
424
- reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
425
- })
426
- }),
427
- (client) =>
428
- Effect.promise(() => {
429
- client.off("error", onListenClientError)
430
- return client.end()
431
- }).pipe(
432
- Effect.timeoutOption(1000)
433
- ),
434
- { interruptible: true }
435
- )
436
- })
437
-
438
- let config: PgClientConfig = {
439
- url: pool.options.connectionString ? Redacted.make(pool.options.connectionString) : undefined,
440
- host: pool.options.host,
441
- port: pool.options.port,
442
- database: pool.options.database,
443
- username: pool.options.user,
444
- password: typeof pool.options.password === "string" ? Redacted.make(pool.options.password) : undefined,
445
- ssl: pool.options.ssl,
446
- applicationName: pool.options.application_name,
447
- types: pool.options.types
448
- }
449
- if (pool.options.connectionString) {
450
- // @effect-diagnostics-next-line tryCatchInEffectGen:off
451
- try {
452
- const parsed = PgConnString.parse(pool.options.connectionString)
453
- config = {
454
- ...config,
455
- host: config.host ?? parsed.host ?? undefined,
456
- port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
457
- username: config.username ?? parsed.user ?? undefined,
458
- password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
459
- database: config.database ?? parsed.database ?? undefined
460
- }
461
- } catch {
462
- //
463
- }
464
- }
465
-
466
- return yield* makeWith({
467
- acquirer: Effect.succeed(makeConection()),
468
- transactionAcquirer: reserve,
469
- listenAcquirer: RcRef.get(listenAcquirer),
470
- config,
471
- spanAttributes: options.spanAttributes,
472
- transformResultNames: options.transformResultNames,
473
- transformQueryNames: options.transformQueryNames,
474
- transformJson: options.transformJson
475
- })
476
- })
477
-
478
- /**
479
- * Builds a PostgreSQL client from a scoped `pg` client acquisition effect, serializing access when sharing the client and optionally using separate clients for streams and LISTEN.
480
- *
481
- * @category constructors
482
- * @since 4.0.0
483
- */
484
- export const fromClient = Effect.fnUntraced(function*(
485
- options: {
486
- readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
487
-
488
- /**
489
- * Whether to acquire a separate client for each sql.stream / sql.listen.
490
- */
491
- readonly acquireForStream: boolean
492
-
493
- readonly applicationName?: string | undefined
494
- readonly spanAttributes?: Record<string, unknown> | undefined
495
-
496
- readonly transformResultNames?: ((str: string) => string) | undefined
497
- readonly transformQueryNames?: ((str: string) => string) | undefined
498
- readonly transformJson?: boolean | undefined
499
- readonly types?: Pg.CustomTypesConfig | undefined
500
- }
501
- ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
502
- function onError() {}
503
- const acquireWithErrorHandler = options.acquire.pipe(
504
- Effect.tap((client) => {
505
- client.on("error", onError)
506
- return Effect.addFinalizer(() => {
507
- client.off("error", onError)
508
- return Effect.void
509
- })
510
- })
511
- )
512
- const client = yield* acquireWithErrorHandler
513
-
514
- const semaphore = Semaphore.makeUnsafe(1)
515
- let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
516
- Effect.as(semaphore.take(1), client),
517
- () => semaphore.release(1)
518
- )
519
-
520
- const makeConection = (client: Pg.Client) =>
521
- new ConnectionImpl(
522
- function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
523
- return Effect.callback<A, SqlError>((resume) => {
524
- f(client, resume)
525
- })
526
- },
527
- streamClient
528
- )
529
- const connection = makeConection(client)
530
- const acquirer = semaphore.withPermit(Effect.succeed(connection))
531
- const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
532
- const fiber = Fiber.getCurrent()!
533
- const scope = Context.getUnsafe(fiber.context, Scope.Scope)
534
- return Effect.as(
535
- Effect.tap(
536
- restore(semaphore.take(1)),
537
- () => Scope.addFinalizer(scope, semaphore.release(1))
538
- ),
539
- connection
540
- )
541
- })
542
-
543
- const config: PgClientConfig = {
544
- ...options,
545
- host: client.host,
546
- port: client.port,
547
- database: client.database,
548
- username: client.user,
549
- password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
550
- ssl: client.ssl
551
- }
552
-
553
- return yield* makeWith({
554
- acquirer,
555
- transactionAcquirer,
556
- listenAcquirer: streamClient,
557
- config,
558
- spanAttributes: options.spanAttributes,
559
- transformResultNames: options.transformResultNames,
560
- transformQueryNames: options.transformQueryNames,
561
- transformJson: options.transformJson
562
- })
563
- })
168
+ ): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
169
+ Effect.flatMap(PgConnection.make(options), (connection) =>
170
+ makeImpl({
171
+ acquirer: Effect.succeed(makeConnection(
172
+ connection,
173
+ options.acquireForStream ? PgConnection.make(options) : undefined
174
+ )),
175
+ transactionAcquirer: Effect.map(connection.pin, makeConnection),
176
+ listenAcquirer: options.acquireForStream ? PgConnection.make(options) : connection.pin,
177
+ config: options
178
+ }))
564
179
 
565
180
  /**
566
- * Creates a `PgClient` from SQL connection acquirers, a LISTEN acquirer, client configuration, and transformation options.
567
- *
568
- * **When to use**
569
- *
570
- * Use to build a PostgreSQL client from custom connection acquisition logic
571
- * instead of the built-in pool or single-client constructors.
572
- *
573
- * @category constructors
574
- * @since 4.0.0
181
+ * Builds the shared SQL facade around native connection acquirers.
575
182
  */
576
- export const makeWith = Effect.fnUntraced(function*(
183
+ const makeImpl = Effect.fnUntraced(function*(
577
184
  options: {
578
- readonly acquirer: SqlConnection.Acquirer
579
- readonly transactionAcquirer: SqlConnection.Acquirer
580
- readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
581
-
185
+ readonly acquirer: Effect.Effect<Connection, SqlError, Scope.Scope>
186
+ readonly borrower?: Borrower | undefined
187
+ readonly transactionAcquirer: Effect.Effect<Connection, SqlError, Scope.Scope>
188
+ readonly listenAcquirer: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope>
582
189
  readonly config: PgClientConfig
583
- readonly spanAttributes?: Record<string, unknown> | undefined
584
-
585
- readonly transformResultNames?: ((str: string) => string) | undefined
586
- readonly transformQueryNames?: ((str: string) => string) | undefined
587
- readonly transformJson?: boolean | undefined
588
190
  }
589
191
  ): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
192
+ const config = options.config
590
193
  const compiler = makeCompiler(
591
- options.transformQueryNames,
592
- options.transformJson
194
+ config.transformQueryNames,
195
+ config.transformJson
593
196
  )
594
- const transformRows = options.transformResultNames ?
197
+ const transformRows = config.transformResultNames ?
595
198
  Statement.defaultTransforms(
596
- options.transformResultNames,
597
- options.transformJson
199
+ config.transformResultNames,
200
+ config.transformJson
598
201
  ).array :
599
202
  undefined
600
203
 
601
- const config = options.config
204
+ const listen = (
205
+ channel: string
206
+ ): Effect.Effect<Queue.Dequeue<PgConnection.Notification>, SqlError, Scope.Scope> =>
207
+ Effect.flatMap(options.listenAcquirer, (connection) => connection.listen(channel))
602
208
 
603
209
  return Object.assign(
604
210
  yield* Client.make({
605
211
  acquirer: options.acquirer,
212
+ borrower: options.borrower,
213
+ // Postgres prepares transaction control like anything else, and a client
214
+ // with preparation turned off falls back to the unnamed path anyway.
215
+ prepareTransactionControls: true,
606
216
  transactionAcquirer: options.transactionAcquirer,
607
217
  compiler,
608
218
  spanAttributes: [
609
- ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
219
+ ...(config.spanAttributes ? Object.entries(config.spanAttributes) : []),
610
220
  [ATTR_DB_SYSTEM_NAME, "postgresql"],
611
221
  [ATTR_DB_NAMESPACE, config.database ?? config.username ?? "postgres"],
612
222
  [ATTR_SERVER_ADDRESS, config.host ?? "localhost"],
@@ -616,70 +226,36 @@ export const makeWith = Effect.fnUntraced(function*(
616
226
  }),
617
227
  {
618
228
  [TypeId]: TypeId as TypeId,
619
- config: options.config,
229
+ config,
620
230
  json: (_: unknown) => Statement.fragment([PgJson(_)]),
621
- listen: (channel: string) =>
622
- Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
623
- const client = yield* options.listenAcquirer
624
- function onNotification(msg: Pg.Notification) {
625
- if (msg.channel === channel && msg.payload) {
626
- Queue.offerUnsafe(queue, msg.payload)
627
- }
628
- }
629
- yield* Effect.addFinalizer(() =>
630
- Effect.promise(() => {
631
- client.off("notification", onNotification)
632
- return client.query(`UNLISTEN ${Pg.escapeIdentifier(channel)}`)
633
- })
634
- )
635
- yield* Effect.tryPromise({
636
- try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
637
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
638
- })
639
- client.on("notification", onNotification)
640
- })),
641
- notify: (channel: string, payload: string) =>
642
- Effect.asVoid(Effect.scoped(Effect.flatMap(
643
- options.acquirer,
644
- (conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
645
- )))
231
+ listen,
232
+ notify: (channel: string, payload: string) => {
233
+ const channelError = validateChannelName(channel, "notify")
234
+ return channelError !== undefined ?
235
+ Effect.fail(channelError) :
236
+ Effect.asVoid(Effect.scoped(Effect.flatMap(
237
+ options.acquirer,
238
+ (conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
239
+ )))
240
+ }
646
241
  }
647
242
  )
648
243
  })
649
244
 
650
245
  class ConnectionImpl implements Connection {
246
+ readonly connection: PgConnection.PgConnection
247
+ readonly streamAcquirer: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> | undefined
248
+
651
249
  constructor(
652
- runWithClient: <A>(
653
- f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
654
- ) => Effect.Effect<A, SqlError>,
655
- reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
250
+ connection: PgConnection.PgConnection,
251
+ streamAcquirer?: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> | undefined
656
252
  ) {
657
- this.runWithClient = runWithClient
658
- this.reserve = reserve
253
+ this.connection = connection
254
+ this.streamAcquirer = streamAcquirer
659
255
  }
660
256
 
661
- private readonly runWithClient: <A>(
662
- f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
663
- ) => Effect.Effect<A, SqlError>
664
- private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
665
-
666
- private run(query: string, params: ReadonlyArray<unknown>) {
667
- return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
668
- client.query(query, params as any, (err, result) => {
669
- if (err) {
670
- resume(
671
- Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
672
- )
673
- } else {
674
- // Multi-statement queries return an array of results
675
- resume(Effect.succeed(
676
- Array.isArray(result)
677
- ? result.map((r) => r.rows ?? [])
678
- : result.rows ?? []
679
- ))
680
- }
681
- })
682
- })
257
+ private run(query: string, params: ReadonlyArray<unknown>, prepare = true) {
258
+ return Effect.map(this.connection.query(query, params, prepare), (result) => result.rows)
683
259
  }
684
260
 
685
261
  execute(
@@ -692,102 +268,48 @@ class ConnectionImpl implements Connection {
692
268
  : this.run(sql, params)
693
269
  }
694
270
  executeRaw(sql: string, params: ReadonlyArray<unknown>) {
695
- return this.runWithClient<Pg.Result>((client, resume) => {
696
- client.query(sql, params as any, (err, result) => {
697
- if (err) {
698
- resume(
699
- Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
700
- )
701
- } else {
702
- resume(Effect.succeed(result))
703
- }
704
- })
705
- })
271
+ return this.connection.query(sql, params)
706
272
  }
707
273
  executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
708
274
  return this.run(sql, params)
709
275
  }
710
276
  executeValues(sql: string, params: ReadonlyArray<unknown>) {
711
- return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
712
- client.query(
713
- {
714
- text: sql,
715
- rowMode: "array",
716
- values: params as Array<string>
717
- },
718
- (err, result) => {
719
- if (err) {
720
- resume(
721
- Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
722
- )
723
- } else {
724
- resume(Effect.succeed(result.rows))
725
- }
726
- }
727
- )
728
- })
277
+ return this.connection.queryValues(sql, params)
729
278
  }
730
279
  executeValuesUnprepared(sql: string, params: ReadonlyArray<unknown>) {
731
- return this.executeValues(sql, params)
280
+ return this.connection.queryValues(sql, params, false)
732
281
  }
733
282
  executeUnprepared(
734
283
  sql: string,
735
284
  params: ReadonlyArray<unknown>,
736
285
  transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
737
286
  ) {
738
- return this.execute(sql, params, transformRows)
287
+ const operation = this.run(sql, params, false)
288
+ return transformRows
289
+ ? Effect.map(operation, transformRows)
290
+ : operation
739
291
  }
740
292
  executeStream(
741
293
  sql: string,
742
294
  params: ReadonlyArray<unknown>,
743
295
  transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
744
296
  ) {
745
- // oxlint-disable-next-line @typescript-eslint/no-this-alias
746
- const self = this
747
- return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
748
- const client = yield* Scope.provide(self.reserve, scope)
749
- yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
750
- const cursor = client.query(new Cursor(sql, params as any))
751
- // @effect-diagnostics-next-line returnEffectInGen:off
752
- return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
753
- cursor.read(128, (err, rows) => {
754
- if (err) {
755
- resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
756
- } else if (Arr.isArrayNonEmpty(rows)) {
757
- resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
758
- } else {
759
- resume(Cause.done())
760
- }
761
- })
762
- })
763
- })))
297
+ const stream = this.streamAcquirer === undefined
298
+ ? this.connection.stream(sql, params)
299
+ : Stream.unwrap(Effect.map(this.streamAcquirer, (connection) => connection.stream(sql, params)))
300
+ return transformRows
301
+ ? Stream.mapArray(stream, (rows) => transformRows(rows) as Arr.NonEmptyReadonlyArray<PgConnection.Row>)
302
+ : stream
764
303
  }
765
304
  }
766
305
 
767
- const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
768
- const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
769
- if (cancelEffects.has(client)) {
770
- return cancelEffects.get(client)!
771
- }
772
- const processId = (client as any).processID
773
- const eff = processId !== undefined
774
- // query cancelation is best-effort, so we don't fail if it doesn't work
775
- ? Effect.callback<void>((resume) => {
776
- if (pool.ending) return resume(Effect.void)
777
- pool.query(`SELECT pg_cancel_backend(${processId})`, () => {
778
- resume(Effect.void)
779
- })
780
- }).pipe(
781
- Effect.interruptible,
782
- Effect.timeoutOption(5000)
783
- )
784
- : undefined
785
- cancelEffects.set(client, eff)
786
- return eff
787
- }
306
+ const makeConnection = (
307
+ connection: PgConnection.PgConnection,
308
+ streamAcquirer?: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> | undefined
309
+ ): Connection => new ConnectionImpl(connection, streamAcquirer)
788
310
 
789
311
  /**
790
- * Creates a layer from an effect that acquires a `PgClient`, providing both `PgClient` and `SqlClient`.
312
+ * Provides both `PgClient` and `SqlClient` from an acquisition effect.
791
313
  *
792
314
  * @category layers
793
315
  * @since 4.0.0
@@ -803,14 +325,12 @@ export const layerFrom = <E, R>(
803
325
  ).pipe(Layer.provide(Reactivity.layer)) as any
804
326
 
805
327
  /**
806
- * Creates a layer from a `Config`-wrapped PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
328
+ * Creates a client layer from wrapped pool configuration.
807
329
  *
808
330
  * @category layers
809
331
  * @since 4.0.0
810
332
  */
811
- export const layerConfig: (
812
- config: Config.Wrap<PgPoolConfig>
813
- ) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
333
+ export const layerConfig = (
814
334
  config: Config.Wrap<PgPoolConfig>
815
335
  ): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
816
336
  layerFrom(Effect.flatMap(
@@ -819,7 +339,7 @@ export const layerConfig: (
819
339
  ))
820
340
 
821
341
  /**
822
- * Creates a layer from a concrete PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
342
+ * Creates a client layer from pool configuration.
823
343
  *
824
344
  * @category layers
825
345
  * @since 4.0.0
@@ -829,7 +349,7 @@ export const layer = (
829
349
  ): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
830
350
 
831
351
  /**
832
- * Creates the PostgreSQL statement compiler, using `$1` placeholders, double-quoted identifiers, PostgreSQL returning clauses, and optional JSON value transformation.
352
+ * Creates the PostgreSQL statement compiler.
833
353
  *
834
354
  * @category constructors
835
355
  * @since 4.0.0
@@ -863,13 +383,12 @@ export const makeCompiler = (
863
383
  onCustom(type, placeholder, withoutTransform) {
864
384
  switch (type.kind) {
865
385
  case "PgJson": {
386
+ const value = withoutTransform || transformValue === undefined
387
+ ? type.paramA
388
+ : transformValue(type.paramA)
866
389
  return [
867
390
  placeholder(undefined),
868
- [
869
- withoutTransform || transformValue === undefined
870
- ? type.paramA
871
- : transformValue(type.paramA)
872
- ]
391
+ [PgTypes.jsonb(value)]
873
392
  ]
874
393
  }
875
394
  }
@@ -880,7 +399,7 @@ export const makeCompiler = (
880
399
  const escape = Statement.defaultEscape("\"")
881
400
 
882
401
  /**
883
- * PostgreSQL-specific custom statement fragments supported by the compiler, currently JSON parameter fragments.
402
+ * PostgreSQL-specific statement fragments.
884
403
  *
885
404
  * @category models
886
405
  * @since 4.0.0
@@ -902,65 +421,3 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
902
421
  const ATTR_DB_NAMESPACE = "db.namespace"
903
422
  const ATTR_SERVER_ADDRESS = "server.address"
904
423
  const ATTR_SERVER_PORT = "server.port"
905
-
906
- const pgCodeFromCause = (cause: unknown): string | undefined => {
907
- if (typeof cause !== "object" || cause === null || !("code" in cause)) {
908
- return undefined
909
- }
910
- const code = cause.code
911
- return typeof code === "string" ? code : undefined
912
- }
913
-
914
- const pgConstraintFromCause = (cause: unknown): string => {
915
- if (typeof cause !== "object" || cause === null || !("constraint" in cause)) {
916
- return "unknown"
917
- }
918
- const constraint = cause.constraint
919
- if (typeof constraint !== "string") {
920
- return "unknown"
921
- }
922
- const normalized = constraint.trim()
923
- return normalized.length === 0 ? "unknown" : normalized
924
- }
925
-
926
- const classifyError = (
927
- cause: unknown,
928
- message: string,
929
- operation: string
930
- ) => {
931
- const props = { cause, message, operation }
932
- const code = pgCodeFromCause(cause)
933
- if (code !== undefined) {
934
- if (code.startsWith("08")) {
935
- return new ConnectionError(props)
936
- }
937
- if (code.startsWith("28")) {
938
- return new AuthenticationError(props)
939
- }
940
- if (code === "42501") {
941
- return new AuthorizationError(props)
942
- }
943
- if (code.startsWith("42")) {
944
- return new SqlSyntaxError(props)
945
- }
946
- if (code === "23505") {
947
- return new UniqueViolation({ ...props, constraint: pgConstraintFromCause(cause) })
948
- }
949
- if (code.startsWith("23")) {
950
- return new ConstraintError(props)
951
- }
952
- if (code === "40P01") {
953
- return new DeadlockError(props)
954
- }
955
- if (code === "40001") {
956
- return new SerializationError(props)
957
- }
958
- if (code === "55P03") {
959
- return new LockTimeoutError(props)
960
- }
961
- if (code === "57014") {
962
- return new StatementTimeoutError(props)
963
- }
964
- }
965
- return new UnknownError(props)
966
- }