neonctl 2.28.0 → 2.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -71
- package/dist/analytics.js +35 -33
- package/dist/api.js +34 -34
- package/dist/auth.js +50 -44
- package/dist/cli.js +2 -2
- package/dist/commands/auth.js +58 -52
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +154 -147
- package/dist/commands/bucket.js +124 -118
- package/dist/commands/checkout.js +49 -49
- package/dist/commands/config.js +212 -88
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +96 -96
- package/dist/commands/databases.js +23 -23
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/dev.js +114 -114
- package/dist/commands/env.js +43 -43
- package/dist/commands/functions.js +97 -98
- package/dist/commands/index.js +26 -26
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +223 -166
- package/dist/commands/neon_auth.js +381 -363
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +101 -99
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +21 -21
- package/dist/commands/schema_diff.js +23 -23
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +17 -17
- package/dist/commands/user.js +5 -5
- package/dist/commands/vpc_endpoints.js +50 -50
- package/dist/config.js +7 -7
- package/dist/config_format.js +5 -5
- package/dist/context.js +23 -16
- package/dist/current_branch_fast_path.js +6 -6
- package/dist/dev/env.js +34 -34
- package/dist/dev/functions.js +4 -4
- package/dist/dev/inputs.js +6 -6
- package/dist/dev/runtime.js +25 -25
- package/dist/env.js +14 -14
- package/dist/env_file.js +13 -13
- package/dist/errors.js +19 -19
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +94 -92
- package/dist/log.js +2 -2
- package/dist/pkg.js +5 -5
- package/dist/psql/cli.js +4 -2
- package/dist/psql/command/cmd_cond.js +61 -61
- package/dist/psql/command/cmd_connect.js +159 -154
- package/dist/psql/command/cmd_copy.js +107 -97
- package/dist/psql/command/cmd_describe.js +368 -363
- package/dist/psql/command/cmd_format.js +276 -263
- package/dist/psql/command/cmd_io.js +269 -263
- package/dist/psql/command/cmd_lo.js +74 -66
- package/dist/psql/command/cmd_meta.js +148 -148
- package/dist/psql/command/cmd_misc.js +17 -17
- package/dist/psql/command/cmd_pipeline.js +142 -135
- package/dist/psql/command/cmd_restrict.js +25 -25
- package/dist/psql/command/cmd_show.js +183 -168
- package/dist/psql/command/dispatch.js +26 -26
- package/dist/psql/command/shared.js +14 -14
- package/dist/psql/complete/filenames.js +16 -16
- package/dist/psql/complete/index.js +4 -4
- package/dist/psql/complete/matcher.js +33 -32
- package/dist/psql/complete/psqlVars.js +173 -173
- package/dist/psql/complete/queries.js +5 -3
- package/dist/psql/complete/rules.js +900 -863
- package/dist/psql/core/common.js +136 -133
- package/dist/psql/core/help.js +343 -343
- package/dist/psql/core/mainloop.js +160 -153
- package/dist/psql/core/prompt.js +126 -123
- package/dist/psql/core/settings.js +111 -111
- package/dist/psql/core/sqlHelp.js +150 -150
- package/dist/psql/core/startup.js +211 -205
- package/dist/psql/core/syncVars.js +14 -14
- package/dist/psql/core/variables.js +24 -24
- package/dist/psql/describe/formatters.js +302 -289
- package/dist/psql/describe/processNamePattern.js +28 -28
- package/dist/psql/describe/queries.js +656 -651
- package/dist/psql/index.js +436 -411
- package/dist/psql/io/history.js +36 -36
- package/dist/psql/io/input.js +15 -15
- package/dist/psql/io/lineEditor/buffer.js +27 -25
- package/dist/psql/io/lineEditor/complete.js +15 -15
- package/dist/psql/io/lineEditor/filename.js +22 -22
- package/dist/psql/io/lineEditor/index.js +65 -62
- package/dist/psql/io/lineEditor/keymap.js +325 -318
- package/dist/psql/io/lineEditor/vt100.js +60 -60
- package/dist/psql/io/pgpass.js +18 -18
- package/dist/psql/io/pgservice.js +14 -14
- package/dist/psql/io/psqlrc.js +46 -46
- package/dist/psql/print/aligned.js +175 -166
- package/dist/psql/print/asciidoc.js +51 -51
- package/dist/psql/print/crosstab.js +34 -31
- package/dist/psql/print/csv.js +25 -22
- package/dist/psql/print/html.js +54 -54
- package/dist/psql/print/json.js +12 -12
- package/dist/psql/print/latex.js +118 -118
- package/dist/psql/print/pager.js +28 -26
- package/dist/psql/print/troff.js +48 -48
- package/dist/psql/print/unaligned.js +15 -14
- package/dist/psql/print/units.js +17 -17
- package/dist/psql/scanner/slash.js +48 -46
- package/dist/psql/scanner/sql.js +88 -84
- package/dist/psql/scanner/stringutils.js +21 -17
- package/dist/psql/types/index.js +7 -7
- package/dist/psql/types/scanner.js +8 -8
- package/dist/psql/wire/connection.js +341 -327
- package/dist/psql/wire/copy.js +7 -7
- package/dist/psql/wire/pipeline.js +26 -24
- package/dist/psql/wire/protocol.js +102 -102
- package/dist/psql/wire/sasl.js +62 -62
- package/dist/psql/wire/tls.js +79 -73
- package/dist/storage_api.js +15 -15
- package/dist/test_utils/fixtures.js +34 -31
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +13 -13
- package/dist/utils/branch_notice.js +5 -5
- package/dist/utils/branch_picker.js +26 -26
- package/dist/utils/compute_units.js +4 -4
- package/dist/utils/enrichers.js +20 -15
- package/dist/utils/esbuild.js +28 -28
- package/dist/utils/formats.js +1 -1
- package/dist/utils/middlewares.js +3 -3
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +12 -12
- package/dist/utils/psql.js +30 -30
- package/dist/utils/string.js +2 -2
- package/dist/utils/ui.js +9 -9
- package/dist/utils/zip.js +1 -1
- package/dist/writer.js +17 -17
- package/package.json +6 -7
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
* - PSTDIN/PSTDOUT are treated as STDIN/STDOUT (no separate "psql stdin
|
|
45
45
|
* vs current input source" distinction — REPL plumbing isn't wired yet).
|
|
46
46
|
*/
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import { pumpReadable } from
|
|
51
|
-
import { getPipelineState } from
|
|
52
|
-
import { writeErr, writeOut } from
|
|
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
53
|
/**
|
|
54
54
|
* Diagnostic emitted when the user tries to run `\copy` (or a raw COPY
|
|
55
55
|
* statement) inside an active `\startpipeline` ... `\endpipeline` block.
|
|
@@ -58,8 +58,8 @@ import { writeErr, writeOut } from './shared.js';
|
|
|
58
58
|
* the wire-layer abort path can reuse the same string and tests can match
|
|
59
59
|
* via a single source of truth.
|
|
60
60
|
*/
|
|
61
|
-
export const COPY_IN_PIPELINE_MSG =
|
|
62
|
-
const WHITESPACE =
|
|
61
|
+
export const COPY_IN_PIPELINE_MSG = "COPY in a pipeline is not supported, aborting connection";
|
|
62
|
+
const WHITESPACE = " \t\n\r";
|
|
63
63
|
/**
|
|
64
64
|
* Tokenise the next term of the `\copy` tail. Mirrors upstream's `strtokx`
|
|
65
65
|
* call sites: each call passes a different combination of (delim chars,
|
|
@@ -83,7 +83,7 @@ const tokenize = (input, delim, quote, doubleQuoteEscape) => {
|
|
|
83
83
|
while (i < n && WHITESPACE.includes(input[i]))
|
|
84
84
|
i++;
|
|
85
85
|
if (i >= n)
|
|
86
|
-
return { token: null, rest:
|
|
86
|
+
return { token: null, rest: "" };
|
|
87
87
|
// 2. Delimiter character returned as single-char token.
|
|
88
88
|
if (delim.length > 0 && delim.includes(input[i])) {
|
|
89
89
|
const token = input[i];
|
|
@@ -101,7 +101,7 @@ const tokenize = (input, delim, quote, doubleQuoteEscape) => {
|
|
|
101
101
|
i++;
|
|
102
102
|
while (i < n) {
|
|
103
103
|
const c = input[i];
|
|
104
|
-
if (doubleQuoteEscape && c ===
|
|
104
|
+
if (doubleQuoteEscape && c === "\\" && i + 1 < n) {
|
|
105
105
|
i += 2;
|
|
106
106
|
continue;
|
|
107
107
|
}
|
|
@@ -146,7 +146,7 @@ const stripSingleQuotes = (token) => {
|
|
|
146
146
|
if (token.length < 2 || !token.startsWith("'") || !token.endsWith("'")) {
|
|
147
147
|
return token;
|
|
148
148
|
}
|
|
149
|
-
let out =
|
|
149
|
+
let out = "";
|
|
150
150
|
let i = 1;
|
|
151
151
|
const end = token.length - 1;
|
|
152
152
|
while (i < end) {
|
|
@@ -167,9 +167,9 @@ const stripSingleQuotes = (token) => {
|
|
|
167
167
|
* Node doesn't expose `getpwnam` cleanly).
|
|
168
168
|
*/
|
|
169
169
|
const expandTilde = (filePath) => {
|
|
170
|
-
if (!filePath.startsWith(
|
|
170
|
+
if (!filePath.startsWith("~"))
|
|
171
171
|
return filePath;
|
|
172
|
-
if (filePath ===
|
|
172
|
+
if (filePath === "~" || filePath.startsWith("~/")) {
|
|
173
173
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
174
174
|
if (home === undefined)
|
|
175
175
|
return filePath;
|
|
@@ -186,7 +186,7 @@ const expandTilde = (filePath) => {
|
|
|
186
186
|
* by the dispatcher's `BackslashContext.rawArgs`).
|
|
187
187
|
*/
|
|
188
188
|
export const parseSlashCopy = (input) => {
|
|
189
|
-
let beforeToFrom =
|
|
189
|
+
let beforeToFrom = "";
|
|
190
190
|
let rest = input;
|
|
191
191
|
let token;
|
|
192
192
|
// Helper to keep the failure messages consistent with upstream.
|
|
@@ -194,18 +194,18 @@ export const parseSlashCopy = (input) => {
|
|
|
194
194
|
ok: false,
|
|
195
195
|
error: tok !== null && tok.length > 0
|
|
196
196
|
? `parse error at "${tok}"`
|
|
197
|
-
:
|
|
197
|
+
: "parse error at end of line",
|
|
198
198
|
});
|
|
199
199
|
// First token: optional BINARY, or table-name / "(" for subquery.
|
|
200
|
-
let r1 = tokenize(rest,
|
|
200
|
+
let r1 = tokenize(rest, ".,()", '"', false);
|
|
201
201
|
token = r1.token;
|
|
202
202
|
rest = r1.rest;
|
|
203
203
|
if (token === null)
|
|
204
204
|
return errAt(null);
|
|
205
205
|
// Optional legacy BINARY keyword (pre-7.3 syntax). Re-emit then read next.
|
|
206
|
-
if (token.toLowerCase() ===
|
|
206
|
+
if (token.toLowerCase() === "binary") {
|
|
207
207
|
beforeToFrom += token;
|
|
208
|
-
r1 = tokenize(rest,
|
|
208
|
+
r1 = tokenize(rest, ".,()", '"', false);
|
|
209
209
|
token = r1.token;
|
|
210
210
|
rest = r1.rest;
|
|
211
211
|
if (token === null)
|
|
@@ -213,62 +213,62 @@ export const parseSlashCopy = (input) => {
|
|
|
213
213
|
}
|
|
214
214
|
// `(query)` subquery form? Re-emit balanced-paren contents verbatim.
|
|
215
215
|
let isSubquery = false;
|
|
216
|
-
if (token ===
|
|
216
|
+
if (token === "(") {
|
|
217
217
|
isSubquery = true;
|
|
218
218
|
let parens = 1;
|
|
219
219
|
while (parens > 0) {
|
|
220
|
-
beforeToFrom +=
|
|
220
|
+
beforeToFrom += " ";
|
|
221
221
|
beforeToFrom += token;
|
|
222
|
-
const r = tokenize(rest,
|
|
222
|
+
const r = tokenize(rest, "()", "\"'", true);
|
|
223
223
|
token = r.token;
|
|
224
224
|
rest = r.rest;
|
|
225
225
|
if (token === null)
|
|
226
226
|
return errAt(null);
|
|
227
|
-
if (token ===
|
|
227
|
+
if (token === "(")
|
|
228
228
|
parens++;
|
|
229
|
-
else if (token ===
|
|
229
|
+
else if (token === ")")
|
|
230
230
|
parens--;
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
beforeToFrom += beforeToFrom.length > 0 ?
|
|
233
|
+
beforeToFrom += beforeToFrom.length > 0 ? " " : "";
|
|
234
234
|
beforeToFrom += token;
|
|
235
235
|
// Next token: schema-separator `.`, column-list opener `(`, or FROM/TO.
|
|
236
|
-
let r2 = tokenize(rest,
|
|
236
|
+
let r2 = tokenize(rest, ".,()", '"', false);
|
|
237
237
|
token = r2.token;
|
|
238
238
|
rest = r2.rest;
|
|
239
239
|
if (token === null)
|
|
240
240
|
return errAt(null);
|
|
241
241
|
// Schema-qualified `schema.table` — upstream just re-emits all three tokens.
|
|
242
|
-
if (token ===
|
|
242
|
+
if (token === ".") {
|
|
243
243
|
beforeToFrom += token;
|
|
244
|
-
r2 = tokenize(rest,
|
|
244
|
+
r2 = tokenize(rest, ".,()", '"', false);
|
|
245
245
|
token = r2.token;
|
|
246
246
|
rest = r2.rest;
|
|
247
247
|
if (token === null)
|
|
248
248
|
return errAt(null);
|
|
249
249
|
beforeToFrom += token;
|
|
250
|
-
r2 = tokenize(rest,
|
|
250
|
+
r2 = tokenize(rest, ".,()", '"', false);
|
|
251
251
|
token = r2.token;
|
|
252
252
|
rest = r2.rest;
|
|
253
253
|
if (token === null)
|
|
254
254
|
return errAt(null);
|
|
255
255
|
}
|
|
256
256
|
// Parenthesised column list `(col1, col2, …)`.
|
|
257
|
-
if (token ===
|
|
257
|
+
if (token === "(") {
|
|
258
258
|
for (;;) {
|
|
259
|
-
beforeToFrom +=
|
|
259
|
+
beforeToFrom += " ";
|
|
260
260
|
beforeToFrom += token;
|
|
261
|
-
const r = tokenize(rest,
|
|
261
|
+
const r = tokenize(rest, "()", '"', false);
|
|
262
262
|
token = r.token;
|
|
263
263
|
rest = r.rest;
|
|
264
264
|
if (token === null)
|
|
265
265
|
return errAt(null);
|
|
266
|
-
if (token ===
|
|
266
|
+
if (token === ")")
|
|
267
267
|
break;
|
|
268
268
|
}
|
|
269
|
-
beforeToFrom +=
|
|
269
|
+
beforeToFrom += " ";
|
|
270
270
|
beforeToFrom += token;
|
|
271
|
-
r2 = tokenize(rest,
|
|
271
|
+
r2 = tokenize(rest, ".,()", '"', false);
|
|
272
272
|
token = r2.token;
|
|
273
273
|
rest = r2.rest;
|
|
274
274
|
if (token === null)
|
|
@@ -276,24 +276,24 @@ export const parseSlashCopy = (input) => {
|
|
|
276
276
|
}
|
|
277
277
|
// FROM / TO keyword.
|
|
278
278
|
let direction;
|
|
279
|
-
if (token.toLowerCase() ===
|
|
280
|
-
direction =
|
|
279
|
+
if (token.toLowerCase() === "from") {
|
|
280
|
+
direction = "from";
|
|
281
281
|
}
|
|
282
|
-
else if (token.toLowerCase() ===
|
|
283
|
-
direction =
|
|
282
|
+
else if (token.toLowerCase() === "to") {
|
|
283
|
+
direction = "to";
|
|
284
284
|
}
|
|
285
285
|
else {
|
|
286
286
|
return errAt(token);
|
|
287
287
|
}
|
|
288
288
|
// \copy (subquery) FROM is invalid — subqueries only make sense with TO.
|
|
289
|
-
if (isSubquery && direction ===
|
|
289
|
+
if (isSubquery && direction === "from") {
|
|
290
290
|
return {
|
|
291
291
|
ok: false,
|
|
292
|
-
error:
|
|
292
|
+
error: "cannot use COPY FROM with a (subquery) source",
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
// Filename / PROGRAM / STDIN / STDOUT / PSTDIN / PSTDOUT.
|
|
296
|
-
let r3 = tokenize(rest,
|
|
296
|
+
let r3 = tokenize(rest, ";", "'", false);
|
|
297
297
|
token = r3.token;
|
|
298
298
|
rest = r3.rest;
|
|
299
299
|
if (token === null)
|
|
@@ -302,22 +302,24 @@ export const parseSlashCopy = (input) => {
|
|
|
302
302
|
let program = false;
|
|
303
303
|
let psqlInOut = false;
|
|
304
304
|
const lower = token.toLowerCase();
|
|
305
|
-
if (lower ===
|
|
306
|
-
r3 = tokenize(rest,
|
|
305
|
+
if (lower === "program") {
|
|
306
|
+
r3 = tokenize(rest, ";", "'", false);
|
|
307
307
|
token = r3.token;
|
|
308
308
|
rest = r3.rest;
|
|
309
309
|
if (token === null)
|
|
310
310
|
return errAt(null);
|
|
311
|
-
if (!token.startsWith("'") ||
|
|
311
|
+
if (!token.startsWith("'") ||
|
|
312
|
+
!token.endsWith("'") ||
|
|
313
|
+
token.length < 2) {
|
|
312
314
|
return errAt(token);
|
|
313
315
|
}
|
|
314
316
|
file = stripSingleQuotes(token);
|
|
315
317
|
program = true;
|
|
316
318
|
}
|
|
317
|
-
else if (lower ===
|
|
319
|
+
else if (lower === "stdin" || lower === "stdout") {
|
|
318
320
|
file = null;
|
|
319
321
|
}
|
|
320
|
-
else if (lower ===
|
|
322
|
+
else if (lower === "pstdin" || lower === "pstdout") {
|
|
321
323
|
file = null;
|
|
322
324
|
psqlInOut = true;
|
|
323
325
|
}
|
|
@@ -351,8 +353,8 @@ export const parseSlashCopy = (input) => {
|
|
|
351
353
|
* invisible to the server because that's what frontend-driven COPY is for.
|
|
352
354
|
*/
|
|
353
355
|
const buildCopySql = (opts) => {
|
|
354
|
-
const tail = opts.direction ===
|
|
355
|
-
const after = opts.afterToFrom !== null ? opts.afterToFrom :
|
|
356
|
+
const tail = opts.direction === "from" ? " FROM STDIN " : " TO STDOUT ";
|
|
357
|
+
const after = opts.afterToFrom !== null ? opts.afterToFrom : "";
|
|
356
358
|
return `COPY ${opts.beforeToFrom}${tail}${after}`.trimEnd();
|
|
357
359
|
};
|
|
358
360
|
/**
|
|
@@ -404,7 +406,7 @@ export const isCopyTextFormat = (afterToFrom) => {
|
|
|
404
406
|
// stripped form will have collapsed quoted values to `''`.
|
|
405
407
|
const m = /\bformat\s+(?:'([A-Za-z_]+)'|([A-Za-z_]+))/i.exec(afterToFrom);
|
|
406
408
|
if (m) {
|
|
407
|
-
return (m[1] ?? m[2]).toLowerCase() ===
|
|
409
|
+
return (m[1] ?? m[2]).toLowerCase() === "text";
|
|
408
410
|
}
|
|
409
411
|
return true;
|
|
410
412
|
};
|
|
@@ -437,7 +439,7 @@ export const isCopyBinaryFormat = (beforeToFrom, afterToFrom) => {
|
|
|
437
439
|
// FORMAT value may be optionally single-quoted in WITH (FORMAT 'binary').
|
|
438
440
|
const m = /\bformat\s+(?:'([A-Za-z_]+)'|([A-Za-z_]+))/i.exec(afterToFrom);
|
|
439
441
|
if (m) {
|
|
440
|
-
return (m[1] ?? m[2]).toLowerCase() ===
|
|
442
|
+
return (m[1] ?? m[2]).toLowerCase() === "binary";
|
|
441
443
|
}
|
|
442
444
|
return false;
|
|
443
445
|
};
|
|
@@ -471,11 +473,11 @@ export const COPY_BINARY_SIGNATURE = Buffer.from([
|
|
|
471
473
|
*/
|
|
472
474
|
export const validateCopyBinarySignature = (buf) => {
|
|
473
475
|
if (buf.length < COPY_BINARY_SIGNATURE.length) {
|
|
474
|
-
return
|
|
476
|
+
return "missing COPY binary signature (input too short)";
|
|
475
477
|
}
|
|
476
478
|
for (let i = 0; i < COPY_BINARY_SIGNATURE.length; i++) {
|
|
477
479
|
if (buf[i] !== COPY_BINARY_SIGNATURE[i]) {
|
|
478
|
-
return
|
|
480
|
+
return "COPY binary signature mismatch";
|
|
479
481
|
}
|
|
480
482
|
}
|
|
481
483
|
return null;
|
|
@@ -493,11 +495,11 @@ const parseCopyTagRows = (tag) => {
|
|
|
493
495
|
return parseInt(m[1], 10);
|
|
494
496
|
};
|
|
495
497
|
const spawnProgram = (cmd, direction) => {
|
|
496
|
-
const child = spawn(
|
|
498
|
+
const child = spawn("sh", ["-c", cmd], {
|
|
497
499
|
stdio: [
|
|
498
|
-
direction ===
|
|
499
|
-
direction ===
|
|
500
|
-
|
|
500
|
+
direction === "to" ? "pipe" : "inherit",
|
|
501
|
+
direction === "from" ? "pipe" : "inherit",
|
|
502
|
+
"inherit",
|
|
501
503
|
],
|
|
502
504
|
});
|
|
503
505
|
// Capture the program's terminal status so the caller can surface a nonzero
|
|
@@ -505,17 +507,17 @@ const spawnProgram = (cmd, direction) => {
|
|
|
505
507
|
// `close` carries (code, signal); `error` fires when the spawn itself
|
|
506
508
|
// failed (e.g. sh missing).
|
|
507
509
|
const closed = new Promise((resolve) => {
|
|
508
|
-
child.once(
|
|
510
|
+
child.once("close", (code, signal) => {
|
|
509
511
|
resolve({ code, signal, error: null });
|
|
510
512
|
});
|
|
511
|
-
child.once(
|
|
513
|
+
child.once("error", (error) => {
|
|
512
514
|
resolve({ code: null, signal: null, error });
|
|
513
515
|
});
|
|
514
516
|
});
|
|
515
517
|
return {
|
|
516
518
|
child,
|
|
517
|
-
readable: direction ===
|
|
518
|
-
writable: direction ===
|
|
519
|
+
readable: direction === "from" ? child.stdout : null,
|
|
520
|
+
writable: direction === "to" ? child.stdin : null,
|
|
519
521
|
closed,
|
|
520
522
|
};
|
|
521
523
|
};
|
|
@@ -589,9 +591,9 @@ const pumpStdinWithEofMarker = async (readable, copyIn) => {
|
|
|
589
591
|
if (settled)
|
|
590
592
|
return;
|
|
591
593
|
settled = true;
|
|
592
|
-
readable.removeListener(
|
|
593
|
-
readable.removeListener(
|
|
594
|
-
readable.removeListener(
|
|
594
|
+
readable.removeListener("data", onData);
|
|
595
|
+
readable.removeListener("end", onEnd);
|
|
596
|
+
readable.removeListener("error", onError);
|
|
595
597
|
run().then(() => {
|
|
596
598
|
resolve(markerHit);
|
|
597
599
|
}, (err) => {
|
|
@@ -615,7 +617,9 @@ const pumpStdinWithEofMarker = async (readable, copyIn) => {
|
|
|
615
617
|
// across chunk boundaries and any non-UTF-8 client_encoding byte
|
|
616
618
|
// (LATIN1/SJIS) into U+FFFD. stdin yields Buffers;
|
|
617
619
|
// guard the rare string case without assuming a lossy re-encode.
|
|
618
|
-
const buf = Buffer.isBuffer(chunk)
|
|
620
|
+
const buf = Buffer.isBuffer(chunk)
|
|
621
|
+
? chunk
|
|
622
|
+
: Buffer.from(chunk, "utf8");
|
|
619
623
|
tail = tail.length === 0 ? buf : Buffer.concat([tail, buf]);
|
|
620
624
|
let nl = tail.indexOf(0x0a); // '\n'
|
|
621
625
|
while (nl !== -1) {
|
|
@@ -638,9 +642,9 @@ const pumpStdinWithEofMarker = async (readable, copyIn) => {
|
|
|
638
642
|
// Pause + remove listeners BEFORE unshifting so the post-marker
|
|
639
643
|
// bytes aren't re-emitted into our own data handler.
|
|
640
644
|
readable.pause();
|
|
641
|
-
readable.removeListener(
|
|
642
|
-
readable.removeListener(
|
|
643
|
-
readable.removeListener(
|
|
645
|
+
readable.removeListener("data", onData);
|
|
646
|
+
readable.removeListener("end", onEnd);
|
|
647
|
+
readable.removeListener("error", onError);
|
|
644
648
|
if (leftover.length > 0) {
|
|
645
649
|
readable.unshift(leftover);
|
|
646
650
|
}
|
|
@@ -650,7 +654,9 @@ const pumpStdinWithEofMarker = async (readable, copyIn) => {
|
|
|
650
654
|
.then(() => {
|
|
651
655
|
resolve(true);
|
|
652
656
|
}, (err) => {
|
|
653
|
-
reject(err instanceof Error
|
|
657
|
+
reject(err instanceof Error
|
|
658
|
+
? err
|
|
659
|
+
: new Error(String(err)));
|
|
654
660
|
});
|
|
655
661
|
return;
|
|
656
662
|
}
|
|
@@ -701,9 +707,9 @@ const pumpStdinWithEofMarker = async (readable, copyIn) => {
|
|
|
701
707
|
throw err;
|
|
702
708
|
});
|
|
703
709
|
};
|
|
704
|
-
readable.on(
|
|
705
|
-
readable.once(
|
|
706
|
-
readable.once(
|
|
710
|
+
readable.on("data", onData);
|
|
711
|
+
readable.once("end", onEnd);
|
|
712
|
+
readable.once("error", onError);
|
|
707
713
|
// Trigger flowing mode in case the readable is paused.
|
|
708
714
|
readable.resume();
|
|
709
715
|
});
|
|
@@ -737,11 +743,11 @@ export const doCopy = async (conn, opts) => {
|
|
|
737
743
|
let fromStdin = false;
|
|
738
744
|
/** Cleanup callbacks run in `finally`. */
|
|
739
745
|
const cleanups = [];
|
|
740
|
-
if (opts.direction ===
|
|
746
|
+
if (opts.direction === "from") {
|
|
741
747
|
if (opts.file !== null) {
|
|
742
748
|
if (opts.program) {
|
|
743
749
|
try {
|
|
744
|
-
program = spawnProgram(opts.file,
|
|
750
|
+
program = spawnProgram(opts.file, "from");
|
|
745
751
|
}
|
|
746
752
|
catch (err) {
|
|
747
753
|
return failWith(`could not execute command "${opts.file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -776,7 +782,7 @@ export const doCopy = async (conn, opts) => {
|
|
|
776
782
|
resolve();
|
|
777
783
|
return;
|
|
778
784
|
}
|
|
779
|
-
stream.once(
|
|
785
|
+
stream.once("close", () => {
|
|
780
786
|
resolve();
|
|
781
787
|
});
|
|
782
788
|
stream.destroy();
|
|
@@ -795,7 +801,7 @@ export const doCopy = async (conn, opts) => {
|
|
|
795
801
|
if (opts.file !== null) {
|
|
796
802
|
if (opts.program) {
|
|
797
803
|
try {
|
|
798
|
-
program = spawnProgram(opts.file,
|
|
804
|
+
program = spawnProgram(opts.file, "to");
|
|
799
805
|
}
|
|
800
806
|
catch (err) {
|
|
801
807
|
return failWith(`could not execute command "${opts.file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -815,7 +821,9 @@ export const doCopy = async (conn, opts) => {
|
|
|
815
821
|
else {
|
|
816
822
|
try {
|
|
817
823
|
// Reject if the path exists and is a directory.
|
|
818
|
-
const stat = await fsPromises
|
|
824
|
+
const stat = await fsPromises
|
|
825
|
+
.stat(opts.file)
|
|
826
|
+
.catch(() => null);
|
|
819
827
|
if (stat?.isDirectory()) {
|
|
820
828
|
return failWith(`${opts.file}: cannot copy from/to a directory`);
|
|
821
829
|
}
|
|
@@ -826,7 +834,7 @@ export const doCopy = async (conn, opts) => {
|
|
|
826
834
|
const stream = createWriteStream(opts.file);
|
|
827
835
|
// Trap the async open/write error synchronously so it can't crash the
|
|
828
836
|
// process; surfaced as a COPY failure after the drive.
|
|
829
|
-
stream.once(
|
|
837
|
+
stream.once("error", (e) => {
|
|
830
838
|
fileWriteErrors.push(e);
|
|
831
839
|
});
|
|
832
840
|
writable = stream;
|
|
@@ -849,9 +857,9 @@ export const doCopy = async (conn, opts) => {
|
|
|
849
857
|
// Drive the COPY.
|
|
850
858
|
let tag = null;
|
|
851
859
|
try {
|
|
852
|
-
if (opts.direction ===
|
|
860
|
+
if (opts.direction === "from") {
|
|
853
861
|
if (readable === null) {
|
|
854
|
-
return failWith(
|
|
862
|
+
return failWith("no input stream for COPY FROM");
|
|
855
863
|
}
|
|
856
864
|
const copyIn = await conn.startCopyIn(sql);
|
|
857
865
|
// STDIN honours the `\.` EOF marker for BOTH text and CSV (psql treats
|
|
@@ -869,7 +877,7 @@ export const doCopy = async (conn, opts) => {
|
|
|
869
877
|
}
|
|
870
878
|
else {
|
|
871
879
|
if (writable === null) {
|
|
872
|
-
return failWith(
|
|
880
|
+
return failWith("no output stream for COPY TO");
|
|
873
881
|
}
|
|
874
882
|
await drainCopyTo(conn, sql, writable);
|
|
875
883
|
// A deferred open()/write() failure on the output file: report it as a
|
|
@@ -887,7 +895,7 @@ export const doCopy = async (conn, opts) => {
|
|
|
887
895
|
program.writable?.end();
|
|
888
896
|
const exit = await program.closed;
|
|
889
897
|
// A program is only spawned when opts.file holds the command string.
|
|
890
|
-
const progErr = describeProgramExit(opts.file ??
|
|
898
|
+
const progErr = describeProgramExit(opts.file ?? "", exit);
|
|
891
899
|
if (progErr !== null)
|
|
892
900
|
throw new Error(progErr);
|
|
893
901
|
}
|
|
@@ -917,7 +925,7 @@ export const doCopy = async (conn, opts) => {
|
|
|
917
925
|
*/
|
|
918
926
|
const readLastCopyTag = (conn) => {
|
|
919
927
|
const maybe = conn.lastCopyTag;
|
|
920
|
-
if (typeof maybe ===
|
|
928
|
+
if (typeof maybe === "string")
|
|
921
929
|
return maybe;
|
|
922
930
|
return null;
|
|
923
931
|
};
|
|
@@ -931,13 +939,15 @@ const readLastCopyTag = (conn) => {
|
|
|
931
939
|
* result-printing pipeline emits the tag.
|
|
932
940
|
*/
|
|
933
941
|
export const cmdCopy = {
|
|
934
|
-
name:
|
|
935
|
-
helpKey:
|
|
942
|
+
name: "copy",
|
|
943
|
+
helpKey: "copy",
|
|
936
944
|
async run(ctx) {
|
|
937
945
|
if (!ctx.settings.db) {
|
|
938
|
-
ctx.settings.lastErrorResult = {
|
|
939
|
-
|
|
940
|
-
|
|
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" };
|
|
941
951
|
}
|
|
942
952
|
// COPY is not supported inside a \startpipeline ... \endpipeline block:
|
|
943
953
|
// upstream libpq aborts the connection with this exact diagnostic and
|
|
@@ -959,25 +969,25 @@ export const cmdCopy = {
|
|
|
959
969
|
catch {
|
|
960
970
|
// best-effort; the connection may already be dead
|
|
961
971
|
}
|
|
962
|
-
return { status:
|
|
972
|
+
return { status: "error" };
|
|
963
973
|
}
|
|
964
974
|
const raw = ctx.restOfLine();
|
|
965
975
|
if (raw.trim().length === 0) {
|
|
966
|
-
ctx.settings.lastErrorResult = { message:
|
|
967
|
-
writeErr(
|
|
968
|
-
return { status:
|
|
976
|
+
ctx.settings.lastErrorResult = { message: "arguments required" };
|
|
977
|
+
writeErr("\\copy: arguments required\n");
|
|
978
|
+
return { status: "error" };
|
|
969
979
|
}
|
|
970
980
|
const parsed = parseSlashCopy(raw);
|
|
971
981
|
if (!parsed.ok) {
|
|
972
982
|
ctx.settings.lastErrorResult = { message: parsed.error };
|
|
973
983
|
writeErr(`\\copy: ${parsed.error}\n`);
|
|
974
|
-
return { status:
|
|
984
|
+
return { status: "error" };
|
|
975
985
|
}
|
|
976
986
|
const result = await doCopy(ctx.settings.db, parsed.value);
|
|
977
987
|
if (!result.ok) {
|
|
978
988
|
ctx.settings.lastErrorResult = { message: result.error };
|
|
979
989
|
writeErr(`\\copy: ${result.error}\n`);
|
|
980
|
-
return { status:
|
|
990
|
+
return { status: "error" };
|
|
981
991
|
}
|
|
982
992
|
// Print the upstream-style command tag (e.g. "COPY 17") so users see the
|
|
983
993
|
// same summary as `psql`. If the connection didn't surface a tag, just
|
|
@@ -990,7 +1000,7 @@ export const cmdCopy = {
|
|
|
990
1000
|
// print path, so the tag has nowhere to land. Mirror that here: only
|
|
991
1001
|
// print when the destination is a file, a program, or when the COPY is
|
|
992
1002
|
// a FROM (where the data flowed *into* the server, not out to stdout).
|
|
993
|
-
const suppressTag = parsed.value.direction ===
|
|
1003
|
+
const suppressTag = parsed.value.direction === "to" &&
|
|
994
1004
|
parsed.value.file === null &&
|
|
995
1005
|
!parsed.value.program;
|
|
996
1006
|
if (!suppressTag) {
|
|
@@ -1002,10 +1012,10 @@ export const cmdCopy = {
|
|
|
1002
1012
|
writeOut(`${result.tag}\n`);
|
|
1003
1013
|
}
|
|
1004
1014
|
else {
|
|
1005
|
-
writeOut(
|
|
1015
|
+
writeOut("COPY\n");
|
|
1006
1016
|
}
|
|
1007
1017
|
}
|
|
1008
|
-
return { status:
|
|
1018
|
+
return { status: "ok" };
|
|
1009
1019
|
},
|
|
1010
1020
|
};
|
|
1011
1021
|
/**
|
|
@@ -1022,4 +1032,4 @@ export { buildCopySql, pumpStdinWithEofMarker };
|
|
|
1022
1032
|
* tests can feed a `Buffer` to {@link doCopy} without re-implementing the
|
|
1023
1033
|
* Readable shim.
|
|
1024
1034
|
*/
|
|
1025
|
-
export const toBuffer = (s) => Buffer.from(s,
|
|
1035
|
+
export const toBuffer = (s) => Buffer.from(s, "utf8");
|