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,1360 +1,749 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* Simplifications vs upstream (each tracked against the WP plan):
|
|
9
|
-
*
|
|
10
|
-
* - Line editing is delegated to `node:readline` for now. Upstream uses
|
|
11
|
-
* GNU readline / libedit for history and completion. The proper raw-mode
|
|
12
|
-
* line editor is owned by WP-24; until then we get sane prompt rendering
|
|
13
|
-
* and Ctrl-C handling for free from the standard library.
|
|
14
|
-
* - History accumulation is omitted (`pg_append_history` / `pg_send_history`
|
|
15
|
-
* in upstream). The history sink lives in WP-25.
|
|
16
|
-
* - `\COPY FROM STDIN` raw-data lines are not wired (WP-16). When that lands,
|
|
17
|
-
* the mainloop will switch to PROMPT3 and forward lines to a CopyInStream.
|
|
18
|
-
* - `\if`/`\elif`/`\else`/`\endif` dispatch is wired directly here so the
|
|
19
|
-
* cmd_cond module can stay decoupled from the dispatch registry. Other
|
|
20
|
-
* backslash commands go through the registry interface that WP-13 owns.
|
|
21
|
-
* - The transaction-state poll (`pset.statusF`/`PQtransactionStatus`) is
|
|
22
|
-
* represented by an optional `txStatus` field on Connection; if the
|
|
23
|
-
* Connection doesn't expose one we treat the state as `unknown` for
|
|
24
|
-
* prompt rendering.
|
|
25
|
-
*
|
|
26
|
-
* Tracked TODOs:
|
|
27
|
-
*
|
|
28
|
-
* - PSQLRC startup script (WP-22).
|
|
29
|
-
* - Encoding / multibyte handling beyond UTF-8 (handled implicitly by JS).
|
|
30
|
-
* - `\watch` continuous-execution mode.
|
|
31
|
-
*/
|
|
32
|
-
import * as readline from "node:readline";
|
|
33
|
-
import { attachCondStack, COND_COMMAND_NAMES, cmdElif, cmdElse, cmdEndif, cmdIf, } from "../command/cmd_cond.js";
|
|
34
|
-
import { consumeBindState, getPipelineState } from "../command/cmd_pipeline.js";
|
|
35
|
-
import { consumeNext as consumeQueuedInput } from "../command/inputQueue.js";
|
|
1
|
+
import { COND_COMMAND_NAMES, attachCondStack, cmdElif, cmdElse, cmdEndif, cmdIf } from "../command/cmd_cond.js";
|
|
2
|
+
import { scanSlashArgs } from "../scanner/slash.js";
|
|
3
|
+
import { formatDurationMs } from "../print/units.js";
|
|
4
|
+
import { appendHistory, defaultHistoryPath, loadHistory, resolveHistSize, truncateHistory } from "../io/history.js";
|
|
5
|
+
import { G as writeQueryError, H as renderResultSet, L as captureLastError, U as sendQuery, V as refreshErrorVars, f as consumeBindState, m as getPipelineState, z as pickOut } from "../../_chunks/cmd_pipeline-CUbBO9U_.js";
|
|
6
|
+
import { consumeNext } from "../command/inputQueue.js";
|
|
36
7
|
import { psqlCompleter } from "../complete/index.js";
|
|
37
|
-
import { appendHistory, defaultHistoryPath, loadHistory, resolveHistSize, truncateHistory, } from "../io/history.js";
|
|
38
8
|
import { LineEditor } from "../io/lineEditor/index.js";
|
|
39
|
-
import { formatDurationMs } from "../print/units.js";
|
|
40
|
-
import { scanSlashArgs } from "../scanner/slash.js";
|
|
41
|
-
import { scanSql } from "../scanner/sql.js";
|
|
42
9
|
import { initialScanState } from "../types/scanner.js";
|
|
43
|
-
import {
|
|
10
|
+
import { scanSql } from "../scanner/sql.js";
|
|
44
11
|
import { renderPromptByName } from "./prompt.js";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
12
|
+
import * as readline from "node:readline";
|
|
13
|
+
//#region src/psql/core/mainloop.ts
|
|
14
|
+
/**
|
|
15
|
+
* psql REPL main loop.
|
|
16
|
+
*
|
|
17
|
+
* TypeScript port of `MainLoop()` in `src/bin/psql/mainloop.c`. Drives the
|
|
18
|
+
* read-eval-print cycle: read a line, feed it to the SQL scanner, dispatch
|
|
19
|
+
* SQL or backslash commands as boundaries appear, print results, and loop.
|
|
20
|
+
*
|
|
21
|
+
* Simplifications vs upstream (each tracked against the WP plan):
|
|
22
|
+
*
|
|
23
|
+
* - Line editing is delegated to `node:readline` for now. Upstream uses
|
|
24
|
+
* GNU readline / libedit for history and completion. The proper raw-mode
|
|
25
|
+
* line editor is owned by WP-24; until then we get sane prompt rendering
|
|
26
|
+
* and Ctrl-C handling for free from the standard library.
|
|
27
|
+
* - History accumulation is omitted (`pg_append_history` / `pg_send_history`
|
|
28
|
+
* in upstream). The history sink lives in WP-25.
|
|
29
|
+
* - `\COPY FROM STDIN` raw-data lines are not wired (WP-16). When that lands,
|
|
30
|
+
* the mainloop will switch to PROMPT3 and forward lines to a CopyInStream.
|
|
31
|
+
* - `\if`/`\elif`/`\else`/`\endif` dispatch is wired directly here so the
|
|
32
|
+
* cmd_cond module can stay decoupled from the dispatch registry. Other
|
|
33
|
+
* backslash commands go through the registry interface that WP-13 owns.
|
|
34
|
+
* - The transaction-state poll (`pset.statusF`/`PQtransactionStatus`) is
|
|
35
|
+
* represented by an optional `txStatus` field on Connection; if the
|
|
36
|
+
* Connection doesn't expose one we treat the state as `unknown` for
|
|
37
|
+
* prompt rendering.
|
|
38
|
+
*
|
|
39
|
+
* Tracked TODOs:
|
|
40
|
+
*
|
|
41
|
+
* - PSQLRC startup script (WP-22).
|
|
42
|
+
* - Encoding / multibyte handling beyond UTF-8 (handled implicitly by JS).
|
|
43
|
+
* - `\watch` continuous-execution mode.
|
|
44
|
+
*/
|
|
45
|
+
const EXIT_SUCCESS = 0;
|
|
46
|
+
const EXIT_FAILURE = 1;
|
|
47
|
+
const EXIT_BADCONN = 2;
|
|
48
|
+
const EXIT_USER = 3;
|
|
49
|
+
const COND_COMMANDS = /* @__PURE__ */ new Map([
|
|
50
|
+
["if", cmdIf],
|
|
51
|
+
["elif", cmdElif],
|
|
52
|
+
["else", cmdElse],
|
|
53
|
+
["endif", cmdEndif]
|
|
61
54
|
]);
|
|
62
55
|
const makeStreamLineReader = (input, out) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return Promise.resolve(lineQueue.shift());
|
|
104
|
-
}
|
|
105
|
-
if (ended)
|
|
106
|
-
return Promise.resolve(null);
|
|
107
|
-
return new Promise((resolve) => {
|
|
108
|
-
waiter = resolve;
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
pushHistory: () => undefined,
|
|
112
|
-
// No prompt to garble; just write straight to stdout.
|
|
113
|
-
interject: (text) => {
|
|
114
|
-
out.write(text);
|
|
115
|
-
},
|
|
116
|
-
close: () => {
|
|
117
|
-
rl.close();
|
|
118
|
-
return Promise.resolve();
|
|
119
|
-
},
|
|
120
|
-
};
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
input,
|
|
58
|
+
crlfDelay: Infinity,
|
|
59
|
+
terminal: false
|
|
60
|
+
});
|
|
61
|
+
const lineQueue = [];
|
|
62
|
+
let waiter = null;
|
|
63
|
+
let ended = false;
|
|
64
|
+
rl.on("line", (line) => {
|
|
65
|
+
if (waiter) {
|
|
66
|
+
const w = waiter;
|
|
67
|
+
waiter = null;
|
|
68
|
+
w(line);
|
|
69
|
+
} else lineQueue.push(line);
|
|
70
|
+
});
|
|
71
|
+
rl.on("close", () => {
|
|
72
|
+
ended = true;
|
|
73
|
+
if (waiter) {
|
|
74
|
+
const w = waiter;
|
|
75
|
+
waiter = null;
|
|
76
|
+
w(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
readLine: () => {
|
|
81
|
+
if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift());
|
|
82
|
+
if (ended) return Promise.resolve(null);
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
waiter = resolve;
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
pushHistory: () => void 0,
|
|
88
|
+
interject: (text) => {
|
|
89
|
+
out.write(text);
|
|
90
|
+
},
|
|
91
|
+
close: () => {
|
|
92
|
+
rl.close();
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
121
96
|
};
|
|
122
97
|
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
98
|
+
* Parse a psql VI_MODE-style boolean. Mirrors `ParseVariableBool` for the
|
|
99
|
+
* common spellings: `on` / `true` / `yes` / `1` are truthy; `off` / `false` /
|
|
100
|
+
* `no` / `0` are falsy; everything else returns `null` so the caller can
|
|
101
|
+
* surface the upstream "invalid value" diagnostic.
|
|
102
|
+
*/
|
|
128
103
|
const parseBoolVar = (raw) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (v === "off" || v === "false" || v === "no" || v === "0") {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
104
|
+
const v = raw.toLowerCase().trim();
|
|
105
|
+
if (v === "" || v === "on" || v === "true" || v === "yes" || v === "1") return true;
|
|
106
|
+
if (v === "off" || v === "false" || v === "no" || v === "0") return false;
|
|
107
|
+
return null;
|
|
137
108
|
};
|
|
138
109
|
/** Translate a psql VI_MODE var value into the LineEditor mode. */
|
|
139
110
|
const viModeOption = (raw) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return parseBoolVar(raw) === true ? "vi" : "emacs";
|
|
111
|
+
if (raw === void 0) return "emacs";
|
|
112
|
+
return parseBoolVar(raw) === true ? "vi" : "emacs";
|
|
143
113
|
};
|
|
144
114
|
const makeEditorLineReader = async (ctx, opts = {}) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
void appendHistory(histPath, trimmed, histControl).catch(() => undefined);
|
|
204
|
-
},
|
|
205
|
-
interject: (text) => {
|
|
206
|
-
editor.interject(text);
|
|
207
|
-
},
|
|
208
|
-
close: async () => {
|
|
209
|
-
try {
|
|
210
|
-
editor.close();
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// ignore
|
|
214
|
-
}
|
|
215
|
-
// Truncate to HISTSIZE on exit (libreadline behaviour).
|
|
216
|
-
try {
|
|
217
|
-
await truncateHistory(histPath, histSize);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
// ignore
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
};
|
|
115
|
+
const env = process.env;
|
|
116
|
+
const histPath = defaultHistoryPath(env);
|
|
117
|
+
const histSize = resolveHistSize(env);
|
|
118
|
+
const histControl = ctx.settings.vars.get("HISTCONTROL") ?? ctx.settings.histControl;
|
|
119
|
+
let history = [];
|
|
120
|
+
try {
|
|
121
|
+
history = await loadHistory(histPath);
|
|
122
|
+
} catch {
|
|
123
|
+
history = [];
|
|
124
|
+
}
|
|
125
|
+
const initialMode = viModeOption(ctx.settings.vars.get("VI_MODE"));
|
|
126
|
+
const editor = new LineEditor({
|
|
127
|
+
stdin: ctx.stdin,
|
|
128
|
+
stdout: ctx.stdout,
|
|
129
|
+
history,
|
|
130
|
+
completer: psqlCompleter({
|
|
131
|
+
settings: ctx.settings,
|
|
132
|
+
getQueryBuf: opts.getQueryBuf
|
|
133
|
+
}),
|
|
134
|
+
mode: initialMode
|
|
135
|
+
});
|
|
136
|
+
ctx.settings.vars.addHook("VI_MODE", (newValue) => {
|
|
137
|
+
if (newValue === null) {
|
|
138
|
+
editor.setMode("emacs");
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
const parsed = parseBoolVar(newValue);
|
|
142
|
+
if (parsed === null) {
|
|
143
|
+
ctx.stderr.write(`\\set: VI_MODE: invalid value "${newValue}"; valid values: on, off\n`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
editor.setMode(parsed ? "vi" : "emacs");
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
readLine: async (prompt) => {
|
|
151
|
+
const r = await editor.readLine(prompt);
|
|
152
|
+
if (r === editor.EOF) return null;
|
|
153
|
+
return r;
|
|
154
|
+
},
|
|
155
|
+
pushHistory: (line) => {
|
|
156
|
+
const trimmed = line.replace(/\n+$/, "");
|
|
157
|
+
if (trimmed.length === 0) return;
|
|
158
|
+
editor.pushHistory(trimmed);
|
|
159
|
+
appendHistory(histPath, trimmed, histControl).catch(() => void 0);
|
|
160
|
+
},
|
|
161
|
+
interject: (text) => {
|
|
162
|
+
editor.interject(text);
|
|
163
|
+
},
|
|
164
|
+
close: async () => {
|
|
165
|
+
try {
|
|
166
|
+
editor.close();
|
|
167
|
+
} catch {}
|
|
168
|
+
try {
|
|
169
|
+
await truncateHistory(histPath, histSize);
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
224
173
|
};
|
|
225
174
|
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
175
|
+
* Recognize upstream psql's `exit` / `quit` shortcut: when typed at the start
|
|
176
|
+
* of a fresh statement (queryBuf empty), they exit the REPL. Accepts a
|
|
177
|
+
* trailing `;` and/or whitespace.
|
|
178
|
+
*/
|
|
230
179
|
const isQuitKeyword = (line) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return stripped === "exit" || stripped === "quit";
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (trimmed.length === 0) return false;
|
|
182
|
+
const stripped = trimmed.replace(/;+\s*$/u, "").trimEnd();
|
|
183
|
+
return stripped === "exit" || stripped === "quit";
|
|
236
184
|
};
|
|
237
185
|
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
186
|
+
* Recognize the bare `help` keyword the same way upstream does: at the start
|
|
187
|
+
* of a fresh statement, it prints a one-screen reminder of the most useful
|
|
188
|
+
* meta-commands and continues the REPL.
|
|
189
|
+
*/
|
|
242
190
|
const isHelpKeyword = (line) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const stripped = trimmed.replace(/;+\s*$/u, "").trimEnd();
|
|
247
|
-
return stripped === "help";
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (trimmed.length === 0) return false;
|
|
193
|
+
return trimmed.replace(/;+\s*$/u, "").trimEnd() === "help";
|
|
248
194
|
};
|
|
249
|
-
const HELP_TEXT = "You are using psql-ts, the embedded TypeScript psql in neon.\n"
|
|
250
|
-
"Type: \\copyright for distribution terms\n" +
|
|
251
|
-
" \\h for help with SQL commands\n" +
|
|
252
|
-
" \\? for help with psql commands\n" +
|
|
253
|
-
" \\g or terminate with semicolon to execute query\n" +
|
|
254
|
-
" \\q to quit\n";
|
|
195
|
+
const HELP_TEXT = "You are using psql-ts, the embedded TypeScript psql in neon.\nType: \\copyright for distribution terms\n \\h for help with SQL commands\n \\? for help with psql commands\n \\g or terminate with semicolon to execute query\n \\q to quit\n";
|
|
255
196
|
const makeLineReader = async (ctx, opts = {}) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
if (debug) {
|
|
272
|
-
ctx.stderr.write(`[psql-debug] LineEditor setup failed, falling back to stream reader: ${err.message}\n`);
|
|
273
|
-
}
|
|
274
|
-
return makeStreamLineReader(ctx.stdin, ctx.stdout);
|
|
275
|
-
}
|
|
197
|
+
const debug = process.env.NEONCTL_PSQL_DEBUG === "1";
|
|
198
|
+
if (ctx.settings.notty) {
|
|
199
|
+
if (debug) ctx.stderr.write("[psql-debug] notty=true; using stream reader (no line editor / no Tab completion)\n");
|
|
200
|
+
return makeStreamLineReader(ctx.stdin, ctx.stdout);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const r = await makeEditorLineReader(ctx, opts);
|
|
204
|
+
if (debug) ctx.stderr.write("[psql-debug] LineEditor engaged (raw mode, Tab completion active)\n");
|
|
205
|
+
return r;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (debug) ctx.stderr.write(`[psql-debug] LineEditor setup failed, falling back to stream reader: ${err.message}\n`);
|
|
208
|
+
return makeStreamLineReader(ctx.stdin, ctx.stdout);
|
|
209
|
+
}
|
|
276
210
|
};
|
|
277
211
|
const transactionState = (ctx) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
case "failed":
|
|
291
|
-
return "failed";
|
|
292
|
-
case "unknown":
|
|
293
|
-
return "unknown";
|
|
294
|
-
default:
|
|
295
|
-
return "idle";
|
|
296
|
-
}
|
|
212
|
+
const db = ctx.settings.db;
|
|
213
|
+
if (!db) return "idle";
|
|
214
|
+
switch (db.txStatus) {
|
|
215
|
+
case "I":
|
|
216
|
+
case "idle": return "idle";
|
|
217
|
+
case "T":
|
|
218
|
+
case "in-block": return "in-block";
|
|
219
|
+
case "E":
|
|
220
|
+
case "failed": return "failed";
|
|
221
|
+
case "unknown": return "unknown";
|
|
222
|
+
default: return "idle";
|
|
223
|
+
}
|
|
297
224
|
};
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
// BackslashContext factory — built per-invocation so the dispatched command
|
|
300
|
-
// sees an isolated arg-cursor.
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
225
|
const makeBackslashContext = (ctx, cmdName, rawArgs, queryBuf) => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
cursors.set(mode, idx + 1);
|
|
330
|
-
return args[idx];
|
|
331
|
-
},
|
|
332
|
-
restOfLine() {
|
|
333
|
-
// Whatever the user typed after the command name, verbatim.
|
|
334
|
-
return rawArgs;
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
return bctx;
|
|
226
|
+
const varLookup = (name) => ctx.settings.vars.get(name);
|
|
227
|
+
const buffered = /* @__PURE__ */ new Map();
|
|
228
|
+
const argsFor = (mode) => {
|
|
229
|
+
const cached = buffered.get(mode);
|
|
230
|
+
if (cached) return cached;
|
|
231
|
+
const parsed = scanSlashArgs(rawArgs, mode, varLookup);
|
|
232
|
+
buffered.set(mode, parsed);
|
|
233
|
+
return parsed;
|
|
234
|
+
};
|
|
235
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
236
|
+
return {
|
|
237
|
+
settings: ctx.settings,
|
|
238
|
+
cmdName,
|
|
239
|
+
queryBuf,
|
|
240
|
+
rawArgs,
|
|
241
|
+
nextArg(mode = "normal") {
|
|
242
|
+
const args = argsFor(mode);
|
|
243
|
+
const idx = cursors.get(mode) ?? 0;
|
|
244
|
+
if (idx >= args.length) return null;
|
|
245
|
+
cursors.set(mode, idx + 1);
|
|
246
|
+
return args[idx];
|
|
247
|
+
},
|
|
248
|
+
restOfLine() {
|
|
249
|
+
return rawArgs;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
338
252
|
};
|
|
339
|
-
// ---------------------------------------------------------------------------
|
|
340
|
-
// Error printing. Keeps the format close to libpq's `psql: ERROR: msg`.
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
253
|
const writeError = (ctx, message) => {
|
|
343
|
-
|
|
254
|
+
ctx.stderr.write(`psql: ERROR: ${message}\n`);
|
|
344
255
|
};
|
|
345
|
-
// ---------------------------------------------------------------------------
|
|
346
|
-
// Prompt context builder.
|
|
347
|
-
// ---------------------------------------------------------------------------
|
|
348
256
|
const buildPromptContext = (ctx, promptStatus, lineNumber) => ({
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
257
|
+
settings: ctx.settings,
|
|
258
|
+
cond: ctx.cond,
|
|
259
|
+
promptStatus,
|
|
260
|
+
lineNumber,
|
|
261
|
+
inTransaction: transactionState(ctx),
|
|
262
|
+
pipelineState: "off"
|
|
355
263
|
});
|
|
356
|
-
// ---------------------------------------------------------------------------
|
|
357
|
-
// Conditional-command dispatch. Returns true if the command was a cond
|
|
358
|
-
// command (handled here), false otherwise. cond commands run regardless of
|
|
359
|
-
// whether the surrounding branch is active.
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
264
|
const dispatchCondCommand = async (ctx, cmdName, rawArgs, queryBuf) => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// leave it unset so the mainloop surfaces the message.
|
|
373
|
-
if (result.status === "error" &&
|
|
374
|
-
!result.errorWritten &&
|
|
375
|
-
ctx.settings.lastErrorResult?.message) {
|
|
376
|
-
writeError(ctx, ctx.settings.lastErrorResult.message);
|
|
377
|
-
}
|
|
378
|
-
return { handled: true, result };
|
|
265
|
+
const spec = COND_COMMANDS.get(cmdName);
|
|
266
|
+
if (!spec) return { handled: false };
|
|
267
|
+
const bctx = makeBackslashContext(ctx, cmdName, rawArgs, queryBuf);
|
|
268
|
+
attachCondStack(bctx, ctx.cond);
|
|
269
|
+
const result = await spec.run(bctx);
|
|
270
|
+
if (result.status === "error" && !result.errorWritten && ctx.settings.lastErrorResult?.message) writeError(ctx, ctx.settings.lastErrorResult.message);
|
|
271
|
+
return {
|
|
272
|
+
handled: true,
|
|
273
|
+
result
|
|
274
|
+
};
|
|
379
275
|
};
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
// Backslash dispatch for non-cond commands. Only runs when cond is active.
|
|
382
|
-
// Returns the BackslashResult, or null if no command was found.
|
|
383
|
-
// ---------------------------------------------------------------------------
|
|
384
276
|
const dispatchRegisteredCommand = async (ctx, cmdName, rawArgs, queryBuf) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// `psql: ERROR: <msg>` shape when the command didn't already surface
|
|
399
|
-
// its own diagnostic. Without this guard, `\gdesc` Parse failures
|
|
400
|
-
// would emit a stray `psql: ERROR: <msg>` line between the LINE/`^`
|
|
401
|
-
// block and the `\errverbose` re-render, breaking the strict ordering
|
|
402
|
-
// check in the conformance regex.
|
|
403
|
-
if (result.status === "error" &&
|
|
404
|
-
!result.errorWritten &&
|
|
405
|
-
ctx.settings.lastErrorResult?.message) {
|
|
406
|
-
writeError(ctx, ctx.settings.lastErrorResult.message);
|
|
407
|
-
}
|
|
408
|
-
return result;
|
|
277
|
+
const spec = ctx.registry.lookup(cmdName);
|
|
278
|
+
if (!spec) {
|
|
279
|
+
writeError(ctx, `invalid command \\${cmdName}`);
|
|
280
|
+
return {
|
|
281
|
+
status: "error",
|
|
282
|
+
errorWritten: true
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const bctx = makeBackslashContext(ctx, cmdName, rawArgs, queryBuf);
|
|
286
|
+
attachCondStack(bctx, ctx.cond);
|
|
287
|
+
const result = await spec.run(bctx);
|
|
288
|
+
if (result.status === "error" && !result.errorWritten && ctx.settings.lastErrorResult?.message) writeError(ctx, ctx.settings.lastErrorResult.message);
|
|
289
|
+
return result;
|
|
409
290
|
};
|
|
410
|
-
// ---------------------------------------------------------------------------
|
|
411
|
-
// SendQuery — delegate to the unified pipeline in `common.ts`. Returns the
|
|
412
|
-
// success flag so the read loop can short-circuit under ON_ERROR_STOP.
|
|
413
|
-
//
|
|
414
|
-
// If `\bind` (WP-21) has stashed parameters on the settings, route through
|
|
415
|
-
// the extended-query path on the Connection. Otherwise use the simple-query
|
|
416
|
-
// pipeline.
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
291
|
/**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
292
|
+
* Refresh psql vars that mirror connection-driven server state. Today this
|
|
293
|
+
* is just `ENCODING` (tracks `client_encoding` ParameterStatus). Upstream
|
|
294
|
+
* does the same check at the tail of `SendQuery` in common.c so a
|
|
295
|
+
* `SET client_encoding = ...` lands on the psql var before the next
|
|
296
|
+
* statement looks it up. Safe to call when no connection is bound.
|
|
297
|
+
*/
|
|
425
298
|
const refreshConnectionVars = (ctx) => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (enc !== undefined && ctx.settings.vars.get("ENCODING") !== enc) {
|
|
431
|
-
ctx.settings.vars.set("ENCODING", enc);
|
|
432
|
-
}
|
|
299
|
+
const db = ctx.settings.db;
|
|
300
|
+
if (!db) return;
|
|
301
|
+
const enc = db.parameterStatus("client_encoding");
|
|
302
|
+
if (enc !== void 0 && ctx.settings.vars.get("ENCODING") !== enc) ctx.settings.vars.set("ENCODING", enc);
|
|
433
303
|
};
|
|
434
304
|
const dispatchSendQuery = async (ctx, sql) => {
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
return true;
|
|
493
|
-
}
|
|
494
|
-
catch (err) {
|
|
495
|
-
const message = captureLastError(ctx.settings, err, sql);
|
|
496
|
-
writeQueryError(ctx, message);
|
|
497
|
-
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
498
|
-
return false;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (bind && ctx.settings.db) {
|
|
502
|
-
const started = ctx.settings.timing ? Date.now() : 0;
|
|
503
|
-
let lastRowCount = null;
|
|
504
|
-
let hadError = false;
|
|
505
|
-
try {
|
|
506
|
-
const rs = await ctx.settings.db.query(sql, bind.values);
|
|
507
|
-
// Route the single ResultSet through the unified printer pipeline so
|
|
508
|
-
// `\bind` output looks identical to a simple-query result (and honours
|
|
509
|
-
// `\o FILE`, format selection, expanded mode, etc.).
|
|
510
|
-
const r = await renderResultSet(ctx, rs, pickOut(ctx));
|
|
511
|
-
lastRowCount = r.lastRowCount;
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
catch (err) {
|
|
515
|
-
// Capture the full ErrorResponse payload (severity / code / position /
|
|
516
|
-
// detail / hint / location) so the layered renderer can honour
|
|
517
|
-
// VERBOSITY and SHOW_CONTEXT exactly like the simple-query path.
|
|
518
|
-
const message = captureLastError(ctx.settings, err, sql);
|
|
519
|
-
writeQueryError(ctx, message);
|
|
520
|
-
hadError = true;
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
finally {
|
|
524
|
-
refreshConnectionVars(ctx);
|
|
525
|
-
refreshErrorVars(ctx.settings, hadError
|
|
526
|
-
? { kind: "error" }
|
|
527
|
-
: { kind: "success", rowCount: lastRowCount });
|
|
528
|
-
if (ctx.settings.timing) {
|
|
529
|
-
ctx.stdout.write("\n" + formatDurationMs(Date.now() - started) + "\n");
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const stats = await sendQuery(ctx, sql);
|
|
534
|
-
refreshConnectionVars(ctx);
|
|
535
|
-
return !stats.hadError;
|
|
305
|
+
const bind = consumeBindState(ctx.settings);
|
|
306
|
+
const ps = getPipelineState(ctx.settings);
|
|
307
|
+
if (ps && ctx.settings.db) {
|
|
308
|
+
const trimmed = sql.trimStart();
|
|
309
|
+
if (/^COPY\b/i.test(trimmed) && /\b(FROM\s+STDIN|TO\s+STDOUT)\b/i.test(trimmed)) {
|
|
310
|
+
ctx.stderr.write("psql: error: COPY in a pipeline is not supported, aborting connection\n");
|
|
311
|
+
try {
|
|
312
|
+
const db = ctx.settings.db;
|
|
313
|
+
if (typeof db.abortForCopyInPipeline === "function") db.abortForCopyInPipeline();
|
|
314
|
+
else if (typeof db.close === "function") await db.close();
|
|
315
|
+
} catch {}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await ps.session.parse("", sql, []);
|
|
320
|
+
await ps.session.bind("", bind?.values ?? []);
|
|
321
|
+
const exec = (async () => {
|
|
322
|
+
await ps.session.execute("", 0);
|
|
323
|
+
})();
|
|
324
|
+
ps.pending.push(exec);
|
|
325
|
+
refreshErrorVars(ctx.settings, {
|
|
326
|
+
kind: "success",
|
|
327
|
+
rowCount: null
|
|
328
|
+
});
|
|
329
|
+
return true;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const message = captureLastError(ctx.settings, err, sql);
|
|
332
|
+
writeQueryError(ctx, message);
|
|
333
|
+
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (bind && ctx.settings.db) {
|
|
338
|
+
const started = ctx.settings.timing ? Date.now() : 0;
|
|
339
|
+
let lastRowCount = null;
|
|
340
|
+
let hadError = false;
|
|
341
|
+
try {
|
|
342
|
+
const rs = await ctx.settings.db.query(sql, bind.values);
|
|
343
|
+
lastRowCount = (await renderResultSet(ctx, rs, pickOut(ctx))).lastRowCount;
|
|
344
|
+
return true;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const message = captureLastError(ctx.settings, err, sql);
|
|
347
|
+
writeQueryError(ctx, message);
|
|
348
|
+
hadError = true;
|
|
349
|
+
return false;
|
|
350
|
+
} finally {
|
|
351
|
+
refreshConnectionVars(ctx);
|
|
352
|
+
refreshErrorVars(ctx.settings, hadError ? { kind: "error" } : {
|
|
353
|
+
kind: "success",
|
|
354
|
+
rowCount: lastRowCount
|
|
355
|
+
});
|
|
356
|
+
if (ctx.settings.timing) ctx.stdout.write("\n" + formatDurationMs(Date.now() - started) + "\n");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const stats = await sendQuery(ctx, sql);
|
|
360
|
+
refreshConnectionVars(ctx);
|
|
361
|
+
return !stats.hadError;
|
|
536
362
|
};
|
|
537
363
|
/**
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
364
|
+
* Format an async NotificationResponse (LISTEN/NOTIFY payload) the way
|
|
365
|
+
* upstream's `PrintNotifications` in common.c does. Empty payloads omit the
|
|
366
|
+
* payload clause for backward-compat with pre-9.0 servers.
|
|
367
|
+
*/
|
|
542
368
|
const formatNotification = (channel, payload, pid) => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
`received from server process with PID ${String(pid)}.\n`);
|
|
546
|
-
}
|
|
547
|
-
return (`Asynchronous notification "${channel}" ` +
|
|
548
|
-
`received from server process with PID ${String(pid)}.\n`);
|
|
369
|
+
if (payload.length > 0) return `Asynchronous notification "${channel}" with payload "${payload}" received from server process with PID ${String(pid)}.\n`;
|
|
370
|
+
return `Asynchronous notification "${channel}" received from server process with PID ${String(pid)}.\n`;
|
|
549
371
|
};
|
|
550
372
|
/**
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
373
|
+
* Subscribe to NotificationResponse on the active connection, rendering each
|
|
374
|
+
* to the REPL output (mirrors upstream `PrintNotifications` writing to
|
|
375
|
+
* `pset.queryFout`). Returns the disposer the connection handed us, or
|
|
376
|
+
* `null` when no connection is bound.
|
|
377
|
+
*/
|
|
556
378
|
const installNotificationHandler = (ctx, reader) => {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
// can clear / re-render its prompt block around the injected line.
|
|
563
|
-
// The stream-reader path treats interject as a plain stdout write.
|
|
564
|
-
reader.interject(formatNotification(channel, payload, pid));
|
|
565
|
-
});
|
|
379
|
+
const db = ctx.settings.db;
|
|
380
|
+
if (!db) return null;
|
|
381
|
+
return db.onNotification((channel, payload, pid) => {
|
|
382
|
+
reader.interject(formatNotification(channel, payload, pid));
|
|
383
|
+
});
|
|
566
384
|
};
|
|
567
385
|
/**
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
386
|
+
* Render a NoticeResponse field the same way libpq's `pqBuildErrorMessage3`
|
|
387
|
+
* does for the default `psql_notice_processor` (which is a thin
|
|
388
|
+
* `fputs(msg, stderr)`). Mirrors VERBOSITY / SHOW_CONTEXT semantics:
|
|
389
|
+
*
|
|
390
|
+
* - `terse` / `sqlstate`: just `<severity>: <message>` (`sqlstate` also
|
|
391
|
+
* prepends the SQLSTATE on the severity line).
|
|
392
|
+
* - `default`: severity line + LINE/^ pointer + DETAIL/HINT, and CONTEXT
|
|
393
|
+
* when SHOW_CONTEXT is `always` (NOTICE is not an error, so the default
|
|
394
|
+
* `errors` setting suppresses its CONTEXT — upstream's libpq path
|
|
395
|
+
* gates context on `severity_nonlocalized == "ERROR"|"FATAL"|"PANIC"`).
|
|
396
|
+
* - `verbose`: full SQLSTATE / DETAIL / HINT / CONTEXT / LOCATION layers.
|
|
397
|
+
*
|
|
398
|
+
* The trailing newline mirrors libpq, so callers can `stderr.write()` the
|
|
399
|
+
* returned string directly.
|
|
400
|
+
*/
|
|
583
401
|
const formatNotice = (notice, verbosity, showContext) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
lines.push(`HINT: ${notice.hint}`);
|
|
601
|
-
// CONTEXT gating mirrors libpq's `pqBuildErrorMessage3`:
|
|
602
|
-
// - `verbose` always includes CONTEXT
|
|
603
|
-
// - `default` shows CONTEXT only when SHOW_CONTEXT is `always` for
|
|
604
|
-
// non-error severities (NOTICE / WARNING / INFO / LOG / DEBUG), or
|
|
605
|
-
// when SHOW_CONTEXT is `errors`/`always` for ERROR-level entries.
|
|
606
|
-
const isError = severity === "ERROR" || severity === "FATAL" || severity === "PANIC";
|
|
607
|
-
const includeContext = verbosity === "verbose" ||
|
|
608
|
-
showContext === "always" ||
|
|
609
|
-
(showContext === "errors" && isError);
|
|
610
|
-
if (includeContext && notice.where) {
|
|
611
|
-
lines.push(`CONTEXT: ${notice.where}`);
|
|
612
|
-
}
|
|
613
|
-
if (verbosity === "verbose" && (notice.routine || notice.file)) {
|
|
614
|
-
const location = (notice.routine ?? "") +
|
|
615
|
-
(notice.file ? `, ${notice.file}:${notice.line ?? ""}` : "");
|
|
616
|
-
lines.push(`LOCATION: ${location}`);
|
|
617
|
-
}
|
|
618
|
-
return lines.join("\n") + "\n";
|
|
402
|
+
const severity = notice.severity || "NOTICE";
|
|
403
|
+
const message = notice.message || "";
|
|
404
|
+
const lines = [];
|
|
405
|
+
if (verbosity === "verbose" || verbosity === "sqlstate") {
|
|
406
|
+
const sqlstate = notice.code ?? "XX000";
|
|
407
|
+
lines.push(`${severity}: ${sqlstate}: ${message}`);
|
|
408
|
+
} else lines.push(`${severity}: ${message}`);
|
|
409
|
+
if (verbosity === "terse" || verbosity === "sqlstate") return lines.join("\n") + "\n";
|
|
410
|
+
if (notice.detail) lines.push(`DETAIL: ${notice.detail}`);
|
|
411
|
+
if (notice.hint) lines.push(`HINT: ${notice.hint}`);
|
|
412
|
+
if ((verbosity === "verbose" || showContext === "always" || showContext === "errors" && (severity === "ERROR" || severity === "FATAL" || severity === "PANIC")) && notice.where) lines.push(`CONTEXT: ${notice.where}`);
|
|
413
|
+
if (verbosity === "verbose" && (notice.routine || notice.file)) {
|
|
414
|
+
const location = (notice.routine ?? "") + (notice.file ? `, ${notice.file}:${notice.line ?? ""}` : "");
|
|
415
|
+
lines.push(`LOCATION: ${location}`);
|
|
416
|
+
}
|
|
417
|
+
return lines.join("\n") + "\n";
|
|
619
418
|
};
|
|
620
419
|
/**
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
420
|
+
* Subscribe to NoticeResponse on the active connection, rendering each to
|
|
421
|
+
* stderr the way libpq's default `psql_notice_processor` does. Returns the
|
|
422
|
+
* disposer the connection handed us, or `null` when no connection is bound.
|
|
423
|
+
*
|
|
424
|
+
* NOTICEs fire synchronously as the wire layer receives them, so an
|
|
425
|
+
* inline `RAISE NOTICE` inside a `\;`-chained batch lands BEFORE the
|
|
426
|
+
* tuples-producing portion of the batch is rendered — matching upstream
|
|
427
|
+
* psql output.
|
|
428
|
+
*/
|
|
630
429
|
const installNoticeHandler = (ctx, reader) => {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (ctx.settings.sendMode === "extended-pipeline")
|
|
641
|
-
return;
|
|
642
|
-
const text = formatNotice(notice, ctx.settings.verbosity, ctx.settings.showContext);
|
|
643
|
-
// Notices go to stderr (libpq default). The LineEditor's prompt-redraw
|
|
644
|
-
// logic uses `interjectErr` to flush the message without disturbing the
|
|
645
|
-
// active prompt block — fall back to a raw stderr write when the reader
|
|
646
|
-
// doesn't expose that hook (stream / notty path).
|
|
647
|
-
const interjectErr = reader.interjectErr;
|
|
648
|
-
if (interjectErr) {
|
|
649
|
-
interjectErr.call(reader, text);
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
ctx.stderr.write(text);
|
|
653
|
-
}
|
|
654
|
-
});
|
|
430
|
+
const db = ctx.settings.db;
|
|
431
|
+
if (!db) return null;
|
|
432
|
+
return db.onNotice((notice) => {
|
|
433
|
+
if (ctx.settings.sendMode === "extended-pipeline") return;
|
|
434
|
+
const text = formatNotice(notice, ctx.settings.verbosity, ctx.settings.showContext);
|
|
435
|
+
const interjectErr = reader.interjectErr;
|
|
436
|
+
if (interjectErr) interjectErr.call(reader, text);
|
|
437
|
+
else ctx.stderr.write(text);
|
|
438
|
+
});
|
|
655
439
|
};
|
|
656
440
|
const installSigint = (ctx, state) => {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
return () => process.off("SIGINT", handler);
|
|
441
|
+
const handler = () => {
|
|
442
|
+
if (state.inQuery && ctx.settings.db) {
|
|
443
|
+
ctx.settings.db.cancel().catch(() => void 0);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
state.resetBuf();
|
|
447
|
+
};
|
|
448
|
+
process.on("SIGINT", handler);
|
|
449
|
+
return () => process.off("SIGINT", handler);
|
|
667
450
|
};
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
// `save_query_text_state` capturing the scanner's input lexer
|
|
956
|
-
// state). Cheap shallow copy: ScanState fields are primitives or
|
|
957
|
-
// small immutable objects.
|
|
958
|
-
const scanStateBefore = { ...scanState };
|
|
959
|
-
const r = await dispatchCondCommand(ctx, cmdName, result.rest, queryBuf);
|
|
960
|
-
if (r.handled && r.result?.status === "exit") {
|
|
961
|
-
exitRequested = true;
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
// Cond commands implement upstream's `discard_query_text` via
|
|
965
|
-
// the `truncateBufTo` field: when `\elif`/`\else`/`\endif`
|
|
966
|
-
// leaves an INACTIVE branch, the SQL text the skipped branch
|
|
967
|
-
// accumulated is rolled back to the snapshot captured at the
|
|
968
|
-
// matching `\if`/`\elif`/`\else`. We also restore the scanner
|
|
969
|
-
// state captured at that checkpoint — without this a `(` inside
|
|
970
|
-
// the skipped branch would leave `parenDepth > 0` and the next
|
|
971
|
-
// `;` wouldn't trigger a dispatch boundary. Stmt line number
|
|
972
|
-
// stays as-is: the surrounding statement is still in flight.
|
|
973
|
-
if (r.handled && r.result?.truncateBufTo !== undefined) {
|
|
974
|
-
const target = r.result.truncateBufTo;
|
|
975
|
-
if (target < queryBuf.length) {
|
|
976
|
-
queryBuf = queryBuf.slice(0, target);
|
|
977
|
-
}
|
|
978
|
-
const savedScan = condScanStateStack[condScanStateStack.length - 1];
|
|
979
|
-
if (savedScan !== undefined) {
|
|
980
|
-
scanState = { ...savedScan };
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
// Sync the parallel scan-state stack with whatever cond.push /
|
|
984
|
-
// cond.pop / cond.setSavedQueryBufLen the command performed.
|
|
985
|
-
// `\if` → push the scan state captured at dispatch.
|
|
986
|
-
// `\elif`/`\else` → replace the top entry with the scan state
|
|
987
|
-
// that prevails AFTER any truncate-on-leaving-
|
|
988
|
-
// inactive restoration (so the new branch
|
|
989
|
-
// starts from a clean checkpoint).
|
|
990
|
-
// `\endif` → pop the top entry.
|
|
991
|
-
if (cmdName === "if") {
|
|
992
|
-
condScanStateStack.push(scanStateBefore);
|
|
993
|
-
}
|
|
994
|
-
else if (cmdName === "elif" || cmdName === "else") {
|
|
995
|
-
// Errors leave the top untouched (cond.setState not called on
|
|
996
|
-
// the no-matching/double-else paths). Only re-anchor when the
|
|
997
|
-
// command succeeded — `status: 'ok'` covers both the active
|
|
998
|
-
// and truncated paths.
|
|
999
|
-
if (condScanStateStack.length > 0 &&
|
|
1000
|
-
r.result?.status === "ok") {
|
|
1001
|
-
condScanStateStack[condScanStateStack.length - 1] =
|
|
1002
|
-
{
|
|
1003
|
-
...scanState,
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
else if (cmdName === "endif") {
|
|
1008
|
-
// Pop only on success — `\endif` with no matching `\if`
|
|
1009
|
-
// returns an error and doesn't actually pop the cond frame.
|
|
1010
|
-
if (r.result?.status === "ok") {
|
|
1011
|
-
condScanStateStack.pop();
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
// Note: we intentionally do NOT update `lastWasError` for cond
|
|
1015
|
-
// errors. Upstream psql exits 0 from a script whose only failure
|
|
1016
|
-
// was `\endif: no matching \if` (or any other cond diagnostic) —
|
|
1017
|
-
// these are printed and the loop continues, but they don't taint
|
|
1018
|
-
// the terminal `lastWasError → EXIT_USER` escalation. Only
|
|
1019
|
-
// ON_ERROR_STOP can escalate cond failures.
|
|
1020
|
-
if (r.handled &&
|
|
1021
|
-
r.result?.status === "error" &&
|
|
1022
|
-
ctx.settings.onErrorStop) {
|
|
1023
|
-
successResult = EXIT_USER;
|
|
1024
|
-
exitRequested = true;
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
// Upstream `HandleSlashCmds` looks up the command name BEFORE
|
|
1030
|
-
// consulting the conditional stack: an unknown name emits
|
|
1031
|
-
// `invalid command \X` to stderr regardless of branch state.
|
|
1032
|
-
// Without this, e.g. `\if false \n \lo \n \endif` silently passes
|
|
1033
|
-
// `\lo` through, but vanilla surfaces the diagnostic. Looking the
|
|
1034
|
-
// command up here also lets us short-circuit the inactive branch
|
|
1035
|
-
// without losing the unknown-command error.
|
|
1036
|
-
if (!ctx.cond.isActive()) {
|
|
1037
|
-
if (ctx.registry.lookup(cmdName) === undefined) {
|
|
1038
|
-
// Bare wording (no `psql: ERROR:` prefix) — vanilla's
|
|
1039
|
-
// `psql_log_pre_callback` short-circuits when input has no
|
|
1040
|
-
// line-number context (stdin pipe path), so the diagnostic
|
|
1041
|
-
// is emitted via `pg_log_error_internal` as a raw string.
|
|
1042
|
-
// Matches the expected output's `invalid command \lo` shape
|
|
1043
|
-
// at psql.out:4698. Other writeError() call sites keep the
|
|
1044
|
-
// prefix because they're errors WE emit from a known
|
|
1045
|
-
// dispatch path, not from the unknown-command lookup miss.
|
|
1046
|
-
ctx.stderr.write(`invalid command \\${cmdName}\n`);
|
|
1047
|
-
// Errors emitted in an inactive branch must NOT taint
|
|
1048
|
-
// `lastWasError` (vanilla exits 0 from `\if false; \lo; \endif`)
|
|
1049
|
-
// and must NOT trigger ON_ERROR_STOP. The diagnostic stands
|
|
1050
|
-
// alone; the loop continues to the next chunk.
|
|
1051
|
-
}
|
|
1052
|
-
// Skip non-cond commands inside an inactive branch (run or not,
|
|
1053
|
-
// registered or not).
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
const bres = await dispatchRegisteredCommand(ctx, cmdName, result.rest, queryBuf);
|
|
1057
|
-
if (bres?.status === "exit") {
|
|
1058
|
-
exitRequested = true;
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
if (bres?.status === "reset-buf") {
|
|
1062
|
-
queryBuf = bres.newBuf ?? "";
|
|
1063
|
-
scanState = initialScanState();
|
|
1064
|
-
stmtLineNumber = 1;
|
|
1065
|
-
// The SQL scanner intentionally stops the backslash boundary on
|
|
1066
|
-
// (not past) the trailing line terminator so that an inter-line
|
|
1067
|
-
// `\n` separating a slash command from continuing SQL on the
|
|
1068
|
-
// next line survives in `working`. That's the right call when
|
|
1069
|
-
// the slash command leaves `queryBuf` intact — the `\n` keeps
|
|
1070
|
-
// line breaks in the assembled multi-line query.
|
|
1071
|
-
//
|
|
1072
|
-
// For `reset-buf`, however, the buffer is being intentionally
|
|
1073
|
-
// dropped: the slash command (`\g`, `\gset`, `\gdesc`, `\gexec`,
|
|
1074
|
-
// `\crosstabview`, `\watch`, `\bind`, `\parse`, …) has just
|
|
1075
|
-
// consumed and dispatched whatever was buffered. A residual
|
|
1076
|
-
// `\n` at the head of `working` is then leftover line-terminator
|
|
1077
|
-
// bytes from the slash-command line itself — NOT a continuation
|
|
1078
|
-
// separator. If we let it survive, the next scanSql pass returns
|
|
1079
|
-
// an `eof` with `sql: '\n'`, the loop's
|
|
1080
|
-
// `queryBuf += result.sql` line folds it into the NEXT
|
|
1081
|
-
// statement's buffer, and commands that store the buffer
|
|
1082
|
-
// verbatim (notably `\parse`, which uses the buffer text as the
|
|
1083
|
-
// prepared-statement source) emit a stray leading 0x0a byte.
|
|
1084
|
-
//
|
|
1085
|
-
// Strip the line terminator here so the next pass starts cleanly.
|
|
1086
|
-
// This matches upstream `psql_scan_slash_command_end()`'s eat-
|
|
1087
|
-
// through-newline behaviour for the buffer-reset case — without
|
|
1088
|
-
// changing the scanner's semantics for the inline-slash + multi-
|
|
1089
|
-
// line shape that depends on the `\n` surviving.
|
|
1090
|
-
if (working.startsWith("\r\n")) {
|
|
1091
|
-
working = working.slice(2);
|
|
1092
|
-
}
|
|
1093
|
-
else if (working.startsWith("\n") ||
|
|
1094
|
-
working.startsWith("\r")) {
|
|
1095
|
-
working = working.slice(1);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
// For status='ok' (the buffer was NOT consumed by the slash command),
|
|
1099
|
-
// also drop the `\n` left in `working` when the slash command was
|
|
1100
|
-
// the sole content of this source line. Upstream's
|
|
1101
|
-
// `query_buf->len == added_nl_pos` strip (mainloop.c lines 480-484)
|
|
1102
|
-
// covers the same shape: a line whose only token is a slash command
|
|
1103
|
-
// doesn't contribute a `\n` to `query_buf`. Without this, e.g.
|
|
1104
|
-
// \set ECHO errors
|
|
1105
|
-
// SELECT * FROM bad;
|
|
1106
|
-
// would assemble the SELECT's queryBuf as `\n` + `SELECT...` —
|
|
1107
|
-
// shifting the server's `LINE N` count by one and contaminating
|
|
1108
|
-
// the `STATEMENT: ...` echo emitted on error.
|
|
1109
|
-
if (bres?.status !== "reset-buf" && slashOnlyLine) {
|
|
1110
|
-
if (working.startsWith("\r\n")) {
|
|
1111
|
-
working = working.slice(2);
|
|
1112
|
-
}
|
|
1113
|
-
else if (working.startsWith("\n") ||
|
|
1114
|
-
working.startsWith("\r")) {
|
|
1115
|
-
working = working.slice(1);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
// Upstream `mainloop.c`: on PSQL_CMD_ERROR, the query buffer is
|
|
1119
|
-
// reset and the scanner state is dropped. Mirrors `resetPQExpBuffer`
|
|
1120
|
-
// + `psql_scan_reset`. Without this, a buffer-consuming command
|
|
1121
|
-
// that fails (e.g. `SELECT 1 \watch 1 1` rejecting duplicate
|
|
1122
|
-
// positional intervals) would leave `SELECT 1 ` in the buffer for
|
|
1123
|
-
// the next prompt — and in notty mode the tail dispatch would
|
|
1124
|
-
// execute it, masking the failure exit code.
|
|
1125
|
-
//
|
|
1126
|
-
// Upstream `HandleSlashCmds` additionally silently discards the
|
|
1127
|
-
// remainder of the current line via `psql_scan_slash_option(scan_state,
|
|
1128
|
-
// OT_WHOLE_LINE, …)` when a backslash command returns PSQL_CMD_ERROR.
|
|
1129
|
-
// Mirror that here by dropping `working` up to and including the next
|
|
1130
|
-
// newline. Without this, `\bind_named NAME 1 2 \gset pref02_ \echo X`
|
|
1131
|
-
// would still execute `\echo X` after the pipeline-mode `\gset`
|
|
1132
|
-
// rejection — vanilla suppresses it.
|
|
1133
|
-
if (bres?.status === "error") {
|
|
1134
|
-
queryBuf = "";
|
|
1135
|
-
scanState = initialScanState();
|
|
1136
|
-
stmtLineNumber = 1;
|
|
1137
|
-
// Discard any trailing content on the SAME physical line — but NOT
|
|
1138
|
-
// the rest of the script. The scanner consumes a slash command's
|
|
1139
|
-
// args but typically leaves the line terminator (`\n`) at the head
|
|
1140
|
-
// of `working`. If `working` starts with `\n` / `\r\n`, the failed
|
|
1141
|
-
// command was already at end-of-line — just drop the terminator
|
|
1142
|
-
// and let the next line dispatch normally. If `working` has
|
|
1143
|
-
// non-newline chars before the next `\n`, drop up to and including
|
|
1144
|
-
// that `\n` (mirrors upstream `HandleSlashCmds`' `OT_WHOLE_LINE`
|
|
1145
|
-
// discard). Without this branch a stack of `\gdesc\n\gdesc\n…`
|
|
1146
|
-
// lines collapses to a single dispatched `\gdesc` because the
|
|
1147
|
-
// first discard ate the second line.
|
|
1148
|
-
if (working.startsWith("\r\n")) {
|
|
1149
|
-
working = working.slice(2);
|
|
1150
|
-
}
|
|
1151
|
-
else if (working.startsWith("\n") ||
|
|
1152
|
-
working.startsWith("\r")) {
|
|
1153
|
-
working = working.slice(1);
|
|
1154
|
-
}
|
|
1155
|
-
else {
|
|
1156
|
-
const nlIdx = working.indexOf("\n");
|
|
1157
|
-
working = nlIdx === -1 ? "" : working.slice(nlIdx + 1);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
// Backslash commands like \connect can also tear down the connection.
|
|
1161
|
-
if (checkConnectionLost())
|
|
1162
|
-
return;
|
|
1163
|
-
if (bres?.status === "error" && ctx.settings.onErrorStop) {
|
|
1164
|
-
successResult = EXIT_USER;
|
|
1165
|
-
exitRequested = true;
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
// incomplete or eof — keep accumulating. Use the substituted
|
|
1171
|
-
// `result.sql` so `:NAME` tokens that were fully consumed in this
|
|
1172
|
-
// chunk land in the buffer in expanded form. (A `:NAME` that
|
|
1173
|
-
// straddles two chunks falls back to the literal — a corner case
|
|
1174
|
-
// upstream also handles only when the variable name fits inside the
|
|
1175
|
-
// current buffer; the line-reader feeds whole lines so this is
|
|
1176
|
-
// effectively unreachable in interactive use.)
|
|
1177
|
-
queryBuf += result.sql;
|
|
1178
|
-
working = "";
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
// -----------------------------------------------------------------------
|
|
1183
|
-
// Read loop. Each iteration:
|
|
1184
|
-
// 1. Drain any pending input enqueued by `\i FILE` (WP-15) — those lines
|
|
1185
|
-
// take precedence over fresh stdin so the include behaves as a
|
|
1186
|
-
// prepend on the input source.
|
|
1187
|
-
// 2. Otherwise, ask the reader for the next line. For notty input this
|
|
1188
|
-
// is a `readline` stream; for TTY input it's the LineEditor with
|
|
1189
|
-
// emacs keybindings + persistent history (WP-24 + WP-25).
|
|
1190
|
-
// 3. Each submitted line is recorded in history.
|
|
1191
|
-
// -----------------------------------------------------------------------
|
|
1192
|
-
try {
|
|
1193
|
-
while (!exitRequested) {
|
|
1194
|
-
// Prompt status drives `%R`. When the query buffer holds an incomplete
|
|
1195
|
-
// statement but the scanner isn't inside any special context (paren,
|
|
1196
|
-
// comment, quoted-string), it still reports `'ready'`; map that to
|
|
1197
|
-
// `'continue'` so PROMPT2 renders `-` instead of `=`. A whitespace-only
|
|
1198
|
-
// residue (e.g. a trailing `\n` left over after a `;` boundary) counts
|
|
1199
|
-
// as empty so the next prompt is PROMPT1 not PROMPT2.
|
|
1200
|
-
const status = queryBuf.trim().length === 0
|
|
1201
|
-
? "ready"
|
|
1202
|
-
: scanState.promptStatus === "ready"
|
|
1203
|
-
? "continue"
|
|
1204
|
-
: scanState.promptStatus;
|
|
1205
|
-
const prompt = computePrompt(status);
|
|
1206
|
-
// 1. Pending input from \i: process as a single chunk and loop again.
|
|
1207
|
-
const queued = consumeQueuedInput();
|
|
1208
|
-
if (queued !== null) {
|
|
1209
|
-
await processChunk(queued.endsWith("\n") ? queued : queued + "\n");
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
// 2. Read the next line from stdin / line editor.
|
|
1213
|
-
let line;
|
|
1214
|
-
try {
|
|
1215
|
-
line = await reader.readLine(prompt);
|
|
1216
|
-
}
|
|
1217
|
-
catch (err) {
|
|
1218
|
-
// SignalError (Ctrl-C on an interactive line) — drop the partial
|
|
1219
|
-
// buffer and re-prompt, matching upstream psql.
|
|
1220
|
-
if (err.name === "SignalError") {
|
|
1221
|
-
resetBuf();
|
|
1222
|
-
continue;
|
|
1223
|
-
}
|
|
1224
|
-
throw err;
|
|
1225
|
-
}
|
|
1226
|
-
if (line === null)
|
|
1227
|
-
break; // EOF
|
|
1228
|
-
// Upstream `mainloop.c` MainLoop():
|
|
1229
|
-
//
|
|
1230
|
-
// if (line[0] == '\0' && !psql_scan_in_quote(scan_state))
|
|
1231
|
-
// {
|
|
1232
|
-
// free(line);
|
|
1233
|
-
// continue;
|
|
1234
|
-
// }
|
|
1235
|
-
//
|
|
1236
|
-
// I.e., bare-empty lines are skipped entirely (no echo, no scanner
|
|
1237
|
-
// pass) UNLESS the scanner is mid-quote (single-, double-, dollar-,
|
|
1238
|
-
// or block-comment continuation). Inside a quote we keep the empty
|
|
1239
|
-
// line so it lands in the assembled query buffer (e.g. a quoted
|
|
1240
|
-
// identifier `"ab\n\nc"` spans multiple input lines including blanks),
|
|
1241
|
-
// and `--echo-all` surfaces it so the echo stream tracks the source
|
|
1242
|
-
// verbatim.
|
|
1243
|
-
//
|
|
1244
|
-
// `psql_scan_in_quote` returns true for all start_states except
|
|
1245
|
-
// INITIAL and xqs — we approximate with the scanner-state fields
|
|
1246
|
-
// that track each quoted construct. `parenDepth` is intentionally
|
|
1247
|
-
// omitted (upstream doesn't count it as in-quote).
|
|
1248
|
-
const scanInQuote = scanState.inBlockComment > 0 ||
|
|
1249
|
-
scanState.inSingleQuote ||
|
|
1250
|
-
scanState.inDoubleQuote ||
|
|
1251
|
-
scanState.dollarTag !== null;
|
|
1252
|
-
if (line.length === 0 && !scanInQuote) {
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
// 2'. ECHO=all — upstream `--echo-all` / `\set ECHO all` echoes every
|
|
1256
|
-
// input line to stdout *before* it's processed. Blank lines outside a
|
|
1257
|
-
// quote already short-circuited above, so a blank reaching here means
|
|
1258
|
-
// the scanner is mid-quote and the line is part of the assembled
|
|
1259
|
-
// statement. ECHO=queries echoes only completed queries — handled
|
|
1260
|
-
// separately by the exec path.
|
|
1261
|
-
if (ctx.settings.echo === "all") {
|
|
1262
|
-
ctx.stdout.write(line + "\n");
|
|
1263
|
-
}
|
|
1264
|
-
// 2a. `exit`/`quit` keyword handling.
|
|
1265
|
-
//
|
|
1266
|
-
// - Empty buffer → exit the REPL.
|
|
1267
|
-
// - Non-empty buf → print "Use \\q to quit." hint and continue
|
|
1268
|
-
// (buffer is preserved so the user can resume editing).
|
|
1269
|
-
//
|
|
1270
|
-
// The buffer may carry whitespace from a prior line's tail, so we
|
|
1271
|
-
// trim before checking.
|
|
1272
|
-
// Bare `quit`/`exit` (and `help` below) are an INTERACTIVE-only
|
|
1273
|
-
// convenience — upstream gates them on `cur_cmd_interactive`. In a
|
|
1274
|
-
// non-interactive script (`printf 'quit;\n' | psql`) they must fall
|
|
1275
|
-
// through and be sent to the server as SQL (syntax error → exit 3),
|
|
1276
|
-
// not silently exit 0.
|
|
1277
|
-
if (!ctx.settings.notty && isQuitKeyword(line)) {
|
|
1278
|
-
if (queryBuf.trim().length === 0) {
|
|
1279
|
-
reader.pushHistory(line);
|
|
1280
|
-
exitRequested = true;
|
|
1281
|
-
break;
|
|
1282
|
-
}
|
|
1283
|
-
ctx.stdout.write("Use \\q to quit.\n");
|
|
1284
|
-
continue;
|
|
1285
|
-
}
|
|
1286
|
-
// 2b. `help` keyword handling, same shape.
|
|
1287
|
-
//
|
|
1288
|
-
// - Empty buffer → print the help text, continue.
|
|
1289
|
-
// - Non-empty buf → print "Use \\? for help." hint, continue.
|
|
1290
|
-
if (!ctx.settings.notty && isHelpKeyword(line)) {
|
|
1291
|
-
if (queryBuf.trim().length === 0) {
|
|
1292
|
-
reader.pushHistory(line);
|
|
1293
|
-
ctx.stdout.write(HELP_TEXT);
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
ctx.stdout.write("Use \\? for help.\n");
|
|
1297
|
-
}
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
|
-
// 3. Push to history once we have a complete submitted line (only
|
|
1301
|
-
// when there's something non-blank to record).
|
|
1302
|
-
reader.pushHistory(line);
|
|
1303
|
-
await processChunk(line + "\n");
|
|
1304
|
-
}
|
|
1305
|
-
// EOF: if there's a residual non-empty buffer in non-interactive mode,
|
|
1306
|
-
// dispatch it (mirroring upstream's tail-of-MainLoop block). For
|
|
1307
|
-
// interactive mode upstream skips this; we match the behaviour. We also
|
|
1308
|
-
// require the buffer to contain non-whitespace SQL — trailing blanks
|
|
1309
|
-
// between statement boundaries and EOF should not produce an empty
|
|
1310
|
-
// execSimple call.
|
|
1311
|
-
if (!exitRequested &&
|
|
1312
|
-
queryBuf.trim().length > 0 &&
|
|
1313
|
-
ctx.settings.notty &&
|
|
1314
|
-
successResult === EXIT_SUCCESS) {
|
|
1315
|
-
if (ctx.cond.isActive()) {
|
|
1316
|
-
sigintState.inQuery = true;
|
|
1317
|
-
const ok = await dispatchSendQuery(ctx, queryBuf);
|
|
1318
|
-
sigintState.inQuery = false;
|
|
1319
|
-
if (ctx.settings.db?.isClosed()) {
|
|
1320
|
-
ctx.stderr.write("psql: error: connection to server was lost\n");
|
|
1321
|
-
successResult = EXIT_BADCONN;
|
|
1322
|
-
}
|
|
1323
|
-
else if (!ok && ctx.settings.onErrorStop) {
|
|
1324
|
-
successResult = EXIT_USER;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
queryBuf = "";
|
|
1328
|
-
}
|
|
1329
|
-
// Warn about unbalanced \if blocks (psql's tail-of-MainLoop check).
|
|
1330
|
-
if (!exitRequested && ctx.cond.depth() > 0) {
|
|
1331
|
-
writeError(ctx, "reached EOF without finding closing \\endif(s)");
|
|
1332
|
-
if (ctx.settings.onErrorStop && ctx.settings.notty) {
|
|
1333
|
-
successResult = EXIT_USER;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
// NOTE: we deliberately do NOT escalate to EXIT_USER just because the last
|
|
1337
|
-
// statement errored. Real psql exits 0 from piped stdin / `-f` even when a
|
|
1338
|
-
// statement failed, UNLESS ON_ERROR_STOP is set — and that case is already
|
|
1339
|
-
// handled by the per-statement `successResult = EXIT_USER` paths above
|
|
1340
|
-
// (verified on psql 18.4: `printf 'SELECT 1;\nSELECT 1/0;\n' | psql` → 0).
|
|
1341
|
-
}
|
|
1342
|
-
finally {
|
|
1343
|
-
await reader.close();
|
|
1344
|
-
removeSigint();
|
|
1345
|
-
if (removeNotificationHandler)
|
|
1346
|
-
removeNotificationHandler();
|
|
1347
|
-
if (removeNoticeHandler)
|
|
1348
|
-
removeNoticeHandler();
|
|
1349
|
-
}
|
|
1350
|
-
return successResult;
|
|
451
|
+
const runMainLoop = async (ctx) => {
|
|
452
|
+
let queryBuf = "";
|
|
453
|
+
const reader = await makeLineReader(ctx, { getQueryBuf: () => queryBuf });
|
|
454
|
+
let scanState = initialScanState();
|
|
455
|
+
let stmtLineNumber = 1;
|
|
456
|
+
let successResult = 0;
|
|
457
|
+
let exitRequested = false;
|
|
458
|
+
const condScanStateStack = [];
|
|
459
|
+
const resetBuf = () => {
|
|
460
|
+
queryBuf = "";
|
|
461
|
+
scanState = initialScanState();
|
|
462
|
+
stmtLineNumber = 1;
|
|
463
|
+
};
|
|
464
|
+
const checkConnectionLost = () => {
|
|
465
|
+
if (ctx.settings.db?.isClosed()) {
|
|
466
|
+
ctx.stderr.write("psql: error: connection to server was lost\n");
|
|
467
|
+
successResult = 2;
|
|
468
|
+
exitRequested = true;
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
};
|
|
473
|
+
const sigintState = {
|
|
474
|
+
inQuery: false,
|
|
475
|
+
resetBuf
|
|
476
|
+
};
|
|
477
|
+
const removeSigint = installSigint(ctx, sigintState);
|
|
478
|
+
refreshConnectionVars(ctx);
|
|
479
|
+
const removeNotificationHandler = installNotificationHandler(ctx, reader);
|
|
480
|
+
const removeNoticeHandler = installNoticeHandler(ctx, reader);
|
|
481
|
+
const computePrompt = (status) => {
|
|
482
|
+
if (ctx.settings.notty) return "";
|
|
483
|
+
const name = queryBuf.trim().length === 0 || status === "ready" ? "PROMPT1" : status === "copy" ? "PROMPT3" : "PROMPT2";
|
|
484
|
+
const promptCtx = buildPromptContext(ctx, status, stmtLineNumber);
|
|
485
|
+
return renderPromptByName(name, promptCtx);
|
|
486
|
+
};
|
|
487
|
+
const sqlVarLookup = (name) => ctx.settings.vars.get(name);
|
|
488
|
+
const slashCmdMode = (cmdName) => {
|
|
489
|
+
const spec = ctx.registry.lookup(cmdName);
|
|
490
|
+
if (!spec) return void 0;
|
|
491
|
+
if (spec.argMode === "whole-line") return "whole-line";
|
|
492
|
+
if (cmdName === "w" || cmdName === "o") return "filepipe";
|
|
493
|
+
};
|
|
494
|
+
/**
|
|
495
|
+
* Strip block / line comments cheaply before scanning so a COPY-shaped
|
|
496
|
+
* comment doesn't trigger pre-buffering or sink wiring.
|
|
497
|
+
*/
|
|
498
|
+
const stripSqlComments = (sql) => sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--[^\n]*/g, "");
|
|
499
|
+
/**
|
|
500
|
+
* Count the number of `COPY ... FROM STDIN` segments in `sql`. Upstream
|
|
501
|
+
* `handleCopyIn` in copy.c is invoked for each one that hits the wire as
|
|
502
|
+
* a `\;`-chained simple-query batch; the mainloop must pre-buffer the
|
|
503
|
+
* `\.`-terminated data block per occurrence and hand them to the wire
|
|
504
|
+
* layer before dispatch so CopyInResponse is satisfied without a
|
|
505
|
+
* blocking callback into the REPL.
|
|
506
|
+
*
|
|
507
|
+
* The regex tolerates the optional column list (`COPY t (a, b)`) and the
|
|
508
|
+
* format clause (`COPY t FROM STDIN WITH (...)` / `... CSV ...`). False
|
|
509
|
+
* positives inside string literals / comments are possible but extremely
|
|
510
|
+
* rare in scripted workloads — and a false positive only over-consumes
|
|
511
|
+
* lines from the input, which is recoverable. The conservative regex
|
|
512
|
+
* here matches upstream `psql`'s scanner heuristics closely enough for
|
|
513
|
+
* the conformance suite (`psql.sql` lines around 1467-1476).
|
|
514
|
+
*/
|
|
515
|
+
const countCopyFromStdin = (sql) => {
|
|
516
|
+
const stripped = stripSqlComments(sql);
|
|
517
|
+
const re = /\bCOPY\b[\s\S]*?\bFROM\s+STDIN\b/giu;
|
|
518
|
+
let n = 0;
|
|
519
|
+
while (re.exec(stripped) !== null) n += 1;
|
|
520
|
+
return n;
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* `true` when `sql` contains at least one `COPY ... TO STDOUT` segment.
|
|
524
|
+
* The wire layer routes mid-batch CopyData into our `copyOutMidBatchSink`
|
|
525
|
+
* when it's set; we install one for the duration of a batch that mentions
|
|
526
|
+
* `TO STDOUT` so the bytes land on the active output stream.
|
|
527
|
+
*/
|
|
528
|
+
const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlComments(sql));
|
|
529
|
+
/**
|
|
530
|
+
* Read one COPY-FROM-STDIN data block: consume lines from the reader
|
|
531
|
+
* until a bare `\.` arrives (or EOF / null). Returns the concatenated
|
|
532
|
+
* payload as a Buffer with trailing newlines preserved (the wire side
|
|
533
|
+
* sends the bytes verbatim; the server treats them as the COPY input).
|
|
534
|
+
* The `\.` terminator itself is NOT included.
|
|
535
|
+
*/
|
|
536
|
+
const readCopyDataBlock = async () => {
|
|
537
|
+
const lines = [];
|
|
538
|
+
for (;;) {
|
|
539
|
+
const line = await reader.readLine("");
|
|
540
|
+
if (line === null) break;
|
|
541
|
+
if (line.replace(/\s+$/, "") === "\\.") break;
|
|
542
|
+
lines.push(line);
|
|
543
|
+
}
|
|
544
|
+
const text = lines.length === 0 ? "" : lines.join("\n") + "\n";
|
|
545
|
+
return Buffer.from(text, "utf8");
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* Process the assembled queryBuf+line through scanSql, dispatching the
|
|
549
|
+
* boundaries it finds. Returns when we hit `incomplete`/`eof` and need
|
|
550
|
+
* the next input line.
|
|
551
|
+
*/
|
|
552
|
+
const processChunk = async (chunk) => {
|
|
553
|
+
let working = chunk;
|
|
554
|
+
while (working.length > 0) {
|
|
555
|
+
const result = scanSql(working, scanState, sqlVarLookup, slashCmdMode, { singleline: ctx.settings.singleline });
|
|
556
|
+
scanState = result.nextState;
|
|
557
|
+
if (result.kind === "semicolon") {
|
|
558
|
+
const sqlText = queryBuf + result.sql;
|
|
559
|
+
queryBuf = "";
|
|
560
|
+
working = working.slice(result.consumed);
|
|
561
|
+
scanState = initialScanState();
|
|
562
|
+
stmtLineNumber = 1;
|
|
563
|
+
if (!ctx.cond.isActive()) continue;
|
|
564
|
+
const copyCount = ctx.settings.db ? countCopyFromStdin(sqlText) : 0;
|
|
565
|
+
const wantsCopyOut = ctx.settings.db !== void 0 && hasCopyToStdout(sqlText);
|
|
566
|
+
if (copyCount > 0 && ctx.settings.db) {
|
|
567
|
+
const conn = ctx.settings.db;
|
|
568
|
+
if (typeof conn.queueCopyInData === "function") {
|
|
569
|
+
conn.clearCopyInDataQueue?.();
|
|
570
|
+
for (let i = 0; i < copyCount; i += 1) {
|
|
571
|
+
const block = await readCopyDataBlock();
|
|
572
|
+
conn.queueCopyInData(block);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (wantsCopyOut && ctx.settings.db) {
|
|
577
|
+
const conn = ctx.settings.db;
|
|
578
|
+
const out = pickOut(ctx);
|
|
579
|
+
conn.copyOutMidBatchSink = (chunk) => {
|
|
580
|
+
out.write(chunk);
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
sigintState.inQuery = true;
|
|
584
|
+
const ok = await dispatchSendQuery(ctx, sqlText);
|
|
585
|
+
sigintState.inQuery = false;
|
|
586
|
+
if (copyCount > 0 && ctx.settings.db) ctx.settings.db.clearCopyInDataQueue?.();
|
|
587
|
+
if (wantsCopyOut && ctx.settings.db) {
|
|
588
|
+
const conn = ctx.settings.db;
|
|
589
|
+
conn.copyOutMidBatchSink = null;
|
|
590
|
+
}
|
|
591
|
+
if (checkConnectionLost()) return;
|
|
592
|
+
if (!ok && ctx.settings.onErrorStop) {
|
|
593
|
+
successResult = 3;
|
|
594
|
+
exitRequested = true;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (result.kind === "backslash") {
|
|
600
|
+
const slashOnlyLine = result.sql.length === 0 && queryBuf.length === 0;
|
|
601
|
+
queryBuf += result.sql;
|
|
602
|
+
working = working.slice(result.consumed);
|
|
603
|
+
const cmdName = result.cmd;
|
|
604
|
+
if (COND_COMMAND_NAMES.has(cmdName)) {
|
|
605
|
+
const scanStateBefore = { ...scanState };
|
|
606
|
+
const r = await dispatchCondCommand(ctx, cmdName, result.rest, queryBuf);
|
|
607
|
+
if (r.handled && r.result?.status === "exit") {
|
|
608
|
+
exitRequested = true;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (r.handled && r.result?.truncateBufTo !== void 0) {
|
|
612
|
+
const target = r.result.truncateBufTo;
|
|
613
|
+
if (target < queryBuf.length) queryBuf = queryBuf.slice(0, target);
|
|
614
|
+
const savedScan = condScanStateStack[condScanStateStack.length - 1];
|
|
615
|
+
if (savedScan !== void 0) scanState = { ...savedScan };
|
|
616
|
+
}
|
|
617
|
+
if (cmdName === "if") condScanStateStack.push(scanStateBefore);
|
|
618
|
+
else if (cmdName === "elif" || cmdName === "else") {
|
|
619
|
+
if (condScanStateStack.length > 0 && r.result?.status === "ok") condScanStateStack[condScanStateStack.length - 1] = { ...scanState };
|
|
620
|
+
} else if (cmdName === "endif") {
|
|
621
|
+
if (r.result?.status === "ok") condScanStateStack.pop();
|
|
622
|
+
}
|
|
623
|
+
if (r.handled && r.result?.status === "error" && ctx.settings.onErrorStop) {
|
|
624
|
+
successResult = 3;
|
|
625
|
+
exitRequested = true;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (!ctx.cond.isActive()) {
|
|
631
|
+
if (ctx.registry.lookup(cmdName) === void 0) ctx.stderr.write(`invalid command \\${cmdName}\n`);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const bres = await dispatchRegisteredCommand(ctx, cmdName, result.rest, queryBuf);
|
|
635
|
+
if (bres?.status === "exit") {
|
|
636
|
+
exitRequested = true;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (bres?.status === "reset-buf") {
|
|
640
|
+
queryBuf = bres.newBuf ?? "";
|
|
641
|
+
scanState = initialScanState();
|
|
642
|
+
stmtLineNumber = 1;
|
|
643
|
+
if (working.startsWith("\r\n")) working = working.slice(2);
|
|
644
|
+
else if (working.startsWith("\n") || working.startsWith("\r")) working = working.slice(1);
|
|
645
|
+
}
|
|
646
|
+
if (bres?.status !== "reset-buf" && slashOnlyLine) {
|
|
647
|
+
if (working.startsWith("\r\n")) working = working.slice(2);
|
|
648
|
+
else if (working.startsWith("\n") || working.startsWith("\r")) working = working.slice(1);
|
|
649
|
+
}
|
|
650
|
+
if (bres?.status === "error") {
|
|
651
|
+
queryBuf = "";
|
|
652
|
+
scanState = initialScanState();
|
|
653
|
+
stmtLineNumber = 1;
|
|
654
|
+
if (working.startsWith("\r\n")) working = working.slice(2);
|
|
655
|
+
else if (working.startsWith("\n") || working.startsWith("\r")) working = working.slice(1);
|
|
656
|
+
else {
|
|
657
|
+
const nlIdx = working.indexOf("\n");
|
|
658
|
+
working = nlIdx === -1 ? "" : working.slice(nlIdx + 1);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (checkConnectionLost()) return;
|
|
662
|
+
if (bres?.status === "error" && ctx.settings.onErrorStop) {
|
|
663
|
+
successResult = 3;
|
|
664
|
+
exitRequested = true;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
queryBuf += result.sql;
|
|
670
|
+
working = "";
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
try {
|
|
675
|
+
while (!exitRequested) {
|
|
676
|
+
const prompt = computePrompt(queryBuf.trim().length === 0 ? "ready" : scanState.promptStatus === "ready" ? "continue" : scanState.promptStatus);
|
|
677
|
+
const queued = consumeNext();
|
|
678
|
+
if (queued !== null) {
|
|
679
|
+
await processChunk(queued.endsWith("\n") ? queued : queued + "\n");
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
let line;
|
|
683
|
+
try {
|
|
684
|
+
line = await reader.readLine(prompt);
|
|
685
|
+
} catch (err) {
|
|
686
|
+
if (err.name === "SignalError") {
|
|
687
|
+
resetBuf();
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
if (line === null) break;
|
|
693
|
+
const scanInQuote = scanState.inBlockComment > 0 || scanState.inSingleQuote || scanState.inDoubleQuote || scanState.dollarTag !== null;
|
|
694
|
+
if (line.length === 0 && !scanInQuote) continue;
|
|
695
|
+
if (ctx.settings.echo === "all") ctx.stdout.write(line + "\n");
|
|
696
|
+
if (!ctx.settings.notty && isQuitKeyword(line)) {
|
|
697
|
+
if (queryBuf.trim().length === 0) {
|
|
698
|
+
reader.pushHistory(line);
|
|
699
|
+
exitRequested = true;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
ctx.stdout.write("Use \\q to quit.\n");
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (!ctx.settings.notty && isHelpKeyword(line)) {
|
|
706
|
+
if (queryBuf.trim().length === 0) {
|
|
707
|
+
reader.pushHistory(line);
|
|
708
|
+
ctx.stdout.write(HELP_TEXT);
|
|
709
|
+
} else ctx.stdout.write("Use \\? for help.\n");
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
reader.pushHistory(line);
|
|
713
|
+
await processChunk(line + "\n");
|
|
714
|
+
}
|
|
715
|
+
if (!exitRequested && queryBuf.trim().length > 0 && ctx.settings.notty && successResult === 0) {
|
|
716
|
+
if (ctx.cond.isActive()) {
|
|
717
|
+
sigintState.inQuery = true;
|
|
718
|
+
const ok = await dispatchSendQuery(ctx, queryBuf);
|
|
719
|
+
sigintState.inQuery = false;
|
|
720
|
+
if (ctx.settings.db?.isClosed()) {
|
|
721
|
+
ctx.stderr.write("psql: error: connection to server was lost\n");
|
|
722
|
+
successResult = 2;
|
|
723
|
+
} else if (!ok && ctx.settings.onErrorStop) successResult = 3;
|
|
724
|
+
}
|
|
725
|
+
queryBuf = "";
|
|
726
|
+
}
|
|
727
|
+
if (!exitRequested && ctx.cond.depth() > 0) {
|
|
728
|
+
writeError(ctx, "reached EOF without finding closing \\endif(s)");
|
|
729
|
+
if (ctx.settings.onErrorStop && ctx.settings.notty) successResult = 3;
|
|
730
|
+
}
|
|
731
|
+
} finally {
|
|
732
|
+
await reader.close();
|
|
733
|
+
removeSigint();
|
|
734
|
+
if (removeNotificationHandler) removeNotificationHandler();
|
|
735
|
+
if (removeNoticeHandler) removeNoticeHandler();
|
|
736
|
+
}
|
|
737
|
+
return successResult;
|
|
1351
738
|
};
|
|
1352
739
|
/**
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
740
|
+
* Test-only surface. Exposes the small VI_MODE helpers so the matching unit
|
|
741
|
+
* tests can exercise the parse / translate logic without engaging the
|
|
742
|
+
* raw-mode LineEditor. Treated as private — callers should not rely on it.
|
|
743
|
+
*/
|
|
744
|
+
const __testing = {
|
|
745
|
+
parseBoolVar,
|
|
746
|
+
viModeOption
|
|
1360
747
|
};
|
|
748
|
+
//#endregion
|
|
749
|
+
export { EXIT_BADCONN, EXIT_FAILURE, EXIT_SUCCESS, EXIT_USER, __testing, runMainLoop };
|