@effect/sql-pg 0.46.0 → 0.48.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
@@ -7,19 +7,23 @@ import type { Connection } from "@effect/sql/SqlConnection"
7
7
  import { SqlError } from "@effect/sql/SqlError"
8
8
  import type { Custom, Fragment, Primitive } from "@effect/sql/Statement"
9
9
  import * as Statement from "@effect/sql/Statement"
10
+ import * as Arr from "effect/Array"
10
11
  import * as Chunk from "effect/Chunk"
11
12
  import * as Config from "effect/Config"
12
- import type { ConfigError } from "effect/ConfigError"
13
+ import type * as ConfigError from "effect/ConfigError"
13
14
  import * as Context from "effect/Context"
14
15
  import * as Duration from "effect/Duration"
15
16
  import * as Effect from "effect/Effect"
17
+ import * as Fiber from "effect/Fiber"
16
18
  import * as Layer from "effect/Layer"
19
+ import * as Option from "effect/Option"
20
+ import * as RcRef from "effect/RcRef"
17
21
  import * as Redacted from "effect/Redacted"
18
- import type * as Scope from "effect/Scope"
22
+ import * as Scope from "effect/Scope"
19
23
  import * as Stream from "effect/Stream"
20
- import type * as NodeStream from "node:stream"
21
24
  import type { ConnectionOptions } from "node:tls"
22
- import postgres from "postgres"
25
+ import * as Pg from "pg"
26
+ import Cursor from "pg-cursor"
23
27
 
24
28
  const ATTR_DB_SYSTEM_NAME = "db.system.name"
25
29
  const ATTR_DB_NAMESPACE = "db.namespace"
@@ -30,13 +34,13 @@ const ATTR_SERVER_PORT = "server.port"
30
34
  * @category type ids
31
35
  * @since 1.0.0
32
36
  */
33
- export const TypeId: unique symbol = Symbol.for("@effect/sql-pg/PgClient")
37
+ export const TypeId: TypeId = "~@effect/sql-pg/PgClient"
34
38
 
35
39
  /**
36
40
  * @category type ids
37
41
  * @since 1.0.0
38
42
  */
39
- export type TypeId = typeof TypeId
43
+ export type TypeId = "~@effect/sql-pg/PgClient"
40
44
 
41
45
  /**
42
46
  * @category models
@@ -46,7 +50,6 @@ export interface PgClient extends Client.SqlClient {
46
50
  readonly [TypeId]: TypeId
47
51
  readonly config: PgClientConfig
48
52
  readonly json: (_: unknown) => Fragment
49
- readonly array: (_: ReadonlyArray<Primitive>) => Fragment
50
53
  readonly listen: (channel: string) => Stream.Stream<string, SqlError>
51
54
  readonly notify: (channel: string, payload: string) => Effect.Effect<void, SqlError>
52
55
  }
@@ -72,33 +75,11 @@ export interface PgClientConfig {
72
75
  readonly username?: string | undefined
73
76
  readonly password?: Redacted.Redacted | undefined
74
77
 
75
- /**
76
- * A function returning a custom socket to use. This parameter is not documented
77
- * in the postgres.js's type signature. See their
78
- * [readme](https://github.com/porsager/postgres?tab=readme-ov-file#connection-details) instead.
79
- *
80
- * @example
81
- * ```ts
82
- * import { AuthTypes, Connector } from "@google-cloud/cloud-sql-connector";
83
- * import { PgClient } from "@effect/sql-pg";
84
- * import { Config, Effect, Layer } from "effect"
85
- *
86
- * const layer = Effect.gen(function*() {
87
- * const connector = new Connector();
88
- * const clientOpts = yield* Effect.promise(() => connector.getOptions({
89
- * instanceConnectionName: "project:region:instance",
90
- * authType: AuthTypes.IAM,
91
- * }));
92
- * return PgClient.layer({ socket: clientOpts.stream, username: "iam-user" });
93
- * }).pipe(Layer.unwrapEffect)
94
- * ```
95
- */
96
- readonly socket?: (() => NodeStream.Duplex) | undefined
97
-
98
78
  readonly idleTimeout?: Duration.DurationInput | undefined
99
79
  readonly connectTimeout?: Duration.DurationInput | undefined
100
80
 
101
81
  readonly maxConnections?: number | undefined
