@effect/sql-pg 4.0.0-beta.7 → 4.0.0-beta.70

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/dist/PgClient.js CHANGED
@@ -1,45 +1,66 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * PostgreSQL client implementation for Effect SQL, backed by `pg`.
3
+ *
4
+ * This module exposes constructors for creating a scoped `PgClient` from a
5
+ * managed `pg` pool, a single managed `pg` client, or lower-level connection
6
+ * acquirers. The resulting service can be provided as both `PgClient` and the
7
+ * generic `SqlClient`, and is intended for application database access,
8
+ * migrations, transactional workflows, row streaming, JSON parameters, and
9
+ * PostgreSQL LISTEN/NOTIFY integration.
10
+ *
11
+ * Pool-backed clients acquire connections per operation and reserve dedicated
12
+ * connections for transactions and cursor streams. Clients built from one
13
+ * `pg.Client` serialize shared access; enable `acquireForStream` when streams
14
+ * or listeners need their own client instead of sharing the query connection.
15
+ * LISTEN uses a scoped long-lived client and automatically issues `UNLISTEN`
16
+ * when the stream scope closes, so listeners should be scoped for as long as
17
+ * notifications are needed.
18
+ *
19
+ * @since 4.0.0
3
20
  */
4
21
  import * as Arr from "effect/Array";
5
22
  import * as Cause from "effect/Cause";
6
23
  import * as Channel from "effect/Channel";
7
24
  import * as Config from "effect/Config";
25
+ import * as Context from "effect/Context";
8
26
  import * as Duration from "effect/Duration";
9
27
  import * as Effect from "effect/Effect";
10
28
  import * as Fiber from "effect/Fiber";
11
29
  import * as Layer from "effect/Layer";
12
30
  import * as Number from "effect/Number";
31
+ import * as Option from "effect/Option";
13
32
  import * as Queue from "effect/Queue";
14
33
  import * as RcRef from "effect/RcRef";
15
34
  import * as Redacted from "effect/Redacted";
16
35
  import * as Scope from "effect/Scope";
17
- import * as ServiceMap from "effect/ServiceMap";
36
+ import * as Semaphore from "effect/Semaphore";
18
37
  import * as Stream from "effect/Stream";
19
38
  import * as Reactivity from "effect/unstable/reactivity/Reactivity";
20
39
  import * as Client from "effect/unstable/sql/SqlClient";
21
- import { SqlError } from "effect/unstable/sql/SqlError";
40
+ import { AuthenticationError, AuthorizationError, ConnectionError, ConstraintError, DeadlockError, LockTimeoutError, SerializationError, SqlError, SqlSyntaxError, StatementTimeoutError, UniqueViolation, UnknownError } from "effect/unstable/sql/SqlError";
22
41
  import * as Statement from "effect/unstable/sql/Statement";
23
42
  import * as Pg from "pg";
24
43
  import * as PgConnString from "pg-connection-string";
25
44
  import Cursor from "pg-cursor";
26
- const ATTR_DB_SYSTEM_NAME = "db.system.name";
27
- const ATTR_DB_NAMESPACE = "db.namespace";
28
- const ATTR_SERVER_ADDRESS = "server.address";
29
- const ATTR_SERVER_PORT = "server.port";
30
45
  /**
31
- * @category type ids
32
- * @since 1.0.0
46
+ * Runtime type identifier used to mark `PgClient` values.
47
+ *
48
+ * @category type IDs
49
+ * @since 4.0.0
33
50
  */
34
51
  export const TypeId = "~@effect/sql-pg/PgClient";
35
52
  /**
53
+ * Context tag used to access the `PgClient` service.
54
+ *
36
55
  * @category tags
37
- * @since 1.0.0
56
+ * @since 4.0.0
38
57
  */
