neon 2.1.2 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +601 -0
  3. package/dist/analytics.js +156 -0
  4. package/dist/api.js +665 -0
  5. package/dist/auth.js +127 -0
  6. package/dist/callback.html +51 -0
  7. package/dist/cli.js +9 -0
  8. package/dist/commands/auth.js +214 -0
  9. package/dist/commands/bootstrap.js +481 -0
  10. package/dist/commands/branches.js +481 -0
  11. package/dist/commands/bucket.js +543 -0
  12. package/dist/commands/checkout.js +289 -0
  13. package/dist/commands/config.js +544 -0
  14. package/dist/commands/connection_string.js +172 -0
  15. package/dist/commands/data_api.js +285 -0
  16. package/dist/commands/databases.js +82 -0
  17. package/dist/commands/deploy.js +26 -0
  18. package/dist/commands/dev.js +698 -0
  19. package/dist/commands/env.js +166 -0
  20. package/dist/commands/functions.js +373 -0
  21. package/dist/commands/index.js +54 -0
  22. package/dist/commands/init.js +73 -0
  23. package/dist/commands/ip_allow.js +137 -0
  24. package/dist/commands/link.js +1121 -0
  25. package/dist/commands/neon_auth.js +1028 -0
  26. package/dist/commands/operations.js +28 -0
  27. package/dist/commands/orgs.js +24 -0
  28. package/dist/commands/projects.js +372 -0
  29. package/dist/commands/psql.js +62 -0
  30. package/dist/commands/roles.js +65 -0
  31. package/dist/commands/schema_diff.js +151 -0
  32. package/dist/commands/set_context.js +29 -0
  33. package/dist/commands/status.js +40 -0
  34. package/dist/commands/user.js +15 -0
  35. package/dist/commands/vpc_endpoints.js +134 -0
  36. package/dist/config.js +11 -0
  37. package/dist/config_format.js +72 -0
  38. package/dist/context.js +177 -0
  39. package/dist/current_branch_fast_path.js +55 -0
  40. package/dist/dev/env.js +240 -0
  41. package/dist/dev/functions.js +70 -0
  42. package/dist/dev/inputs.js +63 -0
  43. package/dist/dev/runtime.js +146 -0
  44. package/dist/env.js +36 -0
  45. package/dist/env_file.js +159 -0
  46. package/dist/errors.js +80 -0
  47. package/dist/functions_api.js +44 -0
  48. package/dist/help.js +146 -0
  49. package/dist/index.js +234 -0
  50. package/dist/log.js +18 -0
  51. package/dist/parameters.gen.js +480 -0
  52. package/dist/pkg.js +25 -0
  53. package/dist/psql/cli.js +53 -0
  54. package/dist/psql/command/cmd_cond.js +437 -0
  55. package/dist/psql/command/cmd_connect.js +820 -0
  56. package/dist/psql/command/cmd_copy.js +1035 -0
  57. package/dist/psql/command/cmd_describe.js +1815 -0
  58. package/dist/psql/command/cmd_format.js +922 -0
  59. package/dist/psql/command/cmd_io.js +2193 -0
  60. package/dist/psql/command/cmd_lo.js +393 -0
  61. package/dist/psql/command/cmd_meta.js +970 -0
  62. package/dist/psql/command/cmd_misc.js +187 -0
  63. package/dist/psql/command/cmd_pipeline.js +1148 -0
  64. package/dist/psql/command/cmd_restrict.js +171 -0
  65. package/dist/psql/command/cmd_show.js +766 -0
  66. package/dist/psql/command/dispatch.js +343 -0
  67. package/dist/psql/command/inputQueue.js +42 -0
  68. package/dist/psql/command/shared.js +71 -0
  69. package/dist/psql/complete/filenames.js +139 -0
  70. package/dist/psql/complete/index.js +104 -0
  71. package/dist/psql/complete/matcher.js +315 -0
  72. package/dist/psql/complete/psqlVars.js +247 -0
  73. package/dist/psql/complete/queries.js +493 -0
  74. package/dist/psql/complete/rules.js +2424 -0
  75. package/dist/psql/core/common.js +1253 -0
  76. package/dist/psql/core/help.js +576 -0
  77. package/dist/psql/core/mainloop.js +1360 -0
  78. package/dist/psql/core/prompt.js +440 -0
  79. package/dist/psql/core/settings.js +684 -0
  80. package/dist/psql/core/sqlHelp.js +1066 -0
  81. package/dist/psql/core/startup.js +846 -0
  82. package/dist/psql/core/syncVars.js +116 -0
  83. package/dist/psql/core/variables.js +287 -0
  84. package/dist/psql/describe/formatters.js +1290 -0
  85. package/dist/psql/describe/processNamePattern.js +270 -0
  86. package/dist/psql/describe/queries.js +2378 -0
  87. package/dist/psql/describe/versionGate.js +43 -0
  88. package/dist/psql/index.js +2030 -0
  89. package/dist/psql/io/history.js +299 -0
  90. package/dist/psql/io/input.js +120 -0
  91. package/dist/psql/io/lineEditor/buffer.js +325 -0
  92. package/dist/psql/io/lineEditor/complete.js +227 -0
  93. package/dist/psql/io/lineEditor/filename.js +159 -0
  94. package/dist/psql/io/lineEditor/index.js +894 -0
  95. package/dist/psql/io/lineEditor/keymap.js +745 -0
  96. package/dist/psql/io/lineEditor/vt100.js +363 -0
  97. package/dist/psql/io/pgpass.js +202 -0
  98. package/dist/psql/io/pgservice.js +194 -0
  99. package/dist/psql/io/psqlrc.js +422 -0
  100. package/dist/psql/print/aligned.js +1765 -0
  101. package/dist/psql/print/asciidoc.js +248 -0
  102. package/dist/psql/print/crosstab.js +463 -0
  103. package/dist/psql/print/csv.js +95 -0
  104. package/dist/psql/print/html.js +258 -0
  105. package/dist/psql/print/json.js +96 -0
  106. package/dist/psql/print/latex.js +396 -0
  107. package/dist/psql/print/pager.js +267 -0
  108. package/dist/psql/print/troff.js +258 -0
  109. package/dist/psql/print/unaligned.js +119 -0
  110. package/dist/psql/print/units.js +135 -0
  111. package/dist/psql/scanner/slash.js +515 -0
  112. package/dist/psql/scanner/sql.js +914 -0
  113. package/dist/psql/scanner/stringutils.js +394 -0
  114. package/dist/psql/types/backslash.js +1 -0
  115. package/dist/psql/types/connection.js +1 -0
  116. package/dist/psql/types/index.js +7 -0
  117. package/dist/psql/types/printer.js +1 -0
  118. package/dist/psql/types/repl.js +1 -0
  119. package/dist/psql/types/scanner.js +24 -0
  120. package/dist/psql/types/settings.js +1 -0
  121. package/dist/psql/types/variables.js +1 -0
  122. package/dist/psql/wire/connection.js +2858 -0
  123. package/dist/psql/wire/copy.js +108 -0
  124. package/dist/psql/wire/notify.js +59 -0
  125. package/dist/psql/wire/pipeline.js +521 -0
  126. package/dist/psql/wire/protocol.js +466 -0
  127. package/dist/psql/wire/sasl.js +296 -0
  128. package/dist/psql/wire/tls.js +602 -0
  129. package/dist/storage_api.js +147 -0
  130. package/dist/test_utils/fixtures.js +122 -0
  131. package/dist/test_utils/oauth_server.js +9 -0
  132. package/dist/types.js +1 -0
  133. package/dist/utils/api_enums.js +33 -0
  134. package/dist/utils/auth.js +5 -0
  135. package/dist/utils/branch_notice.js +22 -0
  136. package/dist/utils/branch_picker.js +103 -0
  137. package/dist/utils/compute_units.js +28 -0
  138. package/dist/utils/enrichers.js +161 -0
  139. package/dist/utils/esbuild.js +158 -0
  140. package/dist/utils/formats.js +18 -0
  141. package/dist/utils/middlewares.js +20 -0
  142. package/dist/utils/package_manager.js +68 -0
  143. package/dist/utils/point_in_time.js +56 -0
  144. package/dist/utils/psql.js +120 -0
  145. package/dist/utils/string.js +5 -0
  146. package/dist/utils/ui.js +59 -0
  147. package/dist/utils/zip.js +4 -0
  148. package/dist/writer.js +97 -0
  149. package/package.json +117 -14
  150. package/.jshintrc +0 -3
  151. package/CHANGELOG.markdown +0 -22
  152. package/LICENSE +0 -21
  153. package/README.markdown +0 -76
  154. package/bower.json +0 -23
  155. package/license.txt +0 -9
  156. package/neon.js +0 -234
  157. package/stdlib/bubbling_support.js +0 -32
  158. package/stdlib/custom_event.js +0 -54
  159. package/stdlib/custom_event_support.js +0 -195
  160. package/stdlib/index.js +0 -7
  161. package/stdlib/node_support.js +0 -123
  162. package/stdlib/widget.js +0 -340
  163. package/test/neon_browser.html +0 -11
  164. package/test/neon_stdlib_browser.html +0 -15
  165. package/test/neon_stdlib_test.js +0 -84
  166. package/test/neon_test.js +0 -64