82
+ readonly minConnections?: number | undefined
102
83
  readonly connectionTTL?: Duration.DurationInput | undefined
103
84
 
104
85
  readonly applicationName?: string | undefined
@@ -107,31 +88,7 @@ export interface PgClientConfig {
107
88
  readonly transformResultNames?: ((str: string) => string) | undefined
108
89
  readonly transformQueryNames?: ((str: string) => string) | undefined
109
90
  readonly transformJson?: boolean | undefined
110
- readonly fetchTypes?: boolean | undefined
111
- readonly prepare?: boolean | undefined
112
- /**
113
- * A callback when postgres has a notice, see
114
- * [readme](https://github.com/porsager/postgres?tab=readme-ov-file#connection-details).
115
- * By default, postgres.js logs these with console.log.
116
- * To silence notices, see the following example:
117
- * @example
118
- * ```ts
119
- * import { PgClient } from "@effect/sql-pg";
120
- * import { Config, Layer } from "effect"
121
- *
122
- * const layer = PgClient.layer({ onnotice: Config.succeed(() => {}) })
123
- * ```
124
- */
125
- readonly onnotice?: (notice: postgres.Notice) => void
126
- readonly types?: Record<string, postgres.PostgresType> | undefined
127
-
128
- readonly debug?: postgres.Options<{}>["debug"] | undefined
129
- }
130
-
131
- type PartialWithUndefined<T> = { [K in keyof T]?: T[K] | undefined }
132
-
133
- interface PostgresOptions extends postgres.Options<{}> {
134
- readonly socket?: (() => NodeStream.Duplex) | undefined
91
+ readonly types?: Pg.CustomTypesConfig | undefined
135
92
  }
136
93
 
