@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.
- package/AGENTS.md +24 -9
- package/CLAUDE.md +24 -9
- package/ai-docs/package.json +2 -2
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
- package/ai-docs/src/01_effect/01_basics/index.md +5 -3
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
- package/ai-docs/src/03_stream/30_encoding.ts +5 -7
- package/ai-docs/src/08_observability/10_logging.ts +1 -1
- package/ai-docs/src/70_cli/10_basics.ts +7 -7
- package/ai-docs/src/71_ai/10_language-model.ts +2 -2
- package/ai-docs/src/71_ai/20_tools.ts +1 -1
- package/ai-docs/src/71_ai/30_chat.ts +1 -1
- package/dist/PgAuth.d.ts +24 -0
- package/dist/PgAuth.d.ts.map +1 -1
- package/dist/PgAuth.js +29 -2
- package/dist/PgAuth.js.map +1 -1
- package/dist/PgClient.d.ts +49 -82
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +58 -510
- package/dist/PgClient.js.map +1 -1
- package/dist/PgConnection.d.ts +177 -0
- package/dist/PgConnection.d.ts.map +1 -0
- package/dist/PgConnection.js +1788 -0
- package/dist/PgConnection.js.map +1 -0
- package/dist/PgPool.d.ts +107 -0
- package/dist/PgPool.d.ts.map +1 -0
- package/dist/PgPool.js +124 -0
- package/dist/PgPool.js.map +1 -0
- package/dist/PgProtocol.d.ts +63 -30
- package/dist/PgProtocol.d.ts.map +1 -1
- package/dist/PgProtocol.js +64 -22
- package/dist/PgProtocol.js.map +1 -1
- package/dist/PgTypes.d.ts +85 -17
- package/dist/PgTypes.d.ts.map +1 -1
- package/dist/PgTypes.js +166 -169
- package/dist/PgTypes.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/connection.d.ts +2 -0
- package/dist/internal/connection.d.ts.map +1 -0
- package/dist/internal/connection.js +5 -0
- package/dist/internal/connection.js.map +1 -0
- package/dist/internal/sqlError.d.ts +10 -0
- package/dist/internal/sqlError.d.ts.map +1 -0
- package/dist/internal/sqlError.js +57 -0
- package/dist/internal/sqlError.js.map +1 -0
- package/package.json +6 -14
- package/src/PgAuth.ts +33 -2
- package/src/PgClient.ts +138 -681
- package/src/PgConnection.ts +2296 -0
- package/src/PgPool.ts +231 -0
- package/src/PgProtocol.ts +93 -50
- package/src/PgTypes.ts +254 -186
- package/src/index.ts +10 -0
- package/src/internal/connection.ts +26 -0
- 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
|