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

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 +1788 -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 +2296 -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
@@ -0,0 +1,1788 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Channel from "effect/Channel";
3
+ import * as Context from "effect/Context";
4
+ import * as Deferred from "effect/Deferred";
5
+ import * as Duration from "effect/Duration";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Exit from "effect/Exit";
8
+ import * as Queue from "effect/Queue";
9
+ import * as Redacted from "effect/Redacted";
10
+ import * as EffectResult from "effect/Result";
11
+ import * as Scope from "effect/Scope";
12
+ import * as Semaphore from "effect/Semaphore";
13
+ import * as Stream from "effect/Stream";
14
+ import { AuthenticationError, ConnectionError, SqlError, UnknownError } from "effect/unstable/sql/SqlError";
15
+ import { Buffer } from "node:buffer";
16
+ import { randomBytes } from "node:crypto";
17
+ import * as Net from "node:net";
18
+ import * as Tls from "node:tls";
19
+ import { internalsKey } from "./internal/connection.js";
20
+ import { classifySqlState, validateChannelName } from "./internal/sqlError.js";
21
+ import * as PgAuth from "./PgAuth.js";
22
+ import * as PgProtocol from "./PgProtocol.js";
23
+ import * as PgTypes from "./PgTypes.js";
24
+ /**
25
+ * The runtime type identifier for `PgConnection`.
26
+ *
27
+ * @category type IDs
28
+ * @since 4.0.0
29
+ */
30
+ export const TypeId = "~@effect/sql-pg/PgConnection";
31
+ /**
32
+ * Builds the prepared-statement cache a session should use, or `undefined`
33
+ * when `prepare` is off or the cache is sized to nothing.
34
+ */
35
+ const preparedCacheFor = config => {
36
+ if (config.prepare === false) return undefined;
37
+ const max = config.preparedStatementCacheSize ?? defaultPreparedStatements;
38
+ return max > 0 ? new PreparedCache(max) : undefined;
39
+ };
40
+ /**
41
+ * The service tag for `PgConnection`.
42
+ *
43
+ * @category services
44
+ * @since 4.0.0
45
+ */
46
+ export const PgConnection = /*#__PURE__*/Context.Service("@effect/sql-pg/PgConnection");
47
+ /**
48
+ * Connects and authenticates a single PostgreSQL session.
49
+ *
50
+ * **Details**
51
+ *
52
+ * The transport, optional `SSLRequest`, startup, and authentication steps run
53
+ * under `connectTimeout` (5 seconds by default). The effect resolves once the
54
+ * backend sends `ReadyForQuery`. When the scope closes, the
55
+ * session sends `Terminate` and ends the socket.
56
+ *
57
+ * When `ssl` is enabled, a server that rejects `SSLRequest` fails the
58
+ * connection. Unix sockets and custom streams should set `ssl.servername`
59
+ * explicitly.
60
+ *
61
+ * @category constructors
62
+ * @since 4.0.0
63
+ */
64
+ export const make = options => Effect.flatMap(resolveConfig(options), config => Effect.acquireRelease(Effect.map(connect(config), session => new PgConnectionImpl(options, config, session, options.types)), connection => Effect.sync(() => connection.closeUnsafe()), {
65
+ interruptible: true
66
+ }).pipe(Effect.timeoutOrElse({
67
+ duration: config.connectTimeout,
68
+ orElse: () => Effect.fail(new SqlError({
69
+ reason: new ConnectionError({
70
+ cause: new Error("Connection timed out"),
71
+ message: "PgConnection: Connection timed out",
72
+ operation: "connect"
73
+ })
74
+ }))
75
+ })));
76
+ const abortDrainTimeoutMillis = 5000;
77
+ const cancelRequestTimeoutMillis = 5000;
78
+ /** How many statements a multiplexed session keeps on the wire at once. */
79
+ const maxPipelineDepth = 128;
80
+ const streamPauseThreshold = 512;
81
+ class PgConnectionImpl {
82
+ [TypeId] = TypeId;
83
+ config;
84
+ processId;
85
+ session;
86
+ /** The custom codec registry, or `undefined` for the builtin catalogue. */
87
+ registry;
88
+ bindEncoder;
89
+ /** The statements this session has named, or `undefined` when disabled. */
90
+ prepared;
91
+ resolved;
92
+ multiplex;
93
+ /** Serializes statements: one in-flight extended-query cycle. */
94
+ wire = /*#__PURE__*/Semaphore.makeUnsafe(1);
95
+ /** Exclusive-ownership queue used by `pin` and unpinned statements. */
96
+ owner = /*#__PURE__*/Semaphore.makeUnsafe(1);
97
+ pinned = false;
98
+ consumer;
99
+ deadWith;
100
+ closed = false;
101
+ channels = /*#__PURE__*/new Map();
102
+ fatalHooks = /*#__PURE__*/new Set();
103
+ /** Queued but not yet written; drained into `pipelineInFlight` on flush. */
104
+ pipelinePending = [];
105
+ /** Written and awaiting their `ReadyForQuery`, oldest first. */
106
+ pipelineInFlight = [];
107
+ pipelineHead = 0;
108
+ pipelineFlushScheduled = false;
109
+ pipelineIdleWaiters = /*#__PURE__*/new Set();
110
+ pinnedView;
111
+ [internalsKey];
112
+ constructor(config, resolved, session, registry) {
113
+ this.config = config;
114
+ this.resolved = resolved;
115
+ this.session = session;
116
+ this.processId = session.processId;
117
+ this.registry = registry;
118
+ this.bindEncoder = makeBindEncoder(registry);
119
+ this.prepared = preparedCacheFor(config);
120
+ this.multiplex = config.multiplex ?? false;
121
+ // Before the pinned view, which copies it.
122
+ this[internalsKey] = {
123
+ base: this,
124
+ deadError: () => this.deadWith,
125
+ fatalHooks: this.fatalHooks
126
+ };
127
+ this.pinnedView = new PinnedPgConnection(this);
128
+ session.socket.on("data", this.onData);
129
+ session.socket.on("error", this.onSocketError);
130
+ session.socket.on("close", this.onSocketClose);
131
+ }
132
+ onData = chunk => {
133
+ try {
134
+ this.session.parser.pushEach(chunk, this.dispatch);
135
+ } catch (cause) {
136
+ // A field reader that threw reports the row it could not decode; anything
137
+ // else came out of the framing itself.
138
+ return this.fatal(cause instanceof PgProtocol.ParseError ? connectionQueryError(cause, "PgConnection: Failed to parse server messages") : queryError(cause, "PgConnection: Failed to decode row"));
139
+ }
140
+ if (this.deadWith === undefined) this.consumer?.onBatchEnd?.();
141
+ };
142
+ onSocketError = cause => this.fatal(connectionQueryError(cause, "PgConnection: Socket error"));
143
+ onSocketClose = () => this.fatal(connectionQueryError(new Error("Connection closed"), "PgConnection: Connection closed"));
144
+ dispatch = message => {
145
+ if (this.deadWith !== undefined) return;
146
+ switch (message._tag) {
147
+ case "NotificationResponse":
148
+ {
149
+ const queues = this.channels.get(message.channel);
150
+ if (queues !== undefined) {
151
+ const notification = {
152
+ processId: message.pid,
153
+ channel: message.channel,
154
+ payload: message.payload
155
+ };
156
+ for (const queue of queues) Queue.offerUnsafe(queue, notification);
157
+ }
158
+ return;
159
+ }
160
+ case "ParameterStatus":
161
+ case "NoticeResponse":
162
+ return;
163
+ }
164
+ if (this.consumer !== undefined) return this.consumer.onMessage(message);
165
+ if (message._tag === "ErrorResponse") {
166
+ return this.fatal(new SqlError({
167
+ reason: classifyFields(message.fields, "PgConnection: The server reported an error", "query")
168
+ }));
169
+ }
170
+ this.fatal(connectionQueryError(new Error(`Unexpected ${message._tag} while idle`), `PgConnection: Unexpected ${message._tag} while idle`));
171
+ };
172
+ /** Marks the session dead: optionally destroys the socket, fails the active statement
173
+ * and every listen queue, and notifies pool hooks unless the session's own
174
+ * scope is being released. */
175
+ fatal(error, destroySocket = true) {
176
+ if (this.deadWith !== undefined) return;
177
+ this.deadWith = error;
178
+ if (destroySocket) this.session.socket.destroy();
179
+ const consumer = this.consumer;
180
+ this.consumer = undefined;
181
+ consumer?.onFatal(error);
182
+ const sets = Array.from(this.channels.values());
183
+ this.channels.clear();
184
+ for (const set of sets) {
185
+ for (const queue of set) Queue.failCauseUnsafe(queue, Cause.interrupt());
186
+ }
187
+ if (!this.closed) {
188
+ for (const hook of this.fatalHooks) hook();
189
+ }
190
+ }
191
+ closeUnsafe() {
192
+ this.closed = true;
193
+ let destroySocket = true;
194
+ if (this.deadWith === undefined && this.session.socket.writable) {
195
+ try {
196
+ this.session.socket.end(PgProtocol.encodeTerminate());
197
+ destroySocket = false;
198
+ } catch {
199
+ // Fall back to the fatal path's immediate destroy below.
200
+ }
201
+ }
202
+ this.fatal(new SqlError({
203
+ reason: new ConnectionError({
204
+ cause: new Error("Connection is closed"),
205
+ message: "PgConnection: Connection is closed",
206
+ operation: "query"
207
+ })
208
+ }), destroySocket);
209
+ }
210
+ /** Plans one execution. `cache` is `undefined` to force the unnamed path. */
211
+ encodeQuery = (sql, params, cache) => encodeQuery(sql, params, this.registry, this.bindEncoder, cache);
212
+ /**
213
+ * Routes backend messages to the oldest statement still on the wire.
214
+ *
215
+ * Every cycle carries its own `Sync`, so the backend answers them in order
216
+ * and finishes each with a `ReadyForQuery`. That boundary is what advances
217
+ * the queue, and it is also why a statement the backend rejects only skips
218
+ * the rest of its own cycle: the ones queued behind it still run.
219
+ */
220
+ pipelineConsumer = {
221
+ onMessage: message => {
222
+ const entry = this.pipelineInFlight[this.pipelineHead];
223
+ if (entry === undefined) {
224
+ return this.fatal(connectionQueryError(new Error(`Unexpected ${message._tag} without an in-flight query`), `PgConnection: Unexpected ${message._tag} without an in-flight query`));
225
+ }
226
+ entry.machine.onMessage(message);
227
+ },
228
+ onBatchEnd: () => this.flushPipeline(),
229
+ onFatal: error => {
230
+ const entries = [...this.pipelineInFlight.slice(this.pipelineHead), ...this.pipelinePending];
231
+ this.pipelineInFlight.length = 0;
232
+ this.pipelineHead = 0;
233
+ this.pipelinePending.length = 0;
234
+ this.pipelineFlushScheduled = false;
235
+ for (const entry of entries) {
236
+ if (!entry.abandoned) Deferred.doneUnsafe(entry.deferred, Effect.fail(error));
237
+ }
238
+ this.notifyPipelineIdle();
239
+ }
240
+ };
241
+ pipelineDepth = () => this.pipelineInFlight.length - this.pipelineHead;
242
+ pipelineIsIdle = () => this.pipelineDepth() === 0 && this.pipelinePending.length === 0;
243
+ notifyPipelineIdle = () => {
244
+ if (!this.pipelineIsIdle()) return;
245
+ this.consumer = undefined;
246
+ this.session.parser.readField = undefined;
247
+ const waiters = Array.from(this.pipelineIdleWaiters);
248
+ this.pipelineIdleWaiters.clear();
249
+ for (const waiter of waiters) waiter();
250
+ };
251
+ /** Resolves once nothing is on the wire, so `pin` can take the connection. */
252
+ waitPipelineIdle = /*#__PURE__*/Effect.callback(resume => {
253
+ if (this.pipelineIsIdle()) return resume(Effect.void);
254
+ const waiter = () => resume(Effect.void);
255
+ this.pipelineIdleWaiters.add(waiter);
256
+ return Effect.sync(() => this.pipelineIdleWaiters.delete(waiter));
257
+ });
258
+ finishPipelineEntry = (entry, result) => {
259
+ if (this.pipelineInFlight[this.pipelineHead] !== entry) {
260
+ return this.fatal(connectionQueryError(new Error("A pipelined query completed out of order"), "PgConnection: A pipelined query completed out of order"));
261
+ }
262
+ this.pipelineHead++;
263
+ // The next statement's rows can be in this same chunk, so its reader has to
264
+ // be in place before the parser reads on.
265
+ this.session.parser.readField = this.pipelineInFlight[this.pipelineHead]?.machine.readField;
266
+ if (!entry.abandoned) Deferred.doneUnsafe(entry.deferred, result);
267
+ if (this.pipelineHead === this.pipelineInFlight.length) {
268
+ this.pipelineInFlight.length = 0;
269
+ this.pipelineHead = 0;
270
+ }
271
+ };
272
+ /** Writes everything queued since the last flush as one batch. */
273
+ flushPipeline = () => {
274
+ this.pipelineFlushScheduled = false;
275
+ if (this.deadWith !== undefined) return;
276
+ let capacity = maxPipelineDepth - this.pipelineDepth();
277
+ if (capacity <= 0) return;
278
+ const wasEmpty = this.pipelineDepth() === 0;
279
+ const batch = [];
280
+ let index = 0;
281
+ while (capacity > 0 && index < this.pipelinePending.length) {
282
+ const entry = this.pipelinePending[index++];
283
+ if (entry.abandoned) {
284
+ if (entry.plan.parses && entry.plan.prepared !== undefined) {
285
+ entry.plan.prepared.parsing = false;
286
+ }
287
+ continue;
288
+ }
289
+ batch.push(entry);
290
+ this.pipelineInFlight.push(entry);
291
+ capacity--;
292
+ }
293
+ this.pipelinePending.splice(0, index);
294
+ if (batch.length === 0) {
295
+ this.notifyPipelineIdle();
296
+ return;
297
+ }
298
+ if (wasEmpty) this.session.parser.readField = batch[0].machine.readField;
299
+ const frame = batch.length === 1 ? batch[0].plan.frame : concat(batch.map(entry => entry.plan.frame));
300
+ try {
301
+ this.session.socket.write(frame);
302
+ } catch (cause) {
303
+ this.fatal(connectionQueryError(cause, "PgConnection: Failed to write query batch"));
304
+ }
305
+ };
306
+ schedulePipelineFlush = () => {
307
+ if (this.pipelineFlushScheduled || this.pipelineDepth() >= maxPipelineDepth) return;
308
+ this.pipelineFlushScheduled = true;
309
+ // A microtask is what lets fibers submitted in the same tick share a write.
310
+ queueMicrotask(this.flushPipeline);
311
+ };
312
+ /**
313
+ * One cycle on a multiplexed connection. The `owner` permit is only taken to
314
+ * queue the statement, so a pin still orders against submissions without
315
+ * serializing them.
316
+ */
317
+ pipelineCycle = (sql, params, wantRows, cache) => Effect.flatMap(this.owner.withPermit(Effect.suspend(() => {
318
+ if (this.deadWith !== undefined) return Effect.fail(this.deadWith);
319
+ let plan;
320
+ try {
321
+ plan = this.encodeQuery(sql, params, cache);
322
+ } catch (cause) {
323
+ return Effect.fail(queryError(cause, "PgConnection: Failed to encode query"));
324
+ }
325
+ const deferred = Deferred.makeUnsafe();
326
+ let entry;
327
+ const machine = new QueryMachine(this, plan, wantRows, result => this.finishPipelineEntry(entry, result));
328
+ entry = {
329
+ plan,
330
+ deferred,
331
+ machine,
332
+ abandoned: false
333
+ };
334
+ this.pipelinePending.push(entry);
335
+ this.consumer = this.pipelineConsumer;
336
+ this.schedulePipelineFlush();
337
+ return Effect.succeed(entry);
338
+ })), entry => {
339
+ const awaited = Deferred.await(entry.deferred).pipe(Effect.onInterrupt(() => Effect.sync(() => {
340
+ entry.abandoned = true;
341
+ })));
342
+ if (cache === undefined || entry.plan.parses) return awaited;
343
+ return retryStale(entry.plan, cache, awaited, () => this.pipelineCycle(sql, params, wantRows, undefined));
344
+ });
345
+ /**
346
+ * One extended-query cycle for a caller that shares the session with others:
347
+ * the fibers inside a transaction, and a stream running beside them. The
348
+ * wire permit is what serializes them.
349
+ */
350
+ cycle = (sql, params, wantRows, cache) => this.wire.withPermit(this.cycleOwned(sql, params, wantRows, cache));
351
+ /**
352
+ * Runs one cycle for a caller that owns the session. No wire permit is needed
353
+ * because competing pinned work and streams must acquire `owner` first.
354
+ */
355
+ cycleOwned = (sql, params, wantRows, cache) => Effect.suspend(() => this.attempt(sql, params, wantRows, cache));
356
+ /**
357
+ * Runs one cycle. A reused statement the backend has since dropped, or whose
358
+ * plan no longer matches its columns, is parsed again on a second attempt;
359
+ * that attempt skips the cache, so it cannot loop.
360
+ */
361
+ attempt = (sql, params, wantRows, cache) => {
362
+ if (this.deadWith !== undefined) return Effect.fail(this.deadWith);
363
+ let plan;
364
+ try {
365
+ plan = this.encodeQuery(sql, params, cache);
366
+ } catch (cause) {
367
+ return Effect.fail(queryError(cause, "PgConnection: Failed to encode query"));
368
+ }
369
+ const run = runQuery(this, plan, wantRows);
370
+ if (cache === undefined || plan.parses) return run;
371
+ return retryStale(plan, cache, run, () => this.attempt(sql, params, wantRows, undefined));
372
+ };
373
+ /** Sends a `CancelRequest` for this session on a side connection. */
374
+ cancel = /*#__PURE__*/Effect.suspend(() => {
375
+ if (this.deadWith !== undefined) return Effect.void;
376
+ return sendCancelRequest(this.resolved, this.session.processId, this.session.secretKey);
377
+ });
378
+ pin = /*#__PURE__*/Effect.suspend(() => {
379
+ const reserve = this[internalsKey].reserve;
380
+ return reserve === undefined ? this.pinExclusive : Effect.andThen(reserve, this.pinExclusive);
381
+ });
382
+ pinExclusive = /*#__PURE__*/Effect.acquireRelease(/*#__PURE__*/Effect.flatMap(/*#__PURE__*/this.owner.take(1), () =>
383
+ // Holding `owner` stops new submissions; a pipeline already on the wire
384
+ // still has to drain before this fiber owns the connection.
385
+ Effect.as(Effect.tap(Effect.onInterrupt(this.waitPipelineIdle, () => this.owner.release(1)), () => Effect.sync(() => {
386
+ this.pinned = true;
387
+ })), this.pinnedView)), () => Effect.suspend(() => {
388
+ this.pinned = false;
389
+ return this.owner.release(1);
390
+ }));
391
+ run = (sql, params, wantRows, prepare) => {
392
+ const cache = prepare ? this.prepared : undefined;
393
+ return this.multiplex ? this.pipelineCycle(sql, params, wantRows, cache) : this.owner.withPermit(this.cycleOwned(sql, params, wantRows, cache));
394
+ };
395
+ query = (sql, params, prepare = true) => Effect.map(this.run(sql, params ?? emptyParams, true, prepare), takeResult);
396
+ queryValues = (sql, params, prepare = true) => Effect.map(this.run(sql, params ?? emptyParams, false, prepare), takeValues);
397
+ stream = (sql, params) => streamRows(this, this.pin, sql, params ?? emptyParams);
398
+ listen = channel => listenChannel(this, this.pin, channel);
399
+ interrupt = /*#__PURE__*/Effect.suspend(() => this.multiplex && !this.pinned ? Effect.void : this.cancel);
400
+ }
401
+ /**
402
+ * The view of a session returned by `pin`: statements skip the ownership
403
+ * queue and re-pinning is a no-op, making `pin` reentrant for `stream` and
404
+ * `listen` running inside a transaction.
405
+ */
406
+ class PinnedPgConnection {
407
+ [TypeId] = TypeId;
408
+ base;
409
+ [internalsKey];
410
+ pin;
411
+ interrupt;
412
+ constructor(base) {
413
+ this.base = base;
414
+ this[internalsKey] = base[internalsKey];
415
+ this.pin = Effect.succeed(this);
416
+ this.interrupt = base.cancel;
417
+ }
418
+ get config() {
419
+ return this.base.config;
420
+ }
421
+ get processId() {
422
+ return this.base.processId;
423
+ }
424
+ query = (sql, params, prepare = true) => Effect.map(this.base.cycle(sql, params ?? emptyParams, true, prepare ? this.base.prepared : undefined), takeResult);
425
+ queryValues = (sql, params, prepare = true) => Effect.map(this.base.cycle(sql, params ?? emptyParams, false, prepare ? this.base.prepared : undefined), takeValues);
426
+ stream = (sql, params) => streamRows(this.base, this.pin, sql, params ?? emptyParams);
427
+ listen = channel => listenChannel(this.base, this.pin, channel);
428
+ }
429
+ const INT32_MIN = -2147483648;
430
+ const INT32_MAX = 2147483647;
431
+ const inferredParameter = (oid, value) => ({
432
+ [PgTypes.ParameterTypeId]: PgTypes.ParameterTypeId,
433
+ oid,
434
+ value
435
+ });
436
+ const inferScalar = value => {
437
+ if (PgTypes.isParameter(value)) return value;
438
+ if (value === null || value === undefined) return inferredParameter(0, null);
439
+ switch (typeof value) {
440
+ case "boolean":
441
+ return inferredParameter(PgTypes.OID.bool, value);
442
+ case "bigint":
443
+ return inferredParameter(PgTypes.OID.int8, value);
444
+ case "number":
445
+ if (Number.isInteger(value)) {
446
+ if (value >= INT32_MIN && value <= INT32_MAX) {
447
+ return inferredParameter(PgTypes.OID.int4, value);
448
+ }
449
+ if (Number.isSafeInteger(value)) {
450
+ return inferredParameter(PgTypes.OID.int8, BigInt(value));
451
+ }
452
+ }
453
+ return inferredParameter(PgTypes.OID.float8, value);
454
+ case "string":
455
+ // Bound with no concrete type, as a text-format literal, so the backend
456
+ // derives the type from the statement: a string works against a bigint
457
+ // or timestamp column the way it did with the text-protocol drivers.
458
+ return inferredParameter(0, value);
459
+ }
460
+ if (value instanceof Date) {
461
+ const time = value.getTime();
462
+ if (Number.isNaN(time)) throw new PgTypes.CodecError({
463
+ message: "Invalid Date parameter"
464
+ });
465
+ return inferredParameter(PgTypes.OID.timestamptz, time);
466
+ }
467
+ if (value instanceof Uint8Array) return inferredParameter(PgTypes.OID.bytea, value);
468
+ if (value instanceof Int8Array) {
469
+ return inferredParameter(PgTypes.OID.bytea, new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
470
+ }
471
+ throw new PgTypes.CodecError({
472
+ message: `Cannot infer a PostgreSQL type for ${String(value)}`
473
+ });
474
+ };
475
+ const inferParameter = (value, registry) => {
476
+ if (!Array.isArray(value)) return inferScalar(value);
477
+ if (value.length === 0) {
478
+ throw new PgTypes.CodecError({
479
+ message: "Cannot infer the type of an empty array; use PgTypes.array"
480
+ });
481
+ }
482
+ let elementOid;
483
+ const values = new Array(value.length);
484
+ for (let index = 0; index < value.length; index++) {
485
+ const element = value[index];
486
+ if (Array.isArray(element)) {
487
+ throw new PgTypes.CodecError({
488
+ message: "Nested array parameters are not supported"
489
+ });
490
+ }
491
+ const parameter = inferScalar(element);
492
+ // A scalar string binds untyped, but an array names its element type.
493
+ let oid = parameter.oid;
494
+ if (oid === 0) {
495
+ if (parameter.value === null) {
496
+ values[index] = null;
497
+ continue;
498
+ }
499
+ oid = PgTypes.OID.text;
500
+ }
501
+ if (Array.isArray(parameter.value)) {
502
+ throw new PgTypes.CodecError({
503
+ message: "Nested array parameters are not supported"
504
+ });
505
+ }
506
+ if (elementOid === undefined) elementOid = oid;else if (elementOid !== oid) {
507
+ throw new PgTypes.CodecError({
508
+ message: "Array parameter elements must have the same inferred OID"
509
+ });
510
+ }
511
+ values[index] = parameter.value;
512
+ }
513
+ if (elementOid === undefined) {
514
+ throw new PgTypes.CodecError({
515
+ message: "Cannot infer the type of an array containing only null values"
516
+ });
517
+ }
518
+ const arrayOid = PgTypes.arrayOidFor(elementOid, registry);
519
+ if (arrayOid === undefined) {
520
+ throw new PgTypes.CodecError({
521
+ message: `No array type known for element OID ${elementOid}`
522
+ });
523
+ }
524
+ return inferredParameter(arrayOid, values);
525
+ };
526
+ /**
527
+ * Joins the parts of a frame into the buffer that goes on the wire.
528
+ *
529
+ * The buffer comes from Node's pool and can be passed directly to
530
+ * `socket.write`. Every byte is initialized before the buffer is returned.
531
+ */
532
+ const concat = chunks => {
533
+ let length = 0;
534
+ for (let index = 0; index < chunks.length; index++) length += chunks[index].length;
535
+ const output = Buffer.allocUnsafe(length);
536
+ let offset = 0;
537
+ for (let index = 0; index < chunks.length; index++) {
538
+ output.set(chunks[index], offset);
539
+ offset += chunks[index].length;
540
+ }
541
+ return output;
542
+ };
543
+ const emptyParams = [];
544
+ const takeResult = output => output.result;
545
+ const takeValues = output => output.values;
546
+ /**
547
+ * The tail of every extended-query frame. `Describe` names the unnamed portal,
548
+ * `Execute` runs it without a row limit, and `Sync` closes the cycle; none of
549
+ * the three carries per-query state, so the bytes are encoded once.
550
+ */
551
+ const describeExecuteSync = /*#__PURE__*/concat([/*#__PURE__*/PgProtocol.encodeDescribe({
552
+ target: "portal",
553
+ name: ""
554
+ }), /*#__PURE__*/PgProtocol.encodeExecute({
555
+ portal: "",
556
+ maxRows: 0
557
+ }), /*#__PURE__*/PgProtocol.encodeSync()]);
558
+ /** The same tail for a statement whose columns are already known. */
559
+ const executeSync = /*#__PURE__*/concat([/*#__PURE__*/PgProtocol.encodeExecute({
560
+ portal: "",
561
+ maxRows: 0
562
+ }), /*#__PURE__*/PgProtocol.encodeSync()]);
563
+ /** The default number of statements a connection keeps prepared. */
564
+ const defaultPreparedStatements = 100;
565
+ /** The unnamed path: `Parse` / `Bind` / `Describe` / `Execute` / `Sync`. */
566
+ const encodeUnnamed = (sql, parameters, parameterTypes, encodeBind) => {
567
+ const parse = PgProtocol.encodeParse({
568
+ name: "",
569
+ query: sql,
570
+ parameterTypes
571
+ });
572
+ if (EffectResult.isFailure(parse)) throw parse.failure;
573
+ const bind = encodeBind({
574
+ portal: "",
575
+ statement: "",
576
+ parameters
577
+ });
578
+ if (EffectResult.isFailure(bind)) throw bind.failure;
579
+ return {
580
+ frame: concat([parse.success, bind.success, describeExecuteSync]),
581
+ closes: 0,
582
+ parses: true,
583
+ describes: true,
584
+ prepared: undefined,
585
+ description: undefined,
586
+ stale: false
587
+ };
588
+ };
589
+ /**
590
+ * Builds one extended-query cycle as a single buffer, so a statement costs one
591
+ * socket write.
592
+ *
593
+ * A statement the backend already holds under a name needs only
594
+ * `Bind` / `Execute` / `Sync`: no `Parse` for the backend to plan and no
595
+ * `Describe`, because the columns came back the first time and are cached with
596
+ * the name. Statements evicted from the cache ride along as `Close` messages
597
+ * rather than paying a round trip of their own.
598
+ */
599
+ const encodeQuery = (sql, params, registry, encodeBind, cache) => {
600
+ const count = params.length;
601
+ const parameters = new Array(count);
602
+ const parameterTypes = new Array(count);
603
+ for (let index = 0; index < count; index++) {
604
+ const parameter = inferParameter(params[index], registry);
605
+ parameters[index] = parameter;
606
+ parameterTypes[index] = parameter.oid;
607
+ }
608
+ if (cache === undefined) {
609
+ return encodeUnnamed(sql, parameters, parameterTypes, encodeBind);
610
+ }
611
+ const prepared = cache.get(sql, parameterTypes);
612
+ if (!prepared.ready && prepared.parsing) {
613
+ // Another cycle is already on the wire carrying this name's `Parse`.
614
+ // Naming it again would collide, and reusing it would mean binding to
615
+ // columns nobody has seen yet, so this execution goes unnamed.
616
+ return encodeUnnamed(sql, parameters, parameterTypes, encodeBind);
617
+ }
618
+ const bind = encodeBind({
619
+ portal: "",
620
+ statement: prepared.name,
621
+ parameters
622
+ });
623
+ if (EffectResult.isFailure(bind)) throw bind.failure;
624
+ const parse = prepared.ready ? undefined : PgProtocol.encodeParse({
625
+ name: prepared.name,
626
+ query: sql,
627
+ parameterTypes
628
+ });
629
+ if (parse !== undefined && EffectResult.isFailure(parse)) throw parse.failure;
630
+ const closeFrames = cache.takeCloses();
631
+ if (parse === undefined) {
632
+ return {
633
+ frame: closeFrames === undefined ? concat([bind.success, executeSync]) : concat([closeFrames.frames, bind.success, executeSync]),
634
+ closes: closeFrames?.count ?? 0,
635
+ parses: false,
636
+ describes: false,
637
+ prepared,
638
+ description: prepared.description,
639
+ stale: false
640
+ };
641
+ }
642
+ prepared.parsing = true;
643
+ return {
644
+ frame: closeFrames === undefined ? concat([parse.success, bind.success, describeExecuteSync]) : concat([closeFrames.frames, parse.success, bind.success, describeExecuteSync]),
645
+ closes: closeFrames?.count ?? 0,
646
+ parses: true,
647
+ describes: true,
648
+ prepared,
649
+ description: undefined,
650
+ stale: false
651
+ };
652
+ };
653
+ /**
654
+ * The statements one connection has prepared, keyed by SQL text and the
655
+ * parameter OIDs inferred for it: the same text with differently typed
656
+ * parameters is a different statement to the backend.
657
+ *
658
+ * The cache is bounded and evicts least-recently-used. An evicted statement is
659
+ * closed on the backend, but its `Close` waits for the next statement to go
660
+ * out rather than taking a round trip of its own.
661
+ */
662
+ class PreparedCache {
663
+ max;
664
+ statements = /*#__PURE__*/new Map();
665
+ closes;
666
+ counter = 0;
667
+ constructor(max) {
668
+ this.max = max;
669
+ }
670
+ get(sql, parameterTypes) {
671
+ const key = parameterTypes.length === 0 ? sql : `${sql}\u0000${parameterTypes.join(",")}`;
672
+ const found = this.statements.get(key);
673
+ if (found !== undefined) {
674
+ // Re-insert to move it to the end: `Map` iterates in insertion order, so
675
+ // the first key is the least recently used one.
676
+ this.statements.delete(key);
677
+ this.statements.set(key, found);
678
+ this.trim(found);
679
+ return found;
680
+ }
681
+ const prepared = {
682
+ name: `effect${++this.counter}`,
683
+ key,
684
+ ready: false,
685
+ parsing: false,
686
+ description: undefined
687
+ };
688
+ this.statements.set(key, prepared);
689
+ this.trim(prepared);
690
+ return prepared;
691
+ }
692
+ /**
693
+ * A statement still being parsed cannot be closed yet: its `Parse` may not
694
+ * have reached the socket. Let the cache exceed its bound temporarily, then
695
+ * discard abandoned entries and close excess ready statements on access.
696
+ */
697
+ trim(protectedStatement) {
698
+ while (this.statements.size > this.max) {
699
+ let evicted;
700
+ for (const prepared of this.statements.values()) {
701
+ if (prepared !== protectedStatement && (prepared.ready || !prepared.parsing)) {
702
+ evicted = prepared;
703
+ break;
704
+ }
705
+ }
706
+ if (evicted === undefined) return;
707
+ this.statements.delete(evicted.key);
708
+ if (evicted.ready) this.close(evicted.name);
709
+ }
710
+ }
711
+ /**
712
+ * Drops a statement the backend no longer holds, or whose plan went stale.
713
+ *
714
+ * The name is closed either way. A plan that went stale is still held under
715
+ * it, and re-parsing would collide; a name the backend has already lost
716
+ * ignores the `Close`, which Postgres treats as a success.
717
+ */
718
+ evict(prepared) {
719
+ if (prepared.parsing) return;
720
+ if (this.statements.delete(prepared.key) && prepared.ready) this.close(prepared.name);
721
+ }
722
+ /**
723
+ * Drops a statement whose parsing cycle failed before its columns arrived.
724
+ *
725
+ * A `Parse` that completed before the error outlives the failed cycle, so
726
+ * the backend may hold the name while the entry can never become ready. The
727
+ * name is closed either way: closing one the backend never registered is a
728
+ * no-op.
729
+ */
730
+ evictFailed(prepared) {
731
+ this.statements.delete(prepared.key);
732
+ this.close(prepared.name);
733
+ }
734
+ close(name) {
735
+ ;
736
+ (this.closes ??= []).push(PgProtocol.encodeClose({
737
+ target: "statement",
738
+ name
739
+ }));
740
+ }
741
+ takeCloses() {
742
+ const closes = this.closes;
743
+ if (closes === undefined) return undefined;
744
+ this.closes = undefined;
745
+ return {
746
+ frames: closes.length === 1 ? closes[0] : concat(closes),
747
+ count: closes.length
748
+ };
749
+ }
750
+ }
751
+ /**
752
+ * SQLSTATEs that mean "this name is no longer usable": the backend lost the
753
+ * statement, or the plan behind it no longer matches the columns it was
754
+ * prepared for. Both are recovered by parsing the statement again.
755
+ */
756
+ const isStalePreparedStatement = code => code === "26000" || code === "0A000";
757
+ /**
758
+ * The default builtin-catalogue encoder, shared by every connection without a
759
+ * custom registry. Passing `PgTypes.writeParameter` itself engages
760
+ * `makeBindEncoder`'s unsafe fast path.
761
+ */
762
+ const defaultBindEncoder = /*#__PURE__*/PgProtocol.makeBindEncoder(PgTypes.writeParameter, PgTypes.isTextFormat);
763
+ /** One bind encoder per registry, since building one allocates a closure. */
764
+ const makeBindEncoder = registry => registry === undefined ? defaultBindEncoder : PgProtocol.makeBindEncoder((sink, parameter) => PgTypes.writeParameter(sink, parameter, registry), PgTypes.isTextFormat);
765
+ const queryError = (cause, message) => new SqlError({
766
+ reason: new UnknownError({
767
+ cause,
768
+ message,
769
+ operation: "query"
770
+ })
771
+ });
772
+ /**
773
+ * Retries a cycle whose named statement the backend no longer honors: the
774
+ * statement is dropped from the cache and the retry runs unnamed, so it cannot
775
+ * loop.
776
+ */
777
+ const retryStale = (plan, cache, run, rerun) => Effect.catchCause(run, cause => {
778
+ if (!plan.stale) return Effect.failCause(cause);
779
+ cache.evict(plan.prepared);
780
+ return rerun();
781
+ });
782
+ const connectionQueryError = (cause, message) => new SqlError({
783
+ reason: new ConnectionError({
784
+ cause,
785
+ message,
786
+ operation: "query"
787
+ })
788
+ });
789
+ const escapeIdentifier = identifier => `"${identifier.replaceAll("\"", "\"\"")}"`;
790
+ /**
791
+ * Splits a command tag such as `SELECT 3` or `INSERT 0 1`. Reading the two
792
+ * spaces directly keeps a completed statement from allocating the parts array
793
+ * that splitting on every space would.
794
+ */
795
+ const parseCommandTag = tag => {
796
+ const firstSpace = tag.indexOf(" ");
797
+ if (firstSpace < 0) return {
798
+ command: tag,
799
+ oid: null,
800
+ rowCount: 0
801
+ };
802
+ const command = tag.slice(0, firstSpace);
803
+ const secondSpace = tag.indexOf(" ", firstSpace + 1);
804
+ if (command === "INSERT" && secondSpace > 0) {
805
+ return {
806
+ command,
807
+ oid: Number(tag.slice(firstSpace + 1, secondSpace)),
808
+ rowCount: Number(tag.slice(secondSpace + 1))
809
+ };
810
+ }
811
+ const last = tag.slice((secondSpace < 0 ? firstSpace : secondSpace) + 1);
812
+ return {
813
+ command,
814
+ oid: null,
815
+ rowCount: isDigits(last) ? Number(last) : 0
816
+ };
817
+ };
818
+ const isDigits = value => {
819
+ if (value.length === 0) return false;
820
+ for (let index = 0; index < value.length; index++) {
821
+ const code = value.charCodeAt(index);
822
+ if (code < 48 || code > 57) return false;
823
+ }
824
+ return true;
825
+ };
826
+ /**
827
+ * Tracks one query cycle while the connection routes backend messages to it.
828
+ * Each pipelined cycle keeps its own parser state and completes once.
829
+ */
830
+ class QueryMachine {
831
+ conn;
832
+ plan;
833
+ finish;
834
+ /** Installed on the parser while this machine is at the head of the queue. */
835
+ readField;
836
+ /** Object rows, or `undefined` when the caller asked for positional ones. */
837
+ rows;
838
+ values;
839
+ closes;
840
+ phase;
841
+ fieldCount;
842
+ resultFields;
843
+ rowBuilder;
844
+ command = "";
845
+ rowCount = 0;
846
+ oid = null;
847
+ failure;
848
+ done = false;
849
+ aborted = false;
850
+ drainDone;
851
+ constructor(conn, plan, wantRows, finish) {
852
+ this.conn = conn;
853
+ this.plan = plan;
854
+ this.finish = finish;
855
+ this.rows = wantRows ? [] : undefined;
856
+ this.values = wantRows ? undefined : [];
857
+ this.closes = plan.closes;
858
+ this.phase = plan.closes > 0 ? "close" : plan.parses ? "parse" : "bind";
859
+ const description = plan.description;
860
+ this.readField = description?.readField;
861
+ this.fieldCount = description?.resultFields.length ?? 0;
862
+ this.resultFields = description?.resultFields ?? emptyFields;
863
+ this.rowBuilder = description?.rowBuilder;
864
+ }
865
+ isDone() {
866
+ return this.done;
867
+ }
868
+ abort(onDrained) {
869
+ if (this.done) return onDrained();
870
+ this.aborted = true;
871
+ this.drainDone = onDrained;
872
+ }
873
+ complete(effect) {
874
+ if (this.done) return;
875
+ this.done = true;
876
+ // Whether it parsed or not, this cycle no longer holds the name: success
877
+ // has already marked it ready, failure leaves it to be parsed again.
878
+ const prepared = this.plan.prepared;
879
+ if (prepared !== undefined) prepared.parsing = false;
880
+ this.finish(effect);
881
+ }
882
+ failDesync(message) {
883
+ this.conn.fatal(connectionQueryError(new Error(message), `PgConnection: ${message}`));
884
+ }
885
+ onFatal(error) {
886
+ if (this.done) return;
887
+ this.done = true;
888
+ const prepared = this.plan.prepared;
889
+ if (prepared !== undefined) prepared.parsing = false;
890
+ if (this.aborted) this.drainDone?.();else this.finish(Effect.fail(error));
891
+ }
892
+ onMessage(message) {
893
+ if (this.aborted) {
894
+ if (message._tag === "ReadyForQuery") {
895
+ this.done = true;
896
+ this.drainDone?.();
897
+ }
898
+ return;
899
+ }
900
+ if (this.phase === "error") {
901
+ if (message._tag === "ReadyForQuery") return this.complete(Effect.fail(this.failure));
902
+ return this.failDesync(`Unexpected ${message._tag} after ErrorResponse`);
903
+ }
904
+ switch (message._tag) {
905
+ case "CloseComplete":
906
+ if (this.phase !== "close") return this.failDesync(`Unexpected CloseComplete during ${this.phase}`);
907
+ if (--this.closes === 0) this.phase = this.plan.parses ? "parse" : "bind";
908
+ return;
909
+ case "ParseComplete":
910
+ if (this.phase !== "parse") return this.failDesync(`Unexpected ParseComplete during ${this.phase}`);
911
+ this.phase = "bind";
912
+ return;
913
+ case "BindComplete":
914
+ if (this.phase !== "bind") return this.failDesync(`Unexpected BindComplete during ${this.phase}`);
915
+ this.phase = this.plan.describes ? "describe" : "rows";
916
+ return;
917
+ case "RowDescription":
918
+ {
919
+ if (this.phase !== "describe") return this.failDesync(`Unexpected RowDescription during ${this.phase}`);
920
+ const description = describe(message.fields, this.conn.registry);
921
+ if (EffectResult.isFailure(description)) {
922
+ return this.conn.fatal(queryError(description.failure, "PgConnection: Failed to decode row"));
923
+ }
924
+ this.fieldCount = message.fields.length;
925
+ this.resultFields = description.success.resultFields;
926
+ this.rowBuilder = description.success.rowBuilder;
927
+ // `pushEach` hands this description over before it reads the rows
928
+ // behind it, including rows that arrived in the same chunk.
929
+ this.conn.session.parser.readField = description.success.readField;
930
+ const prepared = this.plan.prepared;
931
+ if (prepared !== undefined) {
932
+ prepared.description = description.success;
933
+ prepared.ready = true;
934
+ }
935
+ this.phase = "rows";
936
+ return;
937
+ }
938
+ case "NoData":
939
+ {
940
+ if (this.phase !== "describe") return this.failDesync(`Unexpected NoData during ${this.phase}`);
941
+ const prepared = this.plan.prepared;
942
+ if (prepared !== undefined) {
943
+ prepared.description = undefined;
944
+ prepared.ready = true;
945
+ }
946
+ this.phase = "rows";
947
+ return;
948
+ }
949
+ case "DataRow":
950
+ {
951
+ if (this.phase !== "rows" || this.rowBuilder === undefined) {
952
+ return this.failDesync(`Unexpected DataRow during ${this.phase}`);
953
+ }
954
+ const rowValues = message.values;
955
+ if (rowValues.length !== this.fieldCount) {
956
+ return this.failDesync(`DataRow has ${rowValues.length} values for ${this.fieldCount} fields`);
957
+ }
958
+ if (this.rows !== undefined) this.rows.push(this.rowBuilder(rowValues));else this.values.push(rowValues);
959
+ return;
960
+ }
961
+ case "CommandComplete":
962
+ {
963
+ if (this.phase !== "rows") return this.failDesync(`Unexpected CommandComplete during ${this.phase}`);
964
+ const parsed = parseCommandTag(message.commandTag);
965
+ this.command = parsed.command;
966
+ this.rowCount = parsed.rowCount;
967
+ this.oid = parsed.oid;
968
+ this.phase = "complete";
969
+ return;
970
+ }
971
+ case "EmptyQueryResponse":
972
+ if (this.phase !== "rows") return this.failDesync(`Unexpected EmptyQueryResponse during ${this.phase}`);
973
+ this.phase = "complete";
974
+ return;
975
+ case "ErrorResponse":
976
+ {
977
+ if (isStalePreparedStatement(message.fields.code)) this.plan.stale = true;
978
+ // A cycle that carried this statement's `Parse` but failed before its
979
+ // columns arrived leaves an entry that can never become ready while
980
+ // the backend may still hold the name: the `Parse` outlives the
981
+ // failed cycle. Drop the entry and close the name, so the next
982
+ // execution parses fresh under a new one.
983
+ const prepared = this.plan.prepared;
984
+ if (this.plan.parses && prepared !== undefined && !prepared.ready) {
985
+ this.conn.prepared?.evictFailed(prepared);
986
+ }
987
+ // Every phase drains the same way: the backend skips the rest of the
988
+ // cycle and sends `ReadyForQuery` after the `Sync` that closes it, so
989
+ // a statement it refused to parse leaves the session usable.
990
+ this.failure = new SqlError({
991
+ reason: classifyFields(message.fields, "PgConnection: Query failed", "query")
992
+ });
993
+ this.phase = "error";
994
+ return;
995
+ }
996
+ case "ReadyForQuery":
997
+ if (this.phase !== "complete") return this.failDesync(`Unexpected ReadyForQuery during ${this.phase}`);
998
+ return this.complete(Effect.succeed({
999
+ result: {
1000
+ command: this.command,
1001
+ rowCount: this.rowCount,
1002
+ oid: this.oid,
1003
+ rows: this.rows ?? emptyRows,
1004
+ fields: this.resultFields
1005
+ },
1006
+ values: this.values ?? emptyValues
1007
+ }));
1008
+ case "CopyInResponse":
1009
+ case "CopyOutResponse":
1010
+ case "CopyBothResponse":
1011
+ case "CopyData":
1012
+ case "CopyDone":
1013
+ return this.failDesync(`Unexpected ${message._tag}; COPY is not supported`);
1014
+ default:
1015
+ return this.failDesync(`Unexpected ${message._tag} during ${this.phase}`);
1016
+ }
1017
+ }
1018
+ }
1019
+ const emptyRows = [];
1020
+ const emptyValues = [];
1021
+ /** One cycle on a connection that carries a single statement at a time. */
1022
+ const runQuery = (conn, plan, wantRows) => Effect.callback(resume => {
1023
+ if (conn.deadWith !== undefined) {
1024
+ resume(Effect.fail(conn.deadWith));
1025
+ return;
1026
+ }
1027
+ const machine = new QueryMachine(conn, plan, wantRows, effect => {
1028
+ conn.consumer = undefined;
1029
+ resume(effect);
1030
+ });
1031
+ conn.consumer = machine;
1032
+ // A reused statement has no `RowDescription` coming, so its reader has to
1033
+ // be in place before the rows are.
1034
+ conn.session.parser.readField = machine.readField;
1035
+ try {
1036
+ conn.session.socket.write(plan.frame);
1037
+ } catch (cause) {
1038
+ conn.fatal(connectionQueryError(cause, "PgConnection: Failed to write query"));
1039
+ }
1040
+ // On interruption: cancel the statement and drain the connection back to
1041
+ // ReadyForQuery so it stays usable, destroying it when the drain stalls.
1042
+ return Effect.suspend(() => {
1043
+ if (machine.isDone()) return Effect.void;
1044
+ const wait = Effect.callback(resumeWait => {
1045
+ const timer = setTimeout(() => conn.fatal(connectionQueryError(new Error("Query cancellation timed out"), "PgConnection: Query cancellation timed out")), abortDrainTimeoutMillis);
1046
+ machine.abort(() => {
1047
+ clearTimeout(timer);
1048
+ conn.consumer = undefined;
1049
+ resumeWait(Effect.void);
1050
+ });
1051
+ });
1052
+ return Effect.andThen(conn.cancel, wait);
1053
+ });
1054
+ });
1055
+ const emptyFields = [];
1056
+ /** Derives the per-column readers and the row constructor for a description. */
1057
+ const describe = (fields, registry) => {
1058
+ const reader = PgTypes.makeFieldReader(fields, registry);
1059
+ if (EffectResult.isFailure(reader)) return EffectResult.fail(reader.failure);
1060
+ const resultFields = new Array(fields.length);
1061
+ for (let index = 0; index < fields.length; index++) {
1062
+ resultFields[index] = {
1063
+ name: fields[index].name,
1064
+ dataTypeId: fields[index].dataTypeOid
1065
+ };
1066
+ }
1067
+ return EffectResult.succeed({
1068
+ readField: reader.success,
1069
+ rowBuilder: makeRowBuilder(fields),
1070
+ resultFields
1071
+ });
1072
+ };
1073
+ /**
1074
+ * Builds the row constructor for one `RowDescription`.
1075
+ *
1076
+ * Assignment is an order of magnitude faster than `Object.defineProperty` and
1077
+ * stores the same own, enumerable, writable, configurable property - except
1078
+ * for `__proto__`, which assignment routes to the prototype setter instead. A
1079
+ * description carrying that column name falls back to the slow spelling.
1080
+ */
1081
+ const makeRowBuilder = fields => {
1082
+ const names = new Array(fields.length);
1083
+ let hasProto = false;
1084
+ for (let index = 0; index < fields.length; index++) {
1085
+ const name = fields[index].name;
1086
+ names[index] = name;
1087
+ if (name === "__proto__") hasProto = true;
1088
+ }
1089
+ if (hasProto) {
1090
+ return rowValues => {
1091
+ const row = {};
1092
+ for (let index = 0; index < names.length; index++) {
1093
+ Object.defineProperty(row, names[index], {
1094
+ value: rowValues[index],
1095
+ enumerable: true,
1096
+ configurable: true,
1097
+ writable: true
1098
+ });
1099
+ }
1100
+ return row;
1101
+ };
1102
+ }
1103
+ return rowValues => {
1104
+ const row = {};
1105
+ for (let index = 0; index < names.length; index++) {
1106
+ row[names[index]] = rowValues[index];
1107
+ }
1108
+ return row;
1109
+ };
1110
+ };
1111
+ const streamRows = (conn, pin, sql, params) => Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function* (_, scope) {
1112
+ yield* Scope.provide(pin, scope);
1113
+ yield* Scope.provide(Effect.acquireRelease(conn.wire.take(1), () => conn.wire.release(1)), scope);
1114
+ if (conn.deadWith !== undefined) return yield* conn.deadWith;
1115
+ // Streams stay on the unnamed path: a stream pays its setup once over the
1116
+ // whole result, so naming the statement buys little and would need the
1117
+ // stale-plan retry to unwind rows already delivered.
1118
+ const plan = yield* Effect.try({
1119
+ try: () => conn.encodeQuery(sql, params, undefined),
1120
+ catch: cause => queryError(cause, "PgConnection: Failed to encode query")
1121
+ });
1122
+ const frame = plan.frame;
1123
+ const socket = conn.session.socket;
1124
+ const parser = conn.session.parser;
1125
+ let phase = "parse";
1126
+ let fieldCount = 0;
1127
+ let rowBuilder;
1128
+ let buffer = [];
1129
+ let failure;
1130
+ let finished = false;
1131
+ let done = false;
1132
+ let aborted = false;
1133
+ let paused = false;
1134
+ let pending;
1135
+ let drainDone;
1136
+ const setPaused = value => {
1137
+ if (paused === value) return;
1138
+ paused = value;
1139
+ if (value) socket.pause();else socket.resume();
1140
+ };
1141
+ const deliver = () => {
1142
+ if (pending === undefined) return;
1143
+ const resume = pending;
1144
+ if (buffer.length > 0) {
1145
+ const chunk = buffer;
1146
+ buffer = [];
1147
+ pending = undefined;
1148
+ resume(Effect.succeed(chunk));
1149
+ } else if (finished) {
1150
+ pending = undefined;
1151
+ resume(failure !== undefined ? Effect.fail(failure) : Cause.done());
1152
+ }
1153
+ };
1154
+ const onFatal = error => {
1155
+ if (done) return;
1156
+ done = true;
1157
+ finished = true;
1158
+ if (failure === undefined) failure = error;
1159
+ drainDone?.();
1160
+ deliver();
1161
+ };
1162
+ // `conn.fatal` notifies the registered consumer; the direct `onFatal` call
1163
+ // covers the case where the connection was already dead.
1164
+ const failFatal = error => {
1165
+ conn.fatal(error);
1166
+ onFatal(error);
1167
+ };
1168
+ const failDesync = message => failFatal(connectionQueryError(new Error(message), `PgConnection: ${message}`));
1169
+ const onMessage = message => {
1170
+ if (aborted) {
1171
+ if (message._tag === "ReadyForQuery") {
1172
+ done = true;
1173
+ conn.consumer = undefined;
1174
+ drainDone?.();
1175
+ }
1176
+ return;
1177
+ }
1178
+ if (phase === "error") {
1179
+ switch (message._tag) {
1180
+ case "ReadyForQuery":
1181
+ finished = true;
1182
+ done = true;
1183
+ conn.consumer = undefined;
1184
+ deliver();
1185
+ return;
1186
+ default:
1187
+ return failDesync(`Unexpected ${message._tag} after ErrorResponse`);
1188
+ }
1189
+ }
1190
+ switch (message._tag) {
1191
+ case "ParseComplete":
1192
+ if (phase !== "parse") return failDesync(`Unexpected ParseComplete during ${phase}`);
1193
+ phase = "bind";
1194
+ return;
1195
+ case "BindComplete":
1196
+ if (phase !== "bind") return failDesync(`Unexpected BindComplete during ${phase}`);
1197
+ phase = "describe";
1198
+ return;
1199
+ case "RowDescription":
1200
+ {
1201
+ if (phase !== "describe") return failDesync(`Unexpected RowDescription during ${phase}`);
1202
+ const description = describe(message.fields, conn.registry);
1203
+ if (EffectResult.isFailure(description)) {
1204
+ return failFatal(queryError(description.failure, "PgConnection: Failed to decode row"));
1205
+ }
1206
+ fieldCount = message.fields.length;
1207
+ rowBuilder = description.success.rowBuilder;
1208
+ parser.readField = description.success.readField;
1209
+ phase = "rows";
1210
+ return;
1211
+ }
1212
+ case "NoData":
1213
+ if (phase !== "describe") return failDesync(`Unexpected NoData during ${phase}`);
1214
+ phase = "rows";
1215
+ return;
1216
+ case "DataRow":
1217
+ {
1218
+ if (phase !== "rows" || rowBuilder === undefined) {
1219
+ return failDesync(`Unexpected DataRow during ${phase}`);
1220
+ }
1221
+ const rowValues = message.values;
1222
+ if (rowValues.length !== fieldCount) {
1223
+ return failDesync(`DataRow has ${rowValues.length} values for ${fieldCount} fields`);
1224
+ }
1225
+ buffer.push(rowBuilder(rowValues));
1226
+ return;
1227
+ }
1228
+ case "CommandComplete":
1229
+ if (phase !== "rows") return failDesync(`Unexpected CommandComplete during ${phase}`);
1230
+ phase = "complete";
1231
+ return;
1232
+ case "EmptyQueryResponse":
1233
+ if (phase !== "rows") return failDesync(`Unexpected EmptyQueryResponse during ${phase}`);
1234
+ phase = "complete";
1235
+ return;
1236
+ case "ErrorResponse":
1237
+ {
1238
+ failure = new SqlError({
1239
+ reason: classifyFields(message.fields, "PgConnection: Query failed", "query")
1240
+ });
1241
+ phase = "error";
1242
+ return;
1243
+ }
1244
+ case "ReadyForQuery":
1245
+ if (phase !== "complete") return failDesync(`Unexpected ReadyForQuery during ${phase}`);
1246
+ finished = true;
1247
+ done = true;
1248
+ conn.consumer = undefined;
1249
+ deliver();
1250
+ return;
1251
+ case "CopyInResponse":
1252
+ case "CopyOutResponse":
1253
+ case "CopyBothResponse":
1254
+ case "CopyData":
1255
+ case "CopyDone":
1256
+ return failDesync(`Unexpected ${message._tag}; COPY is not supported`);
1257
+ default:
1258
+ return failDesync(`Unexpected ${message._tag} during ${phase}`);
1259
+ }
1260
+ };
1261
+ const onBatchEnd = () => {
1262
+ if (done || aborted) return;
1263
+ if (pending !== undefined) deliver();else if (buffer.length >= streamPauseThreshold) setPaused(true);
1264
+ };
1265
+ // On early abort: cancel the statement and drain back to ReadyForQuery so
1266
+ // the pinned connection stays usable, destroying it when the drain stalls.
1267
+ yield* Scope.addFinalizer(scope, Effect.suspend(() => {
1268
+ if (done) return Effect.void;
1269
+ aborted = true;
1270
+ setPaused(false);
1271
+ const wait = Effect.callback(resumeWait => {
1272
+ if (done) return resumeWait(Effect.void);
1273
+ const timer = setTimeout(() => conn.fatal(connectionQueryError(new Error("Stream cancellation timed out"), "PgConnection: Stream cancellation timed out")), abortDrainTimeoutMillis);
1274
+ drainDone = () => {
1275
+ clearTimeout(timer);
1276
+ resumeWait(Effect.void);
1277
+ };
1278
+ });
1279
+ return Effect.andThen(conn.cancel, wait);
1280
+ }));
1281
+ conn.consumer = {
1282
+ onMessage,
1283
+ onBatchEnd,
1284
+ onFatal
1285
+ };
1286
+ parser.readField = undefined;
1287
+ // @effect-diagnostics-next-line tryCatchInEffectGen:off
1288
+ try {
1289
+ socket.write(frame);
1290
+ } catch (cause) {
1291
+ failFatal(connectionQueryError(cause, "PgConnection: Failed to write query"));
1292
+ }
1293
+ // @effect-diagnostics-next-line returnEffectInGen:off
1294
+ return Effect.callback(resume => {
1295
+ pending = resume;
1296
+ setPaused(false);
1297
+ deliver();
1298
+ if (pending === undefined) return;
1299
+ return Effect.sync(() => {
1300
+ if (pending === resume) pending = undefined;
1301
+ });
1302
+ });
1303
+ })));
1304
+ const listenChannel = (conn, pin, channel) => Effect.uninterruptibleMask(restore => Effect.gen(function* () {
1305
+ const channelError = validateChannelName(channel, "listen");
1306
+ if (channelError !== undefined) return yield* Effect.fail(channelError);
1307
+ const parentScope = yield* Scope.Scope;
1308
+ const scope = yield* Scope.fork(parentScope);
1309
+ return yield* restore(Effect.gen(function* () {
1310
+ const pinned = yield* Scope.provide(pin, scope);
1311
+ if (conn.deadWith !== undefined) return yield* conn.deadWith;
1312
+ const queue = yield* Queue.unbounded();
1313
+ const identifier = escapeIdentifier(channel);
1314
+ let queues = conn.channels.get(channel);
1315
+ if (queues === undefined) {
1316
+ queues = new Set();
1317
+ conn.channels.set(channel, queues);
1318
+ }
1319
+ queues.add(queue);
1320
+ yield* Scope.addFinalizer(scope, Effect.suspend(() => {
1321
+ const current = conn.channels.get(channel);
1322
+ if (current === undefined) return Queue.shutdown(queue);
1323
+ current.delete(queue);
1324
+ if (current.size > 0) return Queue.shutdown(queue);
1325
+ conn.channels.delete(channel);
1326
+ const unlisten = conn.deadWith === undefined ? Effect.ignore(pinned.query(`UNLISTEN ${identifier}`)) : Effect.void;
1327
+ return Effect.andThen(unlisten, Queue.shutdown(queue));
1328
+ }));
1329
+ yield* pinned.query(`LISTEN ${identifier}`);
1330
+ return queue;
1331
+ })).pipe(Effect.tapCause(cause => Scope.close(scope, Exit.failCause(cause))));
1332
+ }));
1333
+ const sendCancelRequest = (config, pid, secret) => Effect.callback(resume => {
1334
+ let done = false;
1335
+ let socket;
1336
+ let timer;
1337
+ const finish = () => {
1338
+ if (done) return;
1339
+ done = true;
1340
+ if (timer !== undefined) clearTimeout(timer);
1341
+ socket?.destroy();
1342
+ resume(Effect.void);
1343
+ };
1344
+ const frame = PgProtocol.encodeCancelRequest({
1345
+ pid,
1346
+ secret
1347
+ });
1348
+ // After the frame is written the server processes the request and closes
1349
+ // the connection, which lands in the `close` handler.
1350
+ const send = () => {
1351
+ socket.write(frame);
1352
+ };
1353
+ const begin = () => {
1354
+ if (config.ssl === false) return send();
1355
+ socket.once("data", chunk => {
1356
+ if (done) return;
1357
+ // Never send the cancel secret over a connection the server refused
1358
+ // to upgrade.
1359
+ if (chunk.length !== 1 || chunk[0] !== 0x53) return finish();
1360
+ const raw = socket;
1361
+ raw.off("error", finish);
1362
+ raw.off("close", finish);
1363
+ socket = Tls.connect({
1364
+ host: config.host,
1365
+ servername: Net.isIP(config.host) === 0 ? config.host : undefined,
1366
+ ...(typeof config.ssl === "object" ? config.ssl : {}),
1367
+ socket: raw
1368
+ });
1369
+ socket.on("error", finish);
1370
+ socket.on("close", finish);
1371
+ socket.once("secureConnect", send);
1372
+ });
1373
+ socket.write(PgProtocol.encodeSslRequest());
1374
+ };
1375
+ try {
1376
+ socket = config.stream !== undefined ? config.stream() : config.path !== undefined ? Net.connect({
1377
+ path: config.path
1378
+ }) : Net.connect({
1379
+ host: config.host,
1380
+ port: config.port,
1381
+ noDelay: true
1382
+ });
1383
+ } catch {
1384
+ resume(Effect.void);
1385
+ return;
1386
+ }
1387
+ timer = setTimeout(finish, cancelRequestTimeoutMillis);
1388
+ socket.on("error", finish);
1389
+ socket.on("close", finish);
1390
+ if (config.stream !== undefined) begin();else socket.once("connect", begin);
1391
+ return Effect.sync(finish);
1392
+ });
1393
+ const connect = config => Effect.callback(resume => {
1394
+ let done = false;
1395
+ let socket;
1396
+ let parser;
1397
+ let sslErrorParser;
1398
+ let scram;
1399
+ let processId = 0;
1400
+ let secretKey = 0;
1401
+ const fail = reason => {
1402
+ if (done) return;
1403
+ done = true;
1404
+ socket?.destroy();
1405
+ resume(Effect.fail(new SqlError({
1406
+ reason
1407
+ })));
1408
+ };
1409
+ const failConnect = (cause, message) => fail(new ConnectionError({
1410
+ cause,
1411
+ message,
1412
+ operation: "connect"
1413
+ }));
1414
+ const failAuth = (cause, message) => fail(new AuthenticationError({
1415
+ cause,
1416
+ message,
1417
+ operation: "connect"
1418
+ }));
1419
+ const onError = cause => failConnect(cause, "PgConnection: Failed to connect");
1420
+ const onClose = () => failConnect(new Error("Connection closed unexpectedly"), "PgConnection: Connection closed during startup");
1421
+ const password = () => {
1422
+ if (config.password === undefined) {
1423
+ failAuth(new Error("The server requested password authentication"), "PgConnection: No password configured");
1424
+ return undefined;
1425
+ }
1426
+ return config.password;
1427
+ };
1428
+ const handleMessage = message => {
1429
+ switch (message._tag) {
1430
+ case "AuthenticationOk":
1431
+ case "NoticeResponse":
1432
+ case "NegotiateProtocolVersion":
1433
+ case "ParameterStatus":
1434
+ return;
1435
+ case "AuthenticationCleartextPassword":
1436
+ {
1437
+ const secret = password();
1438
+ if (secret === undefined) return;
1439
+ socket.write(PgProtocol.encodePasswordMessage({
1440
+ password: secret
1441
+ }));
1442
+ return;
1443
+ }
1444
+ case "AuthenticationMD5Password":
1445
+ {
1446
+ const secret = password();
1447
+ if (secret === undefined) return;
1448
+ const hashed = PgAuth.md5Password({
1449
+ user: config.username,
1450
+ password: secret,
1451
+ salt: message.salt
1452
+ });
1453
+ if (EffectResult.isFailure(hashed)) {
1454
+ return failAuth(hashed.failure, "PgConnection: MD5 authentication failed");
1455
+ }
1456
+ socket.write(PgProtocol.encodePasswordMessage({
1457
+ password: hashed.success
1458
+ }));
1459
+ return;
1460
+ }
1461
+ case "AuthenticationSASL":
1462
+ {
1463
+ if (!message.mechanisms.includes(PgAuth.SCRAM_SHA_256)) {
1464
+ return failAuth(new Error(`Unsupported SASL mechanisms: ${message.mechanisms.join(", ")}`), `PgConnection: Only ${PgAuth.SCRAM_SHA_256} is supported`);
1465
+ }
1466
+ const secret = password();
1467
+ if (secret === undefined) return;
1468
+ const init = PgAuth.scramInit({
1469
+ password: secret,
1470
+ nonce: randomBytes(18).toString("base64")
1471
+ });
1472
+ if (EffectResult.isFailure(init)) {
1473
+ return failAuth(init.failure, "PgConnection: SCRAM authentication failed");
1474
+ }
1475
+ scram = init.success.state;
1476
+ socket.write(PgProtocol.encodeSASLInitialResponse({
1477
+ mechanism: PgAuth.SCRAM_SHA_256,
1478
+ initialResponse: init.success.response
1479
+ }));
1480
+ return;
1481
+ }
1482
+ case "AuthenticationSASLContinue":
1483
+ {
1484
+ if (scram === undefined || scram._tag !== "ScramFirst") {
1485
+ return failConnect(new Error("Unexpected AuthenticationSASLContinue"), "PgConnection: Protocol desync");
1486
+ }
1487
+ const next = PgAuth.scramContinue(scram, message.data);
1488
+ if (EffectResult.isFailure(next)) {
1489
+ return failAuth(next.failure, "PgConnection: SCRAM authentication failed");
1490
+ }
1491
+ scram = next.success.state;
1492
+ socket.write(PgProtocol.encodeSASLResponse({
1493
+ data: next.success.response
1494
+ }));
1495
+ return;
1496
+ }
1497
+ case "AuthenticationSASLFinal":
1498
+ {
1499
+ if (scram === undefined || scram._tag !== "ScramFinal") {
1500
+ return failConnect(new Error("Unexpected AuthenticationSASLFinal"), "PgConnection: Protocol desync");
1501
+ }
1502
+ const verified = PgAuth.scramFinish(scram, message.data);
1503
+ if (EffectResult.isFailure(verified)) {
1504
+ return failAuth(verified.failure, "PgConnection: SCRAM server verification failed");
1505
+ }
1506
+ scram = undefined;
1507
+ return;
1508
+ }
1509
+ case "AuthenticationUnsupported":
1510
+ return failAuth(new Error(`Authentication method ${message.method} is not supported`), "PgConnection: Unsupported authentication method");
1511
+ case "BackendKeyData":
1512
+ processId = message.pid;
1513
+ secretKey = message.secret;
1514
+ return;
1515
+ case "ErrorResponse":
1516
+ return fail(classifyFields(message.fields, "PgConnection: Failed to connect", "connect"));
1517
+ case "ReadyForQuery":
1518
+ if (scram !== undefined) {
1519
+ return failAuth(new Error("The server completed authentication without proving its identity"), "PgConnection: SCRAM exchange did not complete");
1520
+ }
1521
+ done = true;
1522
+ socket.off("data", onData);
1523
+ socket.off("error", onError);
1524
+ socket.off("close", onClose);
1525
+ socket.on("error", ignoreError);
1526
+ resume(Effect.succeed({
1527
+ socket,
1528
+ parser: parser,
1529
+ processId,
1530
+ secretKey
1531
+ }));
1532
+ return;
1533
+ default:
1534
+ return failConnect(new Error(`Unexpected ${message._tag} message during startup`), "PgConnection: Protocol desync");
1535
+ }
1536
+ };
1537
+ const onData = chunk => {
1538
+ let messages;
1539
+ try {
1540
+ messages = parser.push(chunk);
1541
+ } catch (cause) {
1542
+ return failConnect(cause, "PgConnection: Failed to parse server response");
1543
+ }
1544
+ for (const message of messages) {
1545
+ if (done) return;
1546
+ handleMessage(message);
1547
+ }
1548
+ };
1549
+ const startup = () => {
1550
+ parser = PgProtocol.makeParser({
1551
+ maxMessageSize: config.maxMessageSize
1552
+ });
1553
+ socket.on("data", onData);
1554
+ socket.write(PgProtocol.encodeStartupMessage({
1555
+ user: config.username,
1556
+ database: config.database,
1557
+ application_name: config.applicationName
1558
+ }));
1559
+ };
1560
+ const onSslResponse = chunk => {
1561
+ if (done) return;
1562
+ if (sslErrorParser !== undefined || chunk[0] === 0x45) {
1563
+ sslErrorParser ??= PgProtocol.makeParser({
1564
+ maxMessageSize: config.maxMessageSize
1565
+ });
1566
+ let messages;
1567
+ try {
1568
+ messages = sslErrorParser.push(chunk);
1569
+ } catch (cause) {
1570
+ return failConnect(cause, "PgConnection: Failed to parse SSLRequest error response");
1571
+ }
1572
+ if (messages.length === 0) return;
1573
+ socket.off("data", onSslResponse);
1574
+ const message = messages[0];
1575
+ if (messages.length !== 1 || message._tag !== "ErrorResponse") {
1576
+ return failConnect(new Error("Expected one ErrorResponse after SSLRequest"), "PgConnection: Invalid SSLRequest response");
1577
+ }
1578
+ return fail(classifyFields(message.fields, "PgConnection: Failed to negotiate TLS", "connect"));
1579
+ }
1580
+ socket.off("data", onSslResponse);
1581
+ if (chunk.length !== 1) {
1582
+ return failConnect(new Error(`Received ${chunk.length} bytes in response to SSLRequest`), "PgConnection: Invalid SSLRequest response");
1583
+ }
1584
+ const response = PgProtocol.decodeSslResponse(chunk[0]);
1585
+ if (EffectResult.isFailure(response)) {
1586
+ return failConnect(response.failure, "PgConnection: Invalid SSLRequest response");
1587
+ }
1588
+ if (response.success === "N") {
1589
+ return failConnect(new Error("The server does not support TLS"), "PgConnection: Server refused TLS");
1590
+ }
1591
+ const raw = socket;
1592
+ raw.off("error", onError);
1593
+ raw.off("close", onClose);
1594
+ socket = Tls.connect({
1595
+ host: config.host,
1596
+ servername: Net.isIP(config.host) === 0 ? config.host : undefined,
1597
+ ...(typeof config.ssl === "object" ? config.ssl : {}),
1598
+ socket: raw
1599
+ });
1600
+ socket.on("error", onError);
1601
+ socket.on("close", onClose);
1602
+ socket.once("secureConnect", startup);
1603
+ };
1604
+ const begin = () => {
1605
+ if (config.ssl === false) return startup();
1606
+ socket.on("data", onSslResponse);
1607
+ socket.write(PgProtocol.encodeSslRequest());
1608
+ };
1609
+ try {
1610
+ socket = config.stream !== undefined ? config.stream() : config.path !== undefined ? Net.connect({
1611
+ path: config.path
1612
+ })
1613
+ // A statement is one write and then a wait for its answer, so Nagle has
1614
+ // nothing to coalesce and only holds the write back.
1615
+ : Net.connect({
1616
+ host: config.host,
1617
+ port: config.port,
1618
+ noDelay: true
1619
+ });
1620
+ } catch (cause) {
1621
+ resume(Effect.fail(new SqlError({
1622
+ reason: new ConnectionError({
1623
+ cause,
1624
+ message: "PgConnection: Failed to connect",
1625
+ operation: "connect"
1626
+ })
1627
+ })));
1628
+ return;
1629
+ }
1630
+ socket.on("error", onError);
1631
+ socket.on("close", onClose);
1632
+ if (config.stream !== undefined) {
1633
+ begin();
1634
+ } else {
1635
+ socket.once("connect", begin);
1636
+ }
1637
+ return Effect.sync(() => {
1638
+ if (done) return;
1639
+ done = true;
1640
+ socket.destroy();
1641
+ });
1642
+ });
1643
+ const ignoreError = _ => {};
1644
+ const configError = (message, cause) => new SqlError({
1645
+ reason: new ConnectionError({
1646
+ cause: cause ?? new Error(message),
1647
+ message: `PgConnection: ${message}`,
1648
+ operation: "connect"
1649
+ })
1650
+ });
1651
+ const resolveConfig = options => Effect.suspend(() => {
1652
+ const parsed = options.url !== undefined ? parseUrl(Redacted.value(options.url), options.ssl !== undefined) : EffectResult.succeed({});
1653
+ if (EffectResult.isFailure(parsed)) return Effect.fail(parsed.failure);
1654
+ const url = parsed.success;
1655
+ const host = options.host ?? url.host ?? "localhost";
1656
+ const port = options.port ?? url.port ?? 5432;
1657
+ const username = options.username ?? url.username ?? process.env.USER ?? process.env.USERNAME;
1658
+ if (username === undefined) {
1659
+ return Effect.fail(configError("No username configured"));
1660
+ }
1661
+ return Effect.succeed({
1662
+ host,
1663
+ port,
1664
+ path: options.path ?? (host.startsWith("/") ? `${host}/.s.PGSQL.${port}` : undefined),
1665
+ ssl: options.ssl ?? url.ssl ?? false,
1666
+ database: options.database ?? url.database,
1667
+ username,
1668
+ password: options.password !== undefined ? Redacted.value(options.password) : url.password,
1669
+ connectTimeout: Duration.fromInputUnsafe(options.connectTimeout ?? url.connectTimeout ?? Duration.seconds(5)),
1670
+ applicationName: options.applicationName ?? url.applicationName ?? "@effect/sql-pg",
1671
+ stream: options.stream,
1672
+ maxMessageSize: options.maxMessageSize
1673
+ });
1674
+ });
1675
+ const decodeComponent = (value, what) => {
1676
+ try {
1677
+ return EffectResult.succeed(decodeURIComponent(value));
1678
+ } catch {
1679
+ return EffectResult.fail(configError(`Invalid percent-encoding in URL ${what}`));
1680
+ }
1681
+ };
1682
+ const parsePort = (value, what) => {
1683
+ const port = Number(value);
1684
+ return !Number.isInteger(port) || port < 1 || port > 65535 ? EffectResult.fail(configError(`Invalid port in URL ${what}: "${value}"`)) : EffectResult.succeed(port);
1685
+ };
1686
+ const parseUrl = (raw, hasExplicitSsl) => {
1687
+ let url;
1688
+ try {
1689
+ url = new URL(raw);
1690
+ } catch (cause) {
1691
+ return EffectResult.fail(configError("Invalid connection URL", cause));
1692
+ }
1693
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
1694
+ return EffectResult.fail(configError(`Unsupported connection URL protocol: "${url.protocol}"`));
1695
+ }
1696
+ const config = {};
1697
+ if (url.hostname !== "") {
1698
+ if (url.hostname.startsWith("[") && url.hostname.endsWith("]")) {
1699
+ config.host = url.hostname.slice(1, -1);
1700
+ } else {
1701
+ const host = decodeComponent(url.hostname, "host");
1702
+ if (EffectResult.isFailure(host)) return EffectResult.fail(host.failure);
1703
+ config.host = host.success;
1704
+ }
1705
+ }
1706
+ if (url.port !== "") {
1707
+ const port = parsePort(url.port, "authority");
1708
+ if (EffectResult.isFailure(port)) return EffectResult.fail(port.failure);
1709
+ config.port = port.success;
1710
+ }
1711
+ if (url.username !== "") {
1712
+ const username = decodeComponent(url.username, "username");
1713
+ if (EffectResult.isFailure(username)) return EffectResult.fail(username.failure);
1714
+ config.username = username.success;
1715
+ }
1716
+ if (url.password !== "") {
1717
+ const password = decodeComponent(url.password, "password");
1718
+ if (EffectResult.isFailure(password)) return EffectResult.fail(password.failure);
1719
+ config.password = password.success;
1720
+ }
1721
+ const database = decodeComponent(url.pathname.replace(/^\//, ""), "database");
1722
+ if (EffectResult.isFailure(database)) return EffectResult.fail(database.failure);
1723
+ if (database.success !== "") config.database = database.success;
1724
+ for (const [key, value] of url.searchParams) {
1725
+ switch (key) {
1726
+ case "host":
1727
+ config.host = value;
1728
+ break;
1729
+ case "port":
1730
+ {
1731
+ const port = parsePort(value, "port parameter");
1732
+ if (EffectResult.isFailure(port)) return EffectResult.fail(port.failure);
1733
+ config.port = port.success;
1734
+ break;
1735
+ }
1736
+ case "user":
1737
+ config.username = value;
1738
+ break;
1739
+ case "password":
1740
+ config.password = value;
1741
+ break;
1742
+ case "dbname":
1743
+ config.database = value;
1744
+ break;
1745
+ case "application_name":
1746
+ config.applicationName = value;
1747
+ break;
1748
+ case "connect_timeout":
1749
+ {
1750
+ const seconds = Number(value);
1751
+ if (!Number.isInteger(seconds) || seconds < 0) {
1752
+ return EffectResult.fail(configError(`Invalid connect_timeout in URL: "${value}"`));
1753
+ }
1754
+ config.connectTimeout = seconds === 0 ? Duration.infinity : Duration.seconds(seconds);
1755
+ break;
1756
+ }
1757
+ case "sslmode":
1758
+ switch (value) {
1759
+ case "disable":
1760
+ config.ssl = false;
1761
+ break;
1762
+ case "require":
1763
+ case "verify-ca":
1764
+ case "verify-full":
1765
+ config.ssl = true;
1766
+ break;
1767
+ case "prefer":
1768
+ case "allow":
1769
+ if (hasExplicitSsl) break;
1770
+ return EffectResult.fail(configError(`sslmode "${value}" is not supported: set ssl explicitly to true or false`));
1771
+ default:
1772
+ return EffectResult.fail(configError(`Unrecognized sslmode in URL: "${value}"`));
1773
+ }
1774
+ break;
1775
+ // Unknown query parameters are ignored, matching libpq.
1776
+ }
1777
+ }
1778
+ return EffectResult.succeed(config);
1779
+ };
1780
+ const classifyFields = (fields, message, operation) => {
1781
+ const cause = Object.assign(new Error(fields.message ?? "Unknown PostgreSQL error"), fields);
1782
+ return classifySqlState(fields.code, fields.constraint, {
1783
+ cause,
1784
+ message,
1785
+ operation
1786
+ });
1787
+ };
1788
+ //# sourceMappingURL=PgConnection.js.map