137
94
  /**
@@ -153,53 +110,39 @@ export const make = (
153
110
  ).array :
154
111
  undefined
155
112
 
156
- const opts: PartialWithUndefined<PostgresOptions> = {
157
- max: options.maxConnections ?? 10,
158
- max_lifetime: options.connectionTTL
159
- ? Math.round(
160
- Duration.toMillis(Duration.decode(options.connectionTTL)) / 1000
161
- )
162
- : undefined,
163
- idle_timeout: options.idleTimeout
164
- ? Math.round(
165
- Duration.toMillis(Duration.decode(options.idleTimeout)) / 1000
166
- )
167
- : undefined,
168
- connect_timeout: options.connectTimeout
169
- ? Math.round(
170
- Duration.toMillis(Duration.decode(options.connectTimeout)) / 1000
171
- )
172
- : undefined,
173
-
113
+ const pool = new Pg.Pool({
114
+ connectionString: options.url ? Redacted.value(options.url) : undefined,
115
+ user: options.username,
174
116
  host: options.host,
175
- port: options.port,
176
- ssl: options.ssl,
177
- path: options.path,
178
117
  database: options.database,
179
- username: options.username,
180
118
  password: options.password ? Redacted.value(options.password) : undefined,
181
- fetch_types: options.fetchTypes ?? true,
182
- prepare: options.prepare ?? true,
183
- onnotice: options.onnotice,
184
- types: options.types,
185
- debug: options.debug,
186
- connection: {
187
- application_name: options.applicationName ?? "@effect/sql-pg"
188
- },
189
- socket: options.socket
190
- }
119
+ ssl: options.ssl,
120
+ port: options.port,
121
+ connectionTimeoutMillis: options.connectTimeout
122
+ ? Duration.toMillis(options.connectTimeout)
123
+ : undefined,
124
+ idleTimeoutMillis: options.idleTimeout
125
+ ? Duration.toMillis(options.idleTimeout)
126
+ : undefined,
127
+ max: options.maxConnections,
128
+ min: options.minConnections,
129
+ maxLifetimeSeconds: options.connectionTTL
130
+ ? Duration.toSeconds(options.connectionTTL)
131
+ : undefined,
132
+ application_name: options.applicationName ?? "@effect/sql-pg",
133
+ types: options.types
134
+ })
191
135
 
192
- const client = options.url
193
- ? postgres(Redacted.value(options.url), opts as any)
194
- : postgres(opts as any)
136
+ pool.on("error", (_err) => {
137
+ })
195
138
 
196
139
  yield* Effect.acquireRelease(
197
140
  Effect.tryPromise({
198
- try: () => client`select 1`,
141
+ try: () => pool.query("SELECT 1"),
199
142
  catch: (cause) => new SqlError({ cause, message: "PgClient: Failed to connect" })
200
143
  }),
201
144
  () =>
202
- Effect.promise(() => client.end()).pipe(
145
+ Effect.promise(() => pool.end()).pipe(
203
146
  Effect.interruptible,
204
147
  Effect.timeoutOption(1000)
205
148
  )
@@ -215,15 +158,20 @@ export const make = (
215
158
  )
216
159
 
217
160
  class ConnectionImpl implements Connection {
218
- constructor(private readonly pg: postgres.Sql<{}>) {}
161
+ readonly pg: Pg.Pool | Pg.PoolClient
162
+ constructor(pg: Pg.Pool | Pg.PoolClient) {
163
+ this.pg = pg
164
+ }
219
165
 
220
- private run(query: postgres.PendingQuery<any> | postgres.PendingValuesQuery<any>) {
166
+ private run(query: string, params: ReadonlyArray<Primitive>) {
221
167
  return Effect.async<ReadonlyArray<any>, SqlError>((resume) => {
222
- query.then(
223
- (_) => resume(Effect.succeed(_)),
224
- (cause) => resume(new SqlError({ cause, message: "Failed to execute statement" }))
225
- )
226
- return Effect.sync(() => query.cancel())
168
+ this.pg.query(query, params as any, (err, result) => {
169
+ if (err) {
170
+ resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
171
+ } else {
172
+ resume(Effect.succeed(result.rows))
173
+ }
174
+ })
227
175
  })
228
176
  }
229
177
 
@@ -233,17 +181,40 @@ export const make = (
233
181
  transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
234
182
  ) {
235
183
  return transformRows
236
- ? Effect.map(this.run(this.pg.unsafe(sql, params as any)), transformRows)
237
- : this.run(this.pg.unsafe(sql, params as any))
184
+ ? Effect.map(this.run(sql, params), transformRows)
185
+ : this.run(sql, params)
238
186
  }
239
187
  executeRaw(sql: string, params: ReadonlyArray<Primitive>) {
240
- return this.run(this.pg.unsafe(sql, params as any))
188
+ return Effect.async<Pg.Result, SqlError>((resume) => {
189
+ this.pg.query(sql, params as any, (err, result) => {
190
+ if (err) {
191
+ resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
192
+ } else {
193
+ resume(Effect.succeed(result))
194
+ }
195
+ })
196
+ })
241
197
  }
242
198
  executeWithoutTransform(sql: string, params: ReadonlyArray<Primitive>) {
243
- return this.run(this.pg.unsafe(sql, params as any))
199
+ return this.run(sql, params)
244
200
  }
245
201
  executeValues(sql: string, params: ReadonlyArray<Primitive>) {
246
- return this.run(this.pg.unsafe(sql, params as any).values())
202
+ return Effect.async<ReadonlyArray<any>, SqlError>((resume) => {
203
+ this.pg.query(
204
+ {
205
+ text: sql,
206
+ rowMode: "array",
207
+ values: params as Array<string>
208
+ },
209
+ (err, result) => {
210
+ if (err) {
211
+ resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
212
+ } else {
213
+ resume(Effect.succeed(result.rows))
214
+ }
215
+ }
216
+ )
217
+ })
247
218
  }
248
219
  executeUnprepared(
249
220
  sql: string,
@@ -257,38 +228,59 @@ export const make = (
257
228
  params: ReadonlyArray<Primitive>,
258
229
  transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
259
230
  ) {
260
- return Stream.mapChunks(
261
- Stream.fromAsyncIterable(
262
- this.pg.unsafe(sql, params as any).cursor(16) as AsyncIterable<
263
- Array<any>
264
- >,
265
- (cause) => new SqlError({ cause, message: "Failed to execute statement" })
266
- ),
267
- Chunk.flatMap((rows) => Chunk.unsafeFromArray(transformRows ? transformRows(rows) : rows))
231
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
232
+ const self = this
233
+ return Effect.gen(function*() {
234
+ const cursor = yield* Effect.acquireRelease(
235
+ Effect.sync(() => self.pg.query(new Cursor(sql, params as any))),
236
+ (cursor) => Effect.sync(() => cursor.close())
237
+ )
238
+ const pull = Effect.async<Chunk.Chunk<any>, Option.Option<SqlError>>((resume) => {
239
+ cursor.read(128, (err, rows) => {
240
+ if (err) {
241
+ resume(Effect.fail(Option.some(new SqlError({ cause: err, message: "Failed to execute statement" }))))
242
+ } else if (Arr.isNonEmptyArray(rows)) {
243
+ resume(Effect.succeed(Chunk.unsafeFromArray(transformRows ? transformRows(rows) as any : rows)))
244
+ } else {
245
+ resume(Effect.fail(Option.none()))
246
+ }
247
+ })
248
+ })
249
+ return Stream.repeatEffectChunkOption(pull)
250
+ }).pipe(
251
+ Stream.unwrapScoped
268
252
  )
269
253
  }
270
254
  }
271
255
 
256
+ const reserveRaw = Effect.async<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
257
+ const fiber = Option.getOrThrow(Fiber.getCurrentFiber())
258
+ const scope = Context.unsafeGet(fiber.currentContext, Scope.Scope)
259
+ pool.connect((err, client, release) => {
260
+ if (err) {
261
+ resume(Effect.fail(new SqlError({ cause: err, message: "Failed to acquire connection for transaction" })))
262
+ } else {
263
+ resume(Effect.as(Scope.addFinalizer(scope, Effect.sync(release)), client!))
264
+ }
265
+ })
266
+ })
267
+ const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client))
268
+
269
+ const listenClient = yield* RcRef.make({
270
+ acquire: reserveRaw
271
+ })
272
+
272
273
  return Object.assign(
273
274
  yield* Client.make({
274
- acquirer: Effect.succeed(new ConnectionImpl(client)),
275
- transactionAcquirer: Effect.map(
276
- Effect.acquireRelease(
277
- Effect.tryPromise({
278
- try: () => client.reserve(),
279
- catch: (cause) => new SqlError({ cause, message: "Failed to reserve connection" })
280
- }),
281
- (pg) => Effect.sync(() => pg.release())
282
- ),
283
- (_) => new ConnectionImpl(_)
284
- ),
275
+ acquirer: Effect.succeed(new ConnectionImpl(pool)),
276
+ transactionAcquirer: reserve,
285
277
  compiler,
286
278
  spanAttributes: [
287
279
  ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
288
280
  [ATTR_DB_SYSTEM_NAME, "postgresql"],
289
- [ATTR_DB_NAMESPACE, opts.database ?? options.username ?? "postgres"],
290
- [ATTR_SERVER_ADDRESS, opts.host ?? "localhost"],
291
- [ATTR_SERVER_PORT, opts.port ?? 5432]
281
+ [ATTR_DB_NAMESPACE, options.database ?? options.username ?? "postgres"],
282
+ [ATTR_SERVER_ADDRESS, options.host ?? "localhost"],
283
+ [ATTR_SERVER_PORT, options.port ?? 5432]
292
284
  ],
293
285
  transformRows
294
286
  }),
@@ -296,28 +288,42 @@ export const make = (
296
288
  [TypeId]: TypeId as TypeId,
297
289
  config: {
298
290
  ...options,
299
- host: client.options.host[0] ?? undefined,
300
- port: client.options.port[0] ?? undefined,
301
- username: client.options.user,
302
- password: client.options.pass ? Redacted.make(client.options.pass) : undefined,
303
- database: client.options.database
291
+ host: pool.options.host,
292
+ port: pool.options.port,
293
+ username: pool.options.user,
294
+ password: typeof pool.options.password === "string" ? Redacted.make(pool.options.password) : undefined,
295
+ database: pool.options.database
304
296
  },
305
297
  json: (_: unknown) => PgJson(_),
306
- array: (_: ReadonlyArray<Primitive>) => PgArray(_),
307
298
  listen: (channel: string) =>
308
- Stream.asyncPush<string, SqlError>((emit) =>
309
- Effect.acquireRelease(
310
- Effect.tryPromise({
311
- try: () => client.listen(channel, (payload) => emit.single(payload)),
312
- catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
313
- }),
314
- ({ unlisten }) => Effect.promise(() => unlisten())
299
+ Stream.asyncPush<string, SqlError>(Effect.fnUntraced(function*(emit) {
300
+ const client = yield* RcRef.get(listenClient)
301
+ function onNotification(msg: Pg.Notification) {
302
+ if (msg.channel === channel && msg.payload) {
303
+ emit.single(msg.payload)
304
+ }
305
+ }
306
+ yield* Effect.addFinalizer(() =>
307
+ Effect.promise(() => {
308
+ client.off("notification", onNotification)
309
+ return client.query(`UNLISTEN ${Pg.escapeIdentifier(channel)}`)
310
+ })
315
311
  )
316
- ),
312
+ yield* Effect.tryPromise({
313
+ try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
314
+ catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
315
+ })
316
+ client.on("notification", onNotification)
317
+ })),
317
318
  notify: (channel: string, payload: string) =>
318
- Effect.tryPromise({
319
- try: () => client.notify(channel, payload),
320
- catch: (cause) => new SqlError({ cause, message: "Failed to notify" })
319
+ Effect.async<void, SqlError>((resume) => {
320
+ pool.query(`NOTIFY ${Pg.escapeIdentifier(channel)}, $1`, [payload], (err) => {
321
+ if (err) {
322
+ resume(Effect.fail(new SqlError({ cause: err, message: "Failed to notify" })))
323
+ } else {
324
+ resume(Effect.void)
325
+ }
326
+ })
321
327
  })
322
328
  }
323
329
  )
@@ -329,7 +335,7 @@ export const make = (
329
335
  */
