neon 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +30 -5
  2. package/dist/{_shared/auth_selection.js → _chunks/auth_selection-DGgq6ifc.js} +3 -3
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/{_shared/credentials.js → _chunks/credentials-MYdHdKah.js} +3 -3
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/{_shared/env-core/env.js → _chunks/env_services-Tz9G4JeT.js} +179 -43
  7. package/dist/{_shared/paths.js → _chunks/paths-DMq0Lt7a.js} +2 -2
  8. package/dist/{_shared/profiles.js → _chunks/profiles-Ir29rqns.js} +4 -13
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/{_virtual/_rolldown/runtime.js → _chunks/rolldown-runtime-D7D4PA-g.js} +1 -1
  11. package/dist/{_shared/secure_file.js → _chunks/secure_file-BucZj4yQ.js} +2 -4
  12. package/dist/analytics.js +2 -2
  13. package/dist/auth_context.js +1 -1
  14. package/dist/commands/api.js +2 -2
  15. package/dist/commands/api_keys.js +2 -2
  16. package/dist/commands/auth.js +7 -6
  17. package/dist/commands/bootstrap.js +3 -3
  18. package/dist/commands/branches.js +2 -2
  19. package/dist/commands/bucket.js +2 -2
  20. package/dist/commands/checkout.js +2 -2
  21. package/dist/commands/config.js +2 -2
  22. package/dist/commands/connection_string.js +2 -2
  23. package/dist/commands/data_api.js +2 -2
  24. package/dist/commands/databases.js +2 -2
  25. package/dist/commands/deploy.js +2 -2
  26. package/dist/commands/dev.js +3 -3
  27. package/dist/commands/diff.js +2 -2
  28. package/dist/commands/env.js +42 -32
  29. package/dist/commands/functions.js +2 -2
  30. package/dist/commands/index.js +35 -33
  31. package/dist/commands/init.js +3 -3
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/ip_allow.js +2 -2
  34. package/dist/commands/link.js +2 -2
  35. package/dist/commands/logs.js +2 -2
  36. package/dist/commands/neon_auth.js +2 -2
  37. package/dist/commands/open.js +33 -0
  38. package/dist/commands/operations.js +2 -2
  39. package/dist/commands/orgs.js +2 -2
  40. package/dist/commands/profile.js +7 -7
  41. package/dist/commands/projects.js +2 -2
  42. package/dist/commands/psql.js +2 -2
  43. package/dist/commands/roles.js +2 -2
  44. package/dist/commands/set_context.js +2 -2
  45. package/dist/commands/snapshots.js +2 -2
  46. package/dist/commands/status.js +2 -2
  47. package/dist/commands/user.js +2 -2
  48. package/dist/commands/vpc_endpoints.js +2 -2
  49. package/dist/config.js +1 -1
  50. package/dist/dev/env.js +1 -364
  51. package/dist/env_services.js +2 -49
  52. package/dist/index.js +2 -1
  53. package/dist/init/auth.js +4 -4
  54. package/dist/psql/cli.js +1 -1
  55. package/dist/psql/command/cmd_copy.js +1 -1
  56. package/dist/psql/command/cmd_io.js +1 -1403
  57. package/dist/psql/command/cmd_pipeline.js +1 -595
  58. package/dist/psql/command/dispatch.js +1 -2
  59. package/dist/psql/core/common.js +1 -834
  60. package/dist/psql/core/mainloop.js +1 -2
  61. package/dist/psql/core/startup.js +1 -656
  62. package/dist/psql/index.js +1 -1516
  63. package/dist/psql/io/psqlrc.js +1 -1
  64. package/dist/psql/types/index.js +6 -0
  65. package/dist/utils/inspect_db.js +1 -1
  66. package/dist/utils/middlewares.js +2 -1
  67. package/package.json +7 -4
  68. package/dist/_shared/env-core/reuse-secrets.js +0 -179
@@ -1,1404 +1,2 @@
1
- import { writeErr, writeOut } from "./shared.js";
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 };