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,1141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* psql pipeline / extended-query backslash commands (WP-21).
|
|
3
|
+
*
|
|
4
|
+
* Implements the subset of upstream psql's "pipeline mode" backslash commands
|
|
5
|
+
* that drive the extended protocol directly. These are typically used together
|
|
6
|
+
* with the existing `\g` flow: a `\bind` (or `\parse`) command stashes
|
|
7
|
+
* parameter / statement state on the {@link BackslashContext}'s settings via
|
|
8
|
+
* a Symbol-keyed slot; when the mainloop then sees a `;` (or a `\g`), it can
|
|
9
|
+
* notice the stashed state and route the query through
|
|
10
|
+
* `Connection.query(sql, params)` instead of `execSimple`.
|
|
11
|
+
*
|
|
12
|
+
* Because the mainloop integration is owned by other WPs (and was deliberately
|
|
13
|
+
* left untouched here), this module exposes both the command specs AND a
|
|
14
|
+
* small helper, {@link getPipelineState}, that the mainloop will use to
|
|
15
|
+
* consult the stashed state. The commands operate on the buffered query the
|
|
16
|
+
* same way `\g` does — they execute or queue the buffer and reset it.
|
|
17
|
+
*
|
|
18
|
+
* Commands shipped:
|
|
19
|
+
*
|
|
20
|
+
* \bind [VALUE ...] stash params for next ; / \g
|
|
21
|
+
* \bind_named NAME [V ...] stash params + named prepared statement
|
|
22
|
+
* \parse NAME prepare current query buffer as NAME
|
|
23
|
+
* \close_prepared NAME Close('S', NAME)
|
|
24
|
+
* \startpipeline begin a pipeline session (settings.sendMode)
|
|
25
|
+
* \endpipeline end the pipeline session, drain results
|
|
26
|
+
* \syncpipeline send Sync mid-pipeline
|
|
27
|
+
* \sendpipeline submit the current buffered query w/o waiting
|
|
28
|
+
* \flushrequest send Flush
|
|
29
|
+
* \flush alias for \flushrequest
|
|
30
|
+
* \getresults [N] drain pending pipeline results
|
|
31
|
+
* \gdesc describe-without-execute the buffered query
|
|
32
|
+
*
|
|
33
|
+
* The set is registered in bulk via {@link registerPipelineCommands}.
|
|
34
|
+
*/
|
|
35
|
+
import { writeErr } from './shared.js';
|
|
36
|
+
import { alignedPrinter } from '../print/aligned.js';
|
|
37
|
+
import { clearPipelineGateErrors } from './cmd_io.js';
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Settings stash. We can't add new fields to PsqlSettings (frozen WP-00) so
|
|
40
|
+
// we attach Symbol-keyed state.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const BIND_STATE_KEY = Symbol.for('neonctl.psql.bindState');
|
|
43
|
+
const PIPELINE_KEY = Symbol.for('neonctl.psql.pipeline');
|
|
44
|
+
const PREPARED_BY_NAME_KEY = Symbol.for('neonctl.psql.preparedByName');
|
|
45
|
+
const stashOf = (settings) => settings;
|
|
46
|
+
/** Read (and clear) the pending bind params, if any. */
|
|
47
|
+
export const consumeBindState = (settings) => {
|
|
48
|
+
const s = stashOf(settings);
|
|
49
|
+
const cur = s[BIND_STATE_KEY] ?? null;
|
|
50
|
+
s[BIND_STATE_KEY] = undefined;
|
|
51
|
+
return cur;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Peek at the bind stash without consuming. Used by `\g` to decide
|
|
55
|
+
* whether to skip the "empty buffer, no prior query" no-op guard:
|
|
56
|
+
* when a `\bind_named NAME` is pending, the prepared statement carries
|
|
57
|
+
* the SQL server-side so no buffer text is needed.
|
|
58
|
+
*/
|
|
59
|
+
export const stagedNamedBindPresent = (settings) => {
|
|
60
|
+
const cur = stashOf(settings)[BIND_STATE_KEY];
|
|
61
|
+
return !!cur && cur.byName;
|
|
62
|
+
};
|
|
63
|
+
/** Stash a PreparedStatement for later `\bind_named NAME \g` lookup. */
|
|
64
|
+
export const stashPrepared = (settings, name, ps) => {
|
|
65
|
+
const s = stashOf(settings);
|
|
66
|
+
let map = s[PREPARED_BY_NAME_KEY];
|
|
67
|
+
if (!map) {
|
|
68
|
+
map = new Map();
|
|
69
|
+
s[PREPARED_BY_NAME_KEY] = map;
|
|
70
|
+
}
|
|
71
|
+
map.set(name, ps);
|
|
72
|
+
};
|
|
73
|
+
/** Look up a previously-stashed PreparedStatement by name. */
|
|
74
|
+
export const lookupPrepared = (settings, name) => {
|
|
75
|
+
const map = stashOf(settings)[PREPARED_BY_NAME_KEY];
|
|
76
|
+
return map?.get(name) ?? null;
|
|
77
|
+
};
|
|
78
|
+
/** Drop a stashed PreparedStatement (after `\close_prepared`). */
|
|
79
|
+
export const dropPrepared = (settings, name) => {
|
|
80
|
+
const map = stashOf(settings)[PREPARED_BY_NAME_KEY];
|
|
81
|
+
if (map)
|
|
82
|
+
map.delete(name);
|
|
83
|
+
};
|
|
84
|
+
/** Peek at the current pipeline session (or null). */
|
|
85
|
+
export const getPipelineState = (settings) => stashOf(settings)[PIPELINE_KEY] ?? null;
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// PIPELINE_* counter book-keeping.
|
|
88
|
+
//
|
|
89
|
+
// Upstream psql 18 exposes three pipeline counters as `:VAR`-interpolatable
|
|
90
|
+
// session variables (initialized to "0" in `settings.ts` at startup):
|
|
91
|
+
//
|
|
92
|
+
// PIPELINE_COMMAND_COUNT number of P/B/E batches queued since last Sync
|
|
93
|
+
// PIPELINE_SYNC_COUNT number of Syncs issued in the current pipeline
|
|
94
|
+
// PIPELINE_RESULT_COUNT results queued server-side but not yet fetched
|
|
95
|
+
//
|
|
96
|
+
// The counter rules (verified empirically against vanilla psql 18.4):
|
|
97
|
+
//
|
|
98
|
+
// \startpipeline — all three reset to "0".
|
|
99
|
+
// \parse — COMMAND_COUNT++.
|
|
100
|
+
// \sendpipeline — COMMAND_COUNT++.
|
|
101
|
+
// ;-query in pipeline mode — COMMAND_COUNT++ (each implicit-`;` send).
|
|
102
|
+
// \syncpipeline — SYNC_COUNT++, RESULT_COUNT += COMMAND_COUNT,
|
|
103
|
+
// COMMAND_COUNT = 0.
|
|
104
|
+
// \flushrequest /
|
|
105
|
+
// \flush — RESULT_COUNT += COMMAND_COUNT, COMMAND_COUNT = 0.
|
|
106
|
+
// \getresults [N] — RESULT_COUNT -= actually-drained; if RESULT_COUNT
|
|
107
|
+
// hits 0, SYNC_COUNT also resets to 0 (full-drain
|
|
108
|
+
// returns the pipeline to a clean slate).
|
|
109
|
+
// \endpipeline — all three reset to "0".
|
|
110
|
+
//
|
|
111
|
+
// For `;`-queries the increment fires from a wrapper installed around the
|
|
112
|
+
// stashed `Pipeline.execute` in `cmdStartPipeline.run` — mainloop's
|
|
113
|
+
// `dispatchSendQuery` calls `ps.session.parse/bind/execute` directly, and
|
|
114
|
+
// the wrapper picks that up without mainloop having to know about the var
|
|
115
|
+
// store.
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
const readCounter = (settings, name) => {
|
|
118
|
+
const raw = settings.vars.get(name);
|
|
119
|
+
if (raw === undefined)
|
|
120
|
+
return 0;
|
|
121
|
+
const n = parseInt(raw, 10);
|
|
122
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
123
|
+
};
|
|
124
|
+
const setCounter = (settings, name, value) => {
|
|
125
|
+
settings.vars.set(name, String(Math.max(0, value)));
|
|
126
|
+
};
|
|
127
|
+
const bumpCounter = (settings, name, delta) => {
|
|
128
|
+
setCounter(settings, name, readCounter(settings, name) + delta);
|
|
129
|
+
};
|
|
130
|
+
const resetPipelineCounters = (settings) => {
|
|
131
|
+
setCounter(settings, 'PIPELINE_COMMAND_COUNT', 0);
|
|
132
|
+
setCounter(settings, 'PIPELINE_SYNC_COUNT', 0);
|
|
133
|
+
setCounter(settings, 'PIPELINE_RESULT_COUNT', 0);
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Wrap `Pipeline.execute` so each enqueued Execute message bumps
|
|
137
|
+
* `PIPELINE_COMMAND_COUNT`. The wrapping covers both call sites:
|
|
138
|
+
*
|
|
139
|
+
* - `cmdSendPipeline` (this file) — `\sendpipeline`.
|
|
140
|
+
* - `dispatchSendQuery` (mainloop) — implicit `;`-queries while pipeline
|
|
141
|
+
* is active. Mainloop calls `ps.session.execute('', 0)` so the wrapper
|
|
142
|
+
* fires automatically without mainloop knowing about the var store.
|
|
143
|
+
*
|
|
144
|
+
* The wrapper preserves the original function's `this` binding via `apply`.
|
|
145
|
+
*/
|
|
146
|
+
const wrapSessionForCounters = (session, settings) => {
|
|
147
|
+
const origExecute = session.execute.bind(session);
|
|
148
|
+
session.execute = (name, maxRows) => {
|
|
149
|
+
bumpCounter(settings, 'PIPELINE_COMMAND_COUNT', 1);
|
|
150
|
+
return origExecute(name, maxRows);
|
|
151
|
+
};
|
|
152
|
+
return session;
|
|
153
|
+
};
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Helpers
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
const errResult = (ctx, message) => {
|
|
158
|
+
ctx.settings.lastErrorResult = { message };
|
|
159
|
+
writeErr(`\\${ctx.cmdName}: ${message}\n`);
|
|
160
|
+
// Tell mainloop the diagnostic is already on stderr so it doesn't add
|
|
161
|
+
// a `psql: ERROR: <msg>` fallback line.
|
|
162
|
+
return { status: 'error', errorWritten: true };
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Coerce any thrown / rejected value into a printable string. The
|
|
166
|
+
* extended-protocol driver in `PgConnection` rejects with raw
|
|
167
|
+
* ConnectError records (`{severity, code, message, detail, ...}`) —
|
|
168
|
+
* not `Error` instances — so `String(err)` would produce
|
|
169
|
+
* `[object Object]` in the conformance output. We probe for a
|
|
170
|
+
* `.message` property (covering both `Error` and the ConnectError
|
|
171
|
+
* shape) and fall back to JSON.stringify only when there's no
|
|
172
|
+
* message field at all.
|
|
173
|
+
*/
|
|
174
|
+
const errorToMessage = (err) => {
|
|
175
|
+
if (err instanceof Error)
|
|
176
|
+
return err.message;
|
|
177
|
+
if (err !== null &&
|
|
178
|
+
typeof err === 'object' &&
|
|
179
|
+
'message' in err &&
|
|
180
|
+
typeof err.message === 'string') {
|
|
181
|
+
return err.message;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return JSON.stringify(err);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return String(err);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Render a pipeline-collected ConnectError to stderr in the upstream
|
|
192
|
+
* shape: a `SEVERITY: message` line, followed by optional `DETAIL:` /
|
|
193
|
+
* `HINT:` / `CONTEXT:` lines (each on its own line). Matches the
|
|
194
|
+
* subset of `formatErrorReport`-style output the conformance corpus
|
|
195
|
+
* expects from `\endpipeline` for non-FATAL pipeline errors. We
|
|
196
|
+
* inline a minimal renderer here rather than importing the full
|
|
197
|
+
* `formatErrorReport` from `cmd_meta` because we always want the
|
|
198
|
+
* "default verbosity, no LINE/caret context" form — pipeline errors
|
|
199
|
+
* arrive after the buffered query has been reset and we don't carry
|
|
200
|
+
* the originating SQL position through `session.lastError`.
|
|
201
|
+
*/
|
|
202
|
+
const renderPipelineError = (err) => {
|
|
203
|
+
if (err === null || typeof err !== 'object') {
|
|
204
|
+
writeErr(`ERROR: ${errorToMessage(err)}\n`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const e = err;
|
|
208
|
+
// libpq's `PGRES_PIPELINE_ABORTED` marker — the message is the bare
|
|
209
|
+
// "Pipeline aborted, command did not run" text with no `ERROR:` /
|
|
210
|
+
// `SEVERITY:` prefix and no DETAIL/HINT/CONTEXT layers. Mirrors the
|
|
211
|
+
// wording the regress baseline asserts for cascaded skips after a
|
|
212
|
+
// preceding ErrorResponse.
|
|
213
|
+
if (e.pipelineAborted) {
|
|
214
|
+
writeErr(`${e.message ?? 'Pipeline aborted, command did not run'}\n`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const severity = e.severity ?? 'ERROR';
|
|
218
|
+
const message = e.message ?? '';
|
|
219
|
+
writeErr(`${severity}: ${message}\n`);
|
|
220
|
+
if (e.detail)
|
|
221
|
+
writeErr(`DETAIL: ${e.detail}\n`);
|
|
222
|
+
if (e.hint)
|
|
223
|
+
writeErr(`HINT: ${e.hint}\n`);
|
|
224
|
+
if (e.where)
|
|
225
|
+
writeErr(`CONTEXT: ${e.where}\n`);
|
|
226
|
+
};
|
|
227
|
+
const readAllArgs = (ctx) => {
|
|
228
|
+
const out = [];
|
|
229
|
+
for (;;) {
|
|
230
|
+
const arg = ctx.nextArg('normal');
|
|
231
|
+
if (arg === null)
|
|
232
|
+
break;
|
|
233
|
+
out.push(arg);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
};
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// \bind [VALUE ...]
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
export const cmdBind = {
|
|
241
|
+
name: 'bind',
|
|
242
|
+
helpKey: 'bind',
|
|
243
|
+
run(ctx) {
|
|
244
|
+
const values = readAllArgs(ctx);
|
|
245
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = {
|
|
246
|
+
name: '',
|
|
247
|
+
values,
|
|
248
|
+
byName: false,
|
|
249
|
+
};
|
|
250
|
+
return Promise.resolve({ status: 'ok' });
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// \bind_named NAME [VALUE ...]
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
export const cmdBindNamed = {
|
|
257
|
+
name: 'bind_named',
|
|
258
|
+
helpKey: 'bind_named',
|
|
259
|
+
run(ctx) {
|
|
260
|
+
const name = ctx.nextArg('normal');
|
|
261
|
+
// Upstream `exec_command_bind_named` rejects only the missing-arg
|
|
262
|
+
// case. `''` IS valid — it addresses the unnamed prepared statement
|
|
263
|
+
// slot (set via `\parse ''`).
|
|
264
|
+
if (name === null) {
|
|
265
|
+
// Upstream wipes any prior `\bind_named` state on this error so a
|
|
266
|
+
// follow-on `\g` falls back to `pset.last_query` (the previous
|
|
267
|
+
// successful query) instead of executing against a stale handle.
|
|
268
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = undefined;
|
|
269
|
+
return Promise.resolve(errResult(ctx, 'missing required argument'));
|
|
270
|
+
}
|
|
271
|
+
const values = readAllArgs(ctx);
|
|
272
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = { name, values, byName: true };
|
|
273
|
+
return Promise.resolve({ status: 'ok' });
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// \parse NAME — prepare current queryBuf as NAME.
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
export const cmdParse = {
|
|
280
|
+
name: 'parse',
|
|
281
|
+
helpKey: 'parse',
|
|
282
|
+
async run(ctx) {
|
|
283
|
+
const name = ctx.nextArg('normal');
|
|
284
|
+
// Upstream `exec_command_parse` rejects only the missing-arg case
|
|
285
|
+
// with `missing required argument`. An explicit empty string `''`
|
|
286
|
+
// IS valid — it's the "unnamed" prepared statement slot, addressable
|
|
287
|
+
// later via `\bind_named ''`.
|
|
288
|
+
if (name === null) {
|
|
289
|
+
return errResult(ctx, 'missing required argument');
|
|
290
|
+
}
|
|
291
|
+
// Upstream `exec_command_parse` passes the query buffer verbatim to
|
|
292
|
+
// `PQsendPrepare`, with no trim — the server then stores the bytes
|
|
293
|
+
// exactly in `pg_prepared_statements.statement`. Trimming here would
|
|
294
|
+
// strip trailing whitespace that the conformance corpus (and any
|
|
295
|
+
// `LINE 1:` ErrorResponse echo) expects to round-trip byte-for-byte.
|
|
296
|
+
// The empty-buffer guard still uses a trimmed view so a whitespace-
|
|
297
|
+
// only buffer reports `no query buffer` like upstream does.
|
|
298
|
+
const sql = ctx.queryBuf;
|
|
299
|
+
if (sql.trim().length === 0) {
|
|
300
|
+
return errResult(ctx, 'no query buffer');
|
|
301
|
+
}
|
|
302
|
+
if (!ctx.settings.db) {
|
|
303
|
+
return errResult(ctx, 'no connection to the server');
|
|
304
|
+
}
|
|
305
|
+
// In pipeline mode, route the Parse through the active session so
|
|
306
|
+
// it gets queued behind the in-flight P/B/E ops (and the server
|
|
307
|
+
// defers any ParseError until the next Sync). Doing a `db.prepare`
|
|
308
|
+
// here would issue its own Sync mid-pipeline, corrupting the
|
|
309
|
+
// pipeline's reply ordering — the conformance corpus exercises a
|
|
310
|
+
// `\parse '' \parse '' \parse pipeline_1` triple-Parse and
|
|
311
|
+
// expects the third Parse's `could not determine data type` error
|
|
312
|
+
// to surface AT `\endpipeline` time, not synchronously here.
|
|
313
|
+
const pipelineActive = getPipelineState(ctx.settings);
|
|
314
|
+
if (pipelineActive !== null) {
|
|
315
|
+
try {
|
|
316
|
+
// `\parse NAME` is a USER-level command (one entry on libpq's
|
|
317
|
+
// result queue), so route through `parseSlot` (real
|
|
318
|
+
// PipelineSession) which both enqueues the Parse wire op AND
|
|
319
|
+
// registers a `cmdSlots` entry. `\getresults` walks `cmdSlots`,
|
|
320
|
+
// so without the slot the cmd would be invisible to drain
|
|
321
|
+
// accounting. Test mocks that don't implement `parseSlot` fall
|
|
322
|
+
// through to the plain `parse()` method.
|
|
323
|
+
const session = pipelineActive.session;
|
|
324
|
+
if (typeof session.parseSlot === 'function') {
|
|
325
|
+
await session.parseSlot(name, sql, []);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
await session.parse(name, sql, []);
|
|
329
|
+
}
|
|
330
|
+
ctx.settings.lastQuery = sql;
|
|
331
|
+
// Upstream `exec_command_parse` bumps `pset.piped_commands` after
|
|
332
|
+
// PQsendPrepare succeeds — the Parse is one queued command.
|
|
333
|
+
bumpCounter(ctx.settings, 'PIPELINE_COMMAND_COUNT', 1);
|
|
334
|
+
return { status: 'reset-buf', newBuf: '' };
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
return errResult(ctx, errorToMessage(err));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const ps = await ctx.settings.db.prepare(name, sql);
|
|
342
|
+
// Cache for `\bind_named NAME \g` lookup later. Upstream tracks
|
|
343
|
+
// server-side prepared statements by name in `pset.psqlScanState`-
|
|
344
|
+
// adjacent state; we keep a local map so a follow-on `\g` can
|
|
345
|
+
// bind + execute without re-parsing.
|
|
346
|
+
stashPrepared(ctx.settings, name, ps);
|
|
347
|
+
// Upstream `exec_command_parse` also updates `pset.last_query` to
|
|
348
|
+
// the prepared SQL so a subsequent `\g` (e.g. after a failed
|
|
349
|
+
// `\bind_named NAME` that wipes bind state) re-runs the parsed
|
|
350
|
+
// text via the simple-query path. Without this, our `\g` would
|
|
351
|
+
// either no-op or fall back to a stale prior query, missing the
|
|
352
|
+
// "ERROR: there is no parameter $1" the conformance corpus
|
|
353
|
+
// expects from `SELECT $1, $2` executed without bind params.
|
|
354
|
+
ctx.settings.lastQuery = sql;
|
|
355
|
+
return { status: 'reset-buf', newBuf: '' };
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
return errResult(ctx, errorToMessage(err));
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// \close_prepared NAME
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
export const cmdClosePrepared = {
|
|
366
|
+
name: 'close_prepared',
|
|
367
|
+
helpKey: 'close_prepared',
|
|
368
|
+
async run(ctx) {
|
|
369
|
+
const name = ctx.nextArg('normal');
|
|
370
|
+
// Empty string `''` is the valid unnamed prepared statement.
|
|
371
|
+
if (name === null) {
|
|
372
|
+
return errResult(ctx, 'missing required argument');
|
|
373
|
+
}
|
|
374
|
+
const db = ctx.settings.db;
|
|
375
|
+
if (!db) {
|
|
376
|
+
return errResult(ctx, 'no connection to the server');
|
|
377
|
+
}
|
|
378
|
+
// Inside an open pipeline, upstream routes through
|
|
379
|
+
// `PQsendClosePrepared` (queues a `Close('S', name)` behind the
|
|
380
|
+
// already-pending P/B/E ops, no Sync). Issuing a Sync here — as the
|
|
381
|
+
// out-of-pipeline `db.closePreparedStatement` path does — would split
|
|
382
|
+
// the in-flight batch and surface the pipeline's sticky error on the
|
|
383
|
+
// wire, leading to `\close_prepared: bind message supplies …`
|
|
384
|
+
// diagnostics that vanilla never emits.
|
|
385
|
+
const ps = getPipelineState(ctx.settings);
|
|
386
|
+
if (ps !== null) {
|
|
387
|
+
try {
|
|
388
|
+
// `\close_prepared NAME` is a USER-level command — route through
|
|
389
|
+
// `closeSlot` so it registers on `cmdSlots` like Parse / Execute
|
|
390
|
+
// (see the parseSlot comment in `\parse` above). Test mocks
|
|
391
|
+
// that don't implement `closeSlot` fall through to plain
|
|
392
|
+
// `close()`.
|
|
393
|
+
const session = ps.session;
|
|
394
|
+
if (typeof session.closeSlot === 'function') {
|
|
395
|
+
await session.closeSlot(name);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
await session.close(name);
|
|
399
|
+
}
|
|
400
|
+
// Upstream `exec_command_close_prepared` bumps `piped_commands`
|
|
401
|
+
// (PIPELINE_COMMAND_COUNT) after a successful PQsendClosePrepared.
|
|
402
|
+
bumpCounter(ctx.settings, 'PIPELINE_COMMAND_COUNT', 1);
|
|
403
|
+
// Drop any cached binding so a later `\bind_named NAME \g`
|
|
404
|
+
// errors cleanly instead of using a stale handle.
|
|
405
|
+
dropPrepared(ctx.settings, name);
|
|
406
|
+
return { status: 'ok' };
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
return errResult(ctx, errorToMessage(err));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
// Out-of-pipeline path: upstream issues `Close('S', name) + Sync`
|
|
414
|
+
// directly; the server treats Close on a missing name as a no-op
|
|
415
|
+
// (CloseComplete without diagnostics), so we don't need to know
|
|
416
|
+
// whether the statement exists. A previous implementation faked a
|
|
417
|
+
// `prepare(name, 'SELECT 1')` to reach the same Close, which broke
|
|
418
|
+
// when the name was already prepared on the server (Parse fails
|
|
419
|
+
// with `prepared statement "NAME" already exists`).
|
|
420
|
+
if (db.closePreparedStatement) {
|
|
421
|
+
await db.closePreparedStatement(name);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Backwards-compat path for Connection mocks that don't
|
|
425
|
+
// implement the dedicated entry point. The real PgConnection
|
|
426
|
+
// always provides closePreparedStatement; this branch only
|
|
427
|
+
// fires under unit tests with bespoke Connection mocks.
|
|
428
|
+
const stmt = await db.prepare(name, 'SELECT 1');
|
|
429
|
+
await stmt.close();
|
|
430
|
+
}
|
|
431
|
+
// Drop any cached binding so a later `\bind_named NAME \g` errors
|
|
432
|
+
// cleanly instead of using a stale handle.
|
|
433
|
+
dropPrepared(ctx.settings, name);
|
|
434
|
+
return { status: 'ok' };
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
return errResult(ctx, errorToMessage(err));
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// \startpipeline / \endpipeline
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
export const cmdStartPipeline = {
|
|
445
|
+
name: 'startpipeline',
|
|
446
|
+
helpKey: 'startpipeline',
|
|
447
|
+
run(ctx) {
|
|
448
|
+
if (!ctx.settings.db) {
|
|
449
|
+
return Promise.resolve(errResult(ctx, 'no connection to the server'));
|
|
450
|
+
}
|
|
451
|
+
// Vanilla psql 18.4 treats a duplicate `\startpipeline` as a silent
|
|
452
|
+
// no-op (no warning on stdout OR stderr) — verified empirically:
|
|
453
|
+
// `psql --no-psqlrc --echo-all --quiet -X -c '\startpipeline
|
|
454
|
+
// \startpipeline \endpipeline' 2>&1` prints only the three echoed
|
|
455
|
+
// lines. Our prior `errResult('pipeline already active')` was a
|
|
456
|
+
// divergence; match upstream's quiet path.
|
|
457
|
+
if (getPipelineState(ctx.settings) !== null) {
|
|
458
|
+
return Promise.resolve({ status: 'ok' });
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const session = wrapSessionForCounters(ctx.settings.db.pipeline(), ctx.settings);
|
|
462
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = {
|
|
463
|
+
session,
|
|
464
|
+
pending: [],
|
|
465
|
+
drainedCount: 0,
|
|
466
|
+
};
|
|
467
|
+
ctx.settings.sendMode = 'extended-pipeline';
|
|
468
|
+
// Upstream `exec_command_startpipeline` calls SetVariable for the three
|
|
469
|
+
// counter vars (zeroing whatever the prior pipeline left behind).
|
|
470
|
+
resetPipelineCounters(ctx.settings);
|
|
471
|
+
return Promise.resolve({ status: 'ok' });
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
return Promise.resolve(errResult(ctx, errorToMessage(err)));
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
export const cmdEndPipeline = {
|
|
479
|
+
name: 'endpipeline',
|
|
480
|
+
helpKey: 'endpipeline',
|
|
481
|
+
async run(ctx) {
|
|
482
|
+
const ps = getPipelineState(ctx.settings);
|
|
483
|
+
if (!ps) {
|
|
484
|
+
// Upstream `exec_command_endpipeline` writes the diagnostic via
|
|
485
|
+
// `pg_log_error`; in psql 18.4 the line lands on stderr with NO
|
|
486
|
+
// `psql:` / `\endpipeline:` prefix (verified empirically with
|
|
487
|
+
// `psql --no-psqlrc --echo-all --quiet -X -c '\endpipeline'
|
|
488
|
+
// 2>&1` — the only stderr line is the raw message). The
|
|
489
|
+
// conformance corpus mirrors that bare form, so we bypass
|
|
490
|
+
// `errResult` (which would inject `\endpipeline: `) and write
|
|
491
|
+
// the line directly.
|
|
492
|
+
ctx.settings.lastErrorResult = {
|
|
493
|
+
message: 'cannot send pipeline when not in pipeline mode',
|
|
494
|
+
};
|
|
495
|
+
writeErr('cannot send pipeline when not in pipeline mode\n');
|
|
496
|
+
return { status: 'error', errorWritten: true };
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
// Snapshot how many slots were already surfaced by prior
|
|
500
|
+
// `\getresults` calls — only the residual that the final Sync
|
|
501
|
+
// inside `session.end()` flushes should be printed. Upstream
|
|
502
|
+
// psql achieves this implicitly via `PQgetResult()` consuming
|
|
503
|
+
// results from libpq's queue inside `\getresults`; we mirror
|
|
504
|
+
// the semantics with an explicit cursor (`drainedCount`).
|
|
505
|
+
const alreadyDrained = ps.drainedCount;
|
|
506
|
+
// Hold a reference to the session BEFORE end() clears the stash —
|
|
507
|
+
// we still need `lastError` after the pipeline has been torn down.
|
|
508
|
+
// The session is `Pipeline` (interface) but the concrete impl
|
|
509
|
+
// (`PipelineSession`) exposes per-USER-command slot tracking; cast
|
|
510
|
+
// and probe for the field so test mocks (which don't carry the
|
|
511
|
+
// extra fields) still work — they get an empty snapshot and the
|
|
512
|
+
// path degenerates to the previous `sets`-only rendering.
|
|
513
|
+
const session = ps.session;
|
|
514
|
+
// Capture per-USER-command slots as a snapshot — `end()` settles
|
|
515
|
+
// them but doesn't expose the per-op rejection records, which
|
|
516
|
+
// we need to interleave errors at the correct ordinal position.
|
|
517
|
+
const cmdSlotsSnapshot = Array.isArray(session.cmdSlots)
|
|
518
|
+
? session.cmdSlots.slice()
|
|
519
|
+
: [];
|
|
520
|
+
// FETCH_COUNT-in-pipeline detection — when the user set FETCH_COUNT
|
|
521
|
+
// and the pipeline is being aborted, upstream emits the
|
|
522
|
+
// "fetching results in chunked mode failed" wording in lieu of
|
|
523
|
+
// (or in addition to) the regular Pipeline aborted line. Read
|
|
524
|
+
// BEFORE end() resets the counters.
|
|
525
|
+
const fetchCountActive = (ctx.settings.vars.get('FETCH_COUNT') ?? '0') !== '0';
|
|
526
|
+
const sets = await ps.session.end();
|
|
527
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = undefined;
|
|
528
|
+
ctx.settings.sendMode = 'extended-query';
|
|
529
|
+
// Upstream `exec_command_endpipeline` zeroes the counters once the
|
|
530
|
+
// pipeline has drained — mirrors the empirical behaviour of vanilla
|
|
531
|
+
// psql 18.4 where `\echo :PIPELINE_*` reads "0" after `\endpipeline`.
|
|
532
|
+
resetPipelineCounters(ctx.settings);
|
|
533
|
+
// Drop any accumulated pipeline-gate diagnostics (`\gdesc not
|
|
534
|
+
// allowed in pipeline mode` etc.) so a future pipeline starts
|
|
535
|
+
// with a clean error log. Upstream resets the equivalent
|
|
536
|
+
// libpq-side error stack at the same boundary.
|
|
537
|
+
clearPipelineGateErrors(ctx.settings);
|
|
538
|
+
// Walk the per-USER-command slots in issue order, interleaving
|
|
539
|
+
// ErrorResponse renderings with successful ResultSets. Upstream
|
|
540
|
+
// psql 18.4 emits errors EXACTLY where the failed op sat in the
|
|
541
|
+
// wire stream (see expected/psql_pipeline.out line 433: the
|
|
542
|
+
// `bind message supplies 0 parameters` ERROR prints BEFORE the
|
|
543
|
+
// second `\sendpipeline`'s `?column?` table because the failed
|
|
544
|
+
// bind was the first Execute and the successful query was the
|
|
545
|
+
// second). Plain "print all sets then error" would invert the
|
|
546
|
+
// order.
|
|
547
|
+
const settled = await Promise.allSettled(cmdSlotsSnapshot);
|
|
548
|
+
let errorRendered = false;
|
|
549
|
+
// When the snapshot is empty (test mocks that don't track
|
|
550
|
+
// cmdSlots), fall back to printing the `sets` returned by
|
|
551
|
+
// `end()` directly. This preserves the historical behaviour for
|
|
552
|
+
// mocks while keeping the in-order interleaving for the real
|
|
553
|
+
// PipelineSession.
|
|
554
|
+
const entries = settled.length > 0
|
|
555
|
+
? settled
|
|
556
|
+
: sets.map((rs) => ({
|
|
557
|
+
status: 'fulfilled',
|
|
558
|
+
value: rs,
|
|
559
|
+
}));
|
|
560
|
+
// Pre-scan THIS slice (entries from `alreadyDrained` onward) for
|
|
561
|
+
// the first non-aborted rejection. The wire layer cascade-rejects
|
|
562
|
+
// every queued non-sync op on ErrorResponse: the first failing op
|
|
563
|
+
// gets the real `ConnectError`, follow-on ops are rejected with
|
|
564
|
+
// the synthetic `pipelineAborted` marker. When the failing op
|
|
565
|
+
// lives in `pending` (Parse / Bind / Close — none of which are
|
|
566
|
+
// tracked on `cmdSlots` as a separate slot), the slot inherits
|
|
567
|
+
// the cascaded marker — in that case fall through to
|
|
568
|
+
// `session.lastError` which captures the original ERROR from the
|
|
569
|
+
// wire-layer `sync()` / `end()` path.
|
|
570
|
+
const sliceForError = entries.slice(alreadyDrained);
|
|
571
|
+
const realFromSlice = (() => {
|
|
572
|
+
for (const r of sliceForError) {
|
|
573
|
+
if (r.status !== 'rejected')
|
|
574
|
+
continue;
|
|
575
|
+
const reason = r.reason;
|
|
576
|
+
const isAborted = typeof reason === 'object' &&
|
|
577
|
+
reason !== null &&
|
|
578
|
+
reason.pipelineAborted === true;
|
|
579
|
+
if (!isAborted)
|
|
580
|
+
return reason;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
})();
|
|
584
|
+
const realLastError = realFromSlice ??
|
|
585
|
+
(() => {
|
|
586
|
+
const le = session.lastError;
|
|
587
|
+
if (le === null || le === undefined)
|
|
588
|
+
return null;
|
|
589
|
+
if (typeof le === 'object' &&
|
|
590
|
+
le.pipelineAborted) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return le;
|
|
594
|
+
})();
|
|
595
|
+
for (let i = alreadyDrained; i < entries.length; i++) {
|
|
596
|
+
const r = entries[i];
|
|
597
|
+
if (r.status === 'fulfilled') {
|
|
598
|
+
const rs = r.value;
|
|
599
|
+
// Emit any NoticeResponse messages attached to this result
|
|
600
|
+
// to stderr in the upstream libpq shape
|
|
601
|
+
// (`${severity}: ${message}\n`). In pipeline mode the
|
|
602
|
+
// server emits notices interleaved with Bind / Execute
|
|
603
|
+
// replies, so they're attached to the corresponding
|
|
604
|
+
// ResultSet's `notices` array; vanilla psql 18.4 prints
|
|
605
|
+
// them BEFORE the result body at `\endpipeline` time
|
|
606
|
+
// (e.g. `regress/psql_pipeline.out` line 671: the
|
|
607
|
+
// `WARNING: SET LOCAL can only be used in transaction
|
|
608
|
+
// blocks` lands right before the first `statement_timeout`
|
|
609
|
+
// table).
|
|
610
|
+
for (const n of rs.notices) {
|
|
611
|
+
let out = `${n.severity}: ${n.message}\n`;
|
|
612
|
+
if (n.detail !== undefined)
|
|
613
|
+
out += `DETAIL: ${n.detail}\n`;
|
|
614
|
+
if (n.hint !== undefined)
|
|
615
|
+
out += `HINT: ${n.hint}\n`;
|
|
616
|
+
writeErr(out);
|
|
617
|
+
}
|
|
618
|
+
// Print real tuples-producing results — including the 0-column
|
|
619
|
+
// 1-row shape from `SELECT \bind \sendpipeline` which upstream
|
|
620
|
+
// psql renders as `--\n(1 row)\n` (the table glyphs are just
|
|
621
|
+
// the trailing separator row plus the default row-count footer).
|
|
622
|
+
// Skip our internal Sync marker (empty `command`, see
|
|
623
|
+
// wire/pipeline.ts) and DDL-style CommandComplete-only sets
|
|
624
|
+
// (non-empty `command` but no fields and no rows).
|
|
625
|
+
const isSyncOrPlaceholder = rs.fields.length === 0 && rs.command === '' && rs.rows.length === 0;
|
|
626
|
+
const isCommandOnly = rs.fields.length === 0 && rs.rows.length === 0 && rs.command !== '';
|
|
627
|
+
if (!isSyncOrPlaceholder && !isCommandOnly) {
|
|
628
|
+
if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
629
|
+
// 0-column tuples result: the aligned printer's
|
|
630
|
+
// header/rule machinery degenerates to whitespace because
|
|
631
|
+
// there are no column widths to drive the dividers. Emit
|
|
632
|
+
// the upstream-shaped placeholder (`--` separator + row
|
|
633
|
+
// count) inline so we match `psql_pipeline.out`'s
|
|
634
|
+
// `\watch`-rejected SELECT output byte-for-byte.
|
|
635
|
+
const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
|
|
636
|
+
if (!tuplesOnly) {
|
|
637
|
+
process.stdout.write('--\n');
|
|
638
|
+
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? 'row' : 'rows'})\n\n`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else if (!errorRendered) {
|
|
647
|
+
// Render only the FIRST rejection inline — subsequent ops
|
|
648
|
+
// in an aborted pipeline reject with the synthetic
|
|
649
|
+
// `pipelineAborted` marker which we coalesce to one line.
|
|
650
|
+
// When the wire layer cascade-rejected from a Parse / Bind /
|
|
651
|
+
// Close that lives in `pending`, the only entry visible here
|
|
652
|
+
// is one rejected with `pipelineAborted` — fall back to
|
|
653
|
+
// `session.lastError` / `peekRealError` for the original
|
|
654
|
+
// ERROR.
|
|
655
|
+
const reason = r.reason;
|
|
656
|
+
const isAborted = typeof reason === 'object' &&
|
|
657
|
+
reason !== null &&
|
|
658
|
+
reason.pipelineAborted === true;
|
|
659
|
+
// FETCH_COUNT-in-pipeline: upstream emits the chunked-mode
|
|
660
|
+
// diagnostic in addition to the per-op line. Both go to
|
|
661
|
+
// stderr; the chunked-mode line comes FIRST and addresses the
|
|
662
|
+
// SQL-shaped failure (libpq's PQsetSingleRowMode rejection
|
|
663
|
+
// inside a pipeline). Mirror that for any rejection that
|
|
664
|
+
// surfaces at `\endpipeline` time while FETCH_COUNT was set.
|
|
665
|
+
// The chunked-mode prefix is gated on FETCH_COUNT, but the REAL
|
|
666
|
+
// ERROR must still be surfaced — the old code printed the prefix +
|
|
667
|
+
// "Pipeline aborted" for ANY rejection under FETCH_COUNT, swallowing
|
|
668
|
+
// the actual ERROR: text (review: minor divergences). "Pipeline
|
|
669
|
+
// aborted, command did not run" is shown ONLY for the synthetic
|
|
670
|
+
// queue-skip marker (no real error behind it).
|
|
671
|
+
if (fetchCountActive) {
|
|
672
|
+
writeErr('fetching results in chunked mode failed\n');
|
|
673
|
+
}
|
|
674
|
+
if (isAborted && realLastError !== null) {
|
|
675
|
+
renderPipelineError(realLastError);
|
|
676
|
+
}
|
|
677
|
+
else if (isAborted) {
|
|
678
|
+
writeErr('Pipeline aborted, command did not run\n');
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
renderPipelineError(reason);
|
|
682
|
+
}
|
|
683
|
+
errorRendered = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Fallback: if `session.lastError` was set but no per-op
|
|
687
|
+
// rejection was observed (can happen when the error came from
|
|
688
|
+
// the trailing Sync rather than a specific Execute), render it
|
|
689
|
+
// here. Otherwise the diagnostic would be lost.
|
|
690
|
+
if (!errorRendered) {
|
|
691
|
+
const lastErr = session.lastError;
|
|
692
|
+
if (lastErr !== null && lastErr !== undefined) {
|
|
693
|
+
// A real trailing-Sync error: name the chunked-mode failure when
|
|
694
|
+
// FETCH_COUNT was active, but still render the actual ERROR rather
|
|
695
|
+
// than the synthetic "Pipeline aborted" line (review: minor).
|
|
696
|
+
if (fetchCountActive) {
|
|
697
|
+
writeErr('fetching results in chunked mode failed\n');
|
|
698
|
+
}
|
|
699
|
+
renderPipelineError(lastErr);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return { status: 'ok' };
|
|
703
|
+
}
|
|
704
|
+
catch (err) {
|
|
705
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = undefined;
|
|
706
|
+
ctx.settings.sendMode = 'extended-query';
|
|
707
|
+
// Even on a failed `\endpipeline` (e.g. server hung up mid-drain),
|
|
708
|
+
// mirror upstream and clear the counters — the pipeline state is
|
|
709
|
+
// gone, so any non-zero value would be misleading. Same for the
|
|
710
|
+
// pipeline-gate error log.
|
|
711
|
+
resetPipelineCounters(ctx.settings);
|
|
712
|
+
clearPipelineGateErrors(ctx.settings);
|
|
713
|
+
return errResult(ctx, errorToMessage(err));
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// \syncpipeline / \flushrequest / \flush
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
export const cmdSyncPipeline = {
|
|
721
|
+
name: 'syncpipeline',
|
|
722
|
+
helpKey: 'syncpipeline',
|
|
723
|
+
async run(ctx) {
|
|
724
|
+
const ps = getPipelineState(ctx.settings);
|
|
725
|
+
if (!ps)
|
|
726
|
+
return errResult(ctx, 'no pipeline active');
|
|
727
|
+
try {
|
|
728
|
+
await ps.session.sync();
|
|
729
|
+
// Upstream `exec_command_syncpipeline`:
|
|
730
|
+
// piped_results += piped_commands;
|
|
731
|
+
// piped_commands = 0;
|
|
732
|
+
// piped_syncs++;
|
|
733
|
+
// The pending commands have transitioned to "queued results" on the
|
|
734
|
+
// server, and Sync is itself counted as a piped command boundary.
|
|
735
|
+
const queued = readCounter(ctx.settings, 'PIPELINE_COMMAND_COUNT');
|
|
736
|
+
setCounter(ctx.settings, 'PIPELINE_COMMAND_COUNT', 0);
|
|
737
|
+
bumpCounter(ctx.settings, 'PIPELINE_RESULT_COUNT', queued);
|
|
738
|
+
bumpCounter(ctx.settings, 'PIPELINE_SYNC_COUNT', 1);
|
|
739
|
+
return { status: 'ok' };
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
return errResult(ctx, errorToMessage(err));
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
export const cmdFlushRequest = {
|
|
747
|
+
name: 'flushrequest',
|
|
748
|
+
helpKey: 'flushrequest',
|
|
749
|
+
async run(ctx) {
|
|
750
|
+
const ps = getPipelineState(ctx.settings);
|
|
751
|
+
if (!ps)
|
|
752
|
+
return errResult(ctx, 'no pipeline active');
|
|
753
|
+
try {
|
|
754
|
+
await ps.session.flush();
|
|
755
|
+
// Upstream `exec_command_flushrequest`:
|
|
756
|
+
// piped_results += piped_commands;
|
|
757
|
+
// piped_commands = 0;
|
|
758
|
+
// The pending commands move to "queued results" but SYNC isn't issued.
|
|
759
|
+
const queued = readCounter(ctx.settings, 'PIPELINE_COMMAND_COUNT');
|
|
760
|
+
setCounter(ctx.settings, 'PIPELINE_COMMAND_COUNT', 0);
|
|
761
|
+
bumpCounter(ctx.settings, 'PIPELINE_RESULT_COUNT', queued);
|
|
762
|
+
return { status: 'ok' };
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
return errResult(ctx, errorToMessage(err));
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
export const cmdFlush = {
|
|
770
|
+
name: 'flush',
|
|
771
|
+
helpKey: 'flush',
|
|
772
|
+
run(ctx) {
|
|
773
|
+
return cmdFlushRequest.run(ctx);
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// \sendpipeline — submit current buffer with stashed bind params.
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
export const cmdSendPipeline = {
|
|
780
|
+
name: 'sendpipeline',
|
|
781
|
+
helpKey: 'sendpipeline',
|
|
782
|
+
async run(ctx) {
|
|
783
|
+
const ps = getPipelineState(ctx.settings);
|
|
784
|
+
if (!ps) {
|
|
785
|
+
// Upstream wording (psql 18.4): "\\sendpipeline not allowed
|
|
786
|
+
// outside of pipeline mode". Verified empirically; no
|
|
787
|
+
// `\sendpipeline: ` prefix on stderr.
|
|
788
|
+
//
|
|
789
|
+
// Upstream `exec_command_sendpipeline` also calls
|
|
790
|
+
// `clean_extended_state()` here, which clears any pending
|
|
791
|
+
// `\bind` / `\bind_named` parameters. Mirror that so a later
|
|
792
|
+
// `\startpipeline` followed by a bare `\sendpipeline` reports
|
|
793
|
+
// the missing-bind diagnostic instead of replaying the stale
|
|
794
|
+
// parameters from before the failed-outside-pipeline send.
|
|
795
|
+
consumeBindState(ctx.settings);
|
|
796
|
+
ctx.settings.lastErrorResult = {
|
|
797
|
+
message: '\\sendpipeline not allowed outside of pipeline mode',
|
|
798
|
+
};
|
|
799
|
+
writeErr('\\sendpipeline not allowed outside of pipeline mode\n');
|
|
800
|
+
return { status: 'error', errorWritten: true };
|
|
801
|
+
}
|
|
802
|
+
const bind = consumeBindState(ctx.settings);
|
|
803
|
+
// Upstream `exec_command_sendpipeline` (in pipeline mode) requires
|
|
804
|
+
// a preceding `\bind` or `\bind_named`. Without it, the error is
|
|
805
|
+
// "\sendpipeline must be used after \bind or \bind_named", emitted
|
|
806
|
+
// BEFORE the empty-buffer check. The conformance test exercises
|
|
807
|
+
// both `\sendpipeline` (no buffer, no bind) and `SELECT 1
|
|
808
|
+
// \sendpipeline` (with buffer, no bind) — both must produce the
|
|
809
|
+
// same diagnostic, so order matters here.
|
|
810
|
+
if (bind === null) {
|
|
811
|
+
ctx.settings.lastErrorResult = {
|
|
812
|
+
message: '\\sendpipeline must be used after \\bind or \\bind_named',
|
|
813
|
+
};
|
|
814
|
+
writeErr('\\sendpipeline must be used after \\bind or \\bind_named\n');
|
|
815
|
+
return { status: 'error', errorWritten: true };
|
|
816
|
+
}
|
|
817
|
+
const sql = ctx.queryBuf.trim();
|
|
818
|
+
const stmtName = bind.name;
|
|
819
|
+
const params = bind.values;
|
|
820
|
+
// `\bind_named NAME` re-uses a server-side prep stmt, so an empty
|
|
821
|
+
// buffer is fine — we skip the Parse and just Bind + Execute. The
|
|
822
|
+
// anonymous-`\bind` path still needs a buffer because we must
|
|
823
|
+
// Parse the SQL first.
|
|
824
|
+
if (!bind.byName && sql.length === 0) {
|
|
825
|
+
return errResult(ctx, 'no query buffer');
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
// We send the full P/B/E sequence without an intervening Sync — the
|
|
829
|
+
// user is expected to call \syncpipeline or \endpipeline to commit.
|
|
830
|
+
// For `\bind_named`, skip Parse (the prep stmt already exists on
|
|
831
|
+
// the server, named by the user). For anonymous `\bind`, queue
|
|
832
|
+
// an unnamed Parse so the SQL is parsed on the server in this
|
|
833
|
+
// batch.
|
|
834
|
+
if (!bind.byName) {
|
|
835
|
+
await ps.session.parse('', sql, []);
|
|
836
|
+
}
|
|
837
|
+
await ps.session.bind(stmtName, params);
|
|
838
|
+
const exec = (async () => {
|
|
839
|
+
await ps.session.execute('', 0);
|
|
840
|
+
// PipelineSession.execute resolves with void on the public API; the
|
|
841
|
+
// session internally tracks the ResultSet and surfaces it in end().
|
|
842
|
+
return {
|
|
843
|
+
command: '',
|
|
844
|
+
rowCount: null,
|
|
845
|
+
oid: null,
|
|
846
|
+
fields: [],
|
|
847
|
+
rows: [],
|
|
848
|
+
notices: [],
|
|
849
|
+
};
|
|
850
|
+
})();
|
|
851
|
+
ps.pending.push(exec);
|
|
852
|
+
return { status: 'reset-buf', newBuf: '' };
|
|
853
|
+
}
|
|
854
|
+
catch (err) {
|
|
855
|
+
return errResult(ctx, errorToMessage(err));
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
// \getresults [N] — drain N pending results (or all if N omitted).
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
export const cmdGetResults = {
|
|
863
|
+
name: 'getresults',
|
|
864
|
+
helpKey: 'getresults',
|
|
865
|
+
async run(ctx) {
|
|
866
|
+
const ps = getPipelineState(ctx.settings);
|
|
867
|
+
const arg = ctx.nextArg('normal');
|
|
868
|
+
// Upstream `exec_command_getresults` parses the optional count BEFORE
|
|
869
|
+
// checking pipeline state, so an invalid count surfaces even when
|
|
870
|
+
// there's no pipeline active. The wording matches upstream verbatim
|
|
871
|
+
// ("invalid number of requested results").
|
|
872
|
+
let requested = null;
|
|
873
|
+
if (arg !== null && arg.length > 0) {
|
|
874
|
+
const parsed = parseInt(arg, 10);
|
|
875
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
876
|
+
return errResult(ctx, 'invalid number of requested results');
|
|
877
|
+
}
|
|
878
|
+
requested = parsed;
|
|
879
|
+
}
|
|
880
|
+
// No active pipeline → upstream prints `No pending results to get`
|
|
881
|
+
// (the "no-op idle" message), NOT a hard "no pipeline active" error.
|
|
882
|
+
// This matches the conformance test that runs `\getresults` BOTH
|
|
883
|
+
// outside of `\startpipeline / \endpipeline` brackets and right
|
|
884
|
+
// after `\endpipeline`.
|
|
885
|
+
if (!ps) {
|
|
886
|
+
process.stdout.write('No pending results to get\n');
|
|
887
|
+
return { status: 'ok' };
|
|
888
|
+
}
|
|
889
|
+
// Available items to drain = PIPELINE_SYNC_COUNT + PIPELINE_RESULT_COUNT.
|
|
890
|
+
// Sync markers and data-result entries both occupy slots in libpq's
|
|
891
|
+
// pipeline result queue; vanilla's `\getresults N` walks the queue
|
|
892
|
+
// FIFO, draining either kind. A `\sendpipeline` queued on the
|
|
893
|
+
// client but not yet `\flushrequest`-ed / `\syncpipeline`-ed does
|
|
894
|
+
// NOT count — those commands have a still-pending Execute promise
|
|
895
|
+
// but the server hasn't been told to flush replies. Verified
|
|
896
|
+
// empirically with vanilla psql 18.4: SQL like
|
|
897
|
+
// \syncpipeline \syncpipeline SELECT $1 \bind 1 \sendpipeline
|
|
898
|
+
// \flushrequest \getresults 1
|
|
899
|
+
// prints nothing on the first \getresults 1 (SyncMarker drained,
|
|
900
|
+
// SYNC_COUNT: 2 → 1), the second prints nothing (SYNC_COUNT: 1 →
|
|
901
|
+
// 0), and the third prints the SELECT result.
|
|
902
|
+
const syncAvailable = readCounter(ctx.settings, 'PIPELINE_SYNC_COUNT');
|
|
903
|
+
const resultAvailable = readCounter(ctx.settings, 'PIPELINE_RESULT_COUNT');
|
|
904
|
+
const available = syncAvailable + resultAvailable;
|
|
905
|
+
if (available === 0) {
|
|
906
|
+
process.stdout.write('No pending results to get\n');
|
|
907
|
+
return { status: 'ok' };
|
|
908
|
+
}
|
|
909
|
+
// `\getresults 0` and bare `\getresults` mean "all pending"
|
|
910
|
+
// (upstream semantics).
|
|
911
|
+
const n = requested === null || requested === 0
|
|
912
|
+
? available
|
|
913
|
+
: Math.min(requested, available);
|
|
914
|
+
// Pull the next `n` per-USER-COMMAND slots from `cmdSlots`. Each
|
|
915
|
+
// entry mirrors one `PQgetResult` boundary in libpq: a
|
|
916
|
+
// `\sendpipeline` (Parse+Bind+Execute → one slot resolving to the
|
|
917
|
+
// ResultSet), a `\parse NAME` (one slot resolving to a silent
|
|
918
|
+
// placeholder), a `\close_prepared NAME` (silent placeholder),
|
|
919
|
+
// or a `\syncpipeline` (silent SyncMarker). `drainedCount`
|
|
920
|
+
// advances by `n` so a follow-on `\endpipeline` knows to skip
|
|
921
|
+
// what we've already walked. Test mocks that don't populate
|
|
922
|
+
// `cmdSlots` fall back to the legacy counter-only path.
|
|
923
|
+
const session = ps.session;
|
|
924
|
+
const slots = Array.isArray(session.cmdSlots)
|
|
925
|
+
? session.cmdSlots
|
|
926
|
+
: [];
|
|
927
|
+
const start = ps.drainedCount;
|
|
928
|
+
const end = start + n;
|
|
929
|
+
const slice = slots.slice(start, end);
|
|
930
|
+
ps.drainedCount = end;
|
|
931
|
+
// Keep `ps.pending` in sync for any legacy callers that read its
|
|
932
|
+
// length — splice off the consumed count. The spliced promises are
|
|
933
|
+
// synthetic placeholders, so we discard the return value.
|
|
934
|
+
void ps.pending.splice(0, n);
|
|
935
|
+
try {
|
|
936
|
+
const settled = await Promise.allSettled(slice);
|
|
937
|
+
// The wire layer's cascade-reject puts the real `ConnectError` on
|
|
938
|
+
// the FIRST failing op (Parse / Bind / Close — pushed onto
|
|
939
|
+
// `pending`, not `cmdSlots` as a separate slot), and stamps the
|
|
940
|
+
// synthetic `pipelineAborted` marker onto every op queued behind
|
|
941
|
+
// it. The visible cmdSlot for the Execute in the same `\sendpipeline`
|
|
942
|
+
// therefore inherits the cascaded marker — we need a separate
|
|
943
|
+
// look-up to surface the original ERROR.
|
|
944
|
+
//
|
|
945
|
+
// Strategy:
|
|
946
|
+
// 1. Prefer a non-aborted rejection found IN this slice (e.g.
|
|
947
|
+
// a Parse-only command whose Parse failed at the SLOT level).
|
|
948
|
+
// 2. Otherwise fall back to `peekRealError()` which scans
|
|
949
|
+
// pending ∪ results for the first non-aborted rejection.
|
|
950
|
+
// After `\syncpipeline` clears `pending`, this returns null
|
|
951
|
+
// for purely-cascaded batches — which is correct, the slot
|
|
952
|
+
// message ("Pipeline aborted, command did not run") is the
|
|
953
|
+
// one that should surface.
|
|
954
|
+
const sliceErr = (() => {
|
|
955
|
+
for (const r of settled) {
|
|
956
|
+
if (r.status !== 'rejected')
|
|
957
|
+
continue;
|
|
958
|
+
const reason = r.reason;
|
|
959
|
+
const isAborted = typeof reason === 'object' &&
|
|
960
|
+
reason !== null &&
|
|
961
|
+
reason.pipelineAborted === true;
|
|
962
|
+
if (!isAborted)
|
|
963
|
+
return reason;
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
})();
|
|
967
|
+
const sessPeek = session;
|
|
968
|
+
const realErr = sliceErr ??
|
|
969
|
+
(typeof sessPeek.peekRealError === 'function'
|
|
970
|
+
? await sessPeek.peekRealError()
|
|
971
|
+
: null);
|
|
972
|
+
// Per upstream `\getresults`: emit AT MOST ONE error / aborted
|
|
973
|
+
// line per call, even when the slice contains multiple rejections.
|
|
974
|
+
// First rejection's line wins; subsequent ones are suppressed
|
|
975
|
+
// inline (still implicitly accounted via the counter decrement
|
|
976
|
+
// below). Real ERROR trumps the synthetic `Pipeline aborted, …`
|
|
977
|
+
// marker — see `peekRealError`'s discovery semantics.
|
|
978
|
+
let errorRenderedHere = false;
|
|
979
|
+
let walkedItems = 0;
|
|
980
|
+
let syncsDrained = 0;
|
|
981
|
+
let resultsDrained = 0;
|
|
982
|
+
// Cursor into `slots`: indices ≥ this represent SyncMarkers
|
|
983
|
+
// pushed by `session.sync()`. We can't tag the slot in flight
|
|
984
|
+
// without changing the public shape; instead we attribute the
|
|
985
|
+
// first `syncAvailable` silent placeholders to SYNC_COUNT and
|
|
986
|
+
// the remainder to RESULT_COUNT post-walk.
|
|
987
|
+
for (const r of settled) {
|
|
988
|
+
walkedItems++;
|
|
989
|
+
if (r.status !== 'fulfilled') {
|
|
990
|
+
// Rejected promise — Parse / Bind / Execute / Close that the
|
|
991
|
+
// server responded to with ErrorResponse, or a cascaded
|
|
992
|
+
// pipelineAborted marker. Upstream `\getresults` walks
|
|
993
|
+
// libpq's per-Sync result queue inline: the failed entry
|
|
994
|
+
// produces an `ERROR: …` (or `Pipeline aborted, …`) on
|
|
995
|
+
// stderr at the `\getresults` line, not deferred to
|
|
996
|
+
// `\endpipeline`. Match that — but only render the FIRST
|
|
997
|
+
// rejection in this call so we don't double-print when an
|
|
998
|
+
// aborted pipeline funnels multiple sticky rejections
|
|
999
|
+
// through the same `\getresults`.
|
|
1000
|
+
resultsDrained++;
|
|
1001
|
+
if (!errorRenderedHere) {
|
|
1002
|
+
const reason = r.reason;
|
|
1003
|
+
const isAborted = typeof reason === 'object' &&
|
|
1004
|
+
reason !== null &&
|
|
1005
|
+
reason.pipelineAborted ===
|
|
1006
|
+
true;
|
|
1007
|
+
if (isAborted && realErr !== null) {
|
|
1008
|
+
renderPipelineError(realErr);
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
renderPipelineError(reason);
|
|
1012
|
+
}
|
|
1013
|
+
errorRenderedHere = true;
|
|
1014
|
+
}
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const rs = r.value;
|
|
1018
|
+
// Emit any NoticeResponse messages attached to this result to
|
|
1019
|
+
// stderr (libpq shape). Notices arrive interleaved with the
|
|
1020
|
+
// Bind / Execute replies and stick to the relevant ResultSet;
|
|
1021
|
+
// upstream psql renders them inline with each result's prelude.
|
|
1022
|
+
for (const n of rs.notices) {
|
|
1023
|
+
let out = `${n.severity}: ${n.message}\n`;
|
|
1024
|
+
if (n.detail !== undefined)
|
|
1025
|
+
out += `DETAIL: ${n.detail}\n`;
|
|
1026
|
+
if (n.hint !== undefined)
|
|
1027
|
+
out += `HINT: ${n.hint}\n`;
|
|
1028
|
+
writeErr(out);
|
|
1029
|
+
}
|
|
1030
|
+
// Silent placeholder: empty fields, empty command, no rows.
|
|
1031
|
+
// Either a SyncMarker (from `session.sync()`) or a successful
|
|
1032
|
+
// Parse-only / Close-only slot. Both print nothing; counter
|
|
1033
|
+
// attribution is fixed up at the tail of this function based
|
|
1034
|
+
// on `syncAvailable`.
|
|
1035
|
+
if (rs.fields.length === 0 &&
|
|
1036
|
+
rs.command === '' &&
|
|
1037
|
+
rs.rows.length === 0) {
|
|
1038
|
+
syncsDrained++;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
resultsDrained++;
|
|
1042
|
+
if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
1043
|
+
// 0-column tuples result — same upstream placeholder shape as
|
|
1044
|
+
// `\endpipeline` (see comment there).
|
|
1045
|
+
const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
|
|
1046
|
+
if (!tuplesOnly) {
|
|
1047
|
+
process.stdout.write('--\n');
|
|
1048
|
+
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? 'row' : 'rows'})\n\n`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
else if (rs.fields.length > 0) {
|
|
1052
|
+
await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
// Tell the wire layer how far the cmd layer has consumed from its
|
|
1056
|
+
// results queue. `PipelineSession.end()` uses this offset to skip
|
|
1057
|
+
// entries that `\getresults` has already inspected; otherwise the
|
|
1058
|
+
// rejected promise we just rendered here would get re-stashed on
|
|
1059
|
+
// `session.lastError` and `\endpipeline`'s fallback would
|
|
1060
|
+
// double-print the same `ERROR: …` line. The offset is into
|
|
1061
|
+
// `results` (the per-Execute promise list) not `cmdSlots`; we
|
|
1062
|
+
// approximate by passing `end` (close enough for the
|
|
1063
|
+
// `_externalDrained` check, which is only consulted by `end()`
|
|
1064
|
+
// to skip ALREADY-INSPECTED rejections).
|
|
1065
|
+
const sessMark = session;
|
|
1066
|
+
if (typeof sessMark.markDrained === 'function') {
|
|
1067
|
+
sessMark.markDrained(end);
|
|
1068
|
+
}
|
|
1069
|
+
// Once we surface a rejection inline here, also clear any sticky
|
|
1070
|
+
// `lastError` already stashed by a previous wire-layer scan so
|
|
1071
|
+
// `\endpipeline`'s fallback doesn't re-emit the same diagnostic.
|
|
1072
|
+
if (errorRenderedHere) {
|
|
1073
|
+
const sessAny = session;
|
|
1074
|
+
if (typeof sessAny.clearLastError === 'function') {
|
|
1075
|
+
sessAny.clearLastError();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// Fallback when `cmdSlots` wasn't populated (test mocks):
|
|
1079
|
+
// decrement RESULT_COUNT first, then SYNC_COUNT. Real
|
|
1080
|
+
// PipelineSession populates `cmdSlots` so the walk above already
|
|
1081
|
+
// attributed each drain to the right counter.
|
|
1082
|
+
if (walkedItems === 0) {
|
|
1083
|
+
const ddata = Math.min(n, resultAvailable);
|
|
1084
|
+
resultsDrained = ddata;
|
|
1085
|
+
syncsDrained = n - ddata;
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
// The walk lumped successful silent placeholders (Parse / Close
|
|
1089
|
+
// OK) into `syncsDrained`. Re-attribute: SYNC_COUNT only goes
|
|
1090
|
+
// down by the number of SyncMarkers we actually drained (capped
|
|
1091
|
+
// at `syncAvailable`); the rest are result-style drains.
|
|
1092
|
+
const actualSyncs = Math.min(syncsDrained, syncAvailable);
|
|
1093
|
+
const extraResults = syncsDrained - actualSyncs;
|
|
1094
|
+
syncsDrained = actualSyncs;
|
|
1095
|
+
resultsDrained += extraResults;
|
|
1096
|
+
}
|
|
1097
|
+
// Decrement counters. Upstream `exec_command_getresults` does the
|
|
1098
|
+
// same accounting: PIPELINE_SYNC_COUNT and PIPELINE_RESULT_COUNT
|
|
1099
|
+
// are decremented by the actually-consumed items in each
|
|
1100
|
+
// category. SYNC_COUNT only goes down when a drain walks an
|
|
1101
|
+
// actual SyncMarker slot — we don't force-reset it when
|
|
1102
|
+
// RESULT_COUNT hits zero, because the queue may still hold the
|
|
1103
|
+
// pending PGRES_PIPELINE_SYNC entry. The regress test
|
|
1104
|
+
// `\getresults 1` x5 after 4 commands + 1 sync drains the 5th
|
|
1105
|
+
// call silently against the SyncMarker; a 6th call would emit
|
|
1106
|
+
// "No pending results to get".
|
|
1107
|
+
bumpCounter(ctx.settings, 'PIPELINE_SYNC_COUNT', -syncsDrained);
|
|
1108
|
+
bumpCounter(ctx.settings, 'PIPELINE_RESULT_COUNT', -resultsDrained);
|
|
1109
|
+
return { status: 'ok' };
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
return errResult(ctx, errorToMessage(err));
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
// ---------------------------------------------------------------------------
|
|
1117
|
+
// \gdesc — describe the buffered query without executing it.
|
|
1118
|
+
//
|
|
1119
|
+
// The implementation lives in `./cmd_io.ts` (it shares the printer-routing
|
|
1120
|
+
// machinery used by `\g` / `\gx` / `\watch`); we re-export the spec here
|
|
1121
|
+
// so the existing pipeline test (which imports `cmdGdesc` from this
|
|
1122
|
+
// module) continues to compile. Registration is left to `cmd_io.ts`'s
|
|
1123
|
+
// `registerIoCommands` so we don't double-register.
|
|
1124
|
+
// ---------------------------------------------------------------------------
|
|
1125
|
+
export { cmdGdesc } from './cmd_io.js';
|
|
1126
|
+
// ---------------------------------------------------------------------------
|
|
1127
|
+
// Registration entry point.
|
|
1128
|
+
// ---------------------------------------------------------------------------
|
|
1129
|
+
export const registerPipelineCommands = (registry) => {
|
|
1130
|
+
registry.register(cmdBind);
|
|
1131
|
+
registry.register(cmdBindNamed);
|
|
1132
|
+
registry.register(cmdParse);
|
|
1133
|
+
registry.register(cmdClosePrepared);
|
|
1134
|
+
registry.register(cmdStartPipeline);
|
|
1135
|
+
registry.register(cmdEndPipeline);
|
|
1136
|
+
registry.register(cmdSyncPipeline);
|
|
1137
|
+
registry.register(cmdFlushRequest);
|
|
1138
|
+
registry.register(cmdFlush);
|
|
1139
|
+
registry.register(cmdSendPipeline);
|
|
1140
|
+
registry.register(cmdGetResults);
|
|
1141
|
+
};
|