330
336
  export const layerConfig = (
331
337
  config: Config.Config.Wrap<PgClientConfig>
332
- ): Layer.Layer<PgClient | Client.SqlClient, ConfigError | SqlError> =>
338
+ ): Layer.Layer<PgClient | Client.SqlClient, ConfigError.ConfigError | SqlError> =>
333
339
  Layer.scopedContext(
334
340
  Config.unwrap(config).pipe(
335
341
  Effect.flatMap(make),
@@ -347,7 +353,7 @@ export const layerConfig = (
347
353
  */
348
354
  export const layer = (
349
355
  config: PgClientConfig
350
- ): Layer.Layer<PgClient | Client.SqlClient, ConfigError | SqlError> =>
356
+ ): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
351
357
  Layer.scopedContext(
352
358
  Effect.map(make(config), (client) =>
353
359
  Context.make(PgClient, client).pipe(
@@ -363,8 +369,6 @@ export const makeCompiler = (
363
369
  transform?: (_: string) => string,
364
370
  transformJson = true
365
371
  ): Statement.Compiler => {
366
- const pg = postgres({ max: 0 })
367
-
368
372
  const transformValue = transformJson && transform
369
373
  ? Statement.defaultTransforms(transform).value
370
374
  : undefined
@@ -393,33 +397,12 @@ export const makeCompiler = (
393
397
  return [
394
398
  placeholder(undefined),
395
399
  [
396
- pg.json(
397
- withoutTransform || transformValue === undefined
398
- ? type.i0
399
- : transformValue(type.i0)
400
- ) as any
400
+ withoutTransform || transformValue === undefined
401
+ ? type.i0
402
+ : transformValue(type.i0)
401
403
  ]
402
404
  ]
403
405
  }
404
- case "PgArray": {
405
- const param = pg.array(type.i0 as any) as any
406
- const first = type.i0[0]
407
- switch (typeof first) {
408
- case "boolean": {
409
- param.type = 1000
410
- break
411
- }
412
- case "number": {
413
- param.type = 1022
414
- break
415
- }
416
- default: {
417
- param.type = 1009
418
- break
419
- }
420
- }
421
- return [placeholder(undefined), [param]]
422
- }
423
406
  }
424
407
  }
425
408
  })
@@ -431,7 +414,7 @@ const escape = Statement.defaultEscape("\"")
431
414
  * @category custom types
432
415
  * @since 1.0.0
433
416
  */
434
- export type PgCustom = PgJson | PgArray
417
+ export type PgCustom = PgJson
435
418
 
436
419
  /**
437
420
  * @category custom types
@@ -443,14 +426,3 @@ interface PgJson extends Custom<"PgJson", unknown> {}
443
426
  * @since 1.0.0
444
427
  */
445
428
  const PgJson = Statement.custom<PgJson>("PgJson")
446
-
447
- /**
448
- * @category custom types
449
- * @since 1.0.0
450
- */
451
- interface PgArray extends Custom<"PgArray", ReadonlyArray<Primitive>> {}
452
- /**
453
- * @category custom types
454
- * @since 1.0.0
455
- */
456
- const PgArray = Statement.custom<PgArray>("PgArray")