neon 2.1.2 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +601 -0
  3. package/dist/analytics.js +156 -0
  4. package/dist/api.js +665 -0
  5. package/dist/auth.js +127 -0
  6. package/dist/callback.html +51 -0
  7. package/dist/cli.js +9 -0
  8. package/dist/commands/auth.js +214 -0
  9. package/dist/commands/bootstrap.js +481 -0
  10. package/dist/commands/branches.js +481 -0
  11. package/dist/commands/bucket.js +543 -0
  12. package/dist/commands/checkout.js +289 -0
  13. package/dist/commands/config.js +544 -0
  14. package/dist/commands/connection_string.js +172 -0
  15. package/dist/commands/data_api.js +285 -0
  16. package/dist/commands/databases.js +82 -0
  17. package/dist/commands/deploy.js +26 -0
  18. package/dist/commands/dev.js +698 -0
  19. package/dist/commands/env.js +166 -0
  20. package/dist/commands/functions.js +373 -0
  21. package/dist/commands/index.js +54 -0
  22. package/dist/commands/init.js +73 -0
  23. package/dist/commands/ip_allow.js +137 -0
  24. package/dist/commands/link.js +1121 -0
  25. package/dist/commands/neon_auth.js +1028 -0
  26. package/dist/commands/operations.js +28 -0
  27. package/dist/commands/orgs.js +24 -0
  28. package/dist/commands/projects.js +372 -0
  29. package/dist/commands/psql.js +62 -0
  30. package/dist/commands/roles.js +65 -0
  31. package/dist/commands/schema_diff.js +151 -0
  32. package/dist/commands/set_context.js +29 -0
  33. package/dist/commands/status.js +40 -0
  34. package/dist/commands/user.js +15 -0
  35. package/dist/commands/vpc_endpoints.js +134 -0
  36. package/dist/config.js +11 -0
  37. package/dist/config_format.js +72 -0
  38. package/dist/context.js +177 -0
  39. package/dist/current_branch_fast_path.js +55 -0
  40. package/dist/dev/env.js +240 -0
  41. package/dist/dev/functions.js +70 -0
  42. package/dist/dev/inputs.js +63 -0
  43. package/dist/dev/runtime.js +146 -0
  44. package/dist/env.js +36 -0
  45. package/dist/env_file.js +159 -0
  46. package/dist/errors.js +80 -0
  47. package/dist/functions_api.js +44 -0
  48. package/dist/help.js +146 -0
  49. package/dist/index.js +234 -0
  50. package/dist/log.js +18 -0
  51. package/dist/parameters.gen.js +480 -0
  52. package/dist/pkg.js +25 -0
  53. package/dist/psql/cli.js +53 -0
  54. package/dist/psql/command/cmd_cond.js +437 -0
  55. package/dist/psql/command/cmd_connect.js +820 -0
  56. package/dist/psql/command/cmd_copy.js +1035 -0
  57. package/dist/psql/command/cmd_describe.js +1815 -0
  58. package/dist/psql/command/cmd_format.js +922 -0
  59. package/dist/psql/command/cmd_io.js +2193 -0
  60. package/dist/psql/command/cmd_lo.js +393 -0
  61. package/dist/psql/command/cmd_meta.js +970 -0
  62. package/dist/psql/command/cmd_misc.js +187 -0
  63. package/dist/psql/command/cmd_pipeline.js +1148 -0
  64. package/dist/psql/command/cmd_restrict.js +171 -0
  65. package/dist/psql/command/cmd_show.js +766 -0
  66. package/dist/psql/command/dispatch.js +343 -0
  67. package/dist/psql/command/inputQueue.js +42 -0
  68. package/dist/psql/command/shared.js +71 -0
  69. package/dist/psql/complete/filenames.js +139 -0
  70. package/dist/psql/complete/index.js +104 -0
  71. package/dist/psql/complete/matcher.js +315 -0
  72. package/dist/psql/complete/psqlVars.js +247 -0
  73. package/dist/psql/complete/queries.js +493 -0
  74. package/dist/psql/complete/rules.js +2424 -0
  75. package/dist/psql/core/common.js +1253 -0
  76. package/dist/psql/core/help.js +576 -0
  77. package/dist/psql/core/mainloop.js +1360 -0
  78. package/dist/psql/core/prompt.js +440 -0
  79. package/dist/psql/core/settings.js +684 -0
  80. package/dist/psql/core/sqlHelp.js +1066 -0
  81. package/dist/psql/core/startup.js +846 -0
  82. package/dist/psql/core/syncVars.js +116 -0
  83. package/dist/psql/core/variables.js +287 -0
  84. package/dist/psql/describe/formatters.js +1290 -0
  85. package/dist/psql/describe/processNamePattern.js +270 -0
  86. package/dist/psql/describe/queries.js +2378 -0
  87. package/dist/psql/describe/versionGate.js +43 -0
  88. package/dist/psql/index.js +2030 -0
  89. package/dist/psql/io/history.js +299 -0
  90. package/dist/psql/io/input.js +120 -0
  91. package/dist/psql/io/lineEditor/buffer.js +325 -0
  92. package/dist/psql/io/lineEditor/complete.js +227 -0
  93. package/dist/psql/io/lineEditor/filename.js +159 -0
  94. package/dist/psql/io/lineEditor/index.js +894 -0
  95. package/dist/psql/io/lineEditor/keymap.js +745 -0
  96. package/dist/psql/io/lineEditor/vt100.js +363 -0
  97. package/dist/psql/io/pgpass.js +202 -0
  98. package/dist/psql/io/pgservice.js +194 -0
  99. package/dist/psql/io/psqlrc.js +422 -0
  100. package/dist/psql/print/aligned.js +1765 -0
  101. package/dist/psql/print/asciidoc.js +248 -0
  102. package/dist/psql/print/crosstab.js +463 -0
  103. package/dist/psql/print/csv.js +95 -0
  104. package/dist/psql/print/html.js +258 -0
  105. package/dist/psql/print/json.js +96 -0
  106. package/dist/psql/print/latex.js +396 -0
  107. package/dist/psql/print/pager.js +267 -0
  108. package/dist/psql/print/troff.js +258 -0
  109. package/dist/psql/print/unaligned.js +119 -0
  110. package/dist/psql/print/units.js +135 -0
  111. package/dist/psql/scanner/slash.js +515 -0
  112. package/dist/psql/scanner/sql.js +914 -0
  113. package/dist/psql/scanner/stringutils.js +394 -0
  114. package/dist/psql/types/backslash.js +1 -0
  115. package/dist/psql/types/connection.js +1 -0
  116. package/dist/psql/types/index.js +7 -0
  117. package/dist/psql/types/printer.js +1 -0
  118. package/dist/psql/types/repl.js +1 -0
  119. package/dist/psql/types/scanner.js +24 -0
  120. package/dist/psql/types/settings.js +1 -0
  121. package/dist/psql/types/variables.js +1 -0
  122. package/dist/psql/wire/connection.js +2858 -0
  123. package/dist/psql/wire/copy.js +108 -0
  124. package/dist/psql/wire/notify.js +59 -0
  125. package/dist/psql/wire/pipeline.js +521 -0
  126. package/dist/psql/wire/protocol.js +466 -0
  127. package/dist/psql/wire/sasl.js +296 -0
  128. package/dist/psql/wire/tls.js +602 -0
  129. package/dist/storage_api.js +147 -0
  130. package/dist/test_utils/fixtures.js +122 -0
  131. package/dist/test_utils/oauth_server.js +9 -0
  132. package/dist/types.js +1 -0
  133. package/dist/utils/api_enums.js +33 -0
  134. package/dist/utils/auth.js +5 -0
  135. package/dist/utils/branch_notice.js +22 -0
  136. package/dist/utils/branch_picker.js +103 -0
  137. package/dist/utils/compute_units.js +28 -0
  138. package/dist/utils/enrichers.js +161 -0
  139. package/dist/utils/esbuild.js +158 -0
  140. package/dist/utils/formats.js +18 -0
  141. package/dist/utils/middlewares.js +20 -0
  142. package/dist/utils/package_manager.js +68 -0
  143. package/dist/utils/point_in_time.js +56 -0
  144. package/dist/utils/psql.js +120 -0
  145. package/dist/utils/string.js +5 -0
  146. package/dist/utils/ui.js +59 -0
  147. package/dist/utils/zip.js +4 -0
  148. package/dist/writer.js +97 -0
  149. package/package.json +117 -14
  150. package/.jshintrc +0 -3
  151. package/CHANGELOG.markdown +0 -22
  152. package/LICENSE +0 -21
  153. package/README.markdown +0 -76
  154. package/bower.json +0 -23
  155. package/license.txt +0 -9
  156. package/neon.js +0 -234
  157. package/stdlib/bubbling_support.js +0 -32
  158. package/stdlib/custom_event.js +0 -54
  159. package/stdlib/custom_event_support.js +0 -195
  160. package/stdlib/index.js +0 -7
  161. package/stdlib/node_support.js +0 -123
  162. package/stdlib/widget.js +0 -340
  163. package/test/neon_browser.html +0 -11
  164. package/test/neon_stdlib_browser.html +0 -15
  165. package/test/neon_stdlib_test.js +0 -84
  166. package/test/neon_test.js +0 -64
