neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +615 -846
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +2 -446
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +2 -51
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +728 -984
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +2 -2193
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +2 -846
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +2 -2030
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +8 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +39 -45
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
|
@@ -1,1035 +1,779 @@
|
|
|
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
1
|
import { writeErr, writeOut } from "./shared.js";
|
|
2
|
+
import { pumpReadable } from "../wire/copy.js";
|
|
3
|
+
import { m as getPipelineState } from "../../_chunks/cmd_pipeline-CUbBO9U_.js";
|
|
4
|
+
import { createReadStream, createWriteStream, promises } from "node:fs";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
//#region src/psql/command/cmd_copy.ts
|
|
8
|
+
/**
|
|
9
|
+
* psql `\copy` backslash command (WP-16).
|
|
10
|
+
*
|
|
11
|
+
* Port of `parse_slash_copy()` + `do_copy()` from upstream `src/bin/psql/copy.c`.
|
|
12
|
+
* The wire-level protocol (CopyData/CopyDone/CopyFail framing and the
|
|
13
|
+
* in-copy-in/in-copy-out state machine) lives in `../wire/connection.ts`; here
|
|
14
|
+
* we own:
|
|
15
|
+
*
|
|
16
|
+
* 1. Lexing the user-supplied tail of `\copy …`. Mirrors the upstream
|
|
17
|
+
* `strtokx()`-driven tokeniser: we ratchet through the input with the
|
|
18
|
+
* same whitespace/delim/quote rules so the grammar matches psql.
|
|
19
|
+
* 2. Building the COPY SQL the server sees. The client side does
|
|
20
|
+
* file/program plumbing; the server always sees `... FROM STDIN ...` /
|
|
21
|
+
* `... TO STDOUT ...` so the COPY data flows over the protocol stream.
|
|
22
|
+
* 3. Driving the protocol: open the file (or spawn `PROGRAM 'cmd'`), then
|
|
23
|
+
* `startCopyIn(sql)` / `startCopyOut(sql)`, push/pull bytes, and print
|
|
24
|
+
* the upstream-style `COPY <N>` summary on success.
|
|
25
|
+
*
|
|
26
|
+
* Grammar accepted (matching upstream documentation):
|
|
27
|
+
*
|
|
28
|
+
* \copy [BINARY] tablename [(columnlist)] FROM
|
|
29
|
+
* ( 'file' | PROGRAM 'cmd' | STDIN | PSTDIN ) [options]
|
|
30
|
+
* \copy [BINARY] tablename [(columnlist)] TO
|
|
31
|
+
* ( 'file' | PROGRAM 'cmd' | STDOUT | PSTDOUT ) [options]
|
|
32
|
+
* \copy (subquery) TO ( 'file' | PROGRAM 'cmd' | STDOUT | PSTDOUT ) [options]
|
|
33
|
+
*
|
|
34
|
+
* `\copy (subquery) FROM ...` is rejected — COPY FROM requires a real
|
|
35
|
+
* destination table, so the subquery form only makes sense with `TO`.
|
|
36
|
+
*
|
|
37
|
+
* Limitations vs upstream:
|
|
38
|
+
* - Binary COPY (server-side `WITH (FORMAT BINARY)` option) is byte-for-byte
|
|
39
|
+
* transparent: bytes captured by `COPY ... TO STDOUT WITH BINARY` are
|
|
40
|
+
* piped straight to the destination, and on `COPY ... FROM STDIN WITH
|
|
41
|
+
* BINARY` we relay the source bytes verbatim. We do NOT parse tuples;
|
|
42
|
+
* `validateCopyBinarySignature` is offered for callers that want to
|
|
43
|
+
* sniff the 11-byte file header, but the wire path itself is format-
|
|
44
|
+
* agnostic. The legacy `BINARY <table> FROM …` keyword syntax is parsed
|
|
45
|
+
* and re-emitted verbatim; we don't try to interpret the options blob.
|
|
46
|
+
* - The literal `\.` end-of-data marker is honoured when (and only when):
|
|
47
|
+
* the source is STDIN, AND the COPY format is text (not csv, not binary).
|
|
48
|
+
* A line matching exactly `\.` terminates the stream client-side via
|
|
49
|
+
* CopyDone; subsequent input bytes go back to the SQL stream. Matches
|
|
50
|
+
* upstream's stricter behaviour: csv/binary COPY treats `\.` as data.
|
|
51
|
+
* - PSTDIN/PSTDOUT are treated as STDIN/STDOUT (no separate "psql stdin
|
|
52
|
+
* vs current input source" distinction — REPL plumbing isn't wired yet).
|
|
53
|
+
*/
|
|
53
54
|
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const WHITESPACE = "
|
|
55
|
+
* Diagnostic emitted when the user tries to run `\copy` (or a raw COPY
|
|
56
|
+
* statement) inside an active `\startpipeline` ... `\endpipeline` block.
|
|
57
|
+
* Matches upstream libpq's wording so conformance tests grepping stderr
|
|
58
|
+
* (e.g. tap/001_basic.pl lines 490-531) pick it up unchanged. Exported so
|
|
59
|
+
* the wire-layer abort path can reuse the same string and tests can match
|
|
60
|
+
* via a single source of truth.
|
|
61
|
+
*/
|
|
62
|
+
const COPY_IN_PIPELINE_MSG = "COPY in a pipeline is not supported, aborting connection";
|
|
63
|
+
const WHITESPACE = " \n\r";
|
|
63
64
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
* Tokenise the next term of the `\copy` tail. Mirrors upstream's `strtokx`
|
|
66
|
+
* call sites: each call passes a different combination of (delim chars,
|
|
67
|
+
* quote chars, allow-doubled-quotes, allow-E-strings). We faithfully replay
|
|
68
|
+
* those: this isn't a general lexer, it's a state machine indexed by the
|
|
69
|
+
* caller's intent.
|
|
70
|
+
*
|
|
71
|
+
* Returns `{ token, rest }`. `token === null` ⇒ end-of-input.
|
|
72
|
+
*
|
|
73
|
+
* Key differences from upstream `strtokx`:
|
|
74
|
+
* - We return tokens WITH outer quotes intact when the caller asked for
|
|
75
|
+
* them. `dequote` handles strip if desired. Upstream stores quotes
|
|
76
|
+
* in-place and optionally strips via `strip_quotes`.
|
|
77
|
+
* - Delimiter characters in `delim` are returned as single-char tokens
|
|
78
|
+
* when they're the first non-whitespace byte.
|
|
79
|
+
*/
|
|
79
80
|
const tokenize = (input, delim, quote, doubleQuoteEscape) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
while (i < n && WHITESPACE.includes(input[i]))
|
|
138
|
-
i++;
|
|
139
|
-
return { token, rest: input.slice(i) };
|
|
81
|
+
let i = 0;
|
|
82
|
+
const n = input.length;
|
|
83
|
+
while (i < n && WHITESPACE.includes(input[i])) i++;
|
|
84
|
+
if (i >= n) return {
|
|
85
|
+
token: null,
|
|
86
|
+
rest: ""
|
|
87
|
+
};
|
|
88
|
+
if (delim.length > 0 && delim.includes(input[i])) {
|
|
89
|
+
const token = input[i];
|
|
90
|
+
i++;
|
|
91
|
+
while (i < n && WHITESPACE.includes(input[i])) i++;
|
|
92
|
+
return {
|
|
93
|
+
token,
|
|
94
|
+
rest: input.slice(i)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (quote.length > 0 && quote.includes(input[i])) {
|
|
98
|
+
const thisQuote = input[i];
|
|
99
|
+
const start = i;
|
|
100
|
+
i++;
|
|
101
|
+
while (i < n) {
|
|
102
|
+
const c = input[i];
|
|
103
|
+
if (doubleQuoteEscape && c === "\\" && i + 1 < n) {
|
|
104
|
+
i += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === thisQuote && input[i + 1] === thisQuote) {
|
|
108
|
+
i += 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (c === thisQuote) {
|
|
112
|
+
i++;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
const token = input.slice(start, i);
|
|
118
|
+
while (i < n && WHITESPACE.includes(input[i])) i++;
|
|
119
|
+
return {
|
|
120
|
+
token,
|
|
121
|
+
rest: input.slice(i)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const start = i;
|
|
125
|
+
while (i < n) {
|
|
126
|
+
const c = input[i];
|
|
127
|
+
if (WHITESPACE.includes(c)) break;
|
|
128
|
+
if (delim.length > 0 && delim.includes(c)) break;
|
|
129
|
+
if (quote.length > 0 && quote.includes(c)) break;
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
const token = input.slice(start, i);
|
|
133
|
+
while (i < n && WHITESPACE.includes(input[i])) i++;
|
|
134
|
+
return {
|
|
135
|
+
token,
|
|
136
|
+
rest: input.slice(i)
|
|
137
|
+
};
|
|
140
138
|
};
|
|
141
139
|
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
* Strip surrounding single quotes from a filename / program argument and
|
|
141
|
+
* undouble any embedded quotes. Mirrors upstream's `strip_quotes(token, '\'', 0)`.
|
|
142
|
+
*/
|
|
145
143
|
const stripSingleQuotes = (token) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
out += token[i];
|
|
159
|
-
i++;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return out;
|
|
144
|
+
if (token.length < 2 || !token.startsWith("'") || !token.endsWith("'")) return token;
|
|
145
|
+
let out = "";
|
|
146
|
+
let i = 1;
|
|
147
|
+
const end = token.length - 1;
|
|
148
|
+
while (i < end) if (token[i] === "'" && token[i + 1] === "'") {
|
|
149
|
+
out += "'";
|
|
150
|
+
i += 2;
|
|
151
|
+
} else {
|
|
152
|
+
out += token[i];
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
163
156
|
};
|
|
164
157
|
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
* Expand a leading `~/` in filename arguments. Upstream `expand_tilde` only
|
|
159
|
+
* touches the very first character; we do the same (no `~user/` form, since
|
|
160
|
+
* Node doesn't expose `getpwnam` cleanly).
|
|
161
|
+
*/
|
|
169
162
|
const expandTilde = (filePath) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
return filePath;
|
|
163
|
+
if (!filePath.startsWith("~")) return filePath;
|
|
164
|
+
if (filePath === "~" || filePath.startsWith("~/")) {
|
|
165
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
166
|
+
if (home === void 0) return filePath;
|
|
167
|
+
return home + filePath.slice(1);
|
|
168
|
+
}
|
|
169
|
+
return filePath;
|
|
179
170
|
};
|
|
180
171
|
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
};
|
|
172
|
+
* Parse the tail of a `\copy ...` line. Returns a {@link ParsedCopy} on
|
|
173
|
+
* success or an error message on syntax failure (mirroring upstream's
|
|
174
|
+
* `pg_log_error("\\copy: parse error at \"%s\"")`).
|
|
175
|
+
*
|
|
176
|
+
* The input is everything after `\copy` (the command name itself is stripped
|
|
177
|
+
* by the dispatcher's `BackslashContext.rawArgs`).
|
|
178
|
+
*/
|
|
179
|
+
const parseSlashCopy = (input) => {
|
|
180
|
+
let beforeToFrom = "";
|
|
181
|
+
let rest = input;
|
|
182
|
+
let token;
|
|
183
|
+
const errAt = (tok) => ({
|
|
184
|
+
ok: false,
|
|
185
|
+
error: tok !== null && tok.length > 0 ? `parse error at "${tok}"` : "parse error at end of line"
|
|
186
|
+
});
|
|
187
|
+
let r1 = tokenize(rest, ".,()", "\"", false);
|
|
188
|
+
token = r1.token;
|
|
189
|
+
rest = r1.rest;
|
|
190
|
+
if (token === null) return errAt(null);
|
|
191
|
+
if (token.toLowerCase() === "binary") {
|
|
192
|
+
beforeToFrom += token;
|
|
193
|
+
r1 = tokenize(rest, ".,()", "\"", false);
|
|
194
|
+
token = r1.token;
|
|
195
|
+
rest = r1.rest;
|
|
196
|
+
if (token === null) return errAt(null);
|
|
197
|
+
}
|
|
198
|
+
let isSubquery = false;
|
|
199
|
+
if (token === "(") {
|
|
200
|
+
isSubquery = true;
|
|
201
|
+
let parens = 1;
|
|
202
|
+
while (parens > 0) {
|
|
203
|
+
beforeToFrom += " ";
|
|
204
|
+
beforeToFrom += token;
|
|
205
|
+
const r = tokenize(rest, "()", "\"'", true);
|
|
206
|
+
token = r.token;
|
|
207
|
+
rest = r.rest;
|
|
208
|
+
if (token === null) return errAt(null);
|
|
209
|
+
if (token === "(") parens++;
|
|
210
|
+
else if (token === ")") parens--;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
beforeToFrom += beforeToFrom.length > 0 ? " " : "";
|
|
214
|
+
beforeToFrom += token;
|
|
215
|
+
let r2 = tokenize(rest, ".,()", "\"", false);
|
|
216
|
+
token = r2.token;
|
|
217
|
+
rest = r2.rest;
|
|
218
|
+
if (token === null) return errAt(null);
|
|
219
|
+
if (token === ".") {
|
|
220
|
+
beforeToFrom += token;
|
|
221
|
+
r2 = tokenize(rest, ".,()", "\"", false);
|
|
222
|
+
token = r2.token;
|
|
223
|
+
rest = r2.rest;
|
|
224
|
+
if (token === null) return errAt(null);
|
|
225
|
+
beforeToFrom += token;
|
|
226
|
+
r2 = tokenize(rest, ".,()", "\"", false);
|
|
227
|
+
token = r2.token;
|
|
228
|
+
rest = r2.rest;
|
|
229
|
+
if (token === null) return errAt(null);
|
|
230
|
+
}
|
|
231
|
+
if (token === "(") {
|
|
232
|
+
for (;;) {
|
|
233
|
+
beforeToFrom += " ";
|
|
234
|
+
beforeToFrom += token;
|
|
235
|
+
const r = tokenize(rest, "()", "\"", false);
|
|
236
|
+
token = r.token;
|
|
237
|
+
rest = r.rest;
|
|
238
|
+
if (token === null) return errAt(null);
|
|
239
|
+
if (token === ")") break;
|
|
240
|
+
}
|
|
241
|
+
beforeToFrom += " ";
|
|
242
|
+
beforeToFrom += token;
|
|
243
|
+
r2 = tokenize(rest, ".,()", "\"", false);
|
|
244
|
+
token = r2.token;
|
|
245
|
+
rest = r2.rest;
|
|
246
|
+
if (token === null) return errAt(null);
|
|
247
|
+
}
|
|
248
|
+
let direction;
|
|
249
|
+
if (token.toLowerCase() === "from") direction = "from";
|
|
250
|
+
else if (token.toLowerCase() === "to") direction = "to";
|
|
251
|
+
else return errAt(token);
|
|
252
|
+
if (isSubquery && direction === "from") return {
|
|
253
|
+
ok: false,
|
|
254
|
+
error: "cannot use COPY FROM with a (subquery) source"
|
|
255
|
+
};
|
|
256
|
+
let r3 = tokenize(rest, ";", "'", false);
|
|
257
|
+
token = r3.token;
|
|
258
|
+
rest = r3.rest;
|
|
259
|
+
if (token === null) return errAt(null);
|
|
260
|
+
let file = null;
|
|
261
|
+
let program = false;
|
|
262
|
+
let psqlInOut = false;
|
|
263
|
+
const lower = token.toLowerCase();
|
|
264
|
+
if (lower === "program") {
|
|
265
|
+
r3 = tokenize(rest, ";", "'", false);
|
|
266
|
+
token = r3.token;
|
|
267
|
+
rest = r3.rest;
|
|
268
|
+
if (token === null) return errAt(null);
|
|
269
|
+
if (!token.startsWith("'") || !token.endsWith("'") || token.length < 2) return errAt(token);
|
|
270
|
+
file = stripSingleQuotes(token);
|
|
271
|
+
program = true;
|
|
272
|
+
} else if (lower === "stdin" || lower === "stdout") file = null;
|
|
273
|
+
else if (lower === "pstdin" || lower === "pstdout") {
|
|
274
|
+
file = null;
|
|
275
|
+
psqlInOut = true;
|
|
276
|
+
} else file = expandTilde(stripSingleQuotes(token));
|
|
277
|
+
let afterToFrom = null;
|
|
278
|
+
rest = rest.trim();
|
|
279
|
+
if (rest.length > 0) afterToFrom = rest;
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
value: {
|
|
283
|
+
beforeToFrom,
|
|
284
|
+
afterToFrom,
|
|
285
|
+
file,
|
|
286
|
+
program,
|
|
287
|
+
psqlInOut,
|
|
288
|
+
direction
|
|
289
|
+
}
|
|
290
|
+
};
|
|
346
291
|
};
|
|
347
|
-
// ---------------------------------------------------------------------------
|
|
348
|
-
// do_copy
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
292
|
/**
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
293
|
+
* Build the SQL string sent to the backend. The server always sees
|
|
294
|
+
* `STDIN`/`STDOUT` here — client-side `'file'` / `PROGRAM 'cmd'` plumbing is
|
|
295
|
+
* invisible to the server because that's what frontend-driven COPY is for.
|
|
296
|
+
*/
|
|
355
297
|
const buildCopySql = (opts) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
298
|
+
const tail = opts.direction === "from" ? " FROM STDIN " : " TO STDOUT ";
|
|
299
|
+
const after = opts.afterToFrom !== null ? opts.afterToFrom : "";
|
|
300
|
+
return `COPY ${opts.beforeToFrom}${tail}${after}`.trimEnd();
|
|
359
301
|
};
|
|
360
302
|
/**
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
303
|
+
* Strip single-quoted SQL string literals from a fragment so a keyword scan
|
|
304
|
+
* over the result can't false-trigger on a payload character. Handles both
|
|
305
|
+
* the standard `'…''…'` form (doubled-quote escape) and the escape-string
|
|
306
|
+
* `E'…\…'` form (backslash-escape). Each match collapses to `''` so token
|
|
307
|
+
* boundaries around the literal are preserved.
|
|
308
|
+
*
|
|
309
|
+
* This is lenient on the `E` prefix recognition: we don't enforce that the
|
|
310
|
+
* `E` is unescaped (e.g. we'd also strip `xE'…'`). False-positive stripping
|
|
311
|
+
* is safe — we only ever miss a `csv` / `binary` / `format` mention that was
|
|
312
|
+
* intended as a data payload, which is exactly the case we want to skip.
|
|
313
|
+
*/
|
|
372
314
|
const stripCopyOptionsStrings = (s) => {
|
|
373
|
-
|
|
315
|
+
return s.replace(/E'(?:\\.|[^'])*'|'(?:''|[^'])*'/g, "''");
|
|
374
316
|
};
|
|
375
317
|
/**
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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;
|
|
318
|
+
* Detect whether the COPY uses the (default) text format. Upstream psql only
|
|
319
|
+
* honours the `\.` end-of-data marker for text-format COPY; csv/binary treat
|
|
320
|
+
* the bytes as data.
|
|
321
|
+
*
|
|
322
|
+
* The check is a coarse keyword scan of the options string: if any of `csv`,
|
|
323
|
+
* `binary`, or `format <something>` appears (case-insensitive), we assume the
|
|
324
|
+
* user has explicitly selected a non-text format and disable EOF-marker
|
|
325
|
+
* handling. Quoted literals (including `E'…'` escape strings) are stripped
|
|
326
|
+
* first so a column-named "binary" or a `DELIMITER E'\\tbinary'` payload
|
|
327
|
+
* doesn't false-trigger.
|
|
328
|
+
*
|
|
329
|
+
* The `FORMAT` value itself may be optionally single-quoted in the new
|
|
330
|
+
* parenthesised-options syntax (e.g. `WITH (FORMAT 'csv')`); we accept either
|
|
331
|
+
* a bareword or a `'…'` literal there to match upstream's option grammar.
|
|
332
|
+
*/
|
|
333
|
+
const isCopyTextFormat = (afterToFrom) => {
|
|
334
|
+
if (afterToFrom === null) return true;
|
|
335
|
+
const stripped = stripCopyOptionsStrings(afterToFrom);
|
|
336
|
+
if (/\bcsv\b/i.test(stripped)) return false;
|
|
337
|
+
if (/\bbinary\b/i.test(stripped)) return false;
|
|
338
|
+
const m = /\bformat\s+(?:'([A-Za-z_]+)'|([A-Za-z_]+))/i.exec(afterToFrom);
|
|
339
|
+
if (m) return (m[1] ?? m[2]).toLowerCase() === "text";
|
|
340
|
+
return true;
|
|
412
341
|
};
|
|
413
342
|
/**
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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;
|
|
343
|
+
* Mirror of `isCopyTextFormat`'s scan, but returns `true` only when the COPY
|
|
344
|
+
* was explicitly opted into binary format. Used by the `\copy` driver to gate
|
|
345
|
+
* the BINARY-signature byte-for-byte transparency check (we don't want to
|
|
346
|
+
* touch text/csv streams).
|
|
347
|
+
*
|
|
348
|
+
* Matches `WITH BINARY`, `WITH (FORMAT binary)` (with or without quotes around
|
|
349
|
+
* the value), the legacy psql `BINARY t FROM …` keyword (which the parser
|
|
350
|
+
* folds into `beforeToFrom`), and mixed-case variants.
|
|
351
|
+
*/
|
|
352
|
+
const isCopyBinaryFormat = (beforeToFrom, afterToFrom) => {
|
|
353
|
+
if (/^\s*binary\b/i.test(beforeToFrom)) return true;
|
|
354
|
+
if (afterToFrom === null) return false;
|
|
355
|
+
const stripped = stripCopyOptionsStrings(afterToFrom);
|
|
356
|
+
if (/(^|\W)binary(\W|$)/i.test(stripped)) return true;
|
|
357
|
+
const m = /\bformat\s+(?:'([A-Za-z_]+)'|([A-Za-z_]+))/i.exec(afterToFrom);
|
|
358
|
+
if (m) return (m[1] ?? m[2]).toLowerCase() === "binary";
|
|
359
|
+
return false;
|
|
445
360
|
};
|
|
446
361
|
/**
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
362
|
+
* PostgreSQL COPY binary-format file header signature.
|
|
363
|
+
*
|
|
364
|
+
* Per the docs[1]: every binary COPY stream begins with an 11-byte signature
|
|
365
|
+
* (`PGCOPY\n\xff\r\n\0`), followed by a 4-byte flags field and a 4-byte
|
|
366
|
+
* header-extension-area length. After that come zero-or-more tuples, then a
|
|
367
|
+
* 2-byte file trailer of `0xFFFF` (Int16 `-1`).
|
|
368
|
+
*
|
|
369
|
+
* We expose the signature bytes (not the full 19-byte fixed prefix) so callers
|
|
370
|
+
* can sniff incoming streams or assert outgoing streams without depending on
|
|
371
|
+
* server-version-specific flags / extension data.
|
|
372
|
+
*
|
|
373
|
+
* [1] https://www.postgresql.org/docs/current/sql-copy.html#id-1.9.3.55.9.4
|
|
374
|
+
*/
|
|
375
|
+
const COPY_BINARY_SIGNATURE = Buffer.from([
|
|
376
|
+
80,
|
|
377
|
+
71,
|
|
378
|
+
67,
|
|
379
|
+
79,
|
|
380
|
+
80,
|
|
381
|
+
89,
|
|
382
|
+
10,
|
|
383
|
+
255,
|
|
384
|
+
13,
|
|
385
|
+
10,
|
|
386
|
+
0
|
|
462
387
|
]);
|
|
463
388
|
/**
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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;
|
|
389
|
+
* Validate that a buffer starts with the COPY binary signature.
|
|
390
|
+
*
|
|
391
|
+
* Used to assert round-trip transparency: bytes captured by `COPY ... TO
|
|
392
|
+
* STDOUT WITH BINARY` should be byte-for-byte acceptable to `COPY ... FROM
|
|
393
|
+
* STDIN WITH BINARY` on another instance. We don't try to parse tuples —
|
|
394
|
+
* that requires per-type binary decoders the printer doesn't otherwise need.
|
|
395
|
+
*
|
|
396
|
+
* Returns `null` on success or a short diagnostic string on failure (matching
|
|
397
|
+
* the upstream wording style: "missing signature" / "wrong signature").
|
|
398
|
+
*/
|
|
399
|
+
const validateCopyBinarySignature = (buf) => {
|
|
400
|
+
if (buf.length < COPY_BINARY_SIGNATURE.length) return "missing COPY binary signature (input too short)";
|
|
401
|
+
for (let i = 0; i < COPY_BINARY_SIGNATURE.length; i++) if (buf[i] !== COPY_BINARY_SIGNATURE[i]) return "COPY binary signature mismatch";
|
|
402
|
+
return null;
|
|
484
403
|
};
|
|
485
404
|
/**
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
405
|
+
* Parse a CommandComplete tag like `"COPY 17"` into its numeric row count.
|
|
406
|
+
* Returns `null` when the tag is unparseable; callers print it verbatim then.
|
|
407
|
+
*/
|
|
489
408
|
const parseCopyTagRows = (tag) => {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return null;
|
|
495
|
-
return parseInt(m[1], 10);
|
|
409
|
+
if (tag === null) return null;
|
|
410
|
+
const m = /^COPY (\d+)$/.exec(tag.trim());
|
|
411
|
+
if (!m) return null;
|
|
412
|
+
return parseInt(m[1], 10);
|
|
496
413
|
};
|
|
497
414
|
const spawnProgram = (cmd, direction) => {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
415
|
+
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
416
|
+
direction === "to" ? "pipe" : "inherit",
|
|
417
|
+
direction === "from" ? "pipe" : "inherit",
|
|
418
|
+
"inherit"
|
|
419
|
+
] });
|
|
420
|
+
const closed = new Promise((resolve) => {
|
|
421
|
+
child.once("close", (code, signal) => {
|
|
422
|
+
resolve({
|
|
423
|
+
code,
|
|
424
|
+
signal,
|
|
425
|
+
error: null
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
child.once("error", (error) => {
|
|
429
|
+
resolve({
|
|
430
|
+
code: null,
|
|
431
|
+
signal: null,
|
|
432
|
+
error
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
return {
|
|
437
|
+
child,
|
|
438
|
+
readable: direction === "from" ? child.stdout : null,
|
|
439
|
+
writable: direction === "to" ? child.stdin : null,
|
|
440
|
+
closed
|
|
441
|
+
};
|
|
523
442
|
};
|
|
524
443
|
/**
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
444
|
+
* Turn a {@link ProgramExit} into psql-style diagnostic text, or `null` when
|
|
445
|
+
* the program succeeded (exit 0, no signal, no spawn error).
|
|
446
|
+
*/
|
|
528
447
|
const describeProgramExit = (cmd, exit) => {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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;
|
|
448
|
+
if (exit.error !== null) return `could not execute command "${cmd}": ${exit.error.message}`;
|
|
449
|
+
if (exit.signal !== null) return `program "${cmd}" was terminated by signal ${exit.signal}`;
|
|
450
|
+
if (exit.code !== null && exit.code !== 0) return `program "${cmd}" failed with exit code ${exit.code}`;
|
|
451
|
+
return null;
|
|
539
452
|
};
|
|
540
453
|
/**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
454
|
+
* Drain a `CopyOutStream` (AsyncIterable<Buffer>) into a Node Writable. We
|
|
455
|
+
* await each write to honour backpressure. Mirrors upstream's `handleCopyOut`
|
|
456
|
+
* inner loop.
|
|
457
|
+
*/
|
|
545
458
|
const drainCopyTo = async (conn, sql, out) => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
}
|
|
459
|
+
const copyOut = await conn.startCopyOut(sql);
|
|
460
|
+
for await (const chunk of copyOut) {
|
|
461
|
+
if (chunk.length === 0) continue;
|
|
462
|
+
await new Promise((resolve, reject) => {
|
|
463
|
+
out.write(chunk, (err) => {
|
|
464
|
+
if (err !== null && err !== void 0) reject(err);
|
|
465
|
+
else resolve();
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
559
469
|
};
|
|
560
470
|
/**
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
471
|
+
* Pump a Readable into a CopyInStream, honouring the upstream `\.` text-mode
|
|
472
|
+
* EOF marker. A line consisting EXACTLY of `\.` (LF- or CRLF-terminated) ends
|
|
473
|
+
* the COPY via `copyIn.end()`; everything after the marker is left on the
|
|
474
|
+
* Readable for the caller (the REPL goes back to SQL mode and reads it as
|
|
475
|
+
* the next statement).
|
|
476
|
+
*
|
|
477
|
+
* The marker is detected by accumulating a tail buffer until we see a newline,
|
|
478
|
+
* then comparing the line to `\.`. We DO NOT mutate or strip data already
|
|
479
|
+
* flushed — once a chunk has been forwarded as CopyData, it's gone. The
|
|
480
|
+
* implementation reads chunks, splits on newlines, and forwards complete
|
|
481
|
+
* lines individually so the marker can short-circuit the stream cleanly.
|
|
482
|
+
*
|
|
483
|
+
* We DO NOT use `for await (const chunk of readable)` because Node destroys
|
|
484
|
+
* the underlying stream when the async-iterator wrapper exits (even cleanly
|
|
485
|
+
* via `break`), which would prevent the caller from resuming reads after the
|
|
486
|
+
* marker. Instead we drive the readable with explicit data/end event
|
|
487
|
+
* listeners, paused/resumed via `pause()`/`resume()`, and remove them once
|
|
488
|
+
* the marker fires — leaving the source intact for subsequent consumption.
|
|
489
|
+
*
|
|
490
|
+
* Returns true if the marker was hit (caller closed the stream), false on
|
|
491
|
+
* normal EOF.
|
|
492
|
+
*/
|
|
583
493
|
const pumpStdinWithEofMarker = async (readable, copyIn) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
});
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
let tail = Buffer.alloc(0);
|
|
496
|
+
let markerHit = false;
|
|
497
|
+
let settled = false;
|
|
498
|
+
/** In-flight `copyIn.write` chain; we serialize writes for backpressure. */
|
|
499
|
+
let writeChain = Promise.resolve();
|
|
500
|
+
const settle = (run) => {
|
|
501
|
+
if (settled) return;
|
|
502
|
+
settled = true;
|
|
503
|
+
readable.removeListener("data", onData);
|
|
504
|
+
readable.removeListener("end", onEnd);
|
|
505
|
+
readable.removeListener("error", onError);
|
|
506
|
+
run().then(() => {
|
|
507
|
+
resolve(markerHit);
|
|
508
|
+
}, (err) => {
|
|
509
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
const writeBytes = (bytes) => {
|
|
513
|
+
if (bytes.length === 0) return;
|
|
514
|
+
const owned = Buffer.from(bytes);
|
|
515
|
+
writeChain = writeChain.then(() => copyIn.write(owned));
|
|
516
|
+
};
|
|
517
|
+
const handleChunk = (chunk) => {
|
|
518
|
+
if (settled) return;
|
|
519
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
|
|
520
|
+
tail = tail.length === 0 ? buf : Buffer.concat([tail, buf]);
|
|
521
|
+
let nl = tail.indexOf(10);
|
|
522
|
+
while (nl !== -1) {
|
|
523
|
+
const line = tail.subarray(0, nl + 1);
|
|
524
|
+
if (line.length === 3 && line[0] === 92 && line[1] === 46 && line[2] === 10 || line.length === 4 && line[0] === 92 && line[1] === 46 && line[2] === 13 && line[3] === 10) {
|
|
525
|
+
markerHit = true;
|
|
526
|
+
const leftover = Buffer.from(tail.subarray(nl + 1));
|
|
527
|
+
tail = Buffer.alloc(0);
|
|
528
|
+
readable.pause();
|
|
529
|
+
readable.removeListener("data", onData);
|
|
530
|
+
readable.removeListener("end", onEnd);
|
|
531
|
+
readable.removeListener("error", onError);
|
|
532
|
+
if (leftover.length > 0) readable.unshift(leftover);
|
|
533
|
+
settled = true;
|
|
534
|
+
writeChain.then(() => copyIn.end()).then(() => {
|
|
535
|
+
resolve(true);
|
|
536
|
+
}, (err) => {
|
|
537
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
writeBytes(line);
|
|
542
|
+
tail = tail.subarray(nl + 1);
|
|
543
|
+
nl = tail.indexOf(10);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const onData = (chunk) => {
|
|
547
|
+
try {
|
|
548
|
+
handleChunk(chunk);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
settle(async () => {
|
|
551
|
+
try {
|
|
552
|
+
await copyIn.fail(err instanceof Error ? err.message : String(err));
|
|
553
|
+
} catch {}
|
|
554
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const onEnd = () => {
|
|
559
|
+
if (settled) return;
|
|
560
|
+
const trailing = tail;
|
|
561
|
+
tail = Buffer.alloc(0);
|
|
562
|
+
settle(async () => {
|
|
563
|
+
if (trailing.length > 0) writeBytes(trailing);
|
|
564
|
+
await writeChain;
|
|
565
|
+
await copyIn.end();
|
|
566
|
+
});
|
|
567
|
+
};
|
|
568
|
+
const onError = (err) => {
|
|
569
|
+
if (settled) return;
|
|
570
|
+
settle(async () => {
|
|
571
|
+
try {
|
|
572
|
+
await copyIn.fail(err.message);
|
|
573
|
+
} catch {}
|
|
574
|
+
throw err;
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
readable.on("data", onData);
|
|
578
|
+
readable.once("end", onEnd);
|
|
579
|
+
readable.once("error", onError);
|
|
580
|
+
readable.resume();
|
|
581
|
+
});
|
|
716
582
|
};
|
|
717
583
|
/**
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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 };
|
|
584
|
+
* Execute a parsed `\copy`. Opens the file (or spawns the program), wires the
|
|
585
|
+
* stream into `startCopyIn` / `startCopyOut`, and returns the resulting
|
|
586
|
+
* CommandComplete tag (e.g. `"COPY 17"`) on success.
|
|
587
|
+
*/
|
|
588
|
+
const doCopy = async (conn, opts) => {
|
|
589
|
+
const sql = buildCopySql(opts);
|
|
590
|
+
const failWith = (msg) => ({
|
|
591
|
+
ok: false,
|
|
592
|
+
error: msg
|
|
593
|
+
});
|
|
594
|
+
let readable = null;
|
|
595
|
+
let writable = null;
|
|
596
|
+
let program = null;
|
|
597
|
+
const fileWriteErrors = [];
|
|
598
|
+
/**
|
|
599
|
+
* True iff the data path is "psql stdin" — i.e. the user typed
|
|
600
|
+
* `\copy t FROM STDIN`. Only this path honours the `\.` text-mode EOF
|
|
601
|
+
* marker; file and PROGRAM sources stream verbatim to match upstream.
|
|
602
|
+
*/
|
|
603
|
+
let fromStdin = false;
|
|
604
|
+
/** Cleanup callbacks run in `finally`. */
|
|
605
|
+
const cleanups = [];
|
|
606
|
+
if (opts.direction === "from") if (opts.file !== null) if (opts.program) {
|
|
607
|
+
try {
|
|
608
|
+
program = spawnProgram(opts.file, "from");
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return failWith(`could not execute command "${opts.file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
611
|
+
}
|
|
612
|
+
readable = program.readable;
|
|
613
|
+
const p = program;
|
|
614
|
+
cleanups.push(async () => {
|
|
615
|
+
try {
|
|
616
|
+
p.child.stdout?.destroy();
|
|
617
|
+
} catch {}
|
|
618
|
+
await p.closed;
|
|
619
|
+
});
|
|
620
|
+
} else {
|
|
621
|
+
try {
|
|
622
|
+
if ((await promises.stat(opts.file)).isDirectory()) return failWith(`${opts.file}: cannot copy from/to a directory`);
|
|
623
|
+
} catch (err) {
|
|
624
|
+
return failWith(`${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
625
|
+
}
|
|
626
|
+
const stream = createReadStream(opts.file);
|
|
627
|
+
readable = stream;
|
|
628
|
+
cleanups.push(() => new Promise((resolve) => {
|
|
629
|
+
if (stream.destroyed) {
|
|
630
|
+
resolve();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
stream.once("close", () => {
|
|
634
|
+
resolve();
|
|
635
|
+
});
|
|
636
|
+
stream.destroy();
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
readable = process.stdin;
|
|
641
|
+
fromStdin = true;
|
|
642
|
+
}
|
|
643
|
+
else if (opts.file !== null) if (opts.program) {
|
|
644
|
+
try {
|
|
645
|
+
program = spawnProgram(opts.file, "to");
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return failWith(`could not execute command "${opts.file}": ${err instanceof Error ? err.message : String(err)}`);
|
|
648
|
+
}
|
|
649
|
+
writable = program.writable;
|
|
650
|
+
const p = program;
|
|
651
|
+
cleanups.push(async () => {
|
|
652
|
+
try {
|
|
653
|
+
p.child.stdin?.end();
|
|
654
|
+
} catch {}
|
|
655
|
+
await p.closed;
|
|
656
|
+
});
|
|
657
|
+
} else {
|
|
658
|
+
try {
|
|
659
|
+
if ((await promises.stat(opts.file).catch(() => null))?.isDirectory()) return failWith(`${opts.file}: cannot copy from/to a directory`);
|
|
660
|
+
} catch {}
|
|
661
|
+
const stream = createWriteStream(opts.file);
|
|
662
|
+
stream.once("error", (e) => {
|
|
663
|
+
fileWriteErrors.push(e);
|
|
664
|
+
});
|
|
665
|
+
writable = stream;
|
|
666
|
+
cleanups.push(() => new Promise((resolve, reject) => {
|
|
667
|
+
stream.end((err) => {
|
|
668
|
+
if (err) reject(err);
|
|
669
|
+
else resolve();
|
|
670
|
+
});
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
else writable = process.stdout;
|
|
674
|
+
let tag = null;
|
|
675
|
+
try {
|
|
676
|
+
if (opts.direction === "from") {
|
|
677
|
+
if (readable === null) return failWith("no input stream for COPY FROM");
|
|
678
|
+
const copyIn = await conn.startCopyIn(sql);
|
|
679
|
+
if (fromStdin && !isCopyBinaryFormat(opts.beforeToFrom, opts.afterToFrom)) await pumpStdinWithEofMarker(readable, copyIn);
|
|
680
|
+
else await pumpReadable(conn, readable, copyIn);
|
|
681
|
+
} else {
|
|
682
|
+
if (writable === null) return failWith("no output stream for COPY TO");
|
|
683
|
+
await drainCopyTo(conn, sql, writable);
|
|
684
|
+
if (fileWriteErrors.length > 0) return failWith(fileWriteErrors[0].message);
|
|
685
|
+
}
|
|
686
|
+
if (program !== null) {
|
|
687
|
+
program.writable?.end();
|
|
688
|
+
const exit = await program.closed;
|
|
689
|
+
const progErr = describeProgramExit(opts.file ?? "", exit);
|
|
690
|
+
if (progErr !== null) throw new Error(progErr);
|
|
691
|
+
}
|
|
692
|
+
tag = readLastCopyTag(conn);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
return failWith(err instanceof Error ? err.message : String(err));
|
|
695
|
+
} finally {
|
|
696
|
+
for (const c of cleanups) try {
|
|
697
|
+
await c();
|
|
698
|
+
} catch {}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
ok: true,
|
|
702
|
+
tag
|
|
703
|
+
};
|
|
920
704
|
};
|
|
921
705
|
/**
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
706
|
+
* Read the connection's `lastCopyTag` if the implementation exposes it.
|
|
707
|
+
* PgConnection sets this property after each COPY; mock connections in tests
|
|
708
|
+
* may not, in which case we return null and the caller prints just `COPY`.
|
|
709
|
+
*/
|
|
926
710
|
const readLastCopyTag = (conn) => {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
return null;
|
|
711
|
+
const maybe = conn.lastCopyTag;
|
|
712
|
+
if (typeof maybe === "string") return maybe;
|
|
713
|
+
return null;
|
|
931
714
|
};
|
|
932
|
-
// ---------------------------------------------------------------------------
|
|
933
|
-
// Backslash command registration
|
|
934
|
-
// ---------------------------------------------------------------------------
|
|
935
715
|
/**
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
},
|
|
716
|
+
* `\copy` command spec. Mirrors upstream's `exec_command_a_or_copy` path
|
|
717
|
+
* (well, just the copy half). On success we print the trailing `COPY <N>`
|
|
718
|
+
* footer to stdout, matching `do_copy`'s expectation that SendQuery's normal
|
|
719
|
+
* result-printing pipeline emits the tag.
|
|
720
|
+
*/
|
|
721
|
+
const cmdCopy = {
|
|
722
|
+
name: "copy",
|
|
723
|
+
helpKey: "copy",
|
|
724
|
+
async run(ctx) {
|
|
725
|
+
if (!ctx.settings.db) {
|
|
726
|
+
ctx.settings.lastErrorResult = { message: "no connection to the server" };
|
|
727
|
+
writeErr("\\copy: no connection to the server\n");
|
|
728
|
+
return { status: "error" };
|
|
729
|
+
}
|
|
730
|
+
if (getPipelineState(ctx.settings) !== null) {
|
|
731
|
+
ctx.settings.lastErrorResult = { message: COPY_IN_PIPELINE_MSG };
|
|
732
|
+
writeErr(`\\copy: ${COPY_IN_PIPELINE_MSG}\n`);
|
|
733
|
+
try {
|
|
734
|
+
await ctx.settings.db.close();
|
|
735
|
+
} catch {}
|
|
736
|
+
return { status: "error" };
|
|
737
|
+
}
|
|
738
|
+
const raw = ctx.restOfLine();
|
|
739
|
+
if (raw.trim().length === 0) {
|
|
740
|
+
ctx.settings.lastErrorResult = { message: "arguments required" };
|
|
741
|
+
writeErr("\\copy: arguments required\n");
|
|
742
|
+
return { status: "error" };
|
|
743
|
+
}
|
|
744
|
+
const parsed = parseSlashCopy(raw);
|
|
745
|
+
if (!parsed.ok) {
|
|
746
|
+
ctx.settings.lastErrorResult = { message: parsed.error };
|
|
747
|
+
writeErr(`\\copy: ${parsed.error}\n`);
|
|
748
|
+
return { status: "error" };
|
|
749
|
+
}
|
|
750
|
+
const result = await doCopy(ctx.settings.db, parsed.value);
|
|
751
|
+
if (!result.ok) {
|
|
752
|
+
ctx.settings.lastErrorResult = { message: result.error };
|
|
753
|
+
writeErr(`\\copy: ${result.error}\n`);
|
|
754
|
+
return { status: "error" };
|
|
755
|
+
}
|
|
756
|
+
if (!(parsed.value.direction === "to" && parsed.value.file === null && !parsed.value.program)) {
|
|
757
|
+
const rows = parseCopyTagRows(result.tag);
|
|
758
|
+
if (result.tag !== null && rows !== null) writeOut(`COPY ${String(rows)}\n`);
|
|
759
|
+
else if (result.tag !== null) writeOut(`${result.tag}\n`);
|
|
760
|
+
else writeOut("COPY\n");
|
|
761
|
+
}
|
|
762
|
+
return { status: "ok" };
|
|
763
|
+
}
|
|
1020
764
|
};
|
|
1021
765
|
/**
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
766
|
+
* Register the `\copy` command on the supplied registry. Called from
|
|
767
|
+
* `dispatch.ts::defaultRegistry()` (one new line).
|
|
768
|
+
*/
|
|
769
|
+
const registerCopyCommands = (registry) => {
|
|
770
|
+
registry.register(cmdCopy);
|
|
1027
771
|
};
|
|
1028
|
-
// Re-export for direct callers that want to bypass the dispatcher (tests).
|
|
1029
|
-
export { buildCopySql, pumpStdinWithEofMarker };
|
|
1030
772
|
/**
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
773
|
+
* Convenience: encode a JS string as UTF-8 bytes for COPY FROM. Exposed so
|
|
774
|
+
* tests can feed a `Buffer` to {@link doCopy} without re-implementing the
|
|
775
|
+
* Readable shim.
|
|
776
|
+
*/
|
|
777
|
+
const toBuffer = (s) => Buffer.from(s, "utf8");
|
|
778
|
+
//#endregion
|
|
779
|
+
export { COPY_BINARY_SIGNATURE, COPY_IN_PIPELINE_MSG, buildCopySql, cmdCopy, doCopy, isCopyBinaryFormat, isCopyTextFormat, parseSlashCopy, pumpStdinWithEofMarker, registerCopyCommands, toBuffer, validateCopyBinarySignature };
|