neonctl 2.22.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/connection_string.test.js +0 -196
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COPY streaming adapter (WP-16).
|
|
3
|
+
*
|
|
4
|
+
* The wire-level work — driving CopyData / CopyDone / CopyFail through the
|
|
5
|
+
* v3 protocol — lives in `./connection.ts` directly (PgConnection exposes
|
|
6
|
+
* `startCopyIn` and `startCopyOut`). This module is a thin convenience layer
|
|
7
|
+
* that re-exports the public types and offers a single helper that callers
|
|
8
|
+
* can use without learning the connection-level state machine:
|
|
9
|
+
*
|
|
10
|
+
* const tag = await copyFromStream(conn, "COPY t FROM STDIN", readable);
|
|
11
|
+
* // tag → "COPY 17"
|
|
12
|
+
*
|
|
13
|
+
* for await (const chunk of (await conn.startCopyOut("COPY t TO STDOUT"))) {
|
|
14
|
+
* writable.write(chunk);
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Why PgConnection owns the protocol state and not this module: the COPY
|
|
18
|
+
* state machine is a *mode switch* on the same socket — CopyInResponse /
|
|
19
|
+
* CopyOutResponse / CopyDone / CommandComplete are interleaved with normal
|
|
20
|
+
* backend messages and have to be routed through the same MessageParser.
|
|
21
|
+
* Putting that here would require either exposing connection internals
|
|
22
|
+
* (private fields, message dispatch) or duplicating the parser, both worse
|
|
23
|
+
* than the in-place implementation.
|
|
24
|
+
*
|
|
25
|
+
* For the `\copy` parser/runner (cmd_copy.ts), this file is just the
|
|
26
|
+
* doorbell — call `connection.startCopyIn(sql)` to get a `CopyInStream`, then
|
|
27
|
+
* pipe a Readable into it via `pumpReadable()`.
|
|
28
|
+
*/
|
|
29
|
+
import { Buffer } from 'node:buffer';
|
|
30
|
+
/**
|
|
31
|
+
* Pump every chunk from a Node Readable into a CopyInStream, then end it.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors the loop in upstream `handleCopyIn`: read until EOF, write each
|
|
34
|
+
* buffer as CopyData, finalise with CopyDone. On read error, abort via
|
|
35
|
+
* CopyFail so the server returns to ready-state cleanly. Returns whatever
|
|
36
|
+
* the connection recorded as the last COPY command tag (e.g. `"COPY 17"`).
|
|
37
|
+
*
|
|
38
|
+
* The function is text/binary agnostic — we pass raw bytes through. CSV vs
|
|
39
|
+
* TEXT framing is the server's responsibility (controlled by the COPY
|
|
40
|
+
* options the caller stamped on the SQL string).
|
|
41
|
+
*/
|
|
42
|
+
export const pumpReadable = async (conn, readable, copyIn) => {
|
|
43
|
+
let aborted = null;
|
|
44
|
+
try {
|
|
45
|
+
for await (const chunk of readable) {
|
|
46
|
+
const buf = typeof chunk === 'string'
|
|
47
|
+
? Buffer.from(chunk, 'utf8')
|
|
48
|
+
: Buffer.isBuffer(chunk)
|
|
49
|
+
? chunk
|
|
50
|
+
: Buffer.from(chunk);
|
|
51
|
+
await copyIn.write(buf);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
aborted = err;
|
|
56
|
+
}
|
|
57
|
+
if (aborted !== null) {
|
|
58
|
+
const reason = abortReason(aborted);
|
|
59
|
+
try {
|
|
60
|
+
await copyIn.fail(reason);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// ignore — the original read error is what we want to surface.
|
|
64
|
+
}
|
|
65
|
+
throw aborted instanceof Error ? aborted : new Error(reason);
|
|
66
|
+
}
|
|
67
|
+
await copyIn.end();
|
|
68
|
+
// Keep the Connection type as a structural reference so prod consumers
|
|
69
|
+
// import this module even when only using `conn.startCopyIn` directly.
|
|
70
|
+
void conn;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Drain a CopyOutStream into a Node Writable, returning when the server
|
|
74
|
+
* has signalled CopyDone + ReadyForQuery. The Writable is NOT closed by this
|
|
75
|
+
* function — the caller owns its lifetime.
|
|
76
|
+
*/
|
|
77
|
+
export const drainCopyOut = async (copyOut, writable) => {
|
|
78
|
+
for await (const chunk of copyOut) {
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
writable.write(chunk, (err) => {
|
|
81
|
+
if (err !== null && err !== undefined)
|
|
82
|
+
reject(err);
|
|
83
|
+
else
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Coerce an arbitrary thrown value into a string suitable for `CopyFail`
|
|
91
|
+
* (which expects a human-readable reason). We accept Errors, strings, or
|
|
92
|
+
* anything else; the last branch avoids `[object Object]` by JSON-stringifying
|
|
93
|
+
* objects safely.
|
|
94
|
+
*/
|
|
95
|
+
const abortReason = (v) => {
|
|
96
|
+
if (v instanceof Error)
|
|
97
|
+
return v.message;
|
|
98
|
+
if (typeof v === 'string')
|
|
99
|
+
return v;
|
|
100
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
101
|
+
return String(v);
|
|
102
|
+
try {
|
|
103
|
+
return JSON.stringify(v) ?? 'unknown error';
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return 'unknown error';
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async multiplexer for NOTICE / NOTIFY (WP-02).
|
|
3
|
+
*
|
|
4
|
+
* `PgConnection` keeps a single NoticeMultiplexer that all incoming
|
|
5
|
+
* NoticeResponse and NotificationResponse messages flow through. Subscribers
|
|
6
|
+
* (e.g. the REPL, query runner, an external observer) register handlers and
|
|
7
|
+
* receive a disposer function — the same shape upstream pg uses.
|
|
8
|
+
*
|
|
9
|
+
* We don't use Node's `EventEmitter` because:
|
|
10
|
+
* - The Connection interface (frozen WP-00) returns disposers from
|
|
11
|
+
* `onNotice` / `onNotification`, not subscription objects.
|
|
12
|
+
* - We want exceptions in one handler to be isolated from the rest.
|
|
13
|
+
*/
|
|
14
|
+
export class NoticeMultiplexer {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.noticeHandlers = new Set();
|
|
17
|
+
this.notificationHandlers = new Set();
|
|
18
|
+
/** Last error thrown by a handler. Exposed for diagnostics / tests. */
|
|
19
|
+
this.lastHandlerError = undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Subscribe to NoticeResponse. Returns a disposer. */
|
|
22
|
+
onNotice(handler) {
|
|
23
|
+
this.noticeHandlers.add(handler);
|
|
24
|
+
return () => this.noticeHandlers.delete(handler);
|
|
25
|
+
}
|
|
26
|
+
/** Subscribe to NotificationResponse (LISTEN/NOTIFY). Returns a disposer. */
|
|
27
|
+
onNotification(handler) {
|
|
28
|
+
this.notificationHandlers.add(handler);
|
|
29
|
+
return () => this.notificationHandlers.delete(handler);
|
|
30
|
+
}
|
|
31
|
+
emit(notice) {
|
|
32
|
+
for (const h of this.noticeHandlers) {
|
|
33
|
+
// Isolate handler failures: one bad subscriber must not break the
|
|
34
|
+
// whole connection. We swallow + record via `lastHandlerError` so a
|
|
35
|
+
// test can observe it without us pulling in a logger.
|
|
36
|
+
try {
|
|
37
|
+
h(notice);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
this.lastHandlerError = err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
emitNotification(channel, payload, pid) {
|
|
45
|
+
for (const h of this.notificationHandlers) {
|
|
46
|
+
try {
|
|
47
|
+
h(channel, payload, pid);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
this.lastHandlerError = err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Drop every subscriber. Called by Connection.close(). */
|
|
55
|
+
clear() {
|
|
56
|
+
this.noticeHandlers.clear();
|
|
57
|
+
this.notificationHandlers.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PipelineSession — high-level wrapper around the extended-protocol pipeline
|
|
3
|
+
* driver in {@link PgConnection} (WP-21).
|
|
4
|
+
*
|
|
5
|
+
* In pipeline mode (libpq's `PQpipelineSync`), the client streams multiple
|
|
6
|
+
* extended-protocol commands (Parse/Bind/Describe/Execute/Close) without
|
|
7
|
+
* waiting for results. The server processes each in order and queues replies.
|
|
8
|
+
* `sync()` inserts a Sync into the stream — that's the error boundary: any
|
|
9
|
+
* failure before the next Sync causes the server to skip subsequent commands
|
|
10
|
+
* until that Sync. `end()` finalizes the session.
|
|
11
|
+
*
|
|
12
|
+
* Design:
|
|
13
|
+
*
|
|
14
|
+
* - Each method writes the wire frame *and* enqueues a matching op record
|
|
15
|
+
* on `PgConnection.extDriver.queue`. The connection's dispatch loop
|
|
16
|
+
* resolves ops in order as replies arrive.
|
|
17
|
+
* - `execute()` records its own ResultSet promise; callers can `await` each
|
|
18
|
+
* individually or use the implicit ordering via the resolution sequence.
|
|
19
|
+
* - `flush()` sends `Flush`, which forces the server to send any queued
|
|
20
|
+
* replies without committing a Sync barrier. `sync()` sends `Sync` (and
|
|
21
|
+
* resolves on the corresponding ReadyForQuery).
|
|
22
|
+
* - `end()` sends a final Sync, waits for ReadyForQuery, and returns the
|
|
23
|
+
* concatenated ResultSets in execute-order. After end() the connection
|
|
24
|
+
* drops back to `idle`.
|
|
25
|
+
*/
|
|
26
|
+
import { Buffer } from 'node:buffer';
|
|
27
|
+
import { Bind, Close, Describe, Execute, Flush, Parse, Sync, } from './protocol.js';
|
|
28
|
+
export class PipelineSession {
|
|
29
|
+
constructor(conn) {
|
|
30
|
+
this.conn = conn;
|
|
31
|
+
/**
|
|
32
|
+
* Per-Execute ResultSet promises, accumulated in the order
|
|
33
|
+
* `execute()` was called. Public-read so `\getresults` (in
|
|
34
|
+
* `cmd_pipeline.ts`) can surface each pending result the moment a
|
|
35
|
+
* Flush / Sync makes the server emit replies — mirroring upstream
|
|
36
|
+
* psql's `exec_command_getresults`, which calls `PQgetResult()` and
|
|
37
|
+
* prints each one inline.
|
|
38
|
+
*
|
|
39
|
+
* Never spliced internally: `\endpipeline` / `\getresults` track
|
|
40
|
+
* their own "drained count" so already-printed results are skipped.
|
|
41
|
+
*/
|
|
42
|
+
this.results = [];
|
|
43
|
+
/**
|
|
44
|
+
* Per-USER-COMMAND result slots, one entry per `\sendpipeline` /
|
|
45
|
+
* `\parse NAME` / `\close_prepared NAME` / `\syncpipeline`. Mirrors
|
|
46
|
+
* libpq's per-PQsendQueryParams / PQsendPrepare / PQsendClosePrepared
|
|
47
|
+
* result queue: a single `PQgetResult` call produces one entry from
|
|
48
|
+
* this list (NOT one per wire op — Parse/Bind/Execute together count
|
|
49
|
+
* as ONE entry under `PQsendQueryParams`). The cmd layer drains this
|
|
50
|
+
* via `\getresults` / `\endpipeline` instead of walking `results`
|
|
51
|
+
* directly, so Parse-only and Close-only commands occupy a slot even
|
|
52
|
+
* though they don't push onto `results`.
|
|
53
|
+
*
|
|
54
|
+
* For successful Parse-only / Close-only commands, the slot resolves
|
|
55
|
+
* with a placeholder ResultSet (zero fields, empty `command`); for
|
|
56
|
+
* Execute-bearing commands it resolves with the real ResultSet. On
|
|
57
|
+
* error, the slot rejects with the same `ConnectError` /
|
|
58
|
+
* `pipelineAborted` marker the wire layer cascade-rejects with.
|
|
59
|
+
* SyncMarker slots are `Promise.resolve(syncMarker)` so the drain
|
|
60
|
+
* loop walks past them silently.
|
|
61
|
+
*/
|
|
62
|
+
this.cmdSlots = [];
|
|
63
|
+
// Promises for ops whose server-side completion we still need to
|
|
64
|
+
// observe (Parse, Bind, Describe, Close). In pipeline mode the
|
|
65
|
+
// server doesn't reply until the next Sync, so awaiting them in
|
|
66
|
+
// these methods would deadlock — we keep them and let `sync()` /
|
|
67
|
+
// `end()` drain them.
|
|
68
|
+
this.pending = [];
|
|
69
|
+
/**
|
|
70
|
+
* Per-USER-command "wire ops staged but not yet bound to a cmdSlot".
|
|
71
|
+
* `parse()` / `bind()` push their wire op promise here; the next
|
|
72
|
+
* call that closes the user-level command (`execute()` for
|
|
73
|
+
* `\sendpipeline` / `;`-queries, `parseSlot()` for `\parse NAME`,
|
|
74
|
+
* `closeSlot()` for `\close_prepared NAME`) consumes the staged
|
|
75
|
+
* ops and combines them via `Promise.all` with the final wire op
|
|
76
|
+
* — that way the slot rejects with the FIRST wire-op rejection in
|
|
77
|
+
* the group (which carries the real `ConnectError` for the actual
|
|
78
|
+
* server-side failure) rather than the cascaded `pipelineAborted`
|
|
79
|
+
* marker on the trailing Execute / Close.
|
|
80
|
+
*/
|
|
81
|
+
this.currentGroup = [];
|
|
82
|
+
/**
|
|
83
|
+
* Sticky pipeline error captured by the final Sync inside `end()`.
|
|
84
|
+
* Non-FATAL ERROR-class diagnostics (e.g. "could not determine data
|
|
85
|
+
* type of parameter $1", "bind message supplies N parameters")
|
|
86
|
+
* arrive on the wire as `ErrorResponse` and reject the Sync op, but
|
|
87
|
+
* the pipeline session must still surface the queued ResultSets so
|
|
88
|
+
* `\endpipeline` can print them. The cmd layer reads this after
|
|
89
|
+
* `end()` resolves and renders the ERROR line via `writeQueryError`.
|
|
90
|
+
*
|
|
91
|
+
* `null` until end() finishes; thereafter holds the first non-FATAL
|
|
92
|
+
* error seen, or `null` if the pipeline drained cleanly.
|
|
93
|
+
*/
|
|
94
|
+
this._lastError = null;
|
|
95
|
+
this._errorConsumedExternally = false;
|
|
96
|
+
/**
|
|
97
|
+
* Number of entries from `results` that the cmd layer (`\getresults`)
|
|
98
|
+
* has already inspected. `end()`'s post-Sync per-op scan starts at
|
|
99
|
+
* this offset so a rejection that was already surfaced inline by
|
|
100
|
+
* `\getresults` doesn't get re-stashed on `_lastError` and re-rendered
|
|
101
|
+
* by `\endpipeline`.
|
|
102
|
+
*/
|
|
103
|
+
this._externalDrained = 0;
|
|
104
|
+
this.conn._extPipelineActive = true;
|
|
105
|
+
this.conn.startExtendedBatch();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Attach a no-op rejection handler so an unhandled rejection on `p`
|
|
109
|
+
* doesn't bubble up. The original promise is also retained in
|
|
110
|
+
* `this.results` / `this.pending` so callers (end(), getresults) can
|
|
111
|
+
* still observe the outcome — we only swallow the unhandled-rejection
|
|
112
|
+
* warning Node would otherwise emit at process scope.
|
|
113
|
+
*/
|
|
114
|
+
static silenceUnhandled(p) {
|
|
115
|
+
p.catch(() => undefined);
|
|
116
|
+
return p;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Push a synthetic ResultSet slot onto `cmdSlots`. The slot's
|
|
120
|
+
* effective rejection is taken from the FIRST wire-op rejection in
|
|
121
|
+
* the current user-command group ({@link currentGroup}) followed by
|
|
122
|
+
* the final ResultSet promise — mirroring libpq's per-PGresult
|
|
123
|
+
* shape, where the real `ConnectError` lands on the failing wire op
|
|
124
|
+
* (often a Bind / Parse on `pending`, NOT the trailing Execute on
|
|
125
|
+
* `results`). Without this combining, the slot would inherit the
|
|
126
|
+
* cascaded `pipelineAborted` marker from the Execute and the real
|
|
127
|
+
* diagnostic would never reach the cmd layer.
|
|
128
|
+
*
|
|
129
|
+
* The group is reset after consumption so the next user command
|
|
130
|
+
* starts fresh.
|
|
131
|
+
*/
|
|
132
|
+
pushCmdSlot(p) {
|
|
133
|
+
const group = this.currentGroup.splice(0);
|
|
134
|
+
if (group.length === 0) {
|
|
135
|
+
this.cmdSlots.push(PipelineSession.silenceUnhandled(p));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// `Promise.all` rejects with the FIRST rejection (in iteration
|
|
139
|
+
// order), so put the group BEFORE the final result: a bind error
|
|
140
|
+
// on `bind` will surface before the cascaded execute marker.
|
|
141
|
+
const combined = Promise.all([...group, p]).then((arr) => arr[arr.length - 1]);
|
|
142
|
+
this.cmdSlots.push(PipelineSession.silenceUnhandled(combined));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Map a wire-op `Promise<void>` to a `Promise<ResultSet>` that
|
|
146
|
+
* resolves with a silent zero-field placeholder when the op
|
|
147
|
+
* completes, or rejects with the same reason if it fails. Used by
|
|
148
|
+
* `parseSlot` / `closeSlot` so a successful Parse / Close walks the
|
|
149
|
+
* `\getresults` drain loop without emitting any output (matching
|
|
150
|
+
* upstream `PQgetResult` for PGRES_COMMAND_OK on Parse / Close).
|
|
151
|
+
*/
|
|
152
|
+
static voidToPlaceholderResult(p) {
|
|
153
|
+
return p.then(() => ({
|
|
154
|
+
command: '',
|
|
155
|
+
rowCount: null,
|
|
156
|
+
oid: null,
|
|
157
|
+
fields: [],
|
|
158
|
+
rows: [],
|
|
159
|
+
notices: [],
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
parse(name, sql, paramTypes) {
|
|
163
|
+
// parseRaw already retains the wire promise in `pending` (and
|
|
164
|
+
// silences unhandled rejection); the void return here matches the
|
|
165
|
+
// public Pipeline interface.
|
|
166
|
+
void this.parseRaw(name, sql, paramTypes);
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Enqueue a Parse op AND register it as a USER-level command slot
|
|
171
|
+
* (one entry on `cmdSlots`). Mirrors {@link parse} — RESOLVES
|
|
172
|
+
* IMMEDIATELY; the underlying wire-op promise's outcome surfaces
|
|
173
|
+
* via `cmdSlots` (drained by `\getresults` / `\endpipeline`).
|
|
174
|
+
* Returning a slot-bound promise would deadlock callers that await
|
|
175
|
+
* it before Sync.
|
|
176
|
+
*
|
|
177
|
+
* Used by `\parse NAME` (standalone Parse). Do NOT use from inside
|
|
178
|
+
* `\sendpipeline` — that path issues an internal `parse('')` for the
|
|
179
|
+
* SQL buffer; the Execute downstream is the user-visible command.
|
|
180
|
+
*/
|
|
181
|
+
async parseSlot(name, sql, paramTypes) {
|
|
182
|
+
const p = this.parseRaw(name, sql, paramTypes);
|
|
183
|
+
const slot = PipelineSession.voidToPlaceholderResult(p);
|
|
184
|
+
this.pushCmdSlot(slot);
|
|
185
|
+
// Resolve immediately, like `parse()`. The slot's outcome flows
|
|
186
|
+
// through `cmdSlots`.
|
|
187
|
+
await Promise.resolve();
|
|
188
|
+
}
|
|
189
|
+
parseRaw(name, sql, paramTypes) {
|
|
190
|
+
this.conn.startExtendedBatch();
|
|
191
|
+
const p = this.conn.enqueueParse();
|
|
192
|
+
this.conn.writeRaw(Parse(name, sql, paramTypes ?? []));
|
|
193
|
+
// Don't await: server response is held until Sync. Capture into
|
|
194
|
+
// pending so sync()/end() can observe completion. Attach a no-op
|
|
195
|
+
// catch so a server-side reject (ParseError) doesn't fire an
|
|
196
|
+
// UnhandledPromiseRejection at process scope before end()/sync()
|
|
197
|
+
// get a chance to allSettle the list.
|
|
198
|
+
const silenced = PipelineSession.silenceUnhandled(p);
|
|
199
|
+
this.pending.push(silenced);
|
|
200
|
+
// Stage on the current command group so a downstream cmdSlot push
|
|
201
|
+
// can fold this Parse's rejection into the slot (e.g. a Parse-error
|
|
202
|
+
// in `\sendpipeline`'s internal `parse('', sql)` must surface as
|
|
203
|
+
// the slot's rejection, not the cascaded marker on Execute).
|
|
204
|
+
this.currentGroup.push(silenced);
|
|
205
|
+
return p;
|
|
206
|
+
}
|
|
207
|
+
bind(name, values) {
|
|
208
|
+
this.conn.startExtendedBatch();
|
|
209
|
+
const p = this.conn.enqueueBind();
|
|
210
|
+
const encoded = values.map(toBindValue);
|
|
211
|
+
this.conn.writeRaw(Bind('', name, [], encoded, [0]));
|
|
212
|
+
const silenced = PipelineSession.silenceUnhandled(p);
|
|
213
|
+
this.pending.push(silenced);
|
|
214
|
+
// Stage on the current command group so the next slot-pushing op
|
|
215
|
+
// (typically `execute()` for `\sendpipeline`) combines this
|
|
216
|
+
// Bind's rejection into the slot — see {@link pushCmdSlot}.
|
|
217
|
+
this.currentGroup.push(silenced);
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
}
|
|
220
|
+
execute(name, maxRows) {
|
|
221
|
+
this.conn.startExtendedBatch();
|
|
222
|
+
// Mirror libpq's PQsendQueryGuts: Describe('P', '') goes between Bind
|
|
223
|
+
// and Execute so RowDescription arrives and the resulting ResultSet
|
|
224
|
+
// carries field metadata (otherwise the printer renders rows with no
|
|
225
|
+
// columns). The Describe op pipes its resolved fields directly onto
|
|
226
|
+
// the upcoming Execute op (see enqueueDescribePortalIntoNextExecute).
|
|
227
|
+
const dp = this.conn.enqueueDescribePortalIntoNextExecute();
|
|
228
|
+
const silencedDp = PipelineSession.silenceUnhandled(dp);
|
|
229
|
+
this.pending.push(silencedDp);
|
|
230
|
+
// Stage DescribePortal on the current group too — its rejection
|
|
231
|
+
// would otherwise be invisible to the slot.
|
|
232
|
+
this.currentGroup.push(silencedDp);
|
|
233
|
+
this.conn.writeRaw(Describe('P', name));
|
|
234
|
+
const ep = this.conn.enqueueExecute();
|
|
235
|
+
this.results.push(PipelineSession.silenceUnhandled(ep));
|
|
236
|
+
// Each Execute is one libpq-level user command (the Parse + Bind
|
|
237
|
+
// that precede it are accounted to the same PQsendQueryParams /
|
|
238
|
+
// `\sendpipeline` slot). Mirror that on `cmdSlots` so `\getresults`
|
|
239
|
+
// walks one entry per user command; the slot folds in any group
|
|
240
|
+
// ops staged by parse() / bind() / DescP above.
|
|
241
|
+
this.pushCmdSlot(ep);
|
|
242
|
+
this.conn.writeRaw(Execute(name, maxRows ?? 0));
|
|
243
|
+
// Don't await: the ResultSet promise resolves after Sync. It's
|
|
244
|
+
// already in `results` so end() / sync() will surface it.
|
|
245
|
+
return Promise.resolve();
|
|
246
|
+
}
|
|
247
|
+
describe(name) {
|
|
248
|
+
this.conn.startExtendedBatch();
|
|
249
|
+
// psql's `\describe` in pipeline context is portal-targeted.
|
|
250
|
+
const p = this.conn.enqueueDescribePortal();
|
|
251
|
+
this.conn.writeRaw(Describe('P', name));
|
|
252
|
+
this.pending.push(PipelineSession.silenceUnhandled(p));
|
|
253
|
+
return Promise.resolve();
|
|
254
|
+
}
|
|
255
|
+
close(name) {
|
|
256
|
+
void this.closeRaw(name);
|
|
257
|
+
return Promise.resolve();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Enqueue a Close op AND register it as a user-level command slot.
|
|
261
|
+
* Resolves immediately (see {@link parseSlot} for rationale); the
|
|
262
|
+
* slot's outcome surfaces via `cmdSlots`.
|
|
263
|
+
*/
|
|
264
|
+
async closeSlot(name) {
|
|
265
|
+
const p = this.closeRaw(name);
|
|
266
|
+
const slot = PipelineSession.voidToPlaceholderResult(p);
|
|
267
|
+
this.pushCmdSlot(slot);
|
|
268
|
+
await Promise.resolve();
|
|
269
|
+
}
|
|
270
|
+
closeRaw(name) {
|
|
271
|
+
this.conn.startExtendedBatch();
|
|
272
|
+
const p = this.conn.enqueueClose();
|
|
273
|
+
this.conn.writeRaw(Close('S', name));
|
|
274
|
+
const silenced = PipelineSession.silenceUnhandled(p);
|
|
275
|
+
this.pending.push(silenced);
|
|
276
|
+
this.currentGroup.push(silenced);
|
|
277
|
+
return p;
|
|
278
|
+
}
|
|
279
|
+
async flush() {
|
|
280
|
+
this.conn.writeRaw(Flush());
|
|
281
|
+
// Flush has no reply; resolve immediately. The server will start sending
|
|
282
|
+
// any queued replies it had buffered, which our drive loop consumes.
|
|
283
|
+
await Promise.resolve();
|
|
284
|
+
}
|
|
285
|
+
async sync() {
|
|
286
|
+
this.conn.startExtendedBatch();
|
|
287
|
+
const p = this.conn.enqueueSync();
|
|
288
|
+
this.conn.writeRaw(Sync());
|
|
289
|
+
// Each `\syncpipeline` registers an additional "result-queue
|
|
290
|
+
// entry" the caller must drain via `\getresults` — vanilla psql
|
|
291
|
+
// 18.4 surfaces this as the PGRES_PIPELINE_SYNC marker libpq
|
|
292
|
+
// pushes onto its result queue. The conformance test at SQL
|
|
293
|
+
// line 184 (`\syncpipeline count as one command to fetch for
|
|
294
|
+
// \getresults`) expects exactly this drain accounting. We
|
|
295
|
+
// simulate it by pushing an empty (zero-fields) ResultSet
|
|
296
|
+
// promise into the per-Execute queue — `\getresults` skips
|
|
297
|
+
// printing it (the empty-fields guard) but advances `drainedCount`
|
|
298
|
+
// by one, matching upstream's libpq queue layout.
|
|
299
|
+
const syncMarker = {
|
|
300
|
+
command: '',
|
|
301
|
+
rowCount: null,
|
|
302
|
+
oid: null,
|
|
303
|
+
fields: [],
|
|
304
|
+
rows: [],
|
|
305
|
+
notices: [],
|
|
306
|
+
};
|
|
307
|
+
this.results.push(Promise.resolve(syncMarker));
|
|
308
|
+
// Mirror libpq's per-command result queue: each `\syncpipeline`
|
|
309
|
+
// adds exactly one PGRES_PIPELINE_SYNC entry that `\getresults`
|
|
310
|
+
// walks past silently (counted, but not printed). See {@link cmdSlots}.
|
|
311
|
+
// SyncMarker is its own slot, so clear any half-built command
|
|
312
|
+
// group (e.g. a stray `bind()` that wasn't followed by `execute()`)
|
|
313
|
+
// before pushing — otherwise the SyncMarker slot would inherit
|
|
314
|
+
// stale group rejections. Spliced promises were already silenced
|
|
315
|
+
// and tracked on `pending`, so discarding the splice return is
|
|
316
|
+
// safe (no unhandled-rejection escape).
|
|
317
|
+
void this.currentGroup.splice(0);
|
|
318
|
+
this.cmdSlots.push(Promise.resolve(syncMarker));
|
|
319
|
+
// Don't propagate a non-FATAL ErrorResponse here: upstream
|
|
320
|
+
// `\syncpipeline` is silent on stderr — the server-side error
|
|
321
|
+
// surfaces at `\endpipeline` time (see expected/psql_pipeline.out
|
|
322
|
+
// line 433 where the ParseError is printed AFTER the trailing
|
|
323
|
+
// `\endpipeline` echo, not inline with `\syncpipeline`). Stash
|
|
324
|
+
// it on `_lastError` instead so the cmd layer can render it
|
|
325
|
+
// when the pipeline drains.
|
|
326
|
+
await p.catch((err) => {
|
|
327
|
+
if (this._lastError === null)
|
|
328
|
+
this._lastError = err;
|
|
329
|
+
});
|
|
330
|
+
// Settle any per-op promises that arrived before this Sync — Parse,
|
|
331
|
+
// Bind, Describe, Close, and Execute. We don't surface their results
|
|
332
|
+
// here (callers consume `results` via end()), but we do need to drain
|
|
333
|
+
// any rejections so Node doesn't see them as unhandled. The first
|
|
334
|
+
// rejection becomes the sticky `_lastError` if Sync itself was clean.
|
|
335
|
+
const settled = await Promise.allSettled(this.pending.splice(0));
|
|
336
|
+
for (const r of settled) {
|
|
337
|
+
if (r.status === 'rejected' && this._lastError === null) {
|
|
338
|
+
this._lastError = r.reason;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
get lastError() {
|
|
343
|
+
return this._lastError;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Drop any sticky error stashed via `_lastError = err` AND latch a
|
|
347
|
+
* "do not re-stash" sentinel so the next `end()` won't overwrite the
|
|
348
|
+
* cleared state from its final-Sync `fatal` capture or its per-op
|
|
349
|
+
* rejection scan. Used by `\getresults` when it surfaced a rejection
|
|
350
|
+
* inline so the follow-on `\endpipeline` doesn't re-emit the same
|
|
351
|
+
* diagnostic.
|
|
352
|
+
*
|
|
353
|
+
* The sentinel only applies to the IMMEDIATELY-FOLLOWING `end()`
|
|
354
|
+
* call: if a fresh error arrives on the wire AFTER clearLastError
|
|
355
|
+
* (e.g. a `\sendpipeline` queued after the `\getresults` that
|
|
356
|
+
* itself triggers a server-side ERROR), `_lastError` is re-armed
|
|
357
|
+
* normally because the per-op rejection scan only skips entries
|
|
358
|
+
* the cmd layer has already inspected (see `_externalDrained`).
|
|
359
|
+
*/
|
|
360
|
+
clearLastError() {
|
|
361
|
+
this._lastError = null;
|
|
362
|
+
this._errorConsumedExternally = true;
|
|
363
|
+
}
|
|
364
|
+
markDrained(count) {
|
|
365
|
+
if (count > this._externalDrained)
|
|
366
|
+
this._externalDrained = count;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Peek at the FIRST real (non-`pipelineAborted`) rejection anywhere
|
|
370
|
+
* in `pending` ∪ `results`, INSPECTING ONLY promises that are already
|
|
371
|
+
* settled. Used by `\getresults` / `\endpipeline` to find the
|
|
372
|
+
* originating diagnostic when the wire layer's cascade-reject landed
|
|
373
|
+
* the real `ConnectError` on a `pending` Parse/Bind/Close (not visible
|
|
374
|
+
* to `results`-only walks) and stamped `pipelineAborted` onto the
|
|
375
|
+
* visible Execute slots.
|
|
376
|
+
*
|
|
377
|
+
* Each candidate is raced against a microtask-resolved sentinel so a
|
|
378
|
+
* still-pending promise (e.g. the next Execute waiting for its
|
|
379
|
+
* Sync round-trip) doesn't block the peek. Returns `null` when
|
|
380
|
+
* nothing has rejected yet — or when the only rejections so far are
|
|
381
|
+
* the synthetic `pipelineAborted` marker.
|
|
382
|
+
*/
|
|
383
|
+
async peekRealError() {
|
|
384
|
+
const allP = [...this.pending, ...this.results];
|
|
385
|
+
const sentinel = Symbol('pending');
|
|
386
|
+
// Race each candidate against a `setImmediate`-deferred sentinel so
|
|
387
|
+
// any promise that was already settled (synchronously, e.g. by the
|
|
388
|
+
// wire layer's cascade-reject inside its ErrorResponse handler) has
|
|
389
|
+
// a full microtask drain to fulfil its `.then(...)` chain before
|
|
390
|
+
// the sentinel resolves. A bare `Promise.resolve(sentinel)` would
|
|
391
|
+
// race in the same microtask tier and the sentinel sometimes wins
|
|
392
|
+
// even when the candidate has a settled value — that's the
|
|
393
|
+
// ordering quirk this `setImmediate` step pins down.
|
|
394
|
+
const deferred = () => new Promise((resolve) => setImmediate(() => {
|
|
395
|
+
resolve(sentinel);
|
|
396
|
+
}));
|
|
397
|
+
for (const p of allP) {
|
|
398
|
+
const r = (await Promise.race([
|
|
399
|
+
p.then((v) => ({
|
|
400
|
+
status: 'fulfilled',
|
|
401
|
+
value: v,
|
|
402
|
+
}), (e) => ({
|
|
403
|
+
status: 'rejected',
|
|
404
|
+
reason: e,
|
|
405
|
+
})),
|
|
406
|
+
deferred(),
|
|
407
|
+
]));
|
|
408
|
+
if (r === sentinel)
|
|
409
|
+
continue;
|
|
410
|
+
if (r.status !== 'rejected')
|
|
411
|
+
continue;
|
|
412
|
+
const reason = r.reason;
|
|
413
|
+
const isAborted = typeof reason === 'object' &&
|
|
414
|
+
reason !== null &&
|
|
415
|
+
reason.pipelineAborted === true;
|
|
416
|
+
if (!isAborted)
|
|
417
|
+
return reason;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
async end() {
|
|
422
|
+
// Send a terminating Sync so the connection settles back to idle.
|
|
423
|
+
this.conn.startExtendedBatch();
|
|
424
|
+
const finalSync = this.conn.enqueueSync();
|
|
425
|
+
this.conn.writeRaw(Sync());
|
|
426
|
+
// Wait for the final Sync's RfQ, but tolerate a sticky pipeline
|
|
427
|
+
// error: enqueueSync rejects when the pipeline ended in error
|
|
428
|
+
// state. `\endpipeline` still needs to harvest the queued
|
|
429
|
+
// ResultSets, so we capture rather than propagate immediately.
|
|
430
|
+
let fatal = null;
|
|
431
|
+
await finalSync.catch((err) => {
|
|
432
|
+
fatal = err;
|
|
433
|
+
});
|
|
434
|
+
this.conn._extPipelineActive = false;
|
|
435
|
+
// Drain pending ops too — Parse/Bind/Describe/Close that rejected
|
|
436
|
+
// (e.g. bind-param-count mismatch) would otherwise surface as
|
|
437
|
+
// process-level UnhandledPromiseRejection. We only need the
|
|
438
|
+
// settlement; the results aren't surfaced here.
|
|
439
|
+
await Promise.allSettled(this.pending.splice(0));
|
|
440
|
+
const settled = await Promise.allSettled(this.results);
|
|
441
|
+
const out = [];
|
|
442
|
+
for (const r of settled) {
|
|
443
|
+
if (r.status === 'fulfilled')
|
|
444
|
+
out.push(r.value);
|
|
445
|
+
}
|
|
446
|
+
// FATAL-class pipeline aborts (e.g. "COPY in a pipeline is not
|
|
447
|
+
// supported, aborting connection") must surface to the caller so
|
|
448
|
+
// `\endpipeline` can render the diagnostic on stderr. Plain
|
|
449
|
+
// per-op errors stay swallowed so a partial pipeline still
|
|
450
|
+
// returns its successful results — but we stash them on
|
|
451
|
+
// `_lastError` so the cmd layer can print them inline.
|
|
452
|
+
if (fatal !== null) {
|
|
453
|
+
const sev = fatal.severity;
|
|
454
|
+
if (sev === 'FATAL') {
|
|
455
|
+
if (fatal instanceof Error)
|
|
456
|
+
throw fatal;
|
|
457
|
+
const msg = fatal.message ?? 'pipeline aborted';
|
|
458
|
+
throw Object.assign(new Error(msg), fatal);
|
|
459
|
+
}
|
|
460
|
+
// Non-FATAL (typically ERROR) — keep for the cmd layer, unless
|
|
461
|
+
// `\getresults` already consumed the diagnostic (see
|
|
462
|
+
// `clearLastError()` / `_errorConsumedExternally`). In that case
|
|
463
|
+
// the same rejection is mirrored on the final Sync — re-stashing
|
|
464
|
+
// it here would cause `\endpipeline` to double-print.
|
|
465
|
+
if (!this._errorConsumedExternally) {
|
|
466
|
+
this._lastError = fatal;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else if (this._lastError === null) {
|
|
470
|
+
// Also scan the settled per-op promises: a ParseError /
|
|
471
|
+
// BindError that arrived BEFORE the final Sync rejects its own
|
|
472
|
+
// op promise but the Sync may still resolve cleanly (the
|
|
473
|
+
// server marks subsequent ops as "Pipeline aborted, command
|
|
474
|
+
// did not run" instead of erroring). The conformance corpus
|
|
475
|
+
// expects the first ErrorResponse to surface at `\endpipeline`
|
|
476
|
+
// time, so we hunt for the first rejected per-op promise — but
|
|
477
|
+
// skip entries that `\getresults` already inspected and surfaced
|
|
478
|
+
// inline (tracked via `markDrained`). Without this, the same
|
|
479
|
+
// rejection would re-stash on `_lastError` and `\endpipeline`
|
|
480
|
+
// would double-print the diagnostic.
|
|
481
|
+
//
|
|
482
|
+
// Don't OVERWRITE an existing `_lastError`: a prior `sync()`
|
|
483
|
+
// (via `\syncpipeline`) may have already stashed the real
|
|
484
|
+
// ConnectError for an earlier batch, and the second batch's
|
|
485
|
+
// results carry only `pipelineAborted` markers which would
|
|
486
|
+
// shadow the real diagnostic if blindly assigned here. The
|
|
487
|
+
// outer `if (this._lastError === null)` guard makes this a
|
|
488
|
+
// first-wins capture.
|
|
489
|
+
for (let i = this._externalDrained; i < settled.length; i++) {
|
|
490
|
+
const r = settled[i];
|
|
491
|
+
if (r.status === 'rejected') {
|
|
492
|
+
this._lastError = r.reason;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function toBindValue(v) {
|
|
501
|
+
if (v === null || v === undefined)
|
|
502
|
+
return null;
|
|
503
|
+
if (Buffer.isBuffer(v))
|
|
504
|
+
return v;
|
|
505
|
+
if (typeof v === 'string')
|
|
506
|
+
return v;
|
|
507
|
+
if (typeof v === 'boolean')
|
|
508
|
+
return v ? 't' : 'f';
|
|
509
|
+
if (typeof v === 'number' || typeof v === 'bigint')
|
|
510
|
+
return v.toString();
|
|
511
|
+
// Objects, arrays, etc: JSON-stringify deterministically. Falls back to ''
|
|
512
|
+
// if the value is not representable.
|
|
513
|
+
try {
|
|
514
|
+
return JSON.stringify(v);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
return '';
|
|
518
|
+
}
|
|
519
|
+
}
|