neon 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -0,0 +1,2818 @@
1
+ import { writeErr, writeOut } from "../psql/command/shared.js";
2
+ import { formatDurationMs } from "../psql/print/units.js";
3
+ import { alignedPrinter } from "../psql/print/aligned.js";
4
+ import { asciidocPrinter } from "../psql/print/asciidoc.js";
5
+ import { csvPrinter } from "../psql/print/csv.js";
6
+ import { htmlPrinter } from "../psql/print/html.js";
7
+ import { jsonPrinter } from "../psql/print/json.js";
8
+ import { latexLongtablePrinter, latexPrinter } from "../psql/print/latex.js";
9
+ import { troffMsPrinter } from "../psql/print/troff.js";
10
+ import { unalignedPrinter } from "../psql/print/unaligned.js";
11
+ import { openPager, shouldPage } from "../psql/print/pager.js";
12
+ import { formatErrorReport, psqlErrorPrefix } from "../psql/command/cmd_meta.js";
13
+ import { applyPset } from "../psql/command/cmd_format.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/core/common.ts
19
+ /**
20
+ * psql common — the unified send-query / process-result pipeline.
21
+ *
22
+ * TypeScript port of selected functions in `src/bin/psql/common.c`:
23
+ *
24
+ * - {@link sendQuery} ← `SendQuery`
25
+ * - {@link executeAndPrint} ← `ExecQueryAndProcessResults` (the inner
26
+ * result-processing slice, without the
27
+ * AUTOCOMMIT / savepoint scaffolding)
28
+ * - {@link psqlExec} ← `PSQLexec`
29
+ *
30
+ * The minimal version of this logic was inlined in `mainloop.ts` after WP-12;
31
+ * this WP extracts and polishes it. The pieces wired in here:
32
+ *
33
+ * - AUTOCOMMIT: when the variable is 'off' (default 'on') and the current
34
+ * transaction is idle, prepend `BEGIN` once before the next non-exempt
35
+ * statement. The set of exempt commands mirrors upstream
36
+ * `command_no_begin()` — transaction-control verbs and a handful of
37
+ * non-transactional DDL.
38
+ *
39
+ * - ON_ERROR_ROLLBACK: 'off' (default) | 'on' | 'interactive'. When active
40
+ * and we're inside a transaction, issue
41
+ * `SAVEPOINT pg_psql_temporary_savepoint` before the statement; on error
42
+ * `ROLLBACK TO`, on success `RELEASE` (unless the statement is itself a
43
+ * savepoint-management verb that already drops/replaces it).
44
+ *
45
+ * - FETCH_COUNT: integer; when >0 and the statement is a single SELECT-ish
46
+ * query, wrap it in `DECLARE _psql_cursor CURSOR FOR ...; FETCH FORWARD
47
+ * N FROM _psql_cursor` until exhausted. Non-SELECT statements fall back
48
+ * to the simple path.
49
+ *
50
+ * - SINGLESTEP: when `settings.singlestep` is true, print the SQL to stderr
51
+ * with the upstream confirmation banner and read one line from stdin.
52
+ * Input that starts with 'x' cancels the statement.
53
+ *
54
+ * - Timing: when `settings.timing` is true, measure wallclock around the
55
+ * send/print path and write a `Time: X.XXX ms` line to stdout (mirrors
56
+ * upstream's `printf` in common.c, and matches the existing mainloop
57
+ * test expectations).
58
+ *
59
+ * What's deliberately not done here:
60
+ *
61
+ * - SINGLELINE (-S): treating LF as a semicolon is a scanner concern, so it
62
+ * lives there — `scanSql` honours `ScanOptions.singleline` and the mainloop
63
+ * passes `settings.singleline` through on each pass. `sendQuery` itself
64
+ * needs no SINGLELINE branch; by the time a statement reaches here the
65
+ * scanner has already drawn the boundary.
66
+ *
67
+ * - Pipeline mode (-X / `\startpipeline`): upstream gates SendQuery on
68
+ * `pset.send_mode == PSQL_SEND_PIPELINE`; that path is owned by WP-21.
69
+ *
70
+ * - COPY FROM STDIN / TO STDOUT: upstream's ProcessResult dispatches on
71
+ * `PGRES_COPY_IN/OUT`. That belongs to WP-16; here we surface a clear
72
+ * error message if we ever see a copy result come back through
73
+ * `execSimple`.
74
+ */
75
+ const readTxStatus = (conn) => {
76
+ return conn.txStatus ?? "I";
77
+ };
78
+ const SAVEPOINT_NAME = "pg_psql_temporary_savepoint";
79
+ const CURSOR_NAME = "_psql_cursor";
80
+ /**
81
+ * Strip leading whitespace and `--` line / slash-star block comments from
82
+ * `sql`. Mirrors what upstream psql's scanner advances past before handing a
83
+ * statement to `PQexec` — the server-reported error `position` is a 1-based
84
+ * offset into THAT trimmed buffer, so the `LINE N:` re-print computed from
85
+ * `count('\n')` in `sql.slice(0, position - 1)` aligns with vanilla output
86
+ * only when the same leading prelude is stripped here too.
87
+ *
88
+ * Block comments support nested depths (PG extension). Embedded comments
89
+ * mid-statement are intentionally NOT stripped — they participate in the
90
+ * line count of the executing statement, same as upstream.
91
+ *
92
+ * Exported for cmd_io / cmd_pipeline so backslash commands that capture or
93
+ * inspect `queryBuf` see the same shape that the wire and `lastQuery`
94
+ * receive.
95
+ */
96
+ const stripLeadingCommentsAndWS = (sql) => {
97
+ let i = 0;
98
+ const n = sql.length;
99
+ while (i < n) {
100
+ const c = sql.charCodeAt(i);
101
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11) {
102
+ i++;
103
+ continue;
104
+ }
105
+ if (c === 45 && sql.charCodeAt(i + 1) === 45) {
106
+ i += 2;
107
+ while (i < n && sql.charCodeAt(i) !== 10) i++;
108
+ continue;
109
+ }
110
+ if (c === 47 && sql.charCodeAt(i + 1) === 42) {
111
+ i += 2;
112
+ let depth = 1;
113
+ while (i < n && depth > 0) if (sql.charCodeAt(i) === 47 && sql.charCodeAt(i + 1) === 42) {
114
+ depth++;
115
+ i += 2;
116
+ } else if (sql.charCodeAt(i) === 42 && sql.charCodeAt(i + 1) === 47) {
117
+ depth--;
118
+ i += 2;
119
+ } else i++;
120
+ continue;
121
+ }
122
+ break;
123
+ }
124
+ return i === 0 ? sql : sql.slice(i);
125
+ };
126
+ /** Strip leading whitespace and SQL comments, then upper-case for matching. */
127
+ const peekKeywords = (sql, count = 3) => {
128
+ let i = 0;
129
+ while (i < sql.length) {
130
+ const ch = sql[i];
131
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
132
+ i++;
133
+ continue;
134
+ }
135
+ if (ch === "-" && sql[i + 1] === "-") {
136
+ const nl = sql.indexOf("\n", i + 2);
137
+ if (nl === -1) return [];
138
+ i = nl + 1;
139
+ continue;
140
+ }
141
+ if (ch === "/" && sql[i + 1] === "*") {
142
+ const end = sql.indexOf("*/", i + 2);
143
+ if (end === -1) return [];
144
+ i = end + 2;
145
+ continue;
146
+ }
147
+ break;
148
+ }
149
+ return sql.slice(i).split(/[\s,;()]+/u, count + 1).slice(0, count).map((w) => w.toUpperCase());
150
+ };
151
+ /** Mirror of `command_no_begin()` in psql/common.c. */
152
+ const commandNoBegin = (sql) => {
153
+ const [w0, w1, w2, w3] = peekKeywords(sql, 4);
154
+ if (!w0) return false;
155
+ switch (w0) {
156
+ case "ABORT":
157
+ case "BEGIN":
158
+ case "COMMIT":
159
+ case "END":
160
+ case "ROLLBACK":
161
+ case "START":
162
+ case "SAVEPOINT":
163
+ case "RELEASE": return true;
164
+ case "PREPARE": return w1 === "TRANSACTION";
165
+ case "VACUUM": return true;
166
+ case "CLUSTER": return w1 === void 0 || w1 === "";
167
+ case "CREATE":
168
+ if (w1 === "DATABASE" || w1 === "TABLESPACE") return true;
169
+ if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
170
+ if (w1 === "UNIQUE" && w2 === "INDEX" && w3 === "CONCURRENTLY") return true;
171
+ return false;
172
+ case "DROP":
173
+ if (w1 === "DATABASE" || w1 === "TABLESPACE") return true;
174
+ if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
175
+ return false;
176
+ case "REINDEX":
177
+ if (w1 === "DATABASE" || w1 === "SYSTEM") return true;
178
+ if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
179
+ if (w1 === "TABLE" && w2 === "CONCURRENTLY") return true;
180
+ return false;
181
+ case "ALTER": return w1 === "SYSTEM";
182
+ case "DISCARD": return w1 === "ALL";
183
+ default: return false;
184
+ }
185
+ };
186
+ /** True when the statement opens with SELECT / VALUES / TABLE / WITH. */
187
+ const isSelectCommand = (sql) => {
188
+ const [w0] = peekKeywords(sql, 1);
189
+ return w0 === "SELECT" || w0 === "VALUES" || w0 === "TABLE" || w0 === "WITH";
190
+ };
191
+ /**
192
+ * Does this statement effectively destroy / replace the temporary savepoint?
193
+ * Upstream's `svpt_gone` flag is set when the user's command is one of
194
+ * COMMIT / ROLLBACK / SAVEPOINT / RELEASE. In those cases we must skip the
195
+ * matching RELEASE because the named savepoint no longer exists.
196
+ */
197
+ const destroysSavepoint = (sql) => {
198
+ const [w0] = peekKeywords(sql, 1);
199
+ return w0 === "COMMIT" || w0 === "ROLLBACK" || w0 === "SAVEPOINT" || w0 === "RELEASE";
200
+ };
201
+ const pickPrinter = (settings) => {
202
+ switch (settings.popt.topt.format) {
203
+ case "aligned":
204
+ case "wrapped": return alignedPrinter;
205
+ case "unaligned": return unalignedPrinter;
206
+ case "csv": return csvPrinter;
207
+ case "json": return jsonPrinter;
208
+ case "html": return htmlPrinter;
209
+ case "asciidoc": return asciidocPrinter;
210
+ case "latex": return latexPrinter;
211
+ case "latex-longtable": return latexLongtablePrinter;
212
+ case "troff-ms": return troffMsPrinter;
213
+ default: return alignedPrinter;
214
+ }
215
+ };
216
+ /**
217
+ * Pick the output target for a query result.
218
+ *
219
+ * Precedence: explicit `oneShot` argument > the settings stash from
220
+ * `\o FILE` (WP-15) > the REPL context's `stdout`.
221
+ */
222
+ const pickOut$1 = (ctx, oneShot) => {
223
+ if (oneShot) return oneShot;
224
+ return getQueryFout(ctx.settings) ?? ctx.stdout;
225
+ };
226
+ const readAutocommit = (settings) => settings.vars.asBool("AUTOCOMMIT", true);
227
+ const readOnErrorRollback = (settings) => {
228
+ const raw = settings.vars.get("ON_ERROR_ROLLBACK");
229
+ if (raw === void 0) return settings.onErrorRollback;
230
+ const v = raw.toLowerCase();
231
+ if (v === "interactive") return "interactive";
232
+ if (v === "on" || v === "true" || v === "yes" || v === "1") return "on";
233
+ return "off";
234
+ };
235
+ const readFetchCount = (settings) => {
236
+ const v = settings.vars.asInt("FETCH_COUNT", settings.fetchCount);
237
+ if (typeof v !== "number") return 0;
238
+ return Math.max(0, v | 0);
239
+ };
240
+ const readSinglestep = (settings) => settings.singlestep || settings.vars.asBool("SINGLESTEP", false);
241
+ /**
242
+ * SHOW_ALL_RESULTS controls multi-statement `\;` printing. Default 'on' —
243
+ * every result set is rendered. When 'off' / '0', only the LAST result set
244
+ * is printed (upstream's `pset.show_all_results` flag, consulted by
245
+ * `PrintQueryResults` in common.c).
246
+ */
247
+ const readShowAllResults = (settings) => settings.vars.asBool("SHOW_ALL_RESULTS", true);
248
+ const writeError = (ctx, message) => {
249
+ ctx.stderr.write(`psql: ERROR: ${message}\n`);
250
+ };
251
+ /**
252
+ * Render the verbosity-aware error report for the most recently captured
253
+ * query error and write it to `ctx.stderr`. The leading severity line gets
254
+ * the same `psql:[<file>:<n>]:` diagnostic prefix that upstream's
255
+ * `pg_log_pre_callback` prepends — matching the format the regression-
256
+ * derived conformance suite expects (e.g. `psql:<stdin>:N: ERROR: ...`).
257
+ * Subsequent layers (`LINE N: ...`, caret, `DETAIL: ...`, ...) follow on
258
+ * their own lines per {@link formatErrorReport}, unprefixed, to match
259
+ * libpq's `PQresultErrorMessage` output shape. When the captured error is
260
+ * missing (defensive — callers should always pair this with a preceding
261
+ * `recordError`) we fall back to the plain client-side {@link writeError}
262
+ * form so we never swallow the message entirely.
263
+ *
264
+ * Exported so the bind-path in mainloop can share the renderer.
265
+ */
266
+ const writeQueryError = (ctx, fallbackMessage) => {
267
+ const e = ctx.settings.lastErrorResult;
268
+ if (!e || !e.message && !e.code && !e.sqlstate) {
269
+ writeError(ctx, fallbackMessage);
270
+ return;
271
+ }
272
+ const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
273
+ const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
274
+ ctx.stderr.write(prefixed.join("\n") + "\n");
275
+ };
276
+ /**
277
+ * Strip leading whitespace from a query and rebase a 1-based server position
278
+ * to match. Mirrors upstream psql/mainloop.c's behaviour: a line containing
279
+ * only a backslash command does not leave a `\n` in the query buffer, so the
280
+ * subsequent SQL statement starts at "line 1" of its own context rather than
281
+ * inheriting a blank line. Our mainloop doesn't perform that strip, so the
282
+ * captured `sqlText` sometimes has a leading `\n` (e.g. after `\set
283
+ * FETCH_COUNT 1\nSELECT error;`). Without this normalisation, `\errverbose`
284
+ * would render `LINE 2: SELECT error;` where upstream renders `LINE 1: …`.
285
+ *
286
+ * Returns the trimmed text and the rebased position. If the rebased
287
+ * position would land outside the trimmed text, it is dropped so the
288
+ * formatter skips the `LINE`/caret block instead of mis-pointing.
289
+ */
290
+ const normaliseSqlAndPosition = (sqlText, position) => {
291
+ let leading = 0;
292
+ while (leading < sqlText.length) {
293
+ const ch = sqlText.charCodeAt(leading);
294
+ if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13 && ch !== 12) break;
295
+ leading++;
296
+ }
297
+ if (leading === 0) return {
298
+ sqlText,
299
+ position
300
+ };
301
+ const trimmed = sqlText.slice(leading);
302
+ if (typeof position !== "string") return {
303
+ sqlText: trimmed,
304
+ position
305
+ };
306
+ const original = parseInt(position, 10);
307
+ if (!Number.isFinite(original) || original <= 0) return {
308
+ sqlText: trimmed,
309
+ position
310
+ };
311
+ const rebased = original - leading;
312
+ if (rebased <= 0 || rebased > trimmed.length) return {
313
+ sqlText: trimmed,
314
+ position: void 0
315
+ };
316
+ return {
317
+ sqlText: trimmed,
318
+ position: String(rebased)
319
+ };
320
+ };
321
+ /**
322
+ * Capture the full ErrorResponse-shaped payload from a thrown error.
323
+ *
324
+ * Our wire layer copies every named field of the server's ErrorResponse
325
+ * (severity / code / detail / hint / position / file / line / routine /
326
+ * …) onto the thrown Error as own properties (see `asThrowable` in
327
+ * `wire/connection.ts`). We mirror those onto `settings.lastErrorResult`
328
+ * so `\errverbose` can re-render the error in VERBOSE form — including
329
+ * the `LINE N: …` re-print + `^` pointer and the `LOCATION:` footer.
330
+ *
331
+ * `sqlText` is the originating SQL text from the caller; required so the
332
+ * `^` pointer can be positioned under the failing character. Leading
333
+ * whitespace is stripped (and `position` is rebased) so the `LINE N`
334
+ * counter reflects offsets within the user's statement rather than any
335
+ * buffer noise carried over from prior backslash commands.
336
+ */
337
+ const captureLastError = (settings, err, sqlText) => {
338
+ const fallbackMessage = err instanceof Error ? err.message : String(err);
339
+ const e = err ?? {};
340
+ const code = e.code;
341
+ const normalised = normaliseSqlAndPosition(sqlText, e.position);
342
+ settings.lastErrorResult = {
343
+ severity: e.severity,
344
+ code,
345
+ sqlstate: code,
346
+ message: e.message ?? fallbackMessage,
347
+ detail: e.detail,
348
+ hint: e.hint,
349
+ position: normalised.position,
350
+ internalPosition: e.internalPosition,
351
+ internalQuery: e.internalQuery,
352
+ where: e.where,
353
+ schema: e.schema,
354
+ table: e.table,
355
+ column: e.column,
356
+ dataType: e.dataType,
357
+ constraint: e.constraint,
358
+ file: e.file,
359
+ line: e.line,
360
+ routine: e.routine,
361
+ sqlText: normalised.sqlText
362
+ };
363
+ return settings.lastErrorResult.message ?? fallbackMessage;
364
+ };
365
+ const recordError = (ctx, err, sqlText = "") => captureLastError(ctx.settings, err, sqlText);
366
+ /**
367
+ * Update the per-statement diagnostic psql variables that upstream's
368
+ * `SetResultVariables` / `SetErrorVariables` in `src/bin/psql/common.c`
369
+ * maintains. Called after every dispatched statement (success and error
370
+ * paths) so `\echo :LAST_ERROR_MESSAGE` and friends produce the same
371
+ * values vanilla psql does.
372
+ *
373
+ * - `SQLSTATE` SQLSTATE of the *most recent* statement —
374
+ * `"00000"` on success, the server-reported code
375
+ * on error (defaults to `"XX000"` when missing).
376
+ * - `ERROR` `"true"` if the most recent statement failed,
377
+ * else `"false"`.
378
+ * - `ROW_COUNT` affected/returned row count of the most recent
379
+ * statement (from libpq's `PQcmdTuples`). `"0"`
380
+ * on error.
381
+ * - `LAST_ERROR_*` sticky — only mutated on error. Mirrors
382
+ * upstream's "preserve until the next failure"
383
+ * contract so a successful statement does not
384
+ * clobber the prior error info.
385
+ *
386
+ * Exported so mainloop's bind / pipeline paths (which bypass {@link
387
+ * sendQuery}) can share the same updater.
388
+ */
389
+ const refreshErrorVars = (settings, outcome) => {
390
+ const { vars } = settings;
391
+ if (outcome.kind === "error") {
392
+ const last = settings.lastErrorResult;
393
+ const code = last?.code ?? last?.sqlstate ?? "XX000";
394
+ const message = last?.message ?? "";
395
+ vars.set("LAST_ERROR_MESSAGE", message);
396
+ vars.set("LAST_ERROR_SQLSTATE", code);
397
+ vars.set("SQLSTATE", code);
398
+ vars.set("ERROR", "true");
399
+ vars.set("ROW_COUNT", "0");
400
+ return;
401
+ }
402
+ vars.set("SQLSTATE", "00000");
403
+ vars.set("ERROR", "false");
404
+ const rc = outcome.rowCount ?? 0;
405
+ vars.set("ROW_COUNT", String(rc));
406
+ };
407
+ const readOneLine = (stdin) => new Promise((resolve) => {
408
+ let buf = "";
409
+ let resolved = false;
410
+ const onData = (chunk) => {
411
+ buf += chunk.toString();
412
+ const nl = buf.indexOf("\n");
413
+ if (nl !== -1) {
414
+ const line = buf.slice(0, nl);
415
+ cleanup();
416
+ if (!resolved) {
417
+ resolved = true;
418
+ resolve(line);
419
+ }
420
+ }
421
+ };
422
+ const onEnd = () => {
423
+ cleanup();
424
+ if (!resolved) {
425
+ resolved = true;
426
+ resolve(buf);
427
+ }
428
+ };
429
+ const cleanup = () => {
430
+ stdin.off("data", onData);
431
+ stdin.off("end", onEnd);
432
+ stdin.off("close", onEnd);
433
+ };
434
+ stdin.on("data", onData);
435
+ stdin.once("end", onEnd);
436
+ stdin.once("close", onEnd);
437
+ });
438
+ const confirmSinglestep = async (ctx, sql) => {
439
+ ctx.stderr.write(`***(Single step mode: verify command)*******************************************\n${sql}\n***(press return to proceed or enter x and return to cancel)********************\n`);
440
+ return !(await readOneLine(ctx.stdin)).trim().toLowerCase().startsWith("x");
441
+ };
442
+ /**
443
+ * Reconstruct the libpq-style CommandComplete tag from the parsed parts our
444
+ * wire layer stores on a ResultSet. INSERT carries `oid` + `rowCount`; the
445
+ * other DML verbs (UPDATE/DELETE/MERGE/SELECT/MOVE/FETCH/COPY) carry just a
446
+ * `rowCount`; DDL has neither.
447
+ *
448
+ * Matches upstream psql's `PQcmdStatus(conn)` output (which is the raw tag
449
+ * the server sent — we round-trip it through our parser).
450
+ */
451
+ const formatCommandTag = (rs) => {
452
+ const command = (rs.command || "").trim();
453
+ if (command.length === 0) return "";
454
+ if (command === "INSERT") return `INSERT ${rs.oid ?? 0} ${rs.rowCount ?? 0}`;
455
+ if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${rs.rowCount}`;
456
+ return command;
457
+ };
458
+ /**
459
+ * Decide whether the pager should activate for the given batch of result
460
+ * sets. We page when ANY of the sets-that-will-be-printed (i.e. tuples-
461
+ * producing, and not gated out by SHOW_ALL_RESULTS) crosses the threshold.
462
+ * The decision is centralised here so callers can override individual
463
+ * decision inputs (e.g. tests that inject a fake `output`).
464
+ */
465
+ const pickPagerDecision = (ctx, results, out) => {
466
+ const popt = ctx.settings.popt.topt;
467
+ if (popt.pager === "off") return false;
468
+ const redirectedOutput = getQueryFout(ctx.settings) !== null;
469
+ if (redirectedOutput) return false;
470
+ const showAll = readShowAllResults(ctx.settings);
471
+ const lastIdx = results.length - 1;
472
+ for (let i = 0; i < results.length; i++) {
473
+ const rs = results[i];
474
+ if (rs.fields.length === 0) continue;
475
+ if (!(showAll || i === lastIdx)) continue;
476
+ if (shouldPage({
477
+ pager: popt.pager,
478
+ pagerMinLines: popt.pagerMinLines,
479
+ rowCount: rs.rows.length,
480
+ colCount: rs.fields.length,
481
+ output: out,
482
+ redirectedOutput
483
+ })) return true;
484
+ }
485
+ return false;
486
+ };
487
+ const renderResultSets = async (ctx, results, out) => {
488
+ const printer = pickPrinter(ctx.settings);
489
+ let rowsAffected = 0;
490
+ let rowsPrinted = 0;
491
+ const showAll = readShowAllResults(ctx.settings);
492
+ const lastIdx = results.length - 1;
493
+ const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
494
+ const pager = pickPagerDecision(ctx, results, out) ? openPager({
495
+ pager: ctx.settings.popt.topt.pager,
496
+ pagerMinLines: ctx.settings.popt.topt.pagerMinLines,
497
+ stdout: out
498
+ }) : null;
499
+ const sink = pager?.spawned ? pager.out : out;
500
+ try {
501
+ for (let i = 0; i < results.length; i++) {
502
+ const rs = results[i];
503
+ const shouldEmit = showAll || i === lastIdx;
504
+ if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) sink.write(chunk);
505
+ if (rs.fields.length === 0) {
506
+ if (shouldEmit && !tuplesOnly && !ctx.settings.quiet && !rs.copyOutBytes) {
507
+ const tag = formatCommandTag(rs);
508
+ if (tag.length > 0) sink.write(`${tag}\n`);
509
+ }
510
+ rowsAffected += rs.rowCount ?? 0;
511
+ } else {
512
+ if (shouldEmit) await printer.printQuery(rs, ctx.settings.popt, sink);
513
+ rowsPrinted += rs.rows.length;
514
+ }
515
+ }
516
+ } finally {
517
+ if (pager?.spawned) try {
518
+ await pager.close();
519
+ } catch {}
520
+ }
521
+ const lastRowCount = results.length === 0 ? null : (() => {
522
+ const rs = results[results.length - 1];
523
+ if (rs.fields.length > 0) return rs.rows.length;
524
+ return rs.rowCount ?? null;
525
+ })();
526
+ return {
527
+ rowsAffected,
528
+ rowsPrinted,
529
+ lastRowCount
530
+ };
531
+ };
532
+ /**
533
+ * Render a single {@link ResultSet} through the active printer and the
534
+ * configured output target (respecting `\o FILE` redirects). Used by the
535
+ * `\bind` / extended-query path in {@link mainloop.dispatchSendQuery} which
536
+ * comes back with a single result instead of the array shape `execSimple`
537
+ * produces. Returns a tally consistent with {@link renderResultSets}.
538
+ */
539
+ const renderResultSet = (ctx, rs, out) => {
540
+ return renderResultSets(ctx, [rs], out ?? pickOut$1(ctx));
541
+ };
542
+ /**
543
+ * Re-base a server-side `position` field so it points into the user's
544
+ * original SQL rather than the synthetic statement we actually sent.
545
+ *
546
+ * The FETCH_COUNT path sends `DECLARE _psql_cursor NO SCROLL CURSOR FOR
547
+ * <userSql>` for the DECLARE leg and `FETCH FORWARD N FROM _psql_cursor`
548
+ * for each fetch. Server error positions (`P` field) come back in the
549
+ * coordinates of whatever query we sent:
550
+ *
551
+ * - DECLARE-time parser/planner errors carry a position into the DECLARE
552
+ * statement. We subtract the length of the prefix (`DECLARE … FOR `)
553
+ * so the caret lands under the failing token in `userSql`.
554
+ *
555
+ * - FETCH-time runtime errors come from executing the cursor's underlying
556
+ * query (which IS `userSql`). The server reports the position relative
557
+ * to that underlying query, so it's already in `userSql` coordinates
558
+ * and we leave it alone.
559
+ *
560
+ * If we can't rebase a DECLARE-coord position into `userSql` bounds, we
561
+ * strip it rather than render a caret pointing past end-of-line.
562
+ */
563
+ const rebasePositionForCursor = (err, wrapper, userSql) => {
564
+ if (!err || typeof err !== "object") return;
565
+ const e = err;
566
+ if (typeof e.position !== "string") return;
567
+ const original = parseInt(e.position, 10);
568
+ if (!Number.isFinite(original) || original <= 0) return;
569
+ const stripped = userSql.replace(/;\s*$/u, "");
570
+ const offset = wrapper.indexOf(stripped);
571
+ if (offset === -1) return;
572
+ const rebased = original - offset;
573
+ if (rebased <= 0 || rebased > userSql.length) {
574
+ delete e.position;
575
+ return;
576
+ }
577
+ e.position = String(rebased);
578
+ };
579
+ const runCursorLoop = async (ctx, sql, fetchCount, out) => {
580
+ if (!ctx.settings.db) throw new Error("no connection to the server");
581
+ const db = ctx.settings.db;
582
+ const initiallyIdle = readTxStatus(db) === "I";
583
+ if (initiallyIdle) await db.execSimple("BEGIN");
584
+ const stripped = sql.replace(/;\s*$/u, "");
585
+ const declared = `DECLARE ${CURSOR_NAME} NO SCROLL CURSOR FOR ${stripped}`;
586
+ const fetchSql = `FETCH FORWARD ${String(fetchCount)} FROM ${CURSOR_NAME}`;
587
+ const rowsAffected = 0;
588
+ let rowsPrinted = 0;
589
+ let cursorOpen = false;
590
+ let currentWrapper = declared;
591
+ const printer = pickPrinter(ctx.settings);
592
+ let merged = null;
593
+ try {
594
+ await db.execSimple(declared);
595
+ cursorOpen = true;
596
+ while (true) {
597
+ currentWrapper = fetchSql;
598
+ const sets = await db.execSimple(fetchSql);
599
+ if (sets.length === 0) break;
600
+ const rs = sets[sets.length - 1];
601
+ const chunkRows = rs.rows.length;
602
+ if (chunkRows === 0) break;
603
+ if (merged === null) merged = {
604
+ command: rs.command,
605
+ fields: rs.fields,
606
+ rows: rs.rows.slice(),
607
+ rowCount: rs.rowCount,
608
+ oid: rs.oid,
609
+ notices: rs.notices
610
+ };
611
+ else for (const row of rs.rows) merged.rows.push(row);
612
+ rowsPrinted += chunkRows;
613
+ if (chunkRows < fetchCount) break;
614
+ }
615
+ if (merged !== null) {
616
+ merged.rowCount = merged.rows.length;
617
+ await printer.printQuery(merged, ctx.settings.popt, out);
618
+ }
619
+ await db.execSimple(`CLOSE ${CURSOR_NAME}`);
620
+ cursorOpen = false;
621
+ if (initiallyIdle) await db.execSimple("COMMIT");
622
+ return {
623
+ rowsAffected,
624
+ rowsPrinted,
625
+ lastRowCount: rowsPrinted
626
+ };
627
+ } catch (err) {
628
+ if (merged !== null) {
629
+ merged.rowCount = merged.rows.length;
630
+ const partialOpts = {
631
+ ...ctx.settings.popt,
632
+ topt: {
633
+ ...ctx.settings.popt.topt,
634
+ defaultFooter: false,
635
+ stopTable: false
636
+ }
637
+ };
638
+ try {
639
+ await printer.printQuery(merged, partialOpts, out);
640
+ } catch {}
641
+ }
642
+ rebasePositionForCursor(err, currentWrapper, sql);
643
+ if (cursorOpen) try {
644
+ await db.execSimple(`CLOSE ${CURSOR_NAME}`);
645
+ } catch {}
646
+ if (initiallyIdle) try {
647
+ await db.execSimple("ROLLBACK");
648
+ } catch {}
649
+ throw err;
650
+ }
651
+ };
652
+ const executeAndPrint = async (ctx, sqlRaw, opts = {}) => {
653
+ const sql = stripLeadingCommentsAndWS(sqlRaw);
654
+ const started = ctx.settings.timing ? performance.now() : 0;
655
+ const stats = {
656
+ rowsAffected: 0,
657
+ rowsPrinted: 0,
658
+ fetched: false,
659
+ hadError: false,
660
+ durationMs: 0
661
+ };
662
+ if (!ctx.settings.db) {
663
+ writeError(ctx, "no connection to the server");
664
+ stats.hadError = true;
665
+ return stats;
666
+ }
667
+ const out = pickOut$1(ctx, opts.oneShotOut);
668
+ const fetchCount = readFetchCount(ctx.settings);
669
+ let lastRowCount = null;
670
+ try {
671
+ if (fetchCount > 0 && isSelectCommand(sql)) {
672
+ const r = await runCursorLoop(ctx, sql, fetchCount, out);
673
+ stats.rowsAffected = r.rowsAffected;
674
+ stats.rowsPrinted = r.rowsPrinted;
675
+ stats.fetched = true;
676
+ lastRowCount = r.lastRowCount;
677
+ } else {
678
+ const results = await ctx.settings.db.execSimple(sql);
679
+ const r = await renderResultSets(ctx, results, out);
680
+ stats.rowsAffected = r.rowsAffected;
681
+ stats.rowsPrinted = r.rowsPrinted;
682
+ lastRowCount = r.lastRowCount;
683
+ }
684
+ } catch (err) {
685
+ const partial = err.partialResults;
686
+ if (partial && partial.length > 0) try {
687
+ const r = await renderResultSets(ctx, partial, out);
688
+ stats.rowsAffected = r.rowsAffected;
689
+ stats.rowsPrinted = r.rowsPrinted;
690
+ } catch {}
691
+ const message = recordError(ctx, err, sql);
692
+ writeQueryError(ctx, message);
693
+ stats.hadError = true;
694
+ } finally {
695
+ if (ctx.settings.timing) {
696
+ stats.durationMs = performance.now() - started;
697
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
698
+ }
699
+ }
700
+ refreshErrorVars(ctx.settings, stats.hadError ? { kind: "error" } : {
701
+ kind: "success",
702
+ rowCount: lastRowCount
703
+ });
704
+ return stats;
705
+ };
706
+ const sendQuery = async (ctx, sqlRaw, opts = {}) => {
707
+ const sql = stripLeadingCommentsAndWS(sqlRaw);
708
+ const stats = {
709
+ rowsAffected: 0,
710
+ rowsPrinted: 0,
711
+ fetched: false,
712
+ hadError: false,
713
+ durationMs: 0
714
+ };
715
+ ctx.settings.lastQuery = sql;
716
+ if (!ctx.settings.db) {
717
+ writeError(ctx, "no connection to the server");
718
+ stats.hadError = true;
719
+ return stats;
720
+ }
721
+ if (readSinglestep(ctx.settings)) {
722
+ if (!await confirmSinglestep(ctx, sql)) {
723
+ stats.hadError = true;
724
+ ctx.settings.lastErrorResult = { message: "command cancelled by user" };
725
+ return stats;
726
+ }
727
+ }
728
+ const db = ctx.settings.db;
729
+ const autocommit = readAutocommit(ctx.settings);
730
+ const onErrorRollback = readOnErrorRollback(ctx.settings);
731
+ const interactive = !ctx.settings.notty;
732
+ const started = ctx.settings.timing ? performance.now() : 0;
733
+ if (!autocommit && readTxStatus(db) === "I" && !commandNoBegin(sql)) try {
734
+ await db.execSimple("BEGIN");
735
+ } catch (err) {
736
+ const message = recordError(ctx, err);
737
+ writeQueryError(ctx, message);
738
+ stats.hadError = true;
739
+ if (ctx.settings.timing) {
740
+ stats.durationMs = performance.now() - started;
741
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
742
+ }
743
+ return stats;
744
+ }
745
+ const savepointActive = onErrorRollback !== "off" && (onErrorRollback === "on" || onErrorRollback === "interactive" && interactive) && readTxStatus(db) === "T";
746
+ let savepointIssued = false;
747
+ if (savepointActive) try {
748
+ await db.execSimple(`SAVEPOINT ${SAVEPOINT_NAME}`);
749
+ savepointIssued = true;
750
+ } catch (err) {
751
+ const message = recordError(ctx, err);
752
+ writeQueryError(ctx, message);
753
+ stats.hadError = true;
754
+ if (ctx.settings.timing) {
755
+ stats.durationMs = performance.now() - started;
756
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
757
+ }
758
+ return stats;
759
+ }
760
+ const out = pickOut$1(ctx, opts.oneShotOut);
761
+ const fetchCount = readFetchCount(ctx.settings);
762
+ let lastRowCount = null;
763
+ try {
764
+ if (fetchCount > 0 && isSelectCommand(sql)) {
765
+ const r = await runCursorLoop(ctx, sql, fetchCount, out);
766
+ stats.rowsAffected = r.rowsAffected;
767
+ stats.rowsPrinted = r.rowsPrinted;
768
+ stats.fetched = true;
769
+ lastRowCount = r.lastRowCount;
770
+ } else {
771
+ const results = await db.execSimple(sql);
772
+ const r = await renderResultSets(ctx, results, out);
773
+ stats.rowsAffected = r.rowsAffected;
774
+ stats.rowsPrinted = r.rowsPrinted;
775
+ lastRowCount = r.lastRowCount;
776
+ }
777
+ } catch (err) {
778
+ const partial = err.partialResults;
779
+ if (partial && partial.length > 0) try {
780
+ const r = await renderResultSets(ctx, partial, out);
781
+ stats.rowsAffected = r.rowsAffected;
782
+ stats.rowsPrinted = r.rowsPrinted;
783
+ } catch {}
784
+ const message = recordError(ctx, err, sql);
785
+ writeQueryError(ctx, message);
786
+ stats.hadError = true;
787
+ }
788
+ if (savepointIssued) try {
789
+ if (stats.hadError) {
790
+ await db.execSimple(`ROLLBACK TO SAVEPOINT ${SAVEPOINT_NAME}`);
791
+ await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
792
+ } else if (!destroysSavepoint(sql) && readTxStatus(db) === "T") await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
793
+ } catch (err) {
794
+ if (!stats.hadError) {
795
+ const message = recordError(ctx, err);
796
+ writeQueryError(ctx, message);
797
+ stats.hadError = true;
798
+ }
799
+ }
800
+ if (stats.hadError && ctx.settings.echo === "errors") {
801
+ let stmt = sql;
802
+ while (true) {
803
+ const before = stmt.length;
804
+ stmt = stmt.replace(/^[ \t\n\r\f]+/, "");
805
+ stmt = stmt.replace(/^--[^\n\r]*/, "");
806
+ if (stmt.length === before) break;
807
+ }
808
+ if (stmt.endsWith("\n")) stmt = stmt.slice(0, -1);
809
+ ctx.stderr.write(`STATEMENT: ${stmt}\n`);
810
+ }
811
+ refreshErrorVars(ctx.settings, stats.hadError ? { kind: "error" } : {
812
+ kind: "success",
813
+ rowCount: lastRowCount
814
+ });
815
+ if (ctx.settings.timing) {
816
+ stats.durationMs = performance.now() - started;
817
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
818
+ }
819
+ return stats;
820
+ };
821
+ const psqlExec = async (conn, sql, ignoreError = false) => {
822
+ try {
823
+ const sets = await conn.execSimple(sql);
824
+ if (sets.length === 0) return null;
825
+ return sets[sets.length - 1];
826
+ } catch (err) {
827
+ if (ignoreError) return null;
828
+ throw err;
829
+ }
830
+ };
831
+ const __testing = {
832
+ commandNoBegin,
833
+ isSelectCommand,
834
+ destroysSavepoint,
835
+ peekKeywords,
836
+ SAVEPOINT_NAME,
837
+ CURSOR_NAME
838
+ };
839
+ //#endregion
840
+ //#region src/psql/command/cmd_io.ts
841
+ /**
842
+ * psql I/O & control backslash commands.
843
+ *
844
+ * TypeScript port of the following `exec_command_*` functions in upstream
845
+ * PostgreSQL's `src/bin/psql/command.c`:
846
+ *
847
+ * - `\i`, `\include` → exec_command_include (normal)
848
+ * - `\ir`, `\include_relative` → exec_command_include (relative=true)
849
+ * - `\o`, `\out` → exec_command_out
850
+ * - `\w`, `\write` → exec_command_write
851
+ * - `\g` → exec_command_g
852
+ * - `\gx` → exec_command_g (force_expanded=true)
853
+ * - `\gset` → exec_command_gset
854
+ * - `\gdesc` → exec_command_gdesc
855
+ * - `\gexec` → exec_command_gexec
856
+ * - `\watch` → exec_command_watch
857
+ *
858
+ * Each is exported as a `BackslashCmdSpec` and registered via
859
+ * {@link registerIoCommands}. The single line that wires us into the
860
+ * default dispatcher lives in `dispatch.ts::defaultRegistry()`.
861
+ *
862
+ * # Integration touch-points and known limitations
863
+ *
864
+ * Several of these commands really want to participate in the mainloop's
865
+ * scanner/printer pipeline. This WP keeps `src/psql/core/mainloop.ts`
866
+ * untouched, so we provide the data structures and let a follow-up WP wire
867
+ * the consumption sites. Limitations documented per-command:
868
+ *
869
+ * - `\i FILE` enqueues the file's contents on a small input queue
870
+ * (`./inputQueue.ts`) AND, as a stop-gap, executes the file's SQL
871
+ * directly via `Connection.execSimple`. Backslash commands embedded in
872
+ * the file are NOT processed by the scanner; the include is a "best
873
+ * effort: run as one big SQL blob". Once mainloop adopts the queue API
874
+ * this becomes a true include.
875
+ *
876
+ * - `\o FILE` opens a writable stream and stashes it under a symbol on
877
+ * `settings`. We expose a getter (`getQueryFout`) for the mainloop to
878
+ * consult; until that wiring happens, query output continues to flow
879
+ * to the mainloop's `ctx.stdout`. The stash + close-on-rebind logic is
880
+ * in place and fully tested.
881
+ *
882
+ * - `\g` (no arg) executes the current queryBuf directly through
883
+ * `Connection.execSimple` and renders via the aligned printer. This
884
+ * duplicates a tiny slice of mainloop's send/print pipeline, which is
885
+ * fine for the bytewise-simple cases this WP needs to support. For
886
+ * `\g FILE` / `\g |cmd` the output goes through the temporary writer.
887
+ *
888
+ * - `\gx` toggles `topt.expanded` for the single execution and restores
889
+ * the prior value in a `try { ... } finally { ... }`.
890
+ *
891
+ * - `\gset [PREFIX]` executes via `execSimple`, requires the last result
892
+ * to have exactly one row, and stores `${prefix}${colname}` → value
893
+ * for each column on `settings.vars`.
894
+ *
895
+ * - `\gdesc` parses the buffered query with the extended protocol
896
+ * (Parse + Describe by statement, no Execute), then assembles a
897
+ * synthetic `Column / Type` ResultSet and renders it through the
898
+ * active printer (`alignedPrinter` by default; the format picker
899
+ * honours `\pset format`). Tuples-only mode (`\t on`) and `\o FILE`
900
+ * redirects ride along automatically because the same ResultSet
901
+ * goes through the same printer the REPL would use for a query.
902
+ *
903
+ * - `\gexec` iterates the cells of the last result row-major and feeds
904
+ * each non-null cell back as SQL through `execSimple`. Each statement's
905
+ * output is rendered to stdout (or to the active queryFout stash).
906
+ *
907
+ * - `\watch [INTERVAL]` re-executes the queryBuf every `INTERVAL` seconds
908
+ * (default 2) until SIGINT or until the iteration count limit is hit.
909
+ * We hook SIGINT via a transient listener that's removed on completion.
910
+ * Tests bypass the listener by using an AbortController exposed via
911
+ * `WATCH_TEST_CONTROLLER`.
912
+ *
913
+ * # Error format
914
+ *
915
+ * Upstream prints `<cmd>: <msg>` to stderr and returns failure. We mirror
916
+ * that and also stash the message on `settings.lastErrorResult` so the
917
+ * mainloop's `writeError()` wrapper can pick it up.
918
+ */
919
+ const QUERY_FOUT_KEY = Symbol.for("neonctl.psql.queryFout");
920
+ /**
921
+ * Return the currently active queryFout stream (or `null` if none).
922
+ * The mainloop is encouraged to call this in lieu of writing directly to
923
+ * `ctx.stdout` for query results.
924
+ */
925
+ const getQueryFout = (settings) => {
926
+ return settings[QUERY_FOUT_KEY]?.stream ?? null;
927
+ };
928
+ const setQueryFout = (settings, entry) => {
929
+ const stash = settings;
930
+ if (entry === null) stash[QUERY_FOUT_KEY] = void 0;
931
+ else stash[QUERY_FOUT_KEY] = entry;
932
+ };
933
+ const closeQueryFout = async (settings) => {
934
+ const stash = settings;
935
+ const prev = stash[QUERY_FOUT_KEY];
936
+ if (prev) {
937
+ stash[QUERY_FOUT_KEY] = void 0;
938
+ await prev.close();
939
+ }
940
+ };
941
+ const WATCH_TEST_CONTROLLER = { ref: null };
942
+ const errResult$1 = (ctx, message) => {
943
+ ctx.settings.lastErrorResult = { message };
944
+ const prefix = psqlErrorPrefix(ctx.settings);
945
+ writeErr(`${prefix}\\${ctx.cmdName}: ${message}\n`);
946
+ return {
947
+ status: "error",
948
+ errorWritten: true
949
+ };
950
+ };
951
+ /**
952
+ * Reject buffer-consuming commands when an extended pipeline is open. Upstream
953
+ * `exec_command_g` / `gx` / `gset` / `gexec` / `watch` all guard with
954
+ * `PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF` and emit
955
+ * `pg_log_error("\\%s not allowed in pipeline mode", cmd)` (note: no `:`
956
+ * after the command name — different shape from `errResult`).
957
+ *
958
+ * `\gdesc` is the odd one out: upstream uses
959
+ * `pg_log_error("synchronous command execution functions are not allowed in
960
+ * pipeline mode")` because the underlying `PQdescribePrepared`/`PQfn`-style
961
+ * helpers all share that text — the regress baseline asserts this exact
962
+ * wording at three call sites in `psql_pipeline.out`.
963
+ *
964
+ * If the command proceeded it would inject a synchronous Query/Sync into the
965
+ * queue, corrupt the pipeline state, and leave `\endpipeline` waiting forever.
966
+ *
967
+ * Returns `null` when not in pipeline mode (caller proceeds); otherwise
968
+ * returns a populated error result the caller should bubble up.
969
+ *
970
+ * Upstream psql 18.4 leaks gate diagnostics through `pg_log_error_internal`
971
+ * which appends to the libpq result error log on the underlying PGresult;
972
+ * each subsequent gate hit RE-EMITS the full accumulated log plus its own
973
+ * line ("Error messages accumulate and are repeated" — the regress comment
974
+ * is the spec). Two `\gdesc` calls back-to-back therefore emit 3 lines
975
+ * total: 1 for the first call, 2 for the second (one accumulated + one
976
+ * own). Mirror that with a settings-stashed accumulator keyed off the
977
+ * current pipeline session; reset when the pipeline ends.
978
+ */
979
+ const PIPELINE_GATE_ERRORS_KEY = Symbol.for("neonctl.psql.pipelineGateErrors");
980
+ const getGateErrors = (settings) => {
981
+ const s = settings;
982
+ let cur = s[PIPELINE_GATE_ERRORS_KEY];
983
+ if (!cur) {
984
+ cur = [];
985
+ s[PIPELINE_GATE_ERRORS_KEY] = cur;
986
+ }
987
+ return cur;
988
+ };
989
+ /**
990
+ * Drop the accumulated pipeline-gate errors. Called from
991
+ * `\endpipeline` so the next pipeline session starts fresh — without
992
+ * this, gate errors from a closed pipeline would leak into the next
993
+ * one.
994
+ */
995
+ const clearPipelineGateErrors = (settings) => {
996
+ const s = settings;
997
+ s[PIPELINE_GATE_ERRORS_KEY] = void 0;
998
+ };
999
+ const pipelineGate = (ctx) => {
1000
+ if (ctx.settings.sendMode !== "extended-pipeline") return null;
1001
+ const message = ctx.cmdName === "gdesc" ? "synchronous command execution functions are not allowed in pipeline mode" : `\\${ctx.cmdName} not allowed in pipeline mode`;
1002
+ ctx.settings.lastErrorResult = { message };
1003
+ const prefix = psqlErrorPrefix(ctx.settings);
1004
+ if (ctx.cmdName === "gdesc") {
1005
+ const log = getGateErrors(ctx.settings);
1006
+ log.push(message);
1007
+ for (const m of log) writeErr(`${prefix}${m}\n`);
1008
+ } else writeErr(`${prefix}${message}\n`);
1009
+ return {
1010
+ status: "error",
1011
+ errorWritten: true
1012
+ };
1013
+ };
1014
+ /**
1015
+ * Set of psql variables upstream marks as "specially treated" — i.e. names
1016
+ * that have a substitute / assign hook installed in `startup.c`'s
1017
+ * `EstablishVariableSpace`. Used by `\gset` to reject assignments into
1018
+ * those names (matching upstream `StoreQueryTuple`'s `VariableHasHook`
1019
+ * check). We mirror the upstream list directly so a `\gset IGNORE` into
1020
+ * `IGNOREEOF` produces the conformance-expected warning even though our
1021
+ * settings.ts hasn't installed the IGNOREEOF / HISTFILE hooks yet — that
1022
+ * gap is tracked separately and harmless because the values are read-only
1023
+ * for us.
1024
+ */
1025
+ const UPSTREAM_SPECIAL_VAR_NAMES = /* @__PURE__ */ new Set([
1026
+ "AUTOCOMMIT",
1027
+ "COMP_KEYWORD_CASE",
1028
+ "ECHO",
1029
+ "ECHO_HIDDEN",
1030
+ "FETCH_COUNT",
1031
+ "HIDE_TABLEAM",
1032
+ "HIDE_TOAST_COMPRESSION",
1033
+ "HISTCONTROL",
1034
+ "HISTFILE",
1035
+ "HISTSIZE",
1036
+ "IGNOREEOF",
1037
+ "ON_ERROR_ROLLBACK",
1038
+ "ON_ERROR_STOP",
1039
+ "PROMPT1",
1040
+ "PROMPT2",
1041
+ "PROMPT3",
1042
+ "QUIET",
1043
+ "SHOW_ALL_RESULTS",
1044
+ "SHOW_CONTEXT",
1045
+ "SINGLELINE",
1046
+ "SINGLESTEP",
1047
+ "VERBOSITY"
1048
+ ]);
1049
+ /**
1050
+ * True when `name` is a psql variable that `\gset` must skip with an
1051
+ * "attempt to \gset into specially treated variable" message. Combines the
1052
+ * registered-hook check (so future hook installations are automatically
1053
+ * covered) with the upstream-canonical list above (so cases like
1054
+ * IGNOREEOF that aren't hooked in our settings.ts still match upstream's
1055
+ * `\gset` behaviour exactly).
1056
+ */
1057
+ const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) || UPSTREAM_SPECIAL_VAR_NAMES.has(name);
1058
+ /**
1059
+ * Strip line and block comments from `sql` so a COPY-shaped token inside a
1060
+ * comment (e.g. dash-dash `COPY t TO STDOUT`) doesn't trigger the
1061
+ * `\g FILE` mid-batch sink. Mirrors the cheap normaliser the mainloop uses
1062
+ * before wiring `copyOutMidBatchSink`. Embedded literals are NOT stripped —
1063
+ * `'COPY x TO STDOUT'` would still match `hasCopyToStdout`, but that's the
1064
+ * same false-positive shape upstream tolerates (the regex sweep is
1065
+ * intentionally conservative, and the worst-case outcome is "route bytes
1066
+ * that never arrive to the file" — harmless).
1067
+ */
1068
+ const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/--[^\n]*/gu, "");
1069
+ /**
1070
+ * True when `sql` contains at least one `COPY ... TO STDOUT` segment.
1071
+ * Used by `runGCore` to install a CopyData sink while `\g` / `\gx` /
1072
+ * `\g FILE` / `\g |cmd` dispatches a `\;`-chained batch that mixes
1073
+ * COPY-OUT with regular SELECT statements. Without the sink the wire
1074
+ * layer drops the CopyData bytes on the floor, and the file/pipe ends
1075
+ * up with the surrounding tuple results only — see regress/psql lines
1076
+ * 5760-5787 (`COPY (SELECT 'foo') TO STDOUT \; COPY (SELECT 'bar') TO
1077
+ * STDOUT \g :g_out_file`).
1078
+ */
1079
+ const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlCommentsForCopyScan(sql));
1080
+ /**
1081
+ * Render a server-side error in upstream psql's 3-line shape (severity +
1082
+ * message, then `LINE N:` / `^` re-print) and refresh the `:LAST_ERROR_*`
1083
+ * diagnostic variables so a subsequent `\errverbose` sees the rich payload.
1084
+ *
1085
+ * Mirrors the path that `core/common.ts::writeQueryError` takes for top-level
1086
+ * statement errors: capture full ErrorResponse fields onto
1087
+ * `settings.lastErrorResult`, render via `formatErrorReport` (honouring
1088
+ * VERBOSITY + SHOW_CONTEXT), prefix only the leading severity line with
1089
+ * `psql:[<file>:<n>]:`, and update the per-statement diagnostic vars via
1090
+ * `refreshErrorVars`.
1091
+ *
1092
+ * Used by `\g`, `\gx`, `\gset`, `\gdesc`, and `\gexec` so a server-rejected
1093
+ * statement dispatched through them renders the same shape vanilla psql
1094
+ * produces, instead of the legacy `\<cmd>: <message>` one-liner.
1095
+ */
1096
+ const formatServerError = (ctx, err, sql) => {
1097
+ const msg = captureLastError(ctx.settings, err, sql);
1098
+ const e = ctx.settings.lastErrorResult;
1099
+ if (e) {
1100
+ const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
1101
+ const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
1102
+ writeErr(prefixed.join("\n") + "\n");
1103
+ } else {
1104
+ const prefix = psqlErrorPrefix(ctx.settings);
1105
+ writeErr(`${prefix}ERROR: ${msg}\n`);
1106
+ }
1107
+ refreshErrorVars(ctx.settings, { kind: "error" });
1108
+ return {
1109
+ status: "error",
1110
+ errorWritten: true
1111
+ };
1112
+ };
1113
+ /**
1114
+ * Open a writable destination for `\o` / `\w` / `\g FILE` / `\g |cmd`.
1115
+ *
1116
+ * `target` of the form `|cmd` spawns `sh -c cmd` and pipes to its stdin.
1117
+ * The returned closer waits for the child to exit and resolves to its
1118
+ * status + terminating signal (if any) so callers can render
1119
+ * `wait_result_to_str`-style errors. Any other string is treated as a
1120
+ * file path; the file is truncated.
1121
+ */
1122
+ const openWriter = (target) => {
1123
+ if (target.startsWith("|")) {
1124
+ const cmd = target.slice(1);
1125
+ const child = spawn("sh", ["-c", cmd], { stdio: [
1126
+ "pipe",
1127
+ "inherit",
1128
+ "inherit"
1129
+ ] });
1130
+ child.stdin.on("error", (err) => {
1131
+ if (err.code !== "EPIPE") throw err;
1132
+ });
1133
+ return {
1134
+ stream: child.stdin,
1135
+ isPipe: true,
1136
+ close: () => new Promise((resolve) => {
1137
+ let settled = false;
1138
+ const finish = (code, signal) => {
1139
+ if (settled) return;
1140
+ settled = true;
1141
+ resolve({
1142
+ exitCode: code,
1143
+ signal
1144
+ });
1145
+ };
1146
+ child.once("close", (code, signal) => {
1147
+ finish(code, signal);
1148
+ });
1149
+ child.once("error", () => {
1150
+ finish(127, null);
1151
+ });
1152
+ if (!child.stdin.destroyed) child.stdin.end();
1153
+ })
1154
+ };
1155
+ }
1156
+ const fd = openSync(target, "w");
1157
+ const stream = createWriteStream(target, {
1158
+ encoding: "utf8",
1159
+ fd,
1160
+ autoClose: false
1161
+ });
1162
+ let writeError = null;
1163
+ stream.on("error", (err) => {
1164
+ writeError = writeError ?? err;
1165
+ });
1166
+ return {
1167
+ stream,
1168
+ isPipe: false,
1169
+ close: () => new Promise((resolve, reject) => {
1170
+ if (writeError !== null) {
1171
+ try {
1172
+ closeSync(fd);
1173
+ } catch {}
1174
+ reject(writeError);
1175
+ return;
1176
+ }
1177
+ stream.end((err) => {
1178
+ if (err) {
1179
+ try {
1180
+ closeSync(fd);
1181
+ } catch {}
1182
+ reject(err);
1183
+ return;
1184
+ }
1185
+ try {
1186
+ fsyncSync(fd);
1187
+ } catch {}
1188
+ try {
1189
+ closeSync(fd);
1190
+ } catch (closeErr) {
1191
+ reject(closeErr);
1192
+ return;
1193
+ }
1194
+ if (platform() === "darwin") {
1195
+ setTimeout(() => {
1196
+ resolve({});
1197
+ }, 25);
1198
+ return;
1199
+ }
1200
+ resolve({});
1201
+ });
1202
+ })
1203
+ };
1204
+ };
1205
+ /**
1206
+ * Map a Node.js errno (`err.code`) to the libc `strerror()` string
1207
+ * upstream psql renders in its `pg_log_error("%s: %m", fname)` path.
1208
+ *
1209
+ * Falls back to `err.message` (with the verbose `ENOENT: ...` prefix
1210
+ * stripped if present) so unmapped errno values still surface
1211
+ * meaningful text instead of a cryptic Node-internal phrasing.
1212
+ */
1213
+ const errnoToStrerror = (err) => {
1214
+ switch (err.code) {
1215
+ case "ENOENT": return "No such file or directory";
1216
+ case "EACCES": return "Permission denied";
1217
+ case "EISDIR": return "Is a directory";
1218
+ case "ENOTDIR": return "Not a directory";
1219
+ case "EEXIST": return "File exists";
1220
+ case "EROFS": return "Read-only file system";
1221
+ case "ELOOP": return "Too many levels of symbolic links";
1222
+ case "ENAMETOOLONG": return "File name too long";
1223
+ case "ENOSPC": return "No space left on device";
1224
+ case "EMFILE": return "Too many open files";
1225
+ case "ENFILE": return "Too many open files in system";
1226
+ case "EIO": return "Input/output error";
1227
+ case "EFBIG": return "File too large";
1228
+ case "EDQUOT": return "Disk quota exceeded";
1229
+ case "EPERM": return "Operation not permitted";
1230
+ case "EINVAL": return "Invalid argument";
1231
+ default: {
1232
+ const m = /^[A-Z]+: ([^,]+)/.exec(err.message);
1233
+ return m ? m[1] : err.message;
1234
+ }
1235
+ }
1236
+ };
1237
+ /**
1238
+ * Emit a file-open failure for `\g FILE`, `\o FILE`, `\w FILE` in the
1239
+ * exact shape vanilla psql produces: a bare `<path>: <strerror>` line
1240
+ * on stderr, no `\<cmd>:` prefix (matches `pg_log_error` under terse
1241
+ * mode, which is what `psql -X` uses).
1242
+ *
1243
+ * The leading `psql:[<file>:<n>]:` tag is still applied when we're
1244
+ * reading SQL from a `\i FILE` include — `psqlErrorPrefix` returns ''
1245
+ * for stdin so the line stays bare for the interactive / harness case.
1246
+ *
1247
+ * Returns an `error` envelope with `errorWritten: true` so the mainloop
1248
+ * doesn't write a duplicate `psql: ERROR:` fallback.
1249
+ */
1250
+ const reportFileOpenFailure = (ctx, target, err) => {
1251
+ const line = `${target}: ${errnoToStrerror(err)}`;
1252
+ ctx.settings.lastErrorResult = { message: line };
1253
+ const prefix = psqlErrorPrefix(ctx.settings);
1254
+ writeErr(`${prefix}${line}\n`);
1255
+ return {
1256
+ status: "error",
1257
+ errorWritten: true
1258
+ };
1259
+ };
1260
+ /**
1261
+ * True when `err` was thrown by our synchronous `openSync` in
1262
+ * {@link openWriter} (i.e. has an errno `code`) and the caller should
1263
+ * render it via {@link reportFileOpenFailure} rather than the generic
1264
+ * `\<cmd>: <msg>` path.
1265
+ */
1266
+ const isFileOpenFailure = (err) => {
1267
+ if (!err || typeof err !== "object") return false;
1268
+ const e = err;
1269
+ return typeof e.code === "string" && e.code.startsWith("E");
1270
+ };
1271
+ /**
1272
+ * Format a child process exit code + signal into upstream psql's
1273
+ * `wait_result_to_str` style. Mirrors the C helper in
1274
+ * `src/common/wait_error.c`:
1275
+ *
1276
+ * - exit code 127 → `command not found`
1277
+ * - exit code 126 → `command was not executable`
1278
+ * - any other code → `child process exited with exit code N`
1279
+ * - terminated by signal S → `child process was terminated by
1280
+ * signal N: <SIG>`
1281
+ *
1282
+ * Returns null when the child exited cleanly (code 0, no signal).
1283
+ */
1284
+ const formatChildWaitResult = (exitCode, signal) => {
1285
+ if (signal) return `child process was terminated by signal: ${signal}`;
1286
+ if (exitCode === null || exitCode === void 0) return null;
1287
+ if (exitCode === 0) return null;
1288
+ if (exitCode === 127) return "command not found";
1289
+ if (exitCode === 126) return "command was not executable";
1290
+ return `child process exited with exit code ${String(exitCode)}`;
1291
+ };
1292
+ /**
1293
+ * Compose the `CommandComplete`-tag line upstream prints for non-tuples-
1294
+ * producing results (DDL, DML without RETURNING, COPY). Mirrors
1295
+ * `formatCommandTag` in `core/common.ts` — duplicated to avoid the
1296
+ * cmd_io → common import cycle. Returns an empty string when no tag is
1297
+ * available (e.g. `EmptyQueryResponse` carries `command = ''`).
1298
+ */
1299
+ const formatCommandTagText = (rs) => {
1300
+ const command = (rs.command || "").trim();
1301
+ if (command.length === 0) return "";
1302
+ if (command === "INSERT") return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
1303
+ if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${String(rs.rowCount)}`;
1304
+ return command;
1305
+ };
1306
+ /**
1307
+ * Render a `ResultSet` to the supplied writable stream using the printer
1308
+ * picked from the active `\pset format`. Upstream's `do_watch`, `do_gset`,
1309
+ * `do_gexec`, and `\i` all funnel results through the standard query
1310
+ * output pipeline (`ExecQueryAndProcessResults` → `printQuery`), which
1311
+ * honours `\pset format`. Hard-coding the aligned printer here breaks the
1312
+ * conformance harness's `psql -A` runs (which expect unaligned tuples-only
1313
+ * output for things like `\watch` polled rows).
1314
+ *
1315
+ * Non-tuples-producing results (CommandComplete with `fields.length === 0`,
1316
+ * which covers DDL, DML without RETURNING, and the post-CopyDone tag for
1317
+ * `COPY ... TO STDOUT`) are rendered as a single status line instead of
1318
+ * the printer's `(0 rows)` empty-table block — matching `renderResultSets`
1319
+ * in `core/common.ts`. Tuples-only (`\t`) and quiet (`--quiet`) both
1320
+ * suppress the tag entirely.
1321
+ */
1322
+ const renderResult = async (settings, rs, out) => {
1323
+ if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) out.write(chunk);
1324
+ if (rs.fields.length === 0) {
1325
+ if (!settings.popt.topt.tuplesOnly && !settings.quiet && !rs.copyOutBytes) {
1326
+ const tag = formatCommandTagText(rs);
1327
+ if (tag.length > 0) out.write(`${tag}\n`);
1328
+ }
1329
+ return;
1330
+ }
1331
+ await pickActivePrinter(settings).printQuery(rs, settings.popt, out);
1332
+ };
1333
+ /**
1334
+ * Pick the printer for the active output format. Mirrors `pickPrinter`
1335
+ * in `core/common.ts` — duplicated here to avoid the cmd_io → common
1336
+ * import cycle (common.ts depends on this file for `getQueryFout`).
1337
+ *
1338
+ * `wrapped` falls back to the aligned printer (which renders `wrapped`
1339
+ * mode itself via `topt.format`).
1340
+ */
1341
+ const pickActivePrinter = (settings) => {
1342
+ switch (settings.popt.topt.format) {
1343
+ case "aligned":
1344
+ case "wrapped": return alignedPrinter;
1345
+ case "unaligned": return unalignedPrinter;
1346
+ case "csv": return csvPrinter;
1347
+ case "json": return jsonPrinter;
1348
+ case "html": return htmlPrinter;
1349
+ case "asciidoc": return asciidocPrinter;
1350
+ case "latex": return latexPrinter;
1351
+ case "latex-longtable": return latexLongtablePrinter;
1352
+ case "troff-ms": return troffMsPrinter;
1353
+ default: return alignedPrinter;
1354
+ }
1355
+ };
1356
+ /**
1357
+ * Pick the output target for a query result.
1358
+ *
1359
+ * Precedence: explicit `oneShot` (e.g. `\g FILE`) > the settings stash
1360
+ * (`\o FILE`) > `process.stdout`.
1361
+ */
1362
+ const pickOut = (settings, oneShot) => {
1363
+ if (oneShot) return oneShot;
1364
+ return getQueryFout(settings) ?? process.stdout;
1365
+ };
1366
+ const runInclude = async (ctx, relative) => {
1367
+ const arg = ctx.nextArg("normal");
1368
+ if (arg === null || arg.length === 0) return errResult$1(ctx, "missing required argument");
1369
+ let resolved;
1370
+ if (path.isAbsolute(arg)) resolved = arg;
1371
+ else if (relative && ctx.settings.inputfile) resolved = path.resolve(path.dirname(ctx.settings.inputfile), arg);
1372
+ else resolved = path.resolve(process.cwd(), arg);
1373
+ let contents;
1374
+ try {
1375
+ contents = await promises.readFile(resolved, "utf8");
1376
+ } catch (err) {
1377
+ const msg = err instanceof Error ? err.message : String(err);
1378
+ return errResult$1(ctx, msg);
1379
+ }
1380
+ if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
1381
+ const trimmed = contents.trim();
1382
+ if (trimmed.length === 0) return { status: "ok" };
1383
+ const priorInputFile = ctx.settings.inputfile;
1384
+ ctx.settings.inputfile = resolved;
1385
+ try {
1386
+ const results = await ctx.settings.db.execSimple(trimmed);
1387
+ const out = pickOut(ctx.settings, null);
1388
+ for (const rs of results) await renderResult(ctx.settings, rs, out);
1389
+ return { status: "ok" };
1390
+ } catch (err) {
1391
+ const msg = err instanceof Error ? err.message : String(err);
1392
+ return errResult$1(ctx, msg);
1393
+ } finally {
1394
+ ctx.settings.inputfile = priorInputFile;
1395
+ }
1396
+ };
1397
+ const cmdInclude = {
1398
+ name: "i",
1399
+ aliases: ["include"],
1400
+ helpKey: "i",
1401
+ run: (ctx) => runInclude(ctx, false)
1402
+ };
1403
+ const cmdIncludeRel = {
1404
+ name: "ir",
1405
+ aliases: ["include_relative"],
1406
+ helpKey: "ir",
1407
+ run: (ctx) => runInclude(ctx, true)
1408
+ };
1409
+ const cmdOut = {
1410
+ name: "o",
1411
+ aliases: ["out"],
1412
+ helpKey: "o",
1413
+ async run(ctx) {
1414
+ const arg = ctx.nextArg("filepipe");
1415
+ await closeQueryFout(ctx.settings);
1416
+ if (arg === null || arg.length === 0) return { status: "ok" };
1417
+ try {
1418
+ const entry = openWriter(arg);
1419
+ setQueryFout(ctx.settings, entry);
1420
+ return { status: "ok" };
1421
+ } catch (err) {
1422
+ if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
1423
+ const msg = err instanceof Error ? err.message : String(err);
1424
+ return errResult$1(ctx, msg);
1425
+ }
1426
+ }
1427
+ };
1428
+ const cmdWrite = {
1429
+ name: "w",
1430
+ aliases: ["write"],
1431
+ helpKey: "w",
1432
+ async run(ctx) {
1433
+ const arg = ctx.nextArg("filepipe");
1434
+ if (arg === null || arg.length === 0) return errResult$1(ctx, "missing required argument");
1435
+ let entry;
1436
+ try {
1437
+ entry = openWriter(arg);
1438
+ } catch (err) {
1439
+ if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
1440
+ const msg = err instanceof Error ? err.message : String(err);
1441
+ return errResult$1(ctx, msg);
1442
+ }
1443
+ try {
1444
+ await new Promise((resolve, reject) => {
1445
+ entry.stream.write(ctx.queryBuf, (err) => {
1446
+ if (err) reject(err);
1447
+ else resolve();
1448
+ });
1449
+ });
1450
+ } catch (err) {
1451
+ const isEpipe = err instanceof Error && err.code === "EPIPE";
1452
+ if (!entry.isPipe || !isEpipe) {
1453
+ try {
1454
+ await entry.close();
1455
+ } catch {}
1456
+ const msg = err instanceof Error ? err.message : String(err);
1457
+ return errResult$1(ctx, msg);
1458
+ }
1459
+ }
1460
+ try {
1461
+ const result = await entry.close();
1462
+ if (entry.isPipe) {
1463
+ const msg = formatChildWaitResult(result.exitCode, result.signal);
1464
+ if (msg !== null) {
1465
+ const line = `${arg}: ${msg}`;
1466
+ ctx.settings.lastErrorResult = { message: line };
1467
+ const prefix = psqlErrorPrefix(ctx.settings);
1468
+ writeErr(`${prefix}${line}\n`);
1469
+ return {
1470
+ status: "error",
1471
+ errorWritten: true
1472
+ };
1473
+ }
1474
+ }
1475
+ return { status: "ok" };
1476
+ } catch (err) {
1477
+ const msg = err instanceof Error ? err.message : String(err);
1478
+ return errResult$1(ctx, msg);
1479
+ }
1480
+ }
1481
+ };
1482
+ /**
1483
+ * Parse the body of a `\g (option=value option2=value2 ...)` clause —
1484
+ * the text between the outer parentheses, already stripped. Options
1485
+ * are separated by whitespace; values may be single-quoted to embed
1486
+ * spaces. Unquoted values run to the next whitespace.
1487
+ *
1488
+ * Mirrors upstream's `parse_slash_pgopts_list`. We deliberately stay
1489
+ * narrow — the conformance corpus exercises `format=`, `csv_fieldsep=`,
1490
+ * and `title=` only.
1491
+ */
1492
+ const parseGPsetOptions = (body) => {
1493
+ const out = [];
1494
+ let i = 0;
1495
+ while (i < body.length) {
1496
+ while (i < body.length && /\s/.test(body[i])) i++;
1497
+ if (i >= body.length) break;
1498
+ const optStart = i;
1499
+ while (i < body.length && body[i] !== "=" && !/\s/.test(body[i])) i++;
1500
+ const option = body.slice(optStart, i);
1501
+ if (option.length === 0) break;
1502
+ let value = "";
1503
+ if (body[i] === "=") {
1504
+ i++;
1505
+ if (body[i] === "'") {
1506
+ i++;
1507
+ while (i < body.length && body[i] !== "'") {
1508
+ if (body[i] === "\\" && i + 1 < body.length) {
1509
+ const next = body[i + 1];
1510
+ if (next === "n") value += "\n";
1511
+ else if (next === "t") value += " ";
1512
+ else if (next === "r") value += "\r";
1513
+ else if (next === "\\") value += "\\";
1514
+ else if (next === "'") value += "'";
1515
+ else value += next;
1516
+ i += 2;
1517
+ continue;
1518
+ }
1519
+ value += body[i++];
1520
+ }
1521
+ if (body[i] === "'") i++;
1522
+ } else {
1523
+ const vStart = i;
1524
+ while (i < body.length && !/\s/.test(body[i])) i++;
1525
+ value = body.slice(vStart, i);
1526
+ }
1527
+ }
1528
+ out.push({
1529
+ option,
1530
+ value
1531
+ });
1532
+ }
1533
+ return out;
1534
+ };
1535
+ const runGCore = async (ctx, forceExpanded) => {
1536
+ const gated = pipelineGate(ctx);
1537
+ if (gated !== null) return gated;
1538
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1539
+ const bufSql = trimmedBuf.trim();
1540
+ let target;
1541
+ let psetOverrides = null;
1542
+ const rawTrimmed = ctx.rawArgs.trimStart();
1543
+ if (rawTrimmed.startsWith("(")) {
1544
+ const close = rawTrimmed.indexOf(")");
1545
+ if (close === -1) return errResult$1(ctx, "missing right parenthesis in \\g options");
1546
+ psetOverrides = parseGPsetOptions(rawTrimmed.slice(1, close).trim());
1547
+ const afterParen = rawTrimmed.slice(close + 1).trim();
1548
+ target = afterParen.length > 0 ? afterParen : null;
1549
+ } else target = ctx.nextArg("filepipe");
1550
+ const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery;
1551
+ const hasPendingNamedBind = stagedNamedBindPresent(ctx.settings);
1552
+ if (sql.length === 0 && !hasPendingNamedBind) return {
1553
+ status: "reset-buf",
1554
+ newBuf: ""
1555
+ };
1556
+ if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
1557
+ let oneShot = null;
1558
+ if (target !== null && target.length > 0) try {
1559
+ oneShot = openWriter(target);
1560
+ } catch (err) {
1561
+ if (!target.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, target, err);
1562
+ const msg = err instanceof Error ? err.message : String(err);
1563
+ return errResult$1(ctx, msg);
1564
+ }
1565
+ const topt = ctx.settings.popt.topt;
1566
+ const toptSnapshot = { ...topt };
1567
+ if (forceExpanded) topt.expanded = "on";
1568
+ if (psetOverrides) for (const { option, value } of psetOverrides) applyPset(topt, option, value, ctx.cmdName, true);
1569
+ ctx.settings.lastQuery = sql;
1570
+ const bindState = consumeBindState(ctx.settings);
1571
+ let execError = null;
1572
+ let copyOutSinkConn = null;
1573
+ try {
1574
+ const out = pickOut(ctx.settings, oneShot?.stream ?? null);
1575
+ if (bindState?.byName) {
1576
+ const ps = lookupPrepared(ctx.settings, bindState.name);
1577
+ if (!ps) execError = Object.assign(/* @__PURE__ */ new Error(`prepared statement "${bindState.name}" does not exist`), {
1578
+ severity: "ERROR",
1579
+ code: "26000"
1580
+ });
1581
+ else {
1582
+ const rs = await ps.bindAndExecute(bindState.values);
1583
+ await renderResult(ctx.settings, rs, out);
1584
+ }
1585
+ } else if (bindState) {
1586
+ const rs = await ctx.settings.db.query(sql, bindState.values);
1587
+ await renderResult(ctx.settings, rs, out);
1588
+ } else {
1589
+ if (hasCopyToStdout(sql)) {
1590
+ copyOutSinkConn = ctx.settings.db;
1591
+ copyOutSinkConn.copyOutMidBatchSink = (chunk) => {
1592
+ out.write(chunk);
1593
+ };
1594
+ }
1595
+ const results = await ctx.settings.db.execSimple(sql);
1596
+ for (const rs of results) await renderResult(ctx.settings, rs, out);
1597
+ }
1598
+ } catch (err) {
1599
+ execError = err;
1600
+ } finally {
1601
+ Object.assign(topt, toptSnapshot);
1602
+ if (copyOutSinkConn) copyOutSinkConn.copyOutMidBatchSink = null;
1603
+ }
1604
+ let pipeError = null;
1605
+ if (oneShot) try {
1606
+ await oneShot.close();
1607
+ } catch (err) {
1608
+ pipeError = err instanceof Error ? err.message : String(err);
1609
+ }
1610
+ if (execError !== null) return formatServerError(ctx, execError, bufSql.length > 0 ? trimmedBuf : sql);
1611
+ if (pipeError !== null) return errResult$1(ctx, pipeError);
1612
+ return {
1613
+ status: "reset-buf",
1614
+ newBuf: ""
1615
+ };
1616
+ };
1617
+ const cmdG = {
1618
+ name: "g",
1619
+ helpKey: "g",
1620
+ run: (ctx) => runGCore(ctx, false)
1621
+ };
1622
+ const cmdGx = {
1623
+ name: "gx",
1624
+ helpKey: "gx",
1625
+ run: (ctx) => runGCore(ctx, true)
1626
+ };
1627
+ /**
1628
+ * `\p` / `\print` — print the query buffer the next `\g` would execute.
1629
+ *
1630
+ * Mirrors upstream `exec_command_print` in `src/bin/psql/command.c`:
1631
+ *
1632
+ * if (query_buf && query_buf->len > 0)
1633
+ * puts(query_buf->data);
1634
+ * else if (previous_buf && previous_buf->len > 0)
1635
+ * puts(previous_buf->data);
1636
+ * else if (!pset.quiet)
1637
+ * puts(_("Query buffer is empty."));
1638
+ *
1639
+ * Buffer-vs-previous-buffer precedence matters for the regress sequence:
1640
+ *
1641
+ * SELECT 1; -- executes, previous_buf := "SELECT 1;"
1642
+ * \p -- queryBuf empty → prints previous_buf
1643
+ * SELECT 2 \r -- queryBuf="SELECT 2 ", \r resets to "" without
1644
+ * -- touching previous_buf
1645
+ * \p -- queryBuf still empty → prints previous_buf
1646
+ * SELECT 3 \p -- queryBuf="SELECT 3 ", non-empty → prints queryBuf
1647
+ *
1648
+ * Implementation notes:
1649
+ *
1650
+ * - We use `settings.lastQuery` as the previous-buffer source. Upstream
1651
+ * tracks `previous_buf` independently of `pset.last_query`, but our
1652
+ * `lastQuery` is set at the exact same point upstream sets
1653
+ * `previous_buf` (the dispatch site in `SendQuery`-equivalent code paths
1654
+ * in `core/common.ts` and `cmd_io.ts`'s `\g` implementation), so the
1655
+ * semantics match for every shape exercised by the conformance corpus.
1656
+ * - We must NOT clear queryBuf — return `status: 'ok'` so the mainloop
1657
+ * leaves the buffer untouched. The user is inspecting, not executing.
1658
+ * - `puts()` appends a trailing newline. We use `writeOut` and append `\n`
1659
+ * explicitly to match.
1660
+ */
1661
+ const cmdPrint = {
1662
+ name: "p",
1663
+ aliases: ["print"],
1664
+ helpKey: "p",
1665
+ run: (ctx) => {
1666
+ if (ctx.queryBuf.trim().length > 0) writeOut(`${ctx.queryBuf}\n`);
1667
+ else if (ctx.settings.lastQuery.length > 0) writeOut(`${ctx.settings.lastQuery}\n`);
1668
+ else if (!ctx.settings.quiet) writeOut("Query buffer is empty.\n");
1669
+ return Promise.resolve({ status: "ok" });
1670
+ }
1671
+ };
1672
+ const formatCell = (value) => {
1673
+ if (value === null || value === void 0) return "";
1674
+ if (typeof value === "string") return value;
1675
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
1676
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
1677
+ try {
1678
+ return JSON.stringify(value);
1679
+ } catch {
1680
+ return "";
1681
+ }
1682
+ };
1683
+ const cmdGset = {
1684
+ name: "gset",
1685
+ helpKey: "gset",
1686
+ async run(ctx) {
1687
+ const gated = pipelineGate(ctx);
1688
+ if (gated !== null) return gated;
1689
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1690
+ const bufSql = trimmedBuf.trim();
1691
+ const prefix = ctx.nextArg("normal") ?? "";
1692
+ const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
1693
+ if (sql.length === 0) return {
1694
+ status: "reset-buf",
1695
+ newBuf: ""
1696
+ };
1697
+ if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
1698
+ ctx.settings.lastQuery = sql;
1699
+ let results;
1700
+ try {
1701
+ results = await ctx.settings.db.execSimple(sql);
1702
+ } catch (err) {
1703
+ return formatServerError(ctx, err, bufSql.length > 0 ? trimmedBuf : sql);
1704
+ }
1705
+ if (results.length > 1) {
1706
+ const out = pickOut(ctx.settings, null);
1707
+ for (let i = 0; i < results.length - 1; i++) await renderResult(ctx.settings, results[i], out);
1708
+ }
1709
+ const lastRs = results[results.length - 1];
1710
+ if (!lastRs || lastRs.fields.length === 0) return {
1711
+ status: "reset-buf",
1712
+ newBuf: ""
1713
+ };
1714
+ const rs = lastRs;
1715
+ if (rs.rows.length === 0) {
1716
+ ctx.settings.lastErrorResult = { message: "no rows returned for \\gset" };
1717
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1718
+ writeErr(`${errPrefix}no rows returned for \\gset\n`);
1719
+ return {
1720
+ status: "error",
1721
+ errorWritten: true
1722
+ };
1723
+ }
1724
+ if (rs.rows.length > 1) {
1725
+ ctx.settings.lastErrorResult = { message: "more than one row returned for \\gset" };
1726
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1727
+ writeErr(`${errPrefix}more than one row returned for \\gset\n`);
1728
+ return {
1729
+ status: "error",
1730
+ errorWritten: true
1731
+ };
1732
+ }
1733
+ const row = rs.rows[0];
1734
+ for (let i = 0; i < rs.fields.length; i++) {
1735
+ const fieldName = rs.fields[i].name;
1736
+ const name = `${prefix}${fieldName}`;
1737
+ const cell = row[i];
1738
+ const isNull = cell === null || cell === void 0;
1739
+ if (isSpeciallyTreatedVar(ctx.settings, name)) {
1740
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1741
+ writeErr(`${errPrefix}attempt to \\gset into specially treated variable "${name}" ignored\n`);
1742
+ continue;
1743
+ }
1744
+ if (isNull) {
1745
+ ctx.settings.vars.unset(name);
1746
+ continue;
1747
+ }
1748
+ const value = formatCell(cell);
1749
+ if (!ctx.settings.vars.set(name, value)) {
1750
+ ctx.settings.lastErrorResult = { message: `invalid variable name: "${fieldName}"` };
1751
+ const errPrefix = psqlErrorPrefix(ctx.settings);
1752
+ writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
1753
+ return {
1754
+ status: "error",
1755
+ errorWritten: true
1756
+ };
1757
+ }
1758
+ }
1759
+ return {
1760
+ status: "reset-buf",
1761
+ newBuf: ""
1762
+ };
1763
+ }
1764
+ };
1765
+ /**
1766
+ * Build the SQL that resolves each describe-result column's `Type` via
1767
+ * `pg_catalog.format_type(typoid, typmod)`. We feed the names + OIDs
1768
+ * + typmods through a `VALUES` literal so the server does the formatting
1769
+ * for us — the same query upstream issues from `describeFieldsByType`.
1770
+ *
1771
+ * Returns null when there are zero fields (caller emits `(0 rows)` form
1772
+ * by hand because PostgreSQL rejects an empty VALUES list).
1773
+ */
1774
+ const buildGdescFormatQuery = (fields) => {
1775
+ if (fields.length === 0) return null;
1776
+ return `SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type" FROM (VALUES ${fields.map((f, i) => {
1777
+ const safeName = f.name.replace(/'/gu, "''");
1778
+ const oid = String(f.dataTypeID >>> 0);
1779
+ const typmod = String(f.dataTypeModifier | 0);
1780
+ return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
1781
+ }).join(", ")}) AS x(_idx, name, tp, tpm) ORDER BY _idx`;
1782
+ };
1783
+ /**
1784
+ * Field descriptors for the synthetic `Column / Type` ResultSet that
1785
+ * `\gdesc` emits when format_type resolution fails or yields nothing.
1786
+ *
1787
+ * We fall back to the field's raw OID so the user still sees a value.
1788
+ */
1789
+ const GDESC_SYNTHETIC_FIELDS = [{
1790
+ name: "Column",
1791
+ tableID: 0,
1792
+ columnID: 0,
1793
+ dataTypeID: 25,
1794
+ dataTypeSize: -1,
1795
+ dataTypeModifier: -1,
1796
+ format: 0
1797
+ }, {
1798
+ name: "Type",
1799
+ tableID: 0,
1800
+ columnID: 0,
1801
+ dataTypeID: 25,
1802
+ dataTypeSize: -1,
1803
+ dataTypeModifier: -1,
1804
+ format: 0
1805
+ }];
1806
+ const buildSyntheticGdescResultSet = (rows) => ({
1807
+ command: "SELECT",
1808
+ rowCount: rows.length,
1809
+ oid: null,
1810
+ fields: GDESC_SYNTHETIC_FIELDS,
1811
+ rows,
1812
+ notices: []
1813
+ });
1814
+ const cmdGdesc = {
1815
+ name: "gdesc",
1816
+ helpKey: "gdesc",
1817
+ async run(ctx) {
1818
+ const gated = pipelineGate(ctx);
1819
+ if (gated !== null) return gated;
1820
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1821
+ const sql = trimmedBuf.trim();
1822
+ if (sql.length === 0) {
1823
+ process.stdout.write("The command has no result, or the result has no columns.\n");
1824
+ refreshErrorVars(ctx.settings, {
1825
+ kind: "success",
1826
+ rowCount: 0
1827
+ });
1828
+ return {
1829
+ status: "reset-buf",
1830
+ newBuf: ""
1831
+ };
1832
+ }
1833
+ if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
1834
+ ctx.settings.lastQuery = sql;
1835
+ let fields;
1836
+ try {
1837
+ const stmt = await ctx.settings.db.prepare("", sql);
1838
+ fields = await stmt.describe();
1839
+ try {
1840
+ await stmt.close();
1841
+ } catch {}
1842
+ } catch (err) {
1843
+ return formatServerError(ctx, err, trimmedBuf);
1844
+ }
1845
+ if (fields.length === 0) {
1846
+ process.stdout.write("The command has no result, or the result has no columns.\n");
1847
+ refreshErrorVars(ctx.settings, {
1848
+ kind: "success",
1849
+ rowCount: 0
1850
+ });
1851
+ return {
1852
+ status: "reset-buf",
1853
+ newBuf: ""
1854
+ };
1855
+ }
1856
+ let rows;
1857
+ const formatQuery = buildGdescFormatQuery(fields);
1858
+ if (formatQuery === null) rows = [];
1859
+ else {
1860
+ const fallbackRows = () => fields.map((f) => [f.name, String(f.dataTypeID)]);
1861
+ try {
1862
+ const sets = await ctx.settings.db.execSimple(formatQuery);
1863
+ const last = sets[sets.length - 1];
1864
+ rows = last && last.rows.length > 0 ? last.rows : fallbackRows();
1865
+ } catch {
1866
+ rows = fallbackRows();
1867
+ }
1868
+ }
1869
+ const rs = buildSyntheticGdescResultSet(rows);
1870
+ const printer = pickActivePrinter(ctx.settings);
1871
+ const out = pickOut(ctx.settings, null);
1872
+ try {
1873
+ await printer.printQuery(rs, ctx.settings.popt, out);
1874
+ } catch (err) {
1875
+ const msg = err instanceof Error ? err.message : String(err);
1876
+ return errResult$1(ctx, msg);
1877
+ }
1878
+ refreshErrorVars(ctx.settings, {
1879
+ kind: "success",
1880
+ rowCount: rs.rowCount
1881
+ });
1882
+ return {
1883
+ status: "reset-buf",
1884
+ newBuf: ""
1885
+ };
1886
+ }
1887
+ };
1888
+ const cmdGexec = {
1889
+ name: "gexec",
1890
+ helpKey: "gexec",
1891
+ async run(ctx) {
1892
+ const gated = pipelineGate(ctx);
1893
+ if (gated !== null) return gated;
1894
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
1895
+ const bufSql = trimmedBuf.trim();
1896
+ const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
1897
+ if (sql.length === 0) return {
1898
+ status: "reset-buf",
1899
+ newBuf: ""
1900
+ };
1901
+ if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
1902
+ ctx.settings.lastQuery = sql;
1903
+ let firstPass;
1904
+ try {
1905
+ firstPass = await ctx.settings.db.execSimple(sql);
1906
+ } catch (err) {
1907
+ return formatServerError(ctx, err, trimmedBuf);
1908
+ }
1909
+ const tupled = firstPass.filter((r) => r.fields.length > 0);
1910
+ if (tupled.length === 0) return {
1911
+ status: "reset-buf",
1912
+ newBuf: ""
1913
+ };
1914
+ const out = pickOut(ctx.settings, null);
1915
+ const echo = ctx.settings.echo;
1916
+ const shouldEcho = echo === "all" || echo === "queries";
1917
+ for (const rs of tupled) for (const row of rs.rows) for (const cell of row) {
1918
+ if (cell === null || cell === void 0) continue;
1919
+ const statement = formatCell(cell).trim();
1920
+ if (statement.length === 0) continue;
1921
+ if (shouldEcho) out.write(statement + "\n");
1922
+ try {
1923
+ const nested = await ctx.settings.db.execSimple(statement);
1924
+ for (const sub of nested) if (sub.fields.length > 0) await renderResult(ctx.settings, sub, out);
1925
+ } catch (err) {
1926
+ formatServerError(ctx, err, statement);
1927
+ if (ctx.settings.onErrorStop) return {
1928
+ status: "error",
1929
+ errorWritten: true
1930
+ };
1931
+ }
1932
+ }
1933
+ return {
1934
+ status: "reset-buf",
1935
+ newBuf: ""
1936
+ };
1937
+ }
1938
+ };
1939
+ const sleepCancellable = (ms, signal) => new Promise((resolve) => {
1940
+ const timer = setTimeout(() => {
1941
+ signal.removeEventListener("abort", onAbort);
1942
+ resolve();
1943
+ }, ms);
1944
+ const onAbort = () => {
1945
+ clearTimeout(timer);
1946
+ signal.removeEventListener("abort", onAbort);
1947
+ resolve();
1948
+ };
1949
+ if (signal.aborted) {
1950
+ clearTimeout(timer);
1951
+ resolve();
1952
+ return;
1953
+ }
1954
+ signal.addEventListener("abort", onAbort);
1955
+ });
1956
+ /**
1957
+ * Strictly parse a non-negative finite float.
1958
+ *
1959
+ * Returns the parsed number, or `null` for any of:
1960
+ * - empty string
1961
+ * - non-numeric trailing characters (e.g. `10ab`)
1962
+ * - negative values (e.g. `-10`)
1963
+ * - out-of-range / non-finite results (e.g. `10e400` → Infinity)
1964
+ *
1965
+ * Used to validate `\watch` intervals and the `WATCH_INTERVAL` variable.
1966
+ */
1967
+ const parseStrictNonNegativeFloat = (raw) => {
1968
+ if (raw.length === 0) return null;
1969
+ if (!/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(raw)) return null;
1970
+ const value = parseFloat(raw);
1971
+ if (!Number.isFinite(value)) return null;
1972
+ if (value < 0) return null;
1973
+ return value;
1974
+ };
1975
+ /**
1976
+ * Parse a strict non-negative integer (no exponent, no fractional).
1977
+ * Used for `c=` and `m=` / `min_rows=` argument values.
1978
+ */
1979
+ const parseStrictNonNegativeInt = (raw) => {
1980
+ if (raw.length === 0) return null;
1981
+ if (!/^\d+$/.test(raw)) return null;
1982
+ const value = parseInt(raw, 10);
1983
+ if (!Number.isFinite(value)) return null;
1984
+ return value;
1985
+ };
1986
+ /**
1987
+ * Default `\watch` interval (seconds). Mirrors upstream
1988
+ * `DEFAULT_WATCH_INTERVAL`. Exported so `defaultSettings` can substitute
1989
+ * it when the user unsets the `WATCH_INTERVAL` variable — upstream's
1990
+ * `watch_interval_substitute_hook` reseeds the value to `2` on null.
1991
+ */
1992
+ const DEFAULT_WATCH_INTERVAL = "2";
1993
+ /**
1994
+ * Render `\watch`'s per-iteration timestamp in upstream psql's
1995
+ * `ctime`-style layout: `Day Mon DD HH:MM:SS YYYY` (e.g. `Mon May 25
1996
+ * 19:41:55 2026`). Upstream calls `strftime("%c", &tm)` with the C locale;
1997
+ * we reproduce the field order in vanilla English so the output matches
1998
+ * regardless of the host locale.
1999
+ *
2000
+ * Exported only for unit-testing the format ladder.
2001
+ */
2002
+ const WEEKDAYS = [
2003
+ "Sun",
2004
+ "Mon",
2005
+ "Tue",
2006
+ "Wed",
2007
+ "Thu",
2008
+ "Fri",
2009
+ "Sat"
2010
+ ];
2011
+ const MONTHS = [
2012
+ "Jan",
2013
+ "Feb",
2014
+ "Mar",
2015
+ "Apr",
2016
+ "May",
2017
+ "Jun",
2018
+ "Jul",
2019
+ "Aug",
2020
+ "Sep",
2021
+ "Oct",
2022
+ "Nov",
2023
+ "Dec"
2024
+ ];
2025
+ const pad2 = (n) => n < 10 ? `0${String(n)}` : String(n);
2026
+ const formatWatchTimestamp = (now) => {
2027
+ return `${WEEKDAYS[now.getDay()]} ${MONTHS[now.getMonth()]} ${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())} ${String(now.getFullYear())}`;
2028
+ };
2029
+ /**
2030
+ * Upper bound on the `WATCH_INTERVAL` variable and the positional interval
2031
+ * — matches upstream which rejects "out of range" values. Upstream uses
2032
+ * `strtod` and rejects ±Infinity; we tighten further so a single watch loop
2033
+ * cannot sleep for longer than ~100 hours, which catches obvious typos
2034
+ * without breaking legitimate slow polls.
2035
+ */
2036
+ const WATCH_INTERVAL_MAX_SECONDS = 36e4;
2037
+ /**
2038
+ * Resolve the effective default `\watch` interval from the `WATCH_INTERVAL`
2039
+ * psql variable. Returns the parsed value, the documented default
2040
+ * (`DEFAULT_WATCH_INTERVAL`), or an `error` envelope if the variable is set
2041
+ * but parses out of range.
2042
+ */
2043
+ const resolveWatchIntervalDefault = (settings) => {
2044
+ const raw = settings.vars.get("WATCH_INTERVAL") ?? "2";
2045
+ const parsed = parseStrictNonNegativeFloat(raw);
2046
+ if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return { error: `WATCH_INTERVAL "${raw}" is out of range` };
2047
+ return { value: parsed };
2048
+ };
2049
+ /**
2050
+ * Spawn the `\watch` pager for the full duration of the polling loop.
2051
+ *
2052
+ * Upstream `do_watch` wraps the loop in a single `popen` of
2053
+ * `PSQL_WATCH_PAGER`. It deliberately ignores `PSQL_PAGER` and `$PAGER`:
2054
+ *
2055
+ * > we ignore the regular PSQL_PAGER or PAGER environment variables,
2056
+ * > because traditional pagers probably won't be very useful for
2057
+ * > showing a stream of results.
2058
+ *
2059
+ * Mirror that here. Reading from `$PAGER` would silently hijack
2060
+ * `\watch` output for any user whose shell sets `PAGER=less` (the
2061
+ * common default), which makes the loop's output disappear into a
2062
+ * subprocess that diff harnesses can't capture.
2063
+ *
2064
+ * We use a single `sh -c <pager>` spawn so the user can set the
2065
+ * variable to a full command string (`less -R`, `tee /tmp/log`, …)
2066
+ * without the caller having to tokenise it. EPIPE on the stdin pipe is
2067
+ * swallowed for the same reason as in `openWriter`: the user may quit
2068
+ * `less` while we still have writes pending in the next iteration.
2069
+ *
2070
+ * Returns `null` when `PSQL_WATCH_PAGER` is unset or whitespace-only
2071
+ * (upstream's "no pager" rule), so the caller falls back to the normal
2072
+ * output target.
2073
+ */
2074
+ const openWatchPager = () => {
2075
+ const cmd = process.env.PSQL_WATCH_PAGER ?? "";
2076
+ if (cmd.trim().length === 0) return null;
2077
+ const child = spawn("sh", ["-c", cmd], { stdio: [
2078
+ "pipe",
2079
+ "inherit",
2080
+ "inherit"
2081
+ ] });
2082
+ child.stdin.on("error", (err) => {
2083
+ if (err.code !== "EPIPE") throw err;
2084
+ });
2085
+ return {
2086
+ stream: child.stdin,
2087
+ close: () => new Promise((resolve) => {
2088
+ if (child.exitCode !== null || child.signalCode !== null) {
2089
+ resolve();
2090
+ return;
2091
+ }
2092
+ let settled = false;
2093
+ const finish = () => {
2094
+ if (settled) return;
2095
+ settled = true;
2096
+ resolve();
2097
+ };
2098
+ child.once("close", finish);
2099
+ child.once("error", finish);
2100
+ if (!child.stdin.destroyed) try {
2101
+ child.stdin.end();
2102
+ } catch (err) {
2103
+ if (err.code !== "EPIPE") finish();
2104
+ }
2105
+ })
2106
+ };
2107
+ };
2108
+ const cmdWatch = {
2109
+ name: "watch",
2110
+ helpKey: "watch",
2111
+ async run(ctx) {
2112
+ const gated = pipelineGate(ctx);
2113
+ if (gated !== null) return gated;
2114
+ const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
2115
+ const sql = trimmedBuf.trim();
2116
+ if (sql.length === 0) return errResult$1(ctx, "no query buffer");
2117
+ if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
2118
+ let intervalSet = false;
2119
+ let interval = null;
2120
+ let iterSet = false;
2121
+ let iterMax = 0;
2122
+ let minRowsSet = false;
2123
+ let minRows = 0;
2124
+ let positionalSeen = false;
2125
+ while (true) {
2126
+ const arg = ctx.nextArg("normal");
2127
+ if (arg === null) break;
2128
+ if (arg.length === 0) continue;
2129
+ const eqIdx = arg.indexOf("=");
2130
+ if (eqIdx > 0) {
2131
+ const key = arg.slice(0, eqIdx);
2132
+ const value = arg.slice(eqIdx + 1);
2133
+ if (key === "i") {
2134
+ if (intervalSet) return errResult$1(ctx, "interval value is specified more than once");
2135
+ const parsed = parseStrictNonNegativeFloat(value);
2136
+ if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult$1(ctx, `incorrect interval value "${value}"`);
2137
+ interval = parsed;
2138
+ intervalSet = true;
2139
+ continue;
2140
+ }
2141
+ if (key === "c") {
2142
+ if (iterSet) return errResult$1(ctx, "iteration count is specified more than once");
2143
+ const parsed = parseStrictNonNegativeInt(value);
2144
+ if (parsed === null || parsed === 0) return errResult$1(ctx, `incorrect iteration count "${value}"`);
2145
+ iterMax = parsed;
2146
+ iterSet = true;
2147
+ continue;
2148
+ }
2149
+ if (key === "m" || key === "min_rows") {
2150
+ if (minRowsSet) return errResult$1(ctx, "minimum row count specified more than once");
2151
+ const parsed = parseStrictNonNegativeInt(value);
2152
+ if (parsed === null) return errResult$1(ctx, `incorrect minimum row count "${value}"`);
2153
+ minRows = parsed;
2154
+ minRowsSet = true;
2155
+ continue;
2156
+ }
2157
+ return errResult$1(ctx, `unrecognized option "${key}"`);
2158
+ }
2159
+ if (positionalSeen || intervalSet) return errResult$1(ctx, "interval value is specified more than once");
2160
+ const parsed = parseStrictNonNegativeFloat(arg);
2161
+ if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult$1(ctx, `incorrect interval value "${arg}"`);
2162
+ interval = parsed;
2163
+ intervalSet = true;
2164
+ positionalSeen = true;
2165
+ }
2166
+ if (interval === null) {
2167
+ const resolved = resolveWatchIntervalDefault(ctx.settings);
2168
+ if ("error" in resolved) return errResult$1(ctx, resolved.error);
2169
+ interval = resolved.value;
2170
+ }
2171
+ const intervalMs = Math.round(interval * 1e3);
2172
+ const controller = WATCH_TEST_CONTROLLER.ref ?? new AbortController();
2173
+ const sigintHandler = () => {
2174
+ controller.abort();
2175
+ };
2176
+ const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
2177
+ if (installedSigint) process.once("SIGINT", sigintHandler);
2178
+ const pager = openWatchPager();
2179
+ const out = pager?.stream ?? pickOut(ctx.settings, null);
2180
+ try {
2181
+ let iter = 0;
2182
+ while (!controller.signal.aborted) {
2183
+ iter++;
2184
+ const stamp = formatWatchTimestamp(/* @__PURE__ */ new Date());
2185
+ out.write(`${stamp} (every ${String(interval)}s)\n\n`);
2186
+ let lastRowCount = 0;
2187
+ try {
2188
+ const results = await ctx.settings.db.execSimple(sql);
2189
+ for (const rs of results) if (rs.fields.length > 0) {
2190
+ await renderResult(ctx.settings, rs, out);
2191
+ lastRowCount = rs.rows.length;
2192
+ }
2193
+ } catch (err) {
2194
+ return formatServerError(ctx, err, trimmedBuf);
2195
+ }
2196
+ if (iterSet && iter >= iterMax) break;
2197
+ if (minRowsSet && lastRowCount < minRows) break;
2198
+ if (controller.signal.aborted) break;
2199
+ if (intervalMs > 0) await sleepCancellable(intervalMs, controller.signal);
2200
+ }
2201
+ if (!pager) out.write("\n");
2202
+ return {
2203
+ status: "reset-buf",
2204
+ newBuf: ""
2205
+ };
2206
+ } finally {
2207
+ if (installedSigint) process.removeListener("SIGINT", sigintHandler);
2208
+ if (pager) await pager.close();
2209
+ }
2210
+ }
2211
+ };
2212
+ const registerIoCommands = (registry) => {
2213
+ registry.register(cmdInclude);
2214
+ registry.register(cmdIncludeRel);
2215
+ registry.register(cmdOut);
2216
+ registry.register(cmdWrite);
2217
+ registry.register(cmdG);
2218
+ registry.register(cmdGx);
2219
+ registry.register(cmdPrint);
2220
+ registry.register(cmdGset);
2221
+ registry.register(cmdGdesc);
2222
+ registry.register(cmdGexec);
2223
+ registry.register(cmdWatch);
2224
+ };
2225
+ //#endregion
2226
+ //#region src/psql/command/cmd_pipeline.ts
2227
+ /**
2228
+ * psql pipeline / extended-query backslash commands (WP-21).
2229
+ *
2230
+ * Implements the subset of upstream psql's "pipeline mode" backslash commands
2231
+ * that drive the extended protocol directly. These are typically used together
2232
+ * with the existing `\g` flow: a `\bind` (or `\parse`) command stashes
2233
+ * parameter / statement state on the {@link BackslashContext}'s settings via
2234
+ * a Symbol-keyed slot; when the mainloop then sees a `;` (or a `\g`), it can
2235
+ * notice the stashed state and route the query through
2236
+ * `Connection.query(sql, params)` instead of `execSimple`.
2237
+ *
2238
+ * Because the mainloop integration is owned by other WPs (and was deliberately
2239
+ * left untouched here), this module exposes both the command specs AND a
2240
+ * small helper, {@link getPipelineState}, that the mainloop will use to
2241
+ * consult the stashed state. The commands operate on the buffered query the
2242
+ * same way `\g` does — they execute or queue the buffer and reset it.
2243
+ *
2244
+ * Commands shipped:
2245
+ *
2246
+ * \bind [VALUE ...] stash params for next ; / \g
2247
+ * \bind_named NAME [V ...] stash params + named prepared statement
2248
+ * \parse NAME prepare current query buffer as NAME
2249
+ * \close_prepared NAME Close('S', NAME)
2250
+ * \startpipeline begin a pipeline session (settings.sendMode)
2251
+ * \endpipeline end the pipeline session, drain results
2252
+ * \syncpipeline send Sync mid-pipeline
2253
+ * \sendpipeline submit the current buffered query w/o waiting
2254
+ * \flushrequest send Flush
2255
+ * \flush alias for \flushrequest
2256
+ * \getresults [N] drain pending pipeline results
2257
+ * \gdesc describe-without-execute the buffered query
2258
+ *
2259
+ * The set is registered in bulk via {@link registerPipelineCommands}.
2260
+ */
2261
+ const BIND_STATE_KEY = Symbol.for("neonctl.psql.bindState");
2262
+ const PIPELINE_KEY = Symbol.for("neonctl.psql.pipeline");
2263
+ const PREPARED_BY_NAME_KEY = Symbol.for("neonctl.psql.preparedByName");
2264
+ const stashOf = (settings) => settings;
2265
+ /** Read (and clear) the pending bind params, if any. */
2266
+ const consumeBindState = (settings) => {
2267
+ const s = stashOf(settings);
2268
+ const cur = s[BIND_STATE_KEY] ?? null;
2269
+ s[BIND_STATE_KEY] = void 0;
2270
+ return cur;
2271
+ };
2272
+ /**
2273
+ * Peek at the bind stash without consuming. Used by `\g` to decide
2274
+ * whether to skip the "empty buffer, no prior query" no-op guard:
2275
+ * when a `\bind_named NAME` is pending, the prepared statement carries
2276
+ * the SQL server-side so no buffer text is needed.
2277
+ */
2278
+ const stagedNamedBindPresent = (settings) => {
2279
+ const cur = stashOf(settings)[BIND_STATE_KEY];
2280
+ return !!cur && cur.byName;
2281
+ };
2282
+ /** Stash a PreparedStatement for later `\bind_named NAME \g` lookup. */
2283
+ const stashPrepared = (settings, name, ps) => {
2284
+ const s = stashOf(settings);
2285
+ let map = s[PREPARED_BY_NAME_KEY];
2286
+ if (!map) {
2287
+ map = /* @__PURE__ */ new Map();
2288
+ s[PREPARED_BY_NAME_KEY] = map;
2289
+ }
2290
+ map.set(name, ps);
2291
+ };
2292
+ /** Look up a previously-stashed PreparedStatement by name. */
2293
+ const lookupPrepared = (settings, name) => {
2294
+ return stashOf(settings)[PREPARED_BY_NAME_KEY]?.get(name) ?? null;
2295
+ };
2296
+ /** Drop a stashed PreparedStatement (after `\close_prepared`). */
2297
+ const dropPrepared = (settings, name) => {
2298
+ const map = stashOf(settings)[PREPARED_BY_NAME_KEY];
2299
+ if (map) map.delete(name);
2300
+ };
2301
+ /** Peek at the current pipeline session (or null). */
2302
+ const getPipelineState = (settings) => stashOf(settings)[PIPELINE_KEY] ?? null;
2303
+ const readCounter = (settings, name) => {
2304
+ const raw = settings.vars.get(name);
2305
+ if (raw === void 0) return 0;
2306
+ const n = parseInt(raw, 10);
2307
+ return Number.isFinite(n) && n >= 0 ? n : 0;
2308
+ };
2309
+ const setCounter = (settings, name, value) => {
2310
+ settings.vars.set(name, String(Math.max(0, value)));
2311
+ };
2312
+ const bumpCounter = (settings, name, delta) => {
2313
+ setCounter(settings, name, readCounter(settings, name) + delta);
2314
+ };
2315
+ const resetPipelineCounters = (settings) => {
2316
+ setCounter(settings, "PIPELINE_COMMAND_COUNT", 0);
2317
+ setCounter(settings, "PIPELINE_SYNC_COUNT", 0);
2318
+ setCounter(settings, "PIPELINE_RESULT_COUNT", 0);
2319
+ };
2320
+ /**
2321
+ * Wrap `Pipeline.execute` so each enqueued Execute message bumps
2322
+ * `PIPELINE_COMMAND_COUNT`. The wrapping covers both call sites:
2323
+ *
2324
+ * - `cmdSendPipeline` (this file) — `\sendpipeline`.
2325
+ * - `dispatchSendQuery` (mainloop) — implicit `;`-queries while pipeline
2326
+ * is active. Mainloop calls `ps.session.execute('', 0)` so the wrapper
2327
+ * fires automatically without mainloop knowing about the var store.
2328
+ *
2329
+ * The wrapper preserves the original function's `this` binding via `apply`.
2330
+ */
2331
+ const wrapSessionForCounters = (session, settings) => {
2332
+ const origExecute = session.execute.bind(session);
2333
+ session.execute = (name, maxRows) => {
2334
+ bumpCounter(settings, "PIPELINE_COMMAND_COUNT", 1);
2335
+ return origExecute(name, maxRows);
2336
+ };
2337
+ return session;
2338
+ };
2339
+ const errResult = (ctx, message) => {
2340
+ ctx.settings.lastErrorResult = { message };
2341
+ writeErr(`\\${ctx.cmdName}: ${message}\n`);
2342
+ return {
2343
+ status: "error",
2344
+ errorWritten: true
2345
+ };
2346
+ };
2347
+ /**
2348
+ * Coerce any thrown / rejected value into a printable string. The
2349
+ * extended-protocol driver in `PgConnection` rejects with raw
2350
+ * ConnectError records (`{severity, code, message, detail, ...}`) —
2351
+ * not `Error` instances — so `String(err)` would produce
2352
+ * `[object Object]` in the conformance output. We probe for a
2353
+ * `.message` property (covering both `Error` and the ConnectError
2354
+ * shape) and fall back to JSON.stringify only when there's no
2355
+ * message field at all.
2356
+ */
2357
+ const errorToMessage = (err) => {
2358
+ if (err instanceof Error) return err.message;
2359
+ if (err !== null && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message;
2360
+ try {
2361
+ return JSON.stringify(err);
2362
+ } catch {
2363
+ return String(err);
2364
+ }
2365
+ };
2366
+ /**
2367
+ * Render a pipeline-collected ConnectError to stderr in the upstream
2368
+ * shape: a `SEVERITY: message` line, followed by optional `DETAIL:` /
2369
+ * `HINT:` / `CONTEXT:` lines (each on its own line). Matches the
2370
+ * subset of `formatErrorReport`-style output the conformance corpus
2371
+ * expects from `\endpipeline` for non-FATAL pipeline errors. We
2372
+ * inline a minimal renderer here rather than importing the full
2373
+ * `formatErrorReport` from `cmd_meta` because we always want the
2374
+ * "default verbosity, no LINE/caret context" form — pipeline errors
2375
+ * arrive after the buffered query has been reset and we don't carry
2376
+ * the originating SQL position through `session.lastError`.
2377
+ */
2378
+ const renderPipelineError = (err) => {
2379
+ if (err === null || typeof err !== "object") {
2380
+ writeErr(`ERROR: ${errorToMessage(err)}\n`);
2381
+ return;
2382
+ }
2383
+ const e = err;
2384
+ if (e.pipelineAborted) {
2385
+ writeErr(`${e.message ?? "Pipeline aborted, command did not run"}\n`);
2386
+ return;
2387
+ }
2388
+ const severity = e.severity ?? "ERROR";
2389
+ const message = e.message ?? "";
2390
+ writeErr(`${severity}: ${message}\n`);
2391
+ if (e.detail) writeErr(`DETAIL: ${e.detail}\n`);
2392
+ if (e.hint) writeErr(`HINT: ${e.hint}\n`);
2393
+ if (e.where) writeErr(`CONTEXT: ${e.where}\n`);
2394
+ };
2395
+ const readAllArgs = (ctx) => {
2396
+ const out = [];
2397
+ for (;;) {
2398
+ const arg = ctx.nextArg("normal");
2399
+ if (arg === null) break;
2400
+ out.push(arg);
2401
+ }
2402
+ return out;
2403
+ };
2404
+ const cmdBind = {
2405
+ name: "bind",
2406
+ helpKey: "bind",
2407
+ run(ctx) {
2408
+ const values = readAllArgs(ctx);
2409
+ stashOf(ctx.settings)[BIND_STATE_KEY] = {
2410
+ name: "",
2411
+ values,
2412
+ byName: false
2413
+ };
2414
+ return Promise.resolve({ status: "ok" });
2415
+ }
2416
+ };
2417
+ const cmdBindNamed = {
2418
+ name: "bind_named",
2419
+ helpKey: "bind_named",
2420
+ run(ctx) {
2421
+ const name = ctx.nextArg("normal");
2422
+ if (name === null) {
2423
+ stashOf(ctx.settings)[BIND_STATE_KEY] = void 0;
2424
+ return Promise.resolve(errResult(ctx, "missing required argument"));
2425
+ }
2426
+ const values = readAllArgs(ctx);
2427
+ stashOf(ctx.settings)[BIND_STATE_KEY] = {
2428
+ name,
2429
+ values,
2430
+ byName: true
2431
+ };
2432
+ return Promise.resolve({ status: "ok" });
2433
+ }
2434
+ };
2435
+ const cmdParse = {
2436
+ name: "parse",
2437
+ helpKey: "parse",
2438
+ async run(ctx) {
2439
+ const name = ctx.nextArg("normal");
2440
+ if (name === null) return errResult(ctx, "missing required argument");
2441
+ const sql = ctx.queryBuf;
2442
+ if (sql.trim().length === 0) return errResult(ctx, "no query buffer");
2443
+ if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
2444
+ const pipelineActive = getPipelineState(ctx.settings);
2445
+ if (pipelineActive !== null) try {
2446
+ const session = pipelineActive.session;
2447
+ if (typeof session.parseSlot === "function") await session.parseSlot(name, sql, []);
2448
+ else await session.parse(name, sql, []);
2449
+ ctx.settings.lastQuery = sql;
2450
+ bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
2451
+ return {
2452
+ status: "reset-buf",
2453
+ newBuf: ""
2454
+ };
2455
+ } catch (err) {
2456
+ return errResult(ctx, errorToMessage(err));
2457
+ }
2458
+ try {
2459
+ const ps = await ctx.settings.db.prepare(name, sql);
2460
+ stashPrepared(ctx.settings, name, ps);
2461
+ ctx.settings.lastQuery = sql;
2462
+ return {
2463
+ status: "reset-buf",
2464
+ newBuf: ""
2465
+ };
2466
+ } catch (err) {
2467
+ return errResult(ctx, errorToMessage(err));
2468
+ }
2469
+ }
2470
+ };
2471
+ const cmdClosePrepared = {
2472
+ name: "close_prepared",
2473
+ helpKey: "close_prepared",
2474
+ async run(ctx) {
2475
+ const name = ctx.nextArg("normal");
2476
+ if (name === null) return errResult(ctx, "missing required argument");
2477
+ const db = ctx.settings.db;
2478
+ if (!db) return errResult(ctx, "no connection to the server");
2479
+ const ps = getPipelineState(ctx.settings);
2480
+ if (ps !== null) try {
2481
+ const session = ps.session;
2482
+ if (typeof session.closeSlot === "function") await session.closeSlot(name);
2483
+ else await session.close(name);
2484
+ bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
2485
+ dropPrepared(ctx.settings, name);
2486
+ return { status: "ok" };
2487
+ } catch (err) {
2488
+ return errResult(ctx, errorToMessage(err));
2489
+ }
2490
+ try {
2491
+ if (db.closePreparedStatement) await db.closePreparedStatement(name);
2492
+ else await (await db.prepare(name, "SELECT 1")).close();
2493
+ dropPrepared(ctx.settings, name);
2494
+ return { status: "ok" };
2495
+ } catch (err) {
2496
+ return errResult(ctx, errorToMessage(err));
2497
+ }
2498
+ }
2499
+ };
2500
+ const cmdStartPipeline = {
2501
+ name: "startpipeline",
2502
+ helpKey: "startpipeline",
2503
+ run(ctx) {
2504
+ if (!ctx.settings.db) return Promise.resolve(errResult(ctx, "no connection to the server"));
2505
+ if (getPipelineState(ctx.settings) !== null) return Promise.resolve({ status: "ok" });
2506
+ try {
2507
+ const session = wrapSessionForCounters(ctx.settings.db.pipeline(), ctx.settings);
2508
+ stashOf(ctx.settings)[PIPELINE_KEY] = {
2509
+ session,
2510
+ pending: [],
2511
+ drainedCount: 0
2512
+ };
2513
+ ctx.settings.sendMode = "extended-pipeline";
2514
+ resetPipelineCounters(ctx.settings);
2515
+ return Promise.resolve({ status: "ok" });
2516
+ } catch (err) {
2517
+ return Promise.resolve(errResult(ctx, errorToMessage(err)));
2518
+ }
2519
+ }
2520
+ };
2521
+ const cmdEndPipeline = {
2522
+ name: "endpipeline",
2523
+ helpKey: "endpipeline",
2524
+ async run(ctx) {
2525
+ const ps = getPipelineState(ctx.settings);
2526
+ if (!ps) {
2527
+ ctx.settings.lastErrorResult = { message: "cannot send pipeline when not in pipeline mode" };
2528
+ writeErr("cannot send pipeline when not in pipeline mode\n");
2529
+ return {
2530
+ status: "error",
2531
+ errorWritten: true
2532
+ };
2533
+ }
2534
+ try {
2535
+ const alreadyDrained = ps.drainedCount;
2536
+ const session = ps.session;
2537
+ const cmdSlotsSnapshot = Array.isArray(session.cmdSlots) ? session.cmdSlots.slice() : [];
2538
+ const fetchCountActive = (ctx.settings.vars.get("FETCH_COUNT") ?? "0") !== "0";
2539
+ const sets = await ps.session.end();
2540
+ stashOf(ctx.settings)[PIPELINE_KEY] = void 0;
2541
+ ctx.settings.sendMode = "extended-query";
2542
+ resetPipelineCounters(ctx.settings);
2543
+ clearPipelineGateErrors(ctx.settings);
2544
+ const settled = await Promise.allSettled(cmdSlotsSnapshot);
2545
+ let errorRendered = false;
2546
+ const entries = settled.length > 0 ? settled : sets.map((rs) => ({
2547
+ status: "fulfilled",
2548
+ value: rs
2549
+ }));
2550
+ const sliceForError = entries.slice(alreadyDrained);
2551
+ const realLastError = (() => {
2552
+ for (const r of sliceForError) {
2553
+ if (r.status !== "rejected") continue;
2554
+ const reason = r.reason;
2555
+ if (!(typeof reason === "object" && reason !== null && reason.pipelineAborted === true)) return reason;
2556
+ }
2557
+ return null;
2558
+ })() ?? (() => {
2559
+ const le = session.lastError;
2560
+ if (le === null || le === void 0) return null;
2561
+ if (typeof le === "object" && le.pipelineAborted) return null;
2562
+ return le;
2563
+ })();
2564
+ for (let i = alreadyDrained; i < entries.length; i++) {
2565
+ const r = entries[i];
2566
+ if (r.status === "fulfilled") {
2567
+ const rs = r.value;
2568
+ for (const n of rs.notices) {
2569
+ let out = `${n.severity}: ${n.message}\n`;
2570
+ if (n.detail !== void 0) out += `DETAIL: ${n.detail}\n`;
2571
+ if (n.hint !== void 0) out += `HINT: ${n.hint}\n`;
2572
+ writeErr(out);
2573
+ }
2574
+ const isSyncOrPlaceholder = rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0;
2575
+ const isCommandOnly = rs.fields.length === 0 && rs.rows.length === 0 && rs.command !== "";
2576
+ if (!isSyncOrPlaceholder && !isCommandOnly) if (rs.fields.length === 0 && rs.rows.length > 0) {
2577
+ if (!ctx.settings.popt.topt.tuplesOnly) {
2578
+ process.stdout.write("--\n");
2579
+ process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
2580
+ }
2581
+ } else await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
2582
+ } else if (!errorRendered) {
2583
+ const reason = r.reason;
2584
+ const isAborted = typeof reason === "object" && reason !== null && reason.pipelineAborted === true;
2585
+ if (fetchCountActive) writeErr("fetching results in chunked mode failed\n");
2586
+ if (isAborted && realLastError !== null) renderPipelineError(realLastError);
2587
+ else if (isAborted) writeErr("Pipeline aborted, command did not run\n");
2588
+ else renderPipelineError(reason);
2589
+ errorRendered = true;
2590
+ }
2591
+ }
2592
+ if (!errorRendered) {
2593
+ const lastErr = session.lastError;
2594
+ if (lastErr !== null && lastErr !== void 0) {
2595
+ if (fetchCountActive) writeErr("fetching results in chunked mode failed\n");
2596
+ renderPipelineError(lastErr);
2597
+ }
2598
+ }
2599
+ return { status: "ok" };
2600
+ } catch (err) {
2601
+ stashOf(ctx.settings)[PIPELINE_KEY] = void 0;
2602
+ ctx.settings.sendMode = "extended-query";
2603
+ resetPipelineCounters(ctx.settings);
2604
+ clearPipelineGateErrors(ctx.settings);
2605
+ return errResult(ctx, errorToMessage(err));
2606
+ }
2607
+ }
2608
+ };
2609
+ const cmdSyncPipeline = {
2610
+ name: "syncpipeline",
2611
+ helpKey: "syncpipeline",
2612
+ async run(ctx) {
2613
+ const ps = getPipelineState(ctx.settings);
2614
+ if (!ps) return errResult(ctx, "no pipeline active");
2615
+ try {
2616
+ await ps.session.sync();
2617
+ const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
2618
+ setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
2619
+ bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
2620
+ bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", 1);
2621
+ return { status: "ok" };
2622
+ } catch (err) {
2623
+ return errResult(ctx, errorToMessage(err));
2624
+ }
2625
+ }
2626
+ };
2627
+ const cmdFlushRequest = {
2628
+ name: "flushrequest",
2629
+ helpKey: "flushrequest",
2630
+ async run(ctx) {
2631
+ const ps = getPipelineState(ctx.settings);
2632
+ if (!ps) return errResult(ctx, "no pipeline active");
2633
+ try {
2634
+ await ps.session.flush();
2635
+ const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
2636
+ setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
2637
+ bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
2638
+ return { status: "ok" };
2639
+ } catch (err) {
2640
+ return errResult(ctx, errorToMessage(err));
2641
+ }
2642
+ }
2643
+ };
2644
+ const cmdFlush = {
2645
+ name: "flush",
2646
+ helpKey: "flush",
2647
+ run(ctx) {
2648
+ return cmdFlushRequest.run(ctx);
2649
+ }
2650
+ };
2651
+ const cmdSendPipeline = {
2652
+ name: "sendpipeline",
2653
+ helpKey: "sendpipeline",
2654
+ async run(ctx) {
2655
+ const ps = getPipelineState(ctx.settings);
2656
+ if (!ps) {
2657
+ consumeBindState(ctx.settings);
2658
+ ctx.settings.lastErrorResult = { message: "\\sendpipeline not allowed outside of pipeline mode" };
2659
+ writeErr("\\sendpipeline not allowed outside of pipeline mode\n");
2660
+ return {
2661
+ status: "error",
2662
+ errorWritten: true
2663
+ };
2664
+ }
2665
+ const bind = consumeBindState(ctx.settings);
2666
+ if (bind === null) {
2667
+ ctx.settings.lastErrorResult = { message: "\\sendpipeline must be used after \\bind or \\bind_named" };
2668
+ writeErr("\\sendpipeline must be used after \\bind or \\bind_named\n");
2669
+ return {
2670
+ status: "error",
2671
+ errorWritten: true
2672
+ };
2673
+ }
2674
+ const sql = ctx.queryBuf.trim();
2675
+ const stmtName = bind.name;
2676
+ const params = bind.values;
2677
+ if (!bind.byName && sql.length === 0) return errResult(ctx, "no query buffer");
2678
+ try {
2679
+ if (!bind.byName) await ps.session.parse("", sql, []);
2680
+ await ps.session.bind(stmtName, params);
2681
+ const exec = (async () => {
2682
+ await ps.session.execute("", 0);
2683
+ return {
2684
+ command: "",
2685
+ rowCount: null,
2686
+ oid: null,
2687
+ fields: [],
2688
+ rows: [],
2689
+ notices: []
2690
+ };
2691
+ })();
2692
+ ps.pending.push(exec);
2693
+ return {
2694
+ status: "reset-buf",
2695
+ newBuf: ""
2696
+ };
2697
+ } catch (err) {
2698
+ return errResult(ctx, errorToMessage(err));
2699
+ }
2700
+ }
2701
+ };
2702
+ const cmdGetResults = {
2703
+ name: "getresults",
2704
+ helpKey: "getresults",
2705
+ async run(ctx) {
2706
+ const ps = getPipelineState(ctx.settings);
2707
+ const arg = ctx.nextArg("normal");
2708
+ let requested = null;
2709
+ if (arg !== null && arg.length > 0) {
2710
+ const parsed = parseInt(arg, 10);
2711
+ if (!Number.isFinite(parsed) || parsed < 0) return errResult(ctx, "invalid number of requested results");
2712
+ requested = parsed;
2713
+ }
2714
+ if (!ps) {
2715
+ process.stdout.write("No pending results to get\n");
2716
+ return { status: "ok" };
2717
+ }
2718
+ const syncAvailable = readCounter(ctx.settings, "PIPELINE_SYNC_COUNT");
2719
+ const resultAvailable = readCounter(ctx.settings, "PIPELINE_RESULT_COUNT");
2720
+ const available = syncAvailable + resultAvailable;
2721
+ if (available === 0) {
2722
+ process.stdout.write("No pending results to get\n");
2723
+ return { status: "ok" };
2724
+ }
2725
+ const n = requested === null || requested === 0 ? available : Math.min(requested, available);
2726
+ const session = ps.session;
2727
+ const slots = Array.isArray(session.cmdSlots) ? session.cmdSlots : [];
2728
+ const start = ps.drainedCount;
2729
+ const end = start + n;
2730
+ const slice = slots.slice(start, end);
2731
+ ps.drainedCount = end;
2732
+ ps.pending.splice(0, n);
2733
+ try {
2734
+ const settled = await Promise.allSettled(slice);
2735
+ const sliceErr = (() => {
2736
+ for (const r of settled) {
2737
+ if (r.status !== "rejected") continue;
2738
+ const reason = r.reason;
2739
+ if (!(typeof reason === "object" && reason !== null && reason.pipelineAborted === true)) return reason;
2740
+ }
2741
+ return null;
2742
+ })();
2743
+ const sessPeek = session;
2744
+ const realErr = sliceErr ?? (typeof sessPeek.peekRealError === "function" ? await sessPeek.peekRealError() : null);
2745
+ let errorRenderedHere = false;
2746
+ let walkedItems = 0;
2747
+ let syncsDrained = 0;
2748
+ let resultsDrained = 0;
2749
+ for (const r of settled) {
2750
+ walkedItems++;
2751
+ if (r.status !== "fulfilled") {
2752
+ resultsDrained++;
2753
+ if (!errorRenderedHere) {
2754
+ const reason = r.reason;
2755
+ if (typeof reason === "object" && reason !== null && reason.pipelineAborted === true && realErr !== null) renderPipelineError(realErr);
2756
+ else renderPipelineError(reason);
2757
+ errorRenderedHere = true;
2758
+ }
2759
+ continue;
2760
+ }
2761
+ const rs = r.value;
2762
+ for (const n of rs.notices) {
2763
+ let out = `${n.severity}: ${n.message}\n`;
2764
+ if (n.detail !== void 0) out += `DETAIL: ${n.detail}\n`;
2765
+ if (n.hint !== void 0) out += `HINT: ${n.hint}\n`;
2766
+ writeErr(out);
2767
+ }
2768
+ if (rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0) {
2769
+ syncsDrained++;
2770
+ continue;
2771
+ }
2772
+ resultsDrained++;
2773
+ if (rs.fields.length === 0 && rs.rows.length > 0) {
2774
+ if (!ctx.settings.popt.topt.tuplesOnly) {
2775
+ process.stdout.write("--\n");
2776
+ process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
2777
+ }
2778
+ } else if (rs.fields.length > 0) await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
2779
+ }
2780
+ const sessMark = session;
2781
+ if (typeof sessMark.markDrained === "function") sessMark.markDrained(end);
2782
+ if (errorRenderedHere) {
2783
+ const sessAny = session;
2784
+ if (typeof sessAny.clearLastError === "function") sessAny.clearLastError();
2785
+ }
2786
+ if (walkedItems === 0) {
2787
+ const ddata = Math.min(n, resultAvailable);
2788
+ resultsDrained = ddata;
2789
+ syncsDrained = n - ddata;
2790
+ } else {
2791
+ const actualSyncs = Math.min(syncsDrained, syncAvailable);
2792
+ const extraResults = syncsDrained - actualSyncs;
2793
+ syncsDrained = actualSyncs;
2794
+ resultsDrained += extraResults;
2795
+ }
2796
+ bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", -syncsDrained);
2797
+ bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", -resultsDrained);
2798
+ return { status: "ok" };
2799
+ } catch (err) {
2800
+ return errResult(ctx, errorToMessage(err));
2801
+ }
2802
+ }
2803
+ };
2804
+ const registerPipelineCommands = (registry) => {
2805
+ registry.register(cmdBind);
2806
+ registry.register(cmdBindNamed);
2807
+ registry.register(cmdParse);
2808
+ registry.register(cmdClosePrepared);
2809
+ registry.register(cmdStartPipeline);
2810
+ registry.register(cmdEndPipeline);
2811
+ registry.register(cmdSyncPipeline);
2812
+ registry.register(cmdFlushRequest);
2813
+ registry.register(cmdFlush);
2814
+ registry.register(cmdSendPipeline);
2815
+ registry.register(cmdGetResults);
2816
+ };
2817
+ //#endregion
2818
+ export { cmdPrint as A, psqlExec as B, cmdGdesc as C, cmdInclude as D, cmdGx as E, registerIoCommands as F, writeQueryError as G, renderResultSet as H, __testing as I, captureLastError as L, cmdWrite as M, formatWatchTimestamp as N, cmdIncludeRel as O, getQueryFout as P, executeAndPrint as R, cmdG as S, cmdGset as T, sendQuery as U, refreshErrorVars as V, stripLeadingCommentsAndWS as W, stagedNamedBindPresent as _, cmdFlush as a, WATCH_TEST_CONTROLLER as b, cmdParse as c, cmdSyncPipeline as d, consumeBindState as f, registerPipelineCommands as g, lookupPrepared as h, cmdEndPipeline as i, cmdWatch as j, cmdOut as k, cmdSendPipeline as l, getPipelineState as m, cmdBindNamed as n, cmdFlushRequest as o, dropPrepared as p, cmdClosePrepared as r, cmdGetResults as s, cmdBind as t, cmdStartPipeline as u, stashPrepared as v, cmdGexec as w, clearPipelineGateErrors as x, DEFAULT_WATCH_INTERVAL as y, pickOut$1 as z };