neon 2.1.1 → 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,1035 @@
1
+ /**
2
+ * psql `\copy` backslash command (WP-16).
3
+ *
4
+ * Port of `parse_slash_copy()` + `do_copy()` from upstream `src/bin/psql/copy.c`.
5
+ * The wire-level protocol (CopyData/CopyDone/CopyFail framing and the
6
+ * in-copy-in/in-copy-out state machine) lives in `../wire/connection.ts`; here
7
+ * we own:
8
+ *
9
+ * 1. Lexing the user-supplied tail of `\copy …`. Mirrors the upstream
10
+ * `strtokx()`-driven tokeniser: we ratchet through the input with the
11
+ * same whitespace/delim/quote rules so the grammar matches psql.
12
+ * 2. Building the COPY SQL the server sees. The client side does
13
+ * file/program plumbing; the server always sees `... FROM STDIN ...` /
14
+ * `... TO STDOUT ...` so the COPY data flows over the protocol stream.
15
+ * 3. Driving the protocol: open the file (or spawn `PROGRAM 'cmd'`), then
16
+ * `startCopyIn(sql)` / `startCopyOut(sql)`, push/pull bytes, and print
17
+ * the upstream-style `COPY <N>` summary on success.
18
+ *
19
+ * Grammar accepted (matching upstream documentation):
20
+ *
21
+ * \copy [BINARY] tablename [(columnlist)] FROM
22
+ * ( 'file' | PROGRAM 'cmd' | STDIN | PSTDIN ) [options]
23
+ * \copy [BINARY] tablename [(columnlist)] TO
24
+ * ( 'file' | PROGRAM 'cmd' | STDOUT | PSTDOUT ) [options]
25
+ * \copy (subquery) TO ( 'file' | PROGRAM 'cmd' | STDOUT | PSTDOUT ) [options]
26
+ *
27
+ * `\copy (subquery) FROM ...` is rejected — COPY FROM requires a real
28
+ * destination table, so the subquery form only makes sense with `TO`.
29
+ *
30
+ * Limitations vs upstream:
31
+ * - Binary COPY (server-side `WITH (FORMAT BINARY)` option) is byte-for-byte
32
+ * transparent: bytes captured by `COPY ... TO STDOUT WITH BINARY` are
33
+ * piped straight to the destination, and on `COPY ... FROM STDIN WITH
34
+ * BINARY` we relay the source bytes verbatim. We do NOT parse tuples;
35
+ * `validateCopyBinarySignature` is offered for callers that want to
36
+ * sniff the 11-byte file header, but the wire path itself is format-
37
+ * agnostic. The legacy `BINARY <table> FROM …` keyword syntax is parsed
38
+ * and re-emitted verbatim; we don't try to interpret the options blob.
39
+ * - The literal `\.` end-of-data marker is honoured when (and only when):
40
+ * the source is STDIN, AND the COPY format is text (not csv, not binary).
41
+ * A line matching exactly `\.` terminates the stream client-side via
42
+ * CopyDone; subsequent input bytes go back to the SQL stream. Matches
43
+ * upstream's stricter behaviour: csv/binary COPY treats `\.` as data.
44
+ * - PSTDIN/PSTDOUT are treated as STDIN/STDOUT (no separate "psql stdin
45
+ * vs current input source" distinction — REPL plumbing isn't wired yet).
46
+ */
47
+ import { Buffer } from "node:buffer";
48
+ import { spawn } from "node:child_process";
49
+ import { createReadStream, createWriteStream, promises as fsPromises, } from "node:fs";
50
+ import { pumpReadable } from "../wire/copy.js";
51
+ import { getPipelineState } from "./cmd_pipeline.js";
52
+ import { writeErr, writeOut } from "./shared.js";
53
+ /**
54
+ * Diagnostic emitted when the user tries to run `\copy` (or a raw COPY
55
+ * statement) inside an active `\startpipeline` ... `\endpipeline` block.
56
+ * Matches upstream libpq's wording so conformance tests grepping stderr
57
+ * (e.g. tap/001_basic.pl lines 490-531) pick it up unchanged. Exported so
58
+ * the wire-layer abort path can reuse the same string and tests can match
59
+ * via a single source of truth.
60
+ */
61
+ export const COPY_IN_PIPELINE_MSG = "COPY in a pipeline is not supported, aborting connection";
62
+ const WHITESPACE = " \t\n\r";
63
+ /**
64
+ * Tokenise the next term of the `\copy` tail. Mirrors upstream's `strtokx`
65
+ * call sites: each call passes a different combination of (delim chars,
66
+ * quote chars, allow-doubled-quotes, allow-E-strings). We faithfully replay
67
+ * those: this isn't a general lexer, it's a state machine indexed by the
68
+ * caller's intent.
69
+ *
70
+ * Returns `{ token, rest }`. `token === null` ⇒ end-of-input.
71
+ *
72
+ * Key differences from upstream `strtokx`:
73
+ * - We return tokens WITH outer quotes intact when the caller asked for
74
+ * them. `dequote` handles strip if desired. Upstream stores quotes
75
+ * in-place and optionally strips via `strip_quotes`.
76
+ * - Delimiter characters in `delim` are returned as single-char tokens
77
+ * when they're the first non-whitespace byte.
78
+ */
79
+ const tokenize = (input, delim, quote, doubleQuoteEscape) => {
80
+ let i = 0;
81
+ const n = input.length;
82
+ // 1. Skip leading whitespace.
83
+ while (i < n && WHITESPACE.includes(input[i]))
84
+ i++;
85
+ if (i >= n)
86
+ return { token: null, rest: "" };
87
+ // 2. Delimiter character returned as single-char token.
88
+ if (delim.length > 0 && delim.includes(input[i])) {
89
+ const token = input[i];
90
+ i++;
91
+ while (i < n && WHITESPACE.includes(input[i]))
92
+ i++;
93
+ return { token, rest: input.slice(i) };
94
+ }
95
+ // 3. Quoted token. Upstream allows backslash-escape inside `(query)` forms
96
+ // when standard_conforming_strings is off; we model that with the
97
+ // `doubleQuoteEscape` flag (true ⇒ backslash escapes any next char).
98
+ if (quote.length > 0 && quote.includes(input[i])) {
99
+ const thisQuote = input[i];
100
+ const start = i;
101
+ i++;
102
+ while (i < n) {
103
+ const c = input[i];
104
+ if (doubleQuoteEscape && c === "\\" && i + 1 < n) {
105
+ i += 2;
106
+ continue;
107
+ }
108
+ if (c === thisQuote && input[i + 1] === thisQuote) {
109
+ // Doubled quote — stays in token; caller dequotes if needed.
110
+ i += 2;
111
+ continue;
112
+ }
113
+ if (c === thisQuote) {
114
+ i++;
115
+ break;
116
+ }
117
+ i++;
118
+ }
119
+ const token = input.slice(start, i);
120
+ while (i < n && WHITESPACE.includes(input[i]))
121
+ i++;
122
+ return { token, rest: input.slice(i) };
123
+ }
124
+ // 4. Bareword: scan to next whitespace, delim, or quote.
125
+ const start = i;
126
+ while (i < n) {
127
+ const c = input[i];
128
+ if (WHITESPACE.includes(c))
129
+ break;
130
+ if (delim.length > 0 && delim.includes(c))
131
+ break;
132
+ if (quote.length > 0 && quote.includes(c))
133
+ break;
134
+ i++;
135
+ }
136
+ const token = input.slice(start, i);
137
+ while (i < n && WHITESPACE.includes(input[i]))
138
+ i++;
139
+ return { token, rest: input.slice(i) };
140
+ };
141
+ /**
142
+ * Strip surrounding single quotes from a filename / program argument and
143
+ * undouble any embedded quotes. Mirrors upstream's `strip_quotes(token, '\'', 0)`.
144
+ */
145
+ const stripSingleQuotes = (token) => {
146
+ if (token.length < 2 || !token.startsWith("'") || !token.endsWith("'")) {
147
+ return token;
148
+ }
149
+ let out = "";
150
+ let i = 1;
151
+ const end = token.length - 1;
152
+ while (i < end) {
153
+ if (token[i] === "'" && token[i + 1] === "'") {
154
+ out += "'";
155
+ i += 2;
156
+ }
157
+ else {
158
+ out += token[i];
159
+ i++;
160
+ }
161
+ }
162
+ return out;
163
+ };
164
+ /**
165
+ * Expand a leading `~/` in filename arguments. Upstream `expand_tilde` only
166
+ * touches the very first character; we do the same (no `~user/` form, since
167
+ * Node doesn't expose `getpwnam` cleanly).
168
+ */
169
+ const expandTilde = (filePath) => {
170
+ if (!filePath.startsWith("~"))
171
+ return filePath;
172
+ if (filePath === "~" || filePath.startsWith("~/")) {
173
+ const home = process.env.HOME ?? process.env.USERPROFILE;
174
+ if (home === undefined)
175
+ return filePath;
176
+ return home + filePath.slice(1);
177
+ }
178
+ return filePath;
179
+ };
180
+ /**
181
+ * Parse the tail of a `\copy ...` line. Returns a {@link ParsedCopy} on
182
+ * success or an error message on syntax failure (mirroring upstream's
183
+ * `pg_log_error("\\copy: parse error at \"%s\"")`).
184
+ *
185
+ * The input is everything after `\copy` (the command name itself is stripped
186
+ * by the dispatcher's `BackslashContext.rawArgs`).
187
+ */
188
+ export const parseSlashCopy = (input) => {
189
+ let beforeToFrom = "";
190
+ let rest = input;
191
+ let token;
192
+ // Helper to keep the failure messages consistent with upstream.
193
+ const errAt = (tok) => ({
194
+ ok: false,
195
+ error: tok !== null && tok.length > 0
196
+ ? `parse error at "${tok}"`
197
+ : "parse error at end of line",
198
+ });
199
+ // First token: optional BINARY, or table-name / "(" for subquery.
200
+ let r1 = tokenize(rest, ".,()", '"', false);
201
+ token = r1.token;
202
+ rest = r1.rest;
203
+ if (token === null)
204
+ return errAt(null);
205
+ // Optional legacy BINARY keyword (pre-7.3 syntax). Re-emit then read next.
206
+ if (token.toLowerCase() === "binary") {
207
+ beforeToFrom += token;
208
+ r1 = tokenize(rest, ".,()", '"', false);
209
+ token = r1.token;
210
+ rest = r1.rest;
211
+ if (token === null)
212
+ return errAt(null);
213
+ }
214
+ // `(query)` subquery form? Re-emit balanced-paren contents verbatim.
215
+ let isSubquery = false;
216
+ if (token === "(") {
217
+ isSubquery = true;
218
+ let parens = 1;
219
+ while (parens > 0) {
220
+ beforeToFrom += " ";
221
+ beforeToFrom += token;
222
+ const r = tokenize(rest, "()", "\"'", true);
223
+ token = r.token;
224
+ rest = r.rest;
225
+ if (token === null)
226
+ return errAt(null);
227
+ if (token === "(")
228
+ parens++;
229
+ else if (token === ")")
230
+ parens--;
231
+ }
232
+ }
233
+ beforeToFrom += beforeToFrom.length > 0 ? " " : "";
234
+ beforeToFrom += token;
235
+ // Next token: schema-separator `.`, column-list opener `(`, or FROM/TO.
236
+ let r2 = tokenize(rest, ".,()", '"', false);
237
+ token = r2.token;
238
+ rest = r2.rest;
239
+ if (token === null)
240
+ return errAt(null);
241
+ // Schema-qualified `schema.table` — upstream just re-emits all three tokens.
242
+ if (token === ".") {
243
+ beforeToFrom += token;
244
+ r2 = tokenize(rest, ".,()", '"', false);
245
+ token = r2.token;
246
+ rest = r2.rest;
247
+ if (token === null)
248
+ return errAt(null);
249
+ beforeToFrom += token;
250
+ r2 = tokenize(rest, ".,()", '"', false);
251
+ token = r2.token;
252
+ rest = r2.rest;
253
+ if (token === null)
254
+ return errAt(null);
255
+ }
256
+ // Parenthesised column list `(col1, col2, …)`.
257
+ if (token === "(") {
258
+ for (;;) {
259
+ beforeToFrom += " ";
260
+ beforeToFrom += token;
261
+ const r = tokenize(rest, "()", '"', false);
262
+ token = r.token;
263
+ rest = r.rest;
264
+ if (token === null)
265
+ return errAt(null);
266
+ if (token === ")")
267
+ break;
268
+ }
269
+ beforeToFrom += " ";
270
+ beforeToFrom += token;
271
+ r2 = tokenize(rest, ".,()", '"', false);
272
+ token = r2.token;
273
+ rest = r2.rest;
274
+ if (token === null)
275
+ return errAt(null);
276
+ }
277
+ // FROM / TO keyword.
278
+ let direction;
279
+ if (token.toLowerCase() === "from") {
280
+ direction = "from";
281
+ }
282
+ else if (token.toLowerCase() === "to") {
283
+ direction = "to";
284
+ }
285
+ else {
286
+ return errAt(token);
287
+ }
288
+ // \copy (subquery) FROM is invalid — subqueries only make sense with TO.
289
+ if (isSubquery && direction === "from") {
290
+ return {
291
+ ok: false,
292
+ error: "cannot use COPY FROM with a (subquery) source",
293
+ };
294
+ }
295
+ // Filename / PROGRAM / STDIN / STDOUT / PSTDIN / PSTDOUT.
296
+ let r3 = tokenize(rest, ";", "'", false);
297
+ token = r3.token;
298
+ rest = r3.rest;
299
+ if (token === null)
300
+ return errAt(null);
301
+ let file = null;
302
+ let program = false;
303
+ let psqlInOut = false;
304
+ const lower = token.toLowerCase();
305
+ if (lower === "program") {
306
+ r3 = tokenize(rest, ";", "'", false);
307
+ token = r3.token;
308
+ rest = r3.rest;
309
+ if (token === null)
310
+ return errAt(null);
311
+ if (!token.startsWith("'") ||
312
+ !token.endsWith("'") ||
313
+ token.length < 2) {
314
+ return errAt(token);
315
+ }
316
+ file = stripSingleQuotes(token);
317
+ program = true;
318
+ }
319
+ else if (lower === "stdin" || lower === "stdout") {
320
+ file = null;
321
+ }
322
+ else if (lower === "pstdin" || lower === "pstdout") {
323
+ file = null;
324
+ psqlInOut = true;
325
+ }
326
+ else {
327
+ file = expandTilde(stripSingleQuotes(token));
328
+ }
329
+ // Collect the rest as the post-filename options blob (verbatim).
330
+ let afterToFrom = null;
331
+ rest = rest.trim();
332
+ if (rest.length > 0) {
333
+ afterToFrom = rest;
334
+ }
335
+ return {
336
+ ok: true,
337
+ value: {
338
+ beforeToFrom,
339
+ afterToFrom,
340
+ file,
341
+ program,
342
+ psqlInOut,
343
+ direction,
344
+ },
345
+ };
346
+ };
347
+ // ---------------------------------------------------------------------------
348
+ // do_copy
349
+ // ---------------------------------------------------------------------------
350
+ /**
351
+ * Build the SQL string sent to the backend. The server always sees
352
+ * `STDIN`/`STDOUT` here — client-side `'file'` / `PROGRAM 'cmd'` plumbing is
353
+ * invisible to the server because that's what frontend-driven COPY is for.
354
+ */
355
+ const buildCopySql = (opts) => {
356
+ const tail = opts.direction === "from" ? " FROM STDIN " : " TO STDOUT ";
357
+ const after = opts.afterToFrom !== null ? opts.afterToFrom : "";
358
+ return `COPY ${opts.beforeToFrom}${tail}${after}`.trimEnd();
359
+ };
360
+ /**
361
+ * Strip single-quoted SQL string literals from a fragment so a keyword scan
362
+ * over the result can't false-trigger on a payload character. Handles both
363
+ * the standard `'…''…'` form (doubled-quote escape) and the escape-string
364
+ * `E'…\…'` form (backslash-escape). Each match collapses to `''` so token
365
+ * boundaries around the literal are preserved.
366
+ *
367
+ * This is lenient on the `E` prefix recognition: we don't enforce that the
368
+ * `E` is unescaped (e.g. we'd also strip `xE'…'`). False-positive stripping
369
+ * is safe — we only ever miss a `csv` / `binary` / `format` mention that was
370
+ * intended as a data payload, which is exactly the case we want to skip.
371
+ */
372
+ const stripCopyOptionsStrings = (s) => {
373
+ return s.replace(/E'(?:\\.|[^'])*'|'(?:''|[^'])*'/g, "''");
374
+ };
375
+ /**
376
+ * Detect whether the COPY uses the (default) text format. Upstream psql only
377
+ * honours the `\.` end-of-data marker for text-format COPY; csv/binary treat
378
+ * the bytes as data.
379
+ *
380
+ * The check is a coarse keyword scan of the options string: if any of `csv`,
381
+ * `binary`, or `format <something>` appears (case-insensitive), we assume the
382
+ * user has explicitly selected a non-text format and disable EOF-marker
383
+ * handling. Quoted literals (including `E'…'` escape strings) are stripped
384
+ * first so a column-named "binary" or a `DELIMITER E'\\tbinary'` payload
385
+ * doesn't false-trigger.
386
+ *
387
+ * The `FORMAT` value itself may be optionally single-quoted in the new
388
+ * parenthesised-options syntax (e.g. `WITH (FORMAT 'csv')`); we accept either
389
+ * a bareword or a `'…'` literal there to match upstream's option grammar.
390
+ */
391
+ export const isCopyTextFormat = (afterToFrom) => {
392
+ if (afterToFrom === null)
393
+ return true;
394
+ // Strip quoted literals so `DELIMITER 'binary'` and `DELIMITER E'\\tcsv'`
395
+ // don't false-trigger the format-detection regexes below.
396
+ const stripped = stripCopyOptionsStrings(afterToFrom);
397
+ if (/\bcsv\b/i.test(stripped))
398
+ return false;
399
+ if (/\bbinary\b/i.test(stripped))
400
+ return false;
401
+ // The newer WITH (FORMAT <fmt>) form — if `format` appears followed by a
402
+ // non-text token, assume non-text. We don't try to parse the value because
403
+ // anything other than `text` is non-default; treat any FORMAT mention as
404
+ // "user said something explicit" and only allow the marker for `format text`.
405
+ // Match against the ORIGINAL string for the value extraction since the
406
+ // stripped form will have collapsed quoted values to `''`.
407
+ const m = /\bformat\s+(?:'([A-Za-z_]+)'|([A-Za-z_]+))/i.exec(afterToFrom);
408
+ if (m) {
409
+ return (m[1] ?? m[2]).toLowerCase() === "text";
410
+ }
411
+ return true;
412
+ };
413
+ /**
414
+ * Mirror of `isCopyTextFormat`'s scan, but returns `true` only when the COPY
415
+ * was explicitly opted into binary format. Used by the `\copy` driver to gate
416
+ * the BINARY-signature byte-for-byte transparency check (we don't want to
417
+ * touch text/csv streams).
418
+ *
419
+ * Matches `WITH BINARY`, `WITH (FORMAT binary)` (with or without quotes around
420
+ * the value), the legacy psql `BINARY t FROM …` keyword (which the parser
421
+ * folds into `beforeToFrom`), and mixed-case variants.
422
+ */
423
+ export const isCopyBinaryFormat = (beforeToFrom, afterToFrom) => {
424
+ // Legacy syntax: the BINARY keyword sits between `\copy` and the table name,
425
+ // which our parser preserves as the leading token of `beforeToFrom`.
426
+ if (/^\s*binary\b/i.test(beforeToFrom))
427
+ return true;
428
+ if (afterToFrom === null)
429
+ return false;
430
+ // Strip quoted literals (including `E'…'` escape strings) so a column-named
431
+ // `binary` or a payload literal doesn't trigger.
432
+ const stripped = stripCopyOptionsStrings(afterToFrom);
433
+ // Plain `WITH BINARY` (or the bare options token).
434
+ if (/(^|\W)binary(\W|$)/i.test(stripped)) {
435
+ // But only when it isn't part of a `format binary` form (already covered
436
+ // by the regex below — keep both paths so `WITH BINARY` alone still wins).
437
+ return true;
438
+ }
439
+ // FORMAT value may be optionally single-quoted in WITH (FORMAT 'binary').
440
+ const m = /\bformat\s+(?:'([A-Za-z_]+)'|([A-Za-z_]+))/i.exec(afterToFrom);
441
+ if (m) {
442
+ return (m[1] ?? m[2]).toLowerCase() === "binary";
443
+ }
444
+ return false;
445
+ };
446
+ /**
447
+ * PostgreSQL COPY binary-format file header signature.
448
+ *
449
+ * Per the docs[1]: every binary COPY stream begins with an 11-byte signature
450
+ * (`PGCOPY\n\xff\r\n\0`), followed by a 4-byte flags field and a 4-byte
451
+ * header-extension-area length. After that come zero-or-more tuples, then a
452
+ * 2-byte file trailer of `0xFFFF` (Int16 `-1`).
453
+ *
454
+ * We expose the signature bytes (not the full 19-byte fixed prefix) so callers
455
+ * can sniff incoming streams or assert outgoing streams without depending on
456
+ * server-version-specific flags / extension data.
457
+ *
458
+ * [1] https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4
459
+ */
460
+ export const COPY_BINARY_SIGNATURE = Buffer.from([
461
+ 0x50, 0x47, 0x43, 0x4f, 0x50, 0x59, 0x0a, 0xff, 0x0d, 0x0a, 0x00,
462
+ ]);
463
+ /**
464
+ * Validate that a buffer starts with the COPY binary signature.
465
+ *
466
+ * Used to assert round-trip transparency: bytes captured by `COPY ... TO
467
+ * STDOUT WITH BINARY` should be byte-for-byte acceptable to `COPY ... FROM
468
+ * STDIN WITH BINARY` on another instance. We don't try to parse tuples —
469
+ * that requires per-type binary decoders the printer doesn't otherwise need.
470
+ *
471
+ * Returns `null` on success or a short diagnostic string on failure (matching
472
+ * the upstream wording style: "missing signature" / "wrong signature").
473
+ */
474
+ export const validateCopyBinarySignature = (buf) => {
475
+ if (buf.length < COPY_BINARY_SIGNATURE.length) {
476
+ return "missing COPY binary signature (input too short)";
477
+ }
478
+ for (let i = 0; i < COPY_BINARY_SIGNATURE.length; i++) {
479
+ if (buf[i] !== COPY_BINARY_SIGNATURE[i]) {
480
+ return "COPY binary signature mismatch";
481
+ }
482
+ }
483
+ return null;
484
+ };
485
+ /**
486
+ * Parse a CommandComplete tag like `"COPY 17"` into its numeric row count.
487
+ * Returns `null` when the tag is unparseable; callers print it verbatim then.
488
+ */
489
+ const parseCopyTagRows = (tag) => {
490
+ if (tag === null)
491
+ return null;
492
+ const m = /^COPY (\d+)$/.exec(tag.trim());
493
+ if (!m)
494
+ return null;
495
+ return parseInt(m[1], 10);
496
+ };
497
+ const spawnProgram = (cmd, direction) => {
498
+ const child = spawn("sh", ["-c", cmd], {
499
+ stdio: [
500
+ direction === "to" ? "pipe" : "inherit",
501
+ direction === "from" ? "pipe" : "inherit",
502
+ "inherit",
503
+ ],
504
+ });
505
+ // Capture the program's terminal status so the caller can surface a nonzero
506
+ // exit / signal as a COPY failure rather than silently reporting success.
507
+ // `close` carries (code, signal); `error` fires when the spawn itself
508
+ // failed (e.g. sh missing).
509
+ const closed = new Promise((resolve) => {
510
+ child.once("close", (code, signal) => {
511
+ resolve({ code, signal, error: null });
512
+ });
513
+ child.once("error", (error) => {
514
+ resolve({ code: null, signal: null, error });
515
+ });
516
+ });
517
+ return {
518
+ child,
519
+ readable: direction === "from" ? child.stdout : null,
520
+ writable: direction === "to" ? child.stdin : null,
521
+ closed,
522
+ };
523
+ };
524
+ /**
525
+ * Turn a {@link ProgramExit} into psql-style diagnostic text, or `null` when
526
+ * the program succeeded (exit 0, no signal, no spawn error).
527
+ */
528
+ const describeProgramExit = (cmd, exit) => {
529
+ if (exit.error !== null) {
530
+ return `could not execute command "${cmd}": ${exit.error.message}`;
531
+ }
532
+ if (exit.signal !== null) {
533
+ return `program "${cmd}" was terminated by signal ${exit.signal}`;
534
+ }
535
+ if (exit.code !== null && exit.code !== 0) {
536
+ return `program "${cmd}" failed with exit code ${exit.code}`;
537
+ }
538
+ return null;
539
+ };
540
+ /**
541
+ * Drain a `CopyOutStream` (AsyncIterable<Buffer>) into a Node Writable. We
542
+ * await each write to honour backpressure. Mirrors upstream's `handleCopyOut`
543
+ * inner loop.
544
+ */
545
+ const drainCopyTo = async (conn, sql, out) => {
546
+ const copyOut = await conn.startCopyOut(sql);
547
+ for await (const chunk of copyOut) {
548
+ if (chunk.length === 0)
549
+ continue;
550
+ await new Promise((resolve, reject) => {
551
+ out.write(chunk, (err) => {
552
+ if (err !== null && err !== undefined)
553
+ reject(err);
554
+ else
555
+ resolve();
556
+ });
557
+ });
558
+ }
559
+ };
560
+ /**
561
+ * Pump a Readable into a CopyInStream, honouring the upstream `\.` text-mode
562
+ * EOF marker. A line consisting EXACTLY of `\.` (LF- or CRLF-terminated) ends
563
+ * the COPY via `copyIn.end()`; everything after the marker is left on the
564
+ * Readable for the caller (the REPL goes back to SQL mode and reads it as
565
+ * the next statement).
566
+ *
567
+ * The marker is detected by accumulating a tail buffer until we see a newline,
568
+ * then comparing the line to `\.`. We DO NOT mutate or strip data already
569
+ * flushed — once a chunk has been forwarded as CopyData, it's gone. The
570
+ * implementation reads chunks, splits on newlines, and forwards complete
571
+ * lines individually so the marker can short-circuit the stream cleanly.
572
+ *
573
+ * We DO NOT use `for await (const chunk of readable)` because Node destroys
574
+ * the underlying stream when the async-iterator wrapper exits (even cleanly
575
+ * via `break`), which would prevent the caller from resuming reads after the
576
+ * marker. Instead we drive the readable with explicit data/end event
577
+ * listeners, paused/resumed via `pause()`/`resume()`, and remove them once
578
+ * the marker fires — leaving the source intact for subsequent consumption.
579
+ *
580
+ * Returns true if the marker was hit (caller closed the stream), false on
581
+ * normal EOF.
582
+ */
583
+ const pumpStdinWithEofMarker = async (readable, copyIn) => {
584
+ return new Promise((resolve, reject) => {
585
+ let tail = Buffer.alloc(0);
586
+ let markerHit = false;
587
+ let settled = false;
588
+ /** In-flight `copyIn.write` chain; we serialize writes for backpressure. */
589
+ let writeChain = Promise.resolve();
590
+ const settle = (run) => {
591
+ if (settled)
592
+ return;
593
+ settled = true;
594
+ readable.removeListener("data", onData);
595
+ readable.removeListener("end", onEnd);
596
+ readable.removeListener("error", onError);
597
+ run().then(() => {
598
+ resolve(markerHit);
599
+ }, (err) => {
600
+ reject(err instanceof Error ? err : new Error(String(err)));
601
+ });
602
+ };
603
+ const writeBytes = (bytes) => {
604
+ if (bytes.length === 0)
605
+ return;
606
+ // Copy the slice: `subarray` views share memory with `tail`, which is
607
+ // reassigned (and replaced by Buffer.concat) as more chunks arrive. A
608
+ // copy keeps the queued write independent of that churn.
609
+ const owned = Buffer.from(bytes);
610
+ writeChain = writeChain.then(() => copyIn.write(owned));
611
+ };
612
+ const handleChunk = (chunk) => {
613
+ if (settled)
614
+ return;
615
+ // Operate in the BYTE domain — never decode to a JS string. A
616
+ // Buffer -> string -> Buffer round-trip mangles a multibyte char split
617
+ // across chunk boundaries and any non-UTF-8 client_encoding byte
618
+ // (LATIN1/SJIS) into U+FFFD. stdin yields Buffers;
619
+ // guard the rare string case without assuming a lossy re-encode.
620
+ const buf = Buffer.isBuffer(chunk)
621
+ ? chunk
622
+ : Buffer.from(chunk, "utf8");
623
+ tail = tail.length === 0 ? buf : Buffer.concat([tail, buf]);
624
+ let nl = tail.indexOf(0x0a); // '\n'
625
+ while (nl !== -1) {
626
+ const line = tail.subarray(0, nl + 1); // includes the trailing \n
627
+ // Match exactly `\.\n` or `\.\r\n` (0x5c 0x2e [0x0d] 0x0a). Upstream
628
+ // rejects trailing whitespace, so the line length must be exact.
629
+ const isMarker = (line.length === 3 &&
630
+ line[0] === 0x5c &&
631
+ line[1] === 0x2e &&
632
+ line[2] === 0x0a) ||
633
+ (line.length === 4 &&
634
+ line[0] === 0x5c &&
635
+ line[1] === 0x2e &&
636
+ line[2] === 0x0d &&
637
+ line[3] === 0x0a);
638
+ if (isMarker) {
639
+ markerHit = true;
640
+ const leftover = Buffer.from(tail.subarray(nl + 1)); // copy out
641
+ tail = Buffer.alloc(0);
642
+ // Pause + remove listeners BEFORE unshifting so the post-marker
643
+ // bytes aren't re-emitted into our own data handler.
644
+ readable.pause();
645
+ readable.removeListener("data", onData);
646
+ readable.removeListener("end", onEnd);
647
+ readable.removeListener("error", onError);
648
+ if (leftover.length > 0) {
649
+ readable.unshift(leftover);
650
+ }
651
+ settled = true;
652
+ writeChain
653
+ .then(() => copyIn.end())
654
+ .then(() => {
655
+ resolve(true);
656
+ }, (err) => {
657
+ reject(err instanceof Error
658
+ ? err
659
+ : new Error(String(err)));
660
+ });
661
+ return;
662
+ }
663
+ writeBytes(line);
664
+ tail = tail.subarray(nl + 1);
665
+ nl = tail.indexOf(0x0a);
666
+ }
667
+ };
668
+ const onData = (chunk) => {
669
+ try {
670
+ handleChunk(chunk);
671
+ }
672
+ catch (err) {
673
+ settle(async () => {
674
+ try {
675
+ await copyIn.fail(err instanceof Error ? err.message : String(err));
676
+ }
677
+ catch {
678
+ // best-effort
679
+ }
680
+ throw err instanceof Error ? err : new Error(String(err));
681
+ });
682
+ }
683
+ };
684
+ const onEnd = () => {
685
+ if (settled)
686
+ return;
687
+ const trailing = tail;
688
+ tail = Buffer.alloc(0);
689
+ settle(async () => {
690
+ if (trailing.length > 0) {
691
+ writeBytes(trailing);
692
+ }
693
+ await writeChain;
694
+ await copyIn.end();
695
+ });
696
+ };
697
+ const onError = (err) => {
698
+ if (settled)
699
+ return;
700
+ settle(async () => {
701
+ try {
702
+ await copyIn.fail(err.message);
703
+ }
704
+ catch {
705
+ // best-effort
706
+ }
707
+ throw err;
708
+ });
709
+ };
710
+ readable.on("data", onData);
711
+ readable.once("end", onEnd);
712
+ readable.once("error", onError);
713
+ // Trigger flowing mode in case the readable is paused.
714
+ readable.resume();
715
+ });
716
+ };
717
+ /**
718
+ * Execute a parsed `\copy`. Opens the file (or spawns the program), wires the
719
+ * stream into `startCopyIn` / `startCopyOut`, and returns the resulting
720
+ * CommandComplete tag (e.g. `"COPY 17"`) on success.
721
+ */
722
+ export const doCopy = async (conn, opts) => {
723
+ const sql = buildCopySql(opts);
724
+ // Helper to surface a uniform error shape. We deliberately keep the upstream
725
+ // wording for the common "could not execute command" / "<file>: <reason>"
726
+ // variants so tests / users that grep stderr keep working.
727
+ const failWith = (msg) => ({ ok: false, error: msg });
728
+ // Resolve file path / program command into a Readable/Writable.
729
+ let readable = null;
730
+ let writable = null;
731
+ let program = null;
732
+ // Captures an async write-stream error for the COPY TO <file> path. Without
733
+ // a listener, an open() failure (EACCES/ENOTDIR on an unwritable path) emits
734
+ // 'error' with nothing attached and aborts the whole process. An array (vs a
735
+ // nullable let) keeps the captured value visible to control-flow narrowing
736
+ // even though it's only assigned inside the async listener.
737
+ const fileWriteErrors = [];
738
+ /**
739
+ * True iff the data path is "psql stdin" — i.e. the user typed
740
+ * `\copy t FROM STDIN`. Only this path honours the `\.` text-mode EOF
741
+ * marker; file and PROGRAM sources stream verbatim to match upstream.
742
+ */
743
+ let fromStdin = false;
744
+ /** Cleanup callbacks run in `finally`. */
745
+ const cleanups = [];
746
+ if (opts.direction === "from") {
747
+ if (opts.file !== null) {
748
+ if (opts.program) {
749
+ try {
750
+ program = spawnProgram(opts.file, "from");
751
+ }
752
+ catch (err) {
753
+ return failWith(`could not execute command "${opts.file}": ${err instanceof Error ? err.message : String(err)}`);
754
+ }
755
+ readable = program.readable;
756
+ const p = program;
757
+ cleanups.push(async () => {
758
+ try {
759
+ p.child.stdout?.destroy();
760
+ }
761
+ catch {
762
+ // ignore
763
+ }
764
+ await p.closed;
765
+ });
766
+ }
767
+ else {
768
+ try {
769
+ // fstat the path to reject directories before we open a stream.
770
+ const stat = await fsPromises.stat(opts.file);
771
+ if (stat.isDirectory()) {
772
+ return failWith(`${opts.file}: cannot copy from/to a directory`);
773
+ }
774
+ }
775
+ catch (err) {
776
+ return failWith(`${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
777
+ }
778
+ const stream = createReadStream(opts.file);
779
+ readable = stream;
780
+ cleanups.push(() => new Promise((resolve) => {
781
+ if (stream.destroyed) {
782
+ resolve();
783
+ return;
784
+ }
785
+ stream.once("close", () => {
786
+ resolve();
787
+ });
788
+ stream.destroy();
789
+ }));
790
+ }
791
+ }
792
+ else {
793
+ // STDIN form — read from process.stdin. We don't differentiate
794
+ // PSTDIN/STDIN here (see file header limitations).
795
+ readable = process.stdin;
796
+ fromStdin = true;
797
+ }
798
+ }
799
+ else {
800
+ // direction === 'to'
801
+ if (opts.file !== null) {
802
+ if (opts.program) {
803
+ try {
804
+ program = spawnProgram(opts.file, "to");
805
+ }
806
+ catch (err) {
807
+ return failWith(`could not execute command "${opts.file}": ${err instanceof Error ? err.message : String(err)}`);
808
+ }
809
+ writable = program.writable;
810
+ const p = program;
811
+ cleanups.push(async () => {
812
+ try {
813
+ p.child.stdin?.end();
814
+ }
815
+ catch {
816
+ // ignore
817
+ }
818
+ await p.closed;
819
+ });
820
+ }
821
+ else {
822
+ try {
823
+ // Reject if the path exists and is a directory.
824
+ const stat = await fsPromises
825
+ .stat(opts.file)
826
+ .catch(() => null);
827
+ if (stat?.isDirectory()) {
828
+ return failWith(`${opts.file}: cannot copy from/to a directory`);
829
+ }
830
+ }
831
+ catch {
832
+ // ENOENT is fine for write — createWriteStream will create it.
833
+ }
834
+ const stream = createWriteStream(opts.file);
835
+ // Trap the async open/write error synchronously so it can't crash the
836
+ // process; surfaced as a COPY failure after the drive.
837
+ stream.once("error", (e) => {
838
+ fileWriteErrors.push(e);
839
+ });
840
+ writable = stream;
841
+ cleanups.push(() => new Promise((resolve, reject) => {
842
+ stream.end((err) => {
843
+ if (err)
844
+ reject(err);
845
+ else
846
+ resolve();
847
+ });
848
+ }));
849
+ }
850
+ }
851
+ else {
852
+ // STDOUT form. Cast through unknown because process.stdout's `Writable`
853
+ // type isn't strictly compatible with the generic interface.
854
+ writable = process.stdout;
855
+ }
856
+ }
857
+ // Drive the COPY.
858
+ let tag = null;
859
+ try {
860
+ if (opts.direction === "from") {
861
+ if (readable === null) {
862
+ return failWith("no input stream for COPY FROM");
863
+ }
864
+ const copyIn = await conn.startCopyIn(sql);
865
+ // STDIN honours the `\.` EOF marker for BOTH text and CSV (psql treats
866
+ // `\.` on its own line as end-of-data in either) — only binary STDIN and
867
+ // file/PROGRAM sources stream bytes verbatim. Gating on text-only made a
868
+ // CSV `\copy … FROM STDIN` swallow the `\.` line as a data row and the
869
+ // following SQL into the copy stream.
870
+ if (fromStdin &&
871
+ !isCopyBinaryFormat(opts.beforeToFrom, opts.afterToFrom)) {
872
+ await pumpStdinWithEofMarker(readable, copyIn);
873
+ }
874
+ else {
875
+ await pumpReadable(conn, readable, copyIn);
876
+ }
877
+ }
878
+ else {
879
+ if (writable === null) {
880
+ return failWith("no output stream for COPY TO");
881
+ }
882
+ await drainCopyTo(conn, sql, writable);
883
+ // A deferred open()/write() failure on the output file: report it as a
884
+ // COPY failure instead of letting the unhandled 'error' abort the
885
+ // process. (`finally` below still runs the stream cleanups.)
886
+ if (fileWriteErrors.length > 0)
887
+ return failWith(fileWriteErrors[0].message);
888
+ }
889
+ // For `PROGRAM '...'` sources/sinks, wait for the child to exit and fold a
890
+ // nonzero exit / signal / spawn error into the COPY result. Without this a
891
+ // failing program (e.g. `\copy t TO PROGRAM 'false'`) reported success.
892
+ // Close the program's stdin first so a TO PROGRAM child that
893
+ // reads to EOF can finish.
894
+ if (program !== null) {
895
+ program.writable?.end();
896
+ const exit = await program.closed;
897
+ // A program is only spawned when opts.file holds the command string.
898
+ const progErr = describeProgramExit(opts.file ?? "", exit);
899
+ if (progErr !== null)
900
+ throw new Error(progErr);
901
+ }
902
+ // The connection records the trailing CommandComplete tag for us. We
903
+ // narrow via a duck-type check so we don't tighten the Connection type.
904
+ tag = readLastCopyTag(conn);
905
+ }
906
+ catch (err) {
907
+ return failWith(err instanceof Error ? err.message : String(err));
908
+ }
909
+ finally {
910
+ for (const c of cleanups) {
911
+ try {
912
+ await c();
913
+ }
914
+ catch {
915
+ // best-effort cleanup
916
+ }
917
+ }
918
+ }
919
+ return { ok: true, tag };
920
+ };
921
+ /**
922
+ * Read the connection's `lastCopyTag` if the implementation exposes it.
923
+ * PgConnection sets this property after each COPY; mock connections in tests
924
+ * may not, in which case we return null and the caller prints just `COPY`.
925
+ */
926
+ const readLastCopyTag = (conn) => {
927
+ const maybe = conn.lastCopyTag;
928
+ if (typeof maybe === "string")
929
+ return maybe;
930
+ return null;
931
+ };
932
+ // ---------------------------------------------------------------------------
933
+ // Backslash command registration
934
+ // ---------------------------------------------------------------------------
935
+ /**
936
+ * `\copy` command spec. Mirrors upstream's `exec_command_a_or_copy` path
937
+ * (well, just the copy half). On success we print the trailing `COPY <N>`
938
+ * footer to stdout, matching `do_copy`'s expectation that SendQuery's normal
939
+ * result-printing pipeline emits the tag.
940
+ */
941
+ export const cmdCopy = {
942
+ name: "copy",
943
+ helpKey: "copy",
944
+ async run(ctx) {
945
+ if (!ctx.settings.db) {
946
+ ctx.settings.lastErrorResult = {
947
+ message: "no connection to the server",
948
+ };
949
+ writeErr("\\copy: no connection to the server\n");
950
+ return { status: "error" };
951
+ }
952
+ // COPY is not supported inside a \startpipeline ... \endpipeline block:
953
+ // upstream libpq aborts the connection with this exact diagnostic and
954
+ // psql exits non-zero. Detect at the command layer so we don't even
955
+ // send the Query — that lets us short-circuit before the protocol
956
+ // switches into the COPY data phase (which would otherwise hang).
957
+ //
958
+ // We close (but do NOT null) the connection on `ctx.settings.db` so the
959
+ // mainloop's `checkConnectionLost` polls `db.isClosed()` and surfaces
960
+ // the standard "connection to server was lost" diagnostic + EXIT_BADCONN.
961
+ // That matches libpq's "aborting connection" promise — the script halts
962
+ // after this command rather than appearing to recover.
963
+ if (getPipelineState(ctx.settings) !== null) {
964
+ ctx.settings.lastErrorResult = { message: COPY_IN_PIPELINE_MSG };
965
+ writeErr(`\\copy: ${COPY_IN_PIPELINE_MSG}\n`);
966
+ try {
967
+ await ctx.settings.db.close();
968
+ }
969
+ catch {
970
+ // best-effort; the connection may already be dead
971
+ }
972
+ return { status: "error" };
973
+ }
974
+ const raw = ctx.restOfLine();
975
+ if (raw.trim().length === 0) {
976
+ ctx.settings.lastErrorResult = { message: "arguments required" };
977
+ writeErr("\\copy: arguments required\n");
978
+ return { status: "error" };
979
+ }
980
+ const parsed = parseSlashCopy(raw);
981
+ if (!parsed.ok) {
982
+ ctx.settings.lastErrorResult = { message: parsed.error };
983
+ writeErr(`\\copy: ${parsed.error}\n`);
984
+ return { status: "error" };
985
+ }
986
+ const result = await doCopy(ctx.settings.db, parsed.value);
987
+ if (!result.ok) {
988
+ ctx.settings.lastErrorResult = { message: result.error };
989
+ writeErr(`\\copy: ${result.error}\n`);
990
+ return { status: "error" };
991
+ }
992
+ // Print the upstream-style command tag (e.g. "COPY 17") so users see the
993
+ // same summary as `psql`. If the connection didn't surface a tag, just
994
+ // print `COPY` — the operation still succeeded.
995
+ //
996
+ // BUT: when the COPY destination is psql's own stdout (i.e. `\copy ...
997
+ // TO STDOUT` / `TO PSTDOUT`), emitting the tag would mix it into the
998
+ // user's data stream. Upstream `do_copy()` suppresses the tag in this
999
+ // case — `pset.queryFout` is shared between the data stream and the tag
1000
+ // print path, so the tag has nowhere to land. Mirror that here: only
1001
+ // print when the destination is a file, a program, or when the COPY is
1002
+ // a FROM (where the data flowed *into* the server, not out to stdout).
1003
+ const suppressTag = parsed.value.direction === "to" &&
1004
+ parsed.value.file === null &&
1005
+ !parsed.value.program;
1006
+ if (!suppressTag) {
1007
+ const rows = parseCopyTagRows(result.tag);
1008
+ if (result.tag !== null && rows !== null) {
1009
+ writeOut(`COPY ${String(rows)}\n`);
1010
+ }
1011
+ else if (result.tag !== null) {
1012
+ writeOut(`${result.tag}\n`);
1013
+ }
1014
+ else {
1015
+ writeOut("COPY\n");
1016
+ }
1017
+ }
1018
+ return { status: "ok" };
1019
+ },
1020
+ };
1021
+ /**
1022
+ * Register the `\copy` command on the supplied registry. Called from
1023
+ * `dispatch.ts::defaultRegistry()` (one new line).
1024
+ */
1025
+ export const registerCopyCommands = (registry) => {
1026
+ registry.register(cmdCopy);
1027
+ };
1028
+ // Re-export for direct callers that want to bypass the dispatcher (tests).
1029
+ export { buildCopySql, pumpStdinWithEofMarker };
1030
+ /**
1031
+ * Convenience: encode a JS string as UTF-8 bytes for COPY FROM. Exposed so
1032
+ * tests can feed a `Buffer` to {@link doCopy} without re-implementing the
1033
+ * Readable shim.
1034
+ */
1035
+ export const toBuffer = (s) => Buffer.from(s, "utf8");