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

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