@@ -0,0 +1,1253 @@
1
+ /**
2
+ * psql common — the unified send-query / process-result pipeline.
3
+ *
4
+ * TypeScript port of selected functions in `src/bin/psql/common.c`:
5
+ *
6
+ * - {@link sendQuery} ← `SendQuery`
7
+ * - {@link executeAndPrint} ← `ExecQueryAndProcessResults` (the inner
8
+ * result-processing slice, without the
9
+ * AUTOCOMMIT / savepoint scaffolding)
10
+ * - {@link psqlExec} ← `PSQLexec`
11
+ *
12
+ * The minimal version of this logic was inlined in `mainloop.ts` after WP-12;
13
+ * this WP extracts and polishes it. The pieces wired in here:
14
+ *
15
+ * - AUTOCOMMIT: when the variable is 'off' (default 'on') and the current
16
+ * transaction is idle, prepend `BEGIN` once before the next non-exempt
17
+ * statement. The set of exempt commands mirrors upstream
18
+ * `command_no_begin()` — transaction-control verbs and a handful of
19
+ * non-transactional DDL.
20
+ *
21
+ * - ON_ERROR_ROLLBACK: 'off' (default) | 'on' | 'interactive'. When active
22
+ * and we're inside a transaction, issue
23
+ * `SAVEPOINT pg_psql_temporary_savepoint` before the statement; on error
24
+ * `ROLLBACK TO`, on success `RELEASE` (unless the statement is itself a
25
+ * savepoint-management verb that already drops/replaces it).
26
+ *
27
+ * - FETCH_COUNT: integer; when >0 and the statement is a single SELECT-ish
28
+ * query, wrap it in `DECLARE _psql_cursor CURSOR FOR ...; FETCH FORWARD
29
+ * N FROM _psql_cursor` until exhausted. Non-SELECT statements fall back
30
+ * to the simple path.
31
+ *
32
+ * - SINGLESTEP: when `settings.singlestep` is true, print the SQL to stderr
33
+ * with the upstream confirmation banner and read one line from stdin.
34
+ * Input that starts with 'x' cancels the statement.
35
+ *
36
+ * - Timing: when `settings.timing` is true, measure wallclock around the
37
+ * send/print path and write a `Time: X.XXX ms` line to stdout (mirrors
38
+ * upstream's `printf` in common.c, and matches the existing mainloop
39
+ * test expectations).
40
+ *
41
+ * What's deliberately not done here:
42
+ *
43
+ * - SINGLELINE (-S): treating LF as a semicolon is a scanner concern, so it
44
+ * lives there — `scanSql` honours `ScanOptions.singleline` and the mainloop
45
+ * passes `settings.singleline` through on each pass. `sendQuery` itself
46
+ * needs no SINGLELINE branch; by the time a statement reaches here the
47
+ * scanner has already drawn the boundary.
48
+ *
49
+ * - Pipeline mode (-X / `\startpipeline`): upstream gates SendQuery on
50
+ * `pset.send_mode == PSQL_SEND_PIPELINE`; that path is owned by WP-21.
51
+ *
52
+ * - COPY FROM STDIN / TO STDOUT: upstream's ProcessResult dispatches on
53
+ * `PGRES_COPY_IN/OUT`. That belongs to WP-16; here we surface a clear
54
+ * error message if we ever see a copy result come back through
55
+ * `execSimple`.
56
+ */
57
+ import { getQueryFout } from "../command/cmd_io.js";
58
+ import { formatErrorReport, psqlErrorPrefix } from "../command/cmd_meta.js";
59
+ import { alignedPrinter } from "../print/aligned.js";
60
+ import { asciidocPrinter } from "../print/asciidoc.js";
61
+ import { csvPrinter } from "../print/csv.js";
62
+ import { htmlPrinter } from "../print/html.js";
63
+ import { jsonPrinter } from "../print/json.js";
64
+ import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
65
+ import { openPager, shouldPage } from "../print/pager.js";
66
+ import { troffMsPrinter } from "../print/troff.js";
67
+ import { unalignedPrinter } from "../print/unaligned.js";
68
+ import { formatDurationMs } from "../print/units.js";
69
+ const readTxStatus = (conn) => {
70
+ const status = conn.txStatus;
71
+ return status ?? "I";
72
+ };
73
+ // ---------------------------------------------------------------------------
74
+ // Statement classification.
75
+ //
76
+ // Upstream uses two predicates: `command_no_begin()` decides whether a
77
+ // statement is exempt from AUTOCOMMIT's implicit BEGIN, and
78
+ // `is_select_command()` decides whether FETCH_COUNT chunking applies. We
79
+ // mirror both with a lightweight prefix matcher — the SQL was already
80
+ // normalised by the scanner before reaching us.
81
+ // ---------------------------------------------------------------------------
82
+ const SAVEPOINT_NAME = "pg_psql_temporary_savepoint";
83
+ const CURSOR_NAME = "_psql_cursor";
84
+ /**
85
+ * Strip leading whitespace and `--` line / slash-star block comments from
86
+ * `sql`. Mirrors what upstream psql's scanner advances past before handing a
87
+ * statement to `PQexec` — the server-reported error `position` is a 1-based
88
+ * offset into THAT trimmed buffer, so the `LINE N:` re-print computed from
89
+ * `count('\n')` in `sql.slice(0, position - 1)` aligns with vanilla output
90
+ * only when the same leading prelude is stripped here too.
91
+ *
92
+ * Block comments support nested depths (PG extension). Embedded comments
93
+ * mid-statement are intentionally NOT stripped — they participate in the
94
+ * line count of the executing statement, same as upstream.
95
+ *
96
+ * Exported for cmd_io / cmd_pipeline so backslash commands that capture or
97
+ * inspect `queryBuf` see the same shape that the wire and `lastQuery`
98
+ * receive.
99
+ */
100
+ export const stripLeadingCommentsAndWS = (sql) => {
101
+ let i = 0;
102
+ const n = sql.length;
103
+ while (i < n) {
104
+ const c = sql.charCodeAt(i);
105
+ // Whitespace per psql_scan: space, tab, CR, LF, form-feed, vertical-tab.
106
+ if (c === 0x20 ||
107
+ c === 0x09 ||
108
+ c === 0x0a ||
109
+ c === 0x0d ||
110
+ c === 0x0c ||
111
+ c === 0x0b) {
112
+ i++;
113
+ continue;
114
+ }
115
+ // `--` line comment: consume up to (but not including) the next \n.
116
+ if (c === 0x2d && sql.charCodeAt(i + 1) === 0x2d) {
117
+ i += 2;
118
+ while (i < n && sql.charCodeAt(i) !== 0x0a)
119
+ i++;
120
+ continue;
121
+ }
122
+ // `/* … */` block comment with nested depth tracking.
123
+ if (c === 0x2f && sql.charCodeAt(i + 1) === 0x2a) {
124
+ i += 2;
125
+ let depth = 1;
126
+ while (i < n && depth > 0) {
127
+ if (sql.charCodeAt(i) === 0x2f &&
128
+ sql.charCodeAt(i + 1) === 0x2a) {
129
+ depth++;
130
+ i += 2;
131
+ }
132
+ else if (sql.charCodeAt(i) === 0x2a &&
133
+ sql.charCodeAt(i + 1) === 0x2f) {
134
+ depth--;
135
+ i += 2;
136
+ }
137
+ else {
138
+ i++;
139
+ }
140
+ }
141
+ continue;
142
+ }
143
+ break;
144
+ }
145
+ return i === 0 ? sql : sql.slice(i);
146
+ };
147
+ /** Strip leading whitespace and SQL comments, then upper-case for matching. */
148
+ const peekKeywords = (sql, count = 3) => {
149
+ // Skip leading whitespace / SQL line + block comments so we look at the
150
+ // statement's verb regardless of surrounding boilerplate.
151
+ let i = 0;
152
+ while (i < sql.length) {
153
+ const ch = sql[i];
154
+ if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
155
+ i++;
156
+ continue;
157
+ }
158
+ if (ch === "-" && sql[i + 1] === "-") {
159
+ const nl = sql.indexOf("\n", i + 2);
160
+ if (nl === -1)
161
+ return [];
162
+ i = nl + 1;
163
+ continue;
164
+ }
165
+ if (ch === "/" && sql[i + 1] === "*") {
166
+ const end = sql.indexOf("*/", i + 2);
167
+ if (end === -1)
168
+ return [];
169
+ i = end + 2;
170
+ continue;
171
+ }
172
+ break;
173
+ }
174
+ const tail = sql.slice(i);
175
+ // Tokenise on whitespace + a small set of punctuation that can immediately
176
+ // follow a keyword (semicolon, comma, open-paren).
177
+ const words = tail.split(/[\s,;()]+/u, count + 1);
178
+ return words.slice(0, count).map((w) => w.toUpperCase());
179
+ };
180
+ /** Mirror of `command_no_begin()` in psql/common.c. */
181
+ const commandNoBegin = (sql) => {
182
+ const [w0, w1, w2, w3] = peekKeywords(sql, 4);
183
+ if (!w0)
184
+ return false;
185
+ switch (w0) {
186
+ case "ABORT":
187
+ case "BEGIN":
188
+ case "COMMIT":
189
+ case "END":
190
+ case "ROLLBACK":
191
+ case "START":
192
+ case "SAVEPOINT":
193
+ case "RELEASE":
194
+ return true;
195
+ case "PREPARE":
196
+ return w1 === "TRANSACTION";
197
+ case "VACUUM":
198
+ return true;
199
+ case "CLUSTER":
200
+ // CLUSTER without an explicit argument runs over the whole DB and
201
+ // cannot be transactional.
202
+ return w1 === undefined || w1 === "";
203
+ case "CREATE":
204
+ if (w1 === "DATABASE" || w1 === "TABLESPACE")
205
+ return true;
206
+ // CREATE INDEX CONCURRENTLY / CREATE UNIQUE INDEX CONCURRENTLY cannot
207
+ // run inside a transaction block — psql must send them bare even with
208
+ // AUTOCOMMIT=off (review item #24).
209
+ if (w1 === "INDEX" && w2 === "CONCURRENTLY")
210
+ return true;
211
+ if (w1 === "UNIQUE" && w2 === "INDEX" && w3 === "CONCURRENTLY") {
212
+ return true;
213
+ }
214
+ return false;
215
+ case "DROP":
216
+ // DROP DATABASE / TABLESPACE / INDEX CONCURRENTLY. (There is no
217
+ // `DROP TABLE … CONCURRENTLY` in PostgreSQL — removed that bogus case.)
218
+ if (w1 === "DATABASE" || w1 === "TABLESPACE")
219
+ return true;
220
+ if (w1 === "INDEX" && w2 === "CONCURRENTLY")
221
+ return true;
222
+ return false;
223
+ case "REINDEX":
224
+ // REINDEX DATABASE / SYSTEM / INDEX CONCURRENTLY / TABLE CONCURRENTLY.
225
+ if (w1 === "DATABASE" || w1 === "SYSTEM")
226
+ return true;
227
+ if (w1 === "INDEX" && w2 === "CONCURRENTLY")
228
+ return true;
229
+ if (w1 === "TABLE" && w2 === "CONCURRENTLY")
230
+ return true;
231
+ return false;
232
+ case "ALTER":
233
+ return w1 === "SYSTEM";
234
+ case "DISCARD":
235
+ return w1 === "ALL";
236
+ default:
237
+ return false;
238
+ }
239
+ };
240
+ /** True when the statement opens with SELECT / VALUES / TABLE / WITH. */
241
+ const isSelectCommand = (sql) => {
242
+ const [w0] = peekKeywords(sql, 1);
243
+ return (w0 === "SELECT" || w0 === "VALUES" || w0 === "TABLE" || w0 === "WITH");
244
+ };
245
+ /**
246
+ * Does this statement effectively destroy / replace the temporary savepoint?
247
+ * Upstream's `svpt_gone` flag is set when the user's command is one of
248
+ * COMMIT / ROLLBACK / SAVEPOINT / RELEASE. In those cases we must skip the
249
+ * matching RELEASE because the named savepoint no longer exists.
250
+ */
251
+ const destroysSavepoint = (sql) => {
252
+ const [w0] = peekKeywords(sql, 1);
253
+ return (w0 === "COMMIT" ||
254
+ w0 === "ROLLBACK" ||
255
+ w0 === "SAVEPOINT" ||
256
+ w0 === "RELEASE");
257
+ };
258
+ // ---------------------------------------------------------------------------
259
+ // Printer selection. Routes to the printer for the active output format —
260
+ // every format in {@link OutputFormat} that we ship is wired here; `wrapped`
261
+ // falls back to the aligned printer (which renders `wrapped` mode itself
262
+ // via `topt`).
263
+ // ---------------------------------------------------------------------------
264
+ const pickPrinter = (settings) => {
265
+ switch (settings.popt.topt.format) {
266
+ case "aligned":
267
+ case "wrapped":
268
+ return alignedPrinter;
269
+ case "unaligned":
270
+ return unalignedPrinter;
271
+ case "csv":
272
+ return csvPrinter;
273
+ case "json":
274
+ return jsonPrinter;
275
+ case "html":
276
+ return htmlPrinter;
277
+ case "asciidoc":
278
+ return asciidocPrinter;
279
+ case "latex":
280
+ return latexPrinter;
281
+ case "latex-longtable":
282
+ return latexLongtablePrinter;
283
+ case "troff-ms":
284
+ return troffMsPrinter;
285
+ default:
286
+ return alignedPrinter;
287
+ }
288
+ };
289
+ /**
290
+ * Pick the output target for a query result.
291
+ *
292
+ * Precedence: explicit `oneShot` argument > the settings stash from
293
+ * `\o FILE` (WP-15) > the REPL context's `stdout`.
294
+ */
295
+ export const pickOut = (ctx, oneShot) => {
296
+ if (oneShot)
297
+ return oneShot;
298
+ return getQueryFout(ctx.settings) ?? ctx.stdout;
299
+ };
300
+ // ---------------------------------------------------------------------------
301
+ // Settings accessors.
302
+ //
303
+ // AUTOCOMMIT, ON_ERROR_ROLLBACK, FETCH_COUNT all live in the psql var store.
304
+ // Upstream reads them once at the start of SendQuery; we do the same so a
305
+ // hook that mutates them mid-query doesn't reshape our logic underneath us.
306
+ // ---------------------------------------------------------------------------
307
+ const readAutocommit = (settings) => settings.vars.asBool("AUTOCOMMIT", true);
308
+ const readOnErrorRollback = (settings) => {
309
+ const raw = settings.vars.get("ON_ERROR_ROLLBACK");
310
+ if (raw === undefined)
311
+ return settings.onErrorRollback;
312
+ const v = raw.toLowerCase();
313
+ if (v === "interactive")
314
+ return "interactive";
315
+ if (v === "on" || v === "true" || v === "yes" || v === "1")
316
+ return "on";
317
+ return "off";
318
+ };
319
+ const readFetchCount = (settings) => {
320
+ const v = settings.vars.asInt("FETCH_COUNT", settings.fetchCount);
321
+ if (typeof v !== "number")
322
+ return 0;
323
+ return Math.max(0, v | 0);
324
+ };
325
+ const readSinglestep = (settings) => settings.singlestep || settings.vars.asBool("SINGLESTEP", false);
326
+ /**
327
+ * SHOW_ALL_RESULTS controls multi-statement `\;` printing. Default 'on' —
328
+ * every result set is rendered. When 'off' / '0', only the LAST result set
329
+ * is printed (upstream's `pset.show_all_results` flag, consulted by
330
+ * `PrintQueryResults` in common.c).
331
+ */
332
+ const readShowAllResults = (settings) => settings.vars.asBool("SHOW_ALL_RESULTS", true);
333
+ // ---------------------------------------------------------------------------
334
+ // Error printing — mirrors mainloop's `writeError` format. We keep it local
335
+ // so callers other than the mainloop can still emit consistent errors.
336
+ //
337
+ // `writeError` handles client-side diagnostics (e.g., "no connection to the
338
+ // server") that have no server-side ErrorResponse payload — we emit a single
339
+ // `psql: ERROR: <msg>` line.
340
+ //
341
+ // `writeQueryError` is used after a thrown query error has been captured
342
+ // into `settings.lastErrorResult` by `recordError` / `captureLastError`. It
343
+ // dispatches through {@link formatErrorReport} so the verbosity and
344
+ // SHOW_CONTEXT settings decide whether to surface the SQLSTATE / LINE /
345
+ // caret / DETAIL / HINT / CONTEXT / LOCATION layers. Matches upstream
346
+ // psql's `pg_log_error` shape in `src/bin/psql/common.c`.
347
+ // ---------------------------------------------------------------------------
348
+ const writeError = (ctx, message) => {
349
+ ctx.stderr.write(`psql: ERROR: ${message}\n`);
350
+ };
351
+ /**
352
+ * Render the verbosity-aware error report for the most recently captured
353
+ * query error and write it to `ctx.stderr`. The leading severity line gets
354
+ * the same `psql:[<file>:<n>]:` diagnostic prefix that upstream's
355
+ * `pg_log_pre_callback` prepends — matching the format the regression-
356
+ * derived conformance suite expects (e.g. `psql:<stdin>:N: ERROR: ...`).
357
+ * Subsequent layers (`LINE N: ...`, caret, `DETAIL: ...`, ...) follow on
358
+ * their own lines per {@link formatErrorReport}, unprefixed, to match
359
+ * libpq's `PQresultErrorMessage` output shape. When the captured error is
360
+ * missing (defensive — callers should always pair this with a preceding
361
+ * `recordError`) we fall back to the plain client-side {@link writeError}
362
+ * form so we never swallow the message entirely.
363
+ *
364
+ * Exported so the bind-path in mainloop can share the renderer.
365
+ */
366
+ export const writeQueryError = (ctx, fallbackMessage) => {
367
+ const e = ctx.settings.lastErrorResult;
368
+ if (!e || (!e.message && !e.code && !e.sqlstate)) {
369
+ writeError(ctx, fallbackMessage);
370
+ return;
371
+ }
372
+ const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
373
+ const prefix = psqlErrorPrefix(ctx.settings);
374
+ const prefixed = [prefix + lines[0], ...lines.slice(1)];
375
+ ctx.stderr.write(prefixed.join("\n") + "\n");
376
+ };
377
+ /**
378
+ * Strip leading whitespace from a query and rebase a 1-based server position
379
+ * to match. Mirrors upstream psql/mainloop.c's behaviour: a line containing
380
+ * only a backslash command does not leave a `\n` in the query buffer, so the
381
+ * subsequent SQL statement starts at "line 1" of its own context rather than
382
+ * inheriting a blank line. Our mainloop doesn't perform that strip, so the
383
+ * captured `sqlText` sometimes has a leading `\n` (e.g. after `\set
384
+ * FETCH_COUNT 1\nSELECT error;`). Without this normalisation, `\errverbose`
385
+ * would render `LINE 2: SELECT error;` where upstream renders `LINE 1: …`.
386
+ *
387
+ * Returns the trimmed text and the rebased position. If the rebased
388
+ * position would land outside the trimmed text, it is dropped so the
389
+ * formatter skips the `LINE`/caret block instead of mis-pointing.
390
+ */
391
+ const normaliseSqlAndPosition = (sqlText, position) => {
392
+ let leading = 0;
393
+ while (leading < sqlText.length) {
394
+ const ch = sqlText.charCodeAt(leading);
395
+ // Match psql_scan's whitespace set: space, tab, CR, LF, form-feed.
396
+ if (ch !== 0x20 &&
397
+ ch !== 0x09 &&
398
+ ch !== 0x0a &&
399
+ ch !== 0x0d &&
400
+ ch !== 0x0c) {
401
+ break;
402
+ }
403
+ leading++;
404
+ }
405
+ if (leading === 0)
406
+ return { sqlText, position };
407
+ const trimmed = sqlText.slice(leading);
408
+ if (typeof position !== "string")
409
+ return { sqlText: trimmed, position };
410
+ const original = parseInt(position, 10);
411
+ if (!Number.isFinite(original) || original <= 0) {
412
+ return { sqlText: trimmed, position };
413
+ }
414
+ const rebased = original - leading;
415
+ if (rebased <= 0 || rebased > trimmed.length) {
416
+ return { sqlText: trimmed, position: undefined };
417
+ }
418
+ return { sqlText: trimmed, position: String(rebased) };
419
+ };
420
+ /**
421
+ * Capture the full ErrorResponse-shaped payload from a thrown error.
422
+ *
423
+ * Our wire layer copies every named field of the server's ErrorResponse
424
+ * (severity / code / detail / hint / position / file / line / routine /
425
+ * …) onto the thrown Error as own properties (see `asThrowable` in
426
+ * `wire/connection.ts`). We mirror those onto `settings.lastErrorResult`
427
+ * so `\errverbose` can re-render the error in VERBOSE form — including
428
+ * the `LINE N: …` re-print + `^` pointer and the `LOCATION:` footer.
429
+ *
430
+ * `sqlText` is the originating SQL text from the caller; required so the
431
+ * `^` pointer can be positioned under the failing character. Leading
432
+ * whitespace is stripped (and `position` is rebased) so the `LINE N`
433
+ * counter reflects offsets within the user's statement rather than any
434
+ * buffer noise carried over from prior backslash commands.
435
+ */
436
+ export const captureLastError = (settings, err, sqlText) => {
437
+ const fallbackMessage = err instanceof Error ? err.message : String(err);
438
+ const e = (err ?? {});
439
+ const code = e.code;
440
+ const normalised = normaliseSqlAndPosition(sqlText, e.position);
441
+ settings.lastErrorResult = {
442
+ severity: e.severity,
443
+ code,
444
+ // Keep `sqlstate` as an alias for legacy callers / tests.
445
+ sqlstate: code,
446
+ message: e.message ?? fallbackMessage,
447
+ detail: e.detail,
448
+ hint: e.hint,
449
+ position: normalised.position,
450
+ internalPosition: e.internalPosition,
451
+ internalQuery: e.internalQuery,
452
+ where: e.where,
453
+ schema: e.schema,
454
+ table: e.table,
455
+ column: e.column,
456
+ dataType: e.dataType,
457
+ constraint: e.constraint,
458
+ file: e.file,
459
+ line: e.line,
460
+ routine: e.routine,
461
+ sqlText: normalised.sqlText,
462
+ };
463
+ return settings.lastErrorResult.message ?? fallbackMessage;
464
+ };
465
+ const recordError = (ctx, err, sqlText = "") => captureLastError(ctx.settings, err, sqlText);
466
+ /**
467
+ * Update the per-statement diagnostic psql variables that upstream's
468
+ * `SetResultVariables` / `SetErrorVariables` in `src/bin/psql/common.c`
469
+ * maintains. Called after every dispatched statement (success and error
470
+ * paths) so `\echo :LAST_ERROR_MESSAGE` and friends produce the same
471
+ * values vanilla psql does.
472
+ *
473
+ * - `SQLSTATE` SQLSTATE of the *most recent* statement —
474
+ * `"00000"` on success, the server-reported code
475
+ * on error (defaults to `"XX000"` when missing).
476
+ * - `ERROR` `"true"` if the most recent statement failed,
477
+ * else `"false"`.
478
+ * - `ROW_COUNT` affected/returned row count of the most recent
479
+ * statement (from libpq's `PQcmdTuples`). `"0"`
480
+ * on error.
481
+ * - `LAST_ERROR_*` sticky — only mutated on error. Mirrors
482
+ * upstream's "preserve until the next failure"
483
+ * contract so a successful statement does not
484
+ * clobber the prior error info.
485
+ *
486
+ * Exported so mainloop's bind / pipeline paths (which bypass {@link
487
+ * sendQuery}) can share the same updater.
488
+ */
489
+ export const refreshErrorVars = (settings, outcome) => {
490
+ const { vars } = settings;
491
+ if (outcome.kind === "error") {
492
+ const last = settings.lastErrorResult;
493
+ const code = last?.code ?? last?.sqlstate ?? "XX000";
494
+ const message = last?.message ?? "";
495
+ vars.set("LAST_ERROR_MESSAGE", message);
496
+ vars.set("LAST_ERROR_SQLSTATE", code);
497
+ vars.set("SQLSTATE", code);
498
+ vars.set("ERROR", "true");
499
+ vars.set("ROW_COUNT", "0");
500
+ return;
501
+ }
502
+ vars.set("SQLSTATE", "00000");
503
+ vars.set("ERROR", "false");
504
+ const rc = outcome.rowCount ?? 0;
505
+ vars.set("ROW_COUNT", String(rc));
506
+ };
507
+ // ---------------------------------------------------------------------------
508
+ // SINGLESTEP confirmation.
509
+ //
510
+ // Upstream prints to stdout and reads a line from /dev/tty; we use ctx.stderr
511
+ // for the banner (so the SQL preview stays out of any redirected query
512
+ // output) and read one line from ctx.stdin. Returns true to proceed.
513
+ // ---------------------------------------------------------------------------
514
+ const readOneLine = (stdin) => new Promise((resolve) => {
515
+ let buf = "";
516
+ let resolved = false;
517
+ const onData = (chunk) => {
518
+ buf += chunk.toString();
519
+ const nl = buf.indexOf("\n");
520
+ if (nl !== -1) {
521
+ const line = buf.slice(0, nl);
522
+ cleanup();
523
+ if (!resolved) {
524
+ resolved = true;
525
+ resolve(line);
526
+ }
527
+ }
528
+ };
529
+ const onEnd = () => {
530
+ cleanup();
531
+ if (!resolved) {
532
+ resolved = true;
533
+ resolve(buf);
534
+ }
535
+ };
536
+ const cleanup = () => {
537
+ stdin.off("data", onData);
538
+ stdin.off("end", onEnd);
539
+ stdin.off("close", onEnd);
540
+ };
541
+ stdin.on("data", onData);
542
+ stdin.once("end", onEnd);
543
+ stdin.once("close", onEnd);
544
+ });
545
+ const confirmSinglestep = async (ctx, sql) => {
546
+ ctx.stderr.write(`***(Single step mode: verify command)*******************************************\n` +
547
+ `${sql}\n` +
548
+ `***(press return to proceed or enter x and return to cancel)********************\n`);
549
+ const line = await readOneLine(ctx.stdin);
550
+ return !line.trim().toLowerCase().startsWith("x");
551
+ };
552
+ // ---------------------------------------------------------------------------
553
+ // Result rendering. We tally rows printed / rows affected for the QueryStats
554
+ // return; both are best-effort against the libpq-shaped ResultSet (rowCount
555
+ // is null for DDL, rows.length is 0 for COPY etc.).
556
+ // ---------------------------------------------------------------------------
557
+ /**
558
+ * Reconstruct the libpq-style CommandComplete tag from the parsed parts our
559
+ * wire layer stores on a ResultSet. INSERT carries `oid` + `rowCount`; the
560
+ * other DML verbs (UPDATE/DELETE/MERGE/SELECT/MOVE/FETCH/COPY) carry just a
561
+ * `rowCount`; DDL has neither.
562
+ *
563
+ * Matches upstream psql's `PQcmdStatus(conn)` output (which is the raw tag
564
+ * the server sent — we round-trip it through our parser).
565
+ */
566
+ const formatCommandTag = (rs) => {
567
+ const command = (rs.command || "").trim();
568
+ if (command.length === 0)
569
+ return "";
570
+ if (command === "INSERT") {
571
+ // INSERT is the only tag with the legacy oid in front of rowCount.
572
+ return `INSERT ${rs.oid ?? 0} ${rs.rowCount ?? 0}`;
573
+ }
574
+ if (rs.rowCount !== null && rs.rowCount !== undefined) {
575
+ return `${command} ${rs.rowCount}`;
576
+ }
577
+ return command;
578
+ };
579
+ /**
580
+ * Decide whether the pager should activate for the given batch of result
581
+ * sets. We page when ANY of the sets-that-will-be-printed (i.e. tuples-
582
+ * producing, and not gated out by SHOW_ALL_RESULTS) crosses the threshold.
583
+ * The decision is centralised here so callers can override individual
584
+ * decision inputs (e.g. tests that inject a fake `output`).
585
+ */
586
+ const pickPagerDecision = (ctx, results, out) => {
587
+ const popt = ctx.settings.popt.topt;
588
+ // Pager off → never page (cheap exit, no looping needed).
589
+ if (popt.pager === "off")
590
+ return false;
591
+ // `\o FILE` (or `\g FILE`) wins over pager. If the queryFout is set, the
592
+ // pager must not activate even when popt.pager === 'always'.
593
+ const redirectedOutput = getQueryFout(ctx.settings) !== null;
594
+ if (redirectedOutput)
595
+ return false;
596
+ const showAll = readShowAllResults(ctx.settings);
597
+ const lastIdx = results.length - 1;
598
+ for (let i = 0; i < results.length; i++) {
599
+ const rs = results[i];
600
+ if (rs.fields.length === 0)
601
+ continue;
602
+ if (!(showAll || i === lastIdx))
603
+ continue;
604
+ const decision = shouldPage({
605
+ pager: popt.pager,
606
+ pagerMinLines: popt.pagerMinLines,
607
+ rowCount: rs.rows.length,
608
+ colCount: rs.fields.length,
609
+ output: out,
610
+ redirectedOutput,
611
+ });
612
+ if (decision)
613
+ return true;
614
+ }
615
+ return false;
616
+ };
617
+ const renderResultSets = async (ctx, results, out) => {
618
+ const printer = pickPrinter(ctx.settings);
619
+ let rowsAffected = 0;
620
+ let rowsPrinted = 0;
621
+ // When SHOW_ALL_RESULTS is off and we have a `\;`-separated batch, upstream
622
+ // only prints the LAST result set. The tally counters still walk every
623
+ // result so QueryStats stays consistent — only the printer call is gated.
624
+ const showAll = readShowAllResults(ctx.settings);
625
+ const lastIdx = results.length - 1;
626
+ const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
627
+ // Pager wrapping. If the active topt.pager + heuristics call for it, route
628
+ // the printer through a spawned pager (PAGER / PSQL_PAGER, default `less`
629
+ // on POSIX). The pager is opened ONCE per renderResultSets call so a `\;`
630
+ // batch ends up in a single pager session, matching upstream. SIGPIPE /
631
+ // EPIPE handling lives inside the pager module.
632
+ const wantPager = pickPagerDecision(ctx, results, out);
633
+ const pager = wantPager
634
+ ? openPager({
635
+ pager: ctx.settings.popt.topt.pager,
636
+ pagerMinLines: ctx.settings.popt.topt.pagerMinLines,
637
+ stdout: out,
638
+ // shouldPage already verified pager-on conditions; force-spawn at
639
+ // the openPager level by re-passing the topt setting.
640
+ })
641
+ : null;
642
+ const sink = pager?.spawned ? pager.out : out;
643
+ try {
644
+ for (let i = 0; i < results.length; i++) {
645
+ const rs = results[i];
646
+ const shouldEmit = showAll || i === lastIdx;
647
+ if (rs.copyOutBytes && rs.copyOutBytes.length > 0) {
648
+ // `COPY ... TO STDOUT` segment of a `\;`-chained batch — emit the
649
+ // accumulated CopyData payloads at the result's position in the
650
+ // chain (upstream `handleCopyOut` writes the bytes to
651
+ // `pset.queryFout`, which under a normal dispatch is the active
652
+ // stdout). Render unconditionally regardless of SHOW_ALL_RESULTS:
653
+ // upstream gates `\;`-chain row tables on `show_all_results`, but
654
+ // the COPY data flows directly to the output stream and is not
655
+ // affected by the flag. Matches the regress baseline ordering for
656
+ // `... \; COPY x TO STDOUT \; ...`.
657
+ for (const chunk of rs.copyOutBytes) {
658
+ sink.write(chunk);
659
+ }
660
+ }
661
+ if (rs.fields.length === 0) {
662
+ // Non-tuples-producing commands (INSERT/UPDATE/DELETE/DDL) — emit the
663
+ // CommandComplete tag instead of running the table printer (which
664
+ // would render an empty `(0 rows)` block). Suppressed in tuples-only
665
+ // mode (`\t`) and in `--quiet` mode to match upstream
666
+ // (PSQLexec calls SetResultVariables which only prints the tag
667
+ // when !pset.quiet). Also suppressed when the result represents a
668
+ // COPY-out segment whose bytes we already streamed above —
669
+ // upstream's `handleCopyOut` doesn't emit the `COPY N` tag on the
670
+ // queryFout stream; the tag goes to the status stream which we
671
+ // route through the diagnostic vars rather than stdout.
672
+ if (shouldEmit &&
673
+ !tuplesOnly &&
674
+ !ctx.settings.quiet &&
675
+ !rs.copyOutBytes) {
676
+ const tag = formatCommandTag(rs);
677
+ if (tag.length > 0)
678
+ sink.write(`${tag}\n`);
679
+ }
680
+ // rowCount is the affected-row total when libpq sets it.
681
+ rowsAffected += rs.rowCount ?? 0;
682
+ }
683
+ else {
684
+ if (shouldEmit) {
685
+ await printer.printQuery(rs, ctx.settings.popt, sink);
686
+ }
687
+ rowsPrinted += rs.rows.length;
688
+ }
689
+ }
690
+ }
691
+ finally {
692
+ if (pager?.spawned) {
693
+ // End the pager stdin and wait for it to exit. We swallow errors here:
694
+ // the user may have closed the pager early (SIGPIPE → EPIPE) and our
695
+ // callers should not see that as a query failure.
696
+ try {
697
+ await pager.close();
698
+ }
699
+ catch {
700
+ // ignore
701
+ }
702
+ }
703
+ }
704
+ // libpq's `PQcmdTuples(lastResult)` semantic: ROW_COUNT mirrors the LAST
705
+ // result set's affected-row count (or returned-row count for tuples-
706
+ // producing commands). For SELECT-shaped results the wire layer doesn't
707
+ // populate rs.rowCount until CommandComplete arrives, but the array shape
708
+ // (`rs.rows.length`) is the authoritative count.
709
+ const lastRowCount = results.length === 0
710
+ ? null
711
+ : (() => {
712
+ const rs = results[results.length - 1];
713
+ if (rs.fields.length > 0)
714
+ return rs.rows.length;
715
+ return rs.rowCount ?? null;
716
+ })();
717
+ return { rowsAffected, rowsPrinted, lastRowCount };
718
+ };
719
+ /**
720
+ * Render a single {@link ResultSet} through the active printer and the
721
+ * configured output target (respecting `\o FILE` redirects). Used by the
722
+ * `\bind` / extended-query path in {@link mainloop.dispatchSendQuery} which
723
+ * comes back with a single result instead of the array shape `execSimple`
724
+ * produces. Returns a tally consistent with {@link renderResultSets}.
725
+ */
726
+ export const renderResultSet = (ctx, rs, out) => {
727
+ return renderResultSets(ctx, [rs], out ?? pickOut(ctx));
728
+ };
729
+ // ---------------------------------------------------------------------------
730
+ // FETCH_COUNT cursor loop.
731
+ //
732
+ // Wrap `<sql>` in DECLARE/FETCH and stream chunks. We open the cursor inside
733
+ // a transaction (upstream relies on the surrounding implicit BEGIN); when
734
+ // AUTOCOMMIT is on and we're idle, we open one here and close it with a
735
+ // COMMIT on the happy path / ROLLBACK on error.
736
+ // ---------------------------------------------------------------------------
737
+ /**
738
+ * Re-base a server-side `position` field so it points into the user's
739
+ * original SQL rather than the synthetic statement we actually sent.
740
+ *
741
+ * The FETCH_COUNT path sends `DECLARE _psql_cursor NO SCROLL CURSOR FOR
742
+ * <userSql>` for the DECLARE leg and `FETCH FORWARD N FROM _psql_cursor`
743
+ * for each fetch. Server error positions (`P` field) come back in the
744
+ * coordinates of whatever query we sent:
745
+ *
746
+ * - DECLARE-time parser/planner errors carry a position into the DECLARE
747
+ * statement. We subtract the length of the prefix (`DECLARE … FOR `)
748
+ * so the caret lands under the failing token in `userSql`.
749
+ *
750
+ * - FETCH-time runtime errors come from executing the cursor's underlying
751
+ * query (which IS `userSql`). The server reports the position relative
752
+ * to that underlying query, so it's already in `userSql` coordinates
753
+ * and we leave it alone.
754
+ *
755
+ * If we can't rebase a DECLARE-coord position into `userSql` bounds, we
756
+ * strip it rather than render a caret pointing past end-of-line.
757
+ */
758
+ const rebasePositionForCursor = (err, wrapper, userSql) => {
759
+ if (!err || typeof err !== "object")
760
+ return;
761
+ const e = err;
762
+ if (typeof e.position !== "string")
763
+ return;
764
+ const original = parseInt(e.position, 10);
765
+ if (!Number.isFinite(original) || original <= 0)
766
+ return;
767
+ // Find the user's SQL inside the wrapper. If the wrapper *contains* the
768
+ // user's SQL verbatim (the DECLARE case), the prefix length tells us how
769
+ // far to shift. The trailing `;` is stripped before wrapping, so we
770
+ // search for the stripped form.
771
+ const stripped = userSql.replace(/;\s*$/u, "");
772
+ const offset = wrapper.indexOf(stripped);
773
+ if (offset === -1) {
774
+ // FETCH-leg failures: the wrapper is `FETCH FORWARD …` and the server
775
+ // reports the position relative to the cursor's underlying query
776
+ // (i.e. `userSql`), not the FETCH text. Leave the position alone —
777
+ // assuming it's already in user-sql coordinates is the right call,
778
+ // and if it isn't, the LINE/caret renderer clamps gracefully.
779
+ return;
780
+ }
781
+ const rebased = original - offset;
782
+ if (rebased <= 0 || rebased > userSql.length) {
783
+ // Position points outside the user's SQL — likely the parser blamed
784
+ // something inside the wrapper. Drop the field so the formatter skips
785
+ // the `LINE`/caret block instead of mis-pointing.
786
+ delete e.position;
787
+ return;
788
+ }
789
+ e.position = String(rebased);
790
+ };
791
+ const runCursorLoop = async (ctx, sql, fetchCount, out) => {
792
+ if (!ctx.settings.db)
793
+ throw new Error("no connection to the server");
794
+ const db = ctx.settings.db;
795
+ // Make sure we're in a transaction so the cursor survives between FETCH
796
+ // calls. If we're idle, open one here and remember to close it.
797
+ const initiallyIdle = readTxStatus(db) === "I";
798
+ if (initiallyIdle) {
799
+ await db.execSimple("BEGIN");
800
+ }
801
+ // Strip trailing ';' from the user SQL so DECLARE CURSOR FOR <stmt> parses.
802
+ const stripped = sql.replace(/;\s*$/u, "");
803
+ const declared = `DECLARE ${CURSOR_NAME} NO SCROLL CURSOR FOR ${stripped}`;
804
+ const fetchSql = `FETCH FORWARD ${String(fetchCount)} FROM ${CURSOR_NAME}`;
805
+ const rowsAffected = 0;
806
+ let rowsPrinted = 0;
807
+ let cursorOpen = false;
808
+ // Track which synthetic statement is currently running so the catch block
809
+ // can rebase the server-side `position` into the user's SQL coordinates
810
+ // before throwing. Without this, `\errverbose` renders `LINE 1: <user-sql>`
811
+ // with the caret pointing past end-of-line.
812
+ let currentWrapper = declared;
813
+ const printer = pickPrinter(ctx.settings);
814
+ // Upstream's print_cursor.c walks the cursor in chunks and toggles libpq's
815
+ // `flag.start_table` / `flag.stop_table` so the table renders as one
816
+ // continuous block — header on the first chunk, footer on the last. Our
817
+ // `aligned` printer doesn't (yet) honour those toggles, so we merge every
818
+ // chunk into a single synthetic ResultSet and hand it to the printer once.
819
+ // The user-facing output is identical to the non-chunked path, which is
820
+ // what the regress baseline expects (one `(19 rows)` footer instead of
821
+ // `(10 rows)` + `(9 rows)`).
822
+ let merged = null;
823
+ try {
824
+ await db.execSimple(declared);
825
+ cursorOpen = true;
826
+ while (true) {
827
+ currentWrapper = fetchSql;
828
+ const sets = await db.execSimple(fetchSql);
829
+ if (sets.length === 0)
830
+ break;
831
+ const rs = sets[sets.length - 1];
832
+ const chunkRows = rs.rows.length;
833
+ if (chunkRows === 0)
834
+ break;
835
+ if (merged === null) {
836
+ merged = {
837
+ command: rs.command,
838
+ fields: rs.fields,
839
+ rows: rs.rows.slice(),
840
+ rowCount: rs.rowCount,
841
+ oid: rs.oid,
842
+ notices: rs.notices,
843
+ };
844
+ }
845
+ else {
846
+ for (const row of rs.rows)
847
+ merged.rows.push(row);
848
+ }
849
+ rowsPrinted += chunkRows;
850
+ if (chunkRows < fetchCount)
851
+ break;
852
+ }
853
+ if (merged !== null) {
854
+ // Patch the merged rowCount to reflect the actual aggregated row
855
+ // total so command-tag / `(N rows)` footers match the upstream
856
+ // single-statement output.
857
+ merged.rowCount = merged.rows.length;
858
+ await printer.printQuery(merged, ctx.settings.popt, out);
859
+ }
860
+ await db.execSimple(`CLOSE ${CURSOR_NAME}`);
861
+ cursorOpen = false;
862
+ if (initiallyIdle) {
863
+ await db.execSimple("COMMIT");
864
+ }
865
+ return { rowsAffected, rowsPrinted, lastRowCount: rowsPrinted };
866
+ }
867
+ catch (err) {
868
+ // Flush whatever chunks we successfully fetched before the error so the
869
+ // partial output lands ahead of the ERROR line. Mirrors upstream
870
+ // print_cursor.c: each chunk renders incrementally — when a later FETCH
871
+ // raises (e.g. division by zero on row 16 of a 10-row chunked stream),
872
+ // the first chunk's rows have already been printed. We accumulate into
873
+ // a single merged ResultSet here, so the partial flush is "print the
874
+ // merged buffer once, without the `(N rows)` footer the happy-path
875
+ // emits when the cursor completes cleanly". The footer is suppressed
876
+ // because the table is conceptually incomplete (upstream renders no
877
+ // `(N rows)` for the truncated chunk either).
878
+ if (merged !== null) {
879
+ merged.rowCount = merged.rows.length;
880
+ const partialOpts = {
881
+ ...ctx.settings.popt,
882
+ // `stopTable: false` mirrors upstream `print_cursor.c`'s
883
+ // mid-error flush: no `(N rows)` auto-footer, no trailing
884
+ // blank — the ERROR line should land flush against the last
885
+ // data row, not separated by an extra empty line.
886
+ topt: {
887
+ ...ctx.settings.popt.topt,
888
+ defaultFooter: false,
889
+ stopTable: false,
890
+ },
891
+ };
892
+ try {
893
+ await printer.printQuery(merged, partialOpts, out);
894
+ }
895
+ catch {
896
+ // ignore — surface the original error
897
+ }
898
+ }
899
+ // Rebase the server-reported `position` from the synthetic wrapper's
900
+ // coordinates into the user's SQL coordinates in place. Server error
901
+ // positions come back relative to whatever statement we sent (DECLARE
902
+ // `… FOR <user-sql>` or FETCH FORWARD `…`). Without this rewrite, the
903
+ // caller's `recordError(ctx, err, sql)` would stash a position that
904
+ // points past the end of `sql`, and `\errverbose` would render
905
+ // `LINE 1: <user-sql>` with the `^` caret in the wrong column.
906
+ rebasePositionForCursor(err, currentWrapper, sql);
907
+ if (cursorOpen) {
908
+ try {
909
+ await db.execSimple(`CLOSE ${CURSOR_NAME}`);
910
+ }
911
+ catch {
912
+ // ignore — surface the original error
913
+ }
914
+ }
915
+ if (initiallyIdle) {
916
+ try {
917
+ await db.execSimple("ROLLBACK");
918
+ }
919
+ catch {
920
+ // ignore
921
+ }
922
+ }
923
+ throw err;
924
+ }
925
+ };
926
+ // ---------------------------------------------------------------------------
927
+ // `executeAndPrint` — the inner pipeline: execSimple → render → tally.
928
+ // Used directly by `\watch` and `\gexec` (which manage their own transaction
929
+ // scaffolding). Caller is responsible for AUTOCOMMIT / savepoint state.
930
+ // ---------------------------------------------------------------------------
931
+ export const executeAndPrint = async (ctx, sqlRaw, opts = {}) => {
932
+ // Strip leading whitespace + `--` line / slash-star block comments before
933
+ // the wire send so server-reported `position` (1-based offset) and
934
+ // `LINE N:` re-prints align with upstream — vanilla psql's scanner
935
+ // advances past the same prelude before handing the buffer to `PQexec`.
936
+ const sql = stripLeadingCommentsAndWS(sqlRaw);
937
+ const started = ctx.settings.timing ? performance.now() : 0;
938
+ const stats = {
939
+ rowsAffected: 0,
940
+ rowsPrinted: 0,
941
+ fetched: false,
942
+ hadError: false,
943
+ durationMs: 0,
944
+ };
945
+ if (!ctx.settings.db) {
946
+ writeError(ctx, "no connection to the server");
947
+ stats.hadError = true;
948
+ return stats;
949
+ }
950
+ const out = pickOut(ctx, opts.oneShotOut);
951
+ const fetchCount = readFetchCount(ctx.settings);
952
+ let lastRowCount = null;
953
+ try {
954
+ if (fetchCount > 0 && isSelectCommand(sql)) {
955
+ const r = await runCursorLoop(ctx, sql, fetchCount, out);
956
+ stats.rowsAffected = r.rowsAffected;
957
+ stats.rowsPrinted = r.rowsPrinted;
958
+ stats.fetched = true;
959
+ lastRowCount = r.lastRowCount;
960
+ }
961
+ else {
962
+ const results = await ctx.settings.db.execSimple(sql);
963
+ const r = await renderResultSets(ctx, results, out);
964
+ stats.rowsAffected = r.rowsAffected;
965
+ stats.rowsPrinted = r.rowsPrinted;
966
+ lastRowCount = r.lastRowCount;
967
+ }
968
+ }
969
+ catch (err) {
970
+ // `\;`-chained batches surface every result the server produced before
971
+ // the ErrorResponse on the thrown Error's `partialResults` field (set
972
+ // by the wire layer's ReadyForQuery handler). Render them in order
973
+ // before printing the error itself so the user sees the same shape
974
+ // upstream `PQgetResult` walks produce.
975
+ const partial = err
976
+ .partialResults;
977
+ if (partial && partial.length > 0) {
978
+ try {
979
+ const r = await renderResultSets(ctx, partial, out);
980
+ stats.rowsAffected = r.rowsAffected;
981
+ stats.rowsPrinted = r.rowsPrinted;
982
+ }
983
+ catch {
984
+ // Surface the original error; don't shadow it with a render failure.
985
+ }
986
+ }
987
+ const message = recordError(ctx, err, sql);
988
+ writeQueryError(ctx, message);
989
+ stats.hadError = true;
990
+ }
991
+ finally {
992
+ if (ctx.settings.timing) {
993
+ stats.durationMs = performance.now() - started;
994
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
995
+ }
996
+ }
997
+ // Mirror upstream's `SetResultVariables` / `SetErrorVariables` call at the
998
+ // tail of `SendQuery`: refresh the per-statement diagnostic psql vars so
999
+ // `\echo :SQLSTATE` and friends see the most recent outcome. ROW_COUNT
1000
+ // tracks libpq's `PQcmdTuples` on the LAST result of a `\;` batch.
1001
+ refreshErrorVars(ctx.settings, stats.hadError
1002
+ ? { kind: "error" }
1003
+ : { kind: "success", rowCount: lastRowCount });
1004
+ return stats;
1005
+ };
1006
+ // ---------------------------------------------------------------------------
1007
+ // `sendQuery` — the full pipeline: SINGLESTEP confirmation + AUTOCOMMIT
1008
+ // implicit BEGIN + ON_ERROR_ROLLBACK savepoint + execute + savepoint
1009
+ // resolution. Mirrors `SendQuery` in common.c.
1010
+ // ---------------------------------------------------------------------------
1011
+ export const sendQuery = async (ctx, sqlRaw, opts = {}) => {
1012
+ // Strip leading whitespace + `--` line / slash-star block comments before
1013
+ // the wire send AND before storing into `pset.last_query`. Vanilla psql's
1014
+ // scanner advances past the same prelude before handing the buffer to
1015
+ // `PQexec`, so server-reported `position` (1-based) and `LINE N:`
1016
+ // re-prints align with vanilla only after we trim here. `\p` (which falls
1017
+ // back to `lastQuery`) also prints the stripped form so the regress
1018
+ // baseline's `\p` after `-- comment\nSELECT 1;` emits just `SELECT 1;`.
1019
+ const sql = stripLeadingCommentsAndWS(sqlRaw);
1020
+ const stats = {
1021
+ rowsAffected: 0,
1022
+ rowsPrinted: 0,
1023
+ fetched: false,
1024
+ hadError: false,
1025
+ durationMs: 0,
1026
+ };
1027
+ // Track the most recent SQL we're about to ship so `\g` / `\gx` with an
1028
+ // empty buffer can re-run it (upstream `pset.last_query`). Capture even
1029
+ // if the dispatch fails — upstream populates `last_query` before
1030
+ // `PSQLexec` and leaves it set on error.
1031
+ ctx.settings.lastQuery = sql;
1032
+ if (!ctx.settings.db) {
1033
+ writeError(ctx, "no connection to the server");
1034
+ stats.hadError = true;
1035
+ return stats;
1036
+ }
1037
+ // SINGLESTEP: prompt before executing. 'x' aborts; anything else proceeds.
1038
+ if (readSinglestep(ctx.settings)) {
1039
+ const proceed = await confirmSinglestep(ctx, sql);
1040
+ if (!proceed) {
1041
+ // Upstream marks the statement as failed when the user cancels. We
1042
+ // mirror that so ON_ERROR_STOP halts a script.
1043
+ stats.hadError = true;
1044
+ ctx.settings.lastErrorResult = {
1045
+ message: "command cancelled by user",
1046
+ };
1047
+ return stats;
1048
+ }
1049
+ }
1050
+ // SINGLELINE (-S): treating a newline as a semicolon is a scanner concern
1051
+ // and is wired through `scanSql`'s `ScanOptions.singleline` (the mainloop
1052
+ // forwards `ctx.settings.singleline` on each pass). No work is required in
1053
+ // `sendQuery`: the statement boundary has already been drawn before we get
1054
+ // here.
1055
+ const db = ctx.settings.db;
1056
+ const autocommit = readAutocommit(ctx.settings);
1057
+ const onErrorRollback = readOnErrorRollback(ctx.settings);
1058
+ const interactive = !ctx.settings.notty;
1059
+ const started = ctx.settings.timing ? performance.now() : 0;
1060
+ // ----- AUTOCOMMIT: implicit BEGIN ----------------------------------------
1061
+ let implicitBeginIssued = false;
1062
+ if (!autocommit && readTxStatus(db) === "I" && !commandNoBegin(sql)) {
1063
+ try {
1064
+ await db.execSimple("BEGIN");
1065
+ implicitBeginIssued = true;
1066
+ }
1067
+ catch (err) {
1068
+ const message = recordError(ctx, err);
1069
+ writeQueryError(ctx, message);
1070
+ stats.hadError = true;
1071
+ if (ctx.settings.timing) {
1072
+ stats.durationMs = performance.now() - started;
1073
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
1074
+ }
1075
+ return stats;
1076
+ }
1077
+ }
1078
+ // ----- ON_ERROR_ROLLBACK: SAVEPOINT --------------------------------------
1079
+ const savepointActive = onErrorRollback !== "off" &&
1080
+ (onErrorRollback === "on" ||
1081
+ (onErrorRollback === "interactive" && interactive)) &&
1082
+ readTxStatus(db) === "T";
1083
+ let savepointIssued = false;
1084
+ if (savepointActive) {
1085
+ try {
1086
+ await db.execSimple(`SAVEPOINT ${SAVEPOINT_NAME}`);
1087
+ savepointIssued = true;
1088
+ }
1089
+ catch (err) {
1090
+ // Mirror upstream: failure to install the savepoint is a hard error.
1091
+ const message = recordError(ctx, err);
1092
+ writeQueryError(ctx, message);
1093
+ stats.hadError = true;
1094
+ if (ctx.settings.timing) {
1095
+ stats.durationMs = performance.now() - started;
1096
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
1097
+ }
1098
+ return stats;
1099
+ }
1100
+ }
1101
+ // ----- Execute + print ---------------------------------------------------
1102
+ const out = pickOut(ctx, opts.oneShotOut);
1103
+ const fetchCount = readFetchCount(ctx.settings);
1104
+ let lastRowCount = null;
1105
+ try {
1106
+ if (fetchCount > 0 && isSelectCommand(sql)) {
1107
+ const r = await runCursorLoop(ctx, sql, fetchCount, out);
1108
+ stats.rowsAffected = r.rowsAffected;
1109
+ stats.rowsPrinted = r.rowsPrinted;
1110
+ stats.fetched = true;
1111
+ lastRowCount = r.lastRowCount;
1112
+ }
1113
+ else {
1114
+ const results = await db.execSimple(sql);
1115
+ const r = await renderResultSets(ctx, results, out);
1116
+ stats.rowsAffected = r.rowsAffected;
1117
+ stats.rowsPrinted = r.rowsPrinted;
1118
+ lastRowCount = r.lastRowCount;
1119
+ }
1120
+ }
1121
+ catch (err) {
1122
+ // `\;`-chained batches surface every result the server produced before
1123
+ // the ErrorResponse on the thrown Error's `partialResults` field (set
1124
+ // by the wire layer's ReadyForQuery handler). Render them in order
1125
+ // before printing the error itself so the user sees the same shape
1126
+ // upstream `PQgetResult` walks produce.
1127
+ const partial = err
1128
+ .partialResults;
1129
+ if (partial && partial.length > 0) {
1130
+ try {
1131
+ const r = await renderResultSets(ctx, partial, out);
1132
+ stats.rowsAffected = r.rowsAffected;
1133
+ stats.rowsPrinted = r.rowsPrinted;
1134
+ }
1135
+ catch {
1136
+ // Surface the original error; don't shadow it with a render failure.
1137
+ }
1138
+ }
1139
+ const message = recordError(ctx, err, sql);
1140
+ writeQueryError(ctx, message);
1141
+ stats.hadError = true;
1142
+ }
1143
+ // ----- ON_ERROR_ROLLBACK: resolve the savepoint --------------------------
1144
+ if (savepointIssued) {
1145
+ try {
1146
+ if (stats.hadError) {
1147
+ await db.execSimple(`ROLLBACK TO SAVEPOINT ${SAVEPOINT_NAME}`);
1148
+ // Release the now-empty savepoint too, matching upstream.
1149
+ await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
1150
+ }
1151
+ else if (!destroysSavepoint(sql) && readTxStatus(db) === "T") {
1152
+ await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
1153
+ }
1154
+ }
1155
+ catch (err) {
1156
+ // Don't shadow the original error; just record this one if we don't
1157
+ // already have one to report.
1158
+ if (!stats.hadError) {
1159
+ const message = recordError(ctx, err);
1160
+ writeQueryError(ctx, message);
1161
+ stats.hadError = true;
1162
+ }
1163
+ }
1164
+ }
1165
+ // If we issued an implicit BEGIN for AUTOCOMMIT=off and the statement
1166
+ // itself failed in such a way that we ended up idle again, there is
1167
+ // nothing to clean up — the server has already rolled back. We
1168
+ // intentionally do not COMMIT here: that's the user's responsibility under
1169
+ // AUTOCOMMIT=off.
1170
+ void implicitBeginIssued;
1171
+ // Mirror upstream `SendQuery` tail (common.c lines 1217-1218):
1172
+ //
1173
+ // if (!OK && pset.echo == PSQL_ECHO_ERRORS)
1174
+ // pg_log_info("STATEMENT: %s", query);
1175
+ //
1176
+ // When ECHO=errors and the dispatch failed, emit a `STATEMENT: <sql>`
1177
+ // line so the user can correlate the error with the input statement.
1178
+ // `pg_log_info` writes to stderr in upstream and strips one trailing
1179
+ // newline before tacking its own `\n` on the message — we mirror by
1180
+ // going through ctx.stderr and the explicit trim.
1181
+ if (stats.hadError && ctx.settings.echo === "errors") {
1182
+ // Strip leading whitespace + `--`-style comments from queryBuf so the
1183
+ // STATEMENT echo matches upstream's shape. Upstream `psqlscan.l`'s
1184
+ // `{whitespace}` rule (which includes line comments) SUPPRESSES
1185
+ // queryBuf appends until non-whitespace content has been collected;
1186
+ // our scanner accumulates verbatim. The server still ignores the
1187
+ // leading noise for `LINE N:` counting, but the STATEMENT echo
1188
+ // re-prints the buffer as we hold it. Bring them in line by
1189
+ // stripping here. Also strip one trailing `\n` to match
1190
+ // `pg_log_info("STATEMENT: %s", query)` (one-newline-strip +
1191
+ // explicit `\n` append).
1192
+ let stmt = sql;
1193
+ while (true) {
1194
+ const before = stmt.length;
1195
+ // Leading whitespace including form-feed (matches psqlscan's
1196
+ // {space} = [ \t\n\r\f]).
1197
+ stmt = stmt.replace(/^[ \t\n\r\f]+/, "");
1198
+ // Leading `--`-style line comment, up to (but not including) the
1199
+ // next newline. The trailing newline is then eaten by the next
1200
+ // whitespace pass.
1201
+ stmt = stmt.replace(/^--[^\n\r]*/, "");
1202
+ if (stmt.length === before)
1203
+ break;
1204
+ }
1205
+ if (stmt.endsWith("\n"))
1206
+ stmt = stmt.slice(0, -1);
1207
+ ctx.stderr.write(`STATEMENT: ${stmt}\n`);
1208
+ }
1209
+ // Mirror upstream's `SetResultVariables` / `SetErrorVariables` call at the
1210
+ // tail of `SendQuery`. ROW_COUNT mirrors libpq's `PQcmdTuples` on the LAST
1211
+ // result of a `\;` batch; SQLSTATE / ERROR reset every statement; the
1212
+ // LAST_ERROR_* pair only changes on failure (sticky on success).
1213
+ refreshErrorVars(ctx.settings, stats.hadError
1214
+ ? { kind: "error" }
1215
+ : { kind: "success", rowCount: lastRowCount });
1216
+ if (ctx.settings.timing) {
1217
+ stats.durationMs = performance.now() - started;
1218
+ ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
1219
+ }
1220
+ return stats;
1221
+ };
1222
+ // ---------------------------------------------------------------------------
1223
+ // `psqlExec` — silent catalog-style queries used by backslash commands.
1224
+ //
1225
+ // Upstream returns a PGresult; the caller is expected to inspect status and
1226
+ // `PQclear` it. We return the last ResultSet from execSimple (the catalog
1227
+ // queries upstream uses are always single-statement) or null on error when
1228
+ // ignoreError is true.
1229
+ // ---------------------------------------------------------------------------
1230
+ export const psqlExec = async (conn, sql, ignoreError = false) => {
1231
+ try {
1232
+ const sets = await conn.execSimple(sql);
1233
+ if (sets.length === 0)
1234
+ return null;
1235
+ return sets[sets.length - 1];
1236
+ }
1237
+ catch (err) {
1238
+ if (ignoreError)
1239
+ return null;
1240
+ throw err;
1241
+ }
1242
+ };
1243
+ // Internal exports re-used by mainloop. Kept on the public surface so other
1244
+ // future call sites (cmd_io for \gexec, cmd_describe for catalog queries)
1245
+ // can lean on the same primitives.
1246
+ export const __testing = {
1247
+ commandNoBegin,
1248
+ isSelectCommand,
1249
+ destroysSavepoint,
1250
+ peekKeywords,
1251
+ SAVEPOINT_NAME,
1252
+ CURSOR_NAME,
1253
+ };