neonctl 2.22.2 → 2.23.1
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 +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +268 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +43 -0
- package/package.json +15 -5
- 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/esbuild.js +147 -0
- 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/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- 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/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* psql REPL main loop.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `MainLoop()` in `src/bin/psql/mainloop.c`. Drives the
|
|
5
|
+
* read-eval-print cycle: read a line, feed it to the SQL scanner, dispatch
|
|
6
|
+
* SQL or backslash commands as boundaries appear, print results, and loop.
|
|
7
|
+
*
|
|
8
|
+
* Simplifications vs upstream (each tracked against the WP plan):
|
|
9
|
+
*
|
|
10
|
+
* - Line editing is delegated to `node:readline` for now. Upstream uses
|
|
11
|
+
* GNU readline / libedit for history and completion. The proper raw-mode
|
|
12
|
+
* line editor is owned by WP-24; until then we get sane prompt rendering
|
|
13
|
+
* and Ctrl-C handling for free from the standard library.
|
|
14
|
+
* - History accumulation is omitted (`pg_append_history` / `pg_send_history`
|
|
15
|
+
* in upstream). The history sink lives in WP-25.
|
|
16
|
+
* - `\COPY FROM STDIN` raw-data lines are not wired (WP-16). When that lands,
|
|
17
|
+
* the mainloop will switch to PROMPT3 and forward lines to a CopyInStream.
|
|
18
|
+
* - `\if`/`\elif`/`\else`/`\endif` dispatch is wired directly here so the
|
|
19
|
+
* cmd_cond module can stay decoupled from the dispatch registry. Other
|
|
20
|
+
* backslash commands go through the registry interface that WP-13 owns.
|
|
21
|
+
* - The transaction-state poll (`pset.statusF`/`PQtransactionStatus`) is
|
|
22
|
+
* represented by an optional `txStatus` field on Connection; if the
|
|
23
|
+
* Connection doesn't expose one we treat the state as `unknown` for
|
|
24
|
+
* prompt rendering.
|
|
25
|
+
*
|
|
26
|
+
* Tracked TODOs:
|
|
27
|
+
*
|
|
28
|
+
* - PSQLRC startup script (WP-22).
|
|
29
|
+
* - Encoding / multibyte handling beyond UTF-8 (handled implicitly by JS).
|
|
30
|
+
* - `\watch` continuous-execution mode.
|
|
31
|
+
*/
|
|
32
|
+
import * as readline from 'node:readline';
|
|
33
|
+
import { initialScanState } from '../types/scanner.js';
|
|
34
|
+
import { scanSql } from '../scanner/sql.js';
|
|
35
|
+
import { scanSlashArgs } from '../scanner/slash.js';
|
|
36
|
+
import { renderPromptByName } from './prompt.js';
|
|
37
|
+
import { captureLastError, pickOut, refreshErrorVars, renderResultSet, sendQuery, writeQueryError, } from './common.js';
|
|
38
|
+
import { formatDurationMs } from '../print/units.js';
|
|
39
|
+
import { COND_COMMAND_NAMES, attachCondStack, cmdElif, cmdElse, cmdEndif, cmdIf, } from '../command/cmd_cond.js';
|
|
40
|
+
import { consumeNext as consumeQueuedInput } from '../command/inputQueue.js';
|
|
41
|
+
import { consumeBindState, getPipelineState } from '../command/cmd_pipeline.js';
|
|
42
|
+
import { appendHistory, defaultHistoryPath, loadHistory, resolveHistSize, truncateHistory, } from '../io/history.js';
|
|
43
|
+
import { LineEditor } from '../io/lineEditor/index.js';
|
|
44
|
+
import { psqlCompleter } from '../complete/index.js';
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Exit codes — mirror psql's `EXIT_*` constants.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export const EXIT_SUCCESS = 0;
|
|
49
|
+
export const EXIT_FAILURE = 1;
|
|
50
|
+
export const EXIT_BADCONN = 2;
|
|
51
|
+
export const EXIT_USER = 3;
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Built-in cond command map — these are dispatched directly, before the
|
|
54
|
+
// registry lookup, because they must run even inside an inactive branch.
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
const COND_COMMANDS = new Map([
|
|
57
|
+
['if', cmdIf],
|
|
58
|
+
['elif', cmdElif],
|
|
59
|
+
['else', cmdElse],
|
|
60
|
+
['endif', cmdEndif],
|
|
61
|
+
]);
|
|
62
|
+
const makeStreamLineReader = (input, out) => {
|
|
63
|
+
const rl = readline.createInterface({
|
|
64
|
+
input,
|
|
65
|
+
crlfDelay: Infinity,
|
|
66
|
+
terminal: false,
|
|
67
|
+
});
|
|
68
|
+
// We deliberately do NOT use rl[Symbol.asyncIterator]() here. Node's
|
|
69
|
+
// events.on()-based readline iterator races its own close: when the input
|
|
70
|
+
// stream EOFs after pushing many buffered lines, the iterator drains most
|
|
71
|
+
// of them correctly but on the boundary call (when the internal queue
|
|
72
|
+
// empties on the same tick the close completes) it calls
|
|
73
|
+
// Interface.resume() AFTER close has been applied, which throws
|
|
74
|
+
// `ERR_USE_AFTER_CLOSE: readline was closed` instead of returning
|
|
75
|
+
// `{ done: true }`. The previous, *successfully* buffered last `'line'`
|
|
76
|
+
// event ends up dropped. Reproduces in a standalone Node v24 program with
|
|
77
|
+
// ~2000 pushed lines followed by `push(null)`. The 'line' / 'close'
|
|
78
|
+
// event model below is queue-based so we never lose a line on close.
|
|
79
|
+
const lineQueue = [];
|
|
80
|
+
let waiter = null;
|
|
81
|
+
let ended = false;
|
|
82
|
+
rl.on('line', (line) => {
|
|
83
|
+
if (waiter) {
|
|
84
|
+
const w = waiter;
|
|
85
|
+
waiter = null;
|
|
86
|
+
w(line);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
lineQueue.push(line);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
rl.on('close', () => {
|
|
93
|
+
ended = true;
|
|
94
|
+
if (waiter) {
|
|
95
|
+
const w = waiter;
|
|
96
|
+
waiter = null;
|
|
97
|
+
w(null);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
readLine: () => {
|
|
102
|
+
if (lineQueue.length > 0) {
|
|
103
|
+
return Promise.resolve(lineQueue.shift());
|
|
104
|
+
}
|
|
105
|
+
if (ended)
|
|
106
|
+
return Promise.resolve(null);
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
waiter = resolve;
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
pushHistory: () => undefined,
|
|
112
|
+
// No prompt to garble; just write straight to stdout.
|
|
113
|
+
interject: (text) => {
|
|
114
|
+
out.write(text);
|
|
115
|
+
},
|
|
116
|
+
close: () => {
|
|
117
|
+
rl.close();
|
|
118
|
+
return Promise.resolve();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Parse a psql VI_MODE-style boolean. Mirrors `ParseVariableBool` for the
|
|
124
|
+
* common spellings: `on` / `true` / `yes` / `1` are truthy; `off` / `false` /
|
|
125
|
+
* `no` / `0` are falsy; everything else returns `null` so the caller can
|
|
126
|
+
* surface the upstream "invalid value" diagnostic.
|
|
127
|
+
*/
|
|
128
|
+
const parseBoolVar = (raw) => {
|
|
129
|
+
const v = raw.toLowerCase().trim();
|
|
130
|
+
if (v === '' || v === 'on' || v === 'true' || v === 'yes' || v === '1') {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (v === 'off' || v === 'false' || v === 'no' || v === '0') {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
/** Translate a psql VI_MODE var value into the LineEditor mode. */
|
|
139
|
+
const viModeOption = (raw) => {
|
|
140
|
+
if (raw === undefined)
|
|
141
|
+
return 'emacs';
|
|
142
|
+
return parseBoolVar(raw) === true ? 'vi' : 'emacs';
|
|
143
|
+
};
|
|
144
|
+
const makeEditorLineReader = async (ctx, opts = {}) => {
|
|
145
|
+
const env = process.env;
|
|
146
|
+
const histPath = defaultHistoryPath(env);
|
|
147
|
+
const histSize = resolveHistSize(env);
|
|
148
|
+
const histControl = ctx.settings.vars.get('HISTCONTROL') ??
|
|
149
|
+
ctx.settings.histControl;
|
|
150
|
+
let history = [];
|
|
151
|
+
try {
|
|
152
|
+
history = await loadHistory(histPath);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Missing or unreadable history file — start fresh.
|
|
156
|
+
history = [];
|
|
157
|
+
}
|
|
158
|
+
// VI_MODE: upstream readline's `set editing-mode {emacs|vi}`. We read once
|
|
159
|
+
// here for the initial mode, and below we install a VarStore hook so a
|
|
160
|
+
// subsequent `\set VI_MODE on` switches the editor at the next prompt.
|
|
161
|
+
const initialMode = viModeOption(ctx.settings.vars.get('VI_MODE'));
|
|
162
|
+
const editor = new LineEditor({
|
|
163
|
+
stdin: ctx.stdin,
|
|
164
|
+
stdout: ctx.stdout,
|
|
165
|
+
history,
|
|
166
|
+
completer: psqlCompleter({
|
|
167
|
+
settings: ctx.settings,
|
|
168
|
+
getQueryBuf: opts.getQueryBuf,
|
|
169
|
+
}),
|
|
170
|
+
mode: initialMode,
|
|
171
|
+
});
|
|
172
|
+
// Hook: validate the value, reject unrecognised input with psql's
|
|
173
|
+
// `\set: VI_MODE: invalid value "X"; valid values: on, off` diagnostic,
|
|
174
|
+
// and on success forward to `editor.setMode` (which defers the switch to
|
|
175
|
+
// the next readLine boundary). Replay on registration is fine — the hook
|
|
176
|
+
// is idempotent for a no-op `null`/unchanged value.
|
|
177
|
+
ctx.settings.vars.addHook('VI_MODE', (newValue) => {
|
|
178
|
+
if (newValue === null) {
|
|
179
|
+
editor.setMode('emacs');
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
const parsed = parseBoolVar(newValue);
|
|
183
|
+
if (parsed === null) {
|
|
184
|
+
ctx.stderr.write(`\\set: VI_MODE: invalid value "${newValue}"; valid values: on, off\n`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
editor.setMode(parsed ? 'vi' : 'emacs');
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
readLine: async (prompt) => {
|
|
192
|
+
const r = await editor.readLine(prompt);
|
|
193
|
+
if (r === editor.EOF)
|
|
194
|
+
return null;
|
|
195
|
+
return r;
|
|
196
|
+
},
|
|
197
|
+
pushHistory: (line) => {
|
|
198
|
+
const trimmed = line.replace(/\n+$/, '');
|
|
199
|
+
if (trimmed.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
editor.pushHistory(trimmed);
|
|
202
|
+
// Best-effort persist. We don't block the REPL on disk I/O.
|
|
203
|
+
void appendHistory(histPath, trimmed, histControl).catch(() => undefined);
|
|
204
|
+
},
|
|
205
|
+
interject: (text) => {
|
|
206
|
+
editor.interject(text);
|
|
207
|
+
},
|
|
208
|
+
close: async () => {
|
|
209
|
+
try {
|
|
210
|
+
editor.close();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
// Truncate to HISTSIZE on exit (libreadline behaviour).
|
|
216
|
+
try {
|
|
217
|
+
await truncateHistory(histPath, histSize);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// ignore
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Recognize upstream psql's `exit` / `quit` shortcut: when typed at the start
|
|
227
|
+
* of a fresh statement (queryBuf empty), they exit the REPL. Accepts a
|
|
228
|
+
* trailing `;` and/or whitespace.
|
|
229
|
+
*/
|
|
230
|
+
const isQuitKeyword = (line) => {
|
|
231
|
+
const trimmed = line.trim();
|
|
232
|
+
if (trimmed.length === 0)
|
|
233
|
+
return false;
|
|
234
|
+
const stripped = trimmed.replace(/;+\s*$/u, '').trimEnd();
|
|
235
|
+
return stripped === 'exit' || stripped === 'quit';
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Recognize the bare `help` keyword the same way upstream does: at the start
|
|
239
|
+
* of a fresh statement, it prints a one-screen reminder of the most useful
|
|
240
|
+
* meta-commands and continues the REPL.
|
|
241
|
+
*/
|
|
242
|
+
const isHelpKeyword = (line) => {
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (trimmed.length === 0)
|
|
245
|
+
return false;
|
|
246
|
+
const stripped = trimmed.replace(/;+\s*$/u, '').trimEnd();
|
|
247
|
+
return stripped === 'help';
|
|
248
|
+
};
|
|
249
|
+
const HELP_TEXT = 'You are using psql-ts, the embedded TypeScript psql in neonctl.\n' +
|
|
250
|
+
'Type: \\copyright for distribution terms\n' +
|
|
251
|
+
' \\h for help with SQL commands\n' +
|
|
252
|
+
' \\? for help with psql commands\n' +
|
|
253
|
+
' \\g or terminate with semicolon to execute query\n' +
|
|
254
|
+
' \\q to quit\n';
|
|
255
|
+
const makeLineReader = async (ctx, opts = {}) => {
|
|
256
|
+
const debug = process.env.NEONCTL_PSQL_DEBUG === '1';
|
|
257
|
+
if (ctx.settings.notty) {
|
|
258
|
+
if (debug) {
|
|
259
|
+
ctx.stderr.write('[psql-debug] notty=true; using stream reader (no line editor / no Tab completion)\n');
|
|
260
|
+
}
|
|
261
|
+
return makeStreamLineReader(ctx.stdin, ctx.stdout);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const r = await makeEditorLineReader(ctx, opts);
|
|
265
|
+
if (debug) {
|
|
266
|
+
ctx.stderr.write('[psql-debug] LineEditor engaged (raw mode, Tab completion active)\n');
|
|
267
|
+
}
|
|
268
|
+
return r;
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
if (debug) {
|
|
272
|
+
ctx.stderr.write(`[psql-debug] LineEditor setup failed, falling back to stream reader: ${err.message}\n`);
|
|
273
|
+
}
|
|
274
|
+
return makeStreamLineReader(ctx.stdin, ctx.stdout);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const transactionState = (ctx) => {
|
|
278
|
+
const db = ctx.settings.db;
|
|
279
|
+
if (!db)
|
|
280
|
+
return 'idle';
|
|
281
|
+
const status = db.txStatus;
|
|
282
|
+
switch (status) {
|
|
283
|
+
case 'I':
|
|
284
|
+
case 'idle':
|
|
285
|
+
return 'idle';
|
|
286
|
+
case 'T':
|
|
287
|
+
case 'in-block':
|
|
288
|
+
return 'in-block';
|
|
289
|
+
case 'E':
|
|
290
|
+
case 'failed':
|
|
291
|
+
return 'failed';
|
|
292
|
+
case 'unknown':
|
|
293
|
+
return 'unknown';
|
|
294
|
+
default:
|
|
295
|
+
return 'idle';
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// BackslashContext factory — built per-invocation so the dispatched command
|
|
300
|
+
// sees an isolated arg-cursor.
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
const makeBackslashContext = (ctx, cmdName, rawArgs, queryBuf) => {
|
|
303
|
+
// Pre-parse the args once at construction. `nextArg` then pops from this
|
|
304
|
+
// queue. We over-parse a bit (every arg gets normalised through the slash
|
|
305
|
+
// scanner in 'normal' mode), then re-split for non-normal modes lazily on
|
|
306
|
+
// demand. For WP-12 the only consumers are the cond commands, which always
|
|
307
|
+
// request 'normal'; future WPs may need richer routing.
|
|
308
|
+
const varLookup = (name) => ctx.settings.vars.get(name);
|
|
309
|
+
const buffered = new Map();
|
|
310
|
+
const argsFor = (mode) => {
|
|
311
|
+
const cached = buffered.get(mode);
|
|
312
|
+
if (cached)
|
|
313
|
+
return cached;
|
|
314
|
+
const parsed = scanSlashArgs(rawArgs, mode, varLookup);
|
|
315
|
+
buffered.set(mode, parsed);
|
|
316
|
+
return parsed;
|
|
317
|
+
};
|
|
318
|
+
const cursors = new Map();
|
|
319
|
+
const bctx = {
|
|
320
|
+
settings: ctx.settings,
|
|
321
|
+
cmdName,
|
|
322
|
+
queryBuf,
|
|
323
|
+
rawArgs,
|
|
324
|
+
nextArg(mode = 'normal') {
|
|
325
|
+
const args = argsFor(mode);
|
|
326
|
+
const idx = cursors.get(mode) ?? 0;
|
|
327
|
+
if (idx >= args.length)
|
|
328
|
+
return null;
|
|
329
|
+
cursors.set(mode, idx + 1);
|
|
330
|
+
return args[idx];
|
|
331
|
+
},
|
|
332
|
+
restOfLine() {
|
|
333
|
+
// Whatever the user typed after the command name, verbatim.
|
|
334
|
+
return rawArgs;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
return bctx;
|
|
338
|
+
};
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Error printing. Keeps the format close to libpq's `psql: ERROR: msg`.
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
const writeError = (ctx, message) => {
|
|
343
|
+
ctx.stderr.write(`psql: ERROR: ${message}\n`);
|
|
344
|
+
};
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Prompt context builder.
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
const buildPromptContext = (ctx, promptStatus, lineNumber) => ({
|
|
349
|
+
settings: ctx.settings,
|
|
350
|
+
cond: ctx.cond,
|
|
351
|
+
promptStatus,
|
|
352
|
+
lineNumber,
|
|
353
|
+
inTransaction: transactionState(ctx),
|
|
354
|
+
pipelineState: 'off',
|
|
355
|
+
});
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Conditional-command dispatch. Returns true if the command was a cond
|
|
358
|
+
// command (handled here), false otherwise. cond commands run regardless of
|
|
359
|
+
// whether the surrounding branch is active.
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
const dispatchCondCommand = async (ctx, cmdName, rawArgs, queryBuf) => {
|
|
362
|
+
const spec = COND_COMMANDS.get(cmdName);
|
|
363
|
+
if (!spec)
|
|
364
|
+
return { handled: false };
|
|
365
|
+
const bctx = makeBackslashContext(ctx, cmdName, rawArgs, queryBuf);
|
|
366
|
+
attachCondStack(bctx, ctx.cond);
|
|
367
|
+
const result = await spec.run(bctx);
|
|
368
|
+
// Only emit a fallback `psql: ERROR: <msg>` line for commands that did
|
|
369
|
+
// NOT write their own diagnostic. The `errorWritten` flag distinguishes
|
|
370
|
+
// these: commands using cmd_io's `errResult` (and inline writers) set it
|
|
371
|
+
// to `true`; cond commands (which only stash `lastErrorResult.message`)
|
|
372
|
+
// leave it unset so the mainloop surfaces the message.
|
|
373
|
+
if (result.status === 'error' &&
|
|
374
|
+
!result.errorWritten &&
|
|
375
|
+
ctx.settings.lastErrorResult?.message) {
|
|
376
|
+
writeError(ctx, ctx.settings.lastErrorResult.message);
|
|
377
|
+
}
|
|
378
|
+
return { handled: true, result };
|
|
379
|
+
};
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Backslash dispatch for non-cond commands. Only runs when cond is active.
|
|
382
|
+
// Returns the BackslashResult, or null if no command was found.
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
const dispatchRegisteredCommand = async (ctx, cmdName, rawArgs, queryBuf) => {
|
|
385
|
+
const spec = ctx.registry.lookup(cmdName);
|
|
386
|
+
if (!spec) {
|
|
387
|
+
writeError(ctx, `invalid command \\${cmdName}`);
|
|
388
|
+
// Treat the "invalid command" message as already-written so the next
|
|
389
|
+
// layer doesn't add a second one. (Other dispatch paths set
|
|
390
|
+
// `lastErrorResult.message`; this one does not, so the duplicate
|
|
391
|
+
// guard below would skip anyway — flag it explicitly for symmetry.)
|
|
392
|
+
return { status: 'error', errorWritten: true };
|
|
393
|
+
}
|
|
394
|
+
const bctx = makeBackslashContext(ctx, cmdName, rawArgs, queryBuf);
|
|
395
|
+
attachCondStack(bctx, ctx.cond);
|
|
396
|
+
const result = await spec.run(bctx);
|
|
397
|
+
// Same contract as `dispatchCondCommand`: only fall back to the bare
|
|
398
|
+
// `psql: ERROR: <msg>` shape when the command didn't already surface
|
|
399
|
+
// its own diagnostic. Without this guard, `\gdesc` Parse failures
|
|
400
|
+
// would emit a stray `psql: ERROR: <msg>` line between the LINE/`^`
|
|
401
|
+
// block and the `\errverbose` re-render, breaking the strict ordering
|
|
402
|
+
// check in the conformance regex.
|
|
403
|
+
if (result.status === 'error' &&
|
|
404
|
+
!result.errorWritten &&
|
|
405
|
+
ctx.settings.lastErrorResult?.message) {
|
|
406
|
+
writeError(ctx, ctx.settings.lastErrorResult.message);
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
};
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// SendQuery — delegate to the unified pipeline in `common.ts`. Returns the
|
|
412
|
+
// success flag so the read loop can short-circuit under ON_ERROR_STOP.
|
|
413
|
+
//
|
|
414
|
+
// If `\bind` (WP-21) has stashed parameters on the settings, route through
|
|
415
|
+
// the extended-query path on the Connection. Otherwise use the simple-query
|
|
416
|
+
// pipeline.
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
/**
|
|
419
|
+
* Refresh psql vars that mirror connection-driven server state. Today this
|
|
420
|
+
* is just `ENCODING` (tracks `client_encoding` ParameterStatus). Upstream
|
|
421
|
+
* does the same check at the tail of `SendQuery` in common.c so a
|
|
422
|
+
* `SET client_encoding = ...` lands on the psql var before the next
|
|
423
|
+
* statement looks it up. Safe to call when no connection is bound.
|
|
424
|
+
*/
|
|
425
|
+
const refreshConnectionVars = (ctx) => {
|
|
426
|
+
const db = ctx.settings.db;
|
|
427
|
+
if (!db)
|
|
428
|
+
return;
|
|
429
|
+
const enc = db.parameterStatus('client_encoding');
|
|
430
|
+
if (enc !== undefined && ctx.settings.vars.get('ENCODING') !== enc) {
|
|
431
|
+
ctx.settings.vars.set('ENCODING', enc);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const dispatchSendQuery = async (ctx, sql) => {
|
|
435
|
+
// Always consume the bind stash up-front so it's cleared regardless of which
|
|
436
|
+
// branch runs (and regardless of success / failure on the bind path).
|
|
437
|
+
const bind = consumeBindState(ctx.settings);
|
|
438
|
+
// Pipeline-active routing: when `\startpipeline` is in effect, a
|
|
439
|
+
// semicolon-terminated SQL must be appended to the pipeline as
|
|
440
|
+
// Parse/Bind/Describe/Execute (no Sync). Sending it as a simple Query
|
|
441
|
+
// would corrupt the pipeline — the in-flight extended-protocol replies
|
|
442
|
+
// would land in `handleQueryMessage` and the pipeline's ResultSet
|
|
443
|
+
// promises would never settle, leaving `\endpipeline` hung.
|
|
444
|
+
//
|
|
445
|
+
// Mirrors upstream psql: `SendQuery` checks `PQpipelineStatus` and routes
|
|
446
|
+
// through `PQsendQueryParams`/`PQsendQuery` accordingly. We use the
|
|
447
|
+
// session helper so the wire enqueueing matches `\sendpipeline`.
|
|
448
|
+
const ps = getPipelineState(ctx.settings);
|
|
449
|
+
if (ps && ctx.settings.db) {
|
|
450
|
+
// Upstream `libpq` refuses `COPY ... FROM STDIN` / `COPY ... TO
|
|
451
|
+
// STDOUT` inside a pipeline with the fatal diagnostic
|
|
452
|
+
// "COPY in a pipeline is not supported, aborting connection".
|
|
453
|
+
// Detect, emit the same wording client-side, and tear down the
|
|
454
|
+
// connection so the mainloop's `checkConnectionLost` halt path
|
|
455
|
+
// fires for any subsequent statement (matching the upstream
|
|
456
|
+
// "aborting connection" semantics).
|
|
457
|
+
const trimmed = sql.trimStart();
|
|
458
|
+
if (/^COPY\b/i.test(trimmed) &&
|
|
459
|
+
/\b(FROM\s+STDIN|TO\s+STDOUT)\b/i.test(trimmed)) {
|
|
460
|
+
ctx.stderr.write('psql: error: COPY in a pipeline is not supported, aborting connection\n');
|
|
461
|
+
// Hard-abort the underlying socket so isClosed() flips true and the
|
|
462
|
+
// mainloop's post-dispatch `checkConnectionLost` ends the loop.
|
|
463
|
+
try {
|
|
464
|
+
const db = ctx.settings.db;
|
|
465
|
+
if (typeof db.abortForCopyInPipeline === 'function') {
|
|
466
|
+
db.abortForCopyInPipeline();
|
|
467
|
+
}
|
|
468
|
+
else if (typeof db.close === 'function') {
|
|
469
|
+
await db.close();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// ignore — the diagnostic has already been emitted.
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
// Pipeline-mode `;`-queries: empty parameter list, anonymous prepared
|
|
479
|
+
// statement, anonymous portal. The result will surface later through
|
|
480
|
+
// `\endpipeline` / `\getresults`.
|
|
481
|
+
await ps.session.parse('', sql, []);
|
|
482
|
+
await ps.session.bind('', bind?.values ?? []);
|
|
483
|
+
const exec = (async () => {
|
|
484
|
+
await ps.session.execute('', 0);
|
|
485
|
+
return undefined;
|
|
486
|
+
})();
|
|
487
|
+
ps.pending.push(exec);
|
|
488
|
+
// The enqueue succeeded; the actual result will flush at
|
|
489
|
+
// `\endpipeline` time. Mark the diagnostic vars as success-now so
|
|
490
|
+
// intervening `\echo :ERROR` sees "false" between pipeline appends.
|
|
491
|
+
refreshErrorVars(ctx.settings, { kind: 'success', rowCount: null });
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
const message = captureLastError(ctx.settings, err, sql);
|
|
496
|
+
writeQueryError(ctx, message);
|
|
497
|
+
refreshErrorVars(ctx.settings, { kind: 'error' });
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (bind && ctx.settings.db) {
|
|
502
|
+
const started = ctx.settings.timing ? Date.now() : 0;
|
|
503
|
+
let lastRowCount = null;
|
|
504
|
+
let hadError = false;
|
|
505
|
+
try {
|
|
506
|
+
const rs = await ctx.settings.db.query(sql, bind.values);
|
|
507
|
+
// Route the single ResultSet through the unified printer pipeline so
|
|
508
|
+
// `\bind` output looks identical to a simple-query result (and honours
|
|
509
|
+
// `\o FILE`, format selection, expanded mode, etc.).
|
|
510
|
+
const r = await renderResultSet(ctx, rs, pickOut(ctx));
|
|
511
|
+
lastRowCount = r.lastRowCount;
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
// Capture the full ErrorResponse payload (severity / code / position /
|
|
516
|
+
// detail / hint / location) so the layered renderer can honour
|
|
517
|
+
// VERBOSITY and SHOW_CONTEXT exactly like the simple-query path.
|
|
518
|
+
const message = captureLastError(ctx.settings, err, sql);
|
|
519
|
+
writeQueryError(ctx, message);
|
|
520
|
+
hadError = true;
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
refreshConnectionVars(ctx);
|
|
525
|
+
refreshErrorVars(ctx.settings, hadError
|
|
526
|
+
? { kind: 'error' }
|
|
527
|
+
: { kind: 'success', rowCount: lastRowCount });
|
|
528
|
+
if (ctx.settings.timing) {
|
|
529
|
+
ctx.stdout.write('\n' + formatDurationMs(Date.now() - started) + '\n');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const stats = await sendQuery(ctx, sql);
|
|
534
|
+
refreshConnectionVars(ctx);
|
|
535
|
+
return !stats.hadError;
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* Format an async NotificationResponse (LISTEN/NOTIFY payload) the way
|
|
539
|
+
* upstream's `PrintNotifications` in common.c does. Empty payloads omit the
|
|
540
|
+
* payload clause for backward-compat with pre-9.0 servers.
|
|
541
|
+
*/
|
|
542
|
+
const formatNotification = (channel, payload, pid) => {
|
|
543
|
+
if (payload.length > 0) {
|
|
544
|
+
return (`Asynchronous notification "${channel}" with payload "${payload}" ` +
|
|
545
|
+
`received from server process with PID ${String(pid)}.\n`);
|
|
546
|
+
}
|
|
547
|
+
return (`Asynchronous notification "${channel}" ` +
|
|
548
|
+
`received from server process with PID ${String(pid)}.\n`);
|
|
549
|
+
};
|
|
550
|
+
/**
|
|
551
|
+
* Subscribe to NotificationResponse on the active connection, rendering each
|
|
552
|
+
* to the REPL output (mirrors upstream `PrintNotifications` writing to
|
|
553
|
+
* `pset.queryFout`). Returns the disposer the connection handed us, or
|
|
554
|
+
* `null` when no connection is bound.
|
|
555
|
+
*/
|
|
556
|
+
const installNotificationHandler = (ctx, reader) => {
|
|
557
|
+
const db = ctx.settings.db;
|
|
558
|
+
if (!db)
|
|
559
|
+
return null;
|
|
560
|
+
return db.onNotification((channel, payload, pid) => {
|
|
561
|
+
// Route through the reader so the LineEditor (when raw-mode active)
|
|
562
|
+
// can clear / re-render its prompt block around the injected line.
|
|
563
|
+
// The stream-reader path treats interject as a plain stdout write.
|
|
564
|
+
reader.interject(formatNotification(channel, payload, pid));
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
/**
|
|
568
|
+
* Render a NoticeResponse field the same way libpq's `pqBuildErrorMessage3`
|
|
569
|
+
* does for the default `psql_notice_processor` (which is a thin
|
|
570
|
+
* `fputs(msg, stderr)`). Mirrors VERBOSITY / SHOW_CONTEXT semantics:
|
|
571
|
+
*
|
|
572
|
+
* - `terse` / `sqlstate`: just `<severity>: <message>` (`sqlstate` also
|
|
573
|
+
* prepends the SQLSTATE on the severity line).
|
|
574
|
+
* - `default`: severity line + LINE/^ pointer + DETAIL/HINT, and CONTEXT
|
|
575
|
+
* when SHOW_CONTEXT is `always` (NOTICE is not an error, so the default
|
|
576
|
+
* `errors` setting suppresses its CONTEXT — upstream's libpq path
|
|
577
|
+
* gates context on `severity_nonlocalized == "ERROR"|"FATAL"|"PANIC"`).
|
|
578
|
+
* - `verbose`: full SQLSTATE / DETAIL / HINT / CONTEXT / LOCATION layers.
|
|
579
|
+
*
|
|
580
|
+
* The trailing newline mirrors libpq, so callers can `stderr.write()` the
|
|
581
|
+
* returned string directly.
|
|
582
|
+
*/
|
|
583
|
+
const formatNotice = (notice, verbosity, showContext) => {
|
|
584
|
+
const severity = notice.severity || 'NOTICE';
|
|
585
|
+
const message = notice.message || '';
|
|
586
|
+
const lines = [];
|
|
587
|
+
if (verbosity === 'verbose' || verbosity === 'sqlstate') {
|
|
588
|
+
const sqlstate = notice.code ?? 'XX000';
|
|
589
|
+
lines.push(`${severity}: ${sqlstate}: ${message}`);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
lines.push(`${severity}: ${message}`);
|
|
593
|
+
}
|
|
594
|
+
if (verbosity === 'terse' || verbosity === 'sqlstate') {
|
|
595
|
+
return lines.join('\n') + '\n';
|
|
596
|
+
}
|
|
597
|
+
if (notice.detail)
|
|
598
|
+
lines.push(`DETAIL: ${notice.detail}`);
|
|
599
|
+
if (notice.hint)
|
|
600
|
+
lines.push(`HINT: ${notice.hint}`);
|
|
601
|
+
// CONTEXT gating mirrors libpq's `pqBuildErrorMessage3`:
|
|
602
|
+
// - `verbose` always includes CONTEXT
|
|
603
|
+
// - `default` shows CONTEXT only when SHOW_CONTEXT is `always` for
|
|
604
|
+
// non-error severities (NOTICE / WARNING / INFO / LOG / DEBUG), or
|
|
605
|
+
// when SHOW_CONTEXT is `errors`/`always` for ERROR-level entries.
|
|
606
|
+
const isError = severity === 'ERROR' || severity === 'FATAL' || severity === 'PANIC';
|
|
607
|
+
const includeContext = verbosity === 'verbose' ||
|
|
608
|
+
showContext === 'always' ||
|
|
609
|
+
(showContext === 'errors' && isError);
|
|
610
|
+
if (includeContext && notice.where) {
|
|
611
|
+
lines.push(`CONTEXT: ${notice.where}`);
|
|
612
|
+
}
|
|
613
|
+
if (verbosity === 'verbose' && (notice.routine || notice.file)) {
|
|
614
|
+
const location = (notice.routine ?? '') +
|
|
615
|
+
(notice.file ? `, ${notice.file}:${notice.line ?? ''}` : '');
|
|
616
|
+
lines.push(`LOCATION: ${location}`);
|
|
617
|
+
}
|
|
618
|
+
return lines.join('\n') + '\n';
|
|
619
|
+
};
|
|
620
|
+
/**
|
|
621
|
+
* Subscribe to NoticeResponse on the active connection, rendering each to
|
|
622
|
+
* stderr the way libpq's default `psql_notice_processor` does. Returns the
|
|
623
|
+
* disposer the connection handed us, or `null` when no connection is bound.
|
|
624
|
+
*
|
|
625
|
+
* NOTICEs fire synchronously as the wire layer receives them, so an
|
|
626
|
+
* inline `RAISE NOTICE` inside a `\;`-chained batch lands BEFORE the
|
|
627
|
+
* tuples-producing portion of the batch is rendered — matching upstream
|
|
628
|
+
* psql output.
|
|
629
|
+
*/
|
|
630
|
+
const installNoticeHandler = (ctx, reader) => {
|
|
631
|
+
const db = ctx.settings.db;
|
|
632
|
+
if (!db)
|
|
633
|
+
return null;
|
|
634
|
+
return db.onNotice((notice) => {
|
|
635
|
+
// Skip in pipeline mode: cmd_pipeline.ts's `\endpipeline` / `\getresults`
|
|
636
|
+
// re-renders each result's `notices[]` array via the per-result drain so
|
|
637
|
+
// the NOTICE lands AT the result boundary, not before. Emitting here too
|
|
638
|
+
// would duplicate every notice — once when the wire layer parses it,
|
|
639
|
+
// once when the drain walks `rs.notices`.
|
|
640
|
+
if (ctx.settings.sendMode === 'extended-pipeline')
|
|
641
|
+
return;
|
|
642
|
+
const text = formatNotice(notice, ctx.settings.verbosity, ctx.settings.showContext);
|
|
643
|
+
// Notices go to stderr (libpq default). The LineEditor's prompt-redraw
|
|
644
|
+
// logic uses `interjectErr` to flush the message without disturbing the
|
|
645
|
+
// active prompt block — fall back to a raw stderr write when the reader
|
|
646
|
+
// doesn't expose that hook (stream / notty path).
|
|
647
|
+
const interjectErr = reader.interjectErr;
|
|
648
|
+
if (interjectErr) {
|
|
649
|
+
interjectErr.call(reader, text);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
ctx.stderr.write(text);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
const installSigint = (ctx, state) => {
|
|
657
|
+
const handler = () => {
|
|
658
|
+
if (state.inQuery && ctx.settings.db) {
|
|
659
|
+
// Best-effort cancel; ignore errors.
|
|
660
|
+
void ctx.settings.db.cancel().catch(() => undefined);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
state.resetBuf();
|
|
664
|
+
};
|
|
665
|
+
process.on('SIGINT', handler);
|
|
666
|
+
return () => process.off('SIGINT', handler);
|
|
667
|
+
};
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
// The main entry point.
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
export const runMainLoop = async (ctx) => {
|
|
672
|
+
// queryBuf is declared up front so the line reader's tab completer can
|
|
673
|
+
// close over it via `getQueryBuf` and see the in-flight multi-line buffer
|
|
674
|
+
// on every Tab. The mainloop reassigns this variable across statements
|
|
675
|
+
// (resetBuf, after dispatch); the closure stays valid because it captures
|
|
676
|
+
// the binding, not a snapshot.
|
|
677
|
+
let queryBuf = '';
|
|
678
|
+
const reader = await makeLineReader(ctx, { getQueryBuf: () => queryBuf });
|
|
679
|
+
let scanState = initialScanState();
|
|
680
|
+
let stmtLineNumber = 1;
|
|
681
|
+
let successResult = EXIT_SUCCESS;
|
|
682
|
+
let exitRequested = false;
|
|
683
|
+
// Parallel stack of saved scanner states keyed to cond.depth(). Upstream
|
|
684
|
+
// `save_query_text_state` captures both `query_buf->len` AND the scanner
|
|
685
|
+
// state at the `\if` (and at each branch transition); `discard_query_text`
|
|
686
|
+
// restores both. The cond stack frame already carries `savedQueryBufLen`;
|
|
687
|
+
// we keep the matching scanState here so the cond commands stay decoupled
|
|
688
|
+
// from the scanner type. The two arrays are pushed / popped / re-anchored
|
|
689
|
+
// in lock-step with cond.push / cond.pop / cond.setSavedQueryBufLen.
|
|
690
|
+
const condScanStateStack = [];
|
|
691
|
+
const resetBuf = () => {
|
|
692
|
+
queryBuf = '';
|
|
693
|
+
scanState = initialScanState();
|
|
694
|
+
stmtLineNumber = 1;
|
|
695
|
+
};
|
|
696
|
+
// Detect mid-script connection loss and, on first detection, emit the
|
|
697
|
+
// upstream "connection to server was lost" diagnostic + flag EXIT_BADCONN.
|
|
698
|
+
// Subsequent statements would all rethrow against the closed connection;
|
|
699
|
+
// we halt the loop instead so we don't spam ERROR lines for every one.
|
|
700
|
+
const checkConnectionLost = () => {
|
|
701
|
+
if (ctx.settings.db?.isClosed()) {
|
|
702
|
+
ctx.stderr.write('psql: error: connection to server was lost\n');
|
|
703
|
+
successResult = EXIT_BADCONN;
|
|
704
|
+
exitRequested = true;
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
};
|
|
709
|
+
const sigintState = { inQuery: false, resetBuf };
|
|
710
|
+
const removeSigint = installSigint(ctx, sigintState);
|
|
711
|
+
// Seed the ENCODING psql var from the server's client_encoding the first
|
|
712
|
+
// time we enter the REPL — subsequent `SET client_encoding = ...` lands
|
|
713
|
+
// back through `refreshConnectionVars` after each query.
|
|
714
|
+
refreshConnectionVars(ctx);
|
|
715
|
+
// Subscribe to async NotificationResponse (LISTEN/NOTIFY) so a `NOTIFY foo`
|
|
716
|
+
// surfaces upstream's `Asynchronous notification "foo" ...` line. The
|
|
717
|
+
// disposer is run in the finally block at exit so we don't leak listeners.
|
|
718
|
+
const removeNotificationHandler = installNotificationHandler(ctx, reader);
|
|
719
|
+
// Subscribe to NoticeResponse so `RAISE NOTICE` / NOTICE DETAIL / `drop
|
|
720
|
+
// cascades` style server notices surface on stderr — matching libpq's
|
|
721
|
+
// default `psql_notice_processor`. Notices arrive synchronously during
|
|
722
|
+
// query execution, so inline `\;`-chain notices land at the right spot.
|
|
723
|
+
const removeNoticeHandler = installNoticeHandler(ctx, reader);
|
|
724
|
+
// Compute the prompt string for the current state. For notty input we emit
|
|
725
|
+
// the empty string so the stream reader doesn't see prompt bytes interleaved
|
|
726
|
+
// with stdout. For TTY input the LineEditor renders the prompt itself.
|
|
727
|
+
const computePrompt = (status) => {
|
|
728
|
+
if (ctx.settings.notty)
|
|
729
|
+
return '';
|
|
730
|
+
const name = queryBuf.trim().length === 0 || status === 'ready'
|
|
731
|
+
? 'PROMPT1'
|
|
732
|
+
: status === 'copy'
|
|
733
|
+
? 'PROMPT3'
|
|
734
|
+
: 'PROMPT2';
|
|
735
|
+
const promptCtx = buildPromptContext(ctx, status, stmtLineNumber);
|
|
736
|
+
return renderPromptByName(name, promptCtx);
|
|
737
|
+
};
|
|
738
|
+
// Resolves a psql variable for `:NAME` substitution in SQL bodies.
|
|
739
|
+
// Backslash command bodies do their own expansion via `scanSlashArgs` in
|
|
740
|
+
// `makeBackslashContext`, so this lookup only fires inside scanSql.
|
|
741
|
+
const sqlVarLookup = (name) => ctx.settings.vars.get(name);
|
|
742
|
+
// Resolves a backslash-command's argument-mode hint so scanSql can
|
|
743
|
+
// consume the rest of the line correctly for whole-line / filepipe
|
|
744
|
+
// commands. Upstream's psqlscanslash.l flips between `<xslasharg>` and
|
|
745
|
+
// `<xslashwholeline>` based on the same hint — without it, the SQL
|
|
746
|
+
// scanner would terminate a `\!` or `\sf` arg at the next `\` (e.g.
|
|
747
|
+
// `\! whole_line \endif` would split into `\!` + `\endif`).
|
|
748
|
+
const slashCmdMode = (cmdName) => {
|
|
749
|
+
const spec = ctx.registry.lookup(cmdName);
|
|
750
|
+
if (!spec)
|
|
751
|
+
return undefined;
|
|
752
|
+
if (spec.argMode === 'whole-line')
|
|
753
|
+
return 'whole-line';
|
|
754
|
+
// Backslash registry currently only distinguishes whole-line vs the
|
|
755
|
+
// default `lex` mode. Filepipe is signalled per-call via
|
|
756
|
+
// `nextArg('filepipe')` inside cmd implementations rather than the
|
|
757
|
+
// spec, so we infer it from a small allow-list of commands that
|
|
758
|
+
// upstream declares as `OT_FILEPIPE` (`\w` and `\o`). Without this,
|
|
759
|
+
// `\w |/no/such/file \else` would split off `\else` as a separate
|
|
760
|
+
// command instead of capturing it as the file's whole-line arg.
|
|
761
|
+
if (cmdName === 'w' || cmdName === 'o')
|
|
762
|
+
return 'filepipe';
|
|
763
|
+
return undefined;
|
|
764
|
+
};
|
|
765
|
+
/**
|
|
766
|
+
* Strip block / line comments cheaply before scanning so a COPY-shaped
|
|
767
|
+
* comment doesn't trigger pre-buffering or sink wiring.
|
|
768
|
+
*/
|
|
769
|
+
const stripSqlComments = (sql) => sql.replace(/\/\*[\s\S]*?\*\//g, '').replace(/--[^\n]*/g, '');
|
|
770
|
+
/**
|
|
771
|
+
* Count the number of `COPY ... FROM STDIN` segments in `sql`. Upstream
|
|
772
|
+
* `handleCopyIn` in copy.c is invoked for each one that hits the wire as
|
|
773
|
+
* a `\;`-chained simple-query batch; the mainloop must pre-buffer the
|
|
774
|
+
* `\.`-terminated data block per occurrence and hand them to the wire
|
|
775
|
+
* layer before dispatch so CopyInResponse is satisfied without a
|
|
776
|
+
* blocking callback into the REPL.
|
|
777
|
+
*
|
|
778
|
+
* The regex tolerates the optional column list (`COPY t (a, b)`) and the
|
|
779
|
+
* format clause (`COPY t FROM STDIN WITH (...)` / `... CSV ...`). False
|
|
780
|
+
* positives inside string literals / comments are possible but extremely
|
|
781
|
+
* rare in scripted workloads — and a false positive only over-consumes
|
|
782
|
+
* lines from the input, which is recoverable. The conservative regex
|
|
783
|
+
* here matches upstream `psql`'s scanner heuristics closely enough for
|
|
784
|
+
* the conformance suite (`psql.sql` lines around 1467-1476).
|
|
785
|
+
*/
|
|
786
|
+
const countCopyFromStdin = (sql) => {
|
|
787
|
+
const stripped = stripSqlComments(sql);
|
|
788
|
+
const re = /\bCOPY\b[\s\S]*?\bFROM\s+STDIN\b/giu;
|
|
789
|
+
let n = 0;
|
|
790
|
+
while (re.exec(stripped) !== null)
|
|
791
|
+
n += 1;
|
|
792
|
+
return n;
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* `true` when `sql` contains at least one `COPY ... TO STDOUT` segment.
|
|
796
|
+
* The wire layer routes mid-batch CopyData into our `copyOutMidBatchSink`
|
|
797
|
+
* when it's set; we install one for the duration of a batch that mentions
|
|
798
|
+
* `TO STDOUT` so the bytes land on the active output stream.
|
|
799
|
+
*/
|
|
800
|
+
const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlComments(sql));
|
|
801
|
+
/**
|
|
802
|
+
* Read one COPY-FROM-STDIN data block: consume lines from the reader
|
|
803
|
+
* until a bare `\.` arrives (or EOF / null). Returns the concatenated
|
|
804
|
+
* payload as a Buffer with trailing newlines preserved (the wire side
|
|
805
|
+
* sends the bytes verbatim; the server treats them as the COPY input).
|
|
806
|
+
* The `\.` terminator itself is NOT included.
|
|
807
|
+
*/
|
|
808
|
+
const readCopyDataBlock = async () => {
|
|
809
|
+
const lines = [];
|
|
810
|
+
for (;;) {
|
|
811
|
+
const line = await reader.readLine('');
|
|
812
|
+
if (line === null)
|
|
813
|
+
break;
|
|
814
|
+
if (line.replace(/\s+$/, '') === '\\.')
|
|
815
|
+
break;
|
|
816
|
+
lines.push(line);
|
|
817
|
+
// Upstream `handleCopyIn` in copy.c reads COPY data lines straight
|
|
818
|
+
// off `copystream` with `fgets` and ships them to the server via
|
|
819
|
+
// `PQputCopyData` — there is no `--echo-all` branch on this path.
|
|
820
|
+
// Suppressing the echo here keeps the COPY-FROM-STDIN data out of
|
|
821
|
+
// the echo stream, matching vanilla: only the surrounding SQL
|
|
822
|
+
// statement (`COPY ... FROM STDIN`) lands in stdout, not its
|
|
823
|
+
// payload.
|
|
824
|
+
}
|
|
825
|
+
// Each line plus a trailing newline — matches the byte stream COPY
|
|
826
|
+
// expects on its input side.
|
|
827
|
+
const text = lines.length === 0 ? '' : lines.join('\n') + '\n';
|
|
828
|
+
return Buffer.from(text, 'utf8');
|
|
829
|
+
};
|
|
830
|
+
/**
|
|
831
|
+
* Process the assembled queryBuf+line through scanSql, dispatching the
|
|
832
|
+
* boundaries it finds. Returns when we hit `incomplete`/`eof` and need
|
|
833
|
+
* the next input line.
|
|
834
|
+
*/
|
|
835
|
+
const processChunk = async (chunk) => {
|
|
836
|
+
let working = chunk;
|
|
837
|
+
while (working.length > 0) {
|
|
838
|
+
// SINGLELINE (-S / `\set SINGLELINE on`): the scanner treats a top-level
|
|
839
|
+
// newline as an implicit `;`, so each input line dispatches on its own.
|
|
840
|
+
// Read the flag fresh each pass — `\set SINGLELINE` can flip it mid-REPL.
|
|
841
|
+
const result = scanSql(working, scanState, sqlVarLookup, slashCmdMode, {
|
|
842
|
+
singleline: ctx.settings.singleline,
|
|
843
|
+
});
|
|
844
|
+
scanState = result.nextState;
|
|
845
|
+
if (result.kind === 'semicolon') {
|
|
846
|
+
// Use the substituted `result.sql` so `:NAME` references already
|
|
847
|
+
// resolved at scan time make it into the executed SQL.
|
|
848
|
+
const sqlText = queryBuf + result.sql;
|
|
849
|
+
queryBuf = '';
|
|
850
|
+
working = working.slice(result.consumed);
|
|
851
|
+
scanState = initialScanState();
|
|
852
|
+
stmtLineNumber = 1;
|
|
853
|
+
if (!ctx.cond.isActive()) {
|
|
854
|
+
// Suppressed: discard, no execution, no error.
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
// COPY ... FROM STDIN appearing as a segment of a `\;`-chained
|
|
858
|
+
// batch needs its CopyInResponse satisfied with the COPY data
|
|
859
|
+
// block(s) that follow on stdin. We pre-buffer one block per
|
|
860
|
+
// detected `FROM STDIN` occurrence and hand the bytes to the wire
|
|
861
|
+
// layer before dispatch. Mirrors upstream `handleCopyIn` in
|
|
862
|
+
// copy.c — except we pump the bytes up-front instead of via a
|
|
863
|
+
// callback into the REPL when CopyInResponse arrives.
|
|
864
|
+
const copyCount = ctx.settings.db ? countCopyFromStdin(sqlText) : 0;
|
|
865
|
+
const wantsCopyOut = ctx.settings.db !== undefined && hasCopyToStdout(sqlText);
|
|
866
|
+
if (copyCount > 0 && ctx.settings.db) {
|
|
867
|
+
// The Connection type doesn't expose `queueCopyInData` (kept
|
|
868
|
+
// off the frozen interface), but the concrete PgConnection
|
|
869
|
+
// does. We duck-type the method to avoid coupling here.
|
|
870
|
+
const conn = ctx.settings.db;
|
|
871
|
+
if (typeof conn.queueCopyInData === 'function') {
|
|
872
|
+
// Drop any leftover buffers from a previous (failed) batch so
|
|
873
|
+
// we don't accidentally re-use stale data.
|
|
874
|
+
conn.clearCopyInDataQueue?.();
|
|
875
|
+
for (let i = 0; i < copyCount; i += 1) {
|
|
876
|
+
const block = await readCopyDataBlock();
|
|
877
|
+
conn.queueCopyInData(block);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (wantsCopyOut && ctx.settings.db) {
|
|
882
|
+
// Wire a sink so the wire layer can forward mid-batch CopyData
|
|
883
|
+
// bytes verbatim (matching `handleCopyOut`). Routes to the
|
|
884
|
+
// active query output (`\o FILE` stashed stream when set,
|
|
885
|
+
// otherwise `ctx.stdout`) — upstream `handleCopyOut` sinks the
|
|
886
|
+
// bytes into `pset.queryFout`, which is whatever `\o` last
|
|
887
|
+
// pointed at. Bytes already include trailing newlines on each
|
|
888
|
+
// row, so we pass them through unchanged.
|
|
889
|
+
const conn = ctx.settings.db;
|
|
890
|
+
const out = pickOut(ctx);
|
|
891
|
+
conn.copyOutMidBatchSink = (chunk) => {
|
|
892
|
+
out.write(chunk);
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
sigintState.inQuery = true;
|
|
896
|
+
const ok = await dispatchSendQuery(ctx, sqlText);
|
|
897
|
+
sigintState.inQuery = false;
|
|
898
|
+
// Always clear any leftover queued blocks once the batch settles
|
|
899
|
+
// (success or failure) so the next dispatch starts fresh.
|
|
900
|
+
if (copyCount > 0 && ctx.settings.db) {
|
|
901
|
+
const conn = ctx.settings.db;
|
|
902
|
+
conn.clearCopyInDataQueue?.();
|
|
903
|
+
}
|
|
904
|
+
if (wantsCopyOut && ctx.settings.db) {
|
|
905
|
+
const conn = ctx.settings.db;
|
|
906
|
+
conn.copyOutMidBatchSink = null;
|
|
907
|
+
}
|
|
908
|
+
// After any SQL statement, the server may have closed the connection
|
|
909
|
+
// (e.g. pg_terminate_backend on our own pid). Surface that once and
|
|
910
|
+
// halt — psql cannot recover from a lost connection mid-script.
|
|
911
|
+
if (checkConnectionLost())
|
|
912
|
+
return;
|
|
913
|
+
if (!ok && ctx.settings.onErrorStop) {
|
|
914
|
+
successResult = EXIT_USER;
|
|
915
|
+
exitRequested = true;
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (result.kind === 'backslash') {
|
|
921
|
+
// Fold buffered SQL accumulated before the backslash into queryBuf.
|
|
922
|
+
// `result.sql` carries the (possibly empty) text that preceded the
|
|
923
|
+
// backslash in this scan pass — empty when the backslash was at the
|
|
924
|
+
// top of the buffer, non-empty for shapes like
|
|
925
|
+
// `SELECT 1 \watch c=3` or `SELECT error\gdesc`. Buffer-consuming
|
|
926
|
+
// commands (\g, \gx, \gset, \gexec, \gdesc, \crosstabview, \watch,
|
|
927
|
+
// \bind) will read this through `BackslashContext.queryBuf` and
|
|
928
|
+
// return `reset-buf` to clear it; commands that don't care leave it
|
|
929
|
+
// intact for the next dispatch.
|
|
930
|
+
//
|
|
931
|
+
// Track whether this scan started cleanly: empty queryBuf and the
|
|
932
|
+
// backslash was at the head of `working`. In that case the slash
|
|
933
|
+
// command is the ENTIRE source line — the trailing `\n` left in
|
|
934
|
+
// `working` after the slice is just the line terminator, not an
|
|
935
|
+
// inter-line continuation separator. We need to drop it after
|
|
936
|
+
// dispatch so the next chunk's scanSql doesn't return an `eof`
|
|
937
|
+
// with `sql: '\n'` and accumulate a stray leading newline into
|
|
938
|
+
// the NEXT statement's queryBuf. Mirrors upstream `MainLoop()`'s
|
|
939
|
+
// `query_buf->len == added_nl_pos` strip (mainloop.c lines
|
|
940
|
+
// 480-484): when a line contains only a backslash command and
|
|
941
|
+
// the scanner added nothing to the buffer, the appended `\n` is
|
|
942
|
+
// taken back off so the buffer's `LINE N:` counting matches the
|
|
943
|
+
// user's mental model.
|
|
944
|
+
const slashOnlyLine = result.sql.length === 0 && queryBuf.length === 0;
|
|
945
|
+
queryBuf += result.sql;
|
|
946
|
+
working = working.slice(result.consumed);
|
|
947
|
+
const cmdName = result.cmd;
|
|
948
|
+
// Cond commands run unconditionally; everything else respects
|
|
949
|
+
// cond.isActive().
|
|
950
|
+
if (COND_COMMAND_NAMES.has(cmdName)) {
|
|
951
|
+
// Snapshot scanState BEFORE the cond command runs — `\if` will
|
|
952
|
+
// want the snapshot taken at its dispatch point (matches upstream
|
|
953
|
+
// `save_query_text_state` capturing the scanner's input lexer
|
|
954
|
+
// state). Cheap shallow copy: ScanState fields are primitives or
|
|
955
|
+
// small immutable objects.
|
|
956
|
+
const scanStateBefore = { ...scanState };
|
|
957
|
+
const r = await dispatchCondCommand(ctx, cmdName, result.rest, queryBuf);
|
|
958
|
+
if (r.handled && r.result?.status === 'exit') {
|
|
959
|
+
exitRequested = true;
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
// Cond commands implement upstream's `discard_query_text` via
|
|
963
|
+
// the `truncateBufTo` field: when `\elif`/`\else`/`\endif`
|
|
964
|
+
// leaves an INACTIVE branch, the SQL text the skipped branch
|
|
965
|
+
// accumulated is rolled back to the snapshot captured at the
|
|
966
|
+
// matching `\if`/`\elif`/`\else`. We also restore the scanner
|
|
967
|
+
// state captured at that checkpoint — without this a `(` inside
|
|
968
|
+
// the skipped branch would leave `parenDepth > 0` and the next
|
|
969
|
+
// `;` wouldn't trigger a dispatch boundary. Stmt line number
|
|
970
|
+
// stays as-is: the surrounding statement is still in flight.
|
|
971
|
+
if (r.handled && r.result?.truncateBufTo !== undefined) {
|
|
972
|
+
const target = r.result.truncateBufTo;
|
|
973
|
+
if (target < queryBuf.length) {
|
|
974
|
+
queryBuf = queryBuf.slice(0, target);
|
|
975
|
+
}
|
|
976
|
+
const savedScan = condScanStateStack[condScanStateStack.length - 1];
|
|
977
|
+
if (savedScan !== undefined) {
|
|
978
|
+
scanState = { ...savedScan };
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// Sync the parallel scan-state stack with whatever cond.push /
|
|
982
|
+
// cond.pop / cond.setSavedQueryBufLen the command performed.
|
|
983
|
+
// `\if` → push the scan state captured at dispatch.
|
|
984
|
+
// `\elif`/`\else` → replace the top entry with the scan state
|
|
985
|
+
// that prevails AFTER any truncate-on-leaving-
|
|
986
|
+
// inactive restoration (so the new branch
|
|
987
|
+
// starts from a clean checkpoint).
|
|
988
|
+
// `\endif` → pop the top entry.
|
|
989
|
+
if (cmdName === 'if') {
|
|
990
|
+
condScanStateStack.push(scanStateBefore);
|
|
991
|
+
}
|
|
992
|
+
else if (cmdName === 'elif' || cmdName === 'else') {
|
|
993
|
+
// Errors leave the top untouched (cond.setState not called on
|
|
994
|
+
// the no-matching/double-else paths). Only re-anchor when the
|
|
995
|
+
// command succeeded — `status: 'ok'` covers both the active
|
|
996
|
+
// and truncated paths.
|
|
997
|
+
if (condScanStateStack.length > 0 && r.result?.status === 'ok') {
|
|
998
|
+
condScanStateStack[condScanStateStack.length - 1] = {
|
|
999
|
+
...scanState,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else if (cmdName === 'endif') {
|
|
1004
|
+
// Pop only on success — `\endif` with no matching `\if`
|
|
1005
|
+
// returns an error and doesn't actually pop the cond frame.
|
|
1006
|
+
if (r.result?.status === 'ok') {
|
|
1007
|
+
condScanStateStack.pop();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
// Note: we intentionally do NOT update `lastWasError` for cond
|
|
1011
|
+
// errors. Upstream psql exits 0 from a script whose only failure
|
|
1012
|
+
// was `\endif: no matching \if` (or any other cond diagnostic) —
|
|
1013
|
+
// these are printed and the loop continues, but they don't taint
|
|
1014
|
+
// the terminal `lastWasError → EXIT_USER` escalation. Only
|
|
1015
|
+
// ON_ERROR_STOP can escalate cond failures.
|
|
1016
|
+
if (r.handled &&
|
|
1017
|
+
r.result?.status === 'error' &&
|
|
1018
|
+
ctx.settings.onErrorStop) {
|
|
1019
|
+
successResult = EXIT_USER;
|
|
1020
|
+
exitRequested = true;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
// Upstream `HandleSlashCmds` looks up the command name BEFORE
|
|
1026
|
+
// consulting the conditional stack: an unknown name emits
|
|
1027
|
+
// `invalid command \X` to stderr regardless of branch state.
|
|
1028
|
+
// Without this, e.g. `\if false \n \lo \n \endif` silently passes
|
|
1029
|
+
// `\lo` through, but vanilla surfaces the diagnostic. Looking the
|
|
1030
|
+
// command up here also lets us short-circuit the inactive branch
|
|
1031
|
+
// without losing the unknown-command error.
|
|
1032
|
+
if (!ctx.cond.isActive()) {
|
|
1033
|
+
if (ctx.registry.lookup(cmdName) === undefined) {
|
|
1034
|
+
// Bare wording (no `psql: ERROR:` prefix) — vanilla's
|
|
1035
|
+
// `psql_log_pre_callback` short-circuits when input has no
|
|
1036
|
+
// line-number context (stdin pipe path), so the diagnostic
|
|
1037
|
+
// is emitted via `pg_log_error_internal` as a raw string.
|
|
1038
|
+
// Matches the expected output's `invalid command \lo` shape
|
|
1039
|
+
// at psql.out:4698. Other writeError() call sites keep the
|
|
1040
|
+
// prefix because they're errors WE emit from a known
|
|
1041
|
+
// dispatch path, not from the unknown-command lookup miss.
|
|
1042
|
+
ctx.stderr.write(`invalid command \\${cmdName}\n`);
|
|
1043
|
+
// Errors emitted in an inactive branch must NOT taint
|
|
1044
|
+
// `lastWasError` (vanilla exits 0 from `\if false; \lo; \endif`)
|
|
1045
|
+
// and must NOT trigger ON_ERROR_STOP. The diagnostic stands
|
|
1046
|
+
// alone; the loop continues to the next chunk.
|
|
1047
|
+
}
|
|
1048
|
+
// Skip non-cond commands inside an inactive branch (run or not,
|
|
1049
|
+
// registered or not).
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const bres = await dispatchRegisteredCommand(ctx, cmdName, result.rest, queryBuf);
|
|
1053
|
+
if (bres?.status === 'exit') {
|
|
1054
|
+
exitRequested = true;
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (bres?.status === 'reset-buf') {
|
|
1058
|
+
queryBuf = bres.newBuf ?? '';
|
|
1059
|
+
scanState = initialScanState();
|
|
1060
|
+
stmtLineNumber = 1;
|
|
1061
|
+
// The SQL scanner intentionally stops the backslash boundary on
|
|
1062
|
+
// (not past) the trailing line terminator so that an inter-line
|
|
1063
|
+
// `\n` separating a slash command from continuing SQL on the
|
|
1064
|
+
// next line survives in `working`. That's the right call when
|
|
1065
|
+
// the slash command leaves `queryBuf` intact — the `\n` keeps
|
|
1066
|
+
// line breaks in the assembled multi-line query.
|
|
1067
|
+
//
|
|
1068
|
+
// For `reset-buf`, however, the buffer is being intentionally
|
|
1069
|
+
// dropped: the slash command (`\g`, `\gset`, `\gdesc`, `\gexec`,
|
|
1070
|
+
// `\crosstabview`, `\watch`, `\bind`, `\parse`, …) has just
|
|
1071
|
+
// consumed and dispatched whatever was buffered. A residual
|
|
1072
|
+
// `\n` at the head of `working` is then leftover line-terminator
|
|
1073
|
+
// bytes from the slash-command line itself — NOT a continuation
|
|
1074
|
+
// separator. If we let it survive, the next scanSql pass returns
|
|
1075
|
+
// an `eof` with `sql: '\n'`, the loop's
|
|
1076
|
+
// `queryBuf += result.sql` line folds it into the NEXT
|
|
1077
|
+
// statement's buffer, and commands that store the buffer
|
|
1078
|
+
// verbatim (notably `\parse`, which uses the buffer text as the
|
|
1079
|
+
// prepared-statement source) emit a stray leading 0x0a byte.
|
|
1080
|
+
//
|
|
1081
|
+
// Strip the line terminator here so the next pass starts cleanly.
|
|
1082
|
+
// This matches upstream `psql_scan_slash_command_end()`'s eat-
|
|
1083
|
+
// through-newline behaviour for the buffer-reset case — without
|
|
1084
|
+
// changing the scanner's semantics for the inline-slash + multi-
|
|
1085
|
+
// line shape that depends on the `\n` surviving.
|
|
1086
|
+
if (working.startsWith('\r\n')) {
|
|
1087
|
+
working = working.slice(2);
|
|
1088
|
+
}
|
|
1089
|
+
else if (working.startsWith('\n') || working.startsWith('\r')) {
|
|
1090
|
+
working = working.slice(1);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// For status='ok' (the buffer was NOT consumed by the slash command),
|
|
1094
|
+
// also drop the `\n` left in `working` when the slash command was
|
|
1095
|
+
// the sole content of this source line. Upstream's
|
|
1096
|
+
// `query_buf->len == added_nl_pos` strip (mainloop.c lines 480-484)
|
|
1097
|
+
// covers the same shape: a line whose only token is a slash command
|
|
1098
|
+
// doesn't contribute a `\n` to `query_buf`. Without this, e.g.
|
|
1099
|
+
// \set ECHO errors
|
|
1100
|
+
// SELECT * FROM bad;
|
|
1101
|
+
// would assemble the SELECT's queryBuf as `\n` + `SELECT...` —
|
|
1102
|
+
// shifting the server's `LINE N` count by one and contaminating
|
|
1103
|
+
// the `STATEMENT: ...` echo emitted on error.
|
|
1104
|
+
if (bres?.status !== 'reset-buf' && slashOnlyLine) {
|
|
1105
|
+
if (working.startsWith('\r\n')) {
|
|
1106
|
+
working = working.slice(2);
|
|
1107
|
+
}
|
|
1108
|
+
else if (working.startsWith('\n') || working.startsWith('\r')) {
|
|
1109
|
+
working = working.slice(1);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// Upstream `mainloop.c`: on PSQL_CMD_ERROR, the query buffer is
|
|
1113
|
+
// reset and the scanner state is dropped. Mirrors `resetPQExpBuffer`
|
|
1114
|
+
// + `psql_scan_reset`. Without this, a buffer-consuming command
|
|
1115
|
+
// that fails (e.g. `SELECT 1 \watch 1 1` rejecting duplicate
|
|
1116
|
+
// positional intervals) would leave `SELECT 1 ` in the buffer for
|
|
1117
|
+
// the next prompt — and in notty mode the tail dispatch would
|
|
1118
|
+
// execute it, masking the failure exit code.
|
|
1119
|
+
//
|
|
1120
|
+
// Upstream `HandleSlashCmds` additionally silently discards the
|
|
1121
|
+
// remainder of the current line via `psql_scan_slash_option(scan_state,
|
|
1122
|
+
// OT_WHOLE_LINE, …)` when a backslash command returns PSQL_CMD_ERROR.
|
|
1123
|
+
// Mirror that here by dropping `working` up to and including the next
|
|
1124
|
+
// newline. Without this, `\bind_named NAME 1 2 \gset pref02_ \echo X`
|
|
1125
|
+
// would still execute `\echo X` after the pipeline-mode `\gset`
|
|
1126
|
+
// rejection — vanilla suppresses it.
|
|
1127
|
+
if (bres?.status === 'error') {
|
|
1128
|
+
queryBuf = '';
|
|
1129
|
+
scanState = initialScanState();
|
|
1130
|
+
stmtLineNumber = 1;
|
|
1131
|
+
// Discard any trailing content on the SAME physical line — but NOT
|
|
1132
|
+
// the rest of the script. The scanner consumes a slash command's
|
|
1133
|
+
// args but typically leaves the line terminator (`\n`) at the head
|
|
1134
|
+
// of `working`. If `working` starts with `\n` / `\r\n`, the failed
|
|
1135
|
+
// command was already at end-of-line — just drop the terminator
|
|
1136
|
+
// and let the next line dispatch normally. If `working` has
|
|
1137
|
+
// non-newline chars before the next `\n`, drop up to and including
|
|
1138
|
+
// that `\n` (mirrors upstream `HandleSlashCmds`' `OT_WHOLE_LINE`
|
|
1139
|
+
// discard). Without this branch a stack of `\gdesc\n\gdesc\n…`
|
|
1140
|
+
// lines collapses to a single dispatched `\gdesc` because the
|
|
1141
|
+
// first discard ate the second line.
|
|
1142
|
+
if (working.startsWith('\r\n')) {
|
|
1143
|
+
working = working.slice(2);
|
|
1144
|
+
}
|
|
1145
|
+
else if (working.startsWith('\n') || working.startsWith('\r')) {
|
|
1146
|
+
working = working.slice(1);
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
const nlIdx = working.indexOf('\n');
|
|
1150
|
+
working = nlIdx === -1 ? '' : working.slice(nlIdx + 1);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// Backslash commands like \connect can also tear down the connection.
|
|
1154
|
+
if (checkConnectionLost())
|
|
1155
|
+
return;
|
|
1156
|
+
if (bres?.status === 'error' && ctx.settings.onErrorStop) {
|
|
1157
|
+
successResult = EXIT_USER;
|
|
1158
|
+
exitRequested = true;
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
// incomplete or eof — keep accumulating. Use the substituted
|
|
1164
|
+
// `result.sql` so `:NAME` tokens that were fully consumed in this
|
|
1165
|
+
// chunk land in the buffer in expanded form. (A `:NAME` that
|
|
1166
|
+
// straddles two chunks falls back to the literal — a corner case
|
|
1167
|
+
// upstream also handles only when the variable name fits inside the
|
|
1168
|
+
// current buffer; the line-reader feeds whole lines so this is
|
|
1169
|
+
// effectively unreachable in interactive use.)
|
|
1170
|
+
queryBuf += result.sql;
|
|
1171
|
+
working = '';
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
// -----------------------------------------------------------------------
|
|
1176
|
+
// Read loop. Each iteration:
|
|
1177
|
+
// 1. Drain any pending input enqueued by `\i FILE` (WP-15) — those lines
|
|
1178
|
+
// take precedence over fresh stdin so the include behaves as a
|
|
1179
|
+
// prepend on the input source.
|
|
1180
|
+
// 2. Otherwise, ask the reader for the next line. For notty input this
|
|
1181
|
+
// is a `readline` stream; for TTY input it's the LineEditor with
|
|
1182
|
+
// emacs keybindings + persistent history (WP-24 + WP-25).
|
|
1183
|
+
// 3. Each submitted line is recorded in history.
|
|
1184
|
+
// -----------------------------------------------------------------------
|
|
1185
|
+
try {
|
|
1186
|
+
while (!exitRequested) {
|
|
1187
|
+
// Prompt status drives `%R`. When the query buffer holds an incomplete
|
|
1188
|
+
// statement but the scanner isn't inside any special context (paren,
|
|
1189
|
+
// comment, quoted-string), it still reports `'ready'`; map that to
|
|
1190
|
+
// `'continue'` so PROMPT2 renders `-` instead of `=`. A whitespace-only
|
|
1191
|
+
// residue (e.g. a trailing `\n` left over after a `;` boundary) counts
|
|
1192
|
+
// as empty so the next prompt is PROMPT1 not PROMPT2.
|
|
1193
|
+
const status = queryBuf.trim().length === 0
|
|
1194
|
+
? 'ready'
|
|
1195
|
+
: scanState.promptStatus === 'ready'
|
|
1196
|
+
? 'continue'
|
|
1197
|
+
: scanState.promptStatus;
|
|
1198
|
+
const prompt = computePrompt(status);
|
|
1199
|
+
// 1. Pending input from \i: process as a single chunk and loop again.
|
|
1200
|
+
const queued = consumeQueuedInput();
|
|
1201
|
+
if (queued !== null) {
|
|
1202
|
+
await processChunk(queued.endsWith('\n') ? queued : queued + '\n');
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
// 2. Read the next line from stdin / line editor.
|
|
1206
|
+
let line;
|
|
1207
|
+
try {
|
|
1208
|
+
line = await reader.readLine(prompt);
|
|
1209
|
+
}
|
|
1210
|
+
catch (err) {
|
|
1211
|
+
// SignalError (Ctrl-C on an interactive line) — drop the partial
|
|
1212
|
+
// buffer and re-prompt, matching upstream psql.
|
|
1213
|
+
if (err.name === 'SignalError') {
|
|
1214
|
+
resetBuf();
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
throw err;
|
|
1218
|
+
}
|
|
1219
|
+
if (line === null)
|
|
1220
|
+
break; // EOF
|
|
1221
|
+
// Upstream `mainloop.c` MainLoop():
|
|
1222
|
+
//
|
|
1223
|
+
// if (line[0] == '\0' && !psql_scan_in_quote(scan_state))
|
|
1224
|
+
// {
|
|
1225
|
+
// free(line);
|
|
1226
|
+
// continue;
|
|
1227
|
+
// }
|
|
1228
|
+
//
|
|
1229
|
+
// I.e., bare-empty lines are skipped entirely (no echo, no scanner
|
|
1230
|
+
// pass) UNLESS the scanner is mid-quote (single-, double-, dollar-,
|
|
1231
|
+
// or block-comment continuation). Inside a quote we keep the empty
|
|
1232
|
+
// line so it lands in the assembled query buffer (e.g. a quoted
|
|
1233
|
+
// identifier `"ab\n\nc"` spans multiple input lines including blanks),
|
|
1234
|
+
// and `--echo-all` surfaces it so the echo stream tracks the source
|
|
1235
|
+
// verbatim.
|
|
1236
|
+
//
|
|
1237
|
+
// `psql_scan_in_quote` returns true for all start_states except
|
|
1238
|
+
// INITIAL and xqs — we approximate with the scanner-state fields
|
|
1239
|
+
// that track each quoted construct. `parenDepth` is intentionally
|
|
1240
|
+
// omitted (upstream doesn't count it as in-quote).
|
|
1241
|
+
const scanInQuote = scanState.inBlockComment > 0 ||
|
|
1242
|
+
scanState.inSingleQuote ||
|
|
1243
|
+
scanState.inDoubleQuote ||
|
|
1244
|
+
scanState.dollarTag !== null;
|
|
1245
|
+
if (line.length === 0 && !scanInQuote) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
// 2'. ECHO=all — upstream `--echo-all` / `\set ECHO all` echoes every
|
|
1249
|
+
// input line to stdout *before* it's processed. Blank lines outside a
|
|
1250
|
+
// quote already short-circuited above, so a blank reaching here means
|
|
1251
|
+
// the scanner is mid-quote and the line is part of the assembled
|
|
1252
|
+
// statement. ECHO=queries echoes only completed queries — handled
|
|
1253
|
+
// separately by the exec path.
|
|
1254
|
+
if (ctx.settings.echo === 'all') {
|
|
1255
|
+
ctx.stdout.write(line + '\n');
|
|
1256
|
+
}
|
|
1257
|
+
// 2a. `exit`/`quit` keyword handling.
|
|
1258
|
+
//
|
|
1259
|
+
// - Empty buffer → exit the REPL.
|
|
1260
|
+
// - Non-empty buf → print "Use \\q to quit." hint and continue
|
|
1261
|
+
// (buffer is preserved so the user can resume editing).
|
|
1262
|
+
//
|
|
1263
|
+
// The buffer may carry whitespace from a prior line's tail, so we
|
|
1264
|
+
// trim before checking.
|
|
1265
|
+
// Bare `quit`/`exit` (and `help` below) are an INTERACTIVE-only
|
|
1266
|
+
// convenience — upstream gates them on `cur_cmd_interactive`. In a
|
|
1267
|
+
// non-interactive script (`printf 'quit;\n' | psql`) they must fall
|
|
1268
|
+
// through and be sent to the server as SQL (syntax error → exit 3),
|
|
1269
|
+
// not silently exit 0.
|
|
1270
|
+
if (!ctx.settings.notty && isQuitKeyword(line)) {
|
|
1271
|
+
if (queryBuf.trim().length === 0) {
|
|
1272
|
+
reader.pushHistory(line);
|
|
1273
|
+
exitRequested = true;
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
ctx.stdout.write('Use \\q to quit.\n');
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
// 2b. `help` keyword handling, same shape.
|
|
1280
|
+
//
|
|
1281
|
+
// - Empty buffer → print the help text, continue.
|
|
1282
|
+
// - Non-empty buf → print "Use \\? for help." hint, continue.
|
|
1283
|
+
if (!ctx.settings.notty && isHelpKeyword(line)) {
|
|
1284
|
+
if (queryBuf.trim().length === 0) {
|
|
1285
|
+
reader.pushHistory(line);
|
|
1286
|
+
ctx.stdout.write(HELP_TEXT);
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
ctx.stdout.write('Use \\? for help.\n');
|
|
1290
|
+
}
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
// 3. Push to history once we have a complete submitted line (only
|
|
1294
|
+
// when there's something non-blank to record).
|
|
1295
|
+
reader.pushHistory(line);
|
|
1296
|
+
await processChunk(line + '\n');
|
|
1297
|
+
}
|
|
1298
|
+
// EOF: if there's a residual non-empty buffer in non-interactive mode,
|
|
1299
|
+
// dispatch it (mirroring upstream's tail-of-MainLoop block). For
|
|
1300
|
+
// interactive mode upstream skips this; we match the behaviour. We also
|
|
1301
|
+
// require the buffer to contain non-whitespace SQL — trailing blanks
|
|
1302
|
+
// between statement boundaries and EOF should not produce an empty
|
|
1303
|
+
// execSimple call.
|
|
1304
|
+
if (!exitRequested &&
|
|
1305
|
+
queryBuf.trim().length > 0 &&
|
|
1306
|
+
ctx.settings.notty &&
|
|
1307
|
+
successResult === EXIT_SUCCESS) {
|
|
1308
|
+
if (ctx.cond.isActive()) {
|
|
1309
|
+
sigintState.inQuery = true;
|
|
1310
|
+
const ok = await dispatchSendQuery(ctx, queryBuf);
|
|
1311
|
+
sigintState.inQuery = false;
|
|
1312
|
+
if (ctx.settings.db?.isClosed()) {
|
|
1313
|
+
ctx.stderr.write('psql: error: connection to server was lost\n');
|
|
1314
|
+
successResult = EXIT_BADCONN;
|
|
1315
|
+
}
|
|
1316
|
+
else if (!ok && ctx.settings.onErrorStop) {
|
|
1317
|
+
successResult = EXIT_USER;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
queryBuf = '';
|
|
1321
|
+
}
|
|
1322
|
+
// Warn about unbalanced \if blocks (psql's tail-of-MainLoop check).
|
|
1323
|
+
if (!exitRequested && ctx.cond.depth() > 0) {
|
|
1324
|
+
writeError(ctx, 'reached EOF without finding closing \\endif(s)');
|
|
1325
|
+
if (ctx.settings.onErrorStop && ctx.settings.notty) {
|
|
1326
|
+
successResult = EXIT_USER;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// NOTE: we deliberately do NOT escalate to EXIT_USER just because the last
|
|
1330
|
+
// statement errored. Real psql exits 0 from piped stdin / `-f` even when a
|
|
1331
|
+
// statement failed, UNLESS ON_ERROR_STOP is set — and that case is already
|
|
1332
|
+
// handled by the per-statement `successResult = EXIT_USER` paths above
|
|
1333
|
+
// (verified on psql 18.4: `printf 'SELECT 1;\nSELECT 1/0;\n' | psql` → 0).
|
|
1334
|
+
}
|
|
1335
|
+
finally {
|
|
1336
|
+
await reader.close();
|
|
1337
|
+
removeSigint();
|
|
1338
|
+
if (removeNotificationHandler)
|
|
1339
|
+
removeNotificationHandler();
|
|
1340
|
+
if (removeNoticeHandler)
|
|
1341
|
+
removeNoticeHandler();
|
|
1342
|
+
}
|
|
1343
|
+
return successResult;
|
|
1344
|
+
};
|
|
1345
|
+
/**
|
|
1346
|
+
* Test-only surface. Exposes the small VI_MODE helpers so the matching unit
|
|
1347
|
+
* tests can exercise the parse / translate logic without engaging the
|
|
1348
|
+
* raw-mode LineEditor. Treated as private — callers should not rely on it.
|
|
1349
|
+
*/
|
|
1350
|
+
export const __testing = {
|
|
1351
|
+
parseBoolVar,
|
|
1352
|
+
viModeOption,
|
|
1353
|
+
};
|