39
- export const PgClient = /*#__PURE__*/ServiceMap.Service("@effect/sql-pg/PgClient");
58
+ export const PgClient = /*#__PURE__*/Context.Service("@effect/sql-pg/PgClient");
40
59
  /**
60
+ * Creates a scoped PostgreSQL client backed by a managed `pg` connection pool.
61
+ *
41
62
  * @category constructors
42
- * @since 1.0.0
63
+ * @since 4.0.0
43
64
  */
44
65
  export const make = options => fromPool({
45
66
  ...options,
@@ -67,193 +88,178 @@ export const make = options => fromPool({
67
88
  yield* Effect.acquireRelease(Effect.tryPromise({
68
89
  try: () => pool.query("SELECT 1"),
69
90
  catch: cause => new SqlError({
70
- cause,
71
- message: "PgClient: Failed to connect"
91
+ reason: classifyError(cause, "PgClient: Failed to connect", "connect")
72
92
  })
73
- }), () => Effect.promise(() => pool.end()).pipe(Effect.timeoutOption(1000))).pipe(Effect.timeoutOrElse({
93
+ }), () => Effect.promise(() => pool.end()).pipe(Effect.timeoutOption(1000)), {
94
+ interruptible: true
95
+ }).pipe(Effect.timeoutOrElse({
74
96
  duration: options.connectTimeout ?? Duration.seconds(5),
75
- onTimeout: () => Effect.fail(new SqlError({
76
- cause: new Error("Connection timed out"),
77
- message: "PgClient: Connection timed out"
97
+ orElse: () => Effect.fail(new SqlError({
98
+ reason: new ConnectionError({
99
+ cause: new Error("Connection timed out"),
100
+ message: "PgClient: Connection timed out",
101
+ operation: "connect"
102
+ })
78
103
  }))
79
104
  }));
80
105
  return pool;
81
106
  })
82
107
  });
83
108
  /**
109
+ * Creates a scoped PostgreSQL client backed by a managed single `pg` client, optionally acquiring a separate client for streaming and LISTEN operations.
110
+ *
84
111
  * @category constructors
85
- * @since 1.0.0
112
+ * @since 4.0.0
113
+ */
114
+ export const makeClient = options => fromClient({
115
+ ...options,
116
+ acquire: Effect.gen(function* () {
117
+ const client = new Pg.Client({
118
+ connectionString: options.url ? Redacted.value(options.url) : undefined,
119
+ user: options.username,
120
+ host: options.host,
121
+ database: options.database,
122
+ password: options.password ? Redacted.value(options.password) : undefined,
123
+ ssl: options.ssl,
124
+ port: options.port,
125
+ ...(options.stream ? {
126
+ stream: options.stream
127
+ } : {}),
128
+ application_name: options.applicationName ?? "@effect/sql-pg",
129
+ types: options.types
130
+ });
131
+ yield* Effect.acquireRelease(Effect.tryPromise({
132
+ try: () => client.query("SELECT 1"),
133
+ catch: cause => new SqlError({
134
+ reason: classifyError(cause, "PgClient: Failed to connect", "connect")
135
+ })
136
+ }), () => Effect.promise(() => client.end()).pipe(Effect.timeoutOption(1000)), {
137
+ interruptible: true
138
+ }).pipe(Effect.timeoutOrElse({
139
+ duration: options.connectTimeout ?? Duration.seconds(5),
140
+ orElse: () => Effect.fail(new SqlError({
141
+ reason: new ConnectionError({
142
+ cause: new Error("Connection timed out"),
143
+ message: "PgClient: Connection timed out",
144
+ operation: "connect"
145
+ })
146
+ }))
147
+ }));
148
+ return client;
149
+ }),
150
+ acquireForStream: options.acquireForStream ?? false
151
+ });
152
+ /**
153
+ * Builds a PostgreSQL client from a scoped `pg` pool acquisition effect, deriving transaction, streaming, and LISTEN/NOTIFY support from that pool.
154
+ *
155
+ * @category constructors
156
+ * @since 4.0.0
86
157
  */
87
158
  export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
88
- const compiler = makeCompiler(options.transformQueryNames, options.transformJson);
89
- const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames, options.transformJson).array : undefined;
90
159
  const pool = yield* options.acquire;
91
- class ConnectionImpl {
92
- pg;
93
- constructor(pg) {
94
- this.pg = pg;
160
+ const makeConection = client => new ConnectionImpl(function runWithClient(f) {
161
+ if (client !== undefined) {
162
+ return Effect.callback(resume => {
163
+ f(client, resume);
164
+ return makeCancel(pool, client);
165
+ });
95
166
  }
96
- runWithClient(f) {
97
- if (this.pg !== undefined) {
98
- return Effect.callback(resume => {
99
- f(this.pg, resume);
100
- return makeCancel(pool, this.pg);
101
- });
167
+ return Effect.callback(resume => {
168
+ let done = false;
169
+ let cancel = undefined;
170
+ let client = undefined;
171
+ function onError(cause) {
172
+ cleanup(cause);
173
+ resume(Effect.fail(new SqlError({
174
+ reason: classifyError(cause, "Connection error", "acquireConnection")
175
+ })));
102
176
  }
103
- return Effect.callback(resume => {
104
- let done = false;
105
- let cancel = undefined;
106
- let client = undefined;
107
- function onError(cause) {
108
- cleanup(cause);
109
- resume(Effect.fail(new SqlError({
110
- cause,
111
- message: "Connection error"
177
+ function cleanup(cause) {
178
+ if (!done) client?.release(cause);
179
+ done = true;
180
+ client?.off("error", onError);
181
+ }
182
+ pool.connect((cause, client_) => {
183
+ if (cause) {
184
+ return resume(Effect.fail(new SqlError({
185
+ reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
112
186
  })));
113
- }
114
- function cleanup(cause) {
115
- if (!done) client?.release(cause);
116
- done = true;
117
- client?.off("error", onError);
118
- }
119
- pool.connect((cause, client_) => {
120
- if (cause) {
121
- return resume(Effect.fail(new SqlError({
122
- cause,
123
- message: "Failed to acquire connection"
124
- })));
125
- } else if (!client_) {
126
- return resume(Effect.fail(new SqlError({
187
+ } else if (!client_) {
188
+ return resume(Effect.fail(new SqlError({
189
+ reason: new ConnectionError({
127
190
  message: "Failed to acquire connection",
128
- cause: new Error("No client returned")
129
- })));
130
- } else if (done) {
131
- client_.release();
132
- return;
133
- }
134
- client = client_;
135
- client.once("error", onError);
136
- cancel = makeCancel(pool, client);
137
- f(client, eff => {
138
- cleanup();
139
- resume(eff);
140
- });
141
- });
142
- return Effect.suspend(() => {
143
- if (!cancel) {
144
- cleanup();
145
- return Effect.void;
146
- }
147
- return Effect.ensuring(cancel, Effect.sync(cleanup));
148
- });
149
- });
150
- }
151
- run(query, params) {
152
- return this.runWithClient((client, resume) => {
153
- client.query(query, params, (err, result) => {
154
- if (err) {
155
- resume(Effect.fail(new SqlError({
156
- cause: err,
157
- message: "Failed to execute statement"
158
- })));
159
- } else {
160
- // Multi-statement queries return an array of results
161
- resume(Effect.succeed(Array.isArray(result) ? result.map(r => r.rows ?? []) : result.rows ?? []));
162
- }
163
- });
164
- });
165
- }
166
- execute(sql, params, transformRows) {
167
- return transformRows ? Effect.map(this.run(sql, params), transformRows) : this.run(sql, params);
168
- }
169
- executeRaw(sql, params) {
170
- return this.runWithClient((client, resume) => {
171
- client.query(sql, params, (err, result) => {
172
- if (err) {
173
- resume(Effect.fail(new SqlError({
174
- cause: err,
175
- message: "Failed to execute statement"
176
- })));
177
- } else {
178
- resume(Effect.succeed(result));
179
- }
191
+ cause: new Error("No client returned"),
192
+ operation: "acquireConnection"
193
+ })
194
+ })));
195
+ } else if (done) {
196
+ client_.release();
197
+ return;
198
+ }
199
+ client = client_;
200
+ client.once("error", onError);
201
+ cancel = makeCancel(pool, client);
202
+ f(client, eff => {
203
+ cleanup();
204
+ resume(eff);
180
205
  });
181
206
  });
182
- }
183
- executeWithoutTransform(sql, params) {
184
- return this.run(sql, params);
185
- }
186
- executeValues(sql, params) {
187
- return this.runWithClient((client, resume) => {
188
- client.query({
189
- text: sql,
190
- rowMode: "array",
191
- values: params
192
- }, (err, result) => {
193
- if (err) {
194
- resume(Effect.fail(new SqlError({
195
- cause: err,
196
- message: "Failed to execute statement"
197
- })));
198
- } else {
199
- resume(Effect.succeed(result.rows));
200
- }
201
- });
207
+ return Effect.suspend(() => {
208
+ if (!cancel) {
209
+ cleanup();
210
+ return Effect.void;
211
+ }
212
+ return Effect.ensuring(cancel, Effect.sync(cleanup));
202
213
  });
203
- }
204
- executeUnprepared(sql, params, transformRows) {
205
- return this.execute(sql, params, transformRows);
206
- }
207
- executeStream(sql, params, transformRows) {
208
- // oxlint-disable-next-line @typescript-eslint/no-this-alias
209
- const self = this;
210
- return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function* (_, scope) {
211
- const client = self.pg ?? (yield* Scope.provide(reserveRaw, scope));
212
- yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()));
213
- const cursor = client.query(new Cursor(sql, params));
214
- // @effect-diagnostics-next-line returnEffectInGen:off
215
- return Effect.callback(resume => {
216
- cursor.read(128, (err, rows) => {
217
- if (err) {
218
- resume(Effect.fail(new SqlError({
219
- cause: err,
220
- message: "Failed to execute statement"
221
- })));
222
- } else if (Arr.isArrayNonEmpty(rows)) {
223
- resume(Effect.succeed(transformRows ? transformRows(rows) : rows));
224
- } else {
225
- resume(Cause.done());
226
- }
227
- });
228
- });
229
- })));
230
- }
231
- }
214
+ });
215
+ }, client ? Effect.succeed(client) : reserveRaw);
232
216
  const reserveRaw = Effect.callback(resume => {
233
217
  const fiber = Fiber.getCurrent();
234
- const scope = ServiceMap.getUnsafe(fiber.services, Scope.Scope);
218
+ const scope = Context.getUnsafe(fiber.context, Scope.Scope);
235
219
  let cause = undefined;
220
+ function onError(cause_) {
221
+ cause = cause_;
222
+ }
236
223
  pool.connect((err, client, release) => {
237
224
  if (err) {
238
- resume(Effect.fail(new SqlError({
239
- cause: err,
240
- message: "Failed to acquire connection for transaction"
225
+ return resume(Effect.fail(new SqlError({
226
+ reason: classifyError(err, "Failed to acquire connection for transaction", "acquireConnection")
227
+ })));
228
+ } else if (!client) {
229
+ return resume(Effect.fail(new SqlError({
230
+ reason: new ConnectionError({
231
+ message: "Failed to acquire connection for transaction",
232
+ cause: new Error("No client returned"),
233
+ operation: "acquireConnection"
234
+ })
241
235
  })));
242
- } else {
243
- resume(Effect.as(Scope.addFinalizer(scope, Effect.sync(() => {
244
- client.off("error", onError);
245
- release(cause);
246
- })), client));
247
- }
248
- function onError(cause_) {
249
- cause = cause_;
250
236
  }
251
237
  client.on("error", onError);
238
+ resume(Effect.as(Scope.addFinalizer(scope, Effect.sync(() => {
239
+ client.off("error", onError);
240
+ release(cause);
241
+ })), client));
252
242
  });
253
243
  });
254
- const reserve = Effect.map(reserveRaw, client => new ConnectionImpl(client));
255
- const listenClient = yield* RcRef.make({
256
- acquire: reserveRaw
244
+ const reserve = Effect.map(reserveRaw, makeConection);
245
+ const onListenClientError = _ => {};
246
+ const listenAcquirer = yield* RcRef.make({
247
+ acquire: Effect.acquireRelease(Effect.tryPromise({
248
+ try: async () => {
249
+ const client = new Pg.Client(pool.options);
250
+ await client.connect();
251
+ client.on("error", onListenClientError);
252
+ return client;
253
+ },
254
+ catch: cause => new SqlError({
255
+ reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
256
+ })
257
+ }), client => Effect.promise(() => {
258
+ client.off("error", onListenClientError);
259
+ return client.end();
260
+ }).pipe(Effect.timeoutOption(1000)), {
261
+ interruptible: true
262
+ })
257
263
  });
258
264
  let config = {
259
265
  url: pool.options.connectionString ? Redacted.make(pool.options.connectionString) : undefined,
@@ -273,7 +279,7 @@ export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
273
279
  config = {
274
280
  ...config,
275
281
  host: config.host ?? parsed.host ?? undefined,
276
- port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
282
+ port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
277
283
  username: config.username ?? parsed.user ?? undefined,
278
284
  password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
279
285
  database: config.database ?? parsed.database ?? undefined
@@ -282,18 +288,84 @@ export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
282
288
  //
283
289
  }
284
290
  }
285
- return Object.assign(yield* Client.make({
286
- acquirer: Effect.succeed(new ConnectionImpl()),
291
+ return yield* makeWith({
292
+ acquirer: Effect.succeed(makeConection()),
287
293
  transactionAcquirer: reserve,
294
+ listenAcquirer: RcRef.get(listenAcquirer),
295
+ config,
296
+ spanAttributes: options.spanAttributes,
297
+ transformResultNames: options.transformResultNames,
298
+ transformQueryNames: options.transformQueryNames,
299
+ transformJson: options.transformJson
300
+ });
301
+ });
302
+ /**
303
+ * 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.
304
+ *
305
+ * @category constructors
306
+ * @since 4.0.0
307
+ */
308
+ export const fromClient = /*#__PURE__*/Effect.fnUntraced(function* (options) {
309
+ function onError() {}
310
+ const acquireWithErrorHandler = options.acquire.pipe(Effect.tap(client => {
311
+ client.on("error", onError);
312
+ return Effect.addFinalizer(() => {
313
+ client.off("error", onError);
314
+ return Effect.void;
315
+ });
316
+ }));
317
+ const client = yield* acquireWithErrorHandler;
318
+ const semaphore = Semaphore.makeUnsafe(1);
319
+ let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(Effect.as(semaphore.take(1), client), () => semaphore.release(1));
320
+ const makeConection = client => new ConnectionImpl(function runWithClient(f) {
321
+ return Effect.callback(resume => {
322
+ f(client, resume);
323
+ });
324
+ }, streamClient);
325
+ const connection = makeConection(client);
326
+ const acquirer = semaphore.withPermit(Effect.succeed(connection));
327
+ const config = {
328
+ ...options,
329
+ host: client.host,
330
+ port: client.port,
331
+ database: client.database,
332
+ username: client.user,
333
+ password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
334
+ ssl: client.ssl
335
+ };
336
+ return yield* makeWith({
337
+ acquirer,
338
+ transactionAcquirer: acquirer,
339
+ listenAcquirer: streamClient,
340
+ config,
341
+ spanAttributes: options.spanAttributes,
342
+ transformResultNames: options.transformResultNames,
343
+ transformQueryNames: options.transformQueryNames,
344
+ transformJson: options.transformJson
345
+ });
346
+ });
347
+ /**
348
+ * Low-level constructor for a `PgClient` from SQL connection acquirers, a LISTEN acquirer, client configuration, and transformation options.
349
+ *
350
+ * @category constructors
351
+ * @since 4.0.0
352
+ */
353
+ export const makeWith = /*#__PURE__*/Effect.fnUntraced(function* (options) {
354
+ const compiler = makeCompiler(options.transformQueryNames, options.transformJson);
355
+ const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames, options.transformJson).array : undefined;
356
+ const config = options.config;
357
+ return Object.assign(yield* Client.make({
358
+ acquirer: options.acquirer,
359
+ transactionAcquirer: options.transactionAcquirer,
288
360
  compiler,
289
361
  spanAttributes: [...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), [ATTR_DB_SYSTEM_NAME, "postgresql"], [ATTR_DB_NAMESPACE, config.database ?? config.username ?? "postgres"], [ATTR_SERVER_ADDRESS, config.host ?? "localhost"], [ATTR_SERVER_PORT, config.port ?? 5432]],
290
362
  transformRows
291
363
  }), {
292
364
  [TypeId]: TypeId,
293
- config,
365
+ config: options.config,
294
366
  json: _ => Statement.fragment([PgJson(_)]),
295
367
  listen: channel => Stream.callback(Effect.fnUntraced(function* (queue) {
296
- const client = yield* RcRef.get(listenClient);
368
+ const client = yield* options.listenAcquirer;
297
369
  function onNotification(msg) {
298
370
  if (msg.channel === channel && msg.payload) {
299
371
  Queue.offerUnsafe(queue, msg.payload);
@@ -306,26 +378,98 @@ export const fromPool = /*#__PURE__*/Effect.fnUntraced(function* (options) {
306
378
  yield* Effect.tryPromise({
307
379
  try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
308
380
  catch: cause => new SqlError({
309
- cause,
310
- message: "Failed to listen"
381
+ reason: classifyError(cause, "Failed to listen", "listen")
311
382
  })
312
383
  });
313
384
  client.on("notification", onNotification);
314
385
  })),
315
- notify: (channel, payload) => Effect.callback(resume => {
316
- pool.query(`NOTIFY ${Pg.escapeIdentifier(channel)}, $1`, [payload], err => {
386
+ notify: (channel, payload) => Effect.asVoid(Effect.scoped(Effect.flatMap(options.acquirer, conn => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload]))))
387
+ });
388
+ });
389
+ class ConnectionImpl {
390
+ constructor(runWithClient, reserve) {
391
+ this.runWithClient = runWithClient;
392
+ this.reserve = reserve;
393
+ }
394
+ runWithClient;
395
+ reserve;
396
+ run(query, params) {
397
+ return this.runWithClient((client, resume) => {
398
+ client.query(query, params, (err, result) => {
317
399
  if (err) {
318
400
  resume(Effect.fail(new SqlError({
319
- cause: err,
320
- message: "Failed to notify"
401
+ reason: classifyError(err, "Failed to execute statement", "execute")
321
402
  })));
322
403
  } else {
323
- resume(Effect.void);
404
+ // Multi-statement queries return an array of results
405
+ resume(Effect.succeed(Array.isArray(result) ? result.map(r => r.rows ?? []) : result.rows ?? []));
324
406
  }
325
407
  });
326
- })
327
- });
328
- });
408
+ });
409
+ }
410
+ execute(sql, params, transformRows) {
411
+ return transformRows ? Effect.map(this.run(sql, params), transformRows) : this.run(sql, params);
412
+ }
413
+ executeRaw(sql, params) {
414
+ return this.runWithClient((client, resume) => {
415
+ client.query(sql, params, (err, result) => {
416
+ if (err) {
417
+ resume(Effect.fail(new SqlError({
418
+ reason: classifyError(err, "Failed to execute statement", "execute")
419
+ })));
420
+ } else {
421
+ resume(Effect.succeed(result));
422
+ }
423
+ });
424
+ });
425
+ }
426
+ executeWithoutTransform(sql, params) {
427
+ return this.run(sql, params);
428
+ }
429
+ executeValues(sql, params) {
430
+ return this.runWithClient((client, resume) => {
431
+ client.query({
432
+ text: sql,
433
+ rowMode: "array",
434
+ values: params
435
+ }, (err, result) => {
436
+ if (err) {
437
+ resume(Effect.fail(new SqlError({
438
+ reason: classifyError(err, "Failed to execute statement", "execute")
439
+ })));
440
+ } else {
441
+ resume(Effect.succeed(result.rows));
442
+ }
443
+ });
444
+ });
445
+ }
446
+ executeUnprepared(sql, params, transformRows) {
447
+ return this.execute(sql, params, transformRows);
448
+ }
449
+ executeStream(sql, params, transformRows) {
450
+ // oxlint-disable-next-line @typescript-eslint/no-this-alias
451
+ const self = this;
452
+ return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function* (_, scope) {
453
+ const client = yield* Scope.provide(self.reserve, scope);
454
+ yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()));
455
+ const cursor = client.query(new Cursor(sql, params));
456
+ // @effect-diagnostics-next-line returnEffectInGen:off
457
+ return Effect.callback(resume => {
458
+ cursor.read(128, (err, rows) => {
459
+ if (err) {
460
+ resume(Effect.fail(new SqlError({
461
+ reason: classifyError(err, "Failed to execute statement", "stream")
462
+ })));
463
+ } else if (Arr.isArrayNonEmpty(rows)) {
464
+ resume(Effect.succeed(transformRows ? transformRows(rows) : rows));
465
+ } else {
466
+ resume(Cause.done());
467
+ }
468
+ });
469
+ });
470
+ })));
471
+ }
472
+ }
329
473
  const cancelEffects = /*#__PURE__*/new WeakMap();
330
474
  const makeCancel = (pool, client) => {
331
475
  if (cancelEffects.has(client)) {
@@ -344,23 +488,31 @@ const makeCancel = (pool, client) => {
344
488
  return eff;
345
489
  };
346
490
  /**
491
+ * Creates a layer from an effect that acquires a `PgClient`, providing both `PgClient` and `SqlClient`.
492
+ *
347
493
  * @category layers
348
- * @since 1.0.0
494
+ * @since 4.0.0
349
495
  */
350
- export const layerConfig = config => Layer.effectServices(Config.unwrap(config).asEffect().pipe(Effect.flatMap(make), Effect.map(client => ServiceMap.make(PgClient, client).pipe(ServiceMap.add(Client.SqlClient, client))))).pipe(Layer.provide(Reactivity.layer));
496
+ export const layerFrom = acquire => Layer.effectContext(Effect.map(acquire, client => Context.make(PgClient, client).pipe(Context.add(Client.SqlClient, client)))).pipe(Layer.provide(Reactivity.layer));
351
497
  /**
498
+ * Creates a layer from a `Config`-wrapped PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
499
+ *
352
500
  * @category layers
353
- * @since 1.0.0
501
+ * @since 4.0.0
354
502
  */
355
- export const layer = config => Layer.effectServices(Effect.map(make(config), client => ServiceMap.make(PgClient, client).pipe(ServiceMap.add(Client.SqlClient, client)))).pipe(Layer.provide(Reactivity.layer));
503
+ export const layerConfig = config => layerFrom(Effect.flatMap(Config.unwrap(config), make));
356
504
  /**
505
+ * Creates a layer from a concrete PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
506
+ *
357
507
  * @category layers
358
- * @since 1.0.0
508
+ * @since 4.0.0
359
509
  */
360
- export const layerFromPool = options => Layer.effectServices(Effect.map(fromPool(options), client => ServiceMap.make(PgClient, client).pipe(ServiceMap.add(Client.SqlClient, client)))).pipe(Layer.provide(Reactivity.layer));
510
+ export const layer = config => layerFrom(make(config));
361
511
  /**
362
- * @category constructor
363
- * @since 1.0.0
512
+ * Creates the PostgreSQL statement compiler, using `$1` placeholders, double-quoted identifiers, PostgreSQL returning clauses, and optional JSON value transformation.
513
+ *
514
+ * @category constructors
515
+ * @since 4.0.0
364
516
  */
365
517
  export const makeCompiler = (transform, transformJson = true) => {
366
518
  const transformValue = transformJson && transform ? Statement.defaultTransforms(transform).value : undefined;
@@ -388,7 +540,73 @@ export const makeCompiler = (transform, transformJson = true) => {
388
540
  const escape = /*#__PURE__*/Statement.defaultEscape("\"");
389
541
  /**
390
542
  * @category custom types
391
- * @since 1.0.0
543
+ * @since 4.0.0
392
544
  */
393
545
  const PgJson = /*#__PURE__*/Statement.custom("PgJson");
546
+ const ATTR_DB_SYSTEM_NAME = "db.system.name";
547
+ const ATTR_DB_NAMESPACE = "db.namespace";
548
+ const ATTR_SERVER_ADDRESS = "server.address";
549
+ const ATTR_SERVER_PORT = "server.port";
550
+ const pgCodeFromCause = cause => {
551
+ if (typeof cause !== "object" || cause === null || !("code" in cause)) {
552
+ return undefined;
553
+ }
554
+ const code = cause.code;
555
+ return typeof code === "string" ? code : undefined;
556
+ };
557
+ const pgConstraintFromCause = cause => {
558
+ if (typeof cause !== "object" || cause === null || !("constraint" in cause)) {
559
+ return "unknown";
560
+ }
561
+ const constraint = cause.constraint;
562
+ if (typeof constraint !== "string") {
563
+ return "unknown";
564
+ }
565
+ const normalized = constraint.trim();
566
+ return normalized.length === 0 ? "unknown" : normalized;
567
+ };
568
+ const classifyError = (cause, message, operation) => {
569
+ const props = {
570
+ cause,
571
+ message,
572
+ operation
573
+ };
574
+ const code = pgCodeFromCause(cause);
575
+ if (code !== undefined) {
576
+ if (code.startsWith("08")) {
577
+ return new ConnectionError(props);
578
+ }
579
+ if (code.startsWith("28")) {
580
+ return new AuthenticationError(props);
581
+ }
582
+ if (code === "42501") {
583
+ return new AuthorizationError(props);
584
+ }
585
+ if (code.startsWith("42")) {
586
+ return new SqlSyntaxError(props);
587
+ }
588
+ if (code === "23505") {
589
+ return new UniqueViolation({
590
+ ...props,
591
+ constraint: pgConstraintFromCause(cause)
592
+ });
593
+ }
594
+ if (code.startsWith("23")) {
595
+ return new ConstraintError(props);
596
+ }
597
+ if (code === "40P01") {
598
+ return new DeadlockError(props);
599
+ }
600
+ if (code === "40001") {
601
+ return new SerializationError(props);
602
+ }
603
+ if (code === "55P03") {
604
+ return new LockTimeoutError(props);
605
+ }
606
+ if (code === "57014") {
607
+ return new StatementTimeoutError(props);
608
+ }
609
+ }
610
+ return new UnknownError(props);
611
+ };
394
612
  //# sourceMappingURL=PgClient.js.map