neon 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +354 -517
- package/dist/_shared/env-core/reuse-secrets.js +159 -203
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -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 +243 -303
- 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 +614 -845
- 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 +313 -394
- 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 +36 -38
- 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 +727 -983
- 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 +1293 -2082
- 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 +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -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 +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- 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 +626 -815
- 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 +1414 -1927
- 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 +2 -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 +37 -44
- 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 +7 -5
|
@@ -1,86 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* psql I/O & control backslash commands.
|
|
3
|
-
*
|
|
4
|
-
* TypeScript port of the following `exec_command_*` functions in upstream
|
|
5
|
-
* PostgreSQL's `src/bin/psql/command.c`:
|
|
6
|
-
*
|
|
7
|
-
* - `\i`, `\include` → exec_command_include (normal)
|
|
8
|
-
* - `\ir`, `\include_relative` → exec_command_include (relative=true)
|
|
9
|
-
* - `\o`, `\out` → exec_command_out
|
|
10
|
-
* - `\w`, `\write` → exec_command_write
|
|
11
|
-
* - `\g` → exec_command_g
|
|
12
|
-
* - `\gx` → exec_command_g (force_expanded=true)
|
|
13
|
-
* - `\gset` → exec_command_gset
|
|
14
|
-
* - `\gdesc` → exec_command_gdesc
|
|
15
|
-
* - `\gexec` → exec_command_gexec
|
|
16
|
-
* - `\watch` → exec_command_watch
|
|
17
|
-
*
|
|
18
|
-
* Each is exported as a `BackslashCmdSpec` and registered via
|
|
19
|
-
* {@link registerIoCommands}. The single line that wires us into the
|
|
20
|
-
* default dispatcher lives in `dispatch.ts::defaultRegistry()`.
|
|
21
|
-
*
|
|
22
|
-
* # Integration touch-points and known limitations
|
|
23
|
-
*
|
|
24
|
-
* Several of these commands really want to participate in the mainloop's
|
|
25
|
-
* scanner/printer pipeline. This WP keeps `src/psql/core/mainloop.ts`
|
|
26
|
-
* untouched, so we provide the data structures and let a follow-up WP wire
|
|
27
|
-
* the consumption sites. Limitations documented per-command:
|
|
28
|
-
*
|
|
29
|
-
* - `\i FILE` enqueues the file's contents on a small input queue
|
|
30
|
-
* (`./inputQueue.ts`) AND, as a stop-gap, executes the file's SQL
|
|
31
|
-
* directly via `Connection.execSimple`. Backslash commands embedded in
|
|
32
|
-
* the file are NOT processed by the scanner; the include is a "best
|
|
33
|
-
* effort: run as one big SQL blob". Once mainloop adopts the queue API
|
|
34
|
-
* this becomes a true include.
|
|
35
|
-
*
|
|
36
|
-
* - `\o FILE` opens a writable stream and stashes it under a symbol on
|
|
37
|
-
* `settings`. We expose a getter (`getQueryFout`) for the mainloop to
|
|
38
|
-
* consult; until that wiring happens, query output continues to flow
|
|
39
|
-
* to the mainloop's `ctx.stdout`. The stash + close-on-rebind logic is
|
|
40
|
-
* in place and fully tested.
|
|
41
|
-
*
|
|
42
|
-
* - `\g` (no arg) executes the current queryBuf directly through
|
|
43
|
-
* `Connection.execSimple` and renders via the aligned printer. This
|
|
44
|
-
* duplicates a tiny slice of mainloop's send/print pipeline, which is
|
|
45
|
-
* fine for the bytewise-simple cases this WP needs to support. For
|
|
46
|
-
* `\g FILE` / `\g |cmd` the output goes through the temporary writer.
|
|
47
|
-
*
|
|
48
|
-
* - `\gx` toggles `topt.expanded` for the single execution and restores
|
|
49
|
-
* the prior value in a `try { ... } finally { ... }`.
|
|
50
|
-
*
|
|
51
|
-
* - `\gset [PREFIX]` executes via `execSimple`, requires the last result
|
|
52
|
-
* to have exactly one row, and stores `${prefix}${colname}` → value
|
|
53
|
-
* for each column on `settings.vars`.
|
|
54
|
-
*
|
|
55
|
-
* - `\gdesc` parses the buffered query with the extended protocol
|
|
56
|
-
* (Parse + Describe by statement, no Execute), then assembles a
|
|
57
|
-
* synthetic `Column / Type` ResultSet and renders it through the
|
|
58
|
-
* active printer (`alignedPrinter` by default; the format picker
|
|
59
|
-
* honours `\pset format`). Tuples-only mode (`\t on`) and `\o FILE`
|
|
60
|
-
* redirects ride along automatically because the same ResultSet
|
|
61
|
-
* goes through the same printer the REPL would use for a query.
|
|
62
|
-
*
|
|
63
|
-
* - `\gexec` iterates the cells of the last result row-major and feeds
|
|
64
|
-
* each non-null cell back as SQL through `execSimple`. Each statement's
|
|
65
|
-
* output is rendered to stdout (or to the active queryFout stash).
|
|
66
|
-
*
|
|
67
|
-
* - `\watch [INTERVAL]` re-executes the queryBuf every `INTERVAL` seconds
|
|
68
|
-
* (default 2) until SIGINT or until the iteration count limit is hit.
|
|
69
|
-
* We hook SIGINT via a transient listener that's removed on completion.
|
|
70
|
-
* Tests bypass the listener by using an AbortController exposed via
|
|
71
|
-
* `WATCH_TEST_CONTROLLER`.
|
|
72
|
-
*
|
|
73
|
-
* # Error format
|
|
74
|
-
*
|
|
75
|
-
* Upstream prints `<cmd>: <msg>` to stderr and returns failure. We mirror
|
|
76
|
-
* that and also stash the message on `settings.lastErrorResult` so the
|
|
77
|
-
* mainloop's `writeError()` wrapper can pick it up.
|
|
78
|
-
*/
|
|
79
|
-
import { spawn } from "node:child_process";
|
|
80
|
-
import { closeSync, createWriteStream, promises as fsPromises, fsyncSync, openSync, } from "node:fs";
|
|
81
|
-
import { platform } from "node:os";
|
|
82
|
-
import * as path from "node:path";
|
|
83
|
-
import { captureLastError, refreshErrorVars, stripLeadingCommentsAndWS, } from "../core/common.js";
|
|
1
|
+
import { writeErr, writeOut } from "./shared.js";
|
|
84
2
|
import { alignedPrinter } from "../print/aligned.js";
|
|
85
3
|
import { asciidocPrinter } from "../print/asciidoc.js";
|
|
86
4
|
import { csvPrinter } from "../print/csv.js";
|
|
@@ -89,2105 +7,1398 @@ import { jsonPrinter } from "../print/json.js";
|
|
|
89
7
|
import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
|
|
90
8
|
import { troffMsPrinter } from "../print/troff.js";
|
|
91
9
|
import { unalignedPrinter } from "../print/unaligned.js";
|
|
92
|
-
import { applyPset } from "./cmd_format.js";
|
|
93
10
|
import { formatErrorReport, psqlErrorPrefix } from "./cmd_meta.js";
|
|
94
|
-
import {
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
11
|
+
import { captureLastError, refreshErrorVars, stripLeadingCommentsAndWS } from "../core/common.js";
|
|
12
|
+
import { applyPset } from "./cmd_format.js";
|
|
13
|
+
import { consumeBindState, lookupPrepared, stagedNamedBindPresent } from "./cmd_pipeline.js";
|
|
14
|
+
import { closeSync, createWriteStream, fsyncSync, openSync, promises } from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { platform } from "node:os";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
//#region src/psql/command/cmd_io.ts
|
|
19
|
+
/**
|
|
20
|
+
* psql I/O & control backslash commands.
|
|
21
|
+
*
|
|
22
|
+
* TypeScript port of the following `exec_command_*` functions in upstream
|
|
23
|
+
* PostgreSQL's `src/bin/psql/command.c`:
|
|
24
|
+
*
|
|
25
|
+
* - `\i`, `\include` → exec_command_include (normal)
|
|
26
|
+
* - `\ir`, `\include_relative` → exec_command_include (relative=true)
|
|
27
|
+
* - `\o`, `\out` → exec_command_out
|
|
28
|
+
* - `\w`, `\write` → exec_command_write
|
|
29
|
+
* - `\g` → exec_command_g
|
|
30
|
+
* - `\gx` → exec_command_g (force_expanded=true)
|
|
31
|
+
* - `\gset` → exec_command_gset
|
|
32
|
+
* - `\gdesc` → exec_command_gdesc
|
|
33
|
+
* - `\gexec` → exec_command_gexec
|
|
34
|
+
* - `\watch` → exec_command_watch
|
|
35
|
+
*
|
|
36
|
+
* Each is exported as a `BackslashCmdSpec` and registered via
|
|
37
|
+
* {@link registerIoCommands}. The single line that wires us into the
|
|
38
|
+
* default dispatcher lives in `dispatch.ts::defaultRegistry()`.
|
|
39
|
+
*
|
|
40
|
+
* # Integration touch-points and known limitations
|
|
41
|
+
*
|
|
42
|
+
* Several of these commands really want to participate in the mainloop's
|
|
43
|
+
* scanner/printer pipeline. This WP keeps `src/psql/core/mainloop.ts`
|
|
44
|
+
* untouched, so we provide the data structures and let a follow-up WP wire
|
|
45
|
+
* the consumption sites. Limitations documented per-command:
|
|
46
|
+
*
|
|
47
|
+
* - `\i FILE` enqueues the file's contents on a small input queue
|
|
48
|
+
* (`./inputQueue.ts`) AND, as a stop-gap, executes the file's SQL
|
|
49
|
+
* directly via `Connection.execSimple`. Backslash commands embedded in
|
|
50
|
+
* the file are NOT processed by the scanner; the include is a "best
|
|
51
|
+
* effort: run as one big SQL blob". Once mainloop adopts the queue API
|
|
52
|
+
* this becomes a true include.
|
|
53
|
+
*
|
|
54
|
+
* - `\o FILE` opens a writable stream and stashes it under a symbol on
|
|
55
|
+
* `settings`. We expose a getter (`getQueryFout`) for the mainloop to
|
|
56
|
+
* consult; until that wiring happens, query output continues to flow
|
|
57
|
+
* to the mainloop's `ctx.stdout`. The stash + close-on-rebind logic is
|
|
58
|
+
* in place and fully tested.
|
|
59
|
+
*
|
|
60
|
+
* - `\g` (no arg) executes the current queryBuf directly through
|
|
61
|
+
* `Connection.execSimple` and renders via the aligned printer. This
|
|
62
|
+
* duplicates a tiny slice of mainloop's send/print pipeline, which is
|
|
63
|
+
* fine for the bytewise-simple cases this WP needs to support. For
|
|
64
|
+
* `\g FILE` / `\g |cmd` the output goes through the temporary writer.
|
|
65
|
+
*
|
|
66
|
+
* - `\gx` toggles `topt.expanded` for the single execution and restores
|
|
67
|
+
* the prior value in a `try { ... } finally { ... }`.
|
|
68
|
+
*
|
|
69
|
+
* - `\gset [PREFIX]` executes via `execSimple`, requires the last result
|
|
70
|
+
* to have exactly one row, and stores `${prefix}${colname}` → value
|
|
71
|
+
* for each column on `settings.vars`.
|
|
72
|
+
*
|
|
73
|
+
* - `\gdesc` parses the buffered query with the extended protocol
|
|
74
|
+
* (Parse + Describe by statement, no Execute), then assembles a
|
|
75
|
+
* synthetic `Column / Type` ResultSet and renders it through the
|
|
76
|
+
* active printer (`alignedPrinter` by default; the format picker
|
|
77
|
+
* honours `\pset format`). Tuples-only mode (`\t on`) and `\o FILE`
|
|
78
|
+
* redirects ride along automatically because the same ResultSet
|
|
79
|
+
* goes through the same printer the REPL would use for a query.
|
|
80
|
+
*
|
|
81
|
+
* - `\gexec` iterates the cells of the last result row-major and feeds
|
|
82
|
+
* each non-null cell back as SQL through `execSimple`. Each statement's
|
|
83
|
+
* output is rendered to stdout (or to the active queryFout stash).
|
|
84
|
+
*
|
|
85
|
+
* - `\watch [INTERVAL]` re-executes the queryBuf every `INTERVAL` seconds
|
|
86
|
+
* (default 2) until SIGINT or until the iteration count limit is hit.
|
|
87
|
+
* We hook SIGINT via a transient listener that's removed on completion.
|
|
88
|
+
* Tests bypass the listener by using an AbortController exposed via
|
|
89
|
+
* `WATCH_TEST_CONTROLLER`.
|
|
90
|
+
*
|
|
91
|
+
* # Error format
|
|
92
|
+
*
|
|
93
|
+
* Upstream prints `<cmd>: <msg>` to stderr and returns failure. We mirror
|
|
94
|
+
* that and also stash the message on `settings.lastErrorResult` so the
|
|
95
|
+
* mainloop's `writeError()` wrapper can pick it up.
|
|
96
|
+
*/
|
|
104
97
|
const QUERY_FOUT_KEY = Symbol.for("neonctl.psql.queryFout");
|
|
105
98
|
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return stash[QUERY_FOUT_KEY]?.stream ?? null;
|
|
99
|
+
* Return the currently active queryFout stream (or `null` if none).
|
|
100
|
+
* The mainloop is encouraged to call this in lieu of writing directly to
|
|
101
|
+
* `ctx.stdout` for query results.
|
|
102
|
+
*/
|
|
103
|
+
const getQueryFout = (settings) => {
|
|
104
|
+
return settings[QUERY_FOUT_KEY]?.stream ?? null;
|
|
113
105
|
};
|
|
114
106
|
const setQueryFout = (settings, entry) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
stash[QUERY_FOUT_KEY] = entry;
|
|
121
|
-
}
|
|
107
|
+
const stash = settings;
|
|
108
|
+
if (entry === null) stash[QUERY_FOUT_KEY] = void 0;
|
|
109
|
+
else stash[QUERY_FOUT_KEY] = entry;
|
|
122
110
|
};
|
|
123
111
|
const closeQueryFout = async (settings) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
112
|
+
const stash = settings;
|
|
113
|
+
const prev = stash[QUERY_FOUT_KEY];
|
|
114
|
+
if (prev) {
|
|
115
|
+
stash[QUERY_FOUT_KEY] = void 0;
|
|
116
|
+
await prev.close();
|
|
117
|
+
}
|
|
130
118
|
};
|
|
131
|
-
|
|
132
|
-
// Watch SIGINT escape hatch (tests).
|
|
133
|
-
//
|
|
134
|
-
// `\watch` installs a SIGINT handler so Ctrl-C breaks the polling loop in
|
|
135
|
-
// real psql sessions. Tests need to break the loop deterministically; we
|
|
136
|
-
// expose an AbortController hook that, if set, takes precedence.
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
export const WATCH_TEST_CONTROLLER = {
|
|
139
|
-
ref: null,
|
|
140
|
-
};
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
// Small helpers.
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
119
|
+
const WATCH_TEST_CONTROLLER = { ref: null };
|
|
144
120
|
const errResult = (ctx, message) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// it would also write a `psql: ERROR: <msg>` fallback, producing a stray
|
|
153
|
-
// duplicate that breaks the `\errverbose` ordering check on tests like
|
|
154
|
-
// `SELECT error\gdesc\n\errverbose`.
|
|
155
|
-
return { status: "error", errorWritten: true };
|
|
121
|
+
ctx.settings.lastErrorResult = { message };
|
|
122
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
123
|
+
writeErr(`${prefix}\\${ctx.cmdName}: ${message}\n`);
|
|
124
|
+
return {
|
|
125
|
+
status: "error",
|
|
126
|
+
errorWritten: true
|
|
127
|
+
};
|
|
156
128
|
};
|
|
157
129
|
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
130
|
+
* Reject buffer-consuming commands when an extended pipeline is open. Upstream
|
|
131
|
+
* `exec_command_g` / `gx` / `gset` / `gexec` / `watch` all guard with
|
|
132
|
+
* `PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF` and emit
|
|
133
|
+
* `pg_log_error("\\%s not allowed in pipeline mode", cmd)` (note: no `:`
|
|
134
|
+
* after the command name — different shape from `errResult`).
|
|
135
|
+
*
|
|
136
|
+
* `\gdesc` is the odd one out: upstream uses
|
|
137
|
+
* `pg_log_error("synchronous command execution functions are not allowed in
|
|
138
|
+
* pipeline mode")` because the underlying `PQdescribePrepared`/`PQfn`-style
|
|
139
|
+
* helpers all share that text — the regress baseline asserts this exact
|
|
140
|
+
* wording at three call sites in `psql_pipeline.out`.
|
|
141
|
+
*
|
|
142
|
+
* If the command proceeded it would inject a synchronous Query/Sync into the
|
|
143
|
+
* queue, corrupt the pipeline state, and leave `\endpipeline` waiting forever.
|
|
144
|
+
*
|
|
145
|
+
* Returns `null` when not in pipeline mode (caller proceeds); otherwise
|
|
146
|
+
* returns a populated error result the caller should bubble up.
|
|
147
|
+
*
|
|
148
|
+
* Upstream psql 18.4 leaks gate diagnostics through `pg_log_error_internal`
|
|
149
|
+
* which appends to the libpq result error log on the underlying PGresult;
|
|
150
|
+
* each subsequent gate hit RE-EMITS the full accumulated log plus its own
|
|
151
|
+
* line ("Error messages accumulate and are repeated" — the regress comment
|
|
152
|
+
* is the spec). Two `\gdesc` calls back-to-back therefore emit 3 lines
|
|
153
|
+
* total: 1 for the first call, 2 for the second (one accumulated + one
|
|
154
|
+
* own). Mirror that with a settings-stashed accumulator keyed off the
|
|
155
|
+
* current pipeline session; reset when the pipeline ends.
|
|
156
|
+
*/
|
|
185
157
|
const PIPELINE_GATE_ERRORS_KEY = Symbol.for("neonctl.psql.pipelineGateErrors");
|
|
186
158
|
const getGateErrors = (settings) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
159
|
+
const s = settings;
|
|
160
|
+
let cur = s[PIPELINE_GATE_ERRORS_KEY];
|
|
161
|
+
if (!cur) {
|
|
162
|
+
cur = [];
|
|
163
|
+
s[PIPELINE_GATE_ERRORS_KEY] = cur;
|
|
164
|
+
}
|
|
165
|
+
return cur;
|
|
194
166
|
};
|
|
195
167
|
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
168
|
+
* Drop the accumulated pipeline-gate errors. Called from
|
|
169
|
+
* `\endpipeline` so the next pipeline session starts fresh — without
|
|
170
|
+
* this, gate errors from a closed pipeline would leak into the next
|
|
171
|
+
* one.
|
|
172
|
+
*/
|
|
173
|
+
const clearPipelineGateErrors = (settings) => {
|
|
174
|
+
const s = settings;
|
|
175
|
+
s[PIPELINE_GATE_ERRORS_KEY] = void 0;
|
|
204
176
|
};
|
|
205
177
|
const pipelineGate = (ctx) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// `\gset`, `\gexec`, `\watch`) emit a single line per invocation
|
|
220
|
-
// and do NOT participate in the accumulator.
|
|
221
|
-
if (ctx.cmdName === "gdesc") {
|
|
222
|
-
const log = getGateErrors(ctx.settings);
|
|
223
|
-
log.push(message);
|
|
224
|
-
for (const m of log) {
|
|
225
|
-
writeErr(`${prefix}${m}\n`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
writeErr(`${prefix}${message}\n`);
|
|
230
|
-
}
|
|
231
|
-
return { status: "error", errorWritten: true };
|
|
178
|
+
if (ctx.settings.sendMode !== "extended-pipeline") return null;
|
|
179
|
+
const message = ctx.cmdName === "gdesc" ? "synchronous command execution functions are not allowed in pipeline mode" : `\\${ctx.cmdName} not allowed in pipeline mode`;
|
|
180
|
+
ctx.settings.lastErrorResult = { message };
|
|
181
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
182
|
+
if (ctx.cmdName === "gdesc") {
|
|
183
|
+
const log = getGateErrors(ctx.settings);
|
|
184
|
+
log.push(message);
|
|
185
|
+
for (const m of log) writeErr(`${prefix}${m}\n`);
|
|
186
|
+
} else writeErr(`${prefix}${message}\n`);
|
|
187
|
+
return {
|
|
188
|
+
status: "error",
|
|
189
|
+
errorWritten: true
|
|
190
|
+
};
|
|
232
191
|
};
|
|
233
192
|
/**
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const UPSTREAM_SPECIAL_VAR_NAMES = new Set([
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
193
|
+
* Set of psql variables upstream marks as "specially treated" — i.e. names
|
|
194
|
+
* that have a substitute / assign hook installed in `startup.c`'s
|
|
195
|
+
* `EstablishVariableSpace`. Used by `\gset` to reject assignments into
|
|
196
|
+
* those names (matching upstream `StoreQueryTuple`'s `VariableHasHook`
|
|
197
|
+
* check). We mirror the upstream list directly so a `\gset IGNORE` into
|
|
198
|
+
* `IGNOREEOF` produces the conformance-expected warning even though our
|
|
199
|
+
* settings.ts hasn't installed the IGNOREEOF / HISTFILE hooks yet — that
|
|
200
|
+
* gap is tracked separately and harmless because the values are read-only
|
|
201
|
+
* for us.
|
|
202
|
+
*/
|
|
203
|
+
const UPSTREAM_SPECIAL_VAR_NAMES = /* @__PURE__ */ new Set([
|
|
204
|
+
"AUTOCOMMIT",
|
|
205
|
+
"COMP_KEYWORD_CASE",
|
|
206
|
+
"ECHO",
|
|
207
|
+
"ECHO_HIDDEN",
|
|
208
|
+
"FETCH_COUNT",
|
|
209
|
+
"HIDE_TABLEAM",
|
|
210
|
+
"HIDE_TOAST_COMPRESSION",
|
|
211
|
+
"HISTCONTROL",
|
|
212
|
+
"HISTFILE",
|
|
213
|
+
"HISTSIZE",
|
|
214
|
+
"IGNOREEOF",
|
|
215
|
+
"ON_ERROR_ROLLBACK",
|
|
216
|
+
"ON_ERROR_STOP",
|
|
217
|
+
"PROMPT1",
|
|
218
|
+
"PROMPT2",
|
|
219
|
+
"PROMPT3",
|
|
220
|
+
"QUIET",
|
|
221
|
+
"SHOW_ALL_RESULTS",
|
|
222
|
+
"SHOW_CONTEXT",
|
|
223
|
+
"SINGLELINE",
|
|
224
|
+
"SINGLESTEP",
|
|
225
|
+
"VERBOSITY"
|
|
267
226
|
]);
|
|
268
227
|
/**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) ||
|
|
277
|
-
UPSTREAM_SPECIAL_VAR_NAMES.has(name);
|
|
278
|
-
// `stripLeadingCommentsAndWS` lives in core/common.ts so the wire path
|
|
279
|
-
// (sendQuery / executeAndPrint) and the slash-command paths share one
|
|
280
|
-
// implementation. Re-imported from there at the top of the file.
|
|
228
|
+
* True when `name` is a psql variable that `\gset` must skip with an
|
|
229
|
+
* "attempt to \gset into specially treated variable" message. Combines the
|
|
230
|
+
* registered-hook check (so future hook installations are automatically
|
|
231
|
+
* covered) with the upstream-canonical list above (so cases like
|
|
232
|
+
* IGNOREEOF that aren't hooked in our settings.ts still match upstream's
|
|
233
|
+
* `\gset` behaviour exactly).
|
|
234
|
+
*/
|
|
235
|
+
const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) || UPSTREAM_SPECIAL_VAR_NAMES.has(name);
|
|
281
236
|
/**
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
237
|
+
* Strip line and block comments from `sql` so a COPY-shaped token inside a
|
|
238
|
+
* comment (e.g. dash-dash `COPY t TO STDOUT`) doesn't trigger the
|
|
239
|
+
* `\g FILE` mid-batch sink. Mirrors the cheap normaliser the mainloop uses
|
|
240
|
+
* before wiring `copyOutMidBatchSink`. Embedded literals are NOT stripped —
|
|
241
|
+
* `'COPY x TO STDOUT'` would still match `hasCopyToStdout`, but that's the
|
|
242
|
+
* same false-positive shape upstream tolerates (the regex sweep is
|
|
243
|
+
* intentionally conservative, and the worst-case outcome is "route bytes
|
|
244
|
+
* that never arrive to the file" — harmless).
|
|
245
|
+
*/
|
|
291
246
|
const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/--[^\n]*/gu, "");
|
|
292
247
|
/**
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
248
|
+
* True when `sql` contains at least one `COPY ... TO STDOUT` segment.
|
|
249
|
+
* Used by `runGCore` to install a CopyData sink while `\g` / `\gx` /
|
|
250
|
+
* `\g FILE` / `\g |cmd` dispatches a `\;`-chained batch that mixes
|
|
251
|
+
* COPY-OUT with regular SELECT statements. Without the sink the wire
|
|
252
|
+
* layer drops the CopyData bytes on the floor, and the file/pipe ends
|
|
253
|
+
* up with the surrounding tuple results only — see regress/psql lines
|
|
254
|
+
* 5760-5787 (`COPY (SELECT 'foo') TO STDOUT \; COPY (SELECT 'bar') TO
|
|
255
|
+
* STDOUT \g :g_out_file`).
|
|
256
|
+
*/
|
|
302
257
|
const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlCommentsForCopyScan(sql));
|
|
303
258
|
/**
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
259
|
+
* Render a server-side error in upstream psql's 3-line shape (severity +
|
|
260
|
+
* message, then `LINE N:` / `^` re-print) and refresh the `:LAST_ERROR_*`
|
|
261
|
+
* diagnostic variables so a subsequent `\errverbose` sees the rich payload.
|
|
262
|
+
*
|
|
263
|
+
* Mirrors the path that `core/common.ts::writeQueryError` takes for top-level
|
|
264
|
+
* statement errors: capture full ErrorResponse fields onto
|
|
265
|
+
* `settings.lastErrorResult`, render via `formatErrorReport` (honouring
|
|
266
|
+
* VERBOSITY + SHOW_CONTEXT), prefix only the leading severity line with
|
|
267
|
+
* `psql:[<file>:<n>]:`, and update the per-statement diagnostic vars via
|
|
268
|
+
* `refreshErrorVars`.
|
|
269
|
+
*
|
|
270
|
+
* Used by `\g`, `\gx`, `\gset`, `\gdesc`, and `\gexec` so a server-rejected
|
|
271
|
+
* statement dispatched through them renders the same shape vanilla psql
|
|
272
|
+
* produces, instead of the legacy `\<cmd>: <message>` one-liner.
|
|
273
|
+
*/
|
|
319
274
|
const formatServerError = (ctx, err, sql) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// Refresh `:SQLSTATE`, `:ERROR`, `:LAST_ERROR_*`, `:ROW_COUNT` so the
|
|
336
|
-
// following `\echo :LAST_ERROR_MESSAGE` and `\errverbose` see the new
|
|
337
|
-
// outcome. Matches upstream's `SetErrorVariables` call after every
|
|
338
|
-
// failed dispatch.
|
|
339
|
-
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
340
|
-
return { status: "error", errorWritten: true };
|
|
275
|
+
const msg = captureLastError(ctx.settings, err, sql);
|
|
276
|
+
const e = ctx.settings.lastErrorResult;
|
|
277
|
+
if (e) {
|
|
278
|
+
const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
|
|
279
|
+
const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
|
|
280
|
+
writeErr(prefixed.join("\n") + "\n");
|
|
281
|
+
} else {
|
|
282
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
283
|
+
writeErr(`${prefix}ERROR: ${msg}\n`);
|
|
284
|
+
}
|
|
285
|
+
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
286
|
+
return {
|
|
287
|
+
status: "error",
|
|
288
|
+
errorWritten: true
|
|
289
|
+
};
|
|
341
290
|
};
|
|
342
291
|
/**
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
292
|
+
* Open a writable destination for `\o` / `\w` / `\g FILE` / `\g |cmd`.
|
|
293
|
+
*
|
|
294
|
+
* `target` of the form `|cmd` spawns `sh -c cmd` and pipes to its stdin.
|
|
295
|
+
* The returned closer waits for the child to exit and resolves to its
|
|
296
|
+
* status + terminating signal (if any) so callers can render
|
|
297
|
+
* `wait_result_to_str`-style errors. Any other string is treated as a
|
|
298
|
+
* file path; the file is truncated.
|
|
299
|
+
*/
|
|
351
300
|
const openWriter = (target) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
catch {
|
|
434
|
-
// swallow — the write error takes precedence
|
|
435
|
-
}
|
|
436
|
-
reject(writeError);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
// `stream.end(cb)` fires after the internal buffer drains to the
|
|
440
|
-
// underlying fd. Once that returns, the fd still holds dirty data
|
|
441
|
-
// in the kernel buffer cache; on macOS + Docker bind mounts the
|
|
442
|
-
// server inside the container can read the file before the cache
|
|
443
|
-
// flushes through to the bind mount, returning a partial view.
|
|
444
|
-
// Force an fsync against the open fd before closing so the
|
|
445
|
-
// bytes are guaranteed visible to subsequent reads — including
|
|
446
|
-
// server-side `COPY FROM` reading via the mount.
|
|
447
|
-
stream.end((err) => {
|
|
448
|
-
if (err) {
|
|
449
|
-
try {
|
|
450
|
-
closeSync(fd);
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
// swallow — the original error takes precedence
|
|
454
|
-
}
|
|
455
|
-
reject(err);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
try {
|
|
459
|
-
fsyncSync(fd);
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
// ignore — fsync best-effort; the close below still cleans up.
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
closeSync(fd);
|
|
466
|
-
}
|
|
467
|
-
catch (closeErr) {
|
|
468
|
-
reject(closeErr);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
// Docker Desktop on macOS uses virtiofs/gRPC-FUSE for bind
|
|
472
|
-
// mounts; cache propagation from host writes to the container's
|
|
473
|
-
// view is eventual, not synchronous. A subsequent server-side
|
|
474
|
-
// `COPY FROM '/bind/mount/file'` can read a partial view even
|
|
475
|
-
// though the file is fully synced on the host. Linux + Windows
|
|
476
|
-
// bind mounts are coherent, so this branch is macOS-only.
|
|
477
|
-
if (platform() === "darwin") {
|
|
478
|
-
setTimeout(() => {
|
|
479
|
-
resolve({});
|
|
480
|
-
}, 25);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
resolve({});
|
|
484
|
-
});
|
|
485
|
-
}),
|
|
486
|
-
};
|
|
301
|
+
if (target.startsWith("|")) {
|
|
302
|
+
const cmd = target.slice(1);
|
|
303
|
+
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
304
|
+
"pipe",
|
|
305
|
+
"inherit",
|
|
306
|
+
"inherit"
|
|
307
|
+
] });
|
|
308
|
+
child.stdin.on("error", (err) => {
|
|
309
|
+
if (err.code !== "EPIPE") throw err;
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
stream: child.stdin,
|
|
313
|
+
isPipe: true,
|
|
314
|
+
close: () => new Promise((resolve) => {
|
|
315
|
+
let settled = false;
|
|
316
|
+
const finish = (code, signal) => {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
settled = true;
|
|
319
|
+
resolve({
|
|
320
|
+
exitCode: code,
|
|
321
|
+
signal
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
child.once("close", (code, signal) => {
|
|
325
|
+
finish(code, signal);
|
|
326
|
+
});
|
|
327
|
+
child.once("error", () => {
|
|
328
|
+
finish(127, null);
|
|
329
|
+
});
|
|
330
|
+
if (!child.stdin.destroyed) child.stdin.end();
|
|
331
|
+
})
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const fd = openSync(target, "w");
|
|
335
|
+
const stream = createWriteStream(target, {
|
|
336
|
+
encoding: "utf8",
|
|
337
|
+
fd,
|
|
338
|
+
autoClose: false
|
|
339
|
+
});
|
|
340
|
+
let writeError = null;
|
|
341
|
+
stream.on("error", (err) => {
|
|
342
|
+
writeError = writeError ?? err;
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
stream,
|
|
346
|
+
isPipe: false,
|
|
347
|
+
close: () => new Promise((resolve, reject) => {
|
|
348
|
+
if (writeError !== null) {
|
|
349
|
+
try {
|
|
350
|
+
closeSync(fd);
|
|
351
|
+
} catch {}
|
|
352
|
+
reject(writeError);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
stream.end((err) => {
|
|
356
|
+
if (err) {
|
|
357
|
+
try {
|
|
358
|
+
closeSync(fd);
|
|
359
|
+
} catch {}
|
|
360
|
+
reject(err);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
fsyncSync(fd);
|
|
365
|
+
} catch {}
|
|
366
|
+
try {
|
|
367
|
+
closeSync(fd);
|
|
368
|
+
} catch (closeErr) {
|
|
369
|
+
reject(closeErr);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (platform() === "darwin") {
|
|
373
|
+
setTimeout(() => {
|
|
374
|
+
resolve({});
|
|
375
|
+
}, 25);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
resolve({});
|
|
379
|
+
});
|
|
380
|
+
})
|
|
381
|
+
};
|
|
487
382
|
};
|
|
488
383
|
/**
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
384
|
+
* Map a Node.js errno (`err.code`) to the libc `strerror()` string
|
|
385
|
+
* upstream psql renders in its `pg_log_error("%s: %m", fname)` path.
|
|
386
|
+
*
|
|
387
|
+
* Falls back to `err.message` (with the verbose `ENOENT: ...` prefix
|
|
388
|
+
* stripped if present) so unmapped errno values still surface
|
|
389
|
+
* meaningful text instead of a cryptic Node-internal phrasing.
|
|
390
|
+
*/
|
|
496
391
|
const errnoToStrerror = (err) => {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return "Too many open files in system";
|
|
520
|
-
case "EIO":
|
|
521
|
-
return "Input/output error";
|
|
522
|
-
case "EFBIG":
|
|
523
|
-
return "File too large";
|
|
524
|
-
case "EDQUOT":
|
|
525
|
-
return "Disk quota exceeded";
|
|
526
|
-
case "EPERM":
|
|
527
|
-
return "Operation not permitted";
|
|
528
|
-
case "EINVAL":
|
|
529
|
-
return "Invalid argument";
|
|
530
|
-
default: {
|
|
531
|
-
// Strip Node's `ENOENT: no such file or directory, open '/x'`
|
|
532
|
-
// prefix when present so the fallback at least looks like the
|
|
533
|
-
// libc form. The leading `/, ` slice keeps the human-readable
|
|
534
|
-
// phrase ("no such file or directory") if Node's message
|
|
535
|
-
// mirrors the `strerror` text but lowercases it.
|
|
536
|
-
const m = /^[A-Z]+: ([^,]+)/.exec(err.message);
|
|
537
|
-
return m ? m[1] : err.message;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
392
|
+
switch (err.code) {
|
|
393
|
+
case "ENOENT": return "No such file or directory";
|
|
394
|
+
case "EACCES": return "Permission denied";
|
|
395
|
+
case "EISDIR": return "Is a directory";
|
|
396
|
+
case "ENOTDIR": return "Not a directory";
|
|
397
|
+
case "EEXIST": return "File exists";
|
|
398
|
+
case "EROFS": return "Read-only file system";
|
|
399
|
+
case "ELOOP": return "Too many levels of symbolic links";
|
|
400
|
+
case "ENAMETOOLONG": return "File name too long";
|
|
401
|
+
case "ENOSPC": return "No space left on device";
|
|
402
|
+
case "EMFILE": return "Too many open files";
|
|
403
|
+
case "ENFILE": return "Too many open files in system";
|
|
404
|
+
case "EIO": return "Input/output error";
|
|
405
|
+
case "EFBIG": return "File too large";
|
|
406
|
+
case "EDQUOT": return "Disk quota exceeded";
|
|
407
|
+
case "EPERM": return "Operation not permitted";
|
|
408
|
+
case "EINVAL": return "Invalid argument";
|
|
409
|
+
default: {
|
|
410
|
+
const m = /^[A-Z]+: ([^,]+)/.exec(err.message);
|
|
411
|
+
return m ? m[1] : err.message;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
540
414
|
};
|
|
541
415
|
/**
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
416
|
+
* Emit a file-open failure for `\g FILE`, `\o FILE`, `\w FILE` in the
|
|
417
|
+
* exact shape vanilla psql produces: a bare `<path>: <strerror>` line
|
|
418
|
+
* on stderr, no `\<cmd>:` prefix (matches `pg_log_error` under terse
|
|
419
|
+
* mode, which is what `psql -X` uses).
|
|
420
|
+
*
|
|
421
|
+
* The leading `psql:[<file>:<n>]:` tag is still applied when we're
|
|
422
|
+
* reading SQL from a `\i FILE` include — `psqlErrorPrefix` returns ''
|
|
423
|
+
* for stdin so the line stays bare for the interactive / harness case.
|
|
424
|
+
*
|
|
425
|
+
* Returns an `error` envelope with `errorWritten: true` so the mainloop
|
|
426
|
+
* doesn't write a duplicate `psql: ERROR:` fallback.
|
|
427
|
+
*/
|
|
554
428
|
const reportFileOpenFailure = (ctx, target, err) => {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
429
|
+
const line = `${target}: ${errnoToStrerror(err)}`;
|
|
430
|
+
ctx.settings.lastErrorResult = { message: line };
|
|
431
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
432
|
+
writeErr(`${prefix}${line}\n`);
|
|
433
|
+
return {
|
|
434
|
+
status: "error",
|
|
435
|
+
errorWritten: true
|
|
436
|
+
};
|
|
562
437
|
};
|
|
563
438
|
/**
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
439
|
+
* True when `err` was thrown by our synchronous `openSync` in
|
|
440
|
+
* {@link openWriter} (i.e. has an errno `code`) and the caller should
|
|
441
|
+
* render it via {@link reportFileOpenFailure} rather than the generic
|
|
442
|
+
* `\<cmd>: <msg>` path.
|
|
443
|
+
*/
|
|
569
444
|
const isFileOpenFailure = (err) => {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return typeof e.code === "string" && e.code.startsWith("E");
|
|
445
|
+
if (!err || typeof err !== "object") return false;
|
|
446
|
+
const e = err;
|
|
447
|
+
return typeof e.code === "string" && e.code.startsWith("E");
|
|
574
448
|
};
|
|
575
449
|
/**
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
450
|
+
* Format a child process exit code + signal into upstream psql's
|
|
451
|
+
* `wait_result_to_str` style. Mirrors the C helper in
|
|
452
|
+
* `src/common/wait_error.c`:
|
|
453
|
+
*
|
|
454
|
+
* - exit code 127 → `command not found`
|
|
455
|
+
* - exit code 126 → `command was not executable`
|
|
456
|
+
* - any other code → `child process exited with exit code N`
|
|
457
|
+
* - terminated by signal S → `child process was terminated by
|
|
458
|
+
* signal N: <SIG>`
|
|
459
|
+
*
|
|
460
|
+
* Returns null when the child exited cleanly (code 0, no signal).
|
|
461
|
+
*/
|
|
588
462
|
const formatChildWaitResult = (exitCode, signal) => {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return null;
|
|
596
|
-
if (exitCode === 0)
|
|
597
|
-
return null;
|
|
598
|
-
if (exitCode === 127)
|
|
599
|
-
return "command not found";
|
|
600
|
-
if (exitCode === 126)
|
|
601
|
-
return "command was not executable";
|
|
602
|
-
return `child process exited with exit code ${String(exitCode)}`;
|
|
463
|
+
if (signal) return `child process was terminated by signal: ${signal}`;
|
|
464
|
+
if (exitCode === null || exitCode === void 0) return null;
|
|
465
|
+
if (exitCode === 0) return null;
|
|
466
|
+
if (exitCode === 127) return "command not found";
|
|
467
|
+
if (exitCode === 126) return "command was not executable";
|
|
468
|
+
return `child process exited with exit code ${String(exitCode)}`;
|
|
603
469
|
};
|
|
604
470
|
/**
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
471
|
+
* Compose the `CommandComplete`-tag line upstream prints for non-tuples-
|
|
472
|
+
* producing results (DDL, DML without RETURNING, COPY). Mirrors
|
|
473
|
+
* `formatCommandTag` in `core/common.ts` — duplicated to avoid the
|
|
474
|
+
* cmd_io → common import cycle. Returns an empty string when no tag is
|
|
475
|
+
* available (e.g. `EmptyQueryResponse` carries `command = ''`).
|
|
476
|
+
*/
|
|
611
477
|
const formatCommandTagText = (rs) => {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
|
|
618
|
-
}
|
|
619
|
-
if (rs.rowCount !== null && rs.rowCount !== undefined) {
|
|
620
|
-
return `${command} ${String(rs.rowCount)}`;
|
|
621
|
-
}
|
|
622
|
-
return command;
|
|
478
|
+
const command = (rs.command || "").trim();
|
|
479
|
+
if (command.length === 0) return "";
|
|
480
|
+
if (command === "INSERT") return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
|
|
481
|
+
if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${String(rs.rowCount)}`;
|
|
482
|
+
return command;
|
|
623
483
|
};
|
|
624
484
|
/**
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
485
|
+
* Render a `ResultSet` to the supplied writable stream using the printer
|
|
486
|
+
* picked from the active `\pset format`. Upstream's `do_watch`, `do_gset`,
|
|
487
|
+
* `do_gexec`, and `\i` all funnel results through the standard query
|
|
488
|
+
* output pipeline (`ExecQueryAndProcessResults` → `printQuery`), which
|
|
489
|
+
* honours `\pset format`. Hard-coding the aligned printer here breaks the
|
|
490
|
+
* conformance harness's `psql -A` runs (which expect unaligned tuples-only
|
|
491
|
+
* output for things like `\watch` polled rows).
|
|
492
|
+
*
|
|
493
|
+
* Non-tuples-producing results (CommandComplete with `fields.length === 0`,
|
|
494
|
+
* which covers DDL, DML without RETURNING, and the post-CopyDone tag for
|
|
495
|
+
* `COPY ... TO STDOUT`) are rendered as a single status line instead of
|
|
496
|
+
* the printer's `(0 rows)` empty-table block — matching `renderResultSets`
|
|
497
|
+
* in `core/common.ts`. Tuples-only (`\t`) and quiet (`--quiet`) both
|
|
498
|
+
* suppress the tag entirely.
|
|
499
|
+
*/
|
|
640
500
|
const renderResult = async (settings, rs, out) => {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
// `COPY 1`) unless quiet / tuples-only suppresses it. Without this
|
|
651
|
-
// branch the aligned printer renders an empty header + `(0 rows)`
|
|
652
|
-
// footer for the COPY-TO-STDOUT command complete, which doesn't
|
|
653
|
-
// match upstream (where the data already streamed via the COPY-OUT
|
|
654
|
-
// sink and the tag goes to the user's status stream, not the
|
|
655
|
-
// queryFout). The regress fixture sets QUIET=true before the
|
|
656
|
-
// COPY-OUT `\g` shape so the tag stays out of the file under test.
|
|
657
|
-
// For COPY-out results, the tag is suppressed regardless — the bytes
|
|
658
|
-
// already flowed; upstream's `handleCopyOut` doesn't emit `COPY N`
|
|
659
|
-
// on the queryFout.
|
|
660
|
-
if (!settings.popt.topt.tuplesOnly &&
|
|
661
|
-
!settings.quiet &&
|
|
662
|
-
!rs.copyOutBytes) {
|
|
663
|
-
const tag = formatCommandTagText(rs);
|
|
664
|
-
if (tag.length > 0)
|
|
665
|
-
out.write(`${tag}\n`);
|
|
666
|
-
}
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
await pickActivePrinter(settings).printQuery(rs, settings.popt, out);
|
|
501
|
+
if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) out.write(chunk);
|
|
502
|
+
if (rs.fields.length === 0) {
|
|
503
|
+
if (!settings.popt.topt.tuplesOnly && !settings.quiet && !rs.copyOutBytes) {
|
|
504
|
+
const tag = formatCommandTagText(rs);
|
|
505
|
+
if (tag.length > 0) out.write(`${tag}\n`);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
await pickActivePrinter(settings).printQuery(rs, settings.popt, out);
|
|
670
510
|
};
|
|
671
511
|
/**
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
512
|
+
* Pick the printer for the active output format. Mirrors `pickPrinter`
|
|
513
|
+
* in `core/common.ts` — duplicated here to avoid the cmd_io → common
|
|
514
|
+
* import cycle (common.ts depends on this file for `getQueryFout`).
|
|
515
|
+
*
|
|
516
|
+
* `wrapped` falls back to the aligned printer (which renders `wrapped`
|
|
517
|
+
* mode itself via `topt.format`).
|
|
518
|
+
*/
|
|
679
519
|
const pickActivePrinter = (settings) => {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
return asciidocPrinter;
|
|
694
|
-
case "latex":
|
|
695
|
-
return latexPrinter;
|
|
696
|
-
case "latex-longtable":
|
|
697
|
-
return latexLongtablePrinter;
|
|
698
|
-
case "troff-ms":
|
|
699
|
-
return troffMsPrinter;
|
|
700
|
-
default:
|
|
701
|
-
return alignedPrinter;
|
|
702
|
-
}
|
|
520
|
+
switch (settings.popt.topt.format) {
|
|
521
|
+
case "aligned":
|
|
522
|
+
case "wrapped": return alignedPrinter;
|
|
523
|
+
case "unaligned": return unalignedPrinter;
|
|
524
|
+
case "csv": return csvPrinter;
|
|
525
|
+
case "json": return jsonPrinter;
|
|
526
|
+
case "html": return htmlPrinter;
|
|
527
|
+
case "asciidoc": return asciidocPrinter;
|
|
528
|
+
case "latex": return latexPrinter;
|
|
529
|
+
case "latex-longtable": return latexLongtablePrinter;
|
|
530
|
+
case "troff-ms": return troffMsPrinter;
|
|
531
|
+
default: return alignedPrinter;
|
|
532
|
+
}
|
|
703
533
|
};
|
|
704
534
|
/**
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
535
|
+
* Pick the output target for a query result.
|
|
536
|
+
*
|
|
537
|
+
* Precedence: explicit `oneShot` (e.g. `\g FILE`) > the settings stash
|
|
538
|
+
* (`\o FILE`) > `process.stdout`.
|
|
539
|
+
*/
|
|
710
540
|
const pickOut = (settings, oneShot) => {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return getQueryFout(settings) ?? process.stdout;
|
|
541
|
+
if (oneShot) return oneShot;
|
|
542
|
+
return getQueryFout(settings) ?? process.stdout;
|
|
714
543
|
};
|
|
715
|
-
// ---------------------------------------------------------------------------
|
|
716
|
-
// \i FILE / \include FILE
|
|
717
|
-
// ---------------------------------------------------------------------------
|
|
718
544
|
const runInclude = async (ctx, relative) => {
|
|
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
|
-
// AND we run execSimple). See.
|
|
749
|
-
if (!ctx.settings.db) {
|
|
750
|
-
return errResult(ctx, "no connection to the server");
|
|
751
|
-
}
|
|
752
|
-
const trimmed = contents.trim();
|
|
753
|
-
if (trimmed.length === 0) {
|
|
754
|
-
return { status: "ok" };
|
|
755
|
-
}
|
|
756
|
-
// Track the prior inputfile so `\ir` chains relative to the included
|
|
757
|
-
// file's directory.
|
|
758
|
-
const priorInputFile = ctx.settings.inputfile;
|
|
759
|
-
ctx.settings.inputfile = resolved;
|
|
760
|
-
try {
|
|
761
|
-
const results = await ctx.settings.db.execSimple(trimmed);
|
|
762
|
-
const out = pickOut(ctx.settings, null);
|
|
763
|
-
for (const rs of results) {
|
|
764
|
-
await renderResult(ctx.settings, rs, out);
|
|
765
|
-
}
|
|
766
|
-
return { status: "ok" };
|
|
767
|
-
}
|
|
768
|
-
catch (err) {
|
|
769
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
770
|
-
return errResult(ctx, msg);
|
|
771
|
-
}
|
|
772
|
-
finally {
|
|
773
|
-
ctx.settings.inputfile = priorInputFile;
|
|
774
|
-
}
|
|
545
|
+
const arg = ctx.nextArg("normal");
|
|
546
|
+
if (arg === null || arg.length === 0) return errResult(ctx, "missing required argument");
|
|
547
|
+
let resolved;
|
|
548
|
+
if (path.isAbsolute(arg)) resolved = arg;
|
|
549
|
+
else if (relative && ctx.settings.inputfile) resolved = path.resolve(path.dirname(ctx.settings.inputfile), arg);
|
|
550
|
+
else resolved = path.resolve(process.cwd(), arg);
|
|
551
|
+
let contents;
|
|
552
|
+
try {
|
|
553
|
+
contents = await promises.readFile(resolved, "utf8");
|
|
554
|
+
} catch (err) {
|
|
555
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
556
|
+
return errResult(ctx, msg);
|
|
557
|
+
}
|
|
558
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
559
|
+
const trimmed = contents.trim();
|
|
560
|
+
if (trimmed.length === 0) return { status: "ok" };
|
|
561
|
+
const priorInputFile = ctx.settings.inputfile;
|
|
562
|
+
ctx.settings.inputfile = resolved;
|
|
563
|
+
try {
|
|
564
|
+
const results = await ctx.settings.db.execSimple(trimmed);
|
|
565
|
+
const out = pickOut(ctx.settings, null);
|
|
566
|
+
for (const rs of results) await renderResult(ctx.settings, rs, out);
|
|
567
|
+
return { status: "ok" };
|
|
568
|
+
} catch (err) {
|
|
569
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
570
|
+
return errResult(ctx, msg);
|
|
571
|
+
} finally {
|
|
572
|
+
ctx.settings.inputfile = priorInputFile;
|
|
573
|
+
}
|
|
775
574
|
};
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
575
|
+
const cmdInclude = {
|
|
576
|
+
name: "i",
|
|
577
|
+
aliases: ["include"],
|
|
578
|
+
helpKey: "i",
|
|
579
|
+
run: (ctx) => runInclude(ctx, false)
|
|
781
580
|
};
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
581
|
+
const cmdIncludeRel = {
|
|
582
|
+
name: "ir",
|
|
583
|
+
aliases: ["include_relative"],
|
|
584
|
+
helpKey: "ir",
|
|
585
|
+
run: (ctx) => runInclude(ctx, true)
|
|
787
586
|
};
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
return { status: "ok" };
|
|
807
|
-
}
|
|
808
|
-
catch (err) {
|
|
809
|
-
// File targets fail synchronously in `openWriter` via `openSync`;
|
|
810
|
-
// surface them in upstream's `<path>: <strerror>` shape (bare,
|
|
811
|
-
// no `\o:` prefix) and continue with the loop so a follow-up
|
|
812
|
-
// `SELECT` still executes. Pipe spawn failures (which lack an
|
|
813
|
-
// errno code) fall through to the generic `\o: <msg>` path.
|
|
814
|
-
if (!arg.startsWith("|") && isFileOpenFailure(err)) {
|
|
815
|
-
return reportFileOpenFailure(ctx, arg, err);
|
|
816
|
-
}
|
|
817
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
818
|
-
return errResult(ctx, msg);
|
|
819
|
-
}
|
|
820
|
-
},
|
|
587
|
+
const cmdOut = {
|
|
588
|
+
name: "o",
|
|
589
|
+
aliases: ["out"],
|
|
590
|
+
helpKey: "o",
|
|
591
|
+
async run(ctx) {
|
|
592
|
+
const arg = ctx.nextArg("filepipe");
|
|
593
|
+
await closeQueryFout(ctx.settings);
|
|
594
|
+
if (arg === null || arg.length === 0) return { status: "ok" };
|
|
595
|
+
try {
|
|
596
|
+
const entry = openWriter(arg);
|
|
597
|
+
setQueryFout(ctx.settings, entry);
|
|
598
|
+
return { status: "ok" };
|
|
599
|
+
} catch (err) {
|
|
600
|
+
if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
|
|
601
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
602
|
+
return errResult(ctx, msg);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
821
605
|
};
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
877
|
-
return errResult(ctx, msg);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
// Wait for the target to drain. For pipe targets a non-zero exit /
|
|
881
|
-
// killing signal is surfaced as `<fname>: <wait_result_to_str>`,
|
|
882
|
-
// mirroring upstream `exec_command_write`:
|
|
883
|
-
//
|
|
884
|
-
// pg_log_error("%s: %s", fname, wait_result_to_str(result));
|
|
885
|
-
//
|
|
886
|
-
// Note that upstream's `fname` retains the leading `|`, and the
|
|
887
|
-
// message does NOT carry the `\w:` cmd-prefix that the other
|
|
888
|
-
// backslash-command errors use — `pg_log_error` writes the bare
|
|
889
|
-
// formatted message (under terse mode, which is the conformance
|
|
890
|
-
// harness setup). We bypass `errResult` to match that shape exactly.
|
|
891
|
-
try {
|
|
892
|
-
const result = await entry.close();
|
|
893
|
-
if (entry.isPipe) {
|
|
894
|
-
const msg = formatChildWaitResult(result.exitCode, result.signal);
|
|
895
|
-
if (msg !== null) {
|
|
896
|
-
// `arg` still has the leading `|`; emit it verbatim so the
|
|
897
|
-
// text reads `| program: child process exited with exit code 1`.
|
|
898
|
-
const line = `${arg}: ${msg}`;
|
|
899
|
-
ctx.settings.lastErrorResult = { message: line };
|
|
900
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
901
|
-
writeErr(`${prefix}${line}\n`);
|
|
902
|
-
return { status: "error", errorWritten: true };
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
return { status: "ok" };
|
|
906
|
-
}
|
|
907
|
-
catch (err) {
|
|
908
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
909
|
-
return errResult(ctx, msg);
|
|
910
|
-
}
|
|
911
|
-
},
|
|
606
|
+
const cmdWrite = {
|
|
607
|
+
name: "w",
|
|
608
|
+
aliases: ["write"],
|
|
609
|
+
helpKey: "w",
|
|
610
|
+
async run(ctx) {
|
|
611
|
+
const arg = ctx.nextArg("filepipe");
|
|
612
|
+
if (arg === null || arg.length === 0) return errResult(ctx, "missing required argument");
|
|
613
|
+
let entry;
|
|
614
|
+
try {
|
|
615
|
+
entry = openWriter(arg);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
|
|
618
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
619
|
+
return errResult(ctx, msg);
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
await new Promise((resolve, reject) => {
|
|
623
|
+
entry.stream.write(ctx.queryBuf, (err) => {
|
|
624
|
+
if (err) reject(err);
|
|
625
|
+
else resolve();
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
} catch (err) {
|
|
629
|
+
const isEpipe = err instanceof Error && err.code === "EPIPE";
|
|
630
|
+
if (!entry.isPipe || !isEpipe) {
|
|
631
|
+
try {
|
|
632
|
+
await entry.close();
|
|
633
|
+
} catch {}
|
|
634
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
635
|
+
return errResult(ctx, msg);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const result = await entry.close();
|
|
640
|
+
if (entry.isPipe) {
|
|
641
|
+
const msg = formatChildWaitResult(result.exitCode, result.signal);
|
|
642
|
+
if (msg !== null) {
|
|
643
|
+
const line = `${arg}: ${msg}`;
|
|
644
|
+
ctx.settings.lastErrorResult = { message: line };
|
|
645
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
646
|
+
writeErr(`${prefix}${line}\n`);
|
|
647
|
+
return {
|
|
648
|
+
status: "error",
|
|
649
|
+
errorWritten: true
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { status: "ok" };
|
|
654
|
+
} catch (err) {
|
|
655
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
656
|
+
return errResult(ctx, msg);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
912
659
|
};
|
|
913
|
-
// ---------------------------------------------------------------------------
|
|
914
|
-
// \g, \gx — execute the query buffer with optional one-shot redirect.
|
|
915
|
-
// ---------------------------------------------------------------------------
|
|
916
660
|
/**
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
661
|
+
* Parse the body of a `\g (option=value option2=value2 ...)` clause —
|
|
662
|
+
* the text between the outer parentheses, already stripped. Options
|
|
663
|
+
* are separated by whitespace; values may be single-quoted to embed
|
|
664
|
+
* spaces. Unquoted values run to the next whitespace.
|
|
665
|
+
*
|
|
666
|
+
* Mirrors upstream's `parse_slash_pgopts_list`. We deliberately stay
|
|
667
|
+
* narrow — the conformance corpus exercises `format=`, `csv_fieldsep=`,
|
|
668
|
+
* and `title=` only.
|
|
669
|
+
*/
|
|
926
670
|
const parseGPsetOptions = (body) => {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
value += body[i++];
|
|
971
|
-
}
|
|
972
|
-
if (body[i] === "'")
|
|
973
|
-
i++;
|
|
974
|
-
}
|
|
975
|
-
else {
|
|
976
|
-
const vStart = i;
|
|
977
|
-
while (i < body.length && !/\s/.test(body[i]))
|
|
978
|
-
i++;
|
|
979
|
-
value = body.slice(vStart, i);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
out.push({ option, value });
|
|
983
|
-
}
|
|
984
|
-
return out;
|
|
671
|
+
const out = [];
|
|
672
|
+
let i = 0;
|
|
673
|
+
while (i < body.length) {
|
|
674
|
+
while (i < body.length && /\s/.test(body[i])) i++;
|
|
675
|
+
if (i >= body.length) break;
|
|
676
|
+
const optStart = i;
|
|
677
|
+
while (i < body.length && body[i] !== "=" && !/\s/.test(body[i])) i++;
|
|
678
|
+
const option = body.slice(optStart, i);
|
|
679
|
+
if (option.length === 0) break;
|
|
680
|
+
let value = "";
|
|
681
|
+
if (body[i] === "=") {
|
|
682
|
+
i++;
|
|
683
|
+
if (body[i] === "'") {
|
|
684
|
+
i++;
|
|
685
|
+
while (i < body.length && body[i] !== "'") {
|
|
686
|
+
if (body[i] === "\\" && i + 1 < body.length) {
|
|
687
|
+
const next = body[i + 1];
|
|
688
|
+
if (next === "n") value += "\n";
|
|
689
|
+
else if (next === "t") value += " ";
|
|
690
|
+
else if (next === "r") value += "\r";
|
|
691
|
+
else if (next === "\\") value += "\\";
|
|
692
|
+
else if (next === "'") value += "'";
|
|
693
|
+
else value += next;
|
|
694
|
+
i += 2;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
value += body[i++];
|
|
698
|
+
}
|
|
699
|
+
if (body[i] === "'") i++;
|
|
700
|
+
} else {
|
|
701
|
+
const vStart = i;
|
|
702
|
+
while (i < body.length && !/\s/.test(body[i])) i++;
|
|
703
|
+
value = body.slice(vStart, i);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
out.push({
|
|
707
|
+
option,
|
|
708
|
+
value
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return out;
|
|
985
712
|
};
|
|
986
713
|
const runGCore = async (ctx, forceExpanded) => {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
const topt = ctx.settings.popt.topt;
|
|
1070
|
-
// Snapshot topt BEFORE any per-query mutation so the restore in
|
|
1071
|
-
// `finally` covers both `\gx`'s `expanded = 'on'` and any `\g (...)`
|
|
1072
|
-
// pset overrides in one shot. Snapshotting AFTER the `forceExpanded`
|
|
1073
|
-
// mutation would persist `expanded = 'on'` across queries.
|
|
1074
|
-
const toptSnapshot = { ...topt };
|
|
1075
|
-
if (forceExpanded)
|
|
1076
|
-
topt.expanded = "on";
|
|
1077
|
-
// Apply per-query pset overrides silently. Upstream applies the
|
|
1078
|
-
// temporary options without emitting the status lines that
|
|
1079
|
-
// interactive `\pset` would.
|
|
1080
|
-
if (psetOverrides) {
|
|
1081
|
-
for (const { option, value } of psetOverrides) {
|
|
1082
|
-
applyPset(topt, option, value, ctx.cmdName, true);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Track for `\g` / `\gx` re-run with empty buffer. Upstream sets
|
|
1086
|
-
// `pset.last_query` in `PSQLexec` before dispatch.
|
|
1087
|
-
ctx.settings.lastQuery = sql;
|
|
1088
|
-
// Consume any pending `\bind` / `\bind_named` state. Upstream's
|
|
1089
|
-
// `\g` routes through the extended-query protocol when bind params
|
|
1090
|
-
// are set: anonymous `\bind` re-prepares from the buffer; named
|
|
1091
|
-
// `\bind_named NAME` looks up the server-side prepared statement
|
|
1092
|
-
// by NAME (set earlier via `\parse NAME`) and just runs Bind +
|
|
1093
|
-
// Execute against it.
|
|
1094
|
-
const bindState = consumeBindState(ctx.settings);
|
|
1095
|
-
let execError = null;
|
|
1096
|
-
// Track whether we wired the mid-batch COPY-OUT sink so the `finally`
|
|
1097
|
-
// can clear it deterministically — even if `execSimple` threw.
|
|
1098
|
-
let copyOutSinkConn = null;
|
|
1099
|
-
try {
|
|
1100
|
-
const out = pickOut(ctx.settings, oneShot?.stream ?? null);
|
|
1101
|
-
if (bindState?.byName) {
|
|
1102
|
-
// \bind_named NAME — execute the previously-prepared statement
|
|
1103
|
-
// identified by NAME. The cache was populated by `\parse NAME`.
|
|
1104
|
-
// The empty-string NAME is the upstream "unnamed" prepared
|
|
1105
|
-
// statement slot.
|
|
1106
|
-
const ps = lookupPrepared(ctx.settings, bindState.name);
|
|
1107
|
-
if (!ps) {
|
|
1108
|
-
// Synthesise a thrown-Error-like object so formatServerError can
|
|
1109
|
-
// render the same `ERROR: <msg>` shape vanilla emits for the
|
|
1110
|
-
// server's `prepared statement "X" does not exist` error.
|
|
1111
|
-
execError = Object.assign(new Error(`prepared statement "${bindState.name}" does not exist`), { severity: "ERROR", code: "26000" });
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
// Bind + Execute MUST go in one extended-protocol batch: the
|
|
1115
|
-
// anonymous portal is implicitly closed at the next Sync, so a
|
|
1116
|
-
// separate ps.bind() then ps.execute() would lose the portal in
|
|
1117
|
-
// between. `bindAndExecute` issues both messages before the
|
|
1118
|
-
// Sync.
|
|
1119
|
-
const rs = await ps.bindAndExecute(bindState.values);
|
|
1120
|
-
await renderResult(ctx.settings, rs, out);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
else if (bindState) {
|
|
1124
|
-
// Anonymous \bind — re-prepare from the current buffer (or
|
|
1125
|
-
// lastQuery fallback) and execute with the supplied params.
|
|
1126
|
-
const rs = await ctx.settings.db.query(sql, bindState.values);
|
|
1127
|
-
await renderResult(ctx.settings, rs, out);
|
|
1128
|
-
}
|
|
1129
|
-
else {
|
|
1130
|
-
// Plain `\g` / `\gx`: simple-query dispatch.
|
|
1131
|
-
//
|
|
1132
|
-
// When the batch contains `COPY ... TO STDOUT`, the wire layer
|
|
1133
|
-
// forwards CopyData bytes via `copyOutMidBatchSink`. Mainloop wires
|
|
1134
|
-
// that sink to `ctx.stdout` for top-level dispatches; here in `\g`
|
|
1135
|
-
// we redirect it to the current output target (`\g FILE`,
|
|
1136
|
-
// `\g |cmd`, or `\o`-stashed stream when neither is set). Without
|
|
1137
|
-
// this, `COPY (SELECT 'foo') TO STDOUT \g :file` silently drops
|
|
1138
|
-
// `foo` on the floor and the file ends up with only the empty
|
|
1139
|
-
// `(0 rows)` shape printed by `renderResult` for the wire's empty
|
|
1140
|
-
// ResultSet. Matches upstream's `do_copy` / `handleCopyOut` path:
|
|
1141
|
-
// the COPY OUT bytes go wherever the active queryFout points.
|
|
1142
|
-
if (hasCopyToStdout(sql)) {
|
|
1143
|
-
copyOutSinkConn = ctx.settings.db;
|
|
1144
|
-
copyOutSinkConn.copyOutMidBatchSink = (chunk) => {
|
|
1145
|
-
out.write(chunk);
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
const results = await ctx.settings.db.execSimple(sql);
|
|
1149
|
-
for (const rs of results) {
|
|
1150
|
-
await renderResult(ctx.settings, rs, out);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
catch (err) {
|
|
1155
|
-
execError = err;
|
|
1156
|
-
}
|
|
1157
|
-
finally {
|
|
1158
|
-
// Restore the pre-query topt verbatim — covers both the `\gx`
|
|
1159
|
-
// `expanded = 'on'` swap and any `\g (...)` pset overrides, so a
|
|
1160
|
-
// subsequent plain `\g` runs in the user's persistent print mode.
|
|
1161
|
-
Object.assign(topt, toptSnapshot);
|
|
1162
|
-
// Tear down the COPY-OUT sink so subsequent top-level batches reach
|
|
1163
|
-
// mainloop's installer with a clean slate. (Mainloop reinstalls per
|
|
1164
|
-
// batch; leaving ours pointed at a now-closed file would cause a
|
|
1165
|
-
// write-after-close on the next CopyData burst.)
|
|
1166
|
-
if (copyOutSinkConn)
|
|
1167
|
-
copyOutSinkConn.copyOutMidBatchSink = null;
|
|
1168
|
-
}
|
|
1169
|
-
// Close the one-shot writer regardless of execution success so any
|
|
1170
|
-
// partial output is flushed.
|
|
1171
|
-
//
|
|
1172
|
-
// Note: a non-zero exit from `\g | program` is intentionally NOT
|
|
1173
|
-
// surfaced as an error. Upstream `CloseGOutput` (src/bin/psql/common.c)
|
|
1174
|
-
// only feeds the wait status to `SetShellResultVariables`, which sets
|
|
1175
|
-
// `SHELL_ERROR` / `SHELL_EXIT_CODE` for user inspection — no
|
|
1176
|
-
// `pg_log_error` call. This matches `\g | false` in vanilla psql:
|
|
1177
|
-
// silent, exit code 0, the next command (`\echo after`) prints
|
|
1178
|
-
// normally. Bookkeeping for the SHELL_* vars is a follow-up; what
|
|
1179
|
-
// matters here is that we don't emit a stray "program exited" line
|
|
1180
|
-
// that the conformance harness would diff against an empty upstream
|
|
1181
|
-
// stderr.
|
|
1182
|
-
//
|
|
1183
|
-
// The only failure we still surface from a pipe target is a synchronous
|
|
1184
|
-
// `close()` rejection (e.g. EPIPE escaping the swallow above), which
|
|
1185
|
-
// would indicate a genuine bug in our wiring rather than the child
|
|
1186
|
-
// program's exit code.
|
|
1187
|
-
let pipeError = null;
|
|
1188
|
-
if (oneShot) {
|
|
1189
|
-
try {
|
|
1190
|
-
await oneShot.close();
|
|
1191
|
-
}
|
|
1192
|
-
catch (err) {
|
|
1193
|
-
pipeError = err instanceof Error ? err.message : String(err);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
if (execError !== null) {
|
|
1197
|
-
// Render in upstream's `ERROR: <msg>\nLINE N: ...\n ^` shape
|
|
1198
|
-
// by funnelling through `formatServerError` — same path top-level
|
|
1199
|
-
// statement errors take in `core/common.ts::writeQueryError`. The
|
|
1200
|
-
// `\<cmd>:` prefix is reserved for client-side I/O / parse errors
|
|
1201
|
-
// (e.g. `\g: no connection`), not server-side ErrorResponse-shaped
|
|
1202
|
-
// failures. Pass the COMMENT-STRIPPED buffer (`trimmedBuf`) so the
|
|
1203
|
-
// `LINE N:` count starts at the first content line — vanilla strips
|
|
1204
|
-
// leading comments + blank lines from queryBuf before `PQexec`, and
|
|
1205
|
-
// the server's reported `position` is a 1-based offset into THAT
|
|
1206
|
-
// trimmed buffer. We preserve trailing whitespace so a `\g` after
|
|
1207
|
-
// `SELECT $1, $2 ` still renders `LINE 1: SELECT $1, $2 ` verbatim.
|
|
1208
|
-
// When buffer was empty (lastQuery fallback or named-bind path), the
|
|
1209
|
-
// dispatched SQL is `sql` — pass that instead so the `LINE N:` echo
|
|
1210
|
-
// still reflects the executed statement (e.g. `\bind_named NAME \g`
|
|
1211
|
-
// after a `\parse NAME` of `SELECT $1, $2`).
|
|
1212
|
-
return formatServerError(ctx, execError, bufSql.length > 0 ? trimmedBuf : sql);
|
|
1213
|
-
}
|
|
1214
|
-
if (pipeError !== null) {
|
|
1215
|
-
return errResult(ctx, pipeError);
|
|
1216
|
-
}
|
|
1217
|
-
return { status: "reset-buf", newBuf: "" };
|
|
714
|
+
const gated = pipelineGate(ctx);
|
|
715
|
+
if (gated !== null) return gated;
|
|
716
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
717
|
+
const bufSql = trimmedBuf.trim();
|
|
718
|
+
let target;
|
|
719
|
+
let psetOverrides = null;
|
|
720
|
+
const rawTrimmed = ctx.rawArgs.trimStart();
|
|
721
|
+
if (rawTrimmed.startsWith("(")) {
|
|
722
|
+
const close = rawTrimmed.indexOf(")");
|
|
723
|
+
if (close === -1) return errResult(ctx, "missing right parenthesis in \\g options");
|
|
724
|
+
psetOverrides = parseGPsetOptions(rawTrimmed.slice(1, close).trim());
|
|
725
|
+
const afterParen = rawTrimmed.slice(close + 1).trim();
|
|
726
|
+
target = afterParen.length > 0 ? afterParen : null;
|
|
727
|
+
} else target = ctx.nextArg("filepipe");
|
|
728
|
+
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery;
|
|
729
|
+
const hasPendingNamedBind = stagedNamedBindPresent(ctx.settings);
|
|
730
|
+
if (sql.length === 0 && !hasPendingNamedBind) return {
|
|
731
|
+
status: "reset-buf",
|
|
732
|
+
newBuf: ""
|
|
733
|
+
};
|
|
734
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
735
|
+
let oneShot = null;
|
|
736
|
+
if (target !== null && target.length > 0) try {
|
|
737
|
+
oneShot = openWriter(target);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
if (!target.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, target, err);
|
|
740
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
741
|
+
return errResult(ctx, msg);
|
|
742
|
+
}
|
|
743
|
+
const topt = ctx.settings.popt.topt;
|
|
744
|
+
const toptSnapshot = { ...topt };
|
|
745
|
+
if (forceExpanded) topt.expanded = "on";
|
|
746
|
+
if (psetOverrides) for (const { option, value } of psetOverrides) applyPset(topt, option, value, ctx.cmdName, true);
|
|
747
|
+
ctx.settings.lastQuery = sql;
|
|
748
|
+
const bindState = consumeBindState(ctx.settings);
|
|
749
|
+
let execError = null;
|
|
750
|
+
let copyOutSinkConn = null;
|
|
751
|
+
try {
|
|
752
|
+
const out = pickOut(ctx.settings, oneShot?.stream ?? null);
|
|
753
|
+
if (bindState?.byName) {
|
|
754
|
+
const ps = lookupPrepared(ctx.settings, bindState.name);
|
|
755
|
+
if (!ps) execError = Object.assign(/* @__PURE__ */ new Error(`prepared statement "${bindState.name}" does not exist`), {
|
|
756
|
+
severity: "ERROR",
|
|
757
|
+
code: "26000"
|
|
758
|
+
});
|
|
759
|
+
else {
|
|
760
|
+
const rs = await ps.bindAndExecute(bindState.values);
|
|
761
|
+
await renderResult(ctx.settings, rs, out);
|
|
762
|
+
}
|
|
763
|
+
} else if (bindState) {
|
|
764
|
+
const rs = await ctx.settings.db.query(sql, bindState.values);
|
|
765
|
+
await renderResult(ctx.settings, rs, out);
|
|
766
|
+
} else {
|
|
767
|
+
if (hasCopyToStdout(sql)) {
|
|
768
|
+
copyOutSinkConn = ctx.settings.db;
|
|
769
|
+
copyOutSinkConn.copyOutMidBatchSink = (chunk) => {
|
|
770
|
+
out.write(chunk);
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const results = await ctx.settings.db.execSimple(sql);
|
|
774
|
+
for (const rs of results) await renderResult(ctx.settings, rs, out);
|
|
775
|
+
}
|
|
776
|
+
} catch (err) {
|
|
777
|
+
execError = err;
|
|
778
|
+
} finally {
|
|
779
|
+
Object.assign(topt, toptSnapshot);
|
|
780
|
+
if (copyOutSinkConn) copyOutSinkConn.copyOutMidBatchSink = null;
|
|
781
|
+
}
|
|
782
|
+
let pipeError = null;
|
|
783
|
+
if (oneShot) try {
|
|
784
|
+
await oneShot.close();
|
|
785
|
+
} catch (err) {
|
|
786
|
+
pipeError = err instanceof Error ? err.message : String(err);
|
|
787
|
+
}
|
|
788
|
+
if (execError !== null) return formatServerError(ctx, execError, bufSql.length > 0 ? trimmedBuf : sql);
|
|
789
|
+
if (pipeError !== null) return errResult(ctx, pipeError);
|
|
790
|
+
return {
|
|
791
|
+
status: "reset-buf",
|
|
792
|
+
newBuf: ""
|
|
793
|
+
};
|
|
1218
794
|
};
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
795
|
+
const cmdG = {
|
|
796
|
+
name: "g",
|
|
797
|
+
helpKey: "g",
|
|
798
|
+
run: (ctx) => runGCore(ctx, false)
|
|
1223
799
|
};
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
800
|
+
const cmdGx = {
|
|
801
|
+
name: "gx",
|
|
802
|
+
helpKey: "gx",
|
|
803
|
+
run: (ctx) => runGCore(ctx, true)
|
|
1228
804
|
};
|
|
1229
|
-
// ---------------------------------------------------------------------------
|
|
1230
|
-
// \p / \print — print the current or previous query buffer.
|
|
1231
|
-
// ---------------------------------------------------------------------------
|
|
1232
805
|
/**
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
// source line starts with a slash command — so a raw `length > 0`
|
|
1277
|
-
// check here would route to the "print the buffer" arm and emit
|
|
1278
|
-
// `\n\n` instead of falling through to `lastQuery`. The trim-only
|
|
1279
|
-
// emptiness check is purely an empty-vs-content discriminator; the
|
|
1280
|
-
// actual writeOut still uses the un-trimmed buffer text so an inline
|
|
1281
|
-
// `SELECT 3 \p` correctly emits the trailing space upstream prints.
|
|
1282
|
-
if (ctx.queryBuf.trim().length > 0) {
|
|
1283
|
-
writeOut(`${ctx.queryBuf}\n`);
|
|
1284
|
-
}
|
|
1285
|
-
else if (ctx.settings.lastQuery.length > 0) {
|
|
1286
|
-
writeOut(`${ctx.settings.lastQuery}\n`);
|
|
1287
|
-
}
|
|
1288
|
-
else if (!ctx.settings.quiet) {
|
|
1289
|
-
writeOut("Query buffer is empty.\n");
|
|
1290
|
-
}
|
|
1291
|
-
return Promise.resolve({ status: "ok" });
|
|
1292
|
-
},
|
|
806
|
+
* `\p` / `\print` — print the query buffer the next `\g` would execute.
|
|
807
|
+
*
|
|
808
|
+
* Mirrors upstream `exec_command_print` in `src/bin/psql/command.c`:
|
|
809
|
+
*
|
|
810
|
+
* if (query_buf && query_buf->len > 0)
|
|
811
|
+
* puts(query_buf->data);
|
|
812
|
+
* else if (previous_buf && previous_buf->len > 0)
|
|
813
|
+
* puts(previous_buf->data);
|
|
814
|
+
* else if (!pset.quiet)
|
|
815
|
+
* puts(_("Query buffer is empty."));
|
|
816
|
+
*
|
|
817
|
+
* Buffer-vs-previous-buffer precedence matters for the regress sequence:
|
|
818
|
+
*
|
|
819
|
+
* SELECT 1; -- executes, previous_buf := "SELECT 1;"
|
|
820
|
+
* \p -- queryBuf empty → prints previous_buf
|
|
821
|
+
* SELECT 2 \r -- queryBuf="SELECT 2 ", \r resets to "" without
|
|
822
|
+
* -- touching previous_buf
|
|
823
|
+
* \p -- queryBuf still empty → prints previous_buf
|
|
824
|
+
* SELECT 3 \p -- queryBuf="SELECT 3 ", non-empty → prints queryBuf
|
|
825
|
+
*
|
|
826
|
+
* Implementation notes:
|
|
827
|
+
*
|
|
828
|
+
* - We use `settings.lastQuery` as the previous-buffer source. Upstream
|
|
829
|
+
* tracks `previous_buf` independently of `pset.last_query`, but our
|
|
830
|
+
* `lastQuery` is set at the exact same point upstream sets
|
|
831
|
+
* `previous_buf` (the dispatch site in `SendQuery`-equivalent code paths
|
|
832
|
+
* in `core/common.ts` and `cmd_io.ts`'s `\g` implementation), so the
|
|
833
|
+
* semantics match for every shape exercised by the conformance corpus.
|
|
834
|
+
* - We must NOT clear queryBuf — return `status: 'ok'` so the mainloop
|
|
835
|
+
* leaves the buffer untouched. The user is inspecting, not executing.
|
|
836
|
+
* - `puts()` appends a trailing newline. We use `writeOut` and append `\n`
|
|
837
|
+
* explicitly to match.
|
|
838
|
+
*/
|
|
839
|
+
const cmdPrint = {
|
|
840
|
+
name: "p",
|
|
841
|
+
aliases: ["print"],
|
|
842
|
+
helpKey: "p",
|
|
843
|
+
run: (ctx) => {
|
|
844
|
+
if (ctx.queryBuf.trim().length > 0) writeOut(`${ctx.queryBuf}\n`);
|
|
845
|
+
else if (ctx.settings.lastQuery.length > 0) writeOut(`${ctx.settings.lastQuery}\n`);
|
|
846
|
+
else if (!ctx.settings.quiet) writeOut("Query buffer is empty.\n");
|
|
847
|
+
return Promise.resolve({ status: "ok" });
|
|
848
|
+
}
|
|
1293
849
|
};
|
|
1294
|
-
// ---------------------------------------------------------------------------
|
|
1295
|
-
// \gset [PREFIX]
|
|
1296
|
-
// ---------------------------------------------------------------------------
|
|
1297
850
|
const formatCell = (value) => {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
return String(value);
|
|
1308
|
-
}
|
|
1309
|
-
// Plain objects / arrays from JSON columns: JSON-stringify so the test
|
|
1310
|
-
// surface is deterministic and avoids "[object Object]".
|
|
1311
|
-
try {
|
|
1312
|
-
return JSON.stringify(value);
|
|
1313
|
-
}
|
|
1314
|
-
catch {
|
|
1315
|
-
return "";
|
|
1316
|
-
}
|
|
851
|
+
if (value === null || value === void 0) return "";
|
|
852
|
+
if (typeof value === "string") return value;
|
|
853
|
+
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
854
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
855
|
+
try {
|
|
856
|
+
return JSON.stringify(value);
|
|
857
|
+
} catch {
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
1317
860
|
};
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (rs.rows.length > 1) {
|
|
1400
|
-
// Match upstream psql's exact wording from `exec_command_gset` —
|
|
1401
|
-
// bare `more than one row returned for \gset` (no `\gset:` prefix).
|
|
1402
|
-
// Verified against vanilla psql; vendored psql.out emits it bare.
|
|
1403
|
-
ctx.settings.lastErrorResult = {
|
|
1404
|
-
message: "more than one row returned for \\gset",
|
|
1405
|
-
};
|
|
1406
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1407
|
-
writeErr(`${errPrefix}more than one row returned for \\gset\n`);
|
|
1408
|
-
return { status: "error", errorWritten: true };
|
|
1409
|
-
}
|
|
1410
|
-
const row = rs.rows[0];
|
|
1411
|
-
for (let i = 0; i < rs.fields.length; i++) {
|
|
1412
|
-
const fieldName = rs.fields[i].name;
|
|
1413
|
-
const name = `${prefix}${fieldName}`;
|
|
1414
|
-
const cell = row[i];
|
|
1415
|
-
const isNull = cell === null || cell === undefined;
|
|
1416
|
-
// Upstream skips assignments where the target maps to a "specially
|
|
1417
|
-
// treated" variable (one with a substitute / assign hook installed)
|
|
1418
|
-
// whose value would be rejected by the hook. The non-special columns
|
|
1419
|
-
// continue to be assigned: only the offending one is skipped, with
|
|
1420
|
-
// an informational stderr line. See psql.out line ~240:
|
|
1421
|
-
// attempt to \gset into specially treated variable "IGNOREEOF" ignored
|
|
1422
|
-
if (isSpeciallyTreatedVar(ctx.settings, name)) {
|
|
1423
|
-
// The target maps to a "specially treated" variable (one with a
|
|
1424
|
-
// substitute / assign hook installed). Upstream skips just this
|
|
1425
|
-
// assignment with an informational stderr line; other columns
|
|
1426
|
-
// are still processed. We don't actually call the hook — even a
|
|
1427
|
-
// value that the hook would accept must be rejected per upstream:
|
|
1428
|
-
// see `exec_command_gset` and `VariableHasHook`.
|
|
1429
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1430
|
-
writeErr(`${errPrefix}attempt to \\gset into specially treated variable ` +
|
|
1431
|
-
`"${name}" ignored\n`);
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
// Upstream `StoreQueryTuple` in src/bin/psql/common.c:
|
|
1435
|
-
//
|
|
1436
|
-
// if (PQgetisnull(result, 0, i))
|
|
1437
|
-
// UnsetVariable(pset.vars, varname);
|
|
1438
|
-
// else if (!SetVariable(pset.vars, varname, PQgetvalue(...))) { ... }
|
|
1439
|
-
//
|
|
1440
|
-
// i.e. a NULL cell unsets the target variable (so a subsequent
|
|
1441
|
-
// `:var` interpolates to the literal `:var` via the scanner's
|
|
1442
|
-
// unset-var passthrough) rather than setting it to the empty
|
|
1443
|
-
// string. Mirror that semantics here.
|
|
1444
|
-
if (isNull) {
|
|
1445
|
-
ctx.settings.vars.unset(name);
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
const value = formatCell(cell);
|
|
1449
|
-
if (!ctx.settings.vars.set(name, value)) {
|
|
1450
|
-
// Bare `invalid variable name: "<name>"` (no `\gset:` prefix) —
|
|
1451
|
-
// matches upstream psql.out wording for `\gset` exactly.
|
|
1452
|
-
ctx.settings.lastErrorResult = {
|
|
1453
|
-
message: `invalid variable name: "${fieldName}"`,
|
|
1454
|
-
};
|
|
1455
|
-
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1456
|
-
writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
|
|
1457
|
-
return { status: "error", errorWritten: true };
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
return { status: "reset-buf", newBuf: "" };
|
|
1461
|
-
},
|
|
861
|
+
const cmdGset = {
|
|
862
|
+
name: "gset",
|
|
863
|
+
helpKey: "gset",
|
|
864
|
+
async run(ctx) {
|
|
865
|
+
const gated = pipelineGate(ctx);
|
|
866
|
+
if (gated !== null) return gated;
|
|
867
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
868
|
+
const bufSql = trimmedBuf.trim();
|
|
869
|
+
const prefix = ctx.nextArg("normal") ?? "";
|
|
870
|
+
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
871
|
+
if (sql.length === 0) return {
|
|
872
|
+
status: "reset-buf",
|
|
873
|
+
newBuf: ""
|
|
874
|
+
};
|
|
875
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
876
|
+
ctx.settings.lastQuery = sql;
|
|
877
|
+
let results;
|
|
878
|
+
try {
|
|
879
|
+
results = await ctx.settings.db.execSimple(sql);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
return formatServerError(ctx, err, bufSql.length > 0 ? trimmedBuf : sql);
|
|
882
|
+
}
|
|
883
|
+
if (results.length > 1) {
|
|
884
|
+
const out = pickOut(ctx.settings, null);
|
|
885
|
+
for (let i = 0; i < results.length - 1; i++) await renderResult(ctx.settings, results[i], out);
|
|
886
|
+
}
|
|
887
|
+
const lastRs = results[results.length - 1];
|
|
888
|
+
if (!lastRs || lastRs.fields.length === 0) return {
|
|
889
|
+
status: "reset-buf",
|
|
890
|
+
newBuf: ""
|
|
891
|
+
};
|
|
892
|
+
const rs = lastRs;
|
|
893
|
+
if (rs.rows.length === 0) {
|
|
894
|
+
ctx.settings.lastErrorResult = { message: "no rows returned for \\gset" };
|
|
895
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
896
|
+
writeErr(`${errPrefix}no rows returned for \\gset\n`);
|
|
897
|
+
return {
|
|
898
|
+
status: "error",
|
|
899
|
+
errorWritten: true
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (rs.rows.length > 1) {
|
|
903
|
+
ctx.settings.lastErrorResult = { message: "more than one row returned for \\gset" };
|
|
904
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
905
|
+
writeErr(`${errPrefix}more than one row returned for \\gset\n`);
|
|
906
|
+
return {
|
|
907
|
+
status: "error",
|
|
908
|
+
errorWritten: true
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const row = rs.rows[0];
|
|
912
|
+
for (let i = 0; i < rs.fields.length; i++) {
|
|
913
|
+
const fieldName = rs.fields[i].name;
|
|
914
|
+
const name = `${prefix}${fieldName}`;
|
|
915
|
+
const cell = row[i];
|
|
916
|
+
const isNull = cell === null || cell === void 0;
|
|
917
|
+
if (isSpeciallyTreatedVar(ctx.settings, name)) {
|
|
918
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
919
|
+
writeErr(`${errPrefix}attempt to \\gset into specially treated variable "${name}" ignored\n`);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (isNull) {
|
|
923
|
+
ctx.settings.vars.unset(name);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const value = formatCell(cell);
|
|
927
|
+
if (!ctx.settings.vars.set(name, value)) {
|
|
928
|
+
ctx.settings.lastErrorResult = { message: `invalid variable name: "${fieldName}"` };
|
|
929
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
930
|
+
writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
|
|
931
|
+
return {
|
|
932
|
+
status: "error",
|
|
933
|
+
errorWritten: true
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
status: "reset-buf",
|
|
939
|
+
newBuf: ""
|
|
940
|
+
};
|
|
941
|
+
}
|
|
1462
942
|
};
|
|
1463
|
-
// ---------------------------------------------------------------------------
|
|
1464
|
-
// \gdesc — describe the current query without executing it.
|
|
1465
|
-
//
|
|
1466
|
-
// Mirrors upstream `exec_command_gdesc` in `src/bin/psql/command.c`: parse
|
|
1467
|
-
// the buffered query through the extended protocol (Parse + Describe by
|
|
1468
|
-
// statement, no Execute), then build a synthetic two-column ResultSet of
|
|
1469
|
-
// `Column` and `Type` rows and route it through the printer the user's
|
|
1470
|
-
// `\pset format` selected. Tuples-only mode (`\t on`) suppresses the
|
|
1471
|
-
// header / `(N columns)` footer the same way it would for a real query
|
|
1472
|
-
// result, because we hand the synthetic ResultSet to the same printer.
|
|
1473
|
-
//
|
|
1474
|
-
// Type names come from a follow-up `SELECT ... format_type(tp, tpm)`
|
|
1475
|
-
// over a VALUES literal — exactly the round-trip upstream uses so
|
|
1476
|
-
// non-builtin types and typmod modifiers (`numeric(10,2)`, `varchar(64)`)
|
|
1477
|
-
// render with their canonical form.
|
|
1478
|
-
// ---------------------------------------------------------------------------
|
|
1479
943
|
/**
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
944
|
+
* Build the SQL that resolves each describe-result column's `Type` via
|
|
945
|
+
* `pg_catalog.format_type(typoid, typmod)`. We feed the names + OIDs
|
|
946
|
+
* + typmods through a `VALUES` literal so the server does the formatting
|
|
947
|
+
* for us — the same query upstream issues from `describeFieldsByType`.
|
|
948
|
+
*
|
|
949
|
+
* Returns null when there are zero fields (caller emits `(0 rows)` form
|
|
950
|
+
* by hand because PostgreSQL rejects an empty VALUES list).
|
|
951
|
+
*/
|
|
1488
952
|
const buildGdescFormatQuery = (fields) => {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const rows = fields
|
|
1497
|
-
.map((f, i) => {
|
|
1498
|
-
const safeName = f.name.replace(/'/gu, "''");
|
|
1499
|
-
const oid = String(f.dataTypeID >>> 0);
|
|
1500
|
-
const typmod = String(f.dataTypeModifier | 0);
|
|
1501
|
-
return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
|
|
1502
|
-
})
|
|
1503
|
-
.join(", ");
|
|
1504
|
-
// ORDER BY _idx preserves the describe order regardless of how the server
|
|
1505
|
-
// happens to evaluate the VALUES list. Aliases match upstream column
|
|
1506
|
-
// titles exactly so the printer header is identical.
|
|
1507
|
-
return ('SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type"' +
|
|
1508
|
-
` FROM (VALUES ${rows}) AS x(_idx, name, tp, tpm) ORDER BY _idx`);
|
|
953
|
+
if (fields.length === 0) return null;
|
|
954
|
+
return `SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type" FROM (VALUES ${fields.map((f, i) => {
|
|
955
|
+
const safeName = f.name.replace(/'/gu, "''");
|
|
956
|
+
const oid = String(f.dataTypeID >>> 0);
|
|
957
|
+
const typmod = String(f.dataTypeModifier | 0);
|
|
958
|
+
return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
|
|
959
|
+
}).join(", ")}) AS x(_idx, name, tp, tpm) ORDER BY _idx`;
|
|
1509
960
|
};
|
|
1510
961
|
/**
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const GDESC_SYNTHETIC_FIELDS = [
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
format: 0,
|
|
1534
|
-
},
|
|
1535
|
-
];
|
|
962
|
+
* Field descriptors for the synthetic `Column / Type` ResultSet that
|
|
963
|
+
* `\gdesc` emits when format_type resolution fails or yields nothing.
|
|
964
|
+
*
|
|
965
|
+
* We fall back to the field's raw OID so the user still sees a value.
|
|
966
|
+
*/
|
|
967
|
+
const GDESC_SYNTHETIC_FIELDS = [{
|
|
968
|
+
name: "Column",
|
|
969
|
+
tableID: 0,
|
|
970
|
+
columnID: 0,
|
|
971
|
+
dataTypeID: 25,
|
|
972
|
+
dataTypeSize: -1,
|
|
973
|
+
dataTypeModifier: -1,
|
|
974
|
+
format: 0
|
|
975
|
+
}, {
|
|
976
|
+
name: "Type",
|
|
977
|
+
tableID: 0,
|
|
978
|
+
columnID: 0,
|
|
979
|
+
dataTypeID: 25,
|
|
980
|
+
dataTypeSize: -1,
|
|
981
|
+
dataTypeModifier: -1,
|
|
982
|
+
format: 0
|
|
983
|
+
}];
|
|
1536
984
|
const buildSyntheticGdescResultSet = (rows) => ({
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
985
|
+
command: "SELECT",
|
|
986
|
+
rowCount: rows.length,
|
|
987
|
+
oid: null,
|
|
988
|
+
fields: GDESC_SYNTHETIC_FIELDS,
|
|
989
|
+
rows,
|
|
990
|
+
notices: []
|
|
1543
991
|
});
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const formatQuery = buildGdescFormatQuery(fields);
|
|
1618
|
-
if (formatQuery === null) {
|
|
1619
|
-
rows = [];
|
|
1620
|
-
}
|
|
1621
|
-
else {
|
|
1622
|
-
const fallbackRows = () => fields.map((f) => [f.name, String(f.dataTypeID)]);
|
|
1623
|
-
try {
|
|
1624
|
-
const sets = await ctx.settings.db.execSimple(formatQuery);
|
|
1625
|
-
const last = sets[sets.length - 1];
|
|
1626
|
-
rows =
|
|
1627
|
-
last && last.rows.length > 0 ? last.rows : fallbackRows();
|
|
1628
|
-
}
|
|
1629
|
-
catch {
|
|
1630
|
-
rows = fallbackRows();
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
const rs = buildSyntheticGdescResultSet(rows);
|
|
1634
|
-
const printer = pickActivePrinter(ctx.settings);
|
|
1635
|
-
const out = pickOut(ctx.settings, null);
|
|
1636
|
-
try {
|
|
1637
|
-
await printer.printQuery(rs, ctx.settings.popt, out);
|
|
1638
|
-
}
|
|
1639
|
-
catch (err) {
|
|
1640
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1641
|
-
return errResult(ctx, msg);
|
|
1642
|
-
}
|
|
1643
|
-
// Refresh state vars to mark the describe success: `:ERROR=false`,
|
|
1644
|
-
// `:SQLSTATE=00000`, `:ROW_COUNT=<#-described-columns>`. Upstream
|
|
1645
|
-
// routes `\gdesc` through `PSQL_CMD_SEND` so its post-dispatch
|
|
1646
|
-
// `SetResultVariables` sees the synthetic 2-column tuple result and
|
|
1647
|
-
// assigns ROW_COUNT to the field count we just rendered.
|
|
1648
|
-
refreshErrorVars(ctx.settings, {
|
|
1649
|
-
kind: "success",
|
|
1650
|
-
rowCount: rs.rowCount,
|
|
1651
|
-
});
|
|
1652
|
-
return { status: "reset-buf", newBuf: "" };
|
|
1653
|
-
},
|
|
992
|
+
const cmdGdesc = {
|
|
993
|
+
name: "gdesc",
|
|
994
|
+
helpKey: "gdesc",
|
|
995
|
+
async run(ctx) {
|
|
996
|
+
const gated = pipelineGate(ctx);
|
|
997
|
+
if (gated !== null) return gated;
|
|
998
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
999
|
+
const sql = trimmedBuf.trim();
|
|
1000
|
+
if (sql.length === 0) {
|
|
1001
|
+
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1002
|
+
refreshErrorVars(ctx.settings, {
|
|
1003
|
+
kind: "success",
|
|
1004
|
+
rowCount: 0
|
|
1005
|
+
});
|
|
1006
|
+
return {
|
|
1007
|
+
status: "reset-buf",
|
|
1008
|
+
newBuf: ""
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
1012
|
+
ctx.settings.lastQuery = sql;
|
|
1013
|
+
let fields;
|
|
1014
|
+
try {
|
|
1015
|
+
const stmt = await ctx.settings.db.prepare("", sql);
|
|
1016
|
+
fields = await stmt.describe();
|
|
1017
|
+
try {
|
|
1018
|
+
await stmt.close();
|
|
1019
|
+
} catch {}
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
return formatServerError(ctx, err, trimmedBuf);
|
|
1022
|
+
}
|
|
1023
|
+
if (fields.length === 0) {
|
|
1024
|
+
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1025
|
+
refreshErrorVars(ctx.settings, {
|
|
1026
|
+
kind: "success",
|
|
1027
|
+
rowCount: 0
|
|
1028
|
+
});
|
|
1029
|
+
return {
|
|
1030
|
+
status: "reset-buf",
|
|
1031
|
+
newBuf: ""
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
let rows;
|
|
1035
|
+
const formatQuery = buildGdescFormatQuery(fields);
|
|
1036
|
+
if (formatQuery === null) rows = [];
|
|
1037
|
+
else {
|
|
1038
|
+
const fallbackRows = () => fields.map((f) => [f.name, String(f.dataTypeID)]);
|
|
1039
|
+
try {
|
|
1040
|
+
const sets = await ctx.settings.db.execSimple(formatQuery);
|
|
1041
|
+
const last = sets[sets.length - 1];
|
|
1042
|
+
rows = last && last.rows.length > 0 ? last.rows : fallbackRows();
|
|
1043
|
+
} catch {
|
|
1044
|
+
rows = fallbackRows();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const rs = buildSyntheticGdescResultSet(rows);
|
|
1048
|
+
const printer = pickActivePrinter(ctx.settings);
|
|
1049
|
+
const out = pickOut(ctx.settings, null);
|
|
1050
|
+
try {
|
|
1051
|
+
await printer.printQuery(rs, ctx.settings.popt, out);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1054
|
+
return errResult(ctx, msg);
|
|
1055
|
+
}
|
|
1056
|
+
refreshErrorVars(ctx.settings, {
|
|
1057
|
+
kind: "success",
|
|
1058
|
+
rowCount: rs.rowCount
|
|
1059
|
+
});
|
|
1060
|
+
return {
|
|
1061
|
+
status: "reset-buf",
|
|
1062
|
+
newBuf: ""
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1654
1065
|
};
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
// Per-row errors are tolerated: upstream `\gexec` calls
|
|
1706
|
-
// `SendQuery` in a loop and ignores its return value (the only
|
|
1707
|
-
// escape is the global ON_ERROR_STOP variable, which the
|
|
1708
|
-
// conformance harness sets to 0). Without this, the regress
|
|
1709
|
-
// expects `drop table gexec_test\nERROR: ...\nselect ...` and we'd
|
|
1710
|
-
// truncate at the ERROR.
|
|
1711
|
-
let sawError = false;
|
|
1712
|
-
for (const rs of tupled) {
|
|
1713
|
-
for (const row of rs.rows) {
|
|
1714
|
-
for (const cell of row) {
|
|
1715
|
-
if (cell === null || cell === undefined)
|
|
1716
|
-
continue;
|
|
1717
|
-
const statement = formatCell(cell).trim();
|
|
1718
|
-
if (statement.length === 0)
|
|
1719
|
-
continue;
|
|
1720
|
-
if (shouldEcho) {
|
|
1721
|
-
out.write(statement + "\n");
|
|
1722
|
-
}
|
|
1723
|
-
try {
|
|
1724
|
-
const nested = await ctx.settings.db.execSimple(statement);
|
|
1725
|
-
for (const sub of nested) {
|
|
1726
|
-
if (sub.fields.length > 0) {
|
|
1727
|
-
await renderResult(ctx.settings, sub, out);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
catch (err) {
|
|
1732
|
-
// Each iteration is its own statement; render the per-row
|
|
1733
|
-
// server error in upstream's 3-line shape (LINE / caret are
|
|
1734
|
-
// positioned against `statement`, the offending row text)
|
|
1735
|
-
// but DO NOT return — vanilla continues to the next row.
|
|
1736
|
-
formatServerError(ctx, err, statement);
|
|
1737
|
-
sawError = true;
|
|
1738
|
-
// Honour ON_ERROR_STOP: when set, halt the loop after the
|
|
1739
|
-
// first failing row. Upstream's `do_gexec` consults the
|
|
1740
|
-
// global `pset.on_error_stop` flag via `SendQuery`'s
|
|
1741
|
-
// return; we mirror by checking the setting directly.
|
|
1742
|
-
if (ctx.settings.onErrorStop) {
|
|
1743
|
-
return { status: "error", errorWritten: true };
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
// Even with errors, return `reset-buf` so the mainloop clears the
|
|
1750
|
-
// outer `\gexec` buffer. Per-row error rendering already happened;
|
|
1751
|
-
// returning `error` here would re-trigger the writeError path.
|
|
1752
|
-
void sawError;
|
|
1753
|
-
return { status: "reset-buf", newBuf: "" };
|
|
1754
|
-
},
|
|
1066
|
+
const cmdGexec = {
|
|
1067
|
+
name: "gexec",
|
|
1068
|
+
helpKey: "gexec",
|
|
1069
|
+
async run(ctx) {
|
|
1070
|
+
const gated = pipelineGate(ctx);
|
|
1071
|
+
if (gated !== null) return gated;
|
|
1072
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1073
|
+
const bufSql = trimmedBuf.trim();
|
|
1074
|
+
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
1075
|
+
if (sql.length === 0) return {
|
|
1076
|
+
status: "reset-buf",
|
|
1077
|
+
newBuf: ""
|
|
1078
|
+
};
|
|
1079
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
1080
|
+
ctx.settings.lastQuery = sql;
|
|
1081
|
+
let firstPass;
|
|
1082
|
+
try {
|
|
1083
|
+
firstPass = await ctx.settings.db.execSimple(sql);
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
return formatServerError(ctx, err, trimmedBuf);
|
|
1086
|
+
}
|
|
1087
|
+
const tupled = firstPass.filter((r) => r.fields.length > 0);
|
|
1088
|
+
if (tupled.length === 0) return {
|
|
1089
|
+
status: "reset-buf",
|
|
1090
|
+
newBuf: ""
|
|
1091
|
+
};
|
|
1092
|
+
const out = pickOut(ctx.settings, null);
|
|
1093
|
+
const echo = ctx.settings.echo;
|
|
1094
|
+
const shouldEcho = echo === "all" || echo === "queries";
|
|
1095
|
+
for (const rs of tupled) for (const row of rs.rows) for (const cell of row) {
|
|
1096
|
+
if (cell === null || cell === void 0) continue;
|
|
1097
|
+
const statement = formatCell(cell).trim();
|
|
1098
|
+
if (statement.length === 0) continue;
|
|
1099
|
+
if (shouldEcho) out.write(statement + "\n");
|
|
1100
|
+
try {
|
|
1101
|
+
const nested = await ctx.settings.db.execSimple(statement);
|
|
1102
|
+
for (const sub of nested) if (sub.fields.length > 0) await renderResult(ctx.settings, sub, out);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
formatServerError(ctx, err, statement);
|
|
1105
|
+
if (ctx.settings.onErrorStop) return {
|
|
1106
|
+
status: "error",
|
|
1107
|
+
errorWritten: true
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
status: "reset-buf",
|
|
1113
|
+
newBuf: ""
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1755
1116
|
};
|
|
1756
|
-
// ---------------------------------------------------------------------------
|
|
1757
|
-
// \watch [args...]
|
|
1758
|
-
//
|
|
1759
|
-
// Upstream `\watch` accepts:
|
|
1760
|
-
//
|
|
1761
|
-
// \watch [SEC] — legacy positional interval (seconds)
|
|
1762
|
-
// \watch i=SEC — interval as named flag
|
|
1763
|
-
// \watch c=N — iteration count limit
|
|
1764
|
-
// \watch m=N — minimum row count: keep polling until the
|
|
1765
|
-
// result has >= N rows; uses `interval` as the
|
|
1766
|
-
// sleep between polls
|
|
1767
|
-
// \watch min_rows=N — long-form alias of `m=`
|
|
1768
|
-
//
|
|
1769
|
-
// Flags may be combined in any order. Duplicates (including the positional
|
|
1770
|
-
// interval colliding with `i=`) are rejected upstream with the message
|
|
1771
|
-
// "<thing> is specified more than once".
|
|
1772
|
-
//
|
|
1773
|
-
// The `WATCH_INTERVAL` psql variable supplies the default `interval` value
|
|
1774
|
-
// when `i=` is not given (and when there is no positional). The variable is
|
|
1775
|
-
// validated at `\set` time via a hook installed by `defaultSettings`.
|
|
1776
|
-
// ---------------------------------------------------------------------------
|
|
1777
1117
|
const sleepCancellable = (ms, signal) => new Promise((resolve) => {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1118
|
+
const timer = setTimeout(() => {
|
|
1119
|
+
signal.removeEventListener("abort", onAbort);
|
|
1120
|
+
resolve();
|
|
1121
|
+
}, ms);
|
|
1122
|
+
const onAbort = () => {
|
|
1123
|
+
clearTimeout(timer);
|
|
1124
|
+
signal.removeEventListener("abort", onAbort);
|
|
1125
|
+
resolve();
|
|
1126
|
+
};
|
|
1127
|
+
if (signal.aborted) {
|
|
1128
|
+
clearTimeout(timer);
|
|
1129
|
+
resolve();
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
signal.addEventListener("abort", onAbort);
|
|
1793
1133
|
});
|
|
1794
1134
|
/**
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1135
|
+
* Strictly parse a non-negative finite float.
|
|
1136
|
+
*
|
|
1137
|
+
* Returns the parsed number, or `null` for any of:
|
|
1138
|
+
* - empty string
|
|
1139
|
+
* - non-numeric trailing characters (e.g. `10ab`)
|
|
1140
|
+
* - negative values (e.g. `-10`)
|
|
1141
|
+
* - out-of-range / non-finite results (e.g. `10e400` → Infinity)
|
|
1142
|
+
*
|
|
1143
|
+
* Used to validate `\watch` intervals and the `WATCH_INTERVAL` variable.
|
|
1144
|
+
*/
|
|
1805
1145
|
const parseStrictNonNegativeFloat = (raw) => {
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const re = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
1813
|
-
if (!re.test(raw))
|
|
1814
|
-
return null;
|
|
1815
|
-
const value = parseFloat(raw);
|
|
1816
|
-
if (!Number.isFinite(value))
|
|
1817
|
-
return null;
|
|
1818
|
-
if (value < 0)
|
|
1819
|
-
return null;
|
|
1820
|
-
return value;
|
|
1146
|
+
if (raw.length === 0) return null;
|
|
1147
|
+
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(raw)) return null;
|
|
1148
|
+
const value = parseFloat(raw);
|
|
1149
|
+
if (!Number.isFinite(value)) return null;
|
|
1150
|
+
if (value < 0) return null;
|
|
1151
|
+
return value;
|
|
1821
1152
|
};
|
|
1822
1153
|
/**
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1154
|
+
* Parse a strict non-negative integer (no exponent, no fractional).
|
|
1155
|
+
* Used for `c=` and `m=` / `min_rows=` argument values.
|
|
1156
|
+
*/
|
|
1826
1157
|
const parseStrictNonNegativeInt = (raw) => {
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if (!Number.isFinite(value))
|
|
1833
|
-
return null;
|
|
1834
|
-
return value;
|
|
1158
|
+
if (raw.length === 0) return null;
|
|
1159
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
1160
|
+
const value = parseInt(raw, 10);
|
|
1161
|
+
if (!Number.isFinite(value)) return null;
|
|
1162
|
+
return value;
|
|
1835
1163
|
};
|
|
1836
1164
|
/**
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1165
|
+
* Default `\watch` interval (seconds). Mirrors upstream
|
|
1166
|
+
* `DEFAULT_WATCH_INTERVAL`. Exported so `defaultSettings` can substitute
|
|
1167
|
+
* it when the user unsets the `WATCH_INTERVAL` variable — upstream's
|
|
1168
|
+
* `watch_interval_substitute_hook` reseeds the value to `2` on null.
|
|
1169
|
+
*/
|
|
1170
|
+
const DEFAULT_WATCH_INTERVAL = "2";
|
|
1843
1171
|
/**
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
const WEEKDAYS = [
|
|
1172
|
+
* Render `\watch`'s per-iteration timestamp in upstream psql's
|
|
1173
|
+
* `ctime`-style layout: `Day Mon DD HH:MM:SS YYYY` (e.g. `Mon May 25
|
|
1174
|
+
* 19:41:55 2026`). Upstream calls `strftime("%c", &tm)` with the C locale;
|
|
1175
|
+
* we reproduce the field order in vanilla English so the output matches
|
|
1176
|
+
* regardless of the host locale.
|
|
1177
|
+
*
|
|
1178
|
+
* Exported only for unit-testing the format ladder.
|
|
1179
|
+
*/
|
|
1180
|
+
const WEEKDAYS = [
|
|
1181
|
+
"Sun",
|
|
1182
|
+
"Mon",
|
|
1183
|
+
"Tue",
|
|
1184
|
+
"Wed",
|
|
1185
|
+
"Thu",
|
|
1186
|
+
"Fri",
|
|
1187
|
+
"Sat"
|
|
1188
|
+
];
|
|
1853
1189
|
const MONTHS = [
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1190
|
+
"Jan",
|
|
1191
|
+
"Feb",
|
|
1192
|
+
"Mar",
|
|
1193
|
+
"Apr",
|
|
1194
|
+
"May",
|
|
1195
|
+
"Jun",
|
|
1196
|
+
"Jul",
|
|
1197
|
+
"Aug",
|
|
1198
|
+
"Sep",
|
|
1199
|
+
"Oct",
|
|
1200
|
+
"Nov",
|
|
1201
|
+
"Dec"
|
|
1866
1202
|
];
|
|
1867
|
-
const pad2 = (n) =>
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const month = MONTHS[now.getMonth()];
|
|
1871
|
-
const day = pad2(now.getDate());
|
|
1872
|
-
const hh = pad2(now.getHours());
|
|
1873
|
-
const mm = pad2(now.getMinutes());
|
|
1874
|
-
const ss = pad2(now.getSeconds());
|
|
1875
|
-
const year = String(now.getFullYear());
|
|
1876
|
-
return `${weekday} ${month} ${day} ${hh}:${mm}:${ss} ${year}`;
|
|
1203
|
+
const pad2 = (n) => n < 10 ? `0${String(n)}` : String(n);
|
|
1204
|
+
const formatWatchTimestamp = (now) => {
|
|
1205
|
+
return `${WEEKDAYS[now.getDay()]} ${MONTHS[now.getMonth()]} ${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())} ${String(now.getFullYear())}`;
|
|
1877
1206
|
};
|
|
1878
1207
|
/**
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
const WATCH_INTERVAL_MAX_SECONDS =
|
|
1208
|
+
* Upper bound on the `WATCH_INTERVAL` variable and the positional interval
|
|
1209
|
+
* — matches upstream which rejects "out of range" values. Upstream uses
|
|
1210
|
+
* `strtod` and rejects ±Infinity; we tighten further so a single watch loop
|
|
1211
|
+
* cannot sleep for longer than ~100 hours, which catches obvious typos
|
|
1212
|
+
* without breaking legitimate slow polls.
|
|
1213
|
+
*/
|
|
1214
|
+
const WATCH_INTERVAL_MAX_SECONDS = 36e4;
|
|
1886
1215
|
/**
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1216
|
+
* Resolve the effective default `\watch` interval from the `WATCH_INTERVAL`
|
|
1217
|
+
* psql variable. Returns the parsed value, the documented default
|
|
1218
|
+
* (`DEFAULT_WATCH_INTERVAL`), or an `error` envelope if the variable is set
|
|
1219
|
+
* but parses out of range.
|
|
1220
|
+
*/
|
|
1892
1221
|
const resolveWatchIntervalDefault = (settings) => {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
// `DEFAULT_WATCH_INTERVAL` when the var slot is empty.
|
|
1898
|
-
const raw = settings.vars.get("WATCH_INTERVAL") ?? DEFAULT_WATCH_INTERVAL;
|
|
1899
|
-
const parsed = parseStrictNonNegativeFloat(raw);
|
|
1900
|
-
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) {
|
|
1901
|
-
return {
|
|
1902
|
-
error: `WATCH_INTERVAL "${raw}" is out of range`,
|
|
1903
|
-
};
|
|
1904
|
-
}
|
|
1905
|
-
return { value: parsed };
|
|
1222
|
+
const raw = settings.vars.get("WATCH_INTERVAL") ?? "2";
|
|
1223
|
+
const parsed = parseStrictNonNegativeFloat(raw);
|
|
1224
|
+
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return { error: `WATCH_INTERVAL "${raw}" is out of range` };
|
|
1225
|
+
return { value: parsed };
|
|
1906
1226
|
};
|
|
1907
1227
|
/**
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1228
|
+
* Spawn the `\watch` pager for the full duration of the polling loop.
|
|
1229
|
+
*
|
|
1230
|
+
* Upstream `do_watch` wraps the loop in a single `popen` of
|
|
1231
|
+
* `PSQL_WATCH_PAGER`. It deliberately ignores `PSQL_PAGER` and `$PAGER`:
|
|
1232
|
+
*
|
|
1233
|
+
* > we ignore the regular PSQL_PAGER or PAGER environment variables,
|
|
1234
|
+
* > because traditional pagers probably won't be very useful for
|
|
1235
|
+
* > showing a stream of results.
|
|
1236
|
+
*
|
|
1237
|
+
* Mirror that here. Reading from `$PAGER` would silently hijack
|
|
1238
|
+
* `\watch` output for any user whose shell sets `PAGER=less` (the
|
|
1239
|
+
* common default), which makes the loop's output disappear into a
|
|
1240
|
+
* subprocess that diff harnesses can't capture.
|
|
1241
|
+
*
|
|
1242
|
+
* We use a single `sh -c <pager>` spawn so the user can set the
|
|
1243
|
+
* variable to a full command string (`less -R`, `tee /tmp/log`, …)
|
|
1244
|
+
* without the caller having to tokenise it. EPIPE on the stdin pipe is
|
|
1245
|
+
* swallowed for the same reason as in `openWriter`: the user may quit
|
|
1246
|
+
* `less` while we still have writes pending in the next iteration.
|
|
1247
|
+
*
|
|
1248
|
+
* Returns `null` when `PSQL_WATCH_PAGER` is unset or whitespace-only
|
|
1249
|
+
* (upstream's "no pager" rule), so the caller falls back to the normal
|
|
1250
|
+
* output target.
|
|
1251
|
+
*/
|
|
1932
1252
|
const openWatchPager = () => {
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
try {
|
|
1966
|
-
child.stdin.end();
|
|
1967
|
-
}
|
|
1968
|
-
catch (err) {
|
|
1969
|
-
const e = err;
|
|
1970
|
-
if (e.code !== "EPIPE")
|
|
1971
|
-
finish();
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}),
|
|
1975
|
-
};
|
|
1253
|
+
const cmd = process.env.PSQL_WATCH_PAGER ?? "";
|
|
1254
|
+
if (cmd.trim().length === 0) return null;
|
|
1255
|
+
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
1256
|
+
"pipe",
|
|
1257
|
+
"inherit",
|
|
1258
|
+
"inherit"
|
|
1259
|
+
] });
|
|
1260
|
+
child.stdin.on("error", (err) => {
|
|
1261
|
+
if (err.code !== "EPIPE") throw err;
|
|
1262
|
+
});
|
|
1263
|
+
return {
|
|
1264
|
+
stream: child.stdin,
|
|
1265
|
+
close: () => new Promise((resolve) => {
|
|
1266
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1267
|
+
resolve();
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
let settled = false;
|
|
1271
|
+
const finish = () => {
|
|
1272
|
+
if (settled) return;
|
|
1273
|
+
settled = true;
|
|
1274
|
+
resolve();
|
|
1275
|
+
};
|
|
1276
|
+
child.once("close", finish);
|
|
1277
|
+
child.once("error", finish);
|
|
1278
|
+
if (!child.stdin.destroyed) try {
|
|
1279
|
+
child.stdin.end();
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (err.code !== "EPIPE") finish();
|
|
1282
|
+
}
|
|
1283
|
+
})
|
|
1284
|
+
};
|
|
1976
1285
|
};
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
if ("error" in resolved) {
|
|
2081
|
-
return errResult(ctx, resolved.error);
|
|
2082
|
-
}
|
|
2083
|
-
interval = resolved.value;
|
|
2084
|
-
}
|
|
2085
|
-
const intervalMs = Math.round(interval * 1000);
|
|
2086
|
-
// Prefer a test-supplied controller; otherwise install a transient
|
|
2087
|
-
// SIGINT listener that aborts the loop.
|
|
2088
|
-
const controller = WATCH_TEST_CONTROLLER.ref ?? new AbortController();
|
|
2089
|
-
const sigintHandler = () => {
|
|
2090
|
-
controller.abort();
|
|
2091
|
-
};
|
|
2092
|
-
const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
|
|
2093
|
-
if (installedSigint) {
|
|
2094
|
-
process.once("SIGINT", sigintHandler);
|
|
2095
|
-
}
|
|
2096
|
-
// Open the pager once for the whole loop (upstream `do_watch` wraps the
|
|
2097
|
-
// entire session, not each iteration, so the user can scroll the
|
|
2098
|
-
// accumulated output in one go). When PSQL_WATCH_PAGER / PAGER aren't
|
|
2099
|
-
// set we fall through to the normal `pickOut` target.
|
|
2100
|
-
const pager = openWatchPager();
|
|
2101
|
-
const out = pager?.stream ?? pickOut(ctx.settings, null);
|
|
2102
|
-
try {
|
|
2103
|
-
// CONTINUOUS mode: when `c=` is absent, `iterSet` stays false and the
|
|
2104
|
-
// iteration-cap break below never fires, so the loop re-runs the query
|
|
2105
|
-
// on the interval forever — exactly upstream `do_watch`'s `for (i = 0;
|
|
2106
|
-
// !iter || i < iter; i++)` when `iter == 0`. The only exits are a
|
|
2107
|
-
// SIGINT (or the test controller) aborting `controller.signal`, a
|
|
2108
|
-
// server error, or the `min_rows` CONTINUE predicate failing.
|
|
2109
|
-
let iter = 0;
|
|
2110
|
-
while (!controller.signal.aborted) {
|
|
2111
|
-
iter++;
|
|
2112
|
-
const stamp = formatWatchTimestamp(new Date());
|
|
2113
|
-
out.write(`${stamp} (every ${String(interval)}s)\n\n`);
|
|
2114
|
-
let lastRowCount = 0;
|
|
2115
|
-
try {
|
|
2116
|
-
const results = await ctx.settings.db.execSimple(sql);
|
|
2117
|
-
for (const rs of results) {
|
|
2118
|
-
if (rs.fields.length > 0) {
|
|
2119
|
-
await renderResult(ctx.settings, rs, out);
|
|
2120
|
-
lastRowCount = rs.rows.length;
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
catch (err) {
|
|
2125
|
-
// Surface in upstream's 3-line ErrorResponse shape (severity +
|
|
2126
|
-
// message + LINE / caret) — same path top-level statement errors
|
|
2127
|
-
// take. The `\watch:` prefix is reserved for client-side
|
|
2128
|
-
// argument-parsing errors (e.g. `incorrect interval value "-10"`).
|
|
2129
|
-
// Pass the comment-stripped buffer so the `LINE N:` count starts
|
|
2130
|
-
// at the first content line — see runGCore for the rationale.
|
|
2131
|
-
return formatServerError(ctx, err, trimmedBuf);
|
|
2132
|
-
}
|
|
2133
|
-
// Stop if `c=` reached the configured iteration cap, OR if `m=`
|
|
2134
|
-
// was set and the previous result returned FEWER than `min_rows`
|
|
2135
|
-
// tuples. Upstream's `ExecQueryAndProcessResults` sets `return_early`
|
|
2136
|
-
// exactly when `min_rows > 0 && PQntuples(result) < min_rows`, and
|
|
2137
|
-
// `do_watch` breaks out of the loop on that signal — see PG source
|
|
2138
|
-
// `src/bin/psql/common.c::ExecQueryAndProcessResults`. In other
|
|
2139
|
-
// words `min_rows` is a CONTINUE predicate: keep polling while
|
|
2140
|
-
// the result has at least `min_rows` rows; stop the moment it
|
|
2141
|
-
// doesn't.
|
|
2142
|
-
if (iterSet && iter >= iterMax)
|
|
2143
|
-
break;
|
|
2144
|
-
if (minRowsSet && lastRowCount < minRows)
|
|
2145
|
-
break;
|
|
2146
|
-
if (controller.signal.aborted)
|
|
2147
|
-
break;
|
|
2148
|
-
// sleep_ms == 0 is upstream's "tight loop, no wait needed" — skip
|
|
2149
|
-
// the timer round-trip entirely so we don't queue a setTimeout(0)
|
|
2150
|
-
// for every iteration. Matches `do_watch`'s `if (sleep_ms == 0)
|
|
2151
|
-
// continue;` branch.
|
|
2152
|
-
if (intervalMs > 0) {
|
|
2153
|
-
await sleepCancellable(intervalMs, controller.signal);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
// Upstream `do_watch` injects a trailing newline AFTER the loop
|
|
2157
|
-
// ends when no pager is attached, to clear the cursor after a
|
|
2158
|
-
// possible `^C` echo. Mirror that here so the conformance output
|
|
2159
|
-
// shape (`...\n(N rows)\n\n\n`) matches vanilla psql.
|
|
2160
|
-
if (!pager) {
|
|
2161
|
-
out.write("\n");
|
|
2162
|
-
}
|
|
2163
|
-
return { status: "reset-buf", newBuf: "" };
|
|
2164
|
-
}
|
|
2165
|
-
finally {
|
|
2166
|
-
if (installedSigint) {
|
|
2167
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
2168
|
-
}
|
|
2169
|
-
// Drain the pager so its child has a chance to exit before \watch
|
|
2170
|
-
// returns. Failures are swallowed: a broken pager shouldn't mask the
|
|
2171
|
-
// (already-flushed) query results.
|
|
2172
|
-
if (pager) {
|
|
2173
|
-
await pager.close();
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
},
|
|
1286
|
+
const cmdWatch = {
|
|
1287
|
+
name: "watch",
|
|
1288
|
+
helpKey: "watch",
|
|
1289
|
+
async run(ctx) {
|
|
1290
|
+
const gated = pipelineGate(ctx);
|
|
1291
|
+
if (gated !== null) return gated;
|
|
1292
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1293
|
+
const sql = trimmedBuf.trim();
|
|
1294
|
+
if (sql.length === 0) return errResult(ctx, "no query buffer");
|
|
1295
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
1296
|
+
let intervalSet = false;
|
|
1297
|
+
let interval = null;
|
|
1298
|
+
let iterSet = false;
|
|
1299
|
+
let iterMax = 0;
|
|
1300
|
+
let minRowsSet = false;
|
|
1301
|
+
let minRows = 0;
|
|
1302
|
+
let positionalSeen = false;
|
|
1303
|
+
while (true) {
|
|
1304
|
+
const arg = ctx.nextArg("normal");
|
|
1305
|
+
if (arg === null) break;
|
|
1306
|
+
if (arg.length === 0) continue;
|
|
1307
|
+
const eqIdx = arg.indexOf("=");
|
|
1308
|
+
if (eqIdx > 0) {
|
|
1309
|
+
const key = arg.slice(0, eqIdx);
|
|
1310
|
+
const value = arg.slice(eqIdx + 1);
|
|
1311
|
+
if (key === "i") {
|
|
1312
|
+
if (intervalSet) return errResult(ctx, "interval value is specified more than once");
|
|
1313
|
+
const parsed = parseStrictNonNegativeFloat(value);
|
|
1314
|
+
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult(ctx, `incorrect interval value "${value}"`);
|
|
1315
|
+
interval = parsed;
|
|
1316
|
+
intervalSet = true;
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (key === "c") {
|
|
1320
|
+
if (iterSet) return errResult(ctx, "iteration count is specified more than once");
|
|
1321
|
+
const parsed = parseStrictNonNegativeInt(value);
|
|
1322
|
+
if (parsed === null || parsed === 0) return errResult(ctx, `incorrect iteration count "${value}"`);
|
|
1323
|
+
iterMax = parsed;
|
|
1324
|
+
iterSet = true;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
if (key === "m" || key === "min_rows") {
|
|
1328
|
+
if (minRowsSet) return errResult(ctx, "minimum row count specified more than once");
|
|
1329
|
+
const parsed = parseStrictNonNegativeInt(value);
|
|
1330
|
+
if (parsed === null) return errResult(ctx, `incorrect minimum row count "${value}"`);
|
|
1331
|
+
minRows = parsed;
|
|
1332
|
+
minRowsSet = true;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
return errResult(ctx, `unrecognized option "${key}"`);
|
|
1336
|
+
}
|
|
1337
|
+
if (positionalSeen || intervalSet) return errResult(ctx, "interval value is specified more than once");
|
|
1338
|
+
const parsed = parseStrictNonNegativeFloat(arg);
|
|
1339
|
+
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult(ctx, `incorrect interval value "${arg}"`);
|
|
1340
|
+
interval = parsed;
|
|
1341
|
+
intervalSet = true;
|
|
1342
|
+
positionalSeen = true;
|
|
1343
|
+
}
|
|
1344
|
+
if (interval === null) {
|
|
1345
|
+
const resolved = resolveWatchIntervalDefault(ctx.settings);
|
|
1346
|
+
if ("error" in resolved) return errResult(ctx, resolved.error);
|
|
1347
|
+
interval = resolved.value;
|
|
1348
|
+
}
|
|
1349
|
+
const intervalMs = Math.round(interval * 1e3);
|
|
1350
|
+
const controller = WATCH_TEST_CONTROLLER.ref ?? new AbortController();
|
|
1351
|
+
const sigintHandler = () => {
|
|
1352
|
+
controller.abort();
|
|
1353
|
+
};
|
|
1354
|
+
const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
|
|
1355
|
+
if (installedSigint) process.once("SIGINT", sigintHandler);
|
|
1356
|
+
const pager = openWatchPager();
|
|
1357
|
+
const out = pager?.stream ?? pickOut(ctx.settings, null);
|
|
1358
|
+
try {
|
|
1359
|
+
let iter = 0;
|
|
1360
|
+
while (!controller.signal.aborted) {
|
|
1361
|
+
iter++;
|
|
1362
|
+
const stamp = formatWatchTimestamp(/* @__PURE__ */ new Date());
|
|
1363
|
+
out.write(`${stamp} (every ${String(interval)}s)\n\n`);
|
|
1364
|
+
let lastRowCount = 0;
|
|
1365
|
+
try {
|
|
1366
|
+
const results = await ctx.settings.db.execSimple(sql);
|
|
1367
|
+
for (const rs of results) if (rs.fields.length > 0) {
|
|
1368
|
+
await renderResult(ctx.settings, rs, out);
|
|
1369
|
+
lastRowCount = rs.rows.length;
|
|
1370
|
+
}
|
|
1371
|
+
} catch (err) {
|
|
1372
|
+
return formatServerError(ctx, err, trimmedBuf);
|
|
1373
|
+
}
|
|
1374
|
+
if (iterSet && iter >= iterMax) break;
|
|
1375
|
+
if (minRowsSet && lastRowCount < minRows) break;
|
|
1376
|
+
if (controller.signal.aborted) break;
|
|
1377
|
+
if (intervalMs > 0) await sleepCancellable(intervalMs, controller.signal);
|
|
1378
|
+
}
|
|
1379
|
+
if (!pager) out.write("\n");
|
|
1380
|
+
return {
|
|
1381
|
+
status: "reset-buf",
|
|
1382
|
+
newBuf: ""
|
|
1383
|
+
};
|
|
1384
|
+
} finally {
|
|
1385
|
+
if (installedSigint) process.removeListener("SIGINT", sigintHandler);
|
|
1386
|
+
if (pager) await pager.close();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
2177
1389
|
};
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
registry.register(cmdGdesc);
|
|
2191
|
-
registry.register(cmdGexec);
|
|
2192
|
-
registry.register(cmdWatch);
|
|
1390
|
+
const registerIoCommands = (registry) => {
|
|
1391
|
+
registry.register(cmdInclude);
|
|
1392
|
+
registry.register(cmdIncludeRel);
|
|
1393
|
+
registry.register(cmdOut);
|
|
1394
|
+
registry.register(cmdWrite);
|
|
1395
|
+
registry.register(cmdG);
|
|
1396
|
+
registry.register(cmdGx);
|
|
1397
|
+
registry.register(cmdPrint);
|
|
1398
|
+
registry.register(cmdGset);
|
|
1399
|
+
registry.register(cmdGdesc);
|
|
1400
|
+
registry.register(cmdGexec);
|
|
1401
|
+
registry.register(cmdWatch);
|
|
2193
1402
|
};
|
|
1403
|
+
//#endregion
|
|
1404
|
+
export { DEFAULT_WATCH_INTERVAL, WATCH_TEST_CONTROLLER, clearPipelineGateErrors, cmdG, cmdGdesc, cmdGexec, cmdGset, cmdGx, cmdInclude, cmdIncludeRel, cmdOut, cmdPrint, cmdWatch, cmdWrite, formatWatchTimestamp, getQueryFout, registerIoCommands };
|