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