neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +615 -846
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +2 -446
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +2 -51
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +728 -984
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +2 -2193
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +2 -846
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +2 -2030
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +8 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +39 -45
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
|
@@ -1,728 +1,598 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* TypeScript port of the corresponding `exec_command_*` functions in
|
|
5
|
-
* upstream PostgreSQL's `src/bin/psql/command.c`:
|
|
6
|
-
*
|
|
7
|
-
* - `\q` / `\quit` → exec_command_quit
|
|
8
|
-
* - `\r` / `\reset` → exec_command_reset
|
|
9
|
-
* - `\!` → exec_command_shell_escape (do_shell)
|
|
10
|
-
* - `\cd` → exec_command_cd
|
|
11
|
-
* - `\echo`, `\qecho`, `\warn` → exec_command_echo / qecho / warn
|
|
12
|
-
* - `\prompt` → exec_command_prompt
|
|
13
|
-
* - `\set`, `\unset` → exec_command_set / exec_command_unset
|
|
14
|
-
* - `\getenv`, `\setenv` → exec_command_getenv / exec_command_setenv
|
|
15
|
-
* - `\errverbose` → exec_command_errverbose
|
|
16
|
-
* - `\timing` → exec_command_timing
|
|
17
|
-
* - `\copyright` → exec_command_copyright
|
|
18
|
-
* - `\h` / `\help` → exec_command_help (helpSQL)
|
|
19
|
-
*
|
|
20
|
-
* Each command is exported as a `BackslashCmdSpec` so {@link defaultRegistry}
|
|
21
|
-
* in `dispatch.ts` can register them. Error messages follow upstream's
|
|
22
|
-
* `\<cmd>: <message>` shape and go to stderr; on failure we return
|
|
23
|
-
* `{ status: 'error' }`. Successful invocations return `{ status: 'ok' }`.
|
|
24
|
-
*
|
|
25
|
-
* Stubs / deferred behaviour:
|
|
26
|
-
*
|
|
27
|
-
* - `\!` always returns `{ status: 'ok' }` — upstream does not propagate
|
|
28
|
-
* the child's exit status to the surrounding script, only the run-mode.
|
|
29
|
-
* Tests use a stdio mock; in interactive use the child inherits stdio.
|
|
30
|
-
* - `\prompt -` (no-echo password prompting) reads via the shared input
|
|
31
|
-
* layer with echo suppressed on a TTY (falling back to a plain read on
|
|
32
|
-
* non-interactive input).
|
|
33
|
-
* - `\qecho` writes to `settings.logfile` if set, else stdout. Upstream
|
|
34
|
-
* additionally honours a separate "query output" file set via `\o`;
|
|
35
|
-
* that wiring lives in WP-15 and we leave the hook in place.
|
|
36
|
-
*/
|
|
37
|
-
import { spawnSync } from "node:child_process";
|
|
38
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
39
|
-
import { tmpdir } from "node:os";
|
|
40
|
-
import { join } from "node:path";
|
|
1
|
+
import { parseBool, writeErr, writeOut } from "./shared.js";
|
|
2
|
+
import { readLine } from "../io/input.js";
|
|
41
3
|
import { helpSQL, slashUsage } from "../core/help.js";
|
|
42
4
|
import { getHistory } from "../io/history.js";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
//#region src/psql/command/cmd_meta.ts
|
|
10
|
+
/**
|
|
11
|
+
* Meta backslash commands.
|
|
12
|
+
*
|
|
13
|
+
* TypeScript port of the corresponding `exec_command_*` functions in
|
|
14
|
+
* upstream PostgreSQL's `src/bin/psql/command.c`:
|
|
15
|
+
*
|
|
16
|
+
* - `\q` / `\quit` → exec_command_quit
|
|
17
|
+
* - `\r` / `\reset` → exec_command_reset
|
|
18
|
+
* - `\!` → exec_command_shell_escape (do_shell)
|
|
19
|
+
* - `\cd` → exec_command_cd
|
|
20
|
+
* - `\echo`, `\qecho`, `\warn` → exec_command_echo / qecho / warn
|
|
21
|
+
* - `\prompt` → exec_command_prompt
|
|
22
|
+
* - `\set`, `\unset` → exec_command_set / exec_command_unset
|
|
23
|
+
* - `\getenv`, `\setenv` → exec_command_getenv / exec_command_setenv
|
|
24
|
+
* - `\errverbose` → exec_command_errverbose
|
|
25
|
+
* - `\timing` → exec_command_timing
|
|
26
|
+
* - `\copyright` → exec_command_copyright
|
|
27
|
+
* - `\h` / `\help` → exec_command_help (helpSQL)
|
|
28
|
+
*
|
|
29
|
+
* Each command is exported as a `BackslashCmdSpec` so {@link defaultRegistry}
|
|
30
|
+
* in `dispatch.ts` can register them. Error messages follow upstream's
|
|
31
|
+
* `\<cmd>: <message>` shape and go to stderr; on failure we return
|
|
32
|
+
* `{ status: 'error' }`. Successful invocations return `{ status: 'ok' }`.
|
|
33
|
+
*
|
|
34
|
+
* Stubs / deferred behaviour:
|
|
35
|
+
*
|
|
36
|
+
* - `\!` always returns `{ status: 'ok' }` — upstream does not propagate
|
|
37
|
+
* the child's exit status to the surrounding script, only the run-mode.
|
|
38
|
+
* Tests use a stdio mock; in interactive use the child inherits stdio.
|
|
39
|
+
* - `\prompt -` (no-echo password prompting) reads via the shared input
|
|
40
|
+
* layer with echo suppressed on a TTY (falling back to a plain read on
|
|
41
|
+
* non-interactive input).
|
|
42
|
+
* - `\qecho` writes to `settings.logfile` if set, else stdout. Upstream
|
|
43
|
+
* additionally honours a separate "query output" file set via `\o`;
|
|
44
|
+
* that wiring lives in WP-15 and we leave the hook in place.
|
|
45
|
+
*/
|
|
45
46
|
/** `\q` / `\quit` — exit the REPL. */
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const cmdQuit = {
|
|
48
|
+
name: "q",
|
|
49
|
+
aliases: ["quit"],
|
|
50
|
+
helpKey: "q",
|
|
51
|
+
run: () => Promise.resolve({ status: "exit" })
|
|
51
52
|
};
|
|
52
53
|
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// Upstream `do_shell` swallows shell-spawn failures: the REPL has to
|
|
82
|
-
// keep running even when the child won't start. Status stays `ok` so
|
|
83
|
-
// a failing `\!` is purely informational.
|
|
84
|
-
}
|
|
85
|
-
return Promise.resolve({ status: "ok" });
|
|
86
|
-
},
|
|
54
|
+
* `\!` — shell escape. Whole-line mode: the entire rest of the line is the
|
|
55
|
+
* command string.
|
|
56
|
+
*
|
|
57
|
+
* - `\!` (no args) → spawn `$SHELL -i` (fallback `sh -i`)
|
|
58
|
+
* - `\!command args` → spawn `sh -c 'command args'`
|
|
59
|
+
*
|
|
60
|
+
* In both cases the child inherits stdio and we return `{ status: 'ok' }`
|
|
61
|
+
* regardless of the child's exit status — matching upstream `do_shell`,
|
|
62
|
+
* which keeps the REPL alive after a failing shell command rather than
|
|
63
|
+
* propagating the exit code. Catching a spawn-time exception keeps us
|
|
64
|
+
* resilient against environments where `sh` is unavailable.
|
|
65
|
+
*/
|
|
66
|
+
const cmdShell = {
|
|
67
|
+
name: "!",
|
|
68
|
+
argMode: "whole-line",
|
|
69
|
+
helpKey: "!",
|
|
70
|
+
run: (ctx) => {
|
|
71
|
+
const line = ctx.restOfLine().trim();
|
|
72
|
+
try {
|
|
73
|
+
if (line.length === 0) {
|
|
74
|
+
const shell = process.env.SHELL ?? "/bin/sh";
|
|
75
|
+
spawnSync(shell, ["-i"], { stdio: "inherit" });
|
|
76
|
+
} else spawnSync("sh", ["-c", line], { stdio: "inherit" });
|
|
77
|
+
} catch {}
|
|
78
|
+
return Promise.resolve({ status: "ok" });
|
|
79
|
+
}
|
|
87
80
|
};
|
|
88
81
|
/** `\cd [dir]` — change cwd. No arg falls back to `$HOME`. */
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
82
|
+
const cmdCd = {
|
|
83
|
+
name: "cd",
|
|
84
|
+
helpKey: "cd",
|
|
85
|
+
run: (ctx) => {
|
|
86
|
+
const dir = ctx.nextArg("normal");
|
|
87
|
+
const target = dir && dir.length > 0 ? dir : process.env.HOME ?? null;
|
|
88
|
+
if (!target) {
|
|
89
|
+
writeErr(`\\${ctx.cmdName}: could not determine home directory\n`);
|
|
90
|
+
return Promise.resolve({ status: "error" });
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
process.chdir(target);
|
|
94
|
+
return Promise.resolve({ status: "ok" });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
98
|
+
return Promise.resolve({ status: "error" });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
109
101
|
};
|
|
110
102
|
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
* Helper for `\echo` / `\qecho` / `\warn`. Reads args until exhausted,
|
|
104
|
+
* honours the leading `-n` flag (suppresses trailing newline), joins with
|
|
105
|
+
* single spaces, and writes to the chosen stream.
|
|
106
|
+
*
|
|
107
|
+
* Upstream `exec_command_echo` only treats `-n` as a flag when the source
|
|
108
|
+
* was the unquoted two-character token `-n`. `'-n'` (single-quoted) is a
|
|
109
|
+
* literal value: it should be printed AND the trailing newline kept. We
|
|
110
|
+
* inspect `ctx.rawArgs` directly because `nextArg` discards quote
|
|
111
|
+
* metadata after lexing.
|
|
112
|
+
*/
|
|
121
113
|
const runEcho = (ctx, write) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
first = false;
|
|
145
|
-
parts.push(arg);
|
|
146
|
-
}
|
|
147
|
-
const out = parts.join(" ") + (noNewline ? "" : "\n");
|
|
148
|
-
write(out);
|
|
149
|
-
return { status: "ok" };
|
|
114
|
+
const parts = [];
|
|
115
|
+
let noNewline = false;
|
|
116
|
+
let first = true;
|
|
117
|
+
const firstArgIsUnquotedDashN = (() => {
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < ctx.rawArgs.length && /\s/.test(ctx.rawArgs[i])) i++;
|
|
120
|
+
return ctx.rawArgs.slice(i, i + 2) === "-n" && (i + 2 === ctx.rawArgs.length || /\s/.test(ctx.rawArgs[i + 2]));
|
|
121
|
+
})();
|
|
122
|
+
for (;;) {
|
|
123
|
+
const arg = ctx.nextArg("normal");
|
|
124
|
+
if (arg === null) break;
|
|
125
|
+
if (first && firstArgIsUnquotedDashN && arg === "-n") {
|
|
126
|
+
noNewline = true;
|
|
127
|
+
first = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
first = false;
|
|
131
|
+
parts.push(arg);
|
|
132
|
+
}
|
|
133
|
+
write(parts.join(" ") + (noNewline ? "" : "\n"));
|
|
134
|
+
return { status: "ok" };
|
|
150
135
|
};
|
|
151
136
|
/** `\echo` — write args to stdout. */
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
137
|
+
const cmdEcho = {
|
|
138
|
+
name: "echo",
|
|
139
|
+
helpKey: "echo",
|
|
140
|
+
run: (ctx) => Promise.resolve(runEcho(ctx, writeOut))
|
|
156
141
|
};
|
|
157
142
|
/** `\qecho` — write args to the query output (logfile if set, else stdout). */
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
return Promise.resolve(runEcho(ctx, write));
|
|
172
|
-
},
|
|
143
|
+
const cmdQecho = {
|
|
144
|
+
name: "qecho",
|
|
145
|
+
helpKey: "qecho",
|
|
146
|
+
run: (ctx) => {
|
|
147
|
+
const { logfile } = ctx.settings;
|
|
148
|
+
const write = (s) => {
|
|
149
|
+
if (logfile) logfile.write(s);
|
|
150
|
+
else writeOut(s);
|
|
151
|
+
};
|
|
152
|
+
return Promise.resolve(runEcho(ctx, write));
|
|
153
|
+
}
|
|
173
154
|
};
|
|
174
155
|
/** `\warn` — write args to stderr. */
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
156
|
+
const cmdWarn = {
|
|
157
|
+
name: "warn",
|
|
158
|
+
helpKey: "warn",
|
|
159
|
+
run: (ctx) => Promise.resolve(runEcho(ctx, writeErr))
|
|
179
160
|
};
|
|
180
161
|
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return { status: "error" };
|
|
228
|
-
}
|
|
229
|
-
return { status: "ok" };
|
|
230
|
-
},
|
|
162
|
+
* `\prompt [TEXT] varname`
|
|
163
|
+
*
|
|
164
|
+
* Upstream: read one line of input from the terminal, optionally with a
|
|
165
|
+
* prompt prefix, and assign it to a psql variable. A leading `-` flag
|
|
166
|
+
* requests a no-echo read (used for password prompts); we honour it by
|
|
167
|
+
* reading through the shared input layer with echo suppressed on a TTY
|
|
168
|
+
* (and a plain read otherwise — non-interactive input still consumes the
|
|
169
|
+
* line, matching upstream).
|
|
170
|
+
*
|
|
171
|
+
* Args after the optional `-` flag are `[TEXT] varname`: if only one
|
|
172
|
+
* remains it is the variable name and no prompt prefix is shown; if two,
|
|
173
|
+
* the first is the prompt and the second the variable.
|
|
174
|
+
*/
|
|
175
|
+
const cmdPrompt = {
|
|
176
|
+
name: "prompt",
|
|
177
|
+
helpKey: "prompt",
|
|
178
|
+
run: async (ctx) => {
|
|
179
|
+
const args = [];
|
|
180
|
+
for (;;) {
|
|
181
|
+
const a = ctx.nextArg("normal");
|
|
182
|
+
if (a === null) break;
|
|
183
|
+
args.push(a);
|
|
184
|
+
}
|
|
185
|
+
let echo = true;
|
|
186
|
+
if (args.length > 0 && args[0] === "-") {
|
|
187
|
+
echo = false;
|
|
188
|
+
args.shift();
|
|
189
|
+
}
|
|
190
|
+
if (args.length === 0) {
|
|
191
|
+
writeErr(`\\${ctx.cmdName}: missing required argument\n`);
|
|
192
|
+
return { status: "error" };
|
|
193
|
+
}
|
|
194
|
+
let promptText = "";
|
|
195
|
+
let varname;
|
|
196
|
+
if (args.length === 1) varname = args[0];
|
|
197
|
+
else {
|
|
198
|
+
promptText = args[0];
|
|
199
|
+
varname = args[1];
|
|
200
|
+
}
|
|
201
|
+
const line = await readLine(promptText, { echo });
|
|
202
|
+
if (!ctx.settings.vars.set(varname, line)) {
|
|
203
|
+
writeErr(`\\${ctx.cmdName}: invalid variable name "${varname}"\n`);
|
|
204
|
+
return { status: "error" };
|
|
205
|
+
}
|
|
206
|
+
return { status: "ok" };
|
|
207
|
+
}
|
|
231
208
|
};
|
|
232
209
|
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
else if (result.error !== undefined) {
|
|
279
|
-
// Hook supplied its own wording — emit verbatim, prefixed with
|
|
280
|
-
// `psql: `. The message intentionally does NOT carry a severity
|
|
281
|
-
// (`error:` / `ERROR:`) because upstream's per-variable hooks
|
|
282
|
-
// also emit just `psql: <msg>` (see `bool_substitute_hook` etc.).
|
|
283
|
-
writeErr(`${prefix}${result.error}\n`);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
// Hook returned `false` without a message — fall back to a
|
|
287
|
-
// generic line so callers still see something. None of the
|
|
288
|
-
// built-in hooks take this path, but third-party callers might.
|
|
289
|
-
writeErr(`${prefix}error while setting variable "${name}"\n`);
|
|
290
|
-
}
|
|
291
|
-
return Promise.resolve({ status: "error" });
|
|
292
|
-
}
|
|
293
|
-
return Promise.resolve({ status: "ok" });
|
|
294
|
-
},
|
|
210
|
+
* `\set [varname [value...]]`
|
|
211
|
+
*
|
|
212
|
+
* - No args → list all variables (sorted, `name = 'value'` per line) to
|
|
213
|
+
* stdout. Upstream uses single-quotes around the value.
|
|
214
|
+
* - One arg → set the variable to the empty string.
|
|
215
|
+
* - More args → join the rest with a single space and set the variable.
|
|
216
|
+
*
|
|
217
|
+
* Diagnostics mirror upstream `exec_command_set` in `src/bin/psql/command.c`:
|
|
218
|
+
*
|
|
219
|
+
* - Names containing characters outside `[A-Za-z_][A-Za-z0-9_]*` produce
|
|
220
|
+
* `invalid variable name: "<name>"` (prefixed with `psql: `).
|
|
221
|
+
* - Per-variable hook rejections (AUTOCOMMIT / FETCH_COUNT /
|
|
222
|
+
* ON_ERROR_ROLLBACK / VERBOSITY / etc.) carry the hook's message
|
|
223
|
+
* verbatim; we add only the `psql: ` prefix.
|
|
224
|
+
* - Hook vetoes with no message fall back to a generic line. This
|
|
225
|
+
* should not happen in practice — every registered hook either
|
|
226
|
+
* accepts or returns a wording string.
|
|
227
|
+
*/
|
|
228
|
+
const cmdSet = {
|
|
229
|
+
name: "set",
|
|
230
|
+
helpKey: "set",
|
|
231
|
+
run: (ctx) => {
|
|
232
|
+
const name = ctx.nextArg("normal");
|
|
233
|
+
if (name === null) {
|
|
234
|
+
const entries = [...ctx.settings.vars.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
235
|
+
for (const [k, v] of entries) writeOut(`${k} = '${v}'\n`);
|
|
236
|
+
return Promise.resolve({ status: "ok" });
|
|
237
|
+
}
|
|
238
|
+
const values = [];
|
|
239
|
+
for (;;) {
|
|
240
|
+
const a = ctx.nextArg("normal");
|
|
241
|
+
if (a === null) break;
|
|
242
|
+
values.push(a);
|
|
243
|
+
}
|
|
244
|
+
const value = values.join("");
|
|
245
|
+
const result = ctx.settings.vars.trySet(name, value);
|
|
246
|
+
if (!result.ok) {
|
|
247
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
248
|
+
if (result.reason === "invalid-name") writeErr(`${prefix}invalid variable name: "${name}"\n`);
|
|
249
|
+
else if (result.error !== void 0) writeErr(`${prefix}${result.error}\n`);
|
|
250
|
+
else writeErr(`${prefix}error while setting variable "${name}"\n`);
|
|
251
|
+
return Promise.resolve({ status: "error" });
|
|
252
|
+
}
|
|
253
|
+
return Promise.resolve({ status: "ok" });
|
|
254
|
+
}
|
|
295
255
|
};
|
|
296
256
|
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
257
|
+
* `\r` / `\reset` — discard the accumulated query buffer.
|
|
258
|
+
*
|
|
259
|
+
* Mirrors upstream `exec_command_reset`:
|
|
260
|
+
*
|
|
261
|
+
* resetPQExpBuffer(query_buf);
|
|
262
|
+
* psql_scan_reset(scan_state);
|
|
263
|
+
* if (!pset.quiet)
|
|
264
|
+
* puts(_("Query buffer reset (cleared)."));
|
|
265
|
+
*
|
|
266
|
+
* We model the buffer + scanner reset via `status: 'reset-buf'`; the
|
|
267
|
+
* mainloop wipes `queryBuf` and re-initialises `scanState` when it sees
|
|
268
|
+
* this. The diagnostic is gated on the `quiet` setting so `psql -q` (and
|
|
269
|
+
* the regress harness, which passes `--quiet`) produces no output.
|
|
270
|
+
*/
|
|
271
|
+
const cmdReset = {
|
|
272
|
+
name: "r",
|
|
273
|
+
aliases: ["reset"],
|
|
274
|
+
helpKey: "r",
|
|
275
|
+
run: (ctx) => {
|
|
276
|
+
if (!ctx.settings.quiet) writeOut("Query buffer reset (cleared).\n");
|
|
277
|
+
return Promise.resolve({
|
|
278
|
+
status: "reset-buf",
|
|
279
|
+
newBuf: ""
|
|
280
|
+
});
|
|
281
|
+
}
|
|
321
282
|
};
|
|
322
283
|
/** `\unset varname` — unset a psql variable. */
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
284
|
+
const cmdUnset = {
|
|
285
|
+
name: "unset",
|
|
286
|
+
helpKey: "unset",
|
|
287
|
+
run: (ctx) => {
|
|
288
|
+
const name = ctx.nextArg("normal");
|
|
289
|
+
if (name === null) {
|
|
290
|
+
writeErr(`\\${ctx.cmdName}: missing required argument\n`);
|
|
291
|
+
return Promise.resolve({ status: "error" });
|
|
292
|
+
}
|
|
293
|
+
ctx.settings.vars.unset(name);
|
|
294
|
+
return Promise.resolve({ status: "ok" });
|
|
295
|
+
}
|
|
335
296
|
};
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return Promise.resolve({ status: "ok" });
|
|
355
|
-
},
|
|
297
|
+
const cmdGetenv = {
|
|
298
|
+
name: "getenv",
|
|
299
|
+
helpKey: "getenv",
|
|
300
|
+
run: (ctx) => {
|
|
301
|
+
const varname = ctx.nextArg("normal");
|
|
302
|
+
const envname = ctx.nextArg("normal");
|
|
303
|
+
if (varname === null || envname === null) {
|
|
304
|
+
writeErr(`\\${ctx.cmdName}: missing required argument\n`);
|
|
305
|
+
return Promise.resolve({ status: "error" });
|
|
306
|
+
}
|
|
307
|
+
const value = process.env[envname];
|
|
308
|
+
if (value === void 0) return Promise.resolve({ status: "ok" });
|
|
309
|
+
if (!ctx.settings.vars.set(varname, value)) {
|
|
310
|
+
writeErr(`\\${ctx.cmdName}: invalid variable name "${varname}"\n`);
|
|
311
|
+
return Promise.resolve({ status: "error" });
|
|
312
|
+
}
|
|
313
|
+
return Promise.resolve({ status: "ok" });
|
|
314
|
+
}
|
|
356
315
|
};
|
|
357
316
|
/**
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// a single mode for both calls also keeps positional reads in sync —
|
|
382
|
-
// each cursor advances exactly once per call.
|
|
383
|
-
const value = ctx.nextArg("normal");
|
|
384
|
-
if (value === null) {
|
|
385
|
-
delete process.env[envname];
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
process.env[envname] = value;
|
|
389
|
-
}
|
|
390
|
-
return Promise.resolve({ status: "ok" });
|
|
391
|
-
},
|
|
317
|
+
* `\setenv envvar [value]`
|
|
318
|
+
*
|
|
319
|
+
* Set `process.env[envvar] = value`; with no value, delete it. Upstream
|
|
320
|
+
* rejects names containing `=`.
|
|
321
|
+
*/
|
|
322
|
+
const cmdSetenv = {
|
|
323
|
+
name: "setenv",
|
|
324
|
+
helpKey: "setenv",
|
|
325
|
+
run: (ctx) => {
|
|
326
|
+
const envname = ctx.nextArg("normal");
|
|
327
|
+
if (envname === null) {
|
|
328
|
+
writeErr(`\\${ctx.cmdName}: missing required argument\n`);
|
|
329
|
+
return Promise.resolve({ status: "error" });
|
|
330
|
+
}
|
|
331
|
+
if (envname.includes("=")) {
|
|
332
|
+
writeErr(`\\${ctx.cmdName}: environment variable name must not contain "="\n`);
|
|
333
|
+
return Promise.resolve({ status: "error" });
|
|
334
|
+
}
|
|
335
|
+
const value = ctx.nextArg("normal");
|
|
336
|
+
if (value === null) delete process.env[envname];
|
|
337
|
+
else process.env[envname] = value;
|
|
338
|
+
return Promise.resolve({ status: "ok" });
|
|
339
|
+
}
|
|
392
340
|
};
|
|
393
341
|
/**
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
342
|
+
* Build the `psql:` diagnostic prefix that upstream `pg_log_pre_callback`
|
|
343
|
+
* prepends to error lines, but ONLY when reading from a script file. Mirrors:
|
|
344
|
+
*
|
|
345
|
+
* if (cur_cmd_source == QUERY_FROM_FILE)
|
|
346
|
+
* fprintf(stderr, "psql:%s:%d: ", cur_cmd_filename, cur_cmd_lineno);
|
|
347
|
+
*
|
|
348
|
+
* - `curCmdSource === 'file'` (running under `-f FILE`, `\i FILE`,
|
|
349
|
+
* `\ir FILE`, or `.psqlrc`): `psql:<inputfile>:<lineno>: `.
|
|
350
|
+
* - Stdin pipe, `-c "..."`, interactive REPL: empty string — vanilla
|
|
351
|
+
* `psql --no-psqlrc -X` reading SQL from stdin emits NO prefix on
|
|
352
|
+
* either `\set` validation errors or server `ERROR:` lines.
|
|
353
|
+
*
|
|
354
|
+
* Returned string ends in a trailing space when non-empty so callers can
|
|
355
|
+
* concatenate the severity directly (`prefix + 'ERROR: msg'`).
|
|
356
|
+
*/
|
|
357
|
+
const psqlErrorPrefix = (settings, lineNumber) => {
|
|
358
|
+
if (settings.curCmdSource === "file" && settings.inputfile) {
|
|
359
|
+
const lineSuffix = lineNumber !== void 0 ? String(lineNumber) : "";
|
|
360
|
+
return `psql:${settings.inputfile}:${lineSuffix}: `;
|
|
361
|
+
}
|
|
362
|
+
return "";
|
|
415
363
|
};
|
|
416
364
|
/**
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
365
|
+
* Walk past leading whitespace + `--` line comments + slash-star block
|
|
366
|
+
* comments at the head of `sqlText`. Returns the byte index of the first
|
|
367
|
+
* "real" content character. Used by `renderLineAndCaret` to align the
|
|
368
|
+
* `LINE N:` counter with upstream psql — vanilla strips these from the
|
|
369
|
+
* buffer before `PQexec` (so the server's `position` is relative to the
|
|
370
|
+
* trimmed buffer), but `captureLastError`/`normaliseSqlAndPosition` only
|
|
371
|
+
* strips whitespace. Re-stripping here closes the gap when the captured
|
|
372
|
+
* `sqlText` still carries leading `-- comment` lines (the common case
|
|
373
|
+
* for SQL that the mainloop dispatched directly via `sendQuery`,
|
|
374
|
+
* because that path doesn't pre-trim comments).
|
|
375
|
+
*
|
|
376
|
+
* Idempotent for already-trimmed input: if `sqlText` has no leading
|
|
377
|
+
* prelude we return `0`, the caller takes the existing fast-path, and
|
|
378
|
+
* the LINE count remains the count of newlines strictly before
|
|
379
|
+
* `position - 1`.
|
|
380
|
+
*/
|
|
433
381
|
const skipLeadingPrelude = (sqlText) => {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
else if (sqlText.charCodeAt(i) === 0x2a &&
|
|
463
|
-
sqlText.charCodeAt(i + 1) === 0x2f) {
|
|
464
|
-
depth--;
|
|
465
|
-
i += 2;
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
i++;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
break;
|
|
474
|
-
}
|
|
475
|
-
return i;
|
|
382
|
+
let i = 0;
|
|
383
|
+
const n = sqlText.length;
|
|
384
|
+
while (i < n) {
|
|
385
|
+
const c = sqlText.charCodeAt(i);
|
|
386
|
+
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11) {
|
|
387
|
+
i++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (c === 45 && sqlText.charCodeAt(i + 1) === 45) {
|
|
391
|
+
i += 2;
|
|
392
|
+
while (i < n && sqlText.charCodeAt(i) !== 10) i++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (c === 47 && sqlText.charCodeAt(i + 1) === 42) {
|
|
396
|
+
i += 2;
|
|
397
|
+
let depth = 1;
|
|
398
|
+
while (i < n && depth > 0) if (sqlText.charCodeAt(i) === 47 && sqlText.charCodeAt(i + 1) === 42) {
|
|
399
|
+
depth++;
|
|
400
|
+
i += 2;
|
|
401
|
+
} else if (sqlText.charCodeAt(i) === 42 && sqlText.charCodeAt(i + 1) === 47) {
|
|
402
|
+
depth--;
|
|
403
|
+
i += 2;
|
|
404
|
+
} else i++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
return i;
|
|
476
410
|
};
|
|
477
411
|
/**
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const lineText = trimmed.slice(lineStart, lineEnd);
|
|
542
|
-
// Line number for the `LINE N:` prefix — 1-based.
|
|
543
|
-
const before = trimmed.slice(0, lineStart);
|
|
544
|
-
const lineNumber = (before.match(/\n/gu)?.length ?? 0) + 1;
|
|
545
|
-
// Column inside the picked line (0-based) where the `^` goes. Tabs
|
|
546
|
-
// upstream are expanded to a fixed width; we approximate with a
|
|
547
|
-
// single space so the pointer at least lands in the right ballpark.
|
|
548
|
-
let col = idx - lineStart;
|
|
549
|
-
// Snap past trailing whitespace when the position lands inside it —
|
|
550
|
-
// see the function header for the rationale (trim-on-send delta).
|
|
551
|
-
const lineTrimEndLen = lineText.replace(/[ \t\f\v]+$/u, "").length;
|
|
552
|
-
if (col >= lineTrimEndLen && col < lineText.length) {
|
|
553
|
-
col = lineText.length;
|
|
554
|
-
}
|
|
555
|
-
const caretIndent = " ".repeat(Math.max(0, col));
|
|
556
|
-
const prefix = `LINE ${String(lineNumber)}: `;
|
|
557
|
-
return {
|
|
558
|
-
line: `${prefix}${lineText}`,
|
|
559
|
-
caret: `${" ".repeat(prefix.length)}${caretIndent}^`,
|
|
560
|
-
};
|
|
412
|
+
* Render the `LINE N: …` re-print plus the `^` pointer underneath the
|
|
413
|
+
* failing character, mirroring upstream psql's `report_error_query`
|
|
414
|
+
* helper. Returns `null` when we don't have enough context (no SQL text
|
|
415
|
+
* or no position) so the caller can skip the lines entirely.
|
|
416
|
+
*
|
|
417
|
+
* `position` is a 1-based character offset into `sqlText` (as delivered
|
|
418
|
+
* in the server's `P` field). We pick the LINE containing that offset
|
|
419
|
+
* and emit:
|
|
420
|
+
*
|
|
421
|
+
* LINE N: <that line>
|
|
422
|
+
* ^
|
|
423
|
+
*
|
|
424
|
+
* The caret column is aligned to the offset within the picked line so
|
|
425
|
+
* it points at the failing token. Trailing newlines on the picked line
|
|
426
|
+
* are stripped so the `$` end-anchor in upstream's regex still matches.
|
|
427
|
+
*
|
|
428
|
+
* Leading whitespace + comments are skipped before computing the LINE
|
|
429
|
+
* number so the count starts at the first content line — vanilla
|
|
430
|
+
* advances past these before `PQexec`, so the server's `position` is
|
|
431
|
+
* 1-based relative to a trimmed buffer; without the same skip here we'd
|
|
432
|
+
* count newlines that vanilla never sent.
|
|
433
|
+
*
|
|
434
|
+
* Trailing-whitespace fix-up: callers in `cmd_io.ts` strip a `\g`-style
|
|
435
|
+
* buffer's trailing whitespace before handing the SQL to `db.execSimple`
|
|
436
|
+
* / `db.query`, so the server's `position` is relative to the trimmed
|
|
437
|
+
* SQL while `sqlText` (used for the LINE echo) still carries the
|
|
438
|
+
* trailing space(s). When `position` lands on trailing whitespace of
|
|
439
|
+
* the LINE we picked — i.e., past `lineText.trimEnd().length` — that's
|
|
440
|
+
* the "syntax error at end of input" case: vanilla sends the trailing
|
|
441
|
+
* whitespace verbatim and the server reports a position one past the
|
|
442
|
+
* full LINE length. Snap the caret to the end of `lineText` so our
|
|
443
|
+
* output matches vanilla's `^` column. PostgreSQL's scanner never
|
|
444
|
+
* emits positions pointing AT whitespace tokens (they're not lexed as
|
|
445
|
+
* tokens), so the only realistic source of an "in trailing
|
|
446
|
+
* whitespace" position is this trim-on-send delta.
|
|
447
|
+
*
|
|
448
|
+
* Exported so the per-statement error renderer in `core/common.ts` can
|
|
449
|
+
* share the helper with `\errverbose`.
|
|
450
|
+
*/
|
|
451
|
+
const renderLineAndCaret = (sqlText, position) => {
|
|
452
|
+
if (!sqlText || !position) return null;
|
|
453
|
+
const pos = parseInt(position, 10);
|
|
454
|
+
if (!Number.isFinite(pos) || pos <= 0) return null;
|
|
455
|
+
const skip = skipLeadingPrelude(sqlText);
|
|
456
|
+
const trimmed = skip === 0 ? sqlText : sqlText.slice(skip);
|
|
457
|
+
const rebasedPos = pos - skip;
|
|
458
|
+
if (rebasedPos <= 0) return null;
|
|
459
|
+
const idx = Math.min(rebasedPos - 1, trimmed.length);
|
|
460
|
+
let lineStart = trimmed.lastIndexOf("\n", idx - 1);
|
|
461
|
+
lineStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
462
|
+
let lineEnd = trimmed.indexOf("\n", lineStart);
|
|
463
|
+
if (lineEnd === -1) lineEnd = trimmed.length;
|
|
464
|
+
const lineText = trimmed.slice(lineStart, lineEnd);
|
|
465
|
+
const lineNumber = (trimmed.slice(0, lineStart).match(/\n/gu)?.length ?? 0) + 1;
|
|
466
|
+
let col = idx - lineStart;
|
|
467
|
+
const lineTrimEndLen = lineText.replace(/[ \t\f\v]+$/u, "").length;
|
|
468
|
+
if (col >= lineTrimEndLen && col < lineText.length) col = lineText.length;
|
|
469
|
+
const caretIndent = " ".repeat(Math.max(0, col));
|
|
470
|
+
const prefix = `LINE ${String(lineNumber)}: `;
|
|
471
|
+
return {
|
|
472
|
+
line: `${prefix}${lineText}`,
|
|
473
|
+
caret: `${" ".repeat(prefix.length)}${caretIndent}^`
|
|
474
|
+
};
|
|
561
475
|
};
|
|
562
476
|
/**
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
out.push(`${severity}: ${message} at character ${String(pos)}`);
|
|
619
|
-
}
|
|
620
|
-
else {
|
|
621
|
-
out.push(`${severity}: ${message}`);
|
|
622
|
-
}
|
|
623
|
-
return out;
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
out.push(`${severity}: ${message}`);
|
|
627
|
-
}
|
|
628
|
-
const lineCaret = renderLineAndCaret(e.sqlText, e.position);
|
|
629
|
-
if (lineCaret) {
|
|
630
|
-
out.push(lineCaret.line);
|
|
631
|
-
out.push(lineCaret.caret);
|
|
632
|
-
}
|
|
633
|
-
if (e.detail)
|
|
634
|
-
out.push(`DETAIL: ${e.detail}`);
|
|
635
|
-
if (e.hint)
|
|
636
|
-
out.push(`HINT: ${e.hint}`);
|
|
637
|
-
// CONTEXT under default verbosity follows SHOW_CONTEXT: 'never' / 'errors'
|
|
638
|
-
// (the default — show on errors) / 'always'. We treat every call into the
|
|
639
|
-
// formatter as an error report, so 'errors' and 'always' both include
|
|
640
|
-
// CONTEXT, while 'never' suppresses it. Verbose verbosity unconditionally
|
|
641
|
-
// includes CONTEXT.
|
|
642
|
-
const includeContext = verbosity === "verbose" || showContext !== "never";
|
|
643
|
-
if (includeContext && e.where) {
|
|
644
|
-
out.push(`CONTEXT: ${e.where}`);
|
|
645
|
-
}
|
|
646
|
-
if (verbosity === "verbose" && (e.routine || e.file || e.line)) {
|
|
647
|
-
const location = (e.routine ?? "") + (e.file ? `, ${e.file}:${e.line ?? ""}` : "");
|
|
648
|
-
out.push(`LOCATION: ${location}`);
|
|
649
|
-
}
|
|
650
|
-
return out;
|
|
477
|
+
* Render an ErrorResponse-shaped payload as the layered, verbosity-aware
|
|
478
|
+
* report that upstream psql emits to stderr after a failed statement
|
|
479
|
+
* (`PSQLExec` / `ProcessResult` in `src/bin/psql/common.c`).
|
|
480
|
+
*
|
|
481
|
+
* Returned array contains one element per logical line, without trailing
|
|
482
|
+
* newlines — callers join with `\n` and write to their stream.
|
|
483
|
+
*
|
|
484
|
+
* Verbosity / SHOW_CONTEXT semantics, mirrored from upstream:
|
|
485
|
+
*
|
|
486
|
+
* - `terse`: only the severity line (`<sev>: <msg>`) is emitted.
|
|
487
|
+
*
|
|
488
|
+
* - `default`: severity + message, plus `LINE N` / caret, DETAIL, HINT,
|
|
489
|
+
* STATEMENT (we omit STATEMENT — we never echo the query verbatim).
|
|
490
|
+
* CONTEXT and LOCATION are suppressed unless `SHOW_CONTEXT='always'`.
|
|
491
|
+
*
|
|
492
|
+
* - `verbose`: adds the SQLSTATE prefix on the severity line, and
|
|
493
|
+
* CONTEXT plus LOCATION are unconditionally included when present.
|
|
494
|
+
*
|
|
495
|
+
* - `sqlstate`: prepend the SQLSTATE on the severity line (same as
|
|
496
|
+
* `verbose`'s first line), but suppress LINE/DETAIL/HINT/CONTEXT/
|
|
497
|
+
* LOCATION. Matches the upstream "just give me the code" flavour.
|
|
498
|
+
*
|
|
499
|
+
* Empty server fields are skipped silently. The `LINE` / `^` pair only
|
|
500
|
+
* appears when we have both originating SQL text and a 1-based position
|
|
501
|
+
* pointing inside it.
|
|
502
|
+
*/
|
|
503
|
+
const formatErrorReport = (e, verbosity = "default", showContext = "errors") => {
|
|
504
|
+
const severity = e.severity ?? "ERROR";
|
|
505
|
+
const sqlstate = e.code ?? e.sqlstate ?? "XX000";
|
|
506
|
+
const message = e.message ?? "";
|
|
507
|
+
const out = [];
|
|
508
|
+
if (verbosity === "sqlstate") {
|
|
509
|
+
out.push(`${severity}: ${sqlstate}`);
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
if (verbosity === "verbose") out.push(`${severity}: ${sqlstate}: ${message}`);
|
|
513
|
+
else if (verbosity === "terse") {
|
|
514
|
+
const pos = e.position ? Number.parseInt(e.position, 10) : NaN;
|
|
515
|
+
if (Number.isFinite(pos) && pos > 0) out.push(`${severity}: ${message} at character ${String(pos)}`);
|
|
516
|
+
else out.push(`${severity}: ${message}`);
|
|
517
|
+
return out;
|
|
518
|
+
} else out.push(`${severity}: ${message}`);
|
|
519
|
+
const lineCaret = renderLineAndCaret(e.sqlText, e.position);
|
|
520
|
+
if (lineCaret) {
|
|
521
|
+
out.push(lineCaret.line);
|
|
522
|
+
out.push(lineCaret.caret);
|
|
523
|
+
}
|
|
524
|
+
if (e.detail) out.push(`DETAIL: ${e.detail}`);
|
|
525
|
+
if (e.hint) out.push(`HINT: ${e.hint}`);
|
|
526
|
+
if ((verbosity === "verbose" || showContext !== "never") && e.where) out.push(`CONTEXT: ${e.where}`);
|
|
527
|
+
if (verbosity === "verbose" && (e.routine || e.file || e.line)) {
|
|
528
|
+
const location = (e.routine ?? "") + (e.file ? `, ${e.file}:${e.line ?? ""}` : "");
|
|
529
|
+
out.push(`LOCATION: ${location}`);
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
651
532
|
};
|
|
652
533
|
/**
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// `psql:[<file>:<n>]:` tag upstream's `pg_log_pre_callback` adds — only
|
|
686
|
-
// on the leading severity line; subsequent layers (LINE / caret / DETAIL
|
|
687
|
-
// / HINT / LOCATION) stay unprefixed to match libpq's `PQresultErrorMessage`.
|
|
688
|
-
const lines = formatErrorReport(e, "verbose", "always");
|
|
689
|
-
const prefix = psqlErrorPrefix(ctx.settings);
|
|
690
|
-
const prefixed = [prefix + lines[0], ...lines.slice(1)];
|
|
691
|
-
writeErr(prefixed.join("\n") + "\n");
|
|
692
|
-
return Promise.resolve({ status: "ok" });
|
|
693
|
-
},
|
|
534
|
+
* `\errverbose` — print the last error in verbose form. We rely on the
|
|
535
|
+
* mainloop to have stored `settings.lastErrorResult`; this command only
|
|
536
|
+
* formats and prints. Without a saved error, upstream emits "There is no
|
|
537
|
+
* previous error."
|
|
538
|
+
*
|
|
539
|
+
* Verbose output (PG 18 form):
|
|
540
|
+
*
|
|
541
|
+
* ERROR: <sqlstate>: <message>
|
|
542
|
+
* LINE N: <originating line of SQL>
|
|
543
|
+
* ^
|
|
544
|
+
* DETAIL: <detail>
|
|
545
|
+
* HINT: <hint>
|
|
546
|
+
* CONTEXT: <where>
|
|
547
|
+
* LOCATION: <routine>, <file>:<line>
|
|
548
|
+
*
|
|
549
|
+
* Empty fields are omitted. The `LINE` / `^` pair is only emitted when
|
|
550
|
+
* we have both the originating SQL text and a server-provided position.
|
|
551
|
+
*/
|
|
552
|
+
const cmdErrverbose = {
|
|
553
|
+
name: "errverbose",
|
|
554
|
+
helpKey: "errverbose",
|
|
555
|
+
run: (ctx) => {
|
|
556
|
+
const e = ctx.settings.lastErrorResult;
|
|
557
|
+
if (!e || !e.message && !e.sqlstate && !e.code) {
|
|
558
|
+
writeOut("There is no previous error.\n");
|
|
559
|
+
return Promise.resolve({ status: "ok" });
|
|
560
|
+
}
|
|
561
|
+
const lines = formatErrorReport(e, "verbose", "always");
|
|
562
|
+
const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
|
|
563
|
+
writeErr(prefixed.join("\n") + "\n");
|
|
564
|
+
return Promise.resolve({ status: "ok" });
|
|
565
|
+
}
|
|
694
566
|
};
|
|
695
567
|
/**
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return Promise.resolve({ status: "ok" });
|
|
720
|
-
},
|
|
568
|
+
* `\timing [on|off]` — set `settings.timing`. With no arg the value is
|
|
569
|
+
* flipped. Prints the new state to stdout. `toggle` is NOT a valid value —
|
|
570
|
+
* upstream errors "Boolean expected" (review: minor divergences).
|
|
571
|
+
*/
|
|
572
|
+
const cmdTiming = {
|
|
573
|
+
name: "timing",
|
|
574
|
+
helpKey: "timing",
|
|
575
|
+
run: (ctx) => {
|
|
576
|
+
const arg = ctx.nextArg("normal");
|
|
577
|
+
let next;
|
|
578
|
+
if (arg === null) next = !ctx.settings.timing;
|
|
579
|
+
else {
|
|
580
|
+
const parsed = parseBool(arg);
|
|
581
|
+
if (parsed === null) {
|
|
582
|
+
writeErr(`\\${ctx.cmdName}: unrecognized value "${arg}" for "\\timing": Boolean expected\n`);
|
|
583
|
+
return Promise.resolve({ status: "error" });
|
|
584
|
+
}
|
|
585
|
+
next = parsed;
|
|
586
|
+
}
|
|
587
|
+
ctx.settings.timing = next;
|
|
588
|
+
writeOut(`Timing is ${next ? "on" : "off"}.\n`);
|
|
589
|
+
return Promise.resolve({ status: "ok" });
|
|
590
|
+
}
|
|
721
591
|
};
|
|
722
592
|
/**
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
593
|
+
* Static text emitted by `\copyright`. Mirrors upstream psql's
|
|
594
|
+
* `exec_command_copyright()` literal in `src/bin/psql/command.c`.
|
|
595
|
+
*/
|
|
726
596
|
const COPYRIGHT_TEXT = `PostgreSQL Database Management System
|
|
727
597
|
(formerly known as Postgres, then as Postgres95)
|
|
728
598
|
|
|
@@ -748,11 +618,11 @@ ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
|
|
|
748
618
|
PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
|
749
619
|
`;
|
|
750
620
|
/**
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
621
|
+
* neonctl-specific notice appended after the upstream PostgreSQL block.
|
|
622
|
+
* This psql is a pure-TypeScript reimplementation embedded in neonctl, not
|
|
623
|
+
* upstream psql — so we attribute it accordingly. No license claim is made
|
|
624
|
+
* here on purpose (see the project's LICENSE file for terms).
|
|
625
|
+
*/
|
|
756
626
|
const NEON_NOTICE = `
|
|
757
627
|
This is an embedded psql reimplementation that ships with neon, the
|
|
758
628
|
command-line interface for Neon (https://neon.tech). Neon is part of
|
|
@@ -763,207 +633,186 @@ or endorsed by the PostgreSQL Global Development Group. See the neon
|
|
|
763
633
|
LICENSE file for distribution terms.
|
|
764
634
|
`;
|
|
765
635
|
/**
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
636
|
+
* `\copyright` — print the PostgreSQL copyright / license notice, followed
|
|
637
|
+
* by a neonctl + Neon/Databricks attribution block. Takes no arguments.
|
|
638
|
+
* The upstream block is preserved verbatim so the conformance regex
|
|
639
|
+
* `/Copyright/` (from upstream `001_basic.pl` line 75) is satisfied; the
|
|
640
|
+
* Neon notice is appended after it.
|
|
641
|
+
*/
|
|
642
|
+
const cmdCopyright = {
|
|
643
|
+
name: "copyright",
|
|
644
|
+
helpKey: "copyright",
|
|
645
|
+
run: () => {
|
|
646
|
+
writeOut(COPYRIGHT_TEXT + NEON_NOTICE);
|
|
647
|
+
return Promise.resolve({ status: "ok" });
|
|
648
|
+
}
|
|
779
649
|
};
|
|
780
650
|
/**
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
651
|
+
* Terminal width used to lay out `\h` / `\help` topic lists. Upstream
|
|
652
|
+
* uses `pset.popt.topt.envColumns` falling back to `ioctl(TIOCGWINSZ)`;
|
|
653
|
+
* we read `process.stdout.columns` (Node populates this for TTYs) and
|
|
654
|
+
* default to 80 if absent (non-TTY, piped output, etc.).
|
|
655
|
+
*/
|
|
786
656
|
const screenWidth = () => {
|
|
787
|
-
|
|
788
|
-
|
|
657
|
+
const cols = process.stdout.columns;
|
|
658
|
+
return typeof cols === "number" && cols > 0 ? cols : 80;
|
|
789
659
|
};
|
|
790
660
|
/**
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
return Promise.resolve({ status: "ok" });
|
|
808
|
-
},
|
|
661
|
+
* `\h [TOPIC]` (alias `\help`) — show SQL command help.
|
|
662
|
+
*
|
|
663
|
+
* Delegates to {@link helpSQL} in `core/help.ts`, passing the remainder
|
|
664
|
+
* of the line as the topic. With no topic, prints the "Available help:"
|
|
665
|
+
* overview; with a topic, prints the matching synopsis or a list of
|
|
666
|
+
* matches. Mirrors upstream `exec_command_help` in `command.c`.
|
|
667
|
+
*/
|
|
668
|
+
const cmdHelpSQL = {
|
|
669
|
+
name: "h",
|
|
670
|
+
aliases: ["help"],
|
|
671
|
+
helpKey: "h",
|
|
672
|
+
run: (ctx) => {
|
|
673
|
+
const topic = ctx.restOfLine();
|
|
674
|
+
helpSQL(process.stdout, topic.length === 0 ? null : topic, screenWidth());
|
|
675
|
+
return Promise.resolve({ status: "ok" });
|
|
676
|
+
}
|
|
809
677
|
};
|
|
810
678
|
/**
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
return Promise.resolve({ status: "ok" });
|
|
836
|
-
},
|
|
679
|
+
* `\?` — show help for the backslash commands.
|
|
680
|
+
*
|
|
681
|
+
* Delegates to {@link slashUsage} in `core/help.ts`. We pass the output
|
|
682
|
+
* stream (`process.stdout`) and request the pager only when that stream is
|
|
683
|
+
* an interactive TTY — `slashUsage`/`emitHelp` re-check interactivity, but
|
|
684
|
+
* gating the request here keeps the non-interactive path (scripts, piped
|
|
685
|
+
* output, the regress harness) writing straight to stdout with no pager.
|
|
686
|
+
*
|
|
687
|
+
* Upstream `exec_command_help` reads `[commands|options|variables]`; we
|
|
688
|
+
* mirror the default (backslash commands) form, which is the only variant
|
|
689
|
+
* `\?` reaches without an argument. The remainder of the line is consumed
|
|
690
|
+
* so a stray topic doesn't leak into the next command.
|
|
691
|
+
*/
|
|
692
|
+
const cmdSlashHelp = {
|
|
693
|
+
name: "?",
|
|
694
|
+
argMode: "whole-line",
|
|
695
|
+
helpKey: "?",
|
|
696
|
+
run: (ctx) => {
|
|
697
|
+
ctx.restOfLine();
|
|
698
|
+
const out = process.stdout;
|
|
699
|
+
const pager = Boolean(out.isTTY);
|
|
700
|
+
slashUsage(out, pager);
|
|
701
|
+
return Promise.resolve({ status: "ok" });
|
|
702
|
+
}
|
|
837
703
|
};
|
|
838
704
|
/**
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return process.platform === "win32" ? "notepad.exe" : "vi";
|
|
705
|
+
* Resolve the editor command psql would launch for `\e` / `\ef` / `\ev`,
|
|
706
|
+
* mirroring upstream `editFile` / `get_alternate_expansion`:
|
|
707
|
+
*
|
|
708
|
+
* $PSQL_EDITOR || $EDITOR || $VISUAL || platform default
|
|
709
|
+
*
|
|
710
|
+
* The platform default is `notepad.exe` on Windows and `vi` elsewhere,
|
|
711
|
+
* matching upstream's `DEFAULT_EDITOR`.
|
|
712
|
+
*/
|
|
713
|
+
const resolveEditor = (env = process.env) => {
|
|
714
|
+
const explicit = env.PSQL_EDITOR ?? env.EDITOR ?? env.VISUAL;
|
|
715
|
+
if (explicit !== void 0 && explicit.length > 0) return explicit;
|
|
716
|
+
return process.platform === "win32" ? "notepad.exe" : "vi";
|
|
852
717
|
};
|
|
853
718
|
/**
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
finally {
|
|
916
|
-
if (dir) {
|
|
917
|
-
try {
|
|
918
|
-
rmSync(dir, { recursive: true, force: true });
|
|
919
|
-
}
|
|
920
|
-
catch {
|
|
921
|
-
// Temp-dir cleanup is best-effort; a leftover dir is harmless.
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
},
|
|
719
|
+
* `\e` / `\edit [FILE] [LINE]` — edit the current query buffer (or a file)
|
|
720
|
+
* in the user's editor, then load the edited text back into the query
|
|
721
|
+
* buffer.
|
|
722
|
+
*
|
|
723
|
+
* This port implements the common no-FILE form: dump the current query
|
|
724
|
+
* buffer to a temp file, spawn the editor on it inheriting stdio (upstream
|
|
725
|
+
* `do_edit` → `editFile`), and on a clean exit read the file back and
|
|
726
|
+
* return it as the new query buffer via `status: 'reset-buf'`. Upstream
|
|
727
|
+
* strips a single trailing newline the editor may add; we do the same so
|
|
728
|
+
* round-tripping an unchanged buffer is a no-op.
|
|
729
|
+
*
|
|
730
|
+
* Editor selection follows {@link resolveEditor}. The spawn uses
|
|
731
|
+
* `spawnSync(..., { stdio: 'inherit' })` so the editor owns the terminal.
|
|
732
|
+
* If the editor exits non-zero (or fails to spawn) we leave the buffer
|
|
733
|
+
* untouched and report an error, matching upstream's behaviour of not
|
|
734
|
+
* importing a failed edit.
|
|
735
|
+
*
|
|
736
|
+
* FILE / LINE arguments are accepted but the buffer is still seeded from
|
|
737
|
+
* the current query buffer; a future WP can layer file-backed editing
|
|
738
|
+
* (`\e file`) and `\ef`/`\ev` on top.
|
|
739
|
+
*/
|
|
740
|
+
const cmdEdit = {
|
|
741
|
+
name: "e",
|
|
742
|
+
aliases: ["edit"],
|
|
743
|
+
argMode: "whole-line",
|
|
744
|
+
helpKey: "e",
|
|
745
|
+
run: (ctx) => {
|
|
746
|
+
ctx.restOfLine();
|
|
747
|
+
const editor = resolveEditor();
|
|
748
|
+
const seed = ctx.queryBuf.length > 0 && !ctx.queryBuf.endsWith("\n") ? ctx.queryBuf + "\n" : ctx.queryBuf;
|
|
749
|
+
let dir = null;
|
|
750
|
+
try {
|
|
751
|
+
dir = mkdtempSync(join(tmpdir(), "psql.edit."));
|
|
752
|
+
const file = join(dir, "edit.sql");
|
|
753
|
+
writeFileSync(file, seed, "utf8");
|
|
754
|
+
const result = spawnSync(editor, [file], { stdio: "inherit" });
|
|
755
|
+
if (result.error || result.status !== null && result.status !== 0) {
|
|
756
|
+
const why = result.error ? result.error.message : `editor exited with status ${String(result.status)}`;
|
|
757
|
+
writeErr(`\\${ctx.cmdName}: ${why}\n`);
|
|
758
|
+
return Promise.resolve({ status: "error" });
|
|
759
|
+
}
|
|
760
|
+
let edited = readFileSync(file, "utf8");
|
|
761
|
+
if (edited.endsWith("\n")) edited = edited.slice(0, -1);
|
|
762
|
+
return Promise.resolve({
|
|
763
|
+
status: "reset-buf",
|
|
764
|
+
newBuf: edited
|
|
765
|
+
});
|
|
766
|
+
} catch (err) {
|
|
767
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
768
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
769
|
+
return Promise.resolve({ status: "error" });
|
|
770
|
+
} finally {
|
|
771
|
+
if (dir) try {
|
|
772
|
+
rmSync(dir, {
|
|
773
|
+
recursive: true,
|
|
774
|
+
force: true
|
|
775
|
+
});
|
|
776
|
+
} catch {}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
926
779
|
};
|
|
927
780
|
/**
|
|
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
|
-
if (!ctx.settings.quiet) {
|
|
965
|
-
writeOut(`Wrote history to file "${fname}".\n`);
|
|
966
|
-
}
|
|
967
|
-
return Promise.resolve({ status: "ok" });
|
|
968
|
-
},
|
|
781
|
+
* `\s [FILENAME]` — print the command-line history, or save it to FILENAME.
|
|
782
|
+
*
|
|
783
|
+
* Mirrors upstream `exec_command_s` / `printHistory`:
|
|
784
|
+
*
|
|
785
|
+
* - No argument: write the in-memory history (one entry per line) to
|
|
786
|
+
* stdout. Multi-line entries are printed verbatim (with their embedded
|
|
787
|
+
* newlines), matching readline's `\s` dump.
|
|
788
|
+
* - FILENAME given: write the same dump to that file. On success, and
|
|
789
|
+
* unless `\set QUIET` is in effect, print `Wrote history to file
|
|
790
|
+
* "<file>".` to stdout. On failure, emit the OS error to stderr and
|
|
791
|
+
* return an error.
|
|
792
|
+
*
|
|
793
|
+
* The history source is {@link getHistory}, the session's in-memory list
|
|
794
|
+
* populated as each line is submitted (see `io/history.ts`).
|
|
795
|
+
*/
|
|
796
|
+
const cmdS = {
|
|
797
|
+
name: "s",
|
|
798
|
+
helpKey: "s",
|
|
799
|
+
run: (ctx) => {
|
|
800
|
+
const fname = ctx.nextArg("normal");
|
|
801
|
+
const body = getHistory().map((e) => e + "\n").join("");
|
|
802
|
+
if (fname === null || fname.length === 0) {
|
|
803
|
+
writeOut(body);
|
|
804
|
+
return Promise.resolve({ status: "ok" });
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
writeFileSync(fname, body, "utf8");
|
|
808
|
+
} catch (err) {
|
|
809
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
810
|
+
writeErr(`\\${ctx.cmdName}: ${msg}\n`);
|
|
811
|
+
return Promise.resolve({ status: "error" });
|
|
812
|
+
}
|
|
813
|
+
if (!ctx.settings.quiet) writeOut(`Wrote history to file "${fname}".\n`);
|
|
814
|
+
return Promise.resolve({ status: "ok" });
|
|
815
|
+
}
|
|
969
816
|
};
|
|
817
|
+
//#endregion
|
|
818
|
+
export { cmdCd, cmdCopyright, cmdEcho, cmdEdit, cmdErrverbose, cmdGetenv, cmdHelpSQL, cmdPrompt, cmdQecho, cmdQuit, cmdReset, cmdS, cmdSet, cmdSetenv, cmdShell, cmdSlashHelp, cmdTiming, cmdUnset, cmdWarn, formatErrorReport, psqlErrorPrefix, renderLineAndCaret, resolveEditor };
|