@@ -0,0 +1,2193 @@
1
+ /**
2
+ * psql I/O & control backslash commands.
3
+ *
4
+ * TypeScript port of the following `exec_command_*` functions in upstream
5
+ * PostgreSQL's `src/bin/psql/command.c`:
6
+ *
7
+ * - `\i`, `\include` → exec_command_include (normal)
8
+ * - `\ir`, `\include_relative` → exec_command_include (relative=true)
9
+ * - `\o`, `\out` → exec_command_out
10
+ * - `\w`, `\write` → exec_command_write
11
+ * - `\g` → exec_command_g
12
+ * - `\gx` → exec_command_g (force_expanded=true)
13
+ * - `\gset` → exec_command_gset
14
+ * - `\gdesc` → exec_command_gdesc
15
+ * - `\gexec` → exec_command_gexec
16
+ * - `\watch` → exec_command_watch
17
+ *
18
+ * Each is exported as a `BackslashCmdSpec` and registered via
19
+ * {@link registerIoCommands}. The single line that wires us into the
20
+ * default dispatcher lives in `dispatch.ts::defaultRegistry()`.
21
+ *
22
+ * # Integration touch-points and known limitations
23
+ *
24
+ * Several of these commands really want to participate in the mainloop's
25
+ * scanner/printer pipeline. This WP keeps `src/psql/core/mainloop.ts`
26
+ * untouched, so we provide the data structures and let a follow-up WP wire
27
+ * the consumption sites. Limitations documented per-command:
28
+ *
29
+ * - `\i FILE` enqueues the file's contents on a small input queue
30
+ * (`./inputQueue.ts`) AND, as a stop-gap, executes the file's SQL
31
+ * directly via `Connection.execSimple`. Backslash commands embedded in
32
+ * the file are NOT processed by the scanner; the include is a "best
33
+ * effort: run as one big SQL blob". Once mainloop adopts the queue API
34
+ * this becomes a true include.
35
+ *
36
+ * - `\o FILE` opens a writable stream and stashes it under a symbol on
37
+ * `settings`. We expose a getter (`getQueryFout`) for the mainloop to
38
+ * consult; until that wiring happens, query output continues to flow
39
+ * to the mainloop's `ctx.stdout`. The stash + close-on-rebind logic is
40
+ * in place and fully tested.
41
+ *
42
+ * - `\g` (no arg) executes the current queryBuf directly through
43
+ * `Connection.execSimple` and renders via the aligned printer. This
44
+ * duplicates a tiny slice of mainloop's send/print pipeline, which is
45
+ * fine for the bytewise-simple cases this WP needs to support. For
46
+ * `\g FILE` / `\g |cmd` the output goes through the temporary writer.
47
+ *
48
+ * - `\gx` toggles `topt.expanded` for the single execution and restores
49
+ * the prior value in a `try { ... } finally { ... }`.
50
+ *
51
+ * - `\gset [PREFIX]` executes via `execSimple`, requires the last result
52
+ * to have exactly one row, and stores `${prefix}${colname}` → value
53
+ * for each column on `settings.vars`.
54
+ *
55
+ * - `\gdesc` parses the buffered query with the extended protocol
56
+ * (Parse + Describe by statement, no Execute), then assembles a
57
+ * synthetic `Column / Type` ResultSet and renders it through the
58
+ * active printer (`alignedPrinter` by default; the format picker
59
+ * honours `\pset format`). Tuples-only mode (`\t on`) and `\o FILE`
60
+ * redirects ride along automatically because the same ResultSet
61
+ * goes through the same printer the REPL would use for a query.
62
+ *
63
+ * - `\gexec` iterates the cells of the last result row-major and feeds
64
+ * each non-null cell back as SQL through `execSimple`. Each statement's
65
+ * output is rendered to stdout (or to the active queryFout stash).
66
+ *
67
+ * - `\watch [INTERVAL]` re-executes the queryBuf every `INTERVAL` seconds
68
+ * (default 2) until SIGINT or until the iteration count limit is hit.
69
+ * We hook SIGINT via a transient listener that's removed on completion.
70
+ * Tests bypass the listener by using an AbortController exposed via
71
+ * `WATCH_TEST_CONTROLLER`.
72
+ *
73
+ * # Error format
74
+ *
75
+ * Upstream prints `<cmd>: <msg>` to stderr and returns failure. We mirror
76
+ * that and also stash the message on `settings.lastErrorResult` so the
77
+ * mainloop's `writeError()` wrapper can pick it up.
78
+ */
79
+ import { spawn } from "node:child_process";
80
+ import { closeSync, createWriteStream, promises as fsPromises, fsyncSync, openSync, } from "node:fs";
81
+ import { platform } from "node:os";
82
+ import * as path from "node:path";
83
+ import { captureLastError, refreshErrorVars, stripLeadingCommentsAndWS, } from "../core/common.js";
84
+ import { alignedPrinter } from "../print/aligned.js";
85
+ import { asciidocPrinter } from "../print/asciidoc.js";
86
+ import { csvPrinter } from "../print/csv.js";
87
+ import { htmlPrinter } from "../print/html.js";
88
+ import { jsonPrinter } from "../print/json.js";
89
+ import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
90
+ import { troffMsPrinter } from "../print/troff.js";
91
+ import { unalignedPrinter } from "../print/unaligned.js";
92
+ import { applyPset } from "./cmd_format.js";
93
+ import { formatErrorReport, psqlErrorPrefix } from "./cmd_meta.js";
94
+ import { consumeBindState, lookupPrepared, stagedNamedBindPresent, } from "./cmd_pipeline.js";
95
+ import { writeErr, writeOut } from "./shared.js";
96
+ // ---------------------------------------------------------------------------
97
+ // Query-output (queryFout) stash.
98
+ //
99
+ // psql tracks a "query output" file pointer separately from stdout (see
100
+ // pset.queryFout in upstream settings.h). Our PsqlSettings type is frozen
101
+ // at WP-00, so we stash the stream on the settings object via a well-known
102
+ // symbol — the same approach used for the CondStack in cmd_cond.ts.
103
+ // ---------------------------------------------------------------------------
104
+ const QUERY_FOUT_KEY = Symbol.for("neonctl.psql.queryFout");
105
+ /**
106
+ * Return the currently active queryFout stream (or `null` if none).
107
+ * The mainloop is encouraged to call this in lieu of writing directly to
108
+ * `ctx.stdout` for query results.
109
+ */
110
+ export const getQueryFout = (settings) => {
111
+ const stash = settings;
112
+ return stash[QUERY_FOUT_KEY]?.stream ?? null;
113
+ };
114
+ const setQueryFout = (settings, entry) => {
115
+ const stash = settings;
116
+ if (entry === null) {
117
+ stash[QUERY_FOUT_KEY] = undefined;
118
+ }
119
+ else {
120
+ stash[QUERY_FOUT_KEY] = entry;
121
+ }
122
+ };
123
+ const closeQueryFout = async (settings) => {
124
+ const stash = settings;
125
+ const prev = stash[QUERY_FOUT_KEY];
126
+ if (prev) {
127
+ stash[QUERY_FOUT_KEY] = undefined;
128
+ await prev.close();
129
+ }
130
+ };
131
+ // ---------------------------------------------------------------------------
132
+ // Watch SIGINT escape hatch (tests).
133
+ //
134
+ // `\watch` installs a SIGINT handler so Ctrl-C breaks the polling loop in
135
+ // real psql sessions. Tests need to break the loop deterministically; we
136
+ // expose an AbortController hook that, if set, takes precedence.
137
+ // ---------------------------------------------------------------------------
138
+ export const WATCH_TEST_CONTROLLER = {
139
+ ref: null,
140
+ };
141
+ // ---------------------------------------------------------------------------
142
+ // Small helpers.
143
+ // ---------------------------------------------------------------------------
144
+ const errResult = (ctx, message) => {
145
+ ctx.settings.lastErrorResult = { message };
146
+ // Upstream psql prefixes every diagnostic with the `psql:[<file>:<n>]:`
147
+ // tag that `pg_log_pre_callback` adds. Mirror that here so backslash
148
+ // command errors look like upstream when surfaced via `psql_fails_like`.
149
+ const prefix = psqlErrorPrefix(ctx.settings);
150
+ writeErr(`${prefix}\\${ctx.cmdName}: ${message}\n`);
151
+ // Tell the mainloop the error has already been surfaced — without this
152
+ // it would also write a `psql: ERROR: <msg>` fallback, producing a stray
153
+ // duplicate that breaks the `\errverbose` ordering check on tests like
154
+ // `SELECT error\gdesc\n\errverbose`.
155
+ return { status: "error", errorWritten: true };
156
+ };
157
+ /**
158
+ * Reject buffer-consuming commands when an extended pipeline is open. Upstream
159
+ * `exec_command_g` / `gx` / `gset` / `gexec` / `watch` all guard with
160
+ * `PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF` and emit
161
+ * `pg_log_error("\\%s not allowed in pipeline mode", cmd)` (note: no `:`
162
+ * after the command name — different shape from `errResult`).
163
+ *
164
+ * `\gdesc` is the odd one out: upstream uses
165
+ * `pg_log_error("synchronous command execution functions are not allowed in
166
+ * pipeline mode")` because the underlying `PQdescribePrepared`/`PQfn`-style
167
+ * helpers all share that text — the regress baseline asserts this exact
168
+ * wording at three call sites in `psql_pipeline.out`.
169
+ *
170
+ * If the command proceeded it would inject a synchronous Query/Sync into the
171
+ * queue, corrupt the pipeline state, and leave `\endpipeline` waiting forever.
172
+ *
173
+ * Returns `null` when not in pipeline mode (caller proceeds); otherwise
174
+ * returns a populated error result the caller should bubble up.
175
+ *
176
+ * Upstream psql 18.4 leaks gate diagnostics through `pg_log_error_internal`
177
+ * which appends to the libpq result error log on the underlying PGresult;
178
+ * each subsequent gate hit RE-EMITS the full accumulated log plus its own
179
+ * line ("Error messages accumulate and are repeated" — the regress comment
180
+ * is the spec). Two `\gdesc` calls back-to-back therefore emit 3 lines
181
+ * total: 1 for the first call, 2 for the second (one accumulated + one
182
+ * own). Mirror that with a settings-stashed accumulator keyed off the
183
+ * current pipeline session; reset when the pipeline ends.
184
+ */
185
+ const PIPELINE_GATE_ERRORS_KEY = Symbol.for("neonctl.psql.pipelineGateErrors");
186
+ const getGateErrors = (settings) => {
187
+ const s = settings;
188
+ let cur = s[PIPELINE_GATE_ERRORS_KEY];
189
+ if (!cur) {
190
+ cur = [];
191
+ s[PIPELINE_GATE_ERRORS_KEY] = cur;
192
+ }
193
+ return cur;
194
+ };
195
+ /**
196
+ * Drop the accumulated pipeline-gate errors. Called from
197
+ * `\endpipeline` so the next pipeline session starts fresh — without
198
+ * this, gate errors from a closed pipeline would leak into the next
199
+ * one.
200
+ */
201
+ export const clearPipelineGateErrors = (settings) => {
202
+ const s = settings;
203
+ s[PIPELINE_GATE_ERRORS_KEY] = undefined;
204
+ };
205
+ const pipelineGate = (ctx) => {
206
+ if (ctx.settings.sendMode !== "extended-pipeline")
207
+ return null;
208
+ const message = ctx.cmdName === "gdesc"
209
+ ? "synchronous command execution functions are not allowed in pipeline mode"
210
+ : `\\${ctx.cmdName} not allowed in pipeline mode`;
211
+ ctx.settings.lastErrorResult = { message };
212
+ const prefix = psqlErrorPrefix(ctx.settings);
213
+ // Only `\gdesc` accumulates: each call appends its own line to the
214
+ // log AND re-emits the full log to stderr ("Error messages
215
+ // accumulate and are repeated" — regress spec at expected line 648:
216
+ // two consecutive `\gdesc` emit 1+2 = 3 lines total). Upstream's
217
+ // underlying `PQdescribePrepared` path is the one that leaks into
218
+ // the session-scoped error log; other gated commands (`\g`, `\gx`,
219
+ // `\gset`, `\gexec`, `\watch`) emit a single line per invocation
220
+ // and do NOT participate in the accumulator.
221
+ if (ctx.cmdName === "gdesc") {
222
+ const log = getGateErrors(ctx.settings);
223
+ log.push(message);
224
+ for (const m of log) {
225
+ writeErr(`${prefix}${m}\n`);
226
+ }
227
+ }
228
+ else {
229
+ writeErr(`${prefix}${message}\n`);
230
+ }
231
+ return { status: "error", errorWritten: true };
232
+ };
233
+ /**
234
+ * Set of psql variables upstream marks as "specially treated" — i.e. names
235
+ * that have a substitute / assign hook installed in `startup.c`'s
236
+ * `EstablishVariableSpace`. Used by `\gset` to reject assignments into
237
+ * those names (matching upstream `StoreQueryTuple`'s `VariableHasHook`
238
+ * check). We mirror the upstream list directly so a `\gset IGNORE` into
239
+ * `IGNOREEOF` produces the conformance-expected warning even though our
240
+ * settings.ts hasn't installed the IGNOREEOF / HISTFILE hooks yet — that
241
+ * gap is tracked separately and harmless because the values are read-only
242
+ * for us.
243
+ */
244
+ const UPSTREAM_SPECIAL_VAR_NAMES = new Set([
245
+ "AUTOCOMMIT",
246
+ "COMP_KEYWORD_CASE",
247
+ "ECHO",
248
+ "ECHO_HIDDEN",
249
+ "FETCH_COUNT",
250
+ "HIDE_TABLEAM",
251
+ "HIDE_TOAST_COMPRESSION",
252
+ "HISTCONTROL",
253
+ "HISTFILE",
254
+ "HISTSIZE",
255
+ "IGNOREEOF",
256
+ "ON_ERROR_ROLLBACK",
257
+ "ON_ERROR_STOP",
258
+ "PROMPT1",
259
+ "PROMPT2",
260
+ "PROMPT3",
261
+ "QUIET",
262
+ "SHOW_ALL_RESULTS",
263
+ "SHOW_CONTEXT",
264
+ "SINGLELINE",
265
+ "SINGLESTEP",
266
+ "VERBOSITY",
267
+ ]);
268
+ /**
269
+ * True when `name` is a psql variable that `\gset` must skip with an
270
+ * "attempt to \gset into specially treated variable" message. Combines the
271
+ * registered-hook check (so future hook installations are automatically
272
+ * covered) with the upstream-canonical list above (so cases like
273
+ * IGNOREEOF that aren't hooked in our settings.ts still match upstream's
274
+ * `\gset` behaviour exactly).
275
+ */
276
+ const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) ||
277
+ UPSTREAM_SPECIAL_VAR_NAMES.has(name);
278
+ // `stripLeadingCommentsAndWS` lives in core/common.ts so the wire path
279
+ // (sendQuery / executeAndPrint) and the slash-command paths share one
280
+ // implementation. Re-imported from there at the top of the file.
281
+ /**
282
+ * Strip line and block comments from `sql` so a COPY-shaped token inside a
283
+ * comment (e.g. dash-dash `COPY t TO STDOUT`) doesn't trigger the
284
+ * `\g FILE` mid-batch sink. Mirrors the cheap normaliser the mainloop uses
285
+ * before wiring `copyOutMidBatchSink`. Embedded literals are NOT stripped —
286
+ * `'COPY x TO STDOUT'` would still match `hasCopyToStdout`, but that's the
287
+ * same false-positive shape upstream tolerates (the regex sweep is
288
+ * intentionally conservative, and the worst-case outcome is "route bytes
289
+ * that never arrive to the file" — harmless).
290
+ */
291
+ const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/--[^\n]*/gu, "");
292
+ /**
293
+ * True when `sql` contains at least one `COPY ... TO STDOUT` segment.
294
+ * Used by `runGCore` to install a CopyData sink while `\g` / `\gx` /
295
+ * `\g FILE` / `\g |cmd` dispatches a `\;`-chained batch that mixes
296
+ * COPY-OUT with regular SELECT statements. Without the sink the wire
297
+ * layer drops the CopyData bytes on the floor, and the file/pipe ends
298
+ * up with the surrounding tuple results only — see regress/psql lines
299
+ * 5760-5787 (`COPY (SELECT 'foo') TO STDOUT \; COPY (SELECT 'bar') TO
300
+ * STDOUT \g :g_out_file`).
301
+ */
302
+ const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlCommentsForCopyScan(sql));
303
+ /**
304
+ * Render a server-side error in upstream psql's 3-line shape (severity +
305
+ * message, then `LINE N:` / `^` re-print) and refresh the `:LAST_ERROR_*`
306
+ * diagnostic variables so a subsequent `\errverbose` sees the rich payload.
307
+ *
308
+ * Mirrors the path that `core/common.ts::writeQueryError` takes for top-level
309
+ * statement errors: capture full ErrorResponse fields onto
310
+ * `settings.lastErrorResult`, render via `formatErrorReport` (honouring
311
+ * VERBOSITY + SHOW_CONTEXT), prefix only the leading severity line with
312
+ * `psql:[<file>:<n>]:`, and update the per-statement diagnostic vars via
313
+ * `refreshErrorVars`.
314
+ *
315
+ * Used by `\g`, `\gx`, `\gset`, `\gdesc`, and `\gexec` so a server-rejected
316
+ * statement dispatched through them renders the same shape vanilla psql
317
+ * produces, instead of the legacy `\<cmd>: <message>` one-liner.
318
+ */
319
+ const formatServerError = (ctx, err, sql) => {
320
+ // Stash full ErrorResponse payload so `\errverbose` can re-render later.
321
+ const msg = captureLastError(ctx.settings, err, sql);
322
+ const e = ctx.settings.lastErrorResult;
323
+ if (e) {
324
+ const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
325
+ const prefix = psqlErrorPrefix(ctx.settings);
326
+ const prefixed = [prefix + lines[0], ...lines.slice(1)];
327
+ writeErr(prefixed.join("\n") + "\n");
328
+ }
329
+ else {
330
+ // Defensive fallback — captureLastError always sets lastErrorResult,
331
+ // but if a future caller bypasses it, surface at least the message.
332
+ const prefix = psqlErrorPrefix(ctx.settings);
333
+ writeErr(`${prefix}ERROR: ${msg}\n`);
334
+ }
335
+ // Refresh `:SQLSTATE`, `:ERROR`, `:LAST_ERROR_*`, `:ROW_COUNT` so the
336
+ // following `\echo :LAST_ERROR_MESSAGE` and `\errverbose` see the new
337
+ // outcome. Matches upstream's `SetErrorVariables` call after every
338
+ // failed dispatch.
339
+ refreshErrorVars(ctx.settings, { kind: "error" });
340
+ return { status: "error", errorWritten: true };
341
+ };
342
+ /**
343
+ * Open a writable destination for `\o` / `\w` / `\g FILE` / `\g |cmd`.
344
+ *
345
+ * `target` of the form `|cmd` spawns `sh -c cmd` and pipes to its stdin.
346
+ * The returned closer waits for the child to exit and resolves to its
347
+ * status + terminating signal (if any) so callers can render
348
+ * `wait_result_to_str`-style errors. Any other string is treated as a
349
+ * file path; the file is truncated.
350
+ */
351
+ const openWriter = (target) => {
352
+ if (target.startsWith("|")) {
353
+ const cmd = target.slice(1);
354
+ const child = spawn("sh", ["-c", cmd], {
355
+ stdio: ["pipe", "inherit", "inherit"],
356
+ });
357
+ // Swallow EPIPE on the stdin pipe — the child may exit before we
358
+ // finish writing, and Node would otherwise raise an unhandled error.
359
+ child.stdin.on("error", (err) => {
360
+ if (err.code !== "EPIPE") {
361
+ // Re-raise non-EPIPE errors as a crash so they show up; tests
362
+ // run with the default unhandledRejection handler and will see
363
+ // these via the failing assertion.
364
+ throw err;
365
+ }
366
+ });
367
+ return {
368
+ stream: child.stdin,
369
+ isPipe: true,
370
+ close: () => new Promise((resolve) => {
371
+ let settled = false;
372
+ const finish = (code, signal) => {
373
+ if (settled)
374
+ return;
375
+ settled = true;
376
+ resolve({ exitCode: code, signal });
377
+ };
378
+ child.once("close", (code, signal) => {
379
+ finish(code, signal);
380
+ });
381
+ child.once("error", () => {
382
+ // spawn failure or stdio glitch — treat as a non-zero exit so
383
+ // \w sees a failure. \g intentionally ignores this, mirroring
384
+ // upstream `CloseGOutput` which only sets SHELL_ERROR /
385
+ // SHELL_EXIT_CODE.
386
+ finish(127, null);
387
+ });
388
+ // Half-close stdin so the child sees EOF and exits.
389
+ if (!child.stdin.destroyed) {
390
+ child.stdin.end();
391
+ }
392
+ }),
393
+ };
394
+ }
395
+ // Open the file synchronously up-front so a bad path (ENOENT,
396
+ // EACCES, EISDIR, …) throws here — before any write — instead of
397
+ // emitting an asynchronous `'error'` event on the lazily-opened
398
+ // WriteStream that Node would then re-raise as an unhandled
399
+ // exception and kill the process. Upstream psql calls `fopen()`
400
+ // synchronously and reports the failure via `pg_log_error` while
401
+ // continuing to read the next command, which is the behaviour we
402
+ // need to mirror for `\g FILE`, `\o FILE`, `\w FILE` and friends.
403
+ //
404
+ // Wrapping the resulting fd in `createWriteStream({ fd })` retains
405
+ // the streaming write interface the rest of the code expects. Disable
406
+ // `autoClose` so we control the close order — we fsync before close so
407
+ // a follow-on server-side `COPY FROM` (Docker bind-mount on macOS) sees
408
+ // the fully flushed file even when the next command immediately follows
409
+ // the `\g`.
410
+ const fd = openSync(target, "w");
411
+ const stream = createWriteStream(target, {
412
+ encoding: "utf8",
413
+ fd,
414
+ autoClose: false,
415
+ });
416
+ // openSync catches OPEN failures synchronously, but a WRITE-time failure
417
+ // (ENOSPC / EDQUOT after a clean open, e.g. a multi-MB result to a
418
+ // quota-limited fs) emits an asynchronous 'error'. Without a listener Node
419
+ // re-raises it as an uncaught exception and kills the whole neonctl process.
420
+ // Capture it; close() surfaces it to the caller.
421
+ let writeError = null;
422
+ stream.on("error", (err) => {
423
+ writeError = writeError ?? err;
424
+ });
425
+ return {
426
+ stream,
427
+ isPipe: false,
428
+ close: () => new Promise((resolve, reject) => {
429
+ if (writeError !== null) {
430
+ try {
431
+ closeSync(fd);
432
+ }
433
+ catch {
434
+ // swallow — the write error takes precedence
435
+ }
436
+ reject(writeError);
437
+ return;
438
+ }
439
+ // `stream.end(cb)` fires after the internal buffer drains to the
440
+ // underlying fd. Once that returns, the fd still holds dirty data
441
+ // in the kernel buffer cache; on macOS + Docker bind mounts the
442
+ // server inside the container can read the file before the cache
443
+ // flushes through to the bind mount, returning a partial view.
444
+ // Force an fsync against the open fd before closing so the
445
+ // bytes are guaranteed visible to subsequent reads — including
446
+ // server-side `COPY FROM` reading via the mount.
447
+ stream.end((err) => {
448
+ if (err) {
449
+ try {
450
+ closeSync(fd);
451
+ }
452
+ catch {
453
+ // swallow — the original error takes precedence
454
+ }
455
+ reject(err);
456
+ return;
457
+ }
458
+ try {
459
+ fsyncSync(fd);
460
+ }
461
+ catch {
462
+ // ignore — fsync best-effort; the close below still cleans up.
463
+ }
464
+ try {
465
+ closeSync(fd);
466
+ }
467
+ catch (closeErr) {
468
+ reject(closeErr);
469
+ return;
470
+ }
471
+ // Docker Desktop on macOS uses virtiofs/gRPC-FUSE for bind
472
+ // mounts; cache propagation from host writes to the container's
473
+ // view is eventual, not synchronous. A subsequent server-side
474
+ // `COPY FROM '/bind/mount/file'` can read a partial view even
475
+ // though the file is fully synced on the host. Linux + Windows
476
+ // bind mounts are coherent, so this branch is macOS-only.
477
+ if (platform() === "darwin") {
478
+ setTimeout(() => {
479
+ resolve({});
480
+ }, 25);
481
+ return;
482
+ }
483
+ resolve({});
484
+ });
485
+ }),
486
+ };
487
+ };
488
+ /**
489
+ * Map a Node.js errno (`err.code`) to the libc `strerror()` string
490
+ * upstream psql renders in its `pg_log_error("%s: %m", fname)` path.
491
+ *
492
+ * Falls back to `err.message` (with the verbose `ENOENT: ...` prefix
493
+ * stripped if present) so unmapped errno values still surface
494
+ * meaningful text instead of a cryptic Node-internal phrasing.
495
+ */
496
+ const errnoToStrerror = (err) => {
497
+ switch (err.code) {
498
+ case "ENOENT":
499
+ return "No such file or directory";
500
+ case "EACCES":
501
+ return "Permission denied";
502
+ case "EISDIR":
503
+ return "Is a directory";
504
+ case "ENOTDIR":
505
+ return "Not a directory";
506
+ case "EEXIST":
507
+ return "File exists";
508
+ case "EROFS":
509
+ return "Read-only file system";
510
+ case "ELOOP":
511
+ return "Too many levels of symbolic links";
512
+ case "ENAMETOOLONG":
513
+ return "File name too long";
514
+ case "ENOSPC":
515
+ return "No space left on device";
516
+ case "EMFILE":
517
+ return "Too many open files";
518
+ case "ENFILE":
519
+ return "Too many open files in system";
520
+ case "EIO":
521
+ return "Input/output error";
522
+ case "EFBIG":
523
+ return "File too large";
524
+ case "EDQUOT":
525
+ return "Disk quota exceeded";
526
+ case "EPERM":
527
+ return "Operation not permitted";
528
+ case "EINVAL":
529
+ return "Invalid argument";
530
+ default: {
531
+ // Strip Node's `ENOENT: no such file or directory, open '/x'`
532
+ // prefix when present so the fallback at least looks like the
533
+ // libc form. The leading `/, ` slice keeps the human-readable
534
+ // phrase ("no such file or directory") if Node's message
535
+ // mirrors the `strerror` text but lowercases it.
536
+ const m = /^[A-Z]+: ([^,]+)/.exec(err.message);
537
+ return m ? m[1] : err.message;
538
+ }
539
+ }
540
+ };
541
+ /**
542
+ * Emit a file-open failure for `\g FILE`, `\o FILE`, `\w FILE` in the
543
+ * exact shape vanilla psql produces: a bare `<path>: <strerror>` line
544
+ * on stderr, no `\<cmd>:` prefix (matches `pg_log_error` under terse
545
+ * mode, which is what `psql -X` uses).
546
+ *
547
+ * The leading `psql:[<file>:<n>]:` tag is still applied when we're
548
+ * reading SQL from a `\i FILE` include — `psqlErrorPrefix` returns ''
549
+ * for stdin so the line stays bare for the interactive / harness case.
550
+ *
551
+ * Returns an `error` envelope with `errorWritten: true` so the mainloop
552
+ * doesn't write a duplicate `psql: ERROR:` fallback.
553
+ */
554
+ const reportFileOpenFailure = (ctx, target, err) => {
555
+ const errno = err;
556
+ const phrase = errnoToStrerror(errno);
557
+ const line = `${target}: ${phrase}`;
558
+ ctx.settings.lastErrorResult = { message: line };
559
+ const prefix = psqlErrorPrefix(ctx.settings);
560
+ writeErr(`${prefix}${line}\n`);
561
+ return { status: "error", errorWritten: true };
562
+ };
563
+ /**
564
+ * True when `err` was thrown by our synchronous `openSync` in
565
+ * {@link openWriter} (i.e. has an errno `code`) and the caller should
566
+ * render it via {@link reportFileOpenFailure} rather than the generic
567
+ * `\<cmd>: <msg>` path.
568
+ */
569
+ const isFileOpenFailure = (err) => {
570
+ if (!err || typeof err !== "object")
571
+ return false;
572
+ const e = err;
573
+ return typeof e.code === "string" && e.code.startsWith("E");
574
+ };
575
+ /**
576
+ * Format a child process exit code + signal into upstream psql's
577
+ * `wait_result_to_str` style. Mirrors the C helper in
578
+ * `src/common/wait_error.c`:
579
+ *
580
+ * - exit code 127 → `command not found`
581
+ * - exit code 126 → `command was not executable`
582
+ * - any other code → `child process exited with exit code N`
583
+ * - terminated by signal S → `child process was terminated by
584
+ * signal N: <SIG>`
585
+ *
586
+ * Returns null when the child exited cleanly (code 0, no signal).
587
+ */
588
+ const formatChildWaitResult = (exitCode, signal) => {
589
+ if (signal) {
590
+ // Node doesn't expose the numeric signal number; surface the name as
591
+ // upstream's `pg_strsignal` would, with a stable prefix.
592
+ return `child process was terminated by signal: ${signal}`;
593
+ }
594
+ if (exitCode === null || exitCode === undefined)
595
+ return null;
596
+ if (exitCode === 0)
597
+ return null;
598
+ if (exitCode === 127)
599
+ return "command not found";
600
+ if (exitCode === 126)
601
+ return "command was not executable";
602
+ return `child process exited with exit code ${String(exitCode)}`;
603
+ };
604
+ /**
605
+ * Compose the `CommandComplete`-tag line upstream prints for non-tuples-
606
+ * producing results (DDL, DML without RETURNING, COPY). Mirrors
607
+ * `formatCommandTag` in `core/common.ts` — duplicated to avoid the
608
+ * cmd_io → common import cycle. Returns an empty string when no tag is
609
+ * available (e.g. `EmptyQueryResponse` carries `command = ''`).
610
+ */
611
+ const formatCommandTagText = (rs) => {
612
+ const command = (rs.command || "").trim();
613
+ if (command.length === 0)
614
+ return "";
615
+ if (command === "INSERT") {
616
+ // INSERT is the only tag with the legacy oid in front of rowCount.
617
+ return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
618
+ }
619
+ if (rs.rowCount !== null && rs.rowCount !== undefined) {
620
+ return `${command} ${String(rs.rowCount)}`;
621
+ }
622
+ return command;
623
+ };
624
+ /**
625
+ * Render a `ResultSet` to the supplied writable stream using the printer
626
+ * picked from the active `\pset format`. Upstream's `do_watch`, `do_gset`,
627
+ * `do_gexec`, and `\i` all funnel results through the standard query
628
+ * output pipeline (`ExecQueryAndProcessResults` → `printQuery`), which
629
+ * honours `\pset format`. Hard-coding the aligned printer here breaks the
630
+ * conformance harness's `psql -A` runs (which expect unaligned tuples-only
631
+ * output for things like `\watch` polled rows).
632
+ *
633
+ * Non-tuples-producing results (CommandComplete with `fields.length === 0`,
634
+ * which covers DDL, DML without RETURNING, and the post-CopyDone tag for
635
+ * `COPY ... TO STDOUT`) are rendered as a single status line instead of
636
+ * the printer's `(0 rows)` empty-table block — matching `renderResultSets`
637
+ * in `core/common.ts`. Tuples-only (`\t`) and quiet (`--quiet`) both
638
+ * suppress the tag entirely.
639
+ */
640
+ const renderResult = async (settings, rs, out) => {
641
+ // `COPY ... TO STDOUT` segment — emit the accumulated CopyData payloads
642
+ // in arrival order at this result's position in the `\;`-chain.
643
+ if (rs.copyOutBytes && rs.copyOutBytes.length > 0) {
644
+ for (const chunk of rs.copyOutBytes) {
645
+ out.write(chunk);
646
+ }
647
+ }
648
+ if (rs.fields.length === 0) {
649
+ // Mirrors `renderResultSets`'s zero-fields branch: emit the tag (e.g.
650
+ // `COPY 1`) unless quiet / tuples-only suppresses it. Without this
651
+ // branch the aligned printer renders an empty header + `(0 rows)`
652
+ // footer for the COPY-TO-STDOUT command complete, which doesn't
653
+ // match upstream (where the data already streamed via the COPY-OUT
654
+ // sink and the tag goes to the user's status stream, not the
655
+ // queryFout). The regress fixture sets QUIET=true before the
656
+ // COPY-OUT `\g` shape so the tag stays out of the file under test.
657
+ // For COPY-out results, the tag is suppressed regardless — the bytes
658
+ // already flowed; upstream's `handleCopyOut` doesn't emit `COPY N`
659
+ // on the queryFout.
660
+ if (!settings.popt.topt.tuplesOnly &&
661
+ !settings.quiet &&
662
+ !rs.copyOutBytes) {
663
+ const tag = formatCommandTagText(rs);
664
+ if (tag.length > 0)
665
+ out.write(`${tag}\n`);
666
+ }
667
+ return;
668
+ }
669
+ await pickActivePrinter(settings).printQuery(rs, settings.popt, out);
670
+ };
671
+ /**
672
+ * Pick the printer for the active output format. Mirrors `pickPrinter`
673
+ * in `core/common.ts` — duplicated here to avoid the cmd_io → common
674
+ * import cycle (common.ts depends on this file for `getQueryFout`).
675
+ *
676
+ * `wrapped` falls back to the aligned printer (which renders `wrapped`
677
+ * mode itself via `topt.format`).
678
+ */
679
+ const pickActivePrinter = (settings) => {
680
+ switch (settings.popt.topt.format) {
681
+ case "aligned":
682
+ case "wrapped":
683
+ return alignedPrinter;
684
+ case "unaligned":
685
+ return unalignedPrinter;
686
+ case "csv":
687
+ return csvPrinter;
688
+ case "json":
689
+ return jsonPrinter;
690
+ case "html":
691
+ return htmlPrinter;
692
+ case "asciidoc":
693
+ return asciidocPrinter;
694
+ case "latex":
695
+ return latexPrinter;
696
+ case "latex-longtable":
697
+ return latexLongtablePrinter;
698
+ case "troff-ms":
699
+ return troffMsPrinter;
700
+ default:
701
+ return alignedPrinter;
702
+ }
703
+ };
704
+ /**
705
+ * Pick the output target for a query result.
706
+ *
707
+ * Precedence: explicit `oneShot` (e.g. `\g FILE`) > the settings stash
708
+ * (`\o FILE`) > `process.stdout`.
709
+ */
710
+ const pickOut = (settings, oneShot) => {
711
+ if (oneShot)
712
+ return oneShot;
713
+ return getQueryFout(settings) ?? process.stdout;
714
+ };
715
+ // ---------------------------------------------------------------------------
716
+ // \i FILE / \include FILE
717
+ // ---------------------------------------------------------------------------
718
+ const runInclude = async (ctx, relative) => {
719
+ const arg = ctx.nextArg("normal");
720
+ if (arg === null || arg.length === 0) {
721
+ return errResult(ctx, "missing required argument");
722
+ }
723
+ // Resolve path: \ir resolves relative to the current input file's
724
+ // directory (if any); \i resolves relative to cwd unless absolute.
725
+ let resolved;
726
+ if (path.isAbsolute(arg)) {
727
+ resolved = arg;
728
+ }
729
+ else if (relative && ctx.settings.inputfile) {
730
+ resolved = path.resolve(path.dirname(ctx.settings.inputfile), arg);
731
+ }
732
+ else {
733
+ resolved = path.resolve(process.cwd(), arg);
734
+ }
735
+ let contents;
736
+ try {
737
+ contents = await fsPromises.readFile(resolved, "utf8");
738
+ }
739
+ catch (err) {
740
+ const msg = err instanceof Error ? err.message : String(err);
741
+ return errResult(ctx, msg);
742
+ }
743
+ // Execute the included file's SQL directly here. This is the single
744
+ // execution path for BOTH the interactive REPL and the non-interactive
745
+ // -c/-f/stdin path: the latter (`executeInputString`) does not drain the
746
+ // `\i` input queue, so an `enqueueInput()` here would (a) never run under
747
+ // -f/-c and (b) double-run interactively (the mainloop drains the queue
748
+ // AND we run execSimple). See.
749
+ if (!ctx.settings.db) {
750
+ return errResult(ctx, "no connection to the server");
751
+ }
752
+ const trimmed = contents.trim();
753
+ if (trimmed.length === 0) {
754
+ return { status: "ok" };
755
+ }
756
+ // Track the prior inputfile so `\ir` chains relative to the included
757
+ // file's directory.
758
+ const priorInputFile = ctx.settings.inputfile;
759
+ ctx.settings.inputfile = resolved;
760
+ try {
761
+ const results = await ctx.settings.db.execSimple(trimmed);
762
+ const out = pickOut(ctx.settings, null);
763
+ for (const rs of results) {
764
+ await renderResult(ctx.settings, rs, out);
765
+ }
766
+ return { status: "ok" };
767
+ }
768
+ catch (err) {
769
+ const msg = err instanceof Error ? err.message : String(err);
770
+ return errResult(ctx, msg);
771
+ }
772
+ finally {
773
+ ctx.settings.inputfile = priorInputFile;
774
+ }
775
+ };
776
+ export const cmdInclude = {
777
+ name: "i",
778
+ aliases: ["include"],
779
+ helpKey: "i",
780
+ run: (ctx) => runInclude(ctx, false),
781
+ };
782
+ export const cmdIncludeRel = {
783
+ name: "ir",
784
+ aliases: ["include_relative"],
785
+ helpKey: "ir",
786
+ run: (ctx) => runInclude(ctx, true),
787
+ };
788
+ // ---------------------------------------------------------------------------
789
+ // \o [FILE|cmd] / \out
790
+ // ---------------------------------------------------------------------------
791
+ export const cmdOut = {
792
+ name: "o",
793
+ aliases: ["out"],
794
+ helpKey: "o",
795
+ async run(ctx) {
796
+ const arg = ctx.nextArg("filepipe");
797
+ // Drain any previous target first so writes flush before we rebind.
798
+ await closeQueryFout(ctx.settings);
799
+ if (arg === null || arg.length === 0) {
800
+ // Restore default (stdout).
801
+ return { status: "ok" };
802
+ }
803
+ try {
804
+ const entry = openWriter(arg);
805
+ setQueryFout(ctx.settings, entry);
806
+ return { status: "ok" };
807
+ }
808
+ catch (err) {
809
+ // File targets fail synchronously in `openWriter` via `openSync`;
810
+ // surface them in upstream's `<path>: <strerror>` shape (bare,
811
+ // no `\o:` prefix) and continue with the loop so a follow-up
812
+ // `SELECT` still executes. Pipe spawn failures (which lack an
813
+ // errno code) fall through to the generic `\o: <msg>` path.
814
+ if (!arg.startsWith("|") && isFileOpenFailure(err)) {
815
+ return reportFileOpenFailure(ctx, arg, err);
816
+ }
817
+ const msg = err instanceof Error ? err.message : String(err);
818
+ return errResult(ctx, msg);
819
+ }
820
+ },
821
+ };
822
+ // ---------------------------------------------------------------------------
823
+ // \w FILE / \write FILE
824
+ // ---------------------------------------------------------------------------
825
+ export const cmdWrite = {
826
+ name: "w",
827
+ aliases: ["write"],
828
+ helpKey: "w",
829
+ async run(ctx) {
830
+ const arg = ctx.nextArg("filepipe");
831
+ if (arg === null || arg.length === 0) {
832
+ return errResult(ctx, "missing required argument");
833
+ }
834
+ let entry;
835
+ try {
836
+ entry = openWriter(arg);
837
+ }
838
+ catch (err) {
839
+ // Same upstream-shape pivot as `\o`: a missing / unwritable file
840
+ // path errors out as a bare `<path>: <strerror>` line and the
841
+ // shim keeps reading commands. Pipe spawn failures still use
842
+ // the generic `\w: <msg>` envelope.
843
+ if (!arg.startsWith("|") && isFileOpenFailure(err)) {
844
+ return reportFileOpenFailure(ctx, arg, err);
845
+ }
846
+ const msg = err instanceof Error ? err.message : String(err);
847
+ return errResult(ctx, msg);
848
+ }
849
+ try {
850
+ await new Promise((resolve, reject) => {
851
+ entry.stream.write(ctx.queryBuf, (err) => {
852
+ if (err)
853
+ reject(err);
854
+ else
855
+ resolve();
856
+ });
857
+ });
858
+ }
859
+ catch (err) {
860
+ // On pipe targets a fast-exiting child (e.g. `| false` or a
861
+ // command-not-found shell exit) closes its stdin before we finish
862
+ // writing, surfacing as EPIPE. Linux fires this reliably; macOS
863
+ // sometimes races it past us. In either case the child's exit
864
+ // status is what we want to report, NOT the write error — so we
865
+ // swallow EPIPE on pipes and fall through to entry.close() which
866
+ // awaits the child and emits the upstream-shape wait_result_to_str.
867
+ const isEpipe = err instanceof Error &&
868
+ err.code === "EPIPE";
869
+ if (!entry.isPipe || !isEpipe) {
870
+ try {
871
+ await entry.close();
872
+ }
873
+ catch {
874
+ // ignore
875
+ }
876
+ const msg = err instanceof Error ? err.message : String(err);
877
+ return errResult(ctx, msg);
878
+ }
879
+ }
880
+ // Wait for the target to drain. For pipe targets a non-zero exit /
881
+ // killing signal is surfaced as `<fname>: <wait_result_to_str>`,
882
+ // mirroring upstream `exec_command_write`:
883
+ //
884
+ // pg_log_error("%s: %s", fname, wait_result_to_str(result));
885
+ //
886
+ // Note that upstream's `fname` retains the leading `|`, and the
887
+ // message does NOT carry the `\w:` cmd-prefix that the other
888
+ // backslash-command errors use — `pg_log_error` writes the bare
889
+ // formatted message (under terse mode, which is the conformance
890
+ // harness setup). We bypass `errResult` to match that shape exactly.
891
+ try {
892
+ const result = await entry.close();
893
+ if (entry.isPipe) {
894
+ const msg = formatChildWaitResult(result.exitCode, result.signal);
895
+ if (msg !== null) {
896
+ // `arg` still has the leading `|`; emit it verbatim so the
897
+ // text reads `| program: child process exited with exit code 1`.
898
+ const line = `${arg}: ${msg}`;
899
+ ctx.settings.lastErrorResult = { message: line };
900
+ const prefix = psqlErrorPrefix(ctx.settings);
901
+ writeErr(`${prefix}${line}\n`);
902
+ return { status: "error", errorWritten: true };
903
+ }
904
+ }
905
+ return { status: "ok" };
906
+ }
907
+ catch (err) {
908
+ const msg = err instanceof Error ? err.message : String(err);
909
+ return errResult(ctx, msg);
910
+ }
911
+ },
912
+ };
913
+ // ---------------------------------------------------------------------------
914
+ // \g, \gx — execute the query buffer with optional one-shot redirect.
915
+ // ---------------------------------------------------------------------------
916
+ /**
917
+ * Parse the body of a `\g (option=value option2=value2 ...)` clause —
918
+ * the text between the outer parentheses, already stripped. Options
919
+ * are separated by whitespace; values may be single-quoted to embed
920
+ * spaces. Unquoted values run to the next whitespace.
921
+ *
922
+ * Mirrors upstream's `parse_slash_pgopts_list`. We deliberately stay
923
+ * narrow — the conformance corpus exercises `format=`, `csv_fieldsep=`,
924
+ * and `title=` only.
925
+ */
926
+ const parseGPsetOptions = (body) => {
927
+ const out = [];
928
+ let i = 0;
929
+ while (i < body.length) {
930
+ // Skip whitespace between pairs.
931
+ while (i < body.length && /\s/.test(body[i]))
932
+ i++;
933
+ if (i >= body.length)
934
+ break;
935
+ // Read option name up to `=`.
936
+ const optStart = i;
937
+ while (i < body.length && body[i] !== "=" && !/\s/.test(body[i]))
938
+ i++;
939
+ const option = body.slice(optStart, i);
940
+ if (option.length === 0)
941
+ break;
942
+ let value = "";
943
+ if (body[i] === "=") {
944
+ i++; // skip '='
945
+ // Value: single-quoted or unquoted.
946
+ if (body[i] === "'") {
947
+ i++;
948
+ while (i < body.length && body[i] !== "'") {
949
+ // Single-quoted strings support `''` doubling and a few
950
+ // C-style escapes (\n, \t, \\, \'). Mirror enough of the
951
+ // upstream `xslashquote` handling to round-trip the regress
952
+ // corpus.
953
+ if (body[i] === "\\" && i + 1 < body.length) {
954
+ const next = body[i + 1];
955
+ if (next === "n")
956
+ value += "\n";
957
+ else if (next === "t")
958
+ value += "\t";
959
+ else if (next === "r")
960
+ value += "\r";
961
+ else if (next === "\\")
962
+ value += "\\";
963
+ else if (next === "'")
964
+ value += "'";
965
+ else
966
+ value += next;
967
+ i += 2;
968
+ continue;
969
+ }
970
+ value += body[i++];
971
+ }
972
+ if (body[i] === "'")
973
+ i++;
974
+ }
975
+ else {
976
+ const vStart = i;
977
+ while (i < body.length && !/\s/.test(body[i]))
978
+ i++;
979
+ value = body.slice(vStart, i);
980
+ }
981
+ }
982
+ out.push({ option, value });
983
+ }
984
+ return out;
985
+ };
986
+ const runGCore = async (ctx, forceExpanded) => {
987
+ const gated = pipelineGate(ctx);
988
+ if (gated !== null)
989
+ return gated;
990
+ // Strip leading whitespace + `--`/`/* */` comments so the SQL we hand to
991
+ // the wire (and use for `LINE N:` re-print on error) matches what vanilla
992
+ // psql sends through `PQexec`. Without the strip, queryBuf accumulated
993
+ // across `\bind` re-entries carries blank+comment lines from the gap
994
+ // between the previous `\g` and this one, and the server-relative
995
+ // position lands on `LINE 3` instead of `LINE 1`.
996
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
997
+ const bufSql = trimmedBuf.trim();
998
+ let target;
999
+ let psetOverrides = null;
1000
+ // `\g (option=value ...)` — temporary pset overrides for this query
1001
+ // only. Upstream `exec_command_g` recognises a leading `(` and slurps
1002
+ // the rest of the args until matching `)`. We can't call nextArg in
1003
+ // two different modes against the BackslashContext (each mode has its
1004
+ // own cursor), so when the leading char is `(`, parse the entire raw
1005
+ // arg block ourselves; otherwise fall back to normal filepipe arg
1006
+ // extraction.
1007
+ const rawTrimmed = ctx.rawArgs.trimStart();
1008
+ if (rawTrimmed.startsWith("(")) {
1009
+ const close = rawTrimmed.indexOf(")");
1010
+ if (close === -1) {
1011
+ return errResult(ctx, "missing right parenthesis in \\g options");
1012
+ }
1013
+ // Strip parens; parse `key=value` pairs (values may be single-
1014
+ // quoted). The conformance corpus exercises `format=`,
1015
+ // `csv_fieldsep=`, and `title=` only.
1016
+ psetOverrides = parseGPsetOptions(rawTrimmed.slice(1, close).trim());
1017
+ // Anything after the matching `)` is the output target — `\g (format=csv)
1018
+ // out.txt` writes to out.txt. Previously this was dropped, so the file/pipe
1019
+ // redirect was silently ignored whenever options were present.
1020
+ const afterParen = rawTrimmed.slice(close + 1).trim();
1021
+ target = afterParen.length > 0 ? afterParen : null;
1022
+ }
1023
+ else {
1024
+ target = ctx.nextArg("filepipe");
1025
+ }
1026
+ // `\g` / `\gx` with an empty buffer re-runs the most recently submitted
1027
+ // query — upstream tracks this in `pset.last_query` and `PSQLexec` reads
1028
+ // it when the active buffer is empty. We mirror via `settings.lastQuery`,
1029
+ // populated in `sendQuery` before dispatch. Preserve trailing whitespace
1030
+ // on the re-run so the server's `position` (and the `LINE N:` echo we
1031
+ // render on failure) match upstream byte-for-byte — vanilla passes the
1032
+ // un-trimmed `pset.last_query` straight to `PQexec`.
1033
+ const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery;
1034
+ // If a `\bind_named NAME` has staged a server-side prepared statement
1035
+ // lookup, we don't need any SQL text — the prepared statement carries
1036
+ // it server-side. Skip the empty-sql guard so the bind branch below
1037
+ // can do its thing.
1038
+ const hasPendingNamedBind = stagedNamedBindPresent(ctx.settings);
1039
+ if (sql.length === 0 && !hasPendingNamedBind) {
1040
+ // No buffered SQL, no prior query, no staged bind — silent no-op
1041
+ // like upstream.
1042
+ return { status: "reset-buf", newBuf: "" };
1043
+ }
1044
+ if (!ctx.settings.db) {
1045
+ return errResult(ctx, "no connection to the server");
1046
+ }
1047
+ // Open the one-shot writer if a target was supplied; close it on the way
1048
+ // out so the file/pipe is flushed before we return.
1049
+ let oneShot = null;
1050
+ if (target !== null && target.length > 0) {
1051
+ try {
1052
+ oneShot = openWriter(target);
1053
+ }
1054
+ catch (err) {
1055
+ // A `\g FILE` whose path is unopenable (ENOENT, EACCES, EISDIR,
1056
+ // …) — typically because an unresolved `:VAR` substitution left
1057
+ // a literal `:VAR` in the path — must NOT crash the process the
1058
+ // way Node's lazy WriteStream `'error'` event would. Render in
1059
+ // upstream's bare `<path>: <strerror>` shape and continue so the
1060
+ // next command in the script still executes. Pipe spawn
1061
+ // failures retain the generic `\g: <msg>` envelope.
1062
+ if (!target.startsWith("|") && isFileOpenFailure(err)) {
1063
+ return reportFileOpenFailure(ctx, target, err);
1064
+ }
1065
+ const msg = err instanceof Error ? err.message : String(err);
1066
+ return errResult(ctx, msg);
1067
+ }
1068
+ }
1069
+ const topt = ctx.settings.popt.topt;
1070
+ // Snapshot topt BEFORE any per-query mutation so the restore in
1071
+ // `finally` covers both `\gx`'s `expanded = 'on'` and any `\g (...)`
1072
+ // pset overrides in one shot. Snapshotting AFTER the `forceExpanded`
1073
+ // mutation would persist `expanded = 'on'` across queries.
1074
+ const toptSnapshot = { ...topt };
1075
+ if (forceExpanded)
1076
+ topt.expanded = "on";
1077
+ // Apply per-query pset overrides silently. Upstream applies the
1078
+ // temporary options without emitting the status lines that
1079
+ // interactive `\pset` would.
1080
+ if (psetOverrides) {
1081
+ for (const { option, value } of psetOverrides) {
1082
+ applyPset(topt, option, value, ctx.cmdName, true);
1083
+ }
1084
+ }
1085
+ // Track for `\g` / `\gx` re-run with empty buffer. Upstream sets
1086
+ // `pset.last_query` in `PSQLexec` before dispatch.
1087
+ ctx.settings.lastQuery = sql;
1088
+ // Consume any pending `\bind` / `\bind_named` state. Upstream's
1089
+ // `\g` routes through the extended-query protocol when bind params
1090
+ // are set: anonymous `\bind` re-prepares from the buffer; named
1091
+ // `\bind_named NAME` looks up the server-side prepared statement
1092
+ // by NAME (set earlier via `\parse NAME`) and just runs Bind +
1093
+ // Execute against it.
1094
+ const bindState = consumeBindState(ctx.settings);
1095
+ let execError = null;
1096
+ // Track whether we wired the mid-batch COPY-OUT sink so the `finally`
1097
+ // can clear it deterministically — even if `execSimple` threw.
1098
+ let copyOutSinkConn = null;
1099
+ try {
1100
+ const out = pickOut(ctx.settings, oneShot?.stream ?? null);
1101
+ if (bindState?.byName) {
1102
+ // \bind_named NAME — execute the previously-prepared statement
1103
+ // identified by NAME. The cache was populated by `\parse NAME`.
1104
+ // The empty-string NAME is the upstream "unnamed" prepared
1105
+ // statement slot.
1106
+ const ps = lookupPrepared(ctx.settings, bindState.name);
1107
+ if (!ps) {
1108
+ // Synthesise a thrown-Error-like object so formatServerError can
1109
+ // render the same `ERROR: <msg>` shape vanilla emits for the
1110
+ // server's `prepared statement "X" does not exist` error.
1111
+ execError = Object.assign(new Error(`prepared statement "${bindState.name}" does not exist`), { severity: "ERROR", code: "26000" });
1112
+ }
1113
+ else {
1114
+ // Bind + Execute MUST go in one extended-protocol batch: the
1115
+ // anonymous portal is implicitly closed at the next Sync, so a
1116
+ // separate ps.bind() then ps.execute() would lose the portal in
1117
+ // between. `bindAndExecute` issues both messages before the
1118
+ // Sync.
1119
+ const rs = await ps.bindAndExecute(bindState.values);
1120
+ await renderResult(ctx.settings, rs, out);
1121
+ }
1122
+ }
1123
+ else if (bindState) {
1124
+ // Anonymous \bind — re-prepare from the current buffer (or
1125
+ // lastQuery fallback) and execute with the supplied params.
1126
+ const rs = await ctx.settings.db.query(sql, bindState.values);
1127
+ await renderResult(ctx.settings, rs, out);
1128
+ }
1129
+ else {
1130
+ // Plain `\g` / `\gx`: simple-query dispatch.
1131
+ //
1132
+ // When the batch contains `COPY ... TO STDOUT`, the wire layer
1133
+ // forwards CopyData bytes via `copyOutMidBatchSink`. Mainloop wires
1134
+ // that sink to `ctx.stdout` for top-level dispatches; here in `\g`
1135
+ // we redirect it to the current output target (`\g FILE`,
1136
+ // `\g |cmd`, or `\o`-stashed stream when neither is set). Without
1137
+ // this, `COPY (SELECT 'foo') TO STDOUT \g :file` silently drops
1138
+ // `foo` on the floor and the file ends up with only the empty
1139
+ // `(0 rows)` shape printed by `renderResult` for the wire's empty
1140
+ // ResultSet. Matches upstream's `do_copy` / `handleCopyOut` path:
1141
+ // the COPY OUT bytes go wherever the active queryFout points.
1142
+ if (hasCopyToStdout(sql)) {
1143
+ copyOutSinkConn = ctx.settings.db;
1144
+ copyOutSinkConn.copyOutMidBatchSink = (chunk) => {
1145
+ out.write(chunk);
1146
+ };
1147
+ }
1148
+ const results = await ctx.settings.db.execSimple(sql);
1149
+ for (const rs of results) {
1150
+ await renderResult(ctx.settings, rs, out);
1151
+ }
1152
+ }
1153
+ }
1154
+ catch (err) {
1155
+ execError = err;
1156
+ }
1157
+ finally {
1158
+ // Restore the pre-query topt verbatim — covers both the `\gx`
1159
+ // `expanded = 'on'` swap and any `\g (...)` pset overrides, so a
1160
+ // subsequent plain `\g` runs in the user's persistent print mode.
1161
+ Object.assign(topt, toptSnapshot);
1162
+ // Tear down the COPY-OUT sink so subsequent top-level batches reach
1163
+ // mainloop's installer with a clean slate. (Mainloop reinstalls per
1164
+ // batch; leaving ours pointed at a now-closed file would cause a
1165
+ // write-after-close on the next CopyData burst.)
1166
+ if (copyOutSinkConn)
1167
+ copyOutSinkConn.copyOutMidBatchSink = null;
1168
+ }
1169
+ // Close the one-shot writer regardless of execution success so any
1170
+ // partial output is flushed.
1171
+ //
1172
+ // Note: a non-zero exit from `\g | program` is intentionally NOT
1173
+ // surfaced as an error. Upstream `CloseGOutput` (src/bin/psql/common.c)
1174
+ // only feeds the wait status to `SetShellResultVariables`, which sets
1175
+ // `SHELL_ERROR` / `SHELL_EXIT_CODE` for user inspection — no
1176
+ // `pg_log_error` call. This matches `\g | false` in vanilla psql:
1177
+ // silent, exit code 0, the next command (`\echo after`) prints
1178
+ // normally. Bookkeeping for the SHELL_* vars is a follow-up; what
1179
+ // matters here is that we don't emit a stray "program exited" line
1180
+ // that the conformance harness would diff against an empty upstream
1181
+ // stderr.
1182
+ //
1183
+ // The only failure we still surface from a pipe target is a synchronous
1184
+ // `close()` rejection (e.g. EPIPE escaping the swallow above), which
1185
+ // would indicate a genuine bug in our wiring rather than the child
1186
+ // program's exit code.
1187
+ let pipeError = null;
1188
+ if (oneShot) {
1189
+ try {
1190
+ await oneShot.close();
1191
+ }
1192
+ catch (err) {
1193
+ pipeError = err instanceof Error ? err.message : String(err);
1194
+ }
1195
+ }
1196
+ if (execError !== null) {
1197
+ // Render in upstream's `ERROR: <msg>\nLINE N: ...\n ^` shape
1198
+ // by funnelling through `formatServerError` — same path top-level
1199
+ // statement errors take in `core/common.ts::writeQueryError`. The
1200
+ // `\<cmd>:` prefix is reserved for client-side I/O / parse errors
1201
+ // (e.g. `\g: no connection`), not server-side ErrorResponse-shaped
1202
+ // failures. Pass the COMMENT-STRIPPED buffer (`trimmedBuf`) so the
1203
+ // `LINE N:` count starts at the first content line — vanilla strips
1204
+ // leading comments + blank lines from queryBuf before `PQexec`, and
1205
+ // the server's reported `position` is a 1-based offset into THAT
1206
+ // trimmed buffer. We preserve trailing whitespace so a `\g` after
1207
+ // `SELECT $1, $2 ` still renders `LINE 1: SELECT $1, $2 ` verbatim.
1208
+ // When buffer was empty (lastQuery fallback or named-bind path), the
1209
+ // dispatched SQL is `sql` — pass that instead so the `LINE N:` echo
1210
+ // still reflects the executed statement (e.g. `\bind_named NAME \g`
1211
+ // after a `\parse NAME` of `SELECT $1, $2`).
1212
+ return formatServerError(ctx, execError, bufSql.length > 0 ? trimmedBuf : sql);
1213
+ }
1214
+ if (pipeError !== null) {
1215
+ return errResult(ctx, pipeError);
1216
+ }
1217
+ return { status: "reset-buf", newBuf: "" };
1218
+ };
1219
+ export const cmdG = {
1220
+ name: "g",
1221
+ helpKey: "g",
1222
+ run: (ctx) => runGCore(ctx, false),
1223
+ };
1224
+ export const cmdGx = {
1225
+ name: "gx",
1226
+ helpKey: "gx",
1227
+ run: (ctx) => runGCore(ctx, true),
1228
+ };
1229
+ // ---------------------------------------------------------------------------
1230
+ // \p / \print — print the current or previous query buffer.
1231
+ // ---------------------------------------------------------------------------
1232
+ /**
1233
+ * `\p` / `\print` — print the query buffer the next `\g` would execute.
1234
+ *
1235
+ * Mirrors upstream `exec_command_print` in `src/bin/psql/command.c`:
1236
+ *
1237
+ * if (query_buf && query_buf->len > 0)
1238
+ * puts(query_buf->data);
1239
+ * else if (previous_buf && previous_buf->len > 0)
1240
+ * puts(previous_buf->data);
1241
+ * else if (!pset.quiet)
1242
+ * puts(_("Query buffer is empty."));
1243
+ *
1244
+ * Buffer-vs-previous-buffer precedence matters for the regress sequence:
1245
+ *
1246
+ * SELECT 1; -- executes, previous_buf := "SELECT 1;"
1247
+ * \p -- queryBuf empty → prints previous_buf
1248
+ * SELECT 2 \r -- queryBuf="SELECT 2 ", \r resets to "" without
1249
+ * -- touching previous_buf
1250
+ * \p -- queryBuf still empty → prints previous_buf
1251
+ * SELECT 3 \p -- queryBuf="SELECT 3 ", non-empty → prints queryBuf
1252
+ *
1253
+ * Implementation notes:
1254
+ *
1255
+ * - We use `settings.lastQuery` as the previous-buffer source. Upstream
1256
+ * tracks `previous_buf` independently of `pset.last_query`, but our
1257
+ * `lastQuery` is set at the exact same point upstream sets
1258
+ * `previous_buf` (the dispatch site in `SendQuery`-equivalent code paths
1259
+ * in `core/common.ts` and `cmd_io.ts`'s `\g` implementation), so the
1260
+ * semantics match for every shape exercised by the conformance corpus.
1261
+ * - We must NOT clear queryBuf — return `status: 'ok'` so the mainloop
1262
+ * leaves the buffer untouched. The user is inspecting, not executing.
1263
+ * - `puts()` appends a trailing newline. We use `writeOut` and append `\n`
1264
+ * explicitly to match.
1265
+ */
1266
+ export const cmdPrint = {
1267
+ name: "p",
1268
+ aliases: ["print"],
1269
+ helpKey: "p",
1270
+ run: (ctx) => {
1271
+ // `queryBuf.trim()` for the emptiness check — not the printed text.
1272
+ // Upstream's `query_buf->len > 0` is a byte-length check that, in
1273
+ // upstream, is reliably zero after a `;`-dispatch (because PQexec is
1274
+ // followed by `resetPQExpBuffer(query_buf)`). Our mainloop leaves a
1275
+ // residual `\n` in queryBuf after a top-level dispatch when the next
1276
+ // source line starts with a slash command — so a raw `length > 0`
1277
+ // check here would route to the "print the buffer" arm and emit
1278
+ // `\n\n` instead of falling through to `lastQuery`. The trim-only
1279
+ // emptiness check is purely an empty-vs-content discriminator; the
1280
+ // actual writeOut still uses the un-trimmed buffer text so an inline
1281
+ // `SELECT 3 \p` correctly emits the trailing space upstream prints.
1282
+ if (ctx.queryBuf.trim().length > 0) {
1283
+ writeOut(`${ctx.queryBuf}\n`);
1284
+ }
1285
+ else if (ctx.settings.lastQuery.length > 0) {
1286
+ writeOut(`${ctx.settings.lastQuery}\n`);
1287
+ }
1288
+ else if (!ctx.settings.quiet) {
1289
+ writeOut("Query buffer is empty.\n");
1290
+ }
1291
+ return Promise.resolve({ status: "ok" });
1292
+ },
1293
+ };
1294
+ // ---------------------------------------------------------------------------
1295
+ // \gset [PREFIX]
1296
+ // ---------------------------------------------------------------------------
1297
+ const formatCell = (value) => {
1298
+ if (value === null || value === undefined)
1299
+ return "";
1300
+ if (typeof value === "string")
1301
+ return value;
1302
+ if (Buffer.isBuffer(value))
1303
+ return value.toString("utf8");
1304
+ if (typeof value === "number" ||
1305
+ typeof value === "boolean" ||
1306
+ typeof value === "bigint") {
1307
+ return String(value);
1308
+ }
1309
+ // Plain objects / arrays from JSON columns: JSON-stringify so the test
1310
+ // surface is deterministic and avoids "[object Object]".
1311
+ try {
1312
+ return JSON.stringify(value);
1313
+ }
1314
+ catch {
1315
+ return "";
1316
+ }
1317
+ };
1318
+ export const cmdGset = {
1319
+ name: "gset",
1320
+ helpKey: "gset",
1321
+ async run(ctx) {
1322
+ const gated = pipelineGate(ctx);
1323
+ if (gated !== null)
1324
+ return gated;
1325
+ // Strip leading whitespace + comments — see runGCore for the rationale.
1326
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1327
+ const bufSql = trimmedBuf.trim();
1328
+ const prefix = ctx.nextArg("normal") ?? "";
1329
+ // Empty buffer behaviour mirrors upstream `exec_command_gset`'s
1330
+ // `PSQL_CMD_SEND` return: the dispatch loop sends the active
1331
+ // `pset.last_query` (or nothing). Upstream does NOT emit an error
1332
+ // — it's a silent no-op when there's no buffer AND no prior query.
1333
+ // We mirror via `settings.lastQuery`, populated in `sendQuery` before
1334
+ // dispatch.
1335
+ const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
1336
+ if (sql.length === 0) {
1337
+ return { status: "reset-buf", newBuf: "" };
1338
+ }
1339
+ if (!ctx.settings.db) {
1340
+ return errResult(ctx, "no connection to the server");
1341
+ }
1342
+ // Track for a subsequent `\g` re-run with empty buffer. Upstream
1343
+ // `exec_command_gset` updates `pset.last_query` to the dispatched SQL
1344
+ // before sending, so a follow-on `\g` (with the buffer reset by the
1345
+ // implicit `\\` separator in `... \gset pref01_ \\ \g`) re-executes
1346
+ // this same statement and prints the result table.
1347
+ ctx.settings.lastQuery = sql;
1348
+ let results;
1349
+ try {
1350
+ results = await ctx.settings.db.execSimple(sql);
1351
+ }
1352
+ catch (err) {
1353
+ // Server-side ErrorResponse — render in upstream's 3-line shape
1354
+ // (severity + message + LINE N / caret) instead of `\gset: <msg>`.
1355
+ // Pass the comment-stripped buffer so the `LINE N:` count matches
1356
+ // vanilla. When the buffer was empty, fall back to the re-run SQL
1357
+ // — the user wants to see WHICH statement failed.
1358
+ return formatServerError(ctx, err, bufSql.length > 0 ? trimmedBuf : sql);
1359
+ }
1360
+ // `\;`-chained batches: render every result EXCEPT the last to the
1361
+ // active output before `\gset` captures the last. Upstream's
1362
+ // `ExecQueryAndProcessResults` walks the libpq result list and runs
1363
+ // `PrintQueryResults` on each one in order; the trailing `\gset`
1364
+ // applies to the FINAL result (`StoreQueryTuple` in common.c) and
1365
+ // suppresses its print. Without this loop, a script like
1366
+ // `SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset`
1367
+ // would silently drop the `three` table + the warn NOTICE's
1368
+ // surrounding tuples row.
1369
+ if (results.length > 1) {
1370
+ const out = pickOut(ctx.settings, null);
1371
+ for (let i = 0; i < results.length - 1; i++) {
1372
+ await renderResult(ctx.settings, results[i], out);
1373
+ }
1374
+ }
1375
+ // Use the last result that returned rows. Upstream uses the most-recent
1376
+ // tuples-producing statement; results without a row descriptor (e.g.
1377
+ // pure DDL) are skipped.
1378
+ // Upstream `StoreQueryTuple` only runs against the LAST PGresult and
1379
+ // only when that result is `PGRES_TUPLES_OK` (a tuples-producing
1380
+ // statement). Non-tuples results (DDL, INSERT/UPDATE without RETURNING)
1381
+ // fall through to a plain status print — `\gset` is a no-op there, NOT
1382
+ // an error. Mirror that here: pick the last result; if it isn't
1383
+ // tuples-producing, skip the variable-assignment step entirely.
1384
+ const lastRs = results[results.length - 1];
1385
+ if (!lastRs || lastRs.fields.length === 0) {
1386
+ return { status: "reset-buf", newBuf: "" };
1387
+ }
1388
+ const rs = lastRs;
1389
+ if (rs.rows.length === 0) {
1390
+ // Bare `no rows returned for \gset` (no `\gset:` prefix) — matches
1391
+ // upstream psql's `pg_log_error("no rows returned for \\gset")`.
1392
+ ctx.settings.lastErrorResult = {
1393
+ message: "no rows returned for \\gset",
1394
+ };
1395
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1396
+ writeErr(`${errPrefix}no rows returned for \\gset\n`);
1397
+ return { status: "error", errorWritten: true };
1398
+ }
1399
+ if (rs.rows.length > 1) {
1400
+ // Match upstream psql's exact wording from `exec_command_gset` —
1401
+ // bare `more than one row returned for \gset` (no `\gset:` prefix).
1402
+ // Verified against vanilla psql; vendored psql.out emits it bare.
1403
+ ctx.settings.lastErrorResult = {
1404
+ message: "more than one row returned for \\gset",
1405
+ };
1406
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1407
+ writeErr(`${errPrefix}more than one row returned for \\gset\n`);
1408
+ return { status: "error", errorWritten: true };
1409
+ }
1410
+ const row = rs.rows[0];
1411
+ for (let i = 0; i < rs.fields.length; i++) {
1412
+ const fieldName = rs.fields[i].name;
1413
+ const name = `${prefix}${fieldName}`;
1414
+ const cell = row[i];
1415
+ const isNull = cell === null || cell === undefined;
1416
+ // Upstream skips assignments where the target maps to a "specially
1417
+ // treated" variable (one with a substitute / assign hook installed)
1418
+ // whose value would be rejected by the hook. The non-special columns
1419
+ // continue to be assigned: only the offending one is skipped, with
1420
+ // an informational stderr line. See psql.out line ~240:
1421
+ // attempt to \gset into specially treated variable "IGNOREEOF" ignored
1422
+ if (isSpeciallyTreatedVar(ctx.settings, name)) {
1423
+ // The target maps to a "specially treated" variable (one with a
1424
+ // substitute / assign hook installed). Upstream skips just this
1425
+ // assignment with an informational stderr line; other columns
1426
+ // are still processed. We don't actually call the hook — even a
1427
+ // value that the hook would accept must be rejected per upstream:
1428
+ // see `exec_command_gset` and `VariableHasHook`.
1429
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1430
+ writeErr(`${errPrefix}attempt to \\gset into specially treated variable ` +
1431
+ `"${name}" ignored\n`);
1432
+ continue;
1433
+ }
1434
+ // Upstream `StoreQueryTuple` in src/bin/psql/common.c:
1435
+ //
1436
+ // if (PQgetisnull(result, 0, i))
1437
+ // UnsetVariable(pset.vars, varname);
1438
+ // else if (!SetVariable(pset.vars, varname, PQgetvalue(...))) { ... }
1439
+ //
1440
+ // i.e. a NULL cell unsets the target variable (so a subsequent
1441
+ // `:var` interpolates to the literal `:var` via the scanner's
1442
+ // unset-var passthrough) rather than setting it to the empty
1443
+ // string. Mirror that semantics here.
1444
+ if (isNull) {
1445
+ ctx.settings.vars.unset(name);
1446
+ continue;
1447
+ }
1448
+ const value = formatCell(cell);
1449
+ if (!ctx.settings.vars.set(name, value)) {
1450
+ // Bare `invalid variable name: "<name>"` (no `\gset:` prefix) —
1451
+ // matches upstream psql.out wording for `\gset` exactly.
1452
+ ctx.settings.lastErrorResult = {
1453
+ message: `invalid variable name: "${fieldName}"`,
1454
+ };
1455
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1456
+ writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
1457
+ return { status: "error", errorWritten: true };
1458
+ }
1459
+ }
1460
+ return { status: "reset-buf", newBuf: "" };
1461
+ },
1462
+ };
1463
+ // ---------------------------------------------------------------------------
1464
+ // \gdesc — describe the current query without executing it.
1465
+ //
1466
+ // Mirrors upstream `exec_command_gdesc` in `src/bin/psql/command.c`: parse
1467
+ // the buffered query through the extended protocol (Parse + Describe by
1468
+ // statement, no Execute), then build a synthetic two-column ResultSet of
1469
+ // `Column` and `Type` rows and route it through the printer the user's
1470
+ // `\pset format` selected. Tuples-only mode (`\t on`) suppresses the
1471
+ // header / `(N columns)` footer the same way it would for a real query
1472
+ // result, because we hand the synthetic ResultSet to the same printer.
1473
+ //
1474
+ // Type names come from a follow-up `SELECT ... format_type(tp, tpm)`
1475
+ // over a VALUES literal — exactly the round-trip upstream uses so
1476
+ // non-builtin types and typmod modifiers (`numeric(10,2)`, `varchar(64)`)
1477
+ // render with their canonical form.
1478
+ // ---------------------------------------------------------------------------
1479
+ /**
1480
+ * Build the SQL that resolves each describe-result column's `Type` via
1481
+ * `pg_catalog.format_type(typoid, typmod)`. We feed the names + OIDs
1482
+ * + typmods through a `VALUES` literal so the server does the formatting
1483
+ * for us — the same query upstream issues from `describeFieldsByType`.
1484
+ *
1485
+ * Returns null when there are zero fields (caller emits `(0 rows)` form
1486
+ * by hand because PostgreSQL rejects an empty VALUES list).
1487
+ */
1488
+ const buildGdescFormatQuery = (fields) => {
1489
+ if (fields.length === 0)
1490
+ return null;
1491
+ // Each row literal escapes the column name with the standard E'' string
1492
+ // form so embedded quotes survive the round trip. The pg_type catalogue
1493
+ // expects oid + int4 typmod, so we cast accordingly. `_idx` keeps the
1494
+ // VALUES list in insertion order; `format_type` handles -1 typmod
1495
+ // (== "no modifier") natively.
1496
+ const rows = fields
1497
+ .map((f, i) => {
1498
+ const safeName = f.name.replace(/'/gu, "''");
1499
+ const oid = String(f.dataTypeID >>> 0);
1500
+ const typmod = String(f.dataTypeModifier | 0);
1501
+ return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
1502
+ })
1503
+ .join(", ");
1504
+ // ORDER BY _idx preserves the describe order regardless of how the server
1505
+ // happens to evaluate the VALUES list. Aliases match upstream column
1506
+ // titles exactly so the printer header is identical.
1507
+ return ('SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type"' +
1508
+ ` FROM (VALUES ${rows}) AS x(_idx, name, tp, tpm) ORDER BY _idx`);
1509
+ };
1510
+ /**
1511
+ * Field descriptors for the synthetic `Column / Type` ResultSet that
1512
+ * `\gdesc` emits when format_type resolution fails or yields nothing.
1513
+ *
1514
+ * We fall back to the field's raw OID so the user still sees a value.
1515
+ */
1516
+ const GDESC_SYNTHETIC_FIELDS = [
1517
+ {
1518
+ name: "Column",
1519
+ tableID: 0,
1520
+ columnID: 0,
1521
+ dataTypeID: 25, // text
1522
+ dataTypeSize: -1,
1523
+ dataTypeModifier: -1,
1524
+ format: 0,
1525
+ },
1526
+ {
1527
+ name: "Type",
1528
+ tableID: 0,
1529
+ columnID: 0,
1530
+ dataTypeID: 25, // text
1531
+ dataTypeSize: -1,
1532
+ dataTypeModifier: -1,
1533
+ format: 0,
1534
+ },
1535
+ ];
1536
+ const buildSyntheticGdescResultSet = (rows) => ({
1537
+ command: "SELECT",
1538
+ rowCount: rows.length,
1539
+ oid: null,
1540
+ fields: GDESC_SYNTHETIC_FIELDS,
1541
+ rows,
1542
+ notices: [],
1543
+ });
1544
+ export const cmdGdesc = {
1545
+ name: "gdesc",
1546
+ helpKey: "gdesc",
1547
+ async run(ctx) {
1548
+ const gated = pipelineGate(ctx);
1549
+ if (gated !== null)
1550
+ return gated;
1551
+ // Strip leading whitespace + comments — see runGCore for the rationale.
1552
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1553
+ const sql = trimmedBuf.trim();
1554
+ if (sql.length === 0) {
1555
+ // Upstream `\gdesc` with no buffer falls through `PSQL_CMD_SEND` to
1556
+ // the printer which renders the synthetic 0-column result via
1557
+ // `PrintQueryStatus`'s "The command has no result, or the result
1558
+ // has no columns." line. Stdout, exit 0 — not an error. Verified
1559
+ // against vanilla psql 18.
1560
+ process.stdout.write("The command has no result, or the result has no columns.\n");
1561
+ // Match upstream's post-PSQL_CMD_SEND state vars: success, 0 rows.
1562
+ refreshErrorVars(ctx.settings, { kind: "success", rowCount: 0 });
1563
+ return { status: "reset-buf", newBuf: "" };
1564
+ }
1565
+ if (!ctx.settings.db) {
1566
+ return errResult(ctx, "no connection to the server");
1567
+ }
1568
+ // Track for a subsequent `\g` re-run with empty buffer. Upstream
1569
+ // `exec_command_gdesc` updates `pset.last_query` to the dispatched SQL
1570
+ // before sending, so a follow-on `\g` (with the buffer reset because
1571
+ // `\gdesc` dispatches via PSQL_CMD_SEND) re-executes this same statement
1572
+ // and prints the result table. Without this, the regress sequence
1573
+ // SELECT 1 AS x, ... \gdesc
1574
+ // \g
1575
+ // would silently drop the `\g` (empty buffer + stale lastQuery), and
1576
+ // any later `TABLE bububu;` failure would taint `\g`'s re-run output.
1577
+ ctx.settings.lastQuery = sql;
1578
+ let fields;
1579
+ try {
1580
+ const stmt = await ctx.settings.db.prepare("", sql);
1581
+ fields = await stmt.describe();
1582
+ // Close the unnamed prepared statement so we don't leak it. Failure
1583
+ // to close (e.g. server already in error state) is non-fatal.
1584
+ try {
1585
+ await stmt.close();
1586
+ }
1587
+ catch {
1588
+ // ignore
1589
+ }
1590
+ }
1591
+ catch (err) {
1592
+ // Capture + render the full ErrorResponse-shaped payload in upstream's
1593
+ // 3-line shape (severity + message + LINE N / caret), refresh the
1594
+ // diagnostic vars, and signal `errorWritten` to the mainloop so the
1595
+ // `\errverbose` re-render after `\gdesc` sees the rich layers. Pass
1596
+ // the comment-stripped buffer so the `LINE N:` count starts at the
1597
+ // first content line (matches vanilla — see runGCore).
1598
+ return formatServerError(ctx, err, trimmedBuf);
1599
+ }
1600
+ // When the prepared statement describes back zero columns (DDL, empty
1601
+ // SELECT list, etc.), upstream `exec_command_gdesc` prints the
1602
+ // pg_log_info "The command has no result, or the result has no
1603
+ // columns." line to stdout and skips the synthetic-table render.
1604
+ // Verified against vanilla psql 18: `SELECT \gdesc` and
1605
+ // `CREATE TABLE bububu(a int) \gdesc` both produce that text.
1606
+ if (fields.length === 0) {
1607
+ process.stdout.write("The command has no result, or the result has no columns.\n");
1608
+ // Match upstream's post-PSQL_CMD_SEND state vars: success, 0 rows.
1609
+ refreshErrorVars(ctx.settings, { kind: "success", rowCount: 0 });
1610
+ return { status: "reset-buf", newBuf: "" };
1611
+ }
1612
+ // Resolve canonical type names via a follow-up round trip when we have
1613
+ // at least one field. On failure (or when the server returns nothing —
1614
+ // a mock or an unusual connection state) fall back to the raw OID so
1615
+ // the user still sees a row per described column.
1616
+ let rows;
1617
+ const formatQuery = buildGdescFormatQuery(fields);
1618
+ if (formatQuery === null) {
1619
+ rows = [];
1620
+ }
1621
+ else {
1622
+ const fallbackRows = () => fields.map((f) => [f.name, String(f.dataTypeID)]);
1623
+ try {
1624
+ const sets = await ctx.settings.db.execSimple(formatQuery);
1625
+ const last = sets[sets.length - 1];
1626
+ rows =
1627
+ last && last.rows.length > 0 ? last.rows : fallbackRows();
1628
+ }
1629
+ catch {
1630
+ rows = fallbackRows();
1631
+ }
1632
+ }
1633
+ const rs = buildSyntheticGdescResultSet(rows);
1634
+ const printer = pickActivePrinter(ctx.settings);
1635
+ const out = pickOut(ctx.settings, null);
1636
+ try {
1637
+ await printer.printQuery(rs, ctx.settings.popt, out);
1638
+ }
1639
+ catch (err) {
1640
+ const msg = err instanceof Error ? err.message : String(err);
1641
+ return errResult(ctx, msg);
1642
+ }
1643
+ // Refresh state vars to mark the describe success: `:ERROR=false`,
1644
+ // `:SQLSTATE=00000`, `:ROW_COUNT=<#-described-columns>`. Upstream
1645
+ // routes `\gdesc` through `PSQL_CMD_SEND` so its post-dispatch
1646
+ // `SetResultVariables` sees the synthetic 2-column tuple result and
1647
+ // assigns ROW_COUNT to the field count we just rendered.
1648
+ refreshErrorVars(ctx.settings, {
1649
+ kind: "success",
1650
+ rowCount: rs.rowCount,
1651
+ });
1652
+ return { status: "reset-buf", newBuf: "" };
1653
+ },
1654
+ };
1655
+ // ---------------------------------------------------------------------------
1656
+ // \gexec — treat each cell of the result as SQL to execute.
1657
+ // ---------------------------------------------------------------------------
1658
+ export const cmdGexec = {
1659
+ name: "gexec",
1660
+ helpKey: "gexec",
1661
+ async run(ctx) {
1662
+ const gated = pipelineGate(ctx);
1663
+ if (gated !== null)
1664
+ return gated;
1665
+ // Strip leading whitespace + comments — see runGCore for the rationale.
1666
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1667
+ const bufSql = trimmedBuf.trim();
1668
+ // Upstream `\gexec` with no buffer falls through `PSQL_CMD_SEND` and
1669
+ // re-runs `pset.last_query` (or nothing). Silent on empty + no prior
1670
+ // query — exit 0, no message. Verified against vanilla psql 18.
1671
+ const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
1672
+ if (sql.length === 0) {
1673
+ return { status: "reset-buf", newBuf: "" };
1674
+ }
1675
+ if (!ctx.settings.db) {
1676
+ return errResult(ctx, "no connection to the server");
1677
+ }
1678
+ // Track the outer (meta) query for a subsequent `\g` re-run with an empty
1679
+ // buffer. Upstream `exec_command_gexec` runs through PSQL_CMD_SEND, which
1680
+ // bumps `pset.last_query` before dispatch.
1681
+ ctx.settings.lastQuery = sql;
1682
+ let firstPass;
1683
+ try {
1684
+ firstPass = await ctx.settings.db.execSimple(sql);
1685
+ }
1686
+ catch (err) {
1687
+ // Render the first-pass server error in upstream's 3-line shape.
1688
+ // Pass the comment-stripped buffer so the `LINE N:` count matches
1689
+ // vanilla — see runGCore for the rationale.
1690
+ return formatServerError(ctx, err, trimmedBuf);
1691
+ }
1692
+ const tupled = firstPass.filter((r) => r.fields.length > 0);
1693
+ if (tupled.length === 0) {
1694
+ return { status: "reset-buf", newBuf: "" };
1695
+ }
1696
+ const out = pickOut(ctx.settings, null);
1697
+ // Echo each generated SQL when ECHO is `all` or `queries`. Vanilla
1698
+ // `exec_command_gexec` calls `SendQuery` for each row's text, and
1699
+ // SendQuery itself prints the statement via the standard query-echo
1700
+ // path: stdout, no `\gexec:` / `psql:` prefix, trailing LF. The echo
1701
+ // appears BEFORE the result body so the conformance harness sees
1702
+ // the same interleaving vanilla produces.
1703
+ const echo = ctx.settings.echo;
1704
+ const shouldEcho = echo === "all" || echo === "queries";
1705
+ // Per-row errors are tolerated: upstream `\gexec` calls
1706
+ // `SendQuery` in a loop and ignores its return value (the only
1707
+ // escape is the global ON_ERROR_STOP variable, which the
1708
+ // conformance harness sets to 0). Without this, the regress
1709
+ // expects `drop table gexec_test\nERROR: ...\nselect ...` and we'd
1710
+ // truncate at the ERROR.
1711
+ let sawError = false;
1712
+ for (const rs of tupled) {
1713
+ for (const row of rs.rows) {
1714
+ for (const cell of row) {
1715
+ if (cell === null || cell === undefined)
1716
+ continue;
1717
+ const statement = formatCell(cell).trim();
1718
+ if (statement.length === 0)
1719
+ continue;
1720
+ if (shouldEcho) {
1721
+ out.write(statement + "\n");
1722
+ }
1723
+ try {
1724
+ const nested = await ctx.settings.db.execSimple(statement);
1725
+ for (const sub of nested) {
1726
+ if (sub.fields.length > 0) {
1727
+ await renderResult(ctx.settings, sub, out);
1728
+ }
1729
+ }
1730
+ }
1731
+ catch (err) {
1732
+ // Each iteration is its own statement; render the per-row
1733
+ // server error in upstream's 3-line shape (LINE / caret are
1734
+ // positioned against `statement`, the offending row text)
1735
+ // but DO NOT return — vanilla continues to the next row.
1736
+ formatServerError(ctx, err, statement);
1737
+ sawError = true;
1738
+ // Honour ON_ERROR_STOP: when set, halt the loop after the
1739
+ // first failing row. Upstream's `do_gexec` consults the
1740
+ // global `pset.on_error_stop` flag via `SendQuery`'s
1741
+ // return; we mirror by checking the setting directly.
1742
+ if (ctx.settings.onErrorStop) {
1743
+ return { status: "error", errorWritten: true };
1744
+ }
1745
+ }
1746
+ }
1747
+ }
1748
+ }
1749
+ // Even with errors, return `reset-buf` so the mainloop clears the
1750
+ // outer `\gexec` buffer. Per-row error rendering already happened;
1751
+ // returning `error` here would re-trigger the writeError path.
1752
+ void sawError;
1753
+ return { status: "reset-buf", newBuf: "" };
1754
+ },
1755
+ };
1756
+ // ---------------------------------------------------------------------------
1757
+ // \watch [args...]
1758
+ //
1759
+ // Upstream `\watch` accepts:
1760
+ //
1761
+ // \watch [SEC] — legacy positional interval (seconds)
1762
+ // \watch i=SEC — interval as named flag
1763
+ // \watch c=N — iteration count limit
1764
+ // \watch m=N — minimum row count: keep polling until the
1765
+ // result has >= N rows; uses `interval` as the
1766
+ // sleep between polls
1767
+ // \watch min_rows=N — long-form alias of `m=`
1768
+ //
1769
+ // Flags may be combined in any order. Duplicates (including the positional
1770
+ // interval colliding with `i=`) are rejected upstream with the message
1771
+ // "<thing> is specified more than once".
1772
+ //
1773
+ // The `WATCH_INTERVAL` psql variable supplies the default `interval` value
1774
+ // when `i=` is not given (and when there is no positional). The variable is
1775
+ // validated at `\set` time via a hook installed by `defaultSettings`.
1776
+ // ---------------------------------------------------------------------------
1777
+ const sleepCancellable = (ms, signal) => new Promise((resolve) => {
1778
+ const timer = setTimeout(() => {
1779
+ signal.removeEventListener("abort", onAbort);
1780
+ resolve();
1781
+ }, ms);
1782
+ const onAbort = () => {
1783
+ clearTimeout(timer);
1784
+ signal.removeEventListener("abort", onAbort);
1785
+ resolve();
1786
+ };
1787
+ if (signal.aborted) {
1788
+ clearTimeout(timer);
1789
+ resolve();
1790
+ return;
1791
+ }
1792
+ signal.addEventListener("abort", onAbort);
1793
+ });
1794
+ /**
1795
+ * Strictly parse a non-negative finite float.
1796
+ *
1797
+ * Returns the parsed number, or `null` for any of:
1798
+ * - empty string
1799
+ * - non-numeric trailing characters (e.g. `10ab`)
1800
+ * - negative values (e.g. `-10`)
1801
+ * - out-of-range / non-finite results (e.g. `10e400` → Infinity)
1802
+ *
1803
+ * Used to validate `\watch` intervals and the `WATCH_INTERVAL` variable.
1804
+ */
1805
+ const parseStrictNonNegativeFloat = (raw) => {
1806
+ if (raw.length === 0)
1807
+ return null;
1808
+ // Reject anything that doesn't look like a plain float literal. We
1809
+ // accept optional sign + digits + optional fractional + optional
1810
+ // exponent. Trailing garbage (`10ab`), negative values, and exponents
1811
+ // that overflow to Infinity all funnel into the null result.
1812
+ const re = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
1813
+ if (!re.test(raw))
1814
+ return null;
1815
+ const value = parseFloat(raw);
1816
+ if (!Number.isFinite(value))
1817
+ return null;
1818
+ if (value < 0)
1819
+ return null;
1820
+ return value;
1821
+ };
1822
+ /**
1823
+ * Parse a strict non-negative integer (no exponent, no fractional).
1824
+ * Used for `c=` and `m=` / `min_rows=` argument values.
1825
+ */
1826
+ const parseStrictNonNegativeInt = (raw) => {
1827
+ if (raw.length === 0)
1828
+ return null;
1829
+ if (!/^\d+$/.test(raw))
1830
+ return null;
1831
+ const value = parseInt(raw, 10);
1832
+ if (!Number.isFinite(value))
1833
+ return null;
1834
+ return value;
1835
+ };
1836
+ /**
1837
+ * Default `\watch` interval (seconds). Mirrors upstream
1838
+ * `DEFAULT_WATCH_INTERVAL`. Exported so `defaultSettings` can substitute
1839
+ * it when the user unsets the `WATCH_INTERVAL` variable — upstream's
1840
+ * `watch_interval_substitute_hook` reseeds the value to `2` on null.
1841
+ */
1842
+ export const DEFAULT_WATCH_INTERVAL = "2";
1843
+ /**
1844
+ * Render `\watch`'s per-iteration timestamp in upstream psql's
1845
+ * `ctime`-style layout: `Day Mon DD HH:MM:SS YYYY` (e.g. `Mon May 25
1846
+ * 19:41:55 2026`). Upstream calls `strftime("%c", &tm)` with the C locale;
1847
+ * we reproduce the field order in vanilla English so the output matches
1848
+ * regardless of the host locale.
1849
+ *
1850
+ * Exported only for unit-testing the format ladder.
1851
+ */
1852
+ const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1853
+ const MONTHS = [
1854
+ "Jan",
1855
+ "Feb",
1856
+ "Mar",
1857
+ "Apr",
1858
+ "May",
1859
+ "Jun",
1860
+ "Jul",
1861
+ "Aug",
1862
+ "Sep",
1863
+ "Oct",
1864
+ "Nov",
1865
+ "Dec",
1866
+ ];
1867
+ const pad2 = (n) => (n < 10 ? `0${String(n)}` : String(n));
1868
+ export const formatWatchTimestamp = (now) => {
1869
+ const weekday = WEEKDAYS[now.getDay()];
1870
+ const month = MONTHS[now.getMonth()];
1871
+ const day = pad2(now.getDate());
1872
+ const hh = pad2(now.getHours());
1873
+ const mm = pad2(now.getMinutes());
1874
+ const ss = pad2(now.getSeconds());
1875
+ const year = String(now.getFullYear());
1876
+ return `${weekday} ${month} ${day} ${hh}:${mm}:${ss} ${year}`;
1877
+ };
1878
+ /**
1879
+ * Upper bound on the `WATCH_INTERVAL` variable and the positional interval
1880
+ * — matches upstream which rejects "out of range" values. Upstream uses
1881
+ * `strtod` and rejects ±Infinity; we tighten further so a single watch loop
1882
+ * cannot sleep for longer than ~100 hours, which catches obvious typos
1883
+ * without breaking legitimate slow polls.
1884
+ */
1885
+ const WATCH_INTERVAL_MAX_SECONDS = 100 * 3600;
1886
+ /**
1887
+ * Resolve the effective default `\watch` interval from the `WATCH_INTERVAL`
1888
+ * psql variable. Returns the parsed value, the documented default
1889
+ * (`DEFAULT_WATCH_INTERVAL`), or an `error` envelope if the variable is set
1890
+ * but parses out of range.
1891
+ */
1892
+ const resolveWatchIntervalDefault = (settings) => {
1893
+ // The variable is seeded to `DEFAULT_WATCH_INTERVAL` by `defaultSettings`
1894
+ // (and re-seeded on `\unset`), so it's typically a string at use time.
1895
+ // If a future code path leaves it undefined we fall back to the same
1896
+ // documented default — upstream's `ParseVariableDouble` substitutes
1897
+ // `DEFAULT_WATCH_INTERVAL` when the var slot is empty.
1898
+ const raw = settings.vars.get("WATCH_INTERVAL") ?? DEFAULT_WATCH_INTERVAL;
1899
+ const parsed = parseStrictNonNegativeFloat(raw);
1900
+ if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) {
1901
+ return {
1902
+ error: `WATCH_INTERVAL "${raw}" is out of range`,
1903
+ };
1904
+ }
1905
+ return { value: parsed };
1906
+ };
1907
+ /**
1908
+ * Spawn the `\watch` pager for the full duration of the polling loop.
1909
+ *
1910
+ * Upstream `do_watch` wraps the loop in a single `popen` of
1911
+ * `PSQL_WATCH_PAGER`. It deliberately ignores `PSQL_PAGER` and `$PAGER`:
1912
+ *
1913
+ * > we ignore the regular PSQL_PAGER or PAGER environment variables,
1914
+ * > because traditional pagers probably won't be very useful for
1915
+ * > showing a stream of results.
1916
+ *
1917
+ * Mirror that here. Reading from `$PAGER` would silently hijack
1918
+ * `\watch` output for any user whose shell sets `PAGER=less` (the
1919
+ * common default), which makes the loop's output disappear into a
1920
+ * subprocess that diff harnesses can't capture.
1921
+ *
1922
+ * We use a single `sh -c <pager>` spawn so the user can set the
1923
+ * variable to a full command string (`less -R`, `tee /tmp/log`, …)
1924
+ * without the caller having to tokenise it. EPIPE on the stdin pipe is
1925
+ * swallowed for the same reason as in `openWriter`: the user may quit
1926
+ * `less` while we still have writes pending in the next iteration.
1927
+ *
1928
+ * Returns `null` when `PSQL_WATCH_PAGER` is unset or whitespace-only
1929
+ * (upstream's "no pager" rule), so the caller falls back to the normal
1930
+ * output target.
1931
+ */
1932
+ const openWatchPager = () => {
1933
+ const cmd = process.env.PSQL_WATCH_PAGER ?? "";
1934
+ if (cmd.trim().length === 0)
1935
+ return null;
1936
+ const child = spawn("sh", ["-c", cmd], {
1937
+ stdio: ["pipe", "inherit", "inherit"],
1938
+ });
1939
+ child.stdin.on("error", (err) => {
1940
+ if (err.code !== "EPIPE") {
1941
+ throw err;
1942
+ }
1943
+ });
1944
+ return {
1945
+ stream: child.stdin,
1946
+ close: () => new Promise((resolve) => {
1947
+ // If the pager already exited (e.g. PSQL_WATCH_PAGER=false, or the
1948
+ // user quit it) the 'close'/'error' events have ALREADY fired, so a
1949
+ // freshly-registered `once()` listener would never run and close()
1950
+ // would hang forever — taking `\watch` with it.
1951
+ if (child.exitCode !== null || child.signalCode !== null) {
1952
+ resolve();
1953
+ return;
1954
+ }
1955
+ let settled = false;
1956
+ const finish = () => {
1957
+ if (settled)
1958
+ return;
1959
+ settled = true;
1960
+ resolve();
1961
+ };
1962
+ child.once("close", finish);
1963
+ child.once("error", finish);
1964
+ if (!child.stdin.destroyed) {
1965
+ try {
1966
+ child.stdin.end();
1967
+ }
1968
+ catch (err) {
1969
+ const e = err;
1970
+ if (e.code !== "EPIPE")
1971
+ finish();
1972
+ }
1973
+ }
1974
+ }),
1975
+ };
1976
+ };
1977
+ export const cmdWatch = {
1978
+ name: "watch",
1979
+ helpKey: "watch",
1980
+ async run(ctx) {
1981
+ const gated = pipelineGate(ctx);
1982
+ if (gated !== null)
1983
+ return gated;
1984
+ // Strip leading whitespace + comments — see runGCore for the rationale.
1985
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1986
+ const sql = trimmedBuf.trim();
1987
+ if (sql.length === 0) {
1988
+ return errResult(ctx, "no query buffer");
1989
+ }
1990
+ if (!ctx.settings.db) {
1991
+ return errResult(ctx, "no connection to the server");
1992
+ }
1993
+ // Track which options have been seen so we can reject duplicates with
1994
+ // the upstream-formatted "<thing> is specified more than once" message.
1995
+ let intervalSet = false;
1996
+ let interval = null;
1997
+ let iterSet = false;
1998
+ let iterMax = 0; // 0 = unlimited (matches upstream's "no -c").
1999
+ let minRowsSet = false;
2000
+ let minRows = 0;
2001
+ let positionalSeen = false;
2002
+ // Drain all args. Each is either a `key=value` token or a bare
2003
+ // positional (only allowed as the very first arg, and only once).
2004
+ while (true) {
2005
+ const arg = ctx.nextArg("normal");
2006
+ if (arg === null)
2007
+ break;
2008
+ if (arg.length === 0)
2009
+ continue;
2010
+ // Identify named flags by looking for `=`. Upstream tolerates an
2011
+ // empty value (treats it as the option not being provided), but we
2012
+ // mirror its stricter behaviour for the values we care about.
2013
+ const eqIdx = arg.indexOf("=");
2014
+ if (eqIdx > 0) {
2015
+ const key = arg.slice(0, eqIdx);
2016
+ const value = arg.slice(eqIdx + 1);
2017
+ if (key === "i") {
2018
+ if (intervalSet) {
2019
+ return errResult(ctx, "interval value is specified more than once");
2020
+ }
2021
+ const parsed = parseStrictNonNegativeFloat(value);
2022
+ if (parsed === null ||
2023
+ parsed > WATCH_INTERVAL_MAX_SECONDS) {
2024
+ return errResult(ctx, `incorrect interval value "${value}"`);
2025
+ }
2026
+ interval = parsed;
2027
+ intervalSet = true;
2028
+ continue;
2029
+ }
2030
+ if (key === "c") {
2031
+ if (iterSet) {
2032
+ return errResult(ctx, "iteration count is specified more than once");
2033
+ }
2034
+ const parsed = parseStrictNonNegativeInt(value);
2035
+ // Upstream parses the count with `option_parse_int(..., 1, INT_MAX)`
2036
+ // so the iteration count must be >= 1; `c=0` is rejected as out of
2037
+ // range. We reserve the internal `iterMax = 0` sentinel purely for
2038
+ // "no `c=` given" (unlimited continuous mode), so accepting `c=0`
2039
+ // here would silently cap the loop at a single iteration instead.
2040
+ if (parsed === null || parsed === 0) {
2041
+ return errResult(ctx, `incorrect iteration count "${value}"`);
2042
+ }
2043
+ iterMax = parsed;
2044
+ iterSet = true;
2045
+ continue;
2046
+ }
2047
+ if (key === "m" || key === "min_rows") {
2048
+ if (minRowsSet) {
2049
+ return errResult(ctx, "minimum row count specified more than once");
2050
+ }
2051
+ const parsed = parseStrictNonNegativeInt(value);
2052
+ if (parsed === null) {
2053
+ return errResult(ctx, `incorrect minimum row count "${value}"`);
2054
+ }
2055
+ minRows = parsed;
2056
+ minRowsSet = true;
2057
+ continue;
2058
+ }
2059
+ // Unknown key=value: surface a generic error mirroring upstream
2060
+ // ("unrecognized value …").
2061
+ return errResult(ctx, `unrecognized option "${key}"`);
2062
+ }
2063
+ // Positional argument — legacy interval. Allowed only once, and
2064
+ // only collides with `i=` under the same upstream "specified more
2065
+ // than once" rubric.
2066
+ if (positionalSeen || intervalSet) {
2067
+ return errResult(ctx, "interval value is specified more than once");
2068
+ }
2069
+ const parsed = parseStrictNonNegativeFloat(arg);
2070
+ if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) {
2071
+ return errResult(ctx, `incorrect interval value "${arg}"`);
2072
+ }
2073
+ interval = parsed;
2074
+ intervalSet = true;
2075
+ positionalSeen = true;
2076
+ }
2077
+ // If no explicit interval was supplied, fall back to WATCH_INTERVAL.
2078
+ if (interval === null) {
2079
+ const resolved = resolveWatchIntervalDefault(ctx.settings);
2080
+ if ("error" in resolved) {
2081
+ return errResult(ctx, resolved.error);
2082
+ }
2083
+ interval = resolved.value;
2084
+ }
2085
+ const intervalMs = Math.round(interval * 1000);
2086
+ // Prefer a test-supplied controller; otherwise install a transient
2087
+ // SIGINT listener that aborts the loop.
2088
+ const controller = WATCH_TEST_CONTROLLER.ref ?? new AbortController();
2089
+ const sigintHandler = () => {
2090
+ controller.abort();
2091
+ };
2092
+ const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
2093
+ if (installedSigint) {
2094
+ process.once("SIGINT", sigintHandler);
2095
+ }
2096
+ // Open the pager once for the whole loop (upstream `do_watch` wraps the
2097
+ // entire session, not each iteration, so the user can scroll the
2098
+ // accumulated output in one go). When PSQL_WATCH_PAGER / PAGER aren't
2099
+ // set we fall through to the normal `pickOut` target.
2100
+ const pager = openWatchPager();
2101
+ const out = pager?.stream ?? pickOut(ctx.settings, null);
2102
+ try {
2103
+ // CONTINUOUS mode: when `c=` is absent, `iterSet` stays false and the
2104
+ // iteration-cap break below never fires, so the loop re-runs the query
2105
+ // on the interval forever — exactly upstream `do_watch`'s `for (i = 0;
2106
+ // !iter || i < iter; i++)` when `iter == 0`. The only exits are a
2107
+ // SIGINT (or the test controller) aborting `controller.signal`, a
2108
+ // server error, or the `min_rows` CONTINUE predicate failing.
2109
+ let iter = 0;
2110
+ while (!controller.signal.aborted) {
2111
+ iter++;
2112
+ const stamp = formatWatchTimestamp(new Date());
2113
+ out.write(`${stamp} (every ${String(interval)}s)\n\n`);
2114
+ let lastRowCount = 0;
2115
+ try {
2116
+ const results = await ctx.settings.db.execSimple(sql);
2117
+ for (const rs of results) {
2118
+ if (rs.fields.length > 0) {
2119
+ await renderResult(ctx.settings, rs, out);
2120
+ lastRowCount = rs.rows.length;
2121
+ }
2122
+ }
2123
+ }
2124
+ catch (err) {
2125
+ // Surface in upstream's 3-line ErrorResponse shape (severity +
2126
+ // message + LINE / caret) — same path top-level statement errors
2127
+ // take. The `\watch:` prefix is reserved for client-side
2128
+ // argument-parsing errors (e.g. `incorrect interval value "-10"`).
2129
+ // Pass the comment-stripped buffer so the `LINE N:` count starts
2130
+ // at the first content line — see runGCore for the rationale.
2131
+ return formatServerError(ctx, err, trimmedBuf);
2132
+ }
2133
+ // Stop if `c=` reached the configured iteration cap, OR if `m=`
2134
+ // was set and the previous result returned FEWER than `min_rows`
2135
+ // tuples. Upstream's `ExecQueryAndProcessResults` sets `return_early`
2136
+ // exactly when `min_rows > 0 && PQntuples(result) < min_rows`, and
2137
+ // `do_watch` breaks out of the loop on that signal — see PG source
2138
+ // `src/bin/psql/common.c::ExecQueryAndProcessResults`. In other
2139
+ // words `min_rows` is a CONTINUE predicate: keep polling while
2140
+ // the result has at least `min_rows` rows; stop the moment it
2141
+ // doesn't.
2142
+ if (iterSet && iter >= iterMax)
2143
+ break;
2144
+ if (minRowsSet && lastRowCount < minRows)
2145
+ break;
2146
+ if (controller.signal.aborted)
2147
+ break;
2148
+ // sleep_ms == 0 is upstream's "tight loop, no wait needed" — skip
2149
+ // the timer round-trip entirely so we don't queue a setTimeout(0)
2150
+ // for every iteration. Matches `do_watch`'s `if (sleep_ms == 0)
2151
+ // continue;` branch.
2152
+ if (intervalMs > 0) {
2153
+ await sleepCancellable(intervalMs, controller.signal);
2154
+ }
2155
+ }
2156
+ // Upstream `do_watch` injects a trailing newline AFTER the loop
2157
+ // ends when no pager is attached, to clear the cursor after a
2158
+ // possible `^C` echo. Mirror that here so the conformance output
2159
+ // shape (`...\n(N rows)\n\n\n`) matches vanilla psql.
2160
+ if (!pager) {
2161
+ out.write("\n");
2162
+ }
2163
+ return { status: "reset-buf", newBuf: "" };
2164
+ }
2165
+ finally {
2166
+ if (installedSigint) {
2167
+ process.removeListener("SIGINT", sigintHandler);
2168
+ }
2169
+ // Drain the pager so its child has a chance to exit before \watch
2170
+ // returns. Failures are swallowed: a broken pager shouldn't mask the
2171
+ // (already-flushed) query results.
2172
+ if (pager) {
2173
+ await pager.close();
2174
+ }
2175
+ }
2176
+ },
2177
+ };
2178
+ // ---------------------------------------------------------------------------
2179
+ // Registration entry point.
2180
+ // ---------------------------------------------------------------------------
2181
+ export const registerIoCommands = (registry) => {
2182
+ registry.register(cmdInclude);
2183
+ registry.register(cmdIncludeRel);
2184
+ registry.register(cmdOut);
2185
+ registry.register(cmdWrite);
2186
+ registry.register(cmdG);
2187
+ registry.register(cmdGx);
2188
+ registry.register(cmdPrint);
2189
+ registry.register(cmdGset);
2190
+ registry.register(cmdGdesc);
2191
+ registry.register(cmdGexec);
2192
+ registry.register(cmdWatch);
2193
+ };