neonctl 2.27.1 → 2.29.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 +35 -3
- package/dist/analytics.js +52 -34
- package/dist/api.js +643 -13
- package/dist/auth.js +50 -44
- package/dist/cli.js +8 -1
- package/dist/commands/auth.js +64 -51
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +160 -150
- package/dist/commands/bucket.js +183 -146
- package/dist/commands/checkout.js +51 -51
- package/dist/commands/config.js +228 -82
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +100 -101
- package/dist/commands/databases.js +29 -26
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/dev.js +114 -114
- package/dist/commands/env.js +43 -43
- package/dist/commands/functions.js +101 -104
- package/dist/commands/index.js +27 -25
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +232 -182
- package/dist/commands/neon_auth.js +385 -370
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +103 -101
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +27 -24
- package/dist/commands/schema_diff.js +25 -26
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +5 -5
- package/dist/commands/vpc_endpoints.js +50 -50
- package/dist/config.js +7 -7
- package/dist/config_format.js +5 -5
- package/dist/context.js +37 -14
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +33 -33
- package/dist/dev/functions.js +4 -4
- package/dist/dev/inputs.js +6 -6
- package/dist/dev/runtime.js +25 -25
- package/dist/env.js +14 -14
- package/dist/env_file.js +13 -13
- package/dist/errors.js +68 -5
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +110 -107
- package/dist/log.js +2 -2
- package/dist/parameters.gen.js +14 -14
- package/dist/pkg.js +5 -5
- package/dist/psql/cli.js +4 -2
- package/dist/psql/command/cmd_cond.js +61 -61
- package/dist/psql/command/cmd_connect.js +159 -154
- package/dist/psql/command/cmd_copy.js +107 -97
- package/dist/psql/command/cmd_describe.js +368 -363
- package/dist/psql/command/cmd_format.js +276 -263
- package/dist/psql/command/cmd_io.js +269 -263
- package/dist/psql/command/cmd_lo.js +74 -66
- package/dist/psql/command/cmd_meta.js +148 -148
- package/dist/psql/command/cmd_misc.js +17 -17
- package/dist/psql/command/cmd_pipeline.js +142 -135
- package/dist/psql/command/cmd_restrict.js +25 -25
- package/dist/psql/command/cmd_show.js +183 -168
- package/dist/psql/command/dispatch.js +26 -26
- package/dist/psql/command/shared.js +14 -14
- package/dist/psql/complete/filenames.js +16 -16
- package/dist/psql/complete/index.js +4 -4
- package/dist/psql/complete/matcher.js +33 -32
- package/dist/psql/complete/psqlVars.js +173 -173
- package/dist/psql/complete/queries.js +5 -3
- package/dist/psql/complete/rules.js +900 -863
- package/dist/psql/core/common.js +136 -133
- package/dist/psql/core/help.js +343 -343
- package/dist/psql/core/mainloop.js +160 -153
- package/dist/psql/core/prompt.js +126 -123
- package/dist/psql/core/settings.js +111 -111
- package/dist/psql/core/sqlHelp.js +150 -150
- package/dist/psql/core/startup.js +211 -205
- package/dist/psql/core/syncVars.js +14 -14
- package/dist/psql/core/variables.js +24 -24
- package/dist/psql/describe/formatters.js +302 -289
- package/dist/psql/describe/processNamePattern.js +28 -28
- package/dist/psql/describe/queries.js +656 -651
- package/dist/psql/index.js +436 -411
- package/dist/psql/io/history.js +36 -36
- package/dist/psql/io/input.js +15 -15
- package/dist/psql/io/lineEditor/buffer.js +27 -25
- package/dist/psql/io/lineEditor/complete.js +15 -15
- package/dist/psql/io/lineEditor/filename.js +22 -22
- package/dist/psql/io/lineEditor/index.js +65 -62
- package/dist/psql/io/lineEditor/keymap.js +325 -318
- package/dist/psql/io/lineEditor/vt100.js +60 -60
- package/dist/psql/io/pgpass.js +18 -18
- package/dist/psql/io/pgservice.js +14 -14
- package/dist/psql/io/psqlrc.js +46 -46
- package/dist/psql/print/aligned.js +175 -166
- package/dist/psql/print/asciidoc.js +51 -51
- package/dist/psql/print/crosstab.js +34 -31
- package/dist/psql/print/csv.js +25 -22
- package/dist/psql/print/html.js +54 -54
- package/dist/psql/print/json.js +12 -12
- package/dist/psql/print/latex.js +118 -118
- package/dist/psql/print/pager.js +28 -26
- package/dist/psql/print/troff.js +48 -48
- package/dist/psql/print/unaligned.js +15 -14
- package/dist/psql/print/units.js +17 -17
- package/dist/psql/scanner/slash.js +48 -46
- package/dist/psql/scanner/sql.js +88 -84
- package/dist/psql/scanner/stringutils.js +21 -17
- package/dist/psql/types/index.js +7 -7
- package/dist/psql/types/scanner.js +8 -8
- package/dist/psql/wire/connection.js +341 -327
- package/dist/psql/wire/copy.js +7 -7
- package/dist/psql/wire/pipeline.js +26 -24
- package/dist/psql/wire/protocol.js +102 -102
- package/dist/psql/wire/sasl.js +62 -62
- package/dist/psql/wire/tls.js +79 -73
- package/dist/storage_api.js +22 -23
- package/dist/test_utils/fixtures.js +74 -41
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/branch_notice.js +5 -5
- package/dist/utils/branch_picker.js +26 -26
- package/dist/utils/compute_units.js +4 -4
- package/dist/utils/enrichers.js +28 -16
- package/dist/utils/esbuild.js +28 -28
- package/dist/utils/formats.js +1 -1
- package/dist/utils/middlewares.js +3 -3
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +12 -12
- package/dist/utils/psql.js +30 -30
- package/dist/utils/string.js +2 -2
- package/dist/utils/ui.js +9 -9
- package/dist/utils/zip.js +1 -1
- package/dist/writer.js +17 -17
- package/package.json +10 -12
|
@@ -76,23 +76,23 @@
|
|
|
76
76
|
* that and also stash the message on `settings.lastErrorResult` so the
|
|
77
77
|
* mainloop's `writeError()` wrapper can pick it up.
|
|
78
78
|
*/
|
|
79
|
-
import { spawn } from
|
|
80
|
-
import { promises as fsPromises,
|
|
81
|
-
import
|
|
82
|
-
import
|
|
83
|
-
import {
|
|
84
|
-
import {
|
|
85
|
-
import {
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
89
|
-
import {
|
|
90
|
-
import {
|
|
91
|
-
import {
|
|
92
|
-
import {
|
|
93
|
-
import {
|
|
94
|
-
import { consumeBindState, lookupPrepared, stagedNamedBindPresent, } from
|
|
95
|
-
import {
|
|
79
|
+
import { spawn } from "node:child_process";
|
|
80
|
+
import { closeSync, createWriteStream, promises as fsPromises, fsyncSync, openSync, } from "node:fs";
|
|
81
|
+
import { platform } from "node:os";
|
|
82
|
+
import * as path from "node:path";
|
|
83
|
+
import { captureLastError, refreshErrorVars, stripLeadingCommentsAndWS, } from "../core/common.js";
|
|
84
|
+
import { alignedPrinter } from "../print/aligned.js";
|
|
85
|
+
import { asciidocPrinter } from "../print/asciidoc.js";
|
|
86
|
+
import { csvPrinter } from "../print/csv.js";
|
|
87
|
+
import { htmlPrinter } from "../print/html.js";
|
|
88
|
+
import { jsonPrinter } from "../print/json.js";
|
|
89
|
+
import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
|
|
90
|
+
import { troffMsPrinter } from "../print/troff.js";
|
|
91
|
+
import { unalignedPrinter } from "../print/unaligned.js";
|
|
92
|
+
import { applyPset } from "./cmd_format.js";
|
|
93
|
+
import { formatErrorReport, psqlErrorPrefix } from "./cmd_meta.js";
|
|
94
|
+
import { consumeBindState, lookupPrepared, stagedNamedBindPresent, } from "./cmd_pipeline.js";
|
|
95
|
+
import { writeErr, writeOut } from "./shared.js";
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
97
97
|
// Query-output (queryFout) stash.
|
|
98
98
|
//
|
|
@@ -101,7 +101,7 @@ import { captureLastError, refreshErrorVars, stripLeadingCommentsAndWS, } from '
|
|
|
101
101
|
// at WP-00, so we stash the stream on the settings object via a well-known
|
|
102
102
|
// symbol — the same approach used for the CondStack in cmd_cond.ts.
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
|
-
const QUERY_FOUT_KEY = Symbol.for(
|
|
104
|
+
const QUERY_FOUT_KEY = Symbol.for("neonctl.psql.queryFout");
|
|
105
105
|
/**
|
|
106
106
|
* Return the currently active queryFout stream (or `null` if none).
|
|
107
107
|
* The mainloop is encouraged to call this in lieu of writing directly to
|
|
@@ -152,7 +152,7 @@ const errResult = (ctx, message) => {
|
|
|
152
152
|
// it would also write a `psql: ERROR: <msg>` fallback, producing a stray
|
|
153
153
|
// duplicate that breaks the `\errverbose` ordering check on tests like
|
|
154
154
|
// `SELECT error\gdesc\n\errverbose`.
|
|
155
|
-
return { status:
|
|
155
|
+
return { status: "error", errorWritten: true };
|
|
156
156
|
};
|
|
157
157
|
/**
|
|
158
158
|
* Reject buffer-consuming commands when an extended pipeline is open. Upstream
|
|
@@ -182,7 +182,7 @@ const errResult = (ctx, message) => {
|
|
|
182
182
|
* own). Mirror that with a settings-stashed accumulator keyed off the
|
|
183
183
|
* current pipeline session; reset when the pipeline ends.
|
|
184
184
|
*/
|
|
185
|
-
const PIPELINE_GATE_ERRORS_KEY = Symbol.for(
|
|
185
|
+
const PIPELINE_GATE_ERRORS_KEY = Symbol.for("neonctl.psql.pipelineGateErrors");
|
|
186
186
|
const getGateErrors = (settings) => {
|
|
187
187
|
const s = settings;
|
|
188
188
|
let cur = s[PIPELINE_GATE_ERRORS_KEY];
|
|
@@ -203,10 +203,10 @@ export const clearPipelineGateErrors = (settings) => {
|
|
|
203
203
|
s[PIPELINE_GATE_ERRORS_KEY] = undefined;
|
|
204
204
|
};
|
|
205
205
|
const pipelineGate = (ctx) => {
|
|
206
|
-
if (ctx.settings.sendMode !==
|
|
206
|
+
if (ctx.settings.sendMode !== "extended-pipeline")
|
|
207
207
|
return null;
|
|
208
|
-
const message = ctx.cmdName ===
|
|
209
|
-
?
|
|
208
|
+
const message = ctx.cmdName === "gdesc"
|
|
209
|
+
? "synchronous command execution functions are not allowed in pipeline mode"
|
|
210
210
|
: `\\${ctx.cmdName} not allowed in pipeline mode`;
|
|
211
211
|
ctx.settings.lastErrorResult = { message };
|
|
212
212
|
const prefix = psqlErrorPrefix(ctx.settings);
|
|
@@ -218,7 +218,7 @@ const pipelineGate = (ctx) => {
|
|
|
218
218
|
// the session-scoped error log; other gated commands (`\g`, `\gx`,
|
|
219
219
|
// `\gset`, `\gexec`, `\watch`) emit a single line per invocation
|
|
220
220
|
// and do NOT participate in the accumulator.
|
|
221
|
-
if (ctx.cmdName ===
|
|
221
|
+
if (ctx.cmdName === "gdesc") {
|
|
222
222
|
const log = getGateErrors(ctx.settings);
|
|
223
223
|
log.push(message);
|
|
224
224
|
for (const m of log) {
|
|
@@ -228,7 +228,7 @@ const pipelineGate = (ctx) => {
|
|
|
228
228
|
else {
|
|
229
229
|
writeErr(`${prefix}${message}\n`);
|
|
230
230
|
}
|
|
231
|
-
return { status:
|
|
231
|
+
return { status: "error", errorWritten: true };
|
|
232
232
|
};
|
|
233
233
|
/**
|
|
234
234
|
* Set of psql variables upstream marks as "specially treated" — i.e. names
|
|
@@ -242,28 +242,28 @@ const pipelineGate = (ctx) => {
|
|
|
242
242
|
* for us.
|
|
243
243
|
*/
|
|
244
244
|
const UPSTREAM_SPECIAL_VAR_NAMES = new Set([
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
245
|
+
"AUTOCOMMIT",
|
|
246
|
+
"COMP_KEYWORD_CASE",
|
|
247
|
+
"ECHO",
|
|
248
|
+
"ECHO_HIDDEN",
|
|
249
|
+
"FETCH_COUNT",
|
|
250
|
+
"HIDE_TABLEAM",
|
|
251
|
+
"HIDE_TOAST_COMPRESSION",
|
|
252
|
+
"HISTCONTROL",
|
|
253
|
+
"HISTFILE",
|
|
254
|
+
"HISTSIZE",
|
|
255
|
+
"IGNOREEOF",
|
|
256
|
+
"ON_ERROR_ROLLBACK",
|
|
257
|
+
"ON_ERROR_STOP",
|
|
258
|
+
"PROMPT1",
|
|
259
|
+
"PROMPT2",
|
|
260
|
+
"PROMPT3",
|
|
261
|
+
"QUIET",
|
|
262
|
+
"SHOW_ALL_RESULTS",
|
|
263
|
+
"SHOW_CONTEXT",
|
|
264
|
+
"SINGLELINE",
|
|
265
|
+
"SINGLESTEP",
|
|
266
|
+
"VERBOSITY",
|
|
267
267
|
]);
|
|
268
268
|
/**
|
|
269
269
|
* True when `name` is a psql variable that `\gset` must skip with an
|
|
@@ -273,7 +273,8 @@ const UPSTREAM_SPECIAL_VAR_NAMES = new Set([
|
|
|
273
273
|
* IGNOREEOF that aren't hooked in our settings.ts still match upstream's
|
|
274
274
|
* `\gset` behaviour exactly).
|
|
275
275
|
*/
|
|
276
|
-
const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) ||
|
|
276
|
+
const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) ||
|
|
277
|
+
UPSTREAM_SPECIAL_VAR_NAMES.has(name);
|
|
277
278
|
// `stripLeadingCommentsAndWS` lives in core/common.ts so the wire path
|
|
278
279
|
// (sendQuery / executeAndPrint) and the slash-command paths share one
|
|
279
280
|
// implementation. Re-imported from there at the top of the file.
|
|
@@ -287,7 +288,7 @@ const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHoo
|
|
|
287
288
|
* intentionally conservative, and the worst-case outcome is "route bytes
|
|
288
289
|
* that never arrive to the file" — harmless).
|
|
289
290
|
*/
|
|
290
|
-
const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu,
|
|
291
|
+
const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/--[^\n]*/gu, "");
|
|
291
292
|
/**
|
|
292
293
|
* True when `sql` contains at least one `COPY ... TO STDOUT` segment.
|
|
293
294
|
* Used by `runGCore` to install a CopyData sink while `\g` / `\gx` /
|
|
@@ -323,7 +324,7 @@ const formatServerError = (ctx, err, sql) => {
|
|
|
323
324
|
const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
|
|
324
325
|
const prefix = psqlErrorPrefix(ctx.settings);
|
|
325
326
|
const prefixed = [prefix + lines[0], ...lines.slice(1)];
|
|
326
|
-
writeErr(prefixed.join(
|
|
327
|
+
writeErr(prefixed.join("\n") + "\n");
|
|
327
328
|
}
|
|
328
329
|
else {
|
|
329
330
|
// Defensive fallback — captureLastError always sets lastErrorResult,
|
|
@@ -335,8 +336,8 @@ const formatServerError = (ctx, err, sql) => {
|
|
|
335
336
|
// following `\echo :LAST_ERROR_MESSAGE` and `\errverbose` see the new
|
|
336
337
|
// outcome. Matches upstream's `SetErrorVariables` call after every
|
|
337
338
|
// failed dispatch.
|
|
338
|
-
refreshErrorVars(ctx.settings, { kind:
|
|
339
|
-
return { status:
|
|
339
|
+
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
340
|
+
return { status: "error", errorWritten: true };
|
|
340
341
|
};
|
|
341
342
|
/**
|
|
342
343
|
* Open a writable destination for `\o` / `\w` / `\g FILE` / `\g |cmd`.
|
|
@@ -348,15 +349,15 @@ const formatServerError = (ctx, err, sql) => {
|
|
|
348
349
|
* file path; the file is truncated.
|
|
349
350
|
*/
|
|
350
351
|
const openWriter = (target) => {
|
|
351
|
-
if (target.startsWith(
|
|
352
|
+
if (target.startsWith("|")) {
|
|
352
353
|
const cmd = target.slice(1);
|
|
353
|
-
const child = spawn(
|
|
354
|
-
stdio: [
|
|
354
|
+
const child = spawn("sh", ["-c", cmd], {
|
|
355
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
355
356
|
});
|
|
356
357
|
// Swallow EPIPE on the stdin pipe — the child may exit before we
|
|
357
358
|
// finish writing, and Node would otherwise raise an unhandled error.
|
|
358
|
-
child.stdin.on(
|
|
359
|
-
if (err.code !==
|
|
359
|
+
child.stdin.on("error", (err) => {
|
|
360
|
+
if (err.code !== "EPIPE") {
|
|
360
361
|
// Re-raise non-EPIPE errors as a crash so they show up; tests
|
|
361
362
|
// run with the default unhandledRejection handler and will see
|
|
362
363
|
// these via the failing assertion.
|
|
@@ -374,10 +375,10 @@ const openWriter = (target) => {
|
|
|
374
375
|
settled = true;
|
|
375
376
|
resolve({ exitCode: code, signal });
|
|
376
377
|
};
|
|
377
|
-
child.once(
|
|
378
|
+
child.once("close", (code, signal) => {
|
|
378
379
|
finish(code, signal);
|
|
379
380
|
});
|
|
380
|
-
child.once(
|
|
381
|
+
child.once("error", () => {
|
|
381
382
|
// spawn failure or stdio glitch — treat as a non-zero exit so
|
|
382
383
|
// \w sees a failure. \g intentionally ignores this, mirroring
|
|
383
384
|
// upstream `CloseGOutput` which only sets SHELL_ERROR /
|
|
@@ -406,9 +407,9 @@ const openWriter = (target) => {
|
|
|
406
407
|
// a follow-on server-side `COPY FROM` (Docker bind-mount on macOS) sees
|
|
407
408
|
// the fully flushed file even when the next command immediately follows
|
|
408
409
|
// the `\g`.
|
|
409
|
-
const fd = openSync(target,
|
|
410
|
+
const fd = openSync(target, "w");
|
|
410
411
|
const stream = createWriteStream(target, {
|
|
411
|
-
encoding:
|
|
412
|
+
encoding: "utf8",
|
|
412
413
|
fd,
|
|
413
414
|
autoClose: false,
|
|
414
415
|
});
|
|
@@ -418,7 +419,7 @@ const openWriter = (target) => {
|
|
|
418
419
|
// re-raises it as an uncaught exception and kills the whole neonctl process.
|
|
419
420
|
// Capture it; close() surfaces it to the caller.
|
|
420
421
|
let writeError = null;
|
|
421
|
-
stream.on(
|
|
422
|
+
stream.on("error", (err) => {
|
|
422
423
|
writeError = writeError ?? err;
|
|
423
424
|
});
|
|
424
425
|
return {
|
|
@@ -473,7 +474,7 @@ const openWriter = (target) => {
|
|
|
473
474
|
// `COPY FROM '/bind/mount/file'` can read a partial view even
|
|
474
475
|
// though the file is fully synced on the host. Linux + Windows
|
|
475
476
|
// bind mounts are coherent, so this branch is macOS-only.
|
|
476
|
-
if (platform() ===
|
|
477
|
+
if (platform() === "darwin") {
|
|
477
478
|
setTimeout(() => {
|
|
478
479
|
resolve({});
|
|
479
480
|
}, 25);
|
|
@@ -494,38 +495,38 @@ const openWriter = (target) => {
|
|
|
494
495
|
*/
|
|
495
496
|
const errnoToStrerror = (err) => {
|
|
496
497
|
switch (err.code) {
|
|
497
|
-
case
|
|
498
|
-
return
|
|
499
|
-
case
|
|
500
|
-
return
|
|
501
|
-
case
|
|
502
|
-
return
|
|
503
|
-
case
|
|
504
|
-
return
|
|
505
|
-
case
|
|
506
|
-
return
|
|
507
|
-
case
|
|
508
|
-
return
|
|
509
|
-
case
|
|
510
|
-
return
|
|
511
|
-
case
|
|
512
|
-
return
|
|
513
|
-
case
|
|
514
|
-
return
|
|
515
|
-
case
|
|
516
|
-
return
|
|
517
|
-
case
|
|
518
|
-
return
|
|
519
|
-
case
|
|
520
|
-
return
|
|
521
|
-
case
|
|
522
|
-
return
|
|
523
|
-
case
|
|
524
|
-
return
|
|
525
|
-
case
|
|
526
|
-
return
|
|
527
|
-
case
|
|
528
|
-
return
|
|
498
|
+
case "ENOENT":
|
|
499
|
+
return "No such file or directory";
|
|
500
|
+
case "EACCES":
|
|
501
|
+
return "Permission denied";
|
|
502
|
+
case "EISDIR":
|
|
503
|
+
return "Is a directory";
|
|
504
|
+
case "ENOTDIR":
|
|
505
|
+
return "Not a directory";
|
|
506
|
+
case "EEXIST":
|
|
507
|
+
return "File exists";
|
|
508
|
+
case "EROFS":
|
|
509
|
+
return "Read-only file system";
|
|
510
|
+
case "ELOOP":
|
|
511
|
+
return "Too many levels of symbolic links";
|
|
512
|
+
case "ENAMETOOLONG":
|
|
513
|
+
return "File name too long";
|
|
514
|
+
case "ENOSPC":
|
|
515
|
+
return "No space left on device";
|
|
516
|
+
case "EMFILE":
|
|
517
|
+
return "Too many open files";
|
|
518
|
+
case "ENFILE":
|
|
519
|
+
return "Too many open files in system";
|
|
520
|
+
case "EIO":
|
|
521
|
+
return "Input/output error";
|
|
522
|
+
case "EFBIG":
|
|
523
|
+
return "File too large";
|
|
524
|
+
case "EDQUOT":
|
|
525
|
+
return "Disk quota exceeded";
|
|
526
|
+
case "EPERM":
|
|
527
|
+
return "Operation not permitted";
|
|
528
|
+
case "EINVAL":
|
|
529
|
+
return "Invalid argument";
|
|
529
530
|
default: {
|
|
530
531
|
// Strip Node's `ENOENT: no such file or directory, open '/x'`
|
|
531
532
|
// prefix when present so the fallback at least looks like the
|
|
@@ -557,7 +558,7 @@ const reportFileOpenFailure = (ctx, target, err) => {
|
|
|
557
558
|
ctx.settings.lastErrorResult = { message: line };
|
|
558
559
|
const prefix = psqlErrorPrefix(ctx.settings);
|
|
559
560
|
writeErr(`${prefix}${line}\n`);
|
|
560
|
-
return { status:
|
|
561
|
+
return { status: "error", errorWritten: true };
|
|
561
562
|
};
|
|
562
563
|
/**
|
|
563
564
|
* True when `err` was thrown by our synchronous `openSync` in
|
|
@@ -566,10 +567,10 @@ const reportFileOpenFailure = (ctx, target, err) => {
|
|
|
566
567
|
* `\<cmd>: <msg>` path.
|
|
567
568
|
*/
|
|
568
569
|
const isFileOpenFailure = (err) => {
|
|
569
|
-
if (!err || typeof err !==
|
|
570
|
+
if (!err || typeof err !== "object")
|
|
570
571
|
return false;
|
|
571
572
|
const e = err;
|
|
572
|
-
return typeof e.code ===
|
|
573
|
+
return typeof e.code === "string" && e.code.startsWith("E");
|
|
573
574
|
};
|
|
574
575
|
/**
|
|
575
576
|
* Format a child process exit code + signal into upstream psql's
|
|
@@ -595,9 +596,9 @@ const formatChildWaitResult = (exitCode, signal) => {
|
|
|
595
596
|
if (exitCode === 0)
|
|
596
597
|
return null;
|
|
597
598
|
if (exitCode === 127)
|
|
598
|
-
return
|
|
599
|
+
return "command not found";
|
|
599
600
|
if (exitCode === 126)
|
|
600
|
-
return
|
|
601
|
+
return "command was not executable";
|
|
601
602
|
return `child process exited with exit code ${String(exitCode)}`;
|
|
602
603
|
};
|
|
603
604
|
/**
|
|
@@ -608,10 +609,10 @@ const formatChildWaitResult = (exitCode, signal) => {
|
|
|
608
609
|
* available (e.g. `EmptyQueryResponse` carries `command = ''`).
|
|
609
610
|
*/
|
|
610
611
|
const formatCommandTagText = (rs) => {
|
|
611
|
-
const command = (rs.command ||
|
|
612
|
+
const command = (rs.command || "").trim();
|
|
612
613
|
if (command.length === 0)
|
|
613
|
-
return
|
|
614
|
-
if (command ===
|
|
614
|
+
return "";
|
|
615
|
+
if (command === "INSERT") {
|
|
615
616
|
// INSERT is the only tag with the legacy oid in front of rowCount.
|
|
616
617
|
return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
|
|
617
618
|
}
|
|
@@ -656,7 +657,9 @@ const renderResult = async (settings, rs, out) => {
|
|
|
656
657
|
// For COPY-out results, the tag is suppressed regardless — the bytes
|
|
657
658
|
// already flowed; upstream's `handleCopyOut` doesn't emit `COPY N`
|
|
658
659
|
// on the queryFout.
|
|
659
|
-
if (!settings.popt.topt.tuplesOnly &&
|
|
660
|
+
if (!settings.popt.topt.tuplesOnly &&
|
|
661
|
+
!settings.quiet &&
|
|
662
|
+
!rs.copyOutBytes) {
|
|
660
663
|
const tag = formatCommandTagText(rs);
|
|
661
664
|
if (tag.length > 0)
|
|
662
665
|
out.write(`${tag}\n`);
|
|
@@ -675,24 +678,24 @@ const renderResult = async (settings, rs, out) => {
|
|
|
675
678
|
*/
|
|
676
679
|
const pickActivePrinter = (settings) => {
|
|
677
680
|
switch (settings.popt.topt.format) {
|
|
678
|
-
case
|
|
679
|
-
case
|
|
681
|
+
case "aligned":
|
|
682
|
+
case "wrapped":
|
|
680
683
|
return alignedPrinter;
|
|
681
|
-
case
|
|
684
|
+
case "unaligned":
|
|
682
685
|
return unalignedPrinter;
|
|
683
|
-
case
|
|
686
|
+
case "csv":
|
|
684
687
|
return csvPrinter;
|
|
685
|
-
case
|
|
688
|
+
case "json":
|
|
686
689
|
return jsonPrinter;
|
|
687
|
-
case
|
|
690
|
+
case "html":
|
|
688
691
|
return htmlPrinter;
|
|
689
|
-
case
|
|
692
|
+
case "asciidoc":
|
|
690
693
|
return asciidocPrinter;
|
|
691
|
-
case
|
|
694
|
+
case "latex":
|
|
692
695
|
return latexPrinter;
|
|
693
|
-
case
|
|
696
|
+
case "latex-longtable":
|
|
694
697
|
return latexLongtablePrinter;
|
|
695
|
-
case
|
|
698
|
+
case "troff-ms":
|
|
696
699
|
return troffMsPrinter;
|
|
697
700
|
default:
|
|
698
701
|
return alignedPrinter;
|
|
@@ -713,9 +716,9 @@ const pickOut = (settings, oneShot) => {
|
|
|
713
716
|
// \i FILE / \include FILE
|
|
714
717
|
// ---------------------------------------------------------------------------
|
|
715
718
|
const runInclude = async (ctx, relative) => {
|
|
716
|
-
const arg = ctx.nextArg(
|
|
719
|
+
const arg = ctx.nextArg("normal");
|
|
717
720
|
if (arg === null || arg.length === 0) {
|
|
718
|
-
return errResult(ctx,
|
|
721
|
+
return errResult(ctx, "missing required argument");
|
|
719
722
|
}
|
|
720
723
|
// Resolve path: \ir resolves relative to the current input file's
|
|
721
724
|
// directory (if any); \i resolves relative to cwd unless absolute.
|
|
@@ -731,7 +734,7 @@ const runInclude = async (ctx, relative) => {
|
|
|
731
734
|
}
|
|
732
735
|
let contents;
|
|
733
736
|
try {
|
|
734
|
-
contents = await fsPromises.readFile(resolved,
|
|
737
|
+
contents = await fsPromises.readFile(resolved, "utf8");
|
|
735
738
|
}
|
|
736
739
|
catch (err) {
|
|
737
740
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -744,11 +747,11 @@ const runInclude = async (ctx, relative) => {
|
|
|
744
747
|
// -f/-c and (b) double-run interactively (the mainloop drains the queue
|
|
745
748
|
// AND we run execSimple). See.
|
|
746
749
|
if (!ctx.settings.db) {
|
|
747
|
-
return errResult(ctx,
|
|
750
|
+
return errResult(ctx, "no connection to the server");
|
|
748
751
|
}
|
|
749
752
|
const trimmed = contents.trim();
|
|
750
753
|
if (trimmed.length === 0) {
|
|
751
|
-
return { status:
|
|
754
|
+
return { status: "ok" };
|
|
752
755
|
}
|
|
753
756
|
// Track the prior inputfile so `\ir` chains relative to the included
|
|
754
757
|
// file's directory.
|
|
@@ -760,7 +763,7 @@ const runInclude = async (ctx, relative) => {
|
|
|
760
763
|
for (const rs of results) {
|
|
761
764
|
await renderResult(ctx.settings, rs, out);
|
|
762
765
|
}
|
|
763
|
-
return { status:
|
|
766
|
+
return { status: "ok" };
|
|
764
767
|
}
|
|
765
768
|
catch (err) {
|
|
766
769
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -771,36 +774,36 @@ const runInclude = async (ctx, relative) => {
|
|
|
771
774
|
}
|
|
772
775
|
};
|
|
773
776
|
export const cmdInclude = {
|
|
774
|
-
name:
|
|
775
|
-
aliases: [
|
|
776
|
-
helpKey:
|
|
777
|
+
name: "i",
|
|
778
|
+
aliases: ["include"],
|
|
779
|
+
helpKey: "i",
|
|
777
780
|
run: (ctx) => runInclude(ctx, false),
|
|
778
781
|
};
|
|
779
782
|
export const cmdIncludeRel = {
|
|
780
|
-
name:
|
|
781
|
-
aliases: [
|
|
782
|
-
helpKey:
|
|
783
|
+
name: "ir",
|
|
784
|
+
aliases: ["include_relative"],
|
|
785
|
+
helpKey: "ir",
|
|
783
786
|
run: (ctx) => runInclude(ctx, true),
|
|
784
787
|
};
|
|
785
788
|
// ---------------------------------------------------------------------------
|
|
786
789
|
// \o [FILE|cmd] / \out
|
|
787
790
|
// ---------------------------------------------------------------------------
|
|
788
791
|
export const cmdOut = {
|
|
789
|
-
name:
|
|
790
|
-
aliases: [
|
|
791
|
-
helpKey:
|
|
792
|
+
name: "o",
|
|
793
|
+
aliases: ["out"],
|
|
794
|
+
helpKey: "o",
|
|
792
795
|
async run(ctx) {
|
|
793
|
-
const arg = ctx.nextArg(
|
|
796
|
+
const arg = ctx.nextArg("filepipe");
|
|
794
797
|
// Drain any previous target first so writes flush before we rebind.
|
|
795
798
|
await closeQueryFout(ctx.settings);
|
|
796
799
|
if (arg === null || arg.length === 0) {
|
|
797
800
|
// Restore default (stdout).
|
|
798
|
-
return { status:
|
|
801
|
+
return { status: "ok" };
|
|
799
802
|
}
|
|
800
803
|
try {
|
|
801
804
|
const entry = openWriter(arg);
|
|
802
805
|
setQueryFout(ctx.settings, entry);
|
|
803
|
-
return { status:
|
|
806
|
+
return { status: "ok" };
|
|
804
807
|
}
|
|
805
808
|
catch (err) {
|
|
806
809
|
// File targets fail synchronously in `openWriter` via `openSync`;
|
|
@@ -808,7 +811,7 @@ export const cmdOut = {
|
|
|
808
811
|
// no `\o:` prefix) and continue with the loop so a follow-up
|
|
809
812
|
// `SELECT` still executes. Pipe spawn failures (which lack an
|
|
810
813
|
// errno code) fall through to the generic `\o: <msg>` path.
|
|
811
|
-
if (!arg.startsWith(
|
|
814
|
+
if (!arg.startsWith("|") && isFileOpenFailure(err)) {
|
|
812
815
|
return reportFileOpenFailure(ctx, arg, err);
|
|
813
816
|
}
|
|
814
817
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -820,13 +823,13 @@ export const cmdOut = {
|
|
|
820
823
|
// \w FILE / \write FILE
|
|
821
824
|
// ---------------------------------------------------------------------------
|
|
822
825
|
export const cmdWrite = {
|
|
823
|
-
name:
|
|
824
|
-
aliases: [
|
|
825
|
-
helpKey:
|
|
826
|
+
name: "w",
|
|
827
|
+
aliases: ["write"],
|
|
828
|
+
helpKey: "w",
|
|
826
829
|
async run(ctx) {
|
|
827
|
-
const arg = ctx.nextArg(
|
|
830
|
+
const arg = ctx.nextArg("filepipe");
|
|
828
831
|
if (arg === null || arg.length === 0) {
|
|
829
|
-
return errResult(ctx,
|
|
832
|
+
return errResult(ctx, "missing required argument");
|
|
830
833
|
}
|
|
831
834
|
let entry;
|
|
832
835
|
try {
|
|
@@ -837,7 +840,7 @@ export const cmdWrite = {
|
|
|
837
840
|
// path errors out as a bare `<path>: <strerror>` line and the
|
|
838
841
|
// shim keeps reading commands. Pipe spawn failures still use
|
|
839
842
|
// the generic `\w: <msg>` envelope.
|
|
840
|
-
if (!arg.startsWith(
|
|
843
|
+
if (!arg.startsWith("|") && isFileOpenFailure(err)) {
|
|
841
844
|
return reportFileOpenFailure(ctx, arg, err);
|
|
842
845
|
}
|
|
843
846
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -861,7 +864,8 @@ export const cmdWrite = {
|
|
|
861
864
|
// status is what we want to report, NOT the write error — so we
|
|
862
865
|
// swallow EPIPE on pipes and fall through to entry.close() which
|
|
863
866
|
// awaits the child and emits the upstream-shape wait_result_to_str.
|
|
864
|
-
const isEpipe = err instanceof Error &&
|
|
867
|
+
const isEpipe = err instanceof Error &&
|
|
868
|
+
err.code === "EPIPE";
|
|
865
869
|
if (!entry.isPipe || !isEpipe) {
|
|
866
870
|
try {
|
|
867
871
|
await entry.close();
|
|
@@ -895,10 +899,10 @@ export const cmdWrite = {
|
|
|
895
899
|
ctx.settings.lastErrorResult = { message: line };
|
|
896
900
|
const prefix = psqlErrorPrefix(ctx.settings);
|
|
897
901
|
writeErr(`${prefix}${line}\n`);
|
|
898
|
-
return { status:
|
|
902
|
+
return { status: "error", errorWritten: true };
|
|
899
903
|
}
|
|
900
904
|
}
|
|
901
|
-
return { status:
|
|
905
|
+
return { status: "ok" };
|
|
902
906
|
}
|
|
903
907
|
catch (err) {
|
|
904
908
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -930,13 +934,13 @@ const parseGPsetOptions = (body) => {
|
|
|
930
934
|
break;
|
|
931
935
|
// Read option name up to `=`.
|
|
932
936
|
const optStart = i;
|
|
933
|
-
while (i < body.length && body[i] !==
|
|
937
|
+
while (i < body.length && body[i] !== "=" && !/\s/.test(body[i]))
|
|
934
938
|
i++;
|
|
935
939
|
const option = body.slice(optStart, i);
|
|
936
940
|
if (option.length === 0)
|
|
937
941
|
break;
|
|
938
|
-
let value =
|
|
939
|
-
if (body[i] ===
|
|
942
|
+
let value = "";
|
|
943
|
+
if (body[i] === "=") {
|
|
940
944
|
i++; // skip '='
|
|
941
945
|
// Value: single-quoted or unquoted.
|
|
942
946
|
if (body[i] === "'") {
|
|
@@ -946,16 +950,16 @@ const parseGPsetOptions = (body) => {
|
|
|
946
950
|
// C-style escapes (\n, \t, \\, \'). Mirror enough of the
|
|
947
951
|
// upstream `xslashquote` handling to round-trip the regress
|
|
948
952
|
// corpus.
|
|
949
|
-
if (body[i] ===
|
|
953
|
+
if (body[i] === "\\" && i + 1 < body.length) {
|
|
950
954
|
const next = body[i + 1];
|
|
951
|
-
if (next ===
|
|
952
|
-
value +=
|
|
953
|
-
else if (next ===
|
|
954
|
-
value +=
|
|
955
|
-
else if (next ===
|
|
956
|
-
value +=
|
|
957
|
-
else if (next ===
|
|
958
|
-
value +=
|
|
955
|
+
if (next === "n")
|
|
956
|
+
value += "\n";
|
|
957
|
+
else if (next === "t")
|
|
958
|
+
value += "\t";
|
|
959
|
+
else if (next === "r")
|
|
960
|
+
value += "\r";
|
|
961
|
+
else if (next === "\\")
|
|
962
|
+
value += "\\";
|
|
959
963
|
else if (next === "'")
|
|
960
964
|
value += "'";
|
|
961
965
|
else
|
|
@@ -1001,10 +1005,10 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1001
1005
|
// arg block ourselves; otherwise fall back to normal filepipe arg
|
|
1002
1006
|
// extraction.
|
|
1003
1007
|
const rawTrimmed = ctx.rawArgs.trimStart();
|
|
1004
|
-
if (rawTrimmed.startsWith(
|
|
1005
|
-
const close = rawTrimmed.indexOf(
|
|
1008
|
+
if (rawTrimmed.startsWith("(")) {
|
|
1009
|
+
const close = rawTrimmed.indexOf(")");
|
|
1006
1010
|
if (close === -1) {
|
|
1007
|
-
return errResult(ctx,
|
|
1011
|
+
return errResult(ctx, "missing right parenthesis in \\g options");
|
|
1008
1012
|
}
|
|
1009
1013
|
// Strip parens; parse `key=value` pairs (values may be single-
|
|
1010
1014
|
// quoted). The conformance corpus exercises `format=`,
|
|
@@ -1017,7 +1021,7 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1017
1021
|
target = afterParen.length > 0 ? afterParen : null;
|
|
1018
1022
|
}
|
|
1019
1023
|
else {
|
|
1020
|
-
target = ctx.nextArg(
|
|
1024
|
+
target = ctx.nextArg("filepipe");
|
|
1021
1025
|
}
|
|
1022
1026
|
// `\g` / `\gx` with an empty buffer re-runs the most recently submitted
|
|
1023
1027
|
// query — upstream tracks this in `pset.last_query` and `PSQLexec` reads
|
|
@@ -1035,10 +1039,10 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1035
1039
|
if (sql.length === 0 && !hasPendingNamedBind) {
|
|
1036
1040
|
// No buffered SQL, no prior query, no staged bind — silent no-op
|
|
1037
1041
|
// like upstream.
|
|
1038
|
-
return { status:
|
|
1042
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1039
1043
|
}
|
|
1040
1044
|
if (!ctx.settings.db) {
|
|
1041
|
-
return errResult(ctx,
|
|
1045
|
+
return errResult(ctx, "no connection to the server");
|
|
1042
1046
|
}
|
|
1043
1047
|
// Open the one-shot writer if a target was supplied; close it on the way
|
|
1044
1048
|
// out so the file/pipe is flushed before we return.
|
|
@@ -1055,7 +1059,7 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1055
1059
|
// upstream's bare `<path>: <strerror>` shape and continue so the
|
|
1056
1060
|
// next command in the script still executes. Pipe spawn
|
|
1057
1061
|
// failures retain the generic `\g: <msg>` envelope.
|
|
1058
|
-
if (!target.startsWith(
|
|
1062
|
+
if (!target.startsWith("|") && isFileOpenFailure(err)) {
|
|
1059
1063
|
return reportFileOpenFailure(ctx, target, err);
|
|
1060
1064
|
}
|
|
1061
1065
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1069,7 +1073,7 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1069
1073
|
// mutation would persist `expanded = 'on'` across queries.
|
|
1070
1074
|
const toptSnapshot = { ...topt };
|
|
1071
1075
|
if (forceExpanded)
|
|
1072
|
-
topt.expanded =
|
|
1076
|
+
topt.expanded = "on";
|
|
1073
1077
|
// Apply per-query pset overrides silently. Upstream applies the
|
|
1074
1078
|
// temporary options without emitting the status lines that
|
|
1075
1079
|
// interactive `\pset` would.
|
|
@@ -1104,7 +1108,7 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1104
1108
|
// Synthesise a thrown-Error-like object so formatServerError can
|
|
1105
1109
|
// render the same `ERROR: <msg>` shape vanilla emits for the
|
|
1106
1110
|
// server's `prepared statement "X" does not exist` error.
|
|
1107
|
-
execError = Object.assign(new Error(`prepared statement "${bindState.name}" does not exist`), { severity:
|
|
1111
|
+
execError = Object.assign(new Error(`prepared statement "${bindState.name}" does not exist`), { severity: "ERROR", code: "26000" });
|
|
1108
1112
|
}
|
|
1109
1113
|
else {
|
|
1110
1114
|
// Bind + Execute MUST go in one extended-protocol batch: the
|
|
@@ -1210,16 +1214,16 @@ const runGCore = async (ctx, forceExpanded) => {
|
|
|
1210
1214
|
if (pipeError !== null) {
|
|
1211
1215
|
return errResult(ctx, pipeError);
|
|
1212
1216
|
}
|
|
1213
|
-
return { status:
|
|
1217
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1214
1218
|
};
|
|
1215
1219
|
export const cmdG = {
|
|
1216
|
-
name:
|
|
1217
|
-
helpKey:
|
|
1220
|
+
name: "g",
|
|
1221
|
+
helpKey: "g",
|
|
1218
1222
|
run: (ctx) => runGCore(ctx, false),
|
|
1219
1223
|
};
|
|
1220
1224
|
export const cmdGx = {
|
|
1221
|
-
name:
|
|
1222
|
-
helpKey:
|
|
1225
|
+
name: "gx",
|
|
1226
|
+
helpKey: "gx",
|
|
1223
1227
|
run: (ctx) => runGCore(ctx, true),
|
|
1224
1228
|
};
|
|
1225
1229
|
// ---------------------------------------------------------------------------
|
|
@@ -1260,9 +1264,9 @@ export const cmdGx = {
|
|
|
1260
1264
|
* explicitly to match.
|
|
1261
1265
|
*/
|
|
1262
1266
|
export const cmdPrint = {
|
|
1263
|
-
name:
|
|
1264
|
-
aliases: [
|
|
1265
|
-
helpKey:
|
|
1267
|
+
name: "p",
|
|
1268
|
+
aliases: ["print"],
|
|
1269
|
+
helpKey: "p",
|
|
1266
1270
|
run: (ctx) => {
|
|
1267
1271
|
// `queryBuf.trim()` for the emptiness check — not the printed text.
|
|
1268
1272
|
// Upstream's `query_buf->len > 0` is a byte-length check that, in
|
|
@@ -1282,9 +1286,9 @@ export const cmdPrint = {
|
|
|
1282
1286
|
writeOut(`${ctx.settings.lastQuery}\n`);
|
|
1283
1287
|
}
|
|
1284
1288
|
else if (!ctx.settings.quiet) {
|
|
1285
|
-
writeOut(
|
|
1289
|
+
writeOut("Query buffer is empty.\n");
|
|
1286
1290
|
}
|
|
1287
|
-
return Promise.resolve({ status:
|
|
1291
|
+
return Promise.resolve({ status: "ok" });
|
|
1288
1292
|
},
|
|
1289
1293
|
};
|
|
1290
1294
|
// ---------------------------------------------------------------------------
|
|
@@ -1292,14 +1296,14 @@ export const cmdPrint = {
|
|
|
1292
1296
|
// ---------------------------------------------------------------------------
|
|
1293
1297
|
const formatCell = (value) => {
|
|
1294
1298
|
if (value === null || value === undefined)
|
|
1295
|
-
return
|
|
1296
|
-
if (typeof value ===
|
|
1299
|
+
return "";
|
|
1300
|
+
if (typeof value === "string")
|
|
1297
1301
|
return value;
|
|
1298
1302
|
if (Buffer.isBuffer(value))
|
|
1299
|
-
return value.toString(
|
|
1300
|
-
if (typeof value ===
|
|
1301
|
-
typeof value ===
|
|
1302
|
-
typeof value ===
|
|
1303
|
+
return value.toString("utf8");
|
|
1304
|
+
if (typeof value === "number" ||
|
|
1305
|
+
typeof value === "boolean" ||
|
|
1306
|
+
typeof value === "bigint") {
|
|
1303
1307
|
return String(value);
|
|
1304
1308
|
}
|
|
1305
1309
|
// Plain objects / arrays from JSON columns: JSON-stringify so the test
|
|
@@ -1308,12 +1312,12 @@ const formatCell = (value) => {
|
|
|
1308
1312
|
return JSON.stringify(value);
|
|
1309
1313
|
}
|
|
1310
1314
|
catch {
|
|
1311
|
-
return
|
|
1315
|
+
return "";
|
|
1312
1316
|
}
|
|
1313
1317
|
};
|
|
1314
1318
|
export const cmdGset = {
|
|
1315
|
-
name:
|
|
1316
|
-
helpKey:
|
|
1319
|
+
name: "gset",
|
|
1320
|
+
helpKey: "gset",
|
|
1317
1321
|
async run(ctx) {
|
|
1318
1322
|
const gated = pipelineGate(ctx);
|
|
1319
1323
|
if (gated !== null)
|
|
@@ -1321,7 +1325,7 @@ export const cmdGset = {
|
|
|
1321
1325
|
// Strip leading whitespace + comments — see runGCore for the rationale.
|
|
1322
1326
|
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1323
1327
|
const bufSql = trimmedBuf.trim();
|
|
1324
|
-
const prefix = ctx.nextArg(
|
|
1328
|
+
const prefix = ctx.nextArg("normal") ?? "";
|
|
1325
1329
|
// Empty buffer behaviour mirrors upstream `exec_command_gset`'s
|
|
1326
1330
|
// `PSQL_CMD_SEND` return: the dispatch loop sends the active
|
|
1327
1331
|
// `pset.last_query` (or nothing). Upstream does NOT emit an error
|
|
@@ -1330,10 +1334,10 @@ export const cmdGset = {
|
|
|
1330
1334
|
// dispatch.
|
|
1331
1335
|
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
1332
1336
|
if (sql.length === 0) {
|
|
1333
|
-
return { status:
|
|
1337
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1334
1338
|
}
|
|
1335
1339
|
if (!ctx.settings.db) {
|
|
1336
|
-
return errResult(ctx,
|
|
1340
|
+
return errResult(ctx, "no connection to the server");
|
|
1337
1341
|
}
|
|
1338
1342
|
// Track for a subsequent `\g` re-run with empty buffer. Upstream
|
|
1339
1343
|
// `exec_command_gset` updates `pset.last_query` to the dispatched SQL
|
|
@@ -1379,29 +1383,29 @@ export const cmdGset = {
|
|
|
1379
1383
|
// tuples-producing, skip the variable-assignment step entirely.
|
|
1380
1384
|
const lastRs = results[results.length - 1];
|
|
1381
1385
|
if (!lastRs || lastRs.fields.length === 0) {
|
|
1382
|
-
return { status:
|
|
1386
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1383
1387
|
}
|
|
1384
1388
|
const rs = lastRs;
|
|
1385
1389
|
if (rs.rows.length === 0) {
|
|
1386
1390
|
// Bare `no rows returned for \gset` (no `\gset:` prefix) — matches
|
|
1387
1391
|
// upstream psql's `pg_log_error("no rows returned for \\gset")`.
|
|
1388
1392
|
ctx.settings.lastErrorResult = {
|
|
1389
|
-
message:
|
|
1393
|
+
message: "no rows returned for \\gset",
|
|
1390
1394
|
};
|
|
1391
1395
|
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1392
1396
|
writeErr(`${errPrefix}no rows returned for \\gset\n`);
|
|
1393
|
-
return { status:
|
|
1397
|
+
return { status: "error", errorWritten: true };
|
|
1394
1398
|
}
|
|
1395
1399
|
if (rs.rows.length > 1) {
|
|
1396
1400
|
// Match upstream psql's exact wording from `exec_command_gset` —
|
|
1397
1401
|
// bare `more than one row returned for \gset` (no `\gset:` prefix).
|
|
1398
1402
|
// Verified against vanilla psql; vendored psql.out emits it bare.
|
|
1399
1403
|
ctx.settings.lastErrorResult = {
|
|
1400
|
-
message:
|
|
1404
|
+
message: "more than one row returned for \\gset",
|
|
1401
1405
|
};
|
|
1402
1406
|
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1403
1407
|
writeErr(`${errPrefix}more than one row returned for \\gset\n`);
|
|
1404
|
-
return { status:
|
|
1408
|
+
return { status: "error", errorWritten: true };
|
|
1405
1409
|
}
|
|
1406
1410
|
const row = rs.rows[0];
|
|
1407
1411
|
for (let i = 0; i < rs.fields.length; i++) {
|
|
@@ -1450,10 +1454,10 @@ export const cmdGset = {
|
|
|
1450
1454
|
};
|
|
1451
1455
|
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1452
1456
|
writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
|
|
1453
|
-
return { status:
|
|
1457
|
+
return { status: "error", errorWritten: true };
|
|
1454
1458
|
}
|
|
1455
1459
|
}
|
|
1456
|
-
return { status:
|
|
1460
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1457
1461
|
},
|
|
1458
1462
|
};
|
|
1459
1463
|
// ---------------------------------------------------------------------------
|
|
@@ -1496,7 +1500,7 @@ const buildGdescFormatQuery = (fields) => {
|
|
|
1496
1500
|
const typmod = String(f.dataTypeModifier | 0);
|
|
1497
1501
|
return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
|
|
1498
1502
|
})
|
|
1499
|
-
.join(
|
|
1503
|
+
.join(", ");
|
|
1500
1504
|
// ORDER BY _idx preserves the describe order regardless of how the server
|
|
1501
1505
|
// happens to evaluate the VALUES list. Aliases match upstream column
|
|
1502
1506
|
// titles exactly so the printer header is identical.
|
|
@@ -1511,7 +1515,7 @@ const buildGdescFormatQuery = (fields) => {
|
|
|
1511
1515
|
*/
|
|
1512
1516
|
const GDESC_SYNTHETIC_FIELDS = [
|
|
1513
1517
|
{
|
|
1514
|
-
name:
|
|
1518
|
+
name: "Column",
|
|
1515
1519
|
tableID: 0,
|
|
1516
1520
|
columnID: 0,
|
|
1517
1521
|
dataTypeID: 25, // text
|
|
@@ -1520,7 +1524,7 @@ const GDESC_SYNTHETIC_FIELDS = [
|
|
|
1520
1524
|
format: 0,
|
|
1521
1525
|
},
|
|
1522
1526
|
{
|
|
1523
|
-
name:
|
|
1527
|
+
name: "Type",
|
|
1524
1528
|
tableID: 0,
|
|
1525
1529
|
columnID: 0,
|
|
1526
1530
|
dataTypeID: 25, // text
|
|
@@ -1530,7 +1534,7 @@ const GDESC_SYNTHETIC_FIELDS = [
|
|
|
1530
1534
|
},
|
|
1531
1535
|
];
|
|
1532
1536
|
const buildSyntheticGdescResultSet = (rows) => ({
|
|
1533
|
-
command:
|
|
1537
|
+
command: "SELECT",
|
|
1534
1538
|
rowCount: rows.length,
|
|
1535
1539
|
oid: null,
|
|
1536
1540
|
fields: GDESC_SYNTHETIC_FIELDS,
|
|
@@ -1538,8 +1542,8 @@ const buildSyntheticGdescResultSet = (rows) => ({
|
|
|
1538
1542
|
notices: [],
|
|
1539
1543
|
});
|
|
1540
1544
|
export const cmdGdesc = {
|
|
1541
|
-
name:
|
|
1542
|
-
helpKey:
|
|
1545
|
+
name: "gdesc",
|
|
1546
|
+
helpKey: "gdesc",
|
|
1543
1547
|
async run(ctx) {
|
|
1544
1548
|
const gated = pipelineGate(ctx);
|
|
1545
1549
|
if (gated !== null)
|
|
@@ -1553,13 +1557,13 @@ export const cmdGdesc = {
|
|
|
1553
1557
|
// `PrintQueryStatus`'s "The command has no result, or the result
|
|
1554
1558
|
// has no columns." line. Stdout, exit 0 — not an error. Verified
|
|
1555
1559
|
// against vanilla psql 18.
|
|
1556
|
-
process.stdout.write(
|
|
1560
|
+
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1557
1561
|
// Match upstream's post-PSQL_CMD_SEND state vars: success, 0 rows.
|
|
1558
|
-
refreshErrorVars(ctx.settings, { kind:
|
|
1559
|
-
return { status:
|
|
1562
|
+
refreshErrorVars(ctx.settings, { kind: "success", rowCount: 0 });
|
|
1563
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1560
1564
|
}
|
|
1561
1565
|
if (!ctx.settings.db) {
|
|
1562
|
-
return errResult(ctx,
|
|
1566
|
+
return errResult(ctx, "no connection to the server");
|
|
1563
1567
|
}
|
|
1564
1568
|
// Track for a subsequent `\g` re-run with empty buffer. Upstream
|
|
1565
1569
|
// `exec_command_gdesc` updates `pset.last_query` to the dispatched SQL
|
|
@@ -1573,7 +1577,7 @@ export const cmdGdesc = {
|
|
|
1573
1577
|
ctx.settings.lastQuery = sql;
|
|
1574
1578
|
let fields;
|
|
1575
1579
|
try {
|
|
1576
|
-
const stmt = await ctx.settings.db.prepare(
|
|
1580
|
+
const stmt = await ctx.settings.db.prepare("", sql);
|
|
1577
1581
|
fields = await stmt.describe();
|
|
1578
1582
|
// Close the unnamed prepared statement so we don't leak it. Failure
|
|
1579
1583
|
// to close (e.g. server already in error state) is non-fatal.
|
|
@@ -1600,10 +1604,10 @@ export const cmdGdesc = {
|
|
|
1600
1604
|
// Verified against vanilla psql 18: `SELECT \gdesc` and
|
|
1601
1605
|
// `CREATE TABLE bububu(a int) \gdesc` both produce that text.
|
|
1602
1606
|
if (fields.length === 0) {
|
|
1603
|
-
process.stdout.write(
|
|
1607
|
+
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1604
1608
|
// Match upstream's post-PSQL_CMD_SEND state vars: success, 0 rows.
|
|
1605
|
-
refreshErrorVars(ctx.settings, { kind:
|
|
1606
|
-
return { status:
|
|
1609
|
+
refreshErrorVars(ctx.settings, { kind: "success", rowCount: 0 });
|
|
1610
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1607
1611
|
}
|
|
1608
1612
|
// Resolve canonical type names via a follow-up round trip when we have
|
|
1609
1613
|
// at least one field. On failure (or when the server returns nothing —
|
|
@@ -1619,7 +1623,8 @@ export const cmdGdesc = {
|
|
|
1619
1623
|
try {
|
|
1620
1624
|
const sets = await ctx.settings.db.execSimple(formatQuery);
|
|
1621
1625
|
const last = sets[sets.length - 1];
|
|
1622
|
-
rows =
|
|
1626
|
+
rows =
|
|
1627
|
+
last && last.rows.length > 0 ? last.rows : fallbackRows();
|
|
1623
1628
|
}
|
|
1624
1629
|
catch {
|
|
1625
1630
|
rows = fallbackRows();
|
|
@@ -1641,18 +1646,18 @@ export const cmdGdesc = {
|
|
|
1641
1646
|
// `SetResultVariables` sees the synthetic 2-column tuple result and
|
|
1642
1647
|
// assigns ROW_COUNT to the field count we just rendered.
|
|
1643
1648
|
refreshErrorVars(ctx.settings, {
|
|
1644
|
-
kind:
|
|
1649
|
+
kind: "success",
|
|
1645
1650
|
rowCount: rs.rowCount,
|
|
1646
1651
|
});
|
|
1647
|
-
return { status:
|
|
1652
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1648
1653
|
},
|
|
1649
1654
|
};
|
|
1650
1655
|
// ---------------------------------------------------------------------------
|
|
1651
1656
|
// \gexec — treat each cell of the result as SQL to execute.
|
|
1652
1657
|
// ---------------------------------------------------------------------------
|
|
1653
1658
|
export const cmdGexec = {
|
|
1654
|
-
name:
|
|
1655
|
-
helpKey:
|
|
1659
|
+
name: "gexec",
|
|
1660
|
+
helpKey: "gexec",
|
|
1656
1661
|
async run(ctx) {
|
|
1657
1662
|
const gated = pipelineGate(ctx);
|
|
1658
1663
|
if (gated !== null)
|
|
@@ -1665,10 +1670,10 @@ export const cmdGexec = {
|
|
|
1665
1670
|
// query — exit 0, no message. Verified against vanilla psql 18.
|
|
1666
1671
|
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
1667
1672
|
if (sql.length === 0) {
|
|
1668
|
-
return { status:
|
|
1673
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1669
1674
|
}
|
|
1670
1675
|
if (!ctx.settings.db) {
|
|
1671
|
-
return errResult(ctx,
|
|
1676
|
+
return errResult(ctx, "no connection to the server");
|
|
1672
1677
|
}
|
|
1673
1678
|
// Track the outer (meta) query for a subsequent `\g` re-run with an empty
|
|
1674
1679
|
// buffer. Upstream `exec_command_gexec` runs through PSQL_CMD_SEND, which
|
|
@@ -1686,7 +1691,7 @@ export const cmdGexec = {
|
|
|
1686
1691
|
}
|
|
1687
1692
|
const tupled = firstPass.filter((r) => r.fields.length > 0);
|
|
1688
1693
|
if (tupled.length === 0) {
|
|
1689
|
-
return { status:
|
|
1694
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1690
1695
|
}
|
|
1691
1696
|
const out = pickOut(ctx.settings, null);
|
|
1692
1697
|
// Echo each generated SQL when ECHO is `all` or `queries`. Vanilla
|
|
@@ -1696,7 +1701,7 @@ export const cmdGexec = {
|
|
|
1696
1701
|
// appears BEFORE the result body so the conformance harness sees
|
|
1697
1702
|
// the same interleaving vanilla produces.
|
|
1698
1703
|
const echo = ctx.settings.echo;
|
|
1699
|
-
const shouldEcho = echo ===
|
|
1704
|
+
const shouldEcho = echo === "all" || echo === "queries";
|
|
1700
1705
|
// Per-row errors are tolerated: upstream `\gexec` calls
|
|
1701
1706
|
// `SendQuery` in a loop and ignores its return value (the only
|
|
1702
1707
|
// escape is the global ON_ERROR_STOP variable, which the
|
|
@@ -1713,7 +1718,7 @@ export const cmdGexec = {
|
|
|
1713
1718
|
if (statement.length === 0)
|
|
1714
1719
|
continue;
|
|
1715
1720
|
if (shouldEcho) {
|
|
1716
|
-
out.write(statement +
|
|
1721
|
+
out.write(statement + "\n");
|
|
1717
1722
|
}
|
|
1718
1723
|
try {
|
|
1719
1724
|
const nested = await ctx.settings.db.execSimple(statement);
|
|
@@ -1735,7 +1740,7 @@ export const cmdGexec = {
|
|
|
1735
1740
|
// global `pset.on_error_stop` flag via `SendQuery`'s
|
|
1736
1741
|
// return; we mirror by checking the setting directly.
|
|
1737
1742
|
if (ctx.settings.onErrorStop) {
|
|
1738
|
-
return { status:
|
|
1743
|
+
return { status: "error", errorWritten: true };
|
|
1739
1744
|
}
|
|
1740
1745
|
}
|
|
1741
1746
|
}
|
|
@@ -1745,7 +1750,7 @@ export const cmdGexec = {
|
|
|
1745
1750
|
// outer `\gexec` buffer. Per-row error rendering already happened;
|
|
1746
1751
|
// returning `error` here would re-trigger the writeError path.
|
|
1747
1752
|
void sawError;
|
|
1748
|
-
return { status:
|
|
1753
|
+
return { status: "reset-buf", newBuf: "" };
|
|
1749
1754
|
},
|
|
1750
1755
|
};
|
|
1751
1756
|
// ---------------------------------------------------------------------------
|
|
@@ -1771,12 +1776,12 @@ export const cmdGexec = {
|
|
|
1771
1776
|
// ---------------------------------------------------------------------------
|
|
1772
1777
|
const sleepCancellable = (ms, signal) => new Promise((resolve) => {
|
|
1773
1778
|
const timer = setTimeout(() => {
|
|
1774
|
-
signal.removeEventListener(
|
|
1779
|
+
signal.removeEventListener("abort", onAbort);
|
|
1775
1780
|
resolve();
|
|
1776
1781
|
}, ms);
|
|
1777
1782
|
const onAbort = () => {
|
|
1778
1783
|
clearTimeout(timer);
|
|
1779
|
-
signal.removeEventListener(
|
|
1784
|
+
signal.removeEventListener("abort", onAbort);
|
|
1780
1785
|
resolve();
|
|
1781
1786
|
};
|
|
1782
1787
|
if (signal.aborted) {
|
|
@@ -1784,7 +1789,7 @@ const sleepCancellable = (ms, signal) => new Promise((resolve) => {
|
|
|
1784
1789
|
resolve();
|
|
1785
1790
|
return;
|
|
1786
1791
|
}
|
|
1787
|
-
signal.addEventListener(
|
|
1792
|
+
signal.addEventListener("abort", onAbort);
|
|
1788
1793
|
});
|
|
1789
1794
|
/**
|
|
1790
1795
|
* Strictly parse a non-negative finite float.
|
|
@@ -1834,7 +1839,7 @@ const parseStrictNonNegativeInt = (raw) => {
|
|
|
1834
1839
|
* it when the user unsets the `WATCH_INTERVAL` variable — upstream's
|
|
1835
1840
|
* `watch_interval_substitute_hook` reseeds the value to `2` on null.
|
|
1836
1841
|
*/
|
|
1837
|
-
export const DEFAULT_WATCH_INTERVAL =
|
|
1842
|
+
export const DEFAULT_WATCH_INTERVAL = "2";
|
|
1838
1843
|
/**
|
|
1839
1844
|
* Render `\watch`'s per-iteration timestamp in upstream psql's
|
|
1840
1845
|
* `ctime`-style layout: `Day Mon DD HH:MM:SS YYYY` (e.g. `Mon May 25
|
|
@@ -1844,20 +1849,20 @@ export const DEFAULT_WATCH_INTERVAL = '2';
|
|
|
1844
1849
|
*
|
|
1845
1850
|
* Exported only for unit-testing the format ladder.
|
|
1846
1851
|
*/
|
|
1847
|
-
const WEEKDAYS = [
|
|
1852
|
+
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1848
1853
|
const MONTHS = [
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1854
|
+
"Jan",
|
|
1855
|
+
"Feb",
|
|
1856
|
+
"Mar",
|
|
1857
|
+
"Apr",
|
|
1858
|
+
"May",
|
|
1859
|
+
"Jun",
|
|
1860
|
+
"Jul",
|
|
1861
|
+
"Aug",
|
|
1862
|
+
"Sep",
|
|
1863
|
+
"Oct",
|
|
1864
|
+
"Nov",
|
|
1865
|
+
"Dec",
|
|
1861
1866
|
];
|
|
1862
1867
|
const pad2 = (n) => (n < 10 ? `0${String(n)}` : String(n));
|
|
1863
1868
|
export const formatWatchTimestamp = (now) => {
|
|
@@ -1890,7 +1895,7 @@ const resolveWatchIntervalDefault = (settings) => {
|
|
|
1890
1895
|
// If a future code path leaves it undefined we fall back to the same
|
|
1891
1896
|
// documented default — upstream's `ParseVariableDouble` substitutes
|
|
1892
1897
|
// `DEFAULT_WATCH_INTERVAL` when the var slot is empty.
|
|
1893
|
-
const raw = settings.vars.get(
|
|
1898
|
+
const raw = settings.vars.get("WATCH_INTERVAL") ?? DEFAULT_WATCH_INTERVAL;
|
|
1894
1899
|
const parsed = parseStrictNonNegativeFloat(raw);
|
|
1895
1900
|
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) {
|
|
1896
1901
|
return {
|
|
@@ -1925,14 +1930,14 @@ const resolveWatchIntervalDefault = (settings) => {
|
|
|
1925
1930
|
* output target.
|
|
1926
1931
|
*/
|
|
1927
1932
|
const openWatchPager = () => {
|
|
1928
|
-
const cmd = process.env.PSQL_WATCH_PAGER ??
|
|
1933
|
+
const cmd = process.env.PSQL_WATCH_PAGER ?? "";
|
|
1929
1934
|
if (cmd.trim().length === 0)
|
|
1930
1935
|
return null;
|
|
1931
|
-
const child = spawn(
|
|
1932
|
-
stdio: [
|
|
1936
|
+
const child = spawn("sh", ["-c", cmd], {
|
|
1937
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
1933
1938
|
});
|
|
1934
|
-
child.stdin.on(
|
|
1935
|
-
if (err.code !==
|
|
1939
|
+
child.stdin.on("error", (err) => {
|
|
1940
|
+
if (err.code !== "EPIPE") {
|
|
1936
1941
|
throw err;
|
|
1937
1942
|
}
|
|
1938
1943
|
});
|
|
@@ -1954,15 +1959,15 @@ const openWatchPager = () => {
|
|
|
1954
1959
|
settled = true;
|
|
1955
1960
|
resolve();
|
|
1956
1961
|
};
|
|
1957
|
-
child.once(
|
|
1958
|
-
child.once(
|
|
1962
|
+
child.once("close", finish);
|
|
1963
|
+
child.once("error", finish);
|
|
1959
1964
|
if (!child.stdin.destroyed) {
|
|
1960
1965
|
try {
|
|
1961
1966
|
child.stdin.end();
|
|
1962
1967
|
}
|
|
1963
1968
|
catch (err) {
|
|
1964
1969
|
const e = err;
|
|
1965
|
-
if (e.code !==
|
|
1970
|
+
if (e.code !== "EPIPE")
|
|
1966
1971
|
finish();
|
|
1967
1972
|
}
|
|
1968
1973
|
}
|
|
@@ -1970,8 +1975,8 @@ const openWatchPager = () => {
|
|
|
1970
1975
|
};
|
|
1971
1976
|
};
|
|
1972
1977
|
export const cmdWatch = {
|
|
1973
|
-
name:
|
|
1974
|
-
helpKey:
|
|
1978
|
+
name: "watch",
|
|
1979
|
+
helpKey: "watch",
|
|
1975
1980
|
async run(ctx) {
|
|
1976
1981
|
const gated = pipelineGate(ctx);
|
|
1977
1982
|
if (gated !== null)
|
|
@@ -1980,10 +1985,10 @@ export const cmdWatch = {
|
|
|
1980
1985
|
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1981
1986
|
const sql = trimmedBuf.trim();
|
|
1982
1987
|
if (sql.length === 0) {
|
|
1983
|
-
return errResult(ctx,
|
|
1988
|
+
return errResult(ctx, "no query buffer");
|
|
1984
1989
|
}
|
|
1985
1990
|
if (!ctx.settings.db) {
|
|
1986
|
-
return errResult(ctx,
|
|
1991
|
+
return errResult(ctx, "no connection to the server");
|
|
1987
1992
|
}
|
|
1988
1993
|
// Track which options have been seen so we can reject duplicates with
|
|
1989
1994
|
// the upstream-formatted "<thing> is specified more than once" message.
|
|
@@ -1997,7 +2002,7 @@ export const cmdWatch = {
|
|
|
1997
2002
|
// Drain all args. Each is either a `key=value` token or a bare
|
|
1998
2003
|
// positional (only allowed as the very first arg, and only once).
|
|
1999
2004
|
while (true) {
|
|
2000
|
-
const arg = ctx.nextArg(
|
|
2005
|
+
const arg = ctx.nextArg("normal");
|
|
2001
2006
|
if (arg === null)
|
|
2002
2007
|
break;
|
|
2003
2008
|
if (arg.length === 0)
|
|
@@ -2005,25 +2010,26 @@ export const cmdWatch = {
|
|
|
2005
2010
|
// Identify named flags by looking for `=`. Upstream tolerates an
|
|
2006
2011
|
// empty value (treats it as the option not being provided), but we
|
|
2007
2012
|
// mirror its stricter behaviour for the values we care about.
|
|
2008
|
-
const eqIdx = arg.indexOf(
|
|
2013
|
+
const eqIdx = arg.indexOf("=");
|
|
2009
2014
|
if (eqIdx > 0) {
|
|
2010
2015
|
const key = arg.slice(0, eqIdx);
|
|
2011
2016
|
const value = arg.slice(eqIdx + 1);
|
|
2012
|
-
if (key ===
|
|
2017
|
+
if (key === "i") {
|
|
2013
2018
|
if (intervalSet) {
|
|
2014
|
-
return errResult(ctx,
|
|
2019
|
+
return errResult(ctx, "interval value is specified more than once");
|
|
2015
2020
|
}
|
|
2016
2021
|
const parsed = parseStrictNonNegativeFloat(value);
|
|
2017
|
-
if (parsed === null ||
|
|
2022
|
+
if (parsed === null ||
|
|
2023
|
+
parsed > WATCH_INTERVAL_MAX_SECONDS) {
|
|
2018
2024
|
return errResult(ctx, `incorrect interval value "${value}"`);
|
|
2019
2025
|
}
|
|
2020
2026
|
interval = parsed;
|
|
2021
2027
|
intervalSet = true;
|
|
2022
2028
|
continue;
|
|
2023
2029
|
}
|
|
2024
|
-
if (key ===
|
|
2030
|
+
if (key === "c") {
|
|
2025
2031
|
if (iterSet) {
|
|
2026
|
-
return errResult(ctx,
|
|
2032
|
+
return errResult(ctx, "iteration count is specified more than once");
|
|
2027
2033
|
}
|
|
2028
2034
|
const parsed = parseStrictNonNegativeInt(value);
|
|
2029
2035
|
// Upstream parses the count with `option_parse_int(..., 1, INT_MAX)`
|
|
@@ -2038,9 +2044,9 @@ export const cmdWatch = {
|
|
|
2038
2044
|
iterSet = true;
|
|
2039
2045
|
continue;
|
|
2040
2046
|
}
|
|
2041
|
-
if (key ===
|
|
2047
|
+
if (key === "m" || key === "min_rows") {
|
|
2042
2048
|
if (minRowsSet) {
|
|
2043
|
-
return errResult(ctx,
|
|
2049
|
+
return errResult(ctx, "minimum row count specified more than once");
|
|
2044
2050
|
}
|
|
2045
2051
|
const parsed = parseStrictNonNegativeInt(value);
|
|
2046
2052
|
if (parsed === null) {
|
|
@@ -2058,7 +2064,7 @@ export const cmdWatch = {
|
|
|
2058
2064
|
// only collides with `i=` under the same upstream "specified more
|
|
2059
2065
|
// than once" rubric.
|
|
2060
2066
|
if (positionalSeen || intervalSet) {
|
|
2061
|
-
return errResult(ctx,
|
|
2067
|
+
return errResult(ctx, "interval value is specified more than once");
|
|
2062
2068
|
}
|
|
2063
2069
|
const parsed = parseStrictNonNegativeFloat(arg);
|
|
2064
2070
|
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) {
|
|
@@ -2071,7 +2077,7 @@ export const cmdWatch = {
|
|
|
2071
2077
|
// If no explicit interval was supplied, fall back to WATCH_INTERVAL.
|
|
2072
2078
|
if (interval === null) {
|
|
2073
2079
|
const resolved = resolveWatchIntervalDefault(ctx.settings);
|
|
2074
|
-
if (
|
|
2080
|
+
if ("error" in resolved) {
|
|
2075
2081
|
return errResult(ctx, resolved.error);
|
|
2076
2082
|
}
|
|
2077
2083
|
interval = resolved.value;
|
|
@@ -2085,7 +2091,7 @@ export const cmdWatch = {
|
|
|
2085
2091
|
};
|
|
2086
2092
|
const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
|
|
2087
2093
|
if (installedSigint) {
|
|
2088
|
-
process.once(
|
|
2094
|
+
process.once("SIGINT", sigintHandler);
|
|
2089
2095
|
}
|
|
2090
2096
|
// Open the pager once for the whole loop (upstream `do_watch` wraps the
|
|
2091
2097
|
// entire session, not each iteration, so the user can scroll the
|
|
@@ -2152,13 +2158,13 @@ export const cmdWatch = {
|
|
|
2152
2158
|
// possible `^C` echo. Mirror that here so the conformance output
|
|
2153
2159
|
// shape (`...\n(N rows)\n\n\n`) matches vanilla psql.
|
|
2154
2160
|
if (!pager) {
|
|
2155
|
-
out.write(
|
|
2161
|
+
out.write("\n");
|
|
2156
2162
|
}
|
|
2157
|
-
return { status:
|
|
2163
|
+
return { status: "reset-buf", newBuf: "" };
|
|
2158
2164
|
}
|
|
2159
2165
|
finally {
|
|
2160
2166
|
if (installedSigint) {
|
|
2161
|
-
process.removeListener(
|
|
2167
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
2162
2168
|
}
|
|
2163
2169
|
// Drain the pager so its child has a chance to exit before \watch
|
|
2164
2170
|
// returns. Failures are swallowed: a broken pager shouldn't mask the
|