neon 3.1.0 → 3.1.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 +16 -5
- package/dist/{_shared/auth_selection.js → _chunks/auth_selection-DGgq6ifc.js} +3 -3
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/{_shared/credentials.js → _chunks/credentials-MYdHdKah.js} +3 -3
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/{_shared/env-core/env.js → _chunks/env_services-Tz9G4JeT.js} +179 -43
- package/dist/{_shared/paths.js → _chunks/paths-DMq0Lt7a.js} +2 -2
- package/dist/{_shared/profiles.js → _chunks/profiles-Ir29rqns.js} +4 -13
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/{_virtual/_rolldown/runtime.js → _chunks/rolldown-runtime-D7D4PA-g.js} +1 -1
- package/dist/{_shared/secure_file.js → _chunks/secure_file-BucZj4yQ.js} +2 -4
- package/dist/analytics.js +2 -2
- package/dist/auth_context.js +1 -1
- package/dist/commands/api.js +2 -2
- package/dist/commands/api_keys.js +2 -2
- package/dist/commands/auth.js +6 -6
- package/dist/commands/bootstrap.js +3 -3
- package/dist/commands/branches.js +2 -2
- package/dist/commands/bucket.js +2 -2
- package/dist/commands/checkout.js +2 -2
- package/dist/commands/config.js +2 -2
- package/dist/commands/connection_string.js +2 -2
- package/dist/commands/data_api.js +2 -2
- package/dist/commands/databases.js +2 -2
- package/dist/commands/deploy.js +2 -2
- package/dist/commands/dev.js +3 -3
- package/dist/commands/diff.js +2 -2
- package/dist/commands/env.js +42 -32
- package/dist/commands/functions.js +2 -2
- package/dist/commands/index.js +33 -33
- package/dist/commands/init.js +3 -3
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/ip_allow.js +2 -2
- package/dist/commands/link.js +2 -2
- package/dist/commands/logs.js +2 -2
- package/dist/commands/neon_auth.js +2 -2
- package/dist/commands/operations.js +2 -2
- package/dist/commands/orgs.js +2 -2
- package/dist/commands/profile.js +7 -7
- package/dist/commands/projects.js +2 -2
- package/dist/commands/psql.js +2 -2
- package/dist/commands/roles.js +2 -2
- package/dist/commands/set_context.js +2 -2
- package/dist/commands/snapshots.js +2 -2
- package/dist/commands/status.js +2 -2
- package/dist/commands/user.js +2 -2
- package/dist/commands/vpc_endpoints.js +2 -2
- package/dist/config.js +1 -1
- package/dist/dev/env.js +1 -364
- package/dist/env_services.js +2 -49
- package/dist/index.js +1 -1
- package/dist/init/auth.js +4 -4
- package/dist/psql/cli.js +1 -1
- package/dist/psql/command/cmd_copy.js +1 -1
- package/dist/psql/command/cmd_io.js +1 -1403
- package/dist/psql/command/cmd_pipeline.js +1 -595
- package/dist/psql/command/dispatch.js +1 -2
- package/dist/psql/core/common.js +1 -834
- package/dist/psql/core/mainloop.js +1 -2
- package/dist/psql/core/startup.js +1 -656
- package/dist/psql/index.js +1 -1516
- package/dist/psql/io/psqlrc.js +1 -1
- package/dist/psql/types/index.js +6 -0
- package/dist/utils/inspect_db.js +1 -1
- package/dist/utils/middlewares.js +2 -1
- package/package.json +6 -3
- package/dist/_shared/env-core/reuse-secrets.js +0 -179
|
@@ -1,1404 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { alignedPrinter } from "../print/aligned.js";
|
|
3
|
-
import { asciidocPrinter } from "../print/asciidoc.js";
|
|
4
|
-
import { csvPrinter } from "../print/csv.js";
|
|
5
|
-
import { htmlPrinter } from "../print/html.js";
|
|
6
|
-
import { jsonPrinter } from "../print/json.js";
|
|
7
|
-
import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
|
|
8
|
-
import { troffMsPrinter } from "../print/troff.js";
|
|
9
|
-
import { unalignedPrinter } from "../print/unaligned.js";
|
|
10
|
-
import { formatErrorReport, psqlErrorPrefix } from "./cmd_meta.js";
|
|
11
|
-
import { captureLastError, refreshErrorVars, stripLeadingCommentsAndWS } from "../core/common.js";
|
|
12
|
-
import { applyPset } from "./cmd_format.js";
|
|
13
|
-
import { consumeBindState, lookupPrepared, stagedNamedBindPresent } from "./cmd_pipeline.js";
|
|
14
|
-
import { closeSync, createWriteStream, fsyncSync, openSync, promises } from "node:fs";
|
|
15
|
-
import * as path from "node:path";
|
|
16
|
-
import { platform } from "node:os";
|
|
17
|
-
import { spawn } from "node:child_process";
|
|
18
|
-
//#region src/psql/command/cmd_io.ts
|
|
19
|
-
/**
|
|
20
|
-
* psql I/O & control backslash commands.
|
|
21
|
-
*
|
|
22
|
-
* TypeScript port of the following `exec_command_*` functions in upstream
|
|
23
|
-
* PostgreSQL's `src/bin/psql/command.c`:
|
|
24
|
-
*
|
|
25
|
-
* - `\i`, `\include` → exec_command_include (normal)
|
|
26
|
-
* - `\ir`, `\include_relative` → exec_command_include (relative=true)
|
|
27
|
-
* - `\o`, `\out` → exec_command_out
|
|
28
|
-
* - `\w`, `\write` → exec_command_write
|
|
29
|
-
* - `\g` → exec_command_g
|
|
30
|
-
* - `\gx` → exec_command_g (force_expanded=true)
|
|
31
|
-
* - `\gset` → exec_command_gset
|
|
32
|
-
* - `\gdesc` → exec_command_gdesc
|
|
33
|
-
* - `\gexec` → exec_command_gexec
|
|
34
|
-
* - `\watch` → exec_command_watch
|
|
35
|
-
*
|
|
36
|
-
* Each is exported as a `BackslashCmdSpec` and registered via
|
|
37
|
-
* {@link registerIoCommands}. The single line that wires us into the
|
|
38
|
-
* default dispatcher lives in `dispatch.ts::defaultRegistry()`.
|
|
39
|
-
*
|
|
40
|
-
* # Integration touch-points and known limitations
|
|
41
|
-
*
|
|
42
|
-
* Several of these commands really want to participate in the mainloop's
|
|
43
|
-
* scanner/printer pipeline. This WP keeps `src/psql/core/mainloop.ts`
|
|
44
|
-
* untouched, so we provide the data structures and let a follow-up WP wire
|
|
45
|
-
* the consumption sites. Limitations documented per-command:
|
|
46
|
-
*
|
|
47
|
-
* - `\i FILE` enqueues the file's contents on a small input queue
|
|
48
|
-
* (`./inputQueue.ts`) AND, as a stop-gap, executes the file's SQL
|
|
49
|
-
* directly via `Connection.execSimple`. Backslash commands embedded in
|
|
50
|
-
* the file are NOT processed by the scanner; the include is a "best
|
|
51
|
-
* effort: run as one big SQL blob". Once mainloop adopts the queue API
|
|
52
|
-
* this becomes a true include.
|
|
53
|
-
*
|
|
54
|
-
* - `\o FILE` opens a writable stream and stashes it under a symbol on
|
|
55
|
-
* `settings`. We expose a getter (`getQueryFout`) for the mainloop to
|
|
56
|
-
* consult; until that wiring happens, query output continues to flow
|
|
57
|
-
* to the mainloop's `ctx.stdout`. The stash + close-on-rebind logic is
|
|
58
|
-
* in place and fully tested.
|
|
59
|
-
*
|
|
60
|
-
* - `\g` (no arg) executes the current queryBuf directly through
|
|
61
|
-
* `Connection.execSimple` and renders via the aligned printer. This
|
|
62
|
-
* duplicates a tiny slice of mainloop's send/print pipeline, which is
|
|
63
|
-
* fine for the bytewise-simple cases this WP needs to support. For
|
|
64
|
-
* `\g FILE` / `\g |cmd` the output goes through the temporary writer.
|
|
65
|
-
*
|
|
66
|
-
* - `\gx` toggles `topt.expanded` for the single execution and restores
|
|
67
|
-
* the prior value in a `try { ... } finally { ... }`.
|
|
68
|
-
*
|
|
69
|
-
* - `\gset [PREFIX]` executes via `execSimple`, requires the last result
|
|
70
|
-
* to have exactly one row, and stores `${prefix}${colname}` → value
|
|
71
|
-
* for each column on `settings.vars`.
|
|
72
|
-
*
|
|
73
|
-
* - `\gdesc` parses the buffered query with the extended protocol
|
|
74
|
-
* (Parse + Describe by statement, no Execute), then assembles a
|
|
75
|
-
* synthetic `Column / Type` ResultSet and renders it through the
|
|
76
|
-
* active printer (`alignedPrinter` by default; the format picker
|
|
77
|
-
* honours `\pset format`). Tuples-only mode (`\t on`) and `\o FILE`
|
|
78
|
-
* redirects ride along automatically because the same ResultSet
|
|
79
|
-
* goes through the same printer the REPL would use for a query.
|
|
80
|
-
*
|
|
81
|
-
* - `\gexec` iterates the cells of the last result row-major and feeds
|
|
82
|
-
* each non-null cell back as SQL through `execSimple`. Each statement's
|
|
83
|
-
* output is rendered to stdout (or to the active queryFout stash).
|
|
84
|
-
*
|
|
85
|
-
* - `\watch [INTERVAL]` re-executes the queryBuf every `INTERVAL` seconds
|
|
86
|
-
* (default 2) until SIGINT or until the iteration count limit is hit.
|
|
87
|
-
* We hook SIGINT via a transient listener that's removed on completion.
|
|
88
|
-
* Tests bypass the listener by using an AbortController exposed via
|
|
89
|
-
* `WATCH_TEST_CONTROLLER`.
|
|
90
|
-
*
|
|
91
|
-
* # Error format
|
|
92
|
-
*
|
|
93
|
-
* Upstream prints `<cmd>: <msg>` to stderr and returns failure. We mirror
|
|
94
|
-
* that and also stash the message on `settings.lastErrorResult` so the
|
|
95
|
-
* mainloop's `writeError()` wrapper can pick it up.
|
|
96
|
-
*/
|
|
97
|
-
const QUERY_FOUT_KEY = Symbol.for("neonctl.psql.queryFout");
|
|
98
|
-
/**
|
|
99
|
-
* Return the currently active queryFout stream (or `null` if none).
|
|
100
|
-
* The mainloop is encouraged to call this in lieu of writing directly to
|
|
101
|
-
* `ctx.stdout` for query results.
|
|
102
|
-
*/
|
|
103
|
-
const getQueryFout = (settings) => {
|
|
104
|
-
return settings[QUERY_FOUT_KEY]?.stream ?? null;
|
|
105
|
-
};
|
|
106
|
-
const setQueryFout = (settings, entry) => {
|
|
107
|
-
const stash = settings;
|
|
108
|
-
if (entry === null) stash[QUERY_FOUT_KEY] = void 0;
|
|
109
|
-
else stash[QUERY_FOUT_KEY] = entry;
|
|
110
|
-
};
|
|
111
|
-
const closeQueryFout = async (settings) => {
|
|
112
|
-
const stash = settings;
|
|
113
|
-
const prev = stash[QUERY_FOUT_KEY];
|
|
114
|
-
if (prev) {
|
|
115
|
-
stash[QUERY_FOUT_KEY] = void 0;
|
|
116
|
-
await prev.close();
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
const WATCH_TEST_CONTROLLER = { ref: null };
|
|
120
|
-
const errResult = (ctx, message) => {
|
|
121
|
-
ctx.settings.lastErrorResult = { message };
|
|
122
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
123
|
-
writeErr(`${prefix}\\${ctx.cmdName}: ${message}\n`);
|
|
124
|
-
return {
|
|
125
|
-
status: "error",
|
|
126
|
-
errorWritten: true
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
/**
|
|
130
|
-
* Reject buffer-consuming commands when an extended pipeline is open. Upstream
|
|
131
|
-
* `exec_command_g` / `gx` / `gset` / `gexec` / `watch` all guard with
|
|
132
|
-
* `PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF` and emit
|
|
133
|
-
* `pg_log_error("\\%s not allowed in pipeline mode", cmd)` (note: no `:`
|
|
134
|
-
* after the command name — different shape from `errResult`).
|
|
135
|
-
*
|
|
136
|
-
* `\gdesc` is the odd one out: upstream uses
|
|
137
|
-
* `pg_log_error("synchronous command execution functions are not allowed in
|
|
138
|
-
* pipeline mode")` because the underlying `PQdescribePrepared`/`PQfn`-style
|
|
139
|
-
* helpers all share that text — the regress baseline asserts this exact
|
|
140
|
-
* wording at three call sites in `psql_pipeline.out`.
|
|
141
|
-
*
|
|
142
|
-
* If the command proceeded it would inject a synchronous Query/Sync into the
|
|
143
|
-
* queue, corrupt the pipeline state, and leave `\endpipeline` waiting forever.
|
|
144
|
-
*
|
|
145
|
-
* Returns `null` when not in pipeline mode (caller proceeds); otherwise
|
|
146
|
-
* returns a populated error result the caller should bubble up.
|
|
147
|
-
*
|
|
148
|
-
* Upstream psql 18.4 leaks gate diagnostics through `pg_log_error_internal`
|
|
149
|
-
* which appends to the libpq result error log on the underlying PGresult;
|
|
150
|
-
* each subsequent gate hit RE-EMITS the full accumulated log plus its own
|
|
151
|
-
* line ("Error messages accumulate and are repeated" — the regress comment
|
|
152
|
-
* is the spec). Two `\gdesc` calls back-to-back therefore emit 3 lines
|
|
153
|
-
* total: 1 for the first call, 2 for the second (one accumulated + one
|
|
154
|
-
* own). Mirror that with a settings-stashed accumulator keyed off the
|
|
155
|
-
* current pipeline session; reset when the pipeline ends.
|
|
156
|
-
*/
|
|
157
|
-
const PIPELINE_GATE_ERRORS_KEY = Symbol.for("neonctl.psql.pipelineGateErrors");
|
|
158
|
-
const getGateErrors = (settings) => {
|
|
159
|
-
const s = settings;
|
|
160
|
-
let cur = s[PIPELINE_GATE_ERRORS_KEY];
|
|
161
|
-
if (!cur) {
|
|
162
|
-
cur = [];
|
|
163
|
-
s[PIPELINE_GATE_ERRORS_KEY] = cur;
|
|
164
|
-
}
|
|
165
|
-
return cur;
|
|
166
|
-
};
|
|
167
|
-
/**
|
|
168
|
-
* Drop the accumulated pipeline-gate errors. Called from
|
|
169
|
-
* `\endpipeline` so the next pipeline session starts fresh — without
|
|
170
|
-
* this, gate errors from a closed pipeline would leak into the next
|
|
171
|
-
* one.
|
|
172
|
-
*/
|
|
173
|
-
const clearPipelineGateErrors = (settings) => {
|
|
174
|
-
const s = settings;
|
|
175
|
-
s[PIPELINE_GATE_ERRORS_KEY] = void 0;
|
|
176
|
-
};
|
|
177
|
-
const pipelineGate = (ctx) => {
|
|
178
|
-
if (ctx.settings.sendMode !== "extended-pipeline") return null;
|
|
179
|
-
const message = ctx.cmdName === "gdesc" ? "synchronous command execution functions are not allowed in pipeline mode" : `\\${ctx.cmdName} not allowed in pipeline mode`;
|
|
180
|
-
ctx.settings.lastErrorResult = { message };
|
|
181
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
182
|
-
if (ctx.cmdName === "gdesc") {
|
|
183
|
-
const log = getGateErrors(ctx.settings);
|
|
184
|
-
log.push(message);
|
|
185
|
-
for (const m of log) writeErr(`${prefix}${m}\n`);
|
|
186
|
-
} else writeErr(`${prefix}${message}\n`);
|
|
187
|
-
return {
|
|
188
|
-
status: "error",
|
|
189
|
-
errorWritten: true
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* Set of psql variables upstream marks as "specially treated" — i.e. names
|
|
194
|
-
* that have a substitute / assign hook installed in `startup.c`'s
|
|
195
|
-
* `EstablishVariableSpace`. Used by `\gset` to reject assignments into
|
|
196
|
-
* those names (matching upstream `StoreQueryTuple`'s `VariableHasHook`
|
|
197
|
-
* check). We mirror the upstream list directly so a `\gset IGNORE` into
|
|
198
|
-
* `IGNOREEOF` produces the conformance-expected warning even though our
|
|
199
|
-
* settings.ts hasn't installed the IGNOREEOF / HISTFILE hooks yet — that
|
|
200
|
-
* gap is tracked separately and harmless because the values are read-only
|
|
201
|
-
* for us.
|
|
202
|
-
*/
|
|
203
|
-
const UPSTREAM_SPECIAL_VAR_NAMES = /* @__PURE__ */ new Set([
|
|
204
|
-
"AUTOCOMMIT",
|
|
205
|
-
"COMP_KEYWORD_CASE",
|
|
206
|
-
"ECHO",
|
|
207
|
-
"ECHO_HIDDEN",
|
|
208
|
-
"FETCH_COUNT",
|
|
209
|
-
"HIDE_TABLEAM",
|
|
210
|
-
"HIDE_TOAST_COMPRESSION",
|
|
211
|
-
"HISTCONTROL",
|
|
212
|
-
"HISTFILE",
|
|
213
|
-
"HISTSIZE",
|
|
214
|
-
"IGNOREEOF",
|
|
215
|
-
"ON_ERROR_ROLLBACK",
|
|
216
|
-
"ON_ERROR_STOP",
|
|
217
|
-
"PROMPT1",
|
|
218
|
-
"PROMPT2",
|
|
219
|
-
"PROMPT3",
|
|
220
|
-
"QUIET",
|
|
221
|
-
"SHOW_ALL_RESULTS",
|
|
222
|
-
"SHOW_CONTEXT",
|
|
223
|
-
"SINGLELINE",
|
|
224
|
-
"SINGLESTEP",
|
|
225
|
-
"VERBOSITY"
|
|
226
|
-
]);
|
|
227
|
-
/**
|
|
228
|
-
* True when `name` is a psql variable that `\gset` must skip with an
|
|
229
|
-
* "attempt to \gset into specially treated variable" message. Combines the
|
|
230
|
-
* registered-hook check (so future hook installations are automatically
|
|
231
|
-
* covered) with the upstream-canonical list above (so cases like
|
|
232
|
-
* IGNOREEOF that aren't hooked in our settings.ts still match upstream's
|
|
233
|
-
* `\gset` behaviour exactly).
|
|
234
|
-
*/
|
|
235
|
-
const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) || UPSTREAM_SPECIAL_VAR_NAMES.has(name);
|
|
236
|
-
/**
|
|
237
|
-
* Strip line and block comments from `sql` so a COPY-shaped token inside a
|
|
238
|
-
* comment (e.g. dash-dash `COPY t TO STDOUT`) doesn't trigger the
|
|
239
|
-
* `\g FILE` mid-batch sink. Mirrors the cheap normaliser the mainloop uses
|
|
240
|
-
* before wiring `copyOutMidBatchSink`. Embedded literals are NOT stripped —
|
|
241
|
-
* `'COPY x TO STDOUT'` would still match `hasCopyToStdout`, but that's the
|
|
242
|
-
* same false-positive shape upstream tolerates (the regex sweep is
|
|
243
|
-
* intentionally conservative, and the worst-case outcome is "route bytes
|
|
244
|
-
* that never arrive to the file" — harmless).
|
|
245
|
-
*/
|
|
246
|
-
const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/--[^\n]*/gu, "");
|
|
247
|
-
/**
|
|
248
|
-
* True when `sql` contains at least one `COPY ... TO STDOUT` segment.
|
|
249
|
-
* Used by `runGCore` to install a CopyData sink while `\g` / `\gx` /
|
|
250
|
-
* `\g FILE` / `\g |cmd` dispatches a `\;`-chained batch that mixes
|
|
251
|
-
* COPY-OUT with regular SELECT statements. Without the sink the wire
|
|
252
|
-
* layer drops the CopyData bytes on the floor, and the file/pipe ends
|
|
253
|
-
* up with the surrounding tuple results only — see regress/psql lines
|
|
254
|
-
* 5760-5787 (`COPY (SELECT 'foo') TO STDOUT \; COPY (SELECT 'bar') TO
|
|
255
|
-
* STDOUT \g :g_out_file`).
|
|
256
|
-
*/
|
|
257
|
-
const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlCommentsForCopyScan(sql));
|
|
258
|
-
/**
|
|
259
|
-
* Render a server-side error in upstream psql's 3-line shape (severity +
|
|
260
|
-
* message, then `LINE N:` / `^` re-print) and refresh the `:LAST_ERROR_*`
|
|
261
|
-
* diagnostic variables so a subsequent `\errverbose` sees the rich payload.
|
|
262
|
-
*
|
|
263
|
-
* Mirrors the path that `core/common.ts::writeQueryError` takes for top-level
|
|
264
|
-
* statement errors: capture full ErrorResponse fields onto
|
|
265
|
-
* `settings.lastErrorResult`, render via `formatErrorReport` (honouring
|
|
266
|
-
* VERBOSITY + SHOW_CONTEXT), prefix only the leading severity line with
|
|
267
|
-
* `psql:[<file>:<n>]:`, and update the per-statement diagnostic vars via
|
|
268
|
-
* `refreshErrorVars`.
|
|
269
|
-
*
|
|
270
|
-
* Used by `\g`, `\gx`, `\gset`, `\gdesc`, and `\gexec` so a server-rejected
|
|
271
|
-
* statement dispatched through them renders the same shape vanilla psql
|
|
272
|
-
* produces, instead of the legacy `\<cmd>: <message>` one-liner.
|
|
273
|
-
*/
|
|
274
|
-
const formatServerError = (ctx, err, sql) => {
|
|
275
|
-
const msg = captureLastError(ctx.settings, err, sql);
|
|
276
|
-
const e = ctx.settings.lastErrorResult;
|
|
277
|
-
if (e) {
|
|
278
|
-
const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
|
|
279
|
-
const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
|
|
280
|
-
writeErr(prefixed.join("\n") + "\n");
|
|
281
|
-
} else {
|
|
282
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
283
|
-
writeErr(`${prefix}ERROR: ${msg}\n`);
|
|
284
|
-
}
|
|
285
|
-
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
286
|
-
return {
|
|
287
|
-
status: "error",
|
|
288
|
-
errorWritten: true
|
|
289
|
-
};
|
|
290
|
-
};
|
|
291
|
-
/**
|
|
292
|
-
* Open a writable destination for `\o` / `\w` / `\g FILE` / `\g |cmd`.
|
|
293
|
-
*
|
|
294
|
-
* `target` of the form `|cmd` spawns `sh -c cmd` and pipes to its stdin.
|
|
295
|
-
* The returned closer waits for the child to exit and resolves to its
|
|
296
|
-
* status + terminating signal (if any) so callers can render
|
|
297
|
-
* `wait_result_to_str`-style errors. Any other string is treated as a
|
|
298
|
-
* file path; the file is truncated.
|
|
299
|
-
*/
|
|
300
|
-
const openWriter = (target) => {
|
|
301
|
-
if (target.startsWith("|")) {
|
|
302
|
-
const cmd = target.slice(1);
|
|
303
|
-
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
304
|
-
"pipe",
|
|
305
|
-
"inherit",
|
|
306
|
-
"inherit"
|
|
307
|
-
] });
|
|
308
|
-
child.stdin.on("error", (err) => {
|
|
309
|
-
if (err.code !== "EPIPE") throw err;
|
|
310
|
-
});
|
|
311
|
-
return {
|
|
312
|
-
stream: child.stdin,
|
|
313
|
-
isPipe: true,
|
|
314
|
-
close: () => new Promise((resolve) => {
|
|
315
|
-
let settled = false;
|
|
316
|
-
const finish = (code, signal) => {
|
|
317
|
-
if (settled) return;
|
|
318
|
-
settled = true;
|
|
319
|
-
resolve({
|
|
320
|
-
exitCode: code,
|
|
321
|
-
signal
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
child.once("close", (code, signal) => {
|
|
325
|
-
finish(code, signal);
|
|
326
|
-
});
|
|
327
|
-
child.once("error", () => {
|
|
328
|
-
finish(127, null);
|
|
329
|
-
});
|
|
330
|
-
if (!child.stdin.destroyed) child.stdin.end();
|
|
331
|
-
})
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
const fd = openSync(target, "w");
|
|
335
|
-
const stream = createWriteStream(target, {
|
|
336
|
-
encoding: "utf8",
|
|
337
|
-
fd,
|
|
338
|
-
autoClose: false
|
|
339
|
-
});
|
|
340
|
-
let writeError = null;
|
|
341
|
-
stream.on("error", (err) => {
|
|
342
|
-
writeError = writeError ?? err;
|
|
343
|
-
});
|
|
344
|
-
return {
|
|
345
|
-
stream,
|
|
346
|
-
isPipe: false,
|
|
347
|
-
close: () => new Promise((resolve, reject) => {
|
|
348
|
-
if (writeError !== null) {
|
|
349
|
-
try {
|
|
350
|
-
closeSync(fd);
|
|
351
|
-
} catch {}
|
|
352
|
-
reject(writeError);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
stream.end((err) => {
|
|
356
|
-
if (err) {
|
|
357
|
-
try {
|
|
358
|
-
closeSync(fd);
|
|
359
|
-
} catch {}
|
|
360
|
-
reject(err);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
try {
|
|
364
|
-
fsyncSync(fd);
|
|
365
|
-
} catch {}
|
|
366
|
-
try {
|
|
367
|
-
closeSync(fd);
|
|
368
|
-
} catch (closeErr) {
|
|
369
|
-
reject(closeErr);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (platform() === "darwin") {
|
|
373
|
-
setTimeout(() => {
|
|
374
|
-
resolve({});
|
|
375
|
-
}, 25);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
resolve({});
|
|
379
|
-
});
|
|
380
|
-
})
|
|
381
|
-
};
|
|
382
|
-
};
|
|
383
|
-
/**
|
|
384
|
-
* Map a Node.js errno (`err.code`) to the libc `strerror()` string
|
|
385
|
-
* upstream psql renders in its `pg_log_error("%s: %m", fname)` path.
|
|
386
|
-
*
|
|
387
|
-
* Falls back to `err.message` (with the verbose `ENOENT: ...` prefix
|
|
388
|
-
* stripped if present) so unmapped errno values still surface
|
|
389
|
-
* meaningful text instead of a cryptic Node-internal phrasing.
|
|
390
|
-
*/
|
|
391
|
-
const errnoToStrerror = (err) => {
|
|
392
|
-
switch (err.code) {
|
|
393
|
-
case "ENOENT": return "No such file or directory";
|
|
394
|
-
case "EACCES": return "Permission denied";
|
|
395
|
-
case "EISDIR": return "Is a directory";
|
|
396
|
-
case "ENOTDIR": return "Not a directory";
|
|
397
|
-
case "EEXIST": return "File exists";
|
|
398
|
-
case "EROFS": return "Read-only file system";
|
|
399
|
-
case "ELOOP": return "Too many levels of symbolic links";
|
|
400
|
-
case "ENAMETOOLONG": return "File name too long";
|
|
401
|
-
case "ENOSPC": return "No space left on device";
|
|
402
|
-
case "EMFILE": return "Too many open files";
|
|
403
|
-
case "ENFILE": return "Too many open files in system";
|
|
404
|
-
case "EIO": return "Input/output error";
|
|
405
|
-
case "EFBIG": return "File too large";
|
|
406
|
-
case "EDQUOT": return "Disk quota exceeded";
|
|
407
|
-
case "EPERM": return "Operation not permitted";
|
|
408
|
-
case "EINVAL": return "Invalid argument";
|
|
409
|
-
default: {
|
|
410
|
-
const m = /^[A-Z]+: ([^,]+)/.exec(err.message);
|
|
411
|
-
return m ? m[1] : err.message;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
/**
|
|
416
|
-
* Emit a file-open failure for `\g FILE`, `\o FILE`, `\w FILE` in the
|
|
417
|
-
* exact shape vanilla psql produces: a bare `<path>: <strerror>` line
|
|
418
|
-
* on stderr, no `\<cmd>:` prefix (matches `pg_log_error` under terse
|
|
419
|
-
* mode, which is what `psql -X` uses).
|
|
420
|
-
*
|
|
421
|
-
* The leading `psql:[<file>:<n>]:` tag is still applied when we're
|
|
422
|
-
* reading SQL from a `\i FILE` include — `psqlErrorPrefix` returns ''
|
|
423
|
-
* for stdin so the line stays bare for the interactive / harness case.
|
|
424
|
-
*
|
|
425
|
-
* Returns an `error` envelope with `errorWritten: true` so the mainloop
|
|
426
|
-
* doesn't write a duplicate `psql: ERROR:` fallback.
|
|
427
|
-
*/
|
|
428
|
-
const reportFileOpenFailure = (ctx, target, err) => {
|
|
429
|
-
const line = `${target}: ${errnoToStrerror(err)}`;
|
|
430
|
-
ctx.settings.lastErrorResult = { message: line };
|
|
431
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
432
|
-
writeErr(`${prefix}${line}\n`);
|
|
433
|
-
return {
|
|
434
|
-
status: "error",
|
|
435
|
-
errorWritten: true
|
|
436
|
-
};
|
|
437
|
-
};
|
|
438
|
-
/**
|
|
439
|
-
* True when `err` was thrown by our synchronous `openSync` in
|
|
440
|
-
* {@link openWriter} (i.e. has an errno `code`) and the caller should
|
|
441
|
-
* render it via {@link reportFileOpenFailure} rather than the generic
|
|
442
|
-
* `\<cmd>: <msg>` path.
|
|
443
|
-
*/
|
|
444
|
-
const isFileOpenFailure = (err) => {
|
|
445
|
-
if (!err || typeof err !== "object") return false;
|
|
446
|
-
const e = err;
|
|
447
|
-
return typeof e.code === "string" && e.code.startsWith("E");
|
|
448
|
-
};
|
|
449
|
-
/**
|
|
450
|
-
* Format a child process exit code + signal into upstream psql's
|
|
451
|
-
* `wait_result_to_str` style. Mirrors the C helper in
|
|
452
|
-
* `src/common/wait_error.c`:
|
|
453
|
-
*
|
|
454
|
-
* - exit code 127 → `command not found`
|
|
455
|
-
* - exit code 126 → `command was not executable`
|
|
456
|
-
* - any other code → `child process exited with exit code N`
|
|
457
|
-
* - terminated by signal S → `child process was terminated by
|
|
458
|
-
* signal N: <SIG>`
|
|
459
|
-
*
|
|
460
|
-
* Returns null when the child exited cleanly (code 0, no signal).
|
|
461
|
-
*/
|
|
462
|
-
const formatChildWaitResult = (exitCode, signal) => {
|
|
463
|
-
if (signal) return `child process was terminated by signal: ${signal}`;
|
|
464
|
-
if (exitCode === null || exitCode === void 0) return null;
|
|
465
|
-
if (exitCode === 0) return null;
|
|
466
|
-
if (exitCode === 127) return "command not found";
|
|
467
|
-
if (exitCode === 126) return "command was not executable";
|
|
468
|
-
return `child process exited with exit code ${String(exitCode)}`;
|
|
469
|
-
};
|
|
470
|
-
/**
|
|
471
|
-
* Compose the `CommandComplete`-tag line upstream prints for non-tuples-
|
|
472
|
-
* producing results (DDL, DML without RETURNING, COPY). Mirrors
|
|
473
|
-
* `formatCommandTag` in `core/common.ts` — duplicated to avoid the
|
|
474
|
-
* cmd_io → common import cycle. Returns an empty string when no tag is
|
|
475
|
-
* available (e.g. `EmptyQueryResponse` carries `command = ''`).
|
|
476
|
-
*/
|
|
477
|
-
const formatCommandTagText = (rs) => {
|
|
478
|
-
const command = (rs.command || "").trim();
|
|
479
|
-
if (command.length === 0) return "";
|
|
480
|
-
if (command === "INSERT") return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
|
|
481
|
-
if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${String(rs.rowCount)}`;
|
|
482
|
-
return command;
|
|
483
|
-
};
|
|
484
|
-
/**
|
|
485
|
-
* Render a `ResultSet` to the supplied writable stream using the printer
|
|
486
|
-
* picked from the active `\pset format`. Upstream's `do_watch`, `do_gset`,
|
|
487
|
-
* `do_gexec`, and `\i` all funnel results through the standard query
|
|
488
|
-
* output pipeline (`ExecQueryAndProcessResults` → `printQuery`), which
|
|
489
|
-
* honours `\pset format`. Hard-coding the aligned printer here breaks the
|
|
490
|
-
* conformance harness's `psql -A` runs (which expect unaligned tuples-only
|
|
491
|
-
* output for things like `\watch` polled rows).
|
|
492
|
-
*
|
|
493
|
-
* Non-tuples-producing results (CommandComplete with `fields.length === 0`,
|
|
494
|
-
* which covers DDL, DML without RETURNING, and the post-CopyDone tag for
|
|
495
|
-
* `COPY ... TO STDOUT`) are rendered as a single status line instead of
|
|
496
|
-
* the printer's `(0 rows)` empty-table block — matching `renderResultSets`
|
|
497
|
-
* in `core/common.ts`. Tuples-only (`\t`) and quiet (`--quiet`) both
|
|
498
|
-
* suppress the tag entirely.
|
|
499
|
-
*/
|
|
500
|
-
const renderResult = async (settings, rs, out) => {
|
|
501
|
-
if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) out.write(chunk);
|
|
502
|
-
if (rs.fields.length === 0) {
|
|
503
|
-
if (!settings.popt.topt.tuplesOnly && !settings.quiet && !rs.copyOutBytes) {
|
|
504
|
-
const tag = formatCommandTagText(rs);
|
|
505
|
-
if (tag.length > 0) out.write(`${tag}\n`);
|
|
506
|
-
}
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
await pickActivePrinter(settings).printQuery(rs, settings.popt, out);
|
|
510
|
-
};
|
|
511
|
-
/**
|
|
512
|
-
* Pick the printer for the active output format. Mirrors `pickPrinter`
|
|
513
|
-
* in `core/common.ts` — duplicated here to avoid the cmd_io → common
|
|
514
|
-
* import cycle (common.ts depends on this file for `getQueryFout`).
|
|
515
|
-
*
|
|
516
|
-
* `wrapped` falls back to the aligned printer (which renders `wrapped`
|
|
517
|
-
* mode itself via `topt.format`).
|
|
518
|
-
*/
|
|
519
|
-
const pickActivePrinter = (settings) => {
|
|
520
|
-
switch (settings.popt.topt.format) {
|
|
521
|
-
case "aligned":
|
|
522
|
-
case "wrapped": return alignedPrinter;
|
|
523
|
-
case "unaligned": return unalignedPrinter;
|
|
524
|
-
case "csv": return csvPrinter;
|
|
525
|
-
case "json": return jsonPrinter;
|
|
526
|
-
case "html": return htmlPrinter;
|
|
527
|
-
case "asciidoc": return asciidocPrinter;
|
|
528
|
-
case "latex": return latexPrinter;
|
|
529
|
-
case "latex-longtable": return latexLongtablePrinter;
|
|
530
|
-
case "troff-ms": return troffMsPrinter;
|
|
531
|
-
default: return alignedPrinter;
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
/**
|
|
535
|
-
* Pick the output target for a query result.
|
|
536
|
-
*
|
|
537
|
-
* Precedence: explicit `oneShot` (e.g. `\g FILE`) > the settings stash
|
|
538
|
-
* (`\o FILE`) > `process.stdout`.
|
|
539
|
-
*/
|
|
540
|
-
const pickOut = (settings, oneShot) => {
|
|
541
|
-
if (oneShot) return oneShot;
|
|
542
|
-
return getQueryFout(settings) ?? process.stdout;
|
|
543
|
-
};
|
|
544
|
-
const runInclude = async (ctx, relative) => {
|
|
545
|
-
const arg = ctx.nextArg("normal");
|
|
546
|
-
if (arg === null || arg.length === 0) return errResult(ctx, "missing required argument");
|
|
547
|
-
let resolved;
|
|
548
|
-
if (path.isAbsolute(arg)) resolved = arg;
|
|
549
|
-
else if (relative && ctx.settings.inputfile) resolved = path.resolve(path.dirname(ctx.settings.inputfile), arg);
|
|
550
|
-
else resolved = path.resolve(process.cwd(), arg);
|
|
551
|
-
let contents;
|
|
552
|
-
try {
|
|
553
|
-
contents = await promises.readFile(resolved, "utf8");
|
|
554
|
-
} catch (err) {
|
|
555
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
556
|
-
return errResult(ctx, msg);
|
|
557
|
-
}
|
|
558
|
-
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
559
|
-
const trimmed = contents.trim();
|
|
560
|
-
if (trimmed.length === 0) return { status: "ok" };
|
|
561
|
-
const priorInputFile = ctx.settings.inputfile;
|
|
562
|
-
ctx.settings.inputfile = resolved;
|
|
563
|
-
try {
|
|
564
|
-
const results = await ctx.settings.db.execSimple(trimmed);
|
|
565
|
-
const out = pickOut(ctx.settings, null);
|
|
566
|
-
for (const rs of results) await renderResult(ctx.settings, rs, out);
|
|
567
|
-
return { status: "ok" };
|
|
568
|
-
} catch (err) {
|
|
569
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
570
|
-
return errResult(ctx, msg);
|
|
571
|
-
} finally {
|
|
572
|
-
ctx.settings.inputfile = priorInputFile;
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
const cmdInclude = {
|
|
576
|
-
name: "i",
|
|
577
|
-
aliases: ["include"],
|
|
578
|
-
helpKey: "i",
|
|
579
|
-
run: (ctx) => runInclude(ctx, false)
|
|
580
|
-
};
|
|
581
|
-
const cmdIncludeRel = {
|
|
582
|
-
name: "ir",
|
|
583
|
-
aliases: ["include_relative"],
|
|
584
|
-
helpKey: "ir",
|
|
585
|
-
run: (ctx) => runInclude(ctx, true)
|
|
586
|
-
};
|
|
587
|
-
const cmdOut = {
|
|
588
|
-
name: "o",
|
|
589
|
-
aliases: ["out"],
|
|
590
|
-
helpKey: "o",
|
|
591
|
-
async run(ctx) {
|
|
592
|
-
const arg = ctx.nextArg("filepipe");
|
|
593
|
-
await closeQueryFout(ctx.settings);
|
|
594
|
-
if (arg === null || arg.length === 0) return { status: "ok" };
|
|
595
|
-
try {
|
|
596
|
-
const entry = openWriter(arg);
|
|
597
|
-
setQueryFout(ctx.settings, entry);
|
|
598
|
-
return { status: "ok" };
|
|
599
|
-
} catch (err) {
|
|
600
|
-
if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
|
|
601
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
602
|
-
return errResult(ctx, msg);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
const cmdWrite = {
|
|
607
|
-
name: "w",
|
|
608
|
-
aliases: ["write"],
|
|
609
|
-
helpKey: "w",
|
|
610
|
-
async run(ctx) {
|
|
611
|
-
const arg = ctx.nextArg("filepipe");
|
|
612
|
-
if (arg === null || arg.length === 0) return errResult(ctx, "missing required argument");
|
|
613
|
-
let entry;
|
|
614
|
-
try {
|
|
615
|
-
entry = openWriter(arg);
|
|
616
|
-
} catch (err) {
|
|
617
|
-
if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
|
|
618
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
619
|
-
return errResult(ctx, msg);
|
|
620
|
-
}
|
|
621
|
-
try {
|
|
622
|
-
await new Promise((resolve, reject) => {
|
|
623
|
-
entry.stream.write(ctx.queryBuf, (err) => {
|
|
624
|
-
if (err) reject(err);
|
|
625
|
-
else resolve();
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
} catch (err) {
|
|
629
|
-
const isEpipe = err instanceof Error && err.code === "EPIPE";
|
|
630
|
-
if (!entry.isPipe || !isEpipe) {
|
|
631
|
-
try {
|
|
632
|
-
await entry.close();
|
|
633
|
-
} catch {}
|
|
634
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
635
|
-
return errResult(ctx, msg);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
try {
|
|
639
|
-
const result = await entry.close();
|
|
640
|
-
if (entry.isPipe) {
|
|
641
|
-
const msg = formatChildWaitResult(result.exitCode, result.signal);
|
|
642
|
-
if (msg !== null) {
|
|
643
|
-
const line = `${arg}: ${msg}`;
|
|
644
|
-
ctx.settings.lastErrorResult = { message: line };
|
|
645
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
646
|
-
writeErr(`${prefix}${line}\n`);
|
|
647
|
-
return {
|
|
648
|
-
status: "error",
|
|
649
|
-
errorWritten: true
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return { status: "ok" };
|
|
654
|
-
} catch (err) {
|
|
655
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
656
|
-
return errResult(ctx, msg);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
/**
|
|
661
|
-
* Parse the body of a `\g (option=value option2=value2 ...)` clause —
|
|
662
|
-
* the text between the outer parentheses, already stripped. Options
|
|
663
|
-
* are separated by whitespace; values may be single-quoted to embed
|
|
664
|
-
* spaces. Unquoted values run to the next whitespace.
|
|
665
|
-
*
|
|
666
|
-
* Mirrors upstream's `parse_slash_pgopts_list`. We deliberately stay
|
|
667
|
-
* narrow — the conformance corpus exercises `format=`, `csv_fieldsep=`,
|
|
668
|
-
* and `title=` only.
|
|
669
|
-
*/
|
|
670
|
-
const parseGPsetOptions = (body) => {
|
|
671
|
-
const out = [];
|
|
672
|
-
let i = 0;
|
|
673
|
-
while (i < body.length) {
|
|
674
|
-
while (i < body.length && /\s/.test(body[i])) i++;
|
|
675
|
-
if (i >= body.length) break;
|
|
676
|
-
const optStart = i;
|
|
677
|
-
while (i < body.length && body[i] !== "=" && !/\s/.test(body[i])) i++;
|
|
678
|
-
const option = body.slice(optStart, i);
|
|
679
|
-
if (option.length === 0) break;
|
|
680
|
-
let value = "";
|
|
681
|
-
if (body[i] === "=") {
|
|
682
|
-
i++;
|
|
683
|
-
if (body[i] === "'") {
|
|
684
|
-
i++;
|
|
685
|
-
while (i < body.length && body[i] !== "'") {
|
|
686
|
-
if (body[i] === "\\" && i + 1 < body.length) {
|
|
687
|
-
const next = body[i + 1];
|
|
688
|
-
if (next === "n") value += "\n";
|
|
689
|
-
else if (next === "t") value += " ";
|
|
690
|
-
else if (next === "r") value += "\r";
|
|
691
|
-
else if (next === "\\") value += "\\";
|
|
692
|
-
else if (next === "'") value += "'";
|
|
693
|
-
else value += next;
|
|
694
|
-
i += 2;
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
value += body[i++];
|
|
698
|
-
}
|
|
699
|
-
if (body[i] === "'") i++;
|
|
700
|
-
} else {
|
|
701
|
-
const vStart = i;
|
|
702
|
-
while (i < body.length && !/\s/.test(body[i])) i++;
|
|
703
|
-
value = body.slice(vStart, i);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
out.push({
|
|
707
|
-
option,
|
|
708
|
-
value
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
return out;
|
|
712
|
-
};
|
|
713
|
-
const runGCore = async (ctx, forceExpanded) => {
|
|
714
|
-
const gated = pipelineGate(ctx);
|
|
715
|
-
if (gated !== null) return gated;
|
|
716
|
-
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
717
|
-
const bufSql = trimmedBuf.trim();
|
|
718
|
-
let target;
|
|
719
|
-
let psetOverrides = null;
|
|
720
|
-
const rawTrimmed = ctx.rawArgs.trimStart();
|
|
721
|
-
if (rawTrimmed.startsWith("(")) {
|
|
722
|
-
const close = rawTrimmed.indexOf(")");
|
|
723
|
-
if (close === -1) return errResult(ctx, "missing right parenthesis in \\g options");
|
|
724
|
-
psetOverrides = parseGPsetOptions(rawTrimmed.slice(1, close).trim());
|
|
725
|
-
const afterParen = rawTrimmed.slice(close + 1).trim();
|
|
726
|
-
target = afterParen.length > 0 ? afterParen : null;
|
|
727
|
-
} else target = ctx.nextArg("filepipe");
|
|
728
|
-
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery;
|
|
729
|
-
const hasPendingNamedBind = stagedNamedBindPresent(ctx.settings);
|
|
730
|
-
if (sql.length === 0 && !hasPendingNamedBind) return {
|
|
731
|
-
status: "reset-buf",
|
|
732
|
-
newBuf: ""
|
|
733
|
-
};
|
|
734
|
-
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
735
|
-
let oneShot = null;
|
|
736
|
-
if (target !== null && target.length > 0) try {
|
|
737
|
-
oneShot = openWriter(target);
|
|
738
|
-
} catch (err) {
|
|
739
|
-
if (!target.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, target, err);
|
|
740
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
741
|
-
return errResult(ctx, msg);
|
|
742
|
-
}
|
|
743
|
-
const topt = ctx.settings.popt.topt;
|
|
744
|
-
const toptSnapshot = { ...topt };
|
|
745
|
-
if (forceExpanded) topt.expanded = "on";
|
|
746
|
-
if (psetOverrides) for (const { option, value } of psetOverrides) applyPset(topt, option, value, ctx.cmdName, true);
|
|
747
|
-
ctx.settings.lastQuery = sql;
|
|
748
|
-
const bindState = consumeBindState(ctx.settings);
|
|
749
|
-
let execError = null;
|
|
750
|
-
let copyOutSinkConn = null;
|
|
751
|
-
try {
|
|
752
|
-
const out = pickOut(ctx.settings, oneShot?.stream ?? null);
|
|
753
|
-
if (bindState?.byName) {
|
|
754
|
-
const ps = lookupPrepared(ctx.settings, bindState.name);
|
|
755
|
-
if (!ps) execError = Object.assign(/* @__PURE__ */ new Error(`prepared statement "${bindState.name}" does not exist`), {
|
|
756
|
-
severity: "ERROR",
|
|
757
|
-
code: "26000"
|
|
758
|
-
});
|
|
759
|
-
else {
|
|
760
|
-
const rs = await ps.bindAndExecute(bindState.values);
|
|
761
|
-
await renderResult(ctx.settings, rs, out);
|
|
762
|
-
}
|
|
763
|
-
} else if (bindState) {
|
|
764
|
-
const rs = await ctx.settings.db.query(sql, bindState.values);
|
|
765
|
-
await renderResult(ctx.settings, rs, out);
|
|
766
|
-
} else {
|
|
767
|
-
if (hasCopyToStdout(sql)) {
|
|
768
|
-
copyOutSinkConn = ctx.settings.db;
|
|
769
|
-
copyOutSinkConn.copyOutMidBatchSink = (chunk) => {
|
|
770
|
-
out.write(chunk);
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
const results = await ctx.settings.db.execSimple(sql);
|
|
774
|
-
for (const rs of results) await renderResult(ctx.settings, rs, out);
|
|
775
|
-
}
|
|
776
|
-
} catch (err) {
|
|
777
|
-
execError = err;
|
|
778
|
-
} finally {
|
|
779
|
-
Object.assign(topt, toptSnapshot);
|
|
780
|
-
if (copyOutSinkConn) copyOutSinkConn.copyOutMidBatchSink = null;
|
|
781
|
-
}
|
|
782
|
-
let pipeError = null;
|
|
783
|
-
if (oneShot) try {
|
|
784
|
-
await oneShot.close();
|
|
785
|
-
} catch (err) {
|
|
786
|
-
pipeError = err instanceof Error ? err.message : String(err);
|
|
787
|
-
}
|
|
788
|
-
if (execError !== null) return formatServerError(ctx, execError, bufSql.length > 0 ? trimmedBuf : sql);
|
|
789
|
-
if (pipeError !== null) return errResult(ctx, pipeError);
|
|
790
|
-
return {
|
|
791
|
-
status: "reset-buf",
|
|
792
|
-
newBuf: ""
|
|
793
|
-
};
|
|
794
|
-
};
|
|
795
|
-
const cmdG = {
|
|
796
|
-
name: "g",
|
|
797
|
-
helpKey: "g",
|
|
798
|
-
run: (ctx) => runGCore(ctx, false)
|
|
799
|
-
};
|
|
800
|
-
const cmdGx = {
|
|
801
|
-
name: "gx",
|
|
802
|
-
helpKey: "gx",
|
|
803
|
-
run: (ctx) => runGCore(ctx, true)
|
|
804
|
-
};
|
|
805
|
-
/**
|
|
806
|
-
* `\p` / `\print` — print the query buffer the next `\g` would execute.
|
|
807
|
-
*
|
|
808
|
-
* Mirrors upstream `exec_command_print` in `src/bin/psql/command.c`:
|
|
809
|
-
*
|
|
810
|
-
* if (query_buf && query_buf->len > 0)
|
|
811
|
-
* puts(query_buf->data);
|
|
812
|
-
* else if (previous_buf && previous_buf->len > 0)
|
|
813
|
-
* puts(previous_buf->data);
|
|
814
|
-
* else if (!pset.quiet)
|
|
815
|
-
* puts(_("Query buffer is empty."));
|
|
816
|
-
*
|
|
817
|
-
* Buffer-vs-previous-buffer precedence matters for the regress sequence:
|
|
818
|
-
*
|
|
819
|
-
* SELECT 1; -- executes, previous_buf := "SELECT 1;"
|
|
820
|
-
* \p -- queryBuf empty → prints previous_buf
|
|
821
|
-
* SELECT 2 \r -- queryBuf="SELECT 2 ", \r resets to "" without
|
|
822
|
-
* -- touching previous_buf
|
|
823
|
-
* \p -- queryBuf still empty → prints previous_buf
|
|
824
|
-
* SELECT 3 \p -- queryBuf="SELECT 3 ", non-empty → prints queryBuf
|
|
825
|
-
*
|
|
826
|
-
* Implementation notes:
|
|
827
|
-
*
|
|
828
|
-
* - We use `settings.lastQuery` as the previous-buffer source. Upstream
|
|
829
|
-
* tracks `previous_buf` independently of `pset.last_query`, but our
|
|
830
|
-
* `lastQuery` is set at the exact same point upstream sets
|
|
831
|
-
* `previous_buf` (the dispatch site in `SendQuery`-equivalent code paths
|
|
832
|
-
* in `core/common.ts` and `cmd_io.ts`'s `\g` implementation), so the
|
|
833
|
-
* semantics match for every shape exercised by the conformance corpus.
|
|
834
|
-
* - We must NOT clear queryBuf — return `status: 'ok'` so the mainloop
|
|
835
|
-
* leaves the buffer untouched. The user is inspecting, not executing.
|
|
836
|
-
* - `puts()` appends a trailing newline. We use `writeOut` and append `\n`
|
|
837
|
-
* explicitly to match.
|
|
838
|
-
*/
|
|
839
|
-
const cmdPrint = {
|
|
840
|
-
name: "p",
|
|
841
|
-
aliases: ["print"],
|
|
842
|
-
helpKey: "p",
|
|
843
|
-
run: (ctx) => {
|
|
844
|
-
if (ctx.queryBuf.trim().length > 0) writeOut(`${ctx.queryBuf}\n`);
|
|
845
|
-
else if (ctx.settings.lastQuery.length > 0) writeOut(`${ctx.settings.lastQuery}\n`);
|
|
846
|
-
else if (!ctx.settings.quiet) writeOut("Query buffer is empty.\n");
|
|
847
|
-
return Promise.resolve({ status: "ok" });
|
|
848
|
-
}
|
|
849
|
-
};
|
|
850
|
-
const formatCell = (value) => {
|
|
851
|
-
if (value === null || value === void 0) return "";
|
|
852
|
-
if (typeof value === "string") return value;
|
|
853
|
-
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
854
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
855
|
-
try {
|
|
856
|
-
return JSON.stringify(value);
|
|
857
|
-
} catch {
|
|
858
|
-
return "";
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
|
-
const cmdGset = {
|
|
862
|
-
name: "gset",
|
|
863
|
-
helpKey: "gset",
|
|
864
|
-
async run(ctx) {
|
|
865
|
-
const gated = pipelineGate(ctx);
|
|
866
|
-
if (gated !== null) return gated;
|
|
867
|
-
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
868
|
-
const bufSql = trimmedBuf.trim();
|
|
869
|
-
const prefix = ctx.nextArg("normal") ?? "";
|
|
870
|
-
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
871
|
-
if (sql.length === 0) return {
|
|
872
|
-
status: "reset-buf",
|
|
873
|
-
newBuf: ""
|
|
874
|
-
};
|
|
875
|
-
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
876
|
-
ctx.settings.lastQuery = sql;
|
|
877
|
-
let results;
|
|
878
|
-
try {
|
|
879
|
-
results = await ctx.settings.db.execSimple(sql);
|
|
880
|
-
} catch (err) {
|
|
881
|
-
return formatServerError(ctx, err, bufSql.length > 0 ? trimmedBuf : sql);
|
|
882
|
-
}
|
|
883
|
-
if (results.length > 1) {
|
|
884
|
-
const out = pickOut(ctx.settings, null);
|
|
885
|
-
for (let i = 0; i < results.length - 1; i++) await renderResult(ctx.settings, results[i], out);
|
|
886
|
-
}
|
|
887
|
-
const lastRs = results[results.length - 1];
|
|
888
|
-
if (!lastRs || lastRs.fields.length === 0) return {
|
|
889
|
-
status: "reset-buf",
|
|
890
|
-
newBuf: ""
|
|
891
|
-
};
|
|
892
|
-
const rs = lastRs;
|
|
893
|
-
if (rs.rows.length === 0) {
|
|
894
|
-
ctx.settings.lastErrorResult = { message: "no rows returned for \\gset" };
|
|
895
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
896
|
-
writeErr(`${errPrefix}no rows returned for \\gset\n`);
|
|
897
|
-
return {
|
|
898
|
-
status: "error",
|
|
899
|
-
errorWritten: true
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
if (rs.rows.length > 1) {
|
|
903
|
-
ctx.settings.lastErrorResult = { message: "more than one row returned for \\gset" };
|
|
904
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
905
|
-
writeErr(`${errPrefix}more than one row returned for \\gset\n`);
|
|
906
|
-
return {
|
|
907
|
-
status: "error",
|
|
908
|
-
errorWritten: true
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
const row = rs.rows[0];
|
|
912
|
-
for (let i = 0; i < rs.fields.length; i++) {
|
|
913
|
-
const fieldName = rs.fields[i].name;
|
|
914
|
-
const name = `${prefix}${fieldName}`;
|
|
915
|
-
const cell = row[i];
|
|
916
|
-
const isNull = cell === null || cell === void 0;
|
|
917
|
-
if (isSpeciallyTreatedVar(ctx.settings, name)) {
|
|
918
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
919
|
-
writeErr(`${errPrefix}attempt to \\gset into specially treated variable "${name}" ignored\n`);
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
if (isNull) {
|
|
923
|
-
ctx.settings.vars.unset(name);
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
const value = formatCell(cell);
|
|
927
|
-
if (!ctx.settings.vars.set(name, value)) {
|
|
928
|
-
ctx.settings.lastErrorResult = { message: `invalid variable name: "${fieldName}"` };
|
|
929
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
930
|
-
writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
|
|
931
|
-
return {
|
|
932
|
-
status: "error",
|
|
933
|
-
errorWritten: true
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
return {
|
|
938
|
-
status: "reset-buf",
|
|
939
|
-
newBuf: ""
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
/**
|
|
944
|
-
* Build the SQL that resolves each describe-result column's `Type` via
|
|
945
|
-
* `pg_catalog.format_type(typoid, typmod)`. We feed the names + OIDs
|
|
946
|
-
* + typmods through a `VALUES` literal so the server does the formatting
|
|
947
|
-
* for us — the same query upstream issues from `describeFieldsByType`.
|
|
948
|
-
*
|
|
949
|
-
* Returns null when there are zero fields (caller emits `(0 rows)` form
|
|
950
|
-
* by hand because PostgreSQL rejects an empty VALUES list).
|
|
951
|
-
*/
|
|
952
|
-
const buildGdescFormatQuery = (fields) => {
|
|
953
|
-
if (fields.length === 0) return null;
|
|
954
|
-
return `SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type" FROM (VALUES ${fields.map((f, i) => {
|
|
955
|
-
const safeName = f.name.replace(/'/gu, "''");
|
|
956
|
-
const oid = String(f.dataTypeID >>> 0);
|
|
957
|
-
const typmod = String(f.dataTypeModifier | 0);
|
|
958
|
-
return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
|
|
959
|
-
}).join(", ")}) AS x(_idx, name, tp, tpm) ORDER BY _idx`;
|
|
960
|
-
};
|
|
961
|
-
/**
|
|
962
|
-
* Field descriptors for the synthetic `Column / Type` ResultSet that
|
|
963
|
-
* `\gdesc` emits when format_type resolution fails or yields nothing.
|
|
964
|
-
*
|
|
965
|
-
* We fall back to the field's raw OID so the user still sees a value.
|
|
966
|
-
*/
|
|
967
|
-
const GDESC_SYNTHETIC_FIELDS = [{
|
|
968
|
-
name: "Column",
|
|
969
|
-
tableID: 0,
|
|
970
|
-
columnID: 0,
|
|
971
|
-
dataTypeID: 25,
|
|
972
|
-
dataTypeSize: -1,
|
|
973
|
-
dataTypeModifier: -1,
|
|
974
|
-
format: 0
|
|
975
|
-
}, {
|
|
976
|
-
name: "Type",
|
|
977
|
-
tableID: 0,
|
|
978
|
-
columnID: 0,
|
|
979
|
-
dataTypeID: 25,
|
|
980
|
-
dataTypeSize: -1,
|
|
981
|
-
dataTypeModifier: -1,
|
|
982
|
-
format: 0
|
|
983
|
-
}];
|
|
984
|
-
const buildSyntheticGdescResultSet = (rows) => ({
|
|
985
|
-
command: "SELECT",
|
|
986
|
-
rowCount: rows.length,
|
|
987
|
-
oid: null,
|
|
988
|
-
fields: GDESC_SYNTHETIC_FIELDS,
|
|
989
|
-
rows,
|
|
990
|
-
notices: []
|
|
991
|
-
});
|
|
992
|
-
const cmdGdesc = {
|
|
993
|
-
name: "gdesc",
|
|
994
|
-
helpKey: "gdesc",
|
|
995
|
-
async run(ctx) {
|
|
996
|
-
const gated = pipelineGate(ctx);
|
|
997
|
-
if (gated !== null) return gated;
|
|
998
|
-
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
999
|
-
const sql = trimmedBuf.trim();
|
|
1000
|
-
if (sql.length === 0) {
|
|
1001
|
-
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1002
|
-
refreshErrorVars(ctx.settings, {
|
|
1003
|
-
kind: "success",
|
|
1004
|
-
rowCount: 0
|
|
1005
|
-
});
|
|
1006
|
-
return {
|
|
1007
|
-
status: "reset-buf",
|
|
1008
|
-
newBuf: ""
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
1012
|
-
ctx.settings.lastQuery = sql;
|
|
1013
|
-
let fields;
|
|
1014
|
-
try {
|
|
1015
|
-
const stmt = await ctx.settings.db.prepare("", sql);
|
|
1016
|
-
fields = await stmt.describe();
|
|
1017
|
-
try {
|
|
1018
|
-
await stmt.close();
|
|
1019
|
-
} catch {}
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
return formatServerError(ctx, err, trimmedBuf);
|
|
1022
|
-
}
|
|
1023
|
-
if (fields.length === 0) {
|
|
1024
|
-
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1025
|
-
refreshErrorVars(ctx.settings, {
|
|
1026
|
-
kind: "success",
|
|
1027
|
-
rowCount: 0
|
|
1028
|
-
});
|
|
1029
|
-
return {
|
|
1030
|
-
status: "reset-buf",
|
|
1031
|
-
newBuf: ""
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
let rows;
|
|
1035
|
-
const formatQuery = buildGdescFormatQuery(fields);
|
|
1036
|
-
if (formatQuery === null) rows = [];
|
|
1037
|
-
else {
|
|
1038
|
-
const fallbackRows = () => fields.map((f) => [f.name, String(f.dataTypeID)]);
|
|
1039
|
-
try {
|
|
1040
|
-
const sets = await ctx.settings.db.execSimple(formatQuery);
|
|
1041
|
-
const last = sets[sets.length - 1];
|
|
1042
|
-
rows = last && last.rows.length > 0 ? last.rows : fallbackRows();
|
|
1043
|
-
} catch {
|
|
1044
|
-
rows = fallbackRows();
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
const rs = buildSyntheticGdescResultSet(rows);
|
|
1048
|
-
const printer = pickActivePrinter(ctx.settings);
|
|
1049
|
-
const out = pickOut(ctx.settings, null);
|
|
1050
|
-
try {
|
|
1051
|
-
await printer.printQuery(rs, ctx.settings.popt, out);
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1054
|
-
return errResult(ctx, msg);
|
|
1055
|
-
}
|
|
1056
|
-
refreshErrorVars(ctx.settings, {
|
|
1057
|
-
kind: "success",
|
|
1058
|
-
rowCount: rs.rowCount
|
|
1059
|
-
});
|
|
1060
|
-
return {
|
|
1061
|
-
status: "reset-buf",
|
|
1062
|
-
newBuf: ""
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
};
|
|
1066
|
-
const cmdGexec = {
|
|
1067
|
-
name: "gexec",
|
|
1068
|
-
helpKey: "gexec",
|
|
1069
|
-
async run(ctx) {
|
|
1070
|
-
const gated = pipelineGate(ctx);
|
|
1071
|
-
if (gated !== null) return gated;
|
|
1072
|
-
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1073
|
-
const bufSql = trimmedBuf.trim();
|
|
1074
|
-
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
1075
|
-
if (sql.length === 0) return {
|
|
1076
|
-
status: "reset-buf",
|
|
1077
|
-
newBuf: ""
|
|
1078
|
-
};
|
|
1079
|
-
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
1080
|
-
ctx.settings.lastQuery = sql;
|
|
1081
|
-
let firstPass;
|
|
1082
|
-
try {
|
|
1083
|
-
firstPass = await ctx.settings.db.execSimple(sql);
|
|
1084
|
-
} catch (err) {
|
|
1085
|
-
return formatServerError(ctx, err, trimmedBuf);
|
|
1086
|
-
}
|
|
1087
|
-
const tupled = firstPass.filter((r) => r.fields.length > 0);
|
|
1088
|
-
if (tupled.length === 0) return {
|
|
1089
|
-
status: "reset-buf",
|
|
1090
|
-
newBuf: ""
|
|
1091
|
-
};
|
|
1092
|
-
const out = pickOut(ctx.settings, null);
|
|
1093
|
-
const echo = ctx.settings.echo;
|
|
1094
|
-
const shouldEcho = echo === "all" || echo === "queries";
|
|
1095
|
-
for (const rs of tupled) for (const row of rs.rows) for (const cell of row) {
|
|
1096
|
-
if (cell === null || cell === void 0) continue;
|
|
1097
|
-
const statement = formatCell(cell).trim();
|
|
1098
|
-
if (statement.length === 0) continue;
|
|
1099
|
-
if (shouldEcho) out.write(statement + "\n");
|
|
1100
|
-
try {
|
|
1101
|
-
const nested = await ctx.settings.db.execSimple(statement);
|
|
1102
|
-
for (const sub of nested) if (sub.fields.length > 0) await renderResult(ctx.settings, sub, out);
|
|
1103
|
-
} catch (err) {
|
|
1104
|
-
formatServerError(ctx, err, statement);
|
|
1105
|
-
if (ctx.settings.onErrorStop) return {
|
|
1106
|
-
status: "error",
|
|
1107
|
-
errorWritten: true
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
return {
|
|
1112
|
-
status: "reset-buf",
|
|
1113
|
-
newBuf: ""
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
const sleepCancellable = (ms, signal) => new Promise((resolve) => {
|
|
1118
|
-
const timer = setTimeout(() => {
|
|
1119
|
-
signal.removeEventListener("abort", onAbort);
|
|
1120
|
-
resolve();
|
|
1121
|
-
}, ms);
|
|
1122
|
-
const onAbort = () => {
|
|
1123
|
-
clearTimeout(timer);
|
|
1124
|
-
signal.removeEventListener("abort", onAbort);
|
|
1125
|
-
resolve();
|
|
1126
|
-
};
|
|
1127
|
-
if (signal.aborted) {
|
|
1128
|
-
clearTimeout(timer);
|
|
1129
|
-
resolve();
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
signal.addEventListener("abort", onAbort);
|
|
1133
|
-
});
|
|
1134
|
-
/**
|
|
1135
|
-
* Strictly parse a non-negative finite float.
|
|
1136
|
-
*
|
|
1137
|
-
* Returns the parsed number, or `null` for any of:
|
|
1138
|
-
* - empty string
|
|
1139
|
-
* - non-numeric trailing characters (e.g. `10ab`)
|
|
1140
|
-
* - negative values (e.g. `-10`)
|
|
1141
|
-
* - out-of-range / non-finite results (e.g. `10e400` → Infinity)
|
|
1142
|
-
*
|
|
1143
|
-
* Used to validate `\watch` intervals and the `WATCH_INTERVAL` variable.
|
|
1144
|
-
*/
|
|
1145
|
-
const parseStrictNonNegativeFloat = (raw) => {
|
|
1146
|
-
if (raw.length === 0) return null;
|
|
1147
|
-
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(raw)) return null;
|
|
1148
|
-
const value = parseFloat(raw);
|
|
1149
|
-
if (!Number.isFinite(value)) return null;
|
|
1150
|
-
if (value < 0) return null;
|
|
1151
|
-
return value;
|
|
1152
|
-
};
|
|
1153
|
-
/**
|
|
1154
|
-
* Parse a strict non-negative integer (no exponent, no fractional).
|
|
1155
|
-
* Used for `c=` and `m=` / `min_rows=` argument values.
|
|
1156
|
-
*/
|
|
1157
|
-
const parseStrictNonNegativeInt = (raw) => {
|
|
1158
|
-
if (raw.length === 0) return null;
|
|
1159
|
-
if (!/^\d+$/.test(raw)) return null;
|
|
1160
|
-
const value = parseInt(raw, 10);
|
|
1161
|
-
if (!Number.isFinite(value)) return null;
|
|
1162
|
-
return value;
|
|
1163
|
-
};
|
|
1164
|
-
/**
|
|
1165
|
-
* Default `\watch` interval (seconds). Mirrors upstream
|
|
1166
|
-
* `DEFAULT_WATCH_INTERVAL`. Exported so `defaultSettings` can substitute
|
|
1167
|
-
* it when the user unsets the `WATCH_INTERVAL` variable — upstream's
|
|
1168
|
-
* `watch_interval_substitute_hook` reseeds the value to `2` on null.
|
|
1169
|
-
*/
|
|
1170
|
-
const DEFAULT_WATCH_INTERVAL = "2";
|
|
1171
|
-
/**
|
|
1172
|
-
* Render `\watch`'s per-iteration timestamp in upstream psql's
|
|
1173
|
-
* `ctime`-style layout: `Day Mon DD HH:MM:SS YYYY` (e.g. `Mon May 25
|
|
1174
|
-
* 19:41:55 2026`). Upstream calls `strftime("%c", &tm)` with the C locale;
|
|
1175
|
-
* we reproduce the field order in vanilla English so the output matches
|
|
1176
|
-
* regardless of the host locale.
|
|
1177
|
-
*
|
|
1178
|
-
* Exported only for unit-testing the format ladder.
|
|
1179
|
-
*/
|
|
1180
|
-
const WEEKDAYS = [
|
|
1181
|
-
"Sun",
|
|
1182
|
-
"Mon",
|
|
1183
|
-
"Tue",
|
|
1184
|
-
"Wed",
|
|
1185
|
-
"Thu",
|
|
1186
|
-
"Fri",
|
|
1187
|
-
"Sat"
|
|
1188
|
-
];
|
|
1189
|
-
const MONTHS = [
|
|
1190
|
-
"Jan",
|
|
1191
|
-
"Feb",
|
|
1192
|
-
"Mar",
|
|
1193
|
-
"Apr",
|
|
1194
|
-
"May",
|
|
1195
|
-
"Jun",
|
|
1196
|
-
"Jul",
|
|
1197
|
-
"Aug",
|
|
1198
|
-
"Sep",
|
|
1199
|
-
"Oct",
|
|
1200
|
-
"Nov",
|
|
1201
|
-
"Dec"
|
|
1202
|
-
];
|
|
1203
|
-
const pad2 = (n) => n < 10 ? `0${String(n)}` : String(n);
|
|
1204
|
-
const formatWatchTimestamp = (now) => {
|
|
1205
|
-
return `${WEEKDAYS[now.getDay()]} ${MONTHS[now.getMonth()]} ${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())} ${String(now.getFullYear())}`;
|
|
1206
|
-
};
|
|
1207
|
-
/**
|
|
1208
|
-
* Upper bound on the `WATCH_INTERVAL` variable and the positional interval
|
|
1209
|
-
* — matches upstream which rejects "out of range" values. Upstream uses
|
|
1210
|
-
* `strtod` and rejects ±Infinity; we tighten further so a single watch loop
|
|
1211
|
-
* cannot sleep for longer than ~100 hours, which catches obvious typos
|
|
1212
|
-
* without breaking legitimate slow polls.
|
|
1213
|
-
*/
|
|
1214
|
-
const WATCH_INTERVAL_MAX_SECONDS = 36e4;
|
|
1215
|
-
/**
|
|
1216
|
-
* Resolve the effective default `\watch` interval from the `WATCH_INTERVAL`
|
|
1217
|
-
* psql variable. Returns the parsed value, the documented default
|
|
1218
|
-
* (`DEFAULT_WATCH_INTERVAL`), or an `error` envelope if the variable is set
|
|
1219
|
-
* but parses out of range.
|
|
1220
|
-
*/
|
|
1221
|
-
const resolveWatchIntervalDefault = (settings) => {
|
|
1222
|
-
const raw = settings.vars.get("WATCH_INTERVAL") ?? "2";
|
|
1223
|
-
const parsed = parseStrictNonNegativeFloat(raw);
|
|
1224
|
-
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return { error: `WATCH_INTERVAL "${raw}" is out of range` };
|
|
1225
|
-
return { value: parsed };
|
|
1226
|
-
};
|
|
1227
|
-
/**
|
|
1228
|
-
* Spawn the `\watch` pager for the full duration of the polling loop.
|
|
1229
|
-
*
|
|
1230
|
-
* Upstream `do_watch` wraps the loop in a single `popen` of
|
|
1231
|
-
* `PSQL_WATCH_PAGER`. It deliberately ignores `PSQL_PAGER` and `$PAGER`:
|
|
1232
|
-
*
|
|
1233
|
-
* > we ignore the regular PSQL_PAGER or PAGER environment variables,
|
|
1234
|
-
* > because traditional pagers probably won't be very useful for
|
|
1235
|
-
* > showing a stream of results.
|
|
1236
|
-
*
|
|
1237
|
-
* Mirror that here. Reading from `$PAGER` would silently hijack
|
|
1238
|
-
* `\watch` output for any user whose shell sets `PAGER=less` (the
|
|
1239
|
-
* common default), which makes the loop's output disappear into a
|
|
1240
|
-
* subprocess that diff harnesses can't capture.
|
|
1241
|
-
*
|
|
1242
|
-
* We use a single `sh -c <pager>` spawn so the user can set the
|
|
1243
|
-
* variable to a full command string (`less -R`, `tee /tmp/log`, …)
|
|
1244
|
-
* without the caller having to tokenise it. EPIPE on the stdin pipe is
|
|
1245
|
-
* swallowed for the same reason as in `openWriter`: the user may quit
|
|
1246
|
-
* `less` while we still have writes pending in the next iteration.
|
|
1247
|
-
*
|
|
1248
|
-
* Returns `null` when `PSQL_WATCH_PAGER` is unset or whitespace-only
|
|
1249
|
-
* (upstream's "no pager" rule), so the caller falls back to the normal
|
|
1250
|
-
* output target.
|
|
1251
|
-
*/
|
|
1252
|
-
const openWatchPager = () => {
|
|
1253
|
-
const cmd = process.env.PSQL_WATCH_PAGER ?? "";
|
|
1254
|
-
if (cmd.trim().length === 0) return null;
|
|
1255
|
-
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
1256
|
-
"pipe",
|
|
1257
|
-
"inherit",
|
|
1258
|
-
"inherit"
|
|
1259
|
-
] });
|
|
1260
|
-
child.stdin.on("error", (err) => {
|
|
1261
|
-
if (err.code !== "EPIPE") throw err;
|
|
1262
|
-
});
|
|
1263
|
-
return {
|
|
1264
|
-
stream: child.stdin,
|
|
1265
|
-
close: () => new Promise((resolve) => {
|
|
1266
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1267
|
-
resolve();
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
let settled = false;
|
|
1271
|
-
const finish = () => {
|
|
1272
|
-
if (settled) return;
|
|
1273
|
-
settled = true;
|
|
1274
|
-
resolve();
|
|
1275
|
-
};
|
|
1276
|
-
child.once("close", finish);
|
|
1277
|
-
child.once("error", finish);
|
|
1278
|
-
if (!child.stdin.destroyed) try {
|
|
1279
|
-
child.stdin.end();
|
|
1280
|
-
} catch (err) {
|
|
1281
|
-
if (err.code !== "EPIPE") finish();
|
|
1282
|
-
}
|
|
1283
|
-
})
|
|
1284
|
-
};
|
|
1285
|
-
};
|
|
1286
|
-
const cmdWatch = {
|
|
1287
|
-
name: "watch",
|
|
1288
|
-
helpKey: "watch",
|
|
1289
|
-
async run(ctx) {
|
|
1290
|
-
const gated = pipelineGate(ctx);
|
|
1291
|
-
if (gated !== null) return gated;
|
|
1292
|
-
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1293
|
-
const sql = trimmedBuf.trim();
|
|
1294
|
-
if (sql.length === 0) return errResult(ctx, "no query buffer");
|
|
1295
|
-
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
1296
|
-
let intervalSet = false;
|
|
1297
|
-
let interval = null;
|
|
1298
|
-
let iterSet = false;
|
|
1299
|
-
let iterMax = 0;
|
|
1300
|
-
let minRowsSet = false;
|
|
1301
|
-
let minRows = 0;
|
|
1302
|
-
let positionalSeen = false;
|
|
1303
|
-
while (true) {
|
|
1304
|
-
const arg = ctx.nextArg("normal");
|
|
1305
|
-
if (arg === null) break;
|
|
1306
|
-
if (arg.length === 0) continue;
|
|
1307
|
-
const eqIdx = arg.indexOf("=");
|
|
1308
|
-
if (eqIdx > 0) {
|
|
1309
|
-
const key = arg.slice(0, eqIdx);
|
|
1310
|
-
const value = arg.slice(eqIdx + 1);
|
|
1311
|
-
if (key === "i") {
|
|
1312
|
-
if (intervalSet) return errResult(ctx, "interval value is specified more than once");
|
|
1313
|
-
const parsed = parseStrictNonNegativeFloat(value);
|
|
1314
|
-
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult(ctx, `incorrect interval value "${value}"`);
|
|
1315
|
-
interval = parsed;
|
|
1316
|
-
intervalSet = true;
|
|
1317
|
-
continue;
|
|
1318
|
-
}
|
|
1319
|
-
if (key === "c") {
|
|
1320
|
-
if (iterSet) return errResult(ctx, "iteration count is specified more than once");
|
|
1321
|
-
const parsed = parseStrictNonNegativeInt(value);
|
|
1322
|
-
if (parsed === null || parsed === 0) return errResult(ctx, `incorrect iteration count "${value}"`);
|
|
1323
|
-
iterMax = parsed;
|
|
1324
|
-
iterSet = true;
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
if (key === "m" || key === "min_rows") {
|
|
1328
|
-
if (minRowsSet) return errResult(ctx, "minimum row count specified more than once");
|
|
1329
|
-
const parsed = parseStrictNonNegativeInt(value);
|
|
1330
|
-
if (parsed === null) return errResult(ctx, `incorrect minimum row count "${value}"`);
|
|
1331
|
-
minRows = parsed;
|
|
1332
|
-
minRowsSet = true;
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
return errResult(ctx, `unrecognized option "${key}"`);
|
|
1336
|
-
}
|
|
1337
|
-
if (positionalSeen || intervalSet) return errResult(ctx, "interval value is specified more than once");
|
|
1338
|
-
const parsed = parseStrictNonNegativeFloat(arg);
|
|
1339
|
-
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult(ctx, `incorrect interval value "${arg}"`);
|
|
1340
|
-
interval = parsed;
|
|
1341
|
-
intervalSet = true;
|
|
1342
|
-
positionalSeen = true;
|
|
1343
|
-
}
|
|
1344
|
-
if (interval === null) {
|
|
1345
|
-
const resolved = resolveWatchIntervalDefault(ctx.settings);
|
|
1346
|
-
if ("error" in resolved) return errResult(ctx, resolved.error);
|
|
1347
|
-
interval = resolved.value;
|
|
1348
|
-
}
|
|
1349
|
-
const intervalMs = Math.round(interval * 1e3);
|
|
1350
|
-
const controller = WATCH_TEST_CONTROLLER.ref ?? new AbortController();
|
|
1351
|
-
const sigintHandler = () => {
|
|
1352
|
-
controller.abort();
|
|
1353
|
-
};
|
|
1354
|
-
const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
|
|
1355
|
-
if (installedSigint) process.once("SIGINT", sigintHandler);
|
|
1356
|
-
const pager = openWatchPager();
|
|
1357
|
-
const out = pager?.stream ?? pickOut(ctx.settings, null);
|
|
1358
|
-
try {
|
|
1359
|
-
let iter = 0;
|
|
1360
|
-
while (!controller.signal.aborted) {
|
|
1361
|
-
iter++;
|
|
1362
|
-
const stamp = formatWatchTimestamp(/* @__PURE__ */ new Date());
|
|
1363
|
-
out.write(`${stamp} (every ${String(interval)}s)\n\n`);
|
|
1364
|
-
let lastRowCount = 0;
|
|
1365
|
-
try {
|
|
1366
|
-
const results = await ctx.settings.db.execSimple(sql);
|
|
1367
|
-
for (const rs of results) if (rs.fields.length > 0) {
|
|
1368
|
-
await renderResult(ctx.settings, rs, out);
|
|
1369
|
-
lastRowCount = rs.rows.length;
|
|
1370
|
-
}
|
|
1371
|
-
} catch (err) {
|
|
1372
|
-
return formatServerError(ctx, err, trimmedBuf);
|
|
1373
|
-
}
|
|
1374
|
-
if (iterSet && iter >= iterMax) break;
|
|
1375
|
-
if (minRowsSet && lastRowCount < minRows) break;
|
|
1376
|
-
if (controller.signal.aborted) break;
|
|
1377
|
-
if (intervalMs > 0) await sleepCancellable(intervalMs, controller.signal);
|
|
1378
|
-
}
|
|
1379
|
-
if (!pager) out.write("\n");
|
|
1380
|
-
return {
|
|
1381
|
-
status: "reset-buf",
|
|
1382
|
-
newBuf: ""
|
|
1383
|
-
};
|
|
1384
|
-
} finally {
|
|
1385
|
-
if (installedSigint) process.removeListener("SIGINT", sigintHandler);
|
|
1386
|
-
if (pager) await pager.close();
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
const registerIoCommands = (registry) => {
|
|
1391
|
-
registry.register(cmdInclude);
|
|
1392
|
-
registry.register(cmdIncludeRel);
|
|
1393
|
-
registry.register(cmdOut);
|
|
1394
|
-
registry.register(cmdWrite);
|
|
1395
|
-
registry.register(cmdG);
|
|
1396
|
-
registry.register(cmdGx);
|
|
1397
|
-
registry.register(cmdPrint);
|
|
1398
|
-
registry.register(cmdGset);
|
|
1399
|
-
registry.register(cmdGdesc);
|
|
1400
|
-
registry.register(cmdGexec);
|
|
1401
|
-
registry.register(cmdWatch);
|
|
1402
|
-
};
|
|
1403
|
-
//#endregion
|
|
1
|
+
import { A as cmdPrint, C as cmdGdesc, D as cmdInclude, E as cmdGx, F as registerIoCommands, M as cmdWrite, N as formatWatchTimestamp, O as cmdIncludeRel, P as getQueryFout, S as cmdG, T as cmdGset, b as WATCH_TEST_CONTROLLER, j as cmdWatch, k as cmdOut, w as cmdGexec, x as clearPipelineGateErrors, y as DEFAULT_WATCH_INTERVAL } from "../../_chunks/cmd_pipeline-CUbBO9U_.js";
|
|
1404
2
|
export { DEFAULT_WATCH_INTERVAL, WATCH_TEST_CONTROLLER, clearPipelineGateErrors, cmdG, cmdGdesc, cmdGexec, cmdGset, cmdGx, cmdInclude, cmdIncludeRel, cmdOut, cmdPrint, cmdWatch, cmdWrite, formatWatchTimestamp, getQueryFout, registerIoCommands };
|