neonctl 2.22.2 → 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.
Files changed (113) hide show
  1. package/README.md +84 -0
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/connection_string.js +9 -1
  5. package/commands/functions.js +277 -0
  6. package/commands/index.js +4 -0
  7. package/commands/neon_auth.js +1013 -0
  8. package/commands/projects.js +9 -1
  9. package/commands/psql.js +6 -1
  10. package/functions_api.js +44 -0
  11. package/package.json +15 -5
  12. package/psql/cli.js +51 -0
  13. package/psql/command/cmd_cond.js +437 -0
  14. package/psql/command/cmd_connect.js +815 -0
  15. package/psql/command/cmd_copy.js +1025 -0
  16. package/psql/command/cmd_describe.js +1810 -0
  17. package/psql/command/cmd_format.js +909 -0
  18. package/psql/command/cmd_io.js +2187 -0
  19. package/psql/command/cmd_lo.js +385 -0
  20. package/psql/command/cmd_meta.js +970 -0
  21. package/psql/command/cmd_misc.js +187 -0
  22. package/psql/command/cmd_pipeline.js +1141 -0
  23. package/psql/command/cmd_restrict.js +171 -0
  24. package/psql/command/cmd_show.js +751 -0
  25. package/psql/command/dispatch.js +343 -0
  26. package/psql/command/inputQueue.js +42 -0
  27. package/psql/command/shared.js +71 -0
  28. package/psql/complete/filenames.js +139 -0
  29. package/psql/complete/index.js +104 -0
  30. package/psql/complete/matcher.js +314 -0
  31. package/psql/complete/psqlVars.js +247 -0
  32. package/psql/complete/queries.js +491 -0
  33. package/psql/complete/rules.js +2387 -0
  34. package/psql/core/common.js +1250 -0
  35. package/psql/core/help.js +576 -0
  36. package/psql/core/mainloop.js +1353 -0
  37. package/psql/core/prompt.js +437 -0
  38. package/psql/core/settings.js +684 -0
  39. package/psql/core/sqlHelp.js +1066 -0
  40. package/psql/core/startup.js +840 -0
  41. package/psql/core/syncVars.js +116 -0
  42. package/psql/core/variables.js +287 -0
  43. package/psql/describe/formatters.js +1277 -0
  44. package/psql/describe/processNamePattern.js +270 -0
  45. package/psql/describe/queries.js +2373 -0
  46. package/psql/describe/versionGate.js +43 -0
  47. package/psql/index.js +2005 -0
  48. package/psql/io/history.js +299 -0
  49. package/psql/io/input.js +120 -0
  50. package/psql/io/lineEditor/buffer.js +323 -0
  51. package/psql/io/lineEditor/complete.js +227 -0
  52. package/psql/io/lineEditor/filename.js +159 -0
  53. package/psql/io/lineEditor/index.js +891 -0
  54. package/psql/io/lineEditor/keymap.js +738 -0
  55. package/psql/io/lineEditor/vt100.js +363 -0
  56. package/psql/io/pgpass.js +202 -0
  57. package/psql/io/pgservice.js +194 -0
  58. package/psql/io/psqlrc.js +422 -0
  59. package/psql/print/aligned.js +1756 -0
  60. package/psql/print/asciidoc.js +248 -0
  61. package/psql/print/crosstab.js +460 -0
  62. package/psql/print/csv.js +92 -0
  63. package/psql/print/html.js +258 -0
  64. package/psql/print/json.js +96 -0
  65. package/psql/print/latex.js +396 -0
  66. package/psql/print/pager.js +265 -0
  67. package/psql/print/troff.js +258 -0
  68. package/psql/print/unaligned.js +118 -0
  69. package/psql/print/units.js +135 -0
  70. package/psql/scanner/slash.js +513 -0
  71. package/psql/scanner/sql.js +910 -0
  72. package/psql/scanner/stringutils.js +390 -0
  73. package/psql/types/backslash.js +1 -0
  74. package/psql/types/connection.js +1 -0
  75. package/psql/types/index.js +7 -0
  76. package/psql/types/printer.js +1 -0
  77. package/psql/types/repl.js +1 -0
  78. package/psql/types/scanner.js +24 -0
  79. package/psql/types/settings.js +1 -0
  80. package/psql/types/variables.js +1 -0
  81. package/psql/wire/connection.js +2844 -0
  82. package/psql/wire/copy.js +108 -0
  83. package/psql/wire/notify.js +59 -0
  84. package/psql/wire/pipeline.js +519 -0
  85. package/psql/wire/protocol.js +466 -0
  86. package/psql/wire/sasl.js +296 -0
  87. package/psql/wire/tls.js +596 -0
  88. package/test_utils/fixtures.js +1 -0
  89. package/utils/esbuild.js +147 -0
  90. package/utils/psql.js +107 -11
  91. package/utils/zip.js +4 -0
  92. package/writer.js +1 -1
  93. package/commands/auth.test.js +0 -211
  94. package/commands/branches.test.js +0 -460
  95. package/commands/checkout.test.js +0 -170
  96. package/commands/connection_string.test.js +0 -196
  97. package/commands/data_api.test.js +0 -169
  98. package/commands/databases.test.js +0 -39
  99. package/commands/help.test.js +0 -9
  100. package/commands/init.test.js +0 -56
  101. package/commands/ip_allow.test.js +0 -59
  102. package/commands/link.test.js +0 -381
  103. package/commands/operations.test.js +0 -7
  104. package/commands/orgs.test.js +0 -7
  105. package/commands/projects.test.js +0 -144
  106. package/commands/psql.test.js +0 -49
  107. package/commands/roles.test.js +0 -37
  108. package/commands/set_context.test.js +0 -159
  109. package/commands/vpc_endpoints.test.js +0 -69
  110. package/context.test.js +0 -119
  111. package/env.test.js +0 -55
  112. package/utils/formats.test.js +0 -32
  113. 